From 15bbc19501c7a3bc39c3ccb0ac74276cbfd6a9de Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 17:53:11 +0200 Subject: [PATCH 1/2] feat(libScePosixForWebKit): implement getservbyname Implement getservbyname (H1bArJPagV0) in libScePosixForWebKit. Cloud Play (PPSA99600) imports it from this library only (registry index 408, call site 0x1042124); once fnmatch (#3545) is in, its predicted load stop is: FAIL: unresolved ELF import H1bArJPagV0 from libScePosixForWebKit.prx The ps5-payload-dev SDK stubs (f5d40f02, sce_stubs/) declare getservbyname in libScePosixForWebKit.c and not in libkernel.c or libSceLibcInternal.c. The call site is the service-name branch of a getaddrinfo inside the title: a non-numeric service is looked up with protocol "tcp", "udp" or "sctp" and the port is read big-endian at offset 16 of the result, which fixes the servent layout used here (name, aliases, 32-bit port, protocol at 0, 8, 16, 24). The guest has no /etc/services and the console's file is unknown, so the function searches 37 lines of FreeBSD 11.4 etc/services (8c639945): ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps and pop3s with their aliases and protocols, in file order. None of their names or aliases appears on an earlier line of the full file, so these lookups answer as the full file would. The search follows files_servent in lib/libc/net/getservent.c (8c639945): exact, case-sensitive match on the name or an alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage like getserv's NSS TLS state. Any other name returns null without touching errno, as for a name the file lacks. The repository's getaddrinfo still resolves service names through the host, so there was no table to share; this is recorded in TechnicalDebt.md. Co-Authored-By: Claude Fable 5.1 --- .../prx/libScePosixForWebKit/CMakeLists.txt | 9 +++ .../libScePosixForWebKit/GuestResolver.hpp | 11 ++++ .../prx/libScePosixForWebKit/Resolver.cpp | 61 ++++++++++++++++++ core/libs/tests/GuestServiceDatabase.cpp | 62 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 1 + 5 files changed, 144 insertions(+) create mode 100644 core/libs/tests/GuestServiceDatabase.cpp diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index fddddc543..036eb0577 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -36,4 +36,13 @@ if(BUILD_TESTING) set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_service_database_tests ${LIBS_INCLUDE_DIR}/tests/GuestServiceDatabase.cpp) + target_include_directories(guest_service_database_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_service_database_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_service_database_tests) + add_test(NAME guest_service_database COMMAND guest_service_database_tests) + if(WIN32) + set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp index 7ed54e716..5e6fc068c 100644 --- a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp +++ b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp @@ -28,4 +28,15 @@ struct AddressInfo { static_assert(sizeof(AddressInfo) == 48); static_assert(offsetof(AddressInfo, canonicalName) == 24); static_assert(offsetof(AddressInfo, address) == 32); + +struct ServiceEntry { + char* name; + char** aliases; + std::int32_t port; + char* protocol; +}; +static_assert(sizeof(ServiceEntry) == 32); +static_assert(offsetof(ServiceEntry, aliases) == 8); +static_assert(offsetof(ServiceEntry, port) == 16); +static_assert(offsetof(ServiceEntry, protocol) == 24); } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 6441693f4..9333e9c7e 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -31,6 +31,41 @@ struct HostLookup { std::vector addresses; }; +struct ServiceRecord { + const char* name; + std::uint16_t port; + const char* protocol; + std::array aliases; +}; + +constexpr ServiceRecord Services[]{ + {"ftp-data", 20, "tcp", {}}, {"ftp-data", 20, "udp", {}}, {"ftp-data", 20, "sctp", {}}, + {"ftp", 21, "tcp", {}}, {"ftp", 21, "udp", {}}, {"ftp", 21, "sctp", {}}, + {"ssh", 22, "tcp", {}}, {"ssh", 22, "udp", {}}, {"ssh", 22, "sctp", {}}, + {"telnet", 23, "tcp", {}}, {"telnet", 23, "udp", {}}, + {"smtp", 25, "tcp", {"mail"}}, {"smtp", 25, "udp", {"mail"}}, + {"domain", 53, "tcp", {}}, {"domain", 53, "udp", {}}, + {"tftp", 69, "tcp", {}}, {"tftp", 69, "udp", {}}, + {"http", 80, "tcp", {"www", "www-http"}}, {"http", 80, "udp", {"www", "www-http"}}, + {"http", 80, "sctp", {"www", "www-http"}}, + {"pop3", 110, "tcp", {}}, {"pop3", 110, "udp", {}}, + {"ntp", 123, "tcp", {}}, {"ntp", 123, "udp", {}}, + {"imap", 143, "tcp", {"imap2", "imap4"}}, {"imap", 143, "udp", {"imap2", "imap4"}}, + {"https", 443, "tcp", {}}, {"https", 443, "udp", {}}, {"https", 443, "sctp", {}}, + {"rtsp", 554, "tcp", {}}, {"rtsp", 554, "udp", {}}, + {"submission", 587, "tcp", {}}, {"submission", 587, "udp", {}}, + {"imaps", 993, "tcp", {}}, {"imaps", 993, "udp", {}}, + {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, +}; + +struct ServiceLookup { + GuestResolver::ServiceEntry entry{}; + std::string name; + std::string protocol; + std::vector aliasNames; + std::vector aliases; +}; + bool NumericAddress(const char* name, std::array& address) { std::array parts{}; std::size_t count = 0; @@ -155,6 +190,32 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) } } +GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* name, const char* protocol) { + for (const auto& service : Services) { + bool named = std::strcmp(name, service.name) == 0; + for (const char* alias : service.aliases) + named = named || (alias && std::strcmp(name, alias) == 0); + if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; + try { + static thread_local ServiceLookup lookup; + lookup.name = service.name; + lookup.protocol = service.protocol; + lookup.aliasNames.clear(); + lookup.aliases.clear(); + for (const char* alias : service.aliases) + if (alias) lookup.aliasNames.emplace_back(alias); + for (auto& alias : lookup.aliasNames) lookup.aliases.push_back(alias.data()); + lookup.aliases.push_back(nullptr); + lookup.entry = {lookup.name.data(), lookup.aliases.data(), htons(service.port), lookup.protocol.data()}; + return &lookup.entry; + } catch (const std::bad_alloc&) { + errno = ENOMEM; + return nullptr; + } + } + return nullptr; +} + void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo* first) { while (first) { auto* next = first->next; diff --git a/core/libs/tests/GuestServiceDatabase.cpp b/core/libs/tests/GuestServiceDatabase.cpp new file mode 100644 index 000000000..30caec909 --- /dev/null +++ b/core/libs/tests/GuestServiceDatabase.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char*, const char*); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Service database check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool NetworkPort(const GuestResolver::ServiceEntry* entry, std::uint16_t port) { + unsigned char bytes[4]; + std::memcpy(bytes, &entry->port, sizeof(bytes)); + return bytes[0] == port >> 8 && bytes[1] == (port & 0xff) && bytes[2] == 0 && bytes[3] == 0; +} + +int main() { + auto* http = getservbyname_nid_postfix("http", "tcp"); + Require(http && std::strcmp(http->name, "http") == 0 && std::strcmp(http->protocol, "tcp") == 0); + Require(NetworkPort(http, 80)); + Require(http->aliases && std::strcmp(http->aliases[0], "www") == 0); + Require(std::strcmp(http->aliases[1], "www-http") == 0 && http->aliases[2] == nullptr); + + auto* https = getservbyname_nid_postfix("https", nullptr); + Require(https && std::strcmp(https->name, "https") == 0 && std::strcmp(https->protocol, "tcp") == 0); + Require(NetworkPort(https, 443) && https->aliases && https->aliases[0] == nullptr); + + auto* domain = getservbyname_nid_postfix("domain", "udp"); + Require(domain && std::strcmp(domain->name, "domain") == 0 && std::strcmp(domain->protocol, "udp") == 0); + Require(NetworkPort(domain, 53)); + + auto* www = getservbyname_nid_postfix("www", "sctp"); + Require(www && std::strcmp(www->name, "http") == 0 && std::strcmp(www->protocol, "sctp") == 0); + Require(NetworkPort(www, 80)); + + auto* mail = getservbyname_nid_postfix("mail", nullptr); + Require(mail && std::strcmp(mail->name, "smtp") == 0 && NetworkPort(mail, 25)); + + errno = 0; + Require(getservbyname_nid_postfix("no-such-service", nullptr) == nullptr && errno == 0); + Require(getservbyname_nid_postfix("domain", "sctp") == nullptr); + Require(getservbyname_nid_postfix("ntp", "TCP") == nullptr); + Require(getservbyname_nid_postfix("HTTP", "tcp") == nullptr); + + auto* ntp = getservbyname_nid_postfix("ntp", "udp"); + Require(ntp && NetworkPort(ntp, 123)); + std::thread([] { + auto* ssh = getservbyname_nid_postfix("ssh", "tcp"); + Require(ssh && std::strcmp(ssh->name, "ssh") == 0 && NetworkPort(ssh, 22)); + }).join(); + Require(std::strcmp(ntp->name, "ntp") == 0 && std::strcmp(ntp->protocol, "udp") == 0 && NetworkPort(ntp, 123)); + return 0; +} diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index cf5516d57..c57da143e 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -9,3 +9,4 @@ - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws +- [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database From 8b9529b99af3e7c87eef94d5268df92f6afe7743 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 13:16:38 +0200 Subject: [PATCH 2/2] fix(libScePosixForWebKit): keep the getservbyname result trivially destructible The per-thread servent storage held std::string and std::vector members, so getservbyname_nid_postfix registered a thread_local destructor through __cxa_thread_atexit. On Windows that destructor runs after emulated TLS freed the object, and prx_thread_local_destructors (#4029) rejects libScePosixForWebKit.prx for it. The result now lives in a thread_local struct of fixed-size char arrays and an alias pointer array, sized for the longest name (submission), alias (www-http), protocol (sctp) and alias count in the embedded table; static_asserts check the sizes against the table and that the struct is trivially destructible. The guest servent layout and the per-thread result FreeBSD's getservent.c keeps in its NSS TLS state are unchanged. Nothing is allocated, so the ENOMEM path is gone. Co-Authored-By: Claude Fable 5.1 --- .../prx/libScePosixForWebKit/Resolver.cpp | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 9333e9c7e..0dd690183 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -10,12 +10,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include #include #include #include #include +#include #include #include "GuestResolver.hpp" @@ -58,13 +60,38 @@ constexpr ServiceRecord Services[]{ {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, }; +constexpr std::size_t ServiceNameSize = 11; +constexpr std::size_t ServiceAliasSize = 9; +constexpr std::size_t ServiceProtocolSize = 5; +constexpr std::size_t ServiceAliasCount = std::tuple_size_v; + +template +constexpr std::size_t LongestServiceText(TSelect select) { + std::size_t longest = 0; + for (const auto& service : Services) + for (const char* text : select(service)) + if (text) longest = std::max(longest, std::char_traits::length(text)); + return longest; +} + +static_assert(ServiceNameSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.name}; }) + 1); +static_assert(ServiceAliasSize == LongestServiceText([](const ServiceRecord& record) { return record.aliases; }) + 1); +static_assert(ServiceProtocolSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.protocol}; }) + 1); + struct ServiceLookup { - GuestResolver::ServiceEntry entry{}; - std::string name; - std::string protocol; - std::vector aliasNames; - std::vector aliases; + GuestResolver::ServiceEntry entry; + std::array name; + std::array protocol; + std::array, ServiceAliasCount> aliasNames; + std::array aliases; }; +static_assert(std::is_trivially_destructible_v); + +template +char* CopyServiceText(std::array& target, const char* text) { + std::memcpy(target.data(), text, std::strlen(text) + 1); + return target.data(); +} bool NumericAddress(const char* name, std::array& address) { std::array parts{}; @@ -196,22 +223,17 @@ GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* nam for (const char* alias : service.aliases) named = named || (alias && std::strcmp(name, alias) == 0); if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; - try { - static thread_local ServiceLookup lookup; - lookup.name = service.name; - lookup.protocol = service.protocol; - lookup.aliasNames.clear(); - lookup.aliases.clear(); - for (const char* alias : service.aliases) - if (alias) lookup.aliasNames.emplace_back(alias); - for (auto& alias : lookup.aliasNames) lookup.aliases.push_back(alias.data()); - lookup.aliases.push_back(nullptr); - lookup.entry = {lookup.name.data(), lookup.aliases.data(), htons(service.port), lookup.protocol.data()}; - return &lookup.entry; - } catch (const std::bad_alloc&) { - errno = ENOMEM; - return nullptr; + static thread_local ServiceLookup lookup; + std::size_t count = 0; + for (const char* alias : service.aliases) { + if (!alias) continue; + lookup.aliases[count] = CopyServiceText(lookup.aliasNames[count], alias); + ++count; } + lookup.aliases[count] = nullptr; + lookup.entry = {CopyServiceText(lookup.name, service.name), lookup.aliases.data(), htons(service.port), + CopyServiceText(lookup.protocol, service.protocol)}; + return &lookup.entry; } return nullptr; }