fix(libc): switch update streams to reading as FreeBSD does

On Windows the UCRT does not switch an r+, w+ or a+ stream from writing
to reading without fflush or fseek: fread after fwrite returned bytes of
the write buffer and the next flush wrote them over the file, and ungetc
failed on an update stream that was not already reading. On Linux glibc
does not switch either for ungetc, which pushes the byte back over the
pending write, drops the write and can corrupt the heap on the next read,
or for an fread of at least the buffer size, which drops the write and
reads from the old offset.

fread, fgetc, getc_unlocked, fgets, ungetc, ungetwc and fscanf now flush
pending output and mark the stream as reading first, as FreeBSD 11.4's
__srefill and __ungetc do, through _IO_switch_to_get_mode on glibc.
Writes directly after reads still go through the UCRT on Windows; the
gap is recorded in the libc technical debt.
This commit is contained in:
dantdmdarealguy committed 2026-10-20 11:28:19 -05:00
1 parent f6ce8e34a2
commit a7e6e597d3
6 files changed
+149 -6

No files matched your search

+48
View File
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <filesystem>
@@ -16,9 +17,19 @@
#include "prx/libc/include/FileLock.hpp"
extern "C" void* HostStandardHandle_nid_no_patch(int descriptor);
extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle);
struct UcrtStreamState {
char* pointer;
char* base;
int count;
long flags;
};
#else
#include <unistd.h>
#endif
#if defined(__GLIBC__)
#include <stdio_ext.h>
extern "C" int _IO_switch_to_get_mode(std::FILE* stream);
#endif
extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor);
struct GuestFilePrefix {
@@ -175,6 +186,43 @@ public:
(endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0));
}
bool SwitchToReading() {
#ifdef _WIN32
::_lock_file(GetHandle());
const bool switched = SwitchToReadingUnlocked();
::_unlock_file(GetHandle());
return switched;
#else
::flockfile(GetHandle());
const bool switched = SwitchToReadingUnlocked();
::funlockfile(GetHandle());
return switched;
#endif
}
bool SwitchToReadingUnlocked() {
#ifdef _WIN32
constexpr long UcrtReading = 0x0001;
constexpr long UcrtWriting = 0x0002;
constexpr long UcrtUpdate = 0x0004;
auto* native = GetHandle();
std::atomic_ref<long> state(reinterpret_cast<UcrtStreamState*>(native)->flags);
if ((state.load() & (UcrtReading | UcrtUpdate)) != UcrtUpdate) return true;
const bool switched = (state.load() & UcrtWriting) == 0 || ::_fflush_nolock(native) == 0;
if (switched) state.fetch_or(UcrtReading);
SyncUnlockedStatus();
return switched;
#elif defined(__GLIBC__)
auto* native = GetHandle();
if (!::__freadable(native) || !::__fwriting(native)) return true;
const bool switched = ::_IO_switch_to_get_mode(native) == 0;
SyncUnlockedStatus();
return switched;
#else
return true;
#endif
}
void SetError() {
guestError = true;
SyncStatus();
+1
View File
@@ -442,6 +442,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File
stream->SyncStatus();
return count;
}
if (!stream->SwitchToReading()) return 0;
const auto result = std::fread(buffer, size, count, handle);
stream->SyncStatus();
return result;
+1
View File
@@ -292,6 +292,7 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) {
int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) {
if (stream && !stream->IsOpen()) { errno = 9; return -1; }
auto* native = GetNativeStream(stream);
if (!stream->SwitchToReading()) return EOF;
#ifdef _WIN32
__builtin_sysv_va_list args;
__builtin_sysv_va_start(args, format);
+16 -6
View File
@@ -35,7 +35,9 @@ int __isthreaded_nid_postfix = 1;
int APS5_VABI fgetc_nid_postfix(FileStream* stream) {
if (stream && !stream->IsOpen()) { errno = 9; return -1; }
const int result = std::fgetc(GetNativeStream(stream));
auto* native = GetNativeStream(stream);
if (!stream->SwitchToReading()) return -1;
const int result = std::fgetc(native);
stream->SyncStatus();
return result;
}
@@ -75,17 +77,21 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea
stream->SetError();
return -1;
}
const int result = std::ungetc(value, GetNativeStream(stream));
auto* native = GetNativeStream(stream);
if (!stream->SwitchToReading()) return -1;
const int result = std::ungetc(value, native);
stream->SyncStatus();
return result;
}
int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); }
int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) {
if (stream && !stream->IsOpen()) { errno = 9; return -1; }
auto* native = GetNativeStream(stream);
if (!stream->SwitchToReadingUnlocked()) return -1;
#ifdef _WIN32
const int result = _fgetc_nolock(GetNativeStream(stream));
const int result = _fgetc_nolock(native);
#else
const int result = ::getc_unlocked(GetNativeStream(stream));
const int result = ::getc_unlocked(native);
#endif
stream->SyncUnlockedStatus();
return result;
@@ -117,7 +123,9 @@ int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, _
int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); }
int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) {
if (stream && !stream->IsOpen()) { errno = 9; return -1; }
const int result = std::ungetc(value, GetNativeStream(stream));
auto* native = GetNativeStream(stream);
if (value != EOF && !stream->SwitchToReading()) return -1;
const int result = std::ungetc(value, native);
stream->SyncStatus();
return result;
}
@@ -128,7 +136,9 @@ char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) {
errno = 22;
return nullptr;
}
auto* result = std::fgets(buffer, size, GetNativeStream(stream));
auto* native = GetNativeStream(stream);
if (!stream->SwitchToReading()) return nullptr;
auto* result = std::fgets(buffer, size, native);
stream->SyncStatus();
return result;
}
+82
View File
@@ -9,7 +9,9 @@
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <fcntl.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@@ -33,7 +35,13 @@ int APS5_VABI fflush_nid_postfix(FileStream* stream);
int APS5_VABI remove_nid_postfix(const char* path);
int APS5_VABI rename_nid_postfix(const char* from, const char* to);
int APS5_VABI ferror_nid_postfix(FileStream* stream);
int APS5_VABI feof_nid_postfix(FileStream* stream);
void APS5_VABI clearerr_nid_postfix(FileStream* stream);
int APS5_VABI fgetc_nid_postfix(FileStream* stream);
int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream);
char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream);
int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream);
int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...);
}
static void Require(bool condition, const char* message = "File stream check failed") {
@@ -115,9 +123,83 @@ static void CheckExclusiveCppOpen(const std::string& base) {
}
}
static std::string ReadWhole(const std::string& path) {
auto* stream = fopen_nid_postfix(path.c_str(), "rb");
Require(stream != nullptr);
std::array<char, 64> buffer{};
const auto size = fread_nid_postfix(buffer.data(), 1, buffer.size(), stream);
Require(fclose_nid_postfix(stream) == 0);
return std::string(buffer.data(), size);
}
template<typename TRead>
static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) {
auto* stream = fopen_nid_postfix(path.c_str(), "wb");
Require(stream != nullptr && fputs_nid_postfix("0123456789", stream) >= 0 && fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(path.c_str(), mode);
Require(stream != nullptr && fwrite_nid_postfix("AB", 1, 2, stream) == 2);
read(stream);
Require(ferror_nid_postfix(stream) == 0, "Read after write on an update stream set the error indicator");
Require(fclose_nid_postfix(stream) == 0);
Require(ReadWhole(path) == expected, "Read after write on an update stream changed the file");
}
static void CheckUpdateStreamReadsAfterWrites(const std::string& base) {
const std::string path = base + ".update";
constexpr char overwritten[] = "AB23456789";
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
std::array<char, 4> buffer{};
Require(fread_nid_postfix(buffer.data(), 1, 3, stream) == 3, "fread after fwrite on an r+ stream");
Require(std::strcmp(buffer.data(), "234") == 0, "fread after fwrite on an r+ stream read other bytes");
Require(ftell_nid_postfix(stream) == 5);
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
std::vector<char> buffer(65536);
Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 8, "fread of more than the stream buffer after fwrite on an r+ stream");
Require(std::string_view(buffer.data(), 8) == "23456789", "fread of more than the stream buffer after fwrite on an r+ stream read other bytes");
Require(ftell_nid_postfix(stream) == 10);
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
Require(fgetc_nid_postfix(stream) == '2', "fgetc after fwrite on an r+ stream");
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
Require(getc_unlocked_nid_postfix(stream) == '2', "getc_unlocked after fwrite on an r+ stream");
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
std::array<char, 4> buffer{};
Require(fgets_nid_postfix(buffer.data(), static_cast<int>(buffer.size()), stream) == buffer.data(), "fgets after fwrite on an r+ stream");
Require(std::strcmp(buffer.data(), "234") == 0, "fgets after fwrite on an r+ stream read other bytes");
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc after fwrite on an r+ stream");
Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '2', "ungetc after fwrite on an r+ stream pushed back other bytes");
});
CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) {
std::array<char, 4> buffer{};
Require(fscanf_nid_postfix(stream, "%3s", buffer.data()) == 1, "fscanf after fwrite on an r+ stream");
Require(std::strcmp(buffer.data(), "234") == 0, "fscanf after fwrite on an r+ stream read other bytes");
});
CheckReadAfterWrite(path, "w+b", "AB", [](FileStream* stream) {
std::array<char, 4> buffer{};
Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0 && feof_nid_postfix(stream) != 0, "fread after fwrite on a w+ stream");
});
CheckReadAfterWrite(path, "a+b", "0123456789AB", [](FileStream* stream) {
Require(fgetc_nid_postfix(stream) == EOF && feof_nid_postfix(stream) != 0, "fgetc after fwrite on an a+ stream");
});
auto* stream = fopen_nid_postfix(path.c_str(), "r+b");
Require(stream != nullptr);
Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc on a new r+ stream");
Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '0', "ungetc on a new r+ stream pushed back other bytes");
Require(fseek_nid_postfix(stream, 2, SEEK_SET) == 0 && ungetc_nid_postfix('Y', stream) == 'Y', "ungetc after fseek on an r+ stream");
Require(fgetc_nid_postfix(stream) == 'Y' && fgetc_nid_postfix(stream) == '2', "ungetc after fseek on an r+ stream pushed back other bytes");
Require(fclose_nid_postfix(stream) == 0);
Require(std::remove(path.c_str()) == 0);
}
int main(int argc, char** argv) {
Require(argc == 2);
CheckExclusiveCppOpen(argv[1]);
CheckUpdateStreamReadsAfterWrites(argv[1]);
Require(_Stdout_nid_postfix.GetHandle() == stdout);
Require(_Stderr_nid_postfix.GetHandle() == stderr);
Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0);
+1
View File
@@ -92,6 +92,7 @@
- [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way
- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated
- [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry
- [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill`
- [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way
- [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`.
- The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked