From c35fba5ad5dce1b5cbbe76c5b314f38a48371789 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 19:55:06 +0300 Subject: [PATCH 1/3] fix(libc): close the stream when freopen gets an invalid mode FreeBSD closes the stream and keeps errno at EINVAL when freopen cannot parse the mode, with or without a path. The guest stream stayed open and the descriptor leaked. --- core/libs/prx/libc/src/FileAndHeap.cpp | 9 +++++++-- core/libs/tests/GuestStandardStreams.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index ef219f06bbe..507d6e7d46a 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -239,9 +239,14 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { if (!stream || !mode) { errno = 22; return nullptr; } - if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. const auto parsed = ParseFileMode(mode); - if (!parsed) { errno = 22; return nullptr; } + if (!parsed) { + stream->Close(); + if (stream->IsDynamic()) delete stream; + errno = 22; + return nullptr; + } + if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. try { std::filesystem::path path; if (*filename) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0e18fd8fbd6..b9eff6d9e59 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -620,6 +620,12 @@ int main() { Require(freopen_nid_postfix(filename.c_str(), "rb", &failed) == nullptr); Require(*__error_nid_postfix() == 2); Require(failed.GuestState().flags == 0 && failed.GuestState().descriptor == -1); + FileStream invalidMode(std::tmpfile()); + Require(freopen_nid_postfix(filename.c_str(), "zz", &invalidMode) == nullptr && *__error_nid_postfix() == 22); + Require(invalidMode.GuestState().flags == 0 && invalidMode.GuestState().descriptor == -1); + FileStream invalidModeWithoutPath(std::tmpfile()); + Require(freopen_nid_postfix(nullptr, "zz", &invalidModeWithoutPath) == nullptr && *__error_nid_postfix() == 22); + Require(invalidModeWithoutPath.GuestState().flags == 0 && invalidModeWithoutPath.GuestState().descriptor == -1); FileStream locked(std::tmpfile()); flockfile_nid_postfix(&locked); flockfile_nid_postfix(&locked); From bdaedb6859315c58c1375d81d45b2099807622fc Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 20:52:24 +0300 Subject: [PATCH 2/3] fix(libc): let freopen fail an invalid mode on a closed stream freopen with an invalid mode threw on a stream that was already closed instead of returning null with EINVAL. The stream is now closed only once and the mode-only reopening limit is listed in the technical debt. --- core/libs/prx/libc/include/FileStream.hpp | 4 ++++ core/libs/prx/libc/src/FileAndHeap.cpp | 8 +++++--- core/libs/tests/GuestStandardStreams.cpp | 3 +++ docs/dev/debt/libc.md | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 4ad9fa00b8b..385e17d400b 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -71,6 +71,10 @@ class FileStream { return _handle; } + bool IsClosed() const { + return _handle == nullptr; + } + bool IsDynamic() const { return _dynamic; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 507d6e7d46a..8f58a4960ab 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -241,12 +241,14 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!stream || !mode) { errno = 22; return nullptr; } const auto parsed = ParseFileMode(mode); if (!parsed) { - stream->Close(); - if (stream->IsDynamic()) delete stream; + if (!stream->IsClosed()) { + stream->Close(); + if (stream->IsDynamic()) delete stream; + } errno = 22; return nullptr; } - if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. + if (!filename) { errno = 45; return nullptr; } try { std::filesystem::path path; if (*filename) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index b9eff6d9e59..595e9f5315e 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -623,6 +623,9 @@ int main() { FileStream invalidMode(std::tmpfile()); Require(freopen_nid_postfix(filename.c_str(), "zz", &invalidMode) == nullptr && *__error_nid_postfix() == 22); Require(invalidMode.GuestState().flags == 0 && invalidMode.GuestState().descriptor == -1); + FileStream closedInvalidMode(std::tmpfile()); + closedInvalidMode.Close(); + Require(freopen_nid_postfix(filename.c_str(), "zz", &closedInvalidMode) == nullptr && *__error_nid_postfix() == 22); FileStream invalidModeWithoutPath(std::tmpfile()); Require(freopen_nid_postfix(nullptr, "zz", &invalidModeWithoutPath) == nullptr && *__error_nid_postfix() == 22); Require(invalidModeWithoutPath.GuestState().flags == 0 && invalidModeWithoutPath.GuestState().descriptor == -1); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad1..430290e1357 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -50,6 +50,7 @@ - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. +- [freopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) does not reopen a stream with a null path to change its mode: it fails with ENOTSUP and leaves the stream as it was, where FreeBSD reopens the same file with the new mode - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. From 229d5280b31db08a9d0537ce4161ddad324f9190 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 21:00:09 +0300 Subject: [PATCH 3/3] fix(libc): keep the standard streams open when freopen gets an invalid mode Only streams the guest opened itself are closed. stdin, stdout and stderr stay open and usable, and the difference from FreeBSD is listed in the technical debt. --- core/libs/prx/libc/include/FileStream.hpp | 4 ++++ core/libs/prx/libc/src/FileAndHeap.cpp | 2 +- core/libs/tests/GuestStandardStreams.cpp | 15 +++++++++++++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 385e17d400b..9c29d75b0d5 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -71,6 +71,10 @@ class FileStream { return _handle; } + bool IsStandard() const { + return _handle == stdin || _handle == stdout || _handle == stderr; + } + bool IsClosed() const { return _handle == nullptr; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 8f58a4960ab..75bc61cd43f 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -241,7 +241,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!stream || !mode) { errno = 22; return nullptr; } const auto parsed = ParseFileMode(mode); if (!parsed) { - if (!stream->IsClosed()) { + if (!stream->IsClosed() && !stream->IsStandard()) { stream->Close(); if (stream->IsDynamic()) delete stream; } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 595e9f5315e..aed61531607 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -626,6 +626,21 @@ int main() { FileStream closedInvalidMode(std::tmpfile()); closedInvalidMode.Close(); Require(freopen_nid_postfix(filename.c_str(), "zz", &closedInvalidMode) == nullptr && *__error_nid_postfix() == 22); + for (FileStream* standard : {&_Stdout_nid_postfix, &_Stderr_nid_postfix}) { + Require(freopen_nid_postfix(filename.c_str(), "zz", standard) == nullptr && *__error_nid_postfix() == 22); + Require(standard->GetHandle() != nullptr && fflush_nid_postfix(standard) == 0 && ferror_nid_postfix(standard) == 0); + } + Require(freopen_nid_postfix(filename.c_str(), "zz", __stdinp_nid_postfix) == nullptr && *__error_nid_postfix() == 22); + Require(__stdinp_nid_postfix->GetHandle() == stdin); + { + std::ofstream(filename, std::ios::binary).put('x'); + for (int index = 0; index < 10000; ++index) { + FileStream* opened = fopen_nid_postfix(filename.c_str(), "rb"); + Require(opened != nullptr); + Require(freopen_nid_postfix(filename.c_str(), "zz", opened) == nullptr && *__error_nid_postfix() == 22); + } + Require(std::filesystem::remove(filename)); + } FileStream invalidModeWithoutPath(std::tmpfile()); Require(freopen_nid_postfix(nullptr, "zz", &invalidModeWithoutPath) == nullptr && *__error_nid_postfix() == 22); Require(invalidModeWithoutPath.GuestState().flags == 0 && invalidModeWithoutPath.GuestState().descriptor == -1); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 430290e1357..d1115b0ebae 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -50,7 +50,7 @@ - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. -- [freopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) does not reopen a stream with a null path to change its mode: it fails with ENOTSUP and leaves the stream as it was, where FreeBSD reopens the same file with the new mode +- [freopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) does not reopen a stream with a null path to change its mode: it fails with ENOTSUP and leaves the stream as it was, where FreeBSD reopens the same file with the new mode. With an invalid mode, `freopen` closes a stream the guest opened but leaves stdin, stdout and stderr open and usable and fails with EINVAL, where FreeBSD closes them too - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware.