From 2fe382c6580998debde57f4a7e3516e189b020e4 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 16:18:24 +0200 Subject: [PATCH 1/2] feat(libSceLibcInternal): implement the posix_spawn attribute and file action calls XPSemu (PPSA97358) imports eleven posix_spawn calls from libSceLibcInternal.prx, one reference each, and no call site is known. Predicted stop (import order, not measured), behind isnanf (idx 296): `FAIL: unresolved ELF import 44hlATrd47U from libSceLibcInternal.prx` (posix_spawnattr_init, idx 347), followed by p--TkNVsXjA, LG6O0oW9bQU, Sj7y+JO5PcM, Ud8CbISKRGM, Hq9-2AMG+ks, IUfBO5UIZNc, Heh4KJwyoX8 (348-355), UV4m0bznVtU, Ani6e+T-y6Q and 3Nr9caNHhyg (379-381). The nine object calls follow FreeBSD release/11.4.0 (8c639945a445) lib/libc/gen/posix_spawn.c: posix_spawnattr_init allocates a zeroed attribute object (ENOMEM on failure), _destroy frees it, _setflags and _setsigdefault store the value; posix_spawn_file_actions_init allocates an empty list (-1 on failure, as in 11.4), _destroy frees it with its entries, and _addopen (path copied), _adddup2 and _addclose append in order after returning EBADF for a negative descriptor. Null or uninitialized objects, which FreeBSD dereferences, throw. posix_spawn and posix_spawnp throw "process creation not implemented", as execvp does: FreeBSD validates no argument before rfork/vfork, and no child process can be created here. TechnicalDebt records them. guest_posix_spawn covers the attribute round trips, the recorded action sequence, the EBADF cases and the throws. NIDs computed with tools/nid_names.py and present in aerolib. Co-Authored-By: Claude Fable 5.1 --- .../prx/libSceLibcInternal/CMakeLists.txt | 12 +- core/libs/prx/libSceLibcInternal/Spawn.cpp | 119 +++++++++++++++ .../prx/libSceLibcInternal/include/Spawn.hpp | 37 +++++ core/libs/tests/GuestPosixSpawn.cpp | 137 ++++++++++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 5 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceLibcInternal/Spawn.cpp create mode 100644 core/libs/prx/libSceLibcInternal/include/Spawn.hpp create mode 100644 core/libs/tests/GuestPosixSpawn.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 1df68eac3..17cbde0ad 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp UserContext.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) @@ -55,6 +55,16 @@ if(BUILD_TESTING) set_tests_properties(guest_utime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_spawn_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawn.cpp) + target_include_directories(guest_posix_spawn_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_posix_spawn_tests) + add_test(NAME guest_posix_spawn COMMAND guest_posix_spawn_tests) + set_tests_properties(guest_posix_spawn PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_posix_spawn PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_program_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestProgramName.cpp) target_include_directories(guest_program_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_program_name_tests PRIVATE libSceLibcInternal libkernel libc) diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp new file mode 100644 index 000000000..c4571fb64 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -0,0 +1,119 @@ +#include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr int BadDescriptor = 9; +constexpr int OutOfMemory = 12; + +PosixSpawnAttributes& Attributes(PosixSpawnAttributes* const* attributes, const char* function) { + if (attributes == nullptr || *attributes == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized attributes"); + return **attributes; +} + +PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const char* function) { + if (actions == nullptr || *actions == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized file actions"); + return **actions; +} + +int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { + try { + actions.actions.push_back(std::move(action)); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } + return 0; +} + +} + +extern "C" { + +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_init: null attributes"); + auto* created = new (std::nothrow) PosixSpawnAttributes{}; + if (created == nullptr) return OutOfMemory; + *attributes = created; + return 0; +} + +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_destroy: null attributes"); + delete *attributes; + return 0; +} + +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes** attributes, std::int16_t flags) { + Attributes(attributes, "posix_spawnattr_setflags").flags = flags; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalDefault) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_setsigdefault: null signal set"); + target.signalDefault = *signalDefault; + return 0; +} + +int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions** actions) { + if (actions == nullptr) throw std::invalid_argument("posix_spawn_file_actions_init: null file actions"); + auto* created = new (std::nothrow) PosixSpawnFileActions{}; + if (created == nullptr) return -1; + *actions = created; + return 0; +} + +int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions** actions) { + delete &FileActions(actions, "posix_spawn_file_actions_destroy"); + return 0; +} + +int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions** actions, int descriptor, const char* path, int openFlags, std::uint16_t mode) { + if (descriptor < 0) return BadDescriptor; + if (path == nullptr) throw std::invalid_argument("posix_spawn_file_actions_addopen: null path"); + auto& target = FileActions(actions, "posix_spawn_file_actions_addopen"); + try { + return Append(target, {PosixSpawnFileActionKind::Open, descriptor, path, openFlags, mode, 0}); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } +} + +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions** actions, int descriptor, int newDescriptor) { + if (descriptor < 0 || newDescriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_adddup2"), {PosixSpawnFileActionKind::Duplicate, descriptor, {}, 0, 0, newDescriptor}); +} + +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions** actions, int descriptor) { + if (descriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_addclose"), {PosixSpawnFileActionKind::Close, descriptor, {}, 0, 0, 0}); +} + +int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { + (void)processId; + (void)path; + (void)actions; + (void)attributes; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("posix_spawn: process creation"); + return -1; +} + +int APS5_VABI posix_spawnp_nid_postfix(std::int32_t* processId, const char* file, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { + (void)processId; + (void)file; + (void)actions; + (void)attributes; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("posix_spawnp: process creation"); + return -1; +} + +} diff --git a/core/libs/prx/libSceLibcInternal/include/Spawn.hpp b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp new file mode 100644 index 000000000..5e93fa815 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP +#define CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP + +#include +#include +#include + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +static_assert(sizeof(GuestSignalSet) == 16); + +struct PosixSpawnAttributes { + std::int16_t flags; + std::int32_t processGroup; + std::int32_t schedulingPriority; + std::int32_t schedulingPolicy; + GuestSignalSet signalDefault; + GuestSignalSet signalMask; +}; + +enum class PosixSpawnFileActionKind { Open, Duplicate, Close }; + +struct PosixSpawnFileAction { + PosixSpawnFileActionKind kind; + int descriptor; + std::string path; + int openFlags; + std::uint16_t mode; + int newDescriptor; +}; + +struct PosixSpawnFileActions { + std::vector actions; +}; + +#endif diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp new file mode 100644 index 000000000..8fe49f711 --- /dev/null +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions**); +int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions**); +int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions**, int, const char*, int, std::uint16_t); +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions**, int, int); +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions**, int); +int APS5_VABI posix_spawn_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +int APS5_VABI posix_spawnp_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +} + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Posix spawn check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int BadDescriptor = 9; + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template +static bool ReportsNotImplemented(TCall call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == message; + } + return false; +} + +static void CheckAttributes() { + PosixSpawnAttributes* attributes = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && attributes != nullptr); + Check(attributes->flags == 0 && attributes->processGroup == 0 && attributes->schedulingPriority == 0 && attributes->schedulingPolicy == 0); + const GuestSignalSet empty{}; + Check(std::memcmp(&attributes->signalDefault, &empty, sizeof(empty)) == 0 && std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0x30) == 0 && attributes->flags == 0x30); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, -1) == 0 && attributes->flags == -1); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0) == 0 && attributes->flags == 0); + + const GuestSignalSet defaults{{0x00004002u, 0x80000000u, 1u, 0xffffffffu}}; + Check(posix_spawnattr_setsigdefault_nid_postfix(&attributes, &defaults) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + PosixSpawnAttributes* uninitialized = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setflags_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&uninitialized, &defaults); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([] { posix_spawnattr_init_nid_postfix(nullptr); })); + + Check(posix_spawnattr_destroy_nid_postfix(&attributes) == 0); + Check(posix_spawnattr_destroy_nid_postfix(&uninitialized) == 0); +} + +static void CheckFileActions() { + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawn_file_actions_init_nid_postfix(&actions) == 0 && actions != nullptr && actions->actions.empty()); + + char path[] = "/app0/log.txt"; + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, 3, path, 0x601, 0644) == 0); + std::strcpy(path, "/changed"); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 3, 1) == 0); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, 3) == 0); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 0, 0) == 0); + + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, -1, "/app0/other", 0, 0) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, -1, 1) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 1, -1) == BadDescriptor); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, -2) == BadDescriptor); + + const auto& recorded = actions->actions; + Check(recorded.size() == 4); + Check(recorded[0].kind == PosixSpawnFileActionKind::Open && recorded[0].descriptor == 3); + Check(recorded[0].path == "/app0/log.txt" && recorded[0].openFlags == 0x601 && recorded[0].mode == 0644); + Check(recorded[1].kind == PosixSpawnFileActionKind::Duplicate && recorded[1].descriptor == 3 && recorded[1].newDescriptor == 1); + Check(recorded[2].kind == PosixSpawnFileActionKind::Close && recorded[2].descriptor == 3); + Check(recorded[3].kind == PosixSpawnFileActionKind::Duplicate && recorded[3].descriptor == 0 && recorded[3].newDescriptor == 0); + + PosixSpawnFileActions* uninitialized = nullptr; + Check(posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, -1) == BadDescriptor); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_adddup2_nid_postfix(&uninitialized, 1, 2); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&uninitialized, 1, "/app0/a", 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&actions, 1, nullptr, 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_destroy_nid_postfix(&uninitialized); })); + Check(ThrowsInvalidArgument([] { posix_spawn_file_actions_init_nid_postfix(nullptr); })); + Check(actions->actions.size() == 4); + + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0); +} + +static void CheckSpawn() { + PosixSpawnAttributes* attributes = nullptr; + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && posix_spawn_file_actions_init_nid_postfix(&actions) == 0); + char program[] = "/app0/tool.elf"; + char* arguments[] = {program, nullptr}; + char* environment[] = {nullptr}; + std::int32_t processId = 77; + Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, program, &actions, &attributes, arguments, environment); }, + "posix_spawn: process creation not implemented")); + Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, + "posix_spawnp: process creation not implemented")); + Check(processId == 77); + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0 && posix_spawnattr_destroy_nid_postfix(&attributes) == 0); +} + +int main() { + CheckAttributes(); + CheckFileActions(); + CheckSpawn(); +} diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 3e06950f7..eb4d57cad 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -12,4 +12,5 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA97358 imports them and fails to load without them; no call site is known. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified From 7bb59f73b42030438c7d4d585d74ffc42e57950f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 07:40:48 +0000 Subject: [PATCH 2/2] docs(libSceLibcInternal): record spawn import stops --- docs/dev/debt/libSceLibcInternal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index eb4d57cad..0a303516f 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -12,5 +12,5 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA97358 imports them and fails to load without them; no call site is known. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified