Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5c5a686
feat(system-service): add a strict validator for URLs opened on the host
MarcZX10 Oct 29, 2026
01d7f88
test(system-service): cover the host URL validator
MarcZX10 Oct 29, 2026
b55a287
feat(system-service): parse ANYPS5_OPEN_URLS into a launch mode
MarcZX10 Oct 29, 2026
af5ec46
test(system-service): cover ANYPS5_OPEN_URLS parsing
MarcZX10 Oct 29, 2026
9144fbc
feat(system-service): inject the URL launcher and confirmation behind…
MarcZX10 Oct 29, 2026
52e0c62
test(system-service): cover the open-URL policy with fake backends
MarcZX10 Oct 29, 2026
9b853a7
feat(system-service): launch URLs through xdg-open or open without a …
MarcZX10 Oct 29, 2026
6197c3d
test(system-service): run the POSIX launcher against a recording opener
MarcZX10 Oct 29, 2026
d42a26f
feat(system-service): launch URLs through ShellExecuteW on Windows
MarcZX10 Oct 29, 2026
5f63c78
fix(system-service): make the POSIX launcher refuse anything but a va…
MarcZX10 Oct 29, 2026
040915d
test(system-service): check the POSIX launcher refuses other schemes …
MarcZX10 Oct 29, 2026
d357729
feat(system-service): confirm a host URL launch with an SDL message box
MarcZX10 Oct 29, 2026
a9c8667
test(system-service): cover the confirmation text and the no-display …
MarcZX10 Oct 29, 2026
5f5d8ec
feat(system-service): open the URL of sceSystemServiceLaunchWebBrowse…
MarcZX10 Oct 29, 2026
86244b1
test(system-service): cover sceSystemServiceLaunchWebBrowser on the host
MarcZX10 Oct 29, 2026
61d93e5
docs(env): document ANYPS5_OPEN_URLS
MarcZX10 Oct 29, 2026
f51a530
docs(usage): describe ANYPS5_OPEN_URLS and the addresses it opens
MarcZX10 Oct 29, 2026
d9cfcde
docs(debt): say the browser request only fails by default
MarcZX10 Oct 29, 2026
ecac5b6
docs(debt): qualify the EOPNOTSUPP result of sceSystemServiceLaunchWe…
MarcZX10 Oct 29, 2026
d3bcb3c
docs(debt): record the unmeasured success value and host limits of th…
MarcZX10 Oct 29, 2026
ab35d08
docs(debt): record which host launch backends are unverified
MarcZX10 Oct 29, 2026
b9cb9d8
docs(debt): record that the web browser dialog's URL field is unsourced
MarcZX10 Oct 29, 2026
32356e7
feat(system-service): refuse a URL launch while another one is active
MarcZX10 Oct 29, 2026
d440ea4
test(system-service): cover the busy gate and never with counting fakes
MarcZX10 Oct 29, 2026
ed141e9
feat(system-service): cap host URL prompts and launches to three a mi…
MarcZX10 Oct 29, 2026
14314e1
test(system-service): cover the attempt cap, its recovery and what co…
MarcZX10 Oct 29, 2026
6c86617
feat(system-service): stop asking for ten minutes after a Cancel
MarcZX10 Oct 29, 2026
c0fb0de
test(system-service): cover the cooldown after a Cancel
MarcZX10 Oct 29, 2026
1b3df26
test(system-service): move the counting fakes into a shared test header
MarcZX10 Oct 29, 2026
4e7b6f3
feat(system-service): route sceSystemServiceLaunchWebBrowser through …
MarcZX10 Oct 29, 2026
f2b93c8
test(system-service): cover the limits through the exported call
MarcZX10 Oct 29, 2026
20b6d52
feat(system-service): bound the POSIX openers running at once to two
MarcZX10 Oct 29, 2026
4e969b9
test(system-service): cover the bound on running POSIX openers
MarcZX10 Oct 29, 2026
fa4c0fe
docs(usage): say that always opens localhost and private-network addr…
MarcZX10 Oct 29, 2026
0dfdccd
docs(usage): describe the browser request limits
MarcZX10 Oct 29, 2026
924a338
docs(debt): record that the browser request limits are host policy
MarcZX10 Oct 29, 2026
737ac71
test(system-service): pin the documented limit values
MarcZX10 Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions core/libs/prx/libSceSystemService/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ if(APPLE)
target_link_libraries(system_service_host_locale INTERFACE "-framework CoreFoundation")
endif()

add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp)
target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage system_service_host_locale)
add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp HostUrlConfirmation.cpp)
if(WIN32)
target_sources(libSceSystemService PRIVATE HostUrlLauncherWindows.cpp)
target_link_libraries(libSceSystemService PRIVATE shell32)
else()
target_sources(libSceSystemService PRIVATE HostUrlLauncherPosix.cpp)
endif()
target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR})
target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage system_service_host_locale SDL2-static)
configure_windows_unwind(libSceSystemService)

if(BUILD_TESTING)
Expand Down Expand Up @@ -97,6 +103,24 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test_executable(guest_system_service_open_url_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceOpenUrl.cpp)
target_include_directories(guest_system_service_open_url_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_system_service_open_url_tests PRIVATE libSceSystemService libc)
configure_windows_unwind(guest_system_service_open_url_tests)
add_test(NAME guest_system_service_open_url COMMAND guest_system_service_open_url_tests)
if(WIN32)
set_tests_properties(guest_system_service_open_url PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(guest_system_service_open_url_limits_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceOpenUrlLimits.cpp)
target_include_directories(guest_system_service_open_url_limits_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_system_service_open_url_limits_tests PRIVATE libSceSystemService libc)
configure_windows_unwind(guest_system_service_open_url_limits_tests)
add_test(NAME guest_system_service_open_url_limits COMMAND guest_system_service_open_url_limits_tests)
if(WIN32)
set_tests_properties(guest_system_service_open_url_limits PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(guest_system_service_language_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLanguage.cpp)
target_include_directories(guest_system_service_language_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_system_service_language_tests PRIVATE libSceSystemService libc)
Expand Down
24 changes: 23 additions & 1 deletion core/libs/prx/libSceSystemService/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <atomic>
#include <filesystem>
#include <mutex>
#include <stdexcept>
Expand All @@ -32,11 +33,26 @@
#include "prx/libkernel/KernelErrors.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include "prx/libSceSystemService/SystemService.hpp"
#include "prx/libSceSystemService/HostUrlBackends.hpp"
#include "prx/libSceUserService/UserService.hpp"
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"

namespace {

HostBrowserLauncher g_defaultUrlLauncher;
HostMessageBoxConfirmation g_defaultUrlConfirmation;
std::atomic<IUrlLauncher*> g_urlLauncher{&g_defaultUrlLauncher};
std::atomic<IUrlConfirmation*> g_urlConfirmation{&g_defaultUrlConfirmation};
SteadyUrlClock g_steadyUrlClock;
OpenUrlGate g_urlGate(g_steadyUrlClock);

int OpenWebBrowserUrl(const char* uri) {
const OpenUrlMode mode = OpenUrlModeFromEnvironment("sceSystemServiceLaunchWebBrowser");
if (mode == OpenUrlMode::Never) return SCE_KERNEL_ERROR_EOPNOTSUPP;
const OpenUrlGateOutcome outcome = g_urlGate.Run(mode, uri, *g_urlLauncher.load(), *g_urlConfirmation.load());
return outcome == OpenUrlGateOutcome::Launched ? SYSTEM_SERVICE_OK : SCE_KERNEL_ERROR_EOPNOTSUPP;
}

struct MeasuredParameter {
int id;
int value;
Expand Down Expand Up @@ -623,7 +639,13 @@ int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri) {
while (length < 6016 && uri[length] != 0) ++length;
if (length > 6015) return static_cast<int>(0x80a10007);
if (std::strncmp(uri, "http:", 5) != 0 && std::strncmp(uri, "https:", 6) != 0 && std::strncmp(uri, "psno:", 5) != 0) return static_cast<int>(0x80a10008);
return SCE_KERNEL_ERROR_EOPNOTSUPP;
return OpenWebBrowserUrl(uri);
}

extern "C" void SystemServiceSetUrlHandlers_nid_no_patch(IUrlLauncher* launcher, IUrlConfirmation* confirmation, IUrlClock* clock) {
g_urlLauncher = launcher != nullptr ? launcher : &g_defaultUrlLauncher;
g_urlConfirmation = confirmation != nullptr ? confirmation : &g_defaultUrlConfirmation;
g_urlGate.Reset(clock != nullptr ? *clock : static_cast<IUrlClock&>(g_steadyUrlClock));
}

int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param) {
Expand Down
117 changes: 117 additions & 0 deletions core/libs/prx/libSceSystemService/HostUrl.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#pragma once

#include <cstddef>
#include <string_view>

constexpr std::size_t HOST_URL_MAX_LENGTH = 2048;
constexpr std::size_t HOST_URL_MAX_PORT = 65535;

enum class HostUrlVerdict {
Ok,
Empty,
TooLong,
BadCharacter,
BadEscape,
BadScheme,
BadAuthority,
Credentials,
BadPort,
};

namespace host_url {

constexpr bool IsAlphaNumeric(char value) {
return (value >= '0' && value <= '9') || (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z');
}

constexpr bool IsHexDigit(char value) {
return (value >= '0' && value <= '9') || (value >= 'a' && value <= 'f') || (value >= 'A' && value <= 'F');
}

constexpr int HexValue(char value) {
if (value >= '0' && value <= '9') return value - '0';
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
return value - 'A' + 10;
}

constexpr bool IsUrlCharacter(char value) {
if (IsAlphaNumeric(value)) return true;
for (const char allowed : std::string_view("-._~:/?#[]@!$&'()*+,;=%"))
if (value == allowed) return true;
return false;
}

constexpr bool IsHostCharacter(char value) {
return IsAlphaNumeric(value) || value == '-' || value == '.' || value == '_';
}

constexpr HostUrlVerdict ValidateEscapes(std::string_view url) {
for (std::size_t index = 0; index < url.size(); ++index) {
if (url[index] != '%') continue;
if (index + 2 >= url.size()) return HostUrlVerdict::BadEscape;
if (!IsHexDigit(url[index + 1]) || !IsHexDigit(url[index + 2])) return HostUrlVerdict::BadEscape;
const int decoded = HexValue(url[index + 1]) * 16 + HexValue(url[index + 2]);
if (decoded < 0x20 || decoded == 0x7f) return HostUrlVerdict::BadEscape;
index += 2;
}
return HostUrlVerdict::Ok;
}

constexpr HostUrlVerdict ValidatePort(std::string_view port) {
if (port.empty() || port.size() > 5) return HostUrlVerdict::BadPort;
std::size_t value = 0;
for (const char digit : port) {
if (digit < '0' || digit > '9') return HostUrlVerdict::BadPort;
value = value * 10 + static_cast<std::size_t>(digit - '0');
}
return value >= 1 && value <= HOST_URL_MAX_PORT ? HostUrlVerdict::Ok : HostUrlVerdict::BadPort;
}

constexpr HostUrlVerdict ValidateAuthority(std::string_view authority) {
if (authority.empty()) return HostUrlVerdict::BadAuthority;
if (authority.find('@') != std::string_view::npos) return HostUrlVerdict::Credentials;
std::string_view host = authority;
std::string_view port;
bool hasPort = false;
if (authority.front() == '[') {
const std::size_t close = authority.find(']');
if (close == std::string_view::npos || close == 1) return HostUrlVerdict::BadAuthority;
host = authority.substr(1, close - 1);
for (const char value : host)
if (!IsHexDigit(value) && value != ':' && value != '.') return HostUrlVerdict::BadAuthority;
const std::string_view rest = authority.substr(close + 1);
if (!rest.empty()) {
if (rest.front() != ':') return HostUrlVerdict::BadAuthority;
hasPort = true;
port = rest.substr(1);
}
} else {
const std::size_t colon = authority.find(':');
if (colon != std::string_view::npos) {
host = authority.substr(0, colon);
hasPort = true;
port = authority.substr(colon + 1);
}
if (host.empty()) return HostUrlVerdict::BadAuthority;
for (const char value : host)
if (!IsHostCharacter(value)) return HostUrlVerdict::BadAuthority;
}
if (hasPort) return ValidatePort(port);
return HostUrlVerdict::Ok;
}

}

constexpr HostUrlVerdict ValidateHostUrl(std::string_view url) {
if (url.empty()) return HostUrlVerdict::Empty;
if (url.size() > HOST_URL_MAX_LENGTH) return HostUrlVerdict::TooLong;
for (const char value : url)
if (!host_url::IsUrlCharacter(value)) return HostUrlVerdict::BadCharacter;
if (const HostUrlVerdict escapes = host_url::ValidateEscapes(url); escapes != HostUrlVerdict::Ok) return escapes;
std::string_view rest = url;
if (rest.substr(0, 7) == "http://") rest.remove_prefix(7);
else if (rest.substr(0, 8) == "https://") rest.remove_prefix(8);
else return HostUrlVerdict::BadScheme;
const std::size_t end = rest.find_first_of("/?#");
return host_url::ValidateAuthority(end == std::string_view::npos ? rest : rest.substr(0, end));
}
28 changes: 28 additions & 0 deletions core/libs/prx/libSceSystemService/HostUrlBackends.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <atomic>
#include <memory>
#include <string>
#include "prx/libSceSystemService/HostUrlPolicy.hpp"

constexpr int HOST_BROWSER_MAX_OPENERS = 2;

class HostBrowserLauncher final : public IUrlLauncher {
public:
HostBrowserLauncher();
explicit HostBrowserLauncher(std::string program);
bool Launch(const std::string& url) override;

private:
std::string program;
std::shared_ptr<std::atomic<int>> running = std::make_shared<std::atomic<int>>(0);
};

class HostMessageBoxConfirmation final : public IUrlConfirmation {
public:
bool Confirm(const std::string& url) override;
};

std::string HostConfirmationText(const std::string& url);

extern "C" void SystemServiceSetUrlHandlers_nid_no_patch(IUrlLauncher* launcher, IUrlConfirmation* confirmation, IUrlClock* clock);
37 changes: 37 additions & 0 deletions core/libs/prx/libSceSystemService/HostUrlConfirmation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "prx/libSceSystemService/HostUrlBackends.hpp"
#include <SDL.h>

namespace {

constexpr std::size_t WRAP_WIDTH = 96;
constexpr int BUTTON_CANCEL = 0;
constexpr int BUTTON_OPEN = 1;

}

std::string HostConfirmationText(const std::string& url) {
std::string text = "A game asks to open this address in your default web browser:\n\n";
for (std::size_t offset = 0; offset < url.size(); offset += WRAP_WIDTH) {
if (offset != 0) text += '\n';
text.append(url, offset, WRAP_WIDTH);
}
text += "\n\nOpen it only if you trust it.";
return text;
}

bool HostMessageBoxConfirmation::Confirm(const std::string& url) {
const std::string text = HostConfirmationText(url);
const SDL_MessageBoxButtonData buttons[] = {
{SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT | SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTON_CANCEL, "Cancel"},
{0, BUTTON_OPEN, "Open"},
};
SDL_MessageBoxData data{};
data.flags = SDL_MESSAGEBOX_WARNING;
data.title = "AnyPS5: open a link?";
data.message = text.c_str();
data.numbuttons = 2;
data.buttons = buttons;
int pressed = BUTTON_CANCEL;
if (SDL_ShowMessageBox(&data, &pressed) != 0) return false;
return pressed == BUTTON_OPEN;
}
71 changes: 71 additions & 0 deletions core/libs/prx/libSceSystemService/HostUrlLauncherPosix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "prx/libSceSystemService/HostUrl.hpp"
#include "prx/libSceSystemService/HostUrlBackends.hpp"
#include <cerrno>
#include <fcntl.h>
#include <spawn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <thread>
#include <utility>
#ifdef __APPLE__
#include <crt_externs.h>
#else
extern char** environ;
#endif

namespace {

char** HostEnvironment() {
#ifdef __APPLE__
return *_NSGetEnviron();
#else
return environ;
#endif
}

const char* DefaultProgram() {
#ifdef __APPLE__
return "/usr/bin/open";
#else
return "xdg-open";
#endif
}

}

HostBrowserLauncher::HostBrowserLauncher() : program(DefaultProgram()) {}

HostBrowserLauncher::HostBrowserLauncher(std::string program) : program(std::move(program)) {}

bool HostBrowserLauncher::Launch(const std::string& url) {
if (ValidateHostUrl(url) != HostUrlVerdict::Ok || program.empty()) return false;
if (running->fetch_add(1) >= HOST_BROWSER_MAX_OPENERS) {
--*running;
return false;
}
std::string name = program;
std::string address = url;
char* arguments[] = {name.data(), address.data(), nullptr};
posix_spawn_file_actions_t actions;
if (posix_spawn_file_actions_init(&actions) != 0) {
--*running;
return false;
}
bool prepared = posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0) == 0 &&
posix_spawn_file_actions_addopen(&actions, 1, "/dev/null", O_WRONLY, 0) == 0 &&
posix_spawn_file_actions_addopen(&actions, 2, "/dev/null", O_WRONLY, 0) == 0;
pid_t child = 0;
const int error = prepared ? posix_spawnp(&child, name.c_str(), &actions, nullptr, arguments, HostEnvironment()) : -1;
posix_spawn_file_actions_destroy(&actions);
if (error != 0) {
--*running;
return false;
}
std::thread([child, count = running] {
int status = 0;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {
}
--*count;
}).detach();
return true;
}
20 changes: 20 additions & 0 deletions core/libs/prx/libSceSystemService/HostUrlLauncherWindows.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "prx/libSceSystemService/HostUrl.hpp"
#include "prx/libSceSystemService/HostUrlBackends.hpp"
#include <string>
#include <utility>
#include <windows.h>
#include <shellapi.h>

HostBrowserLauncher::HostBrowserLauncher() = default;

HostBrowserLauncher::HostBrowserLauncher(std::string program) : program(std::move(program)) {}

bool HostBrowserLauncher::Launch(const std::string& url) {
if (ValidateHostUrl(url) != HostUrlVerdict::Ok) return false;
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, url.c_str(), static_cast<int>(url.size()), nullptr, 0);
if (length <= 0) return false;
std::wstring address(static_cast<std::size_t>(length), L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, url.c_str(), static_cast<int>(url.size()), address.data(), length) != length) return false;
const auto result = reinterpret_cast<std::intptr_t>(ShellExecuteW(nullptr, L"open", address.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
return result > 32;
}
Loading
Loading