From 1337bd7e45f51cbf2bdd556d9056cb7efb2d4a6b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 08:00:21 +0000 Subject: [PATCH] fix(libc): leave a stream that is reading untouched in fflush on Linux fflush() passed every stream to glibc's fflush(). For a stream that is reading, glibc seeks the descriptor back and drops the read buffer, and with a pushed-back character it gets the position wrong: after fgetc() returned 'a' from "abcdef\n" and ungetc('Z'), fflush() returned 0 and the next fread() returned "bcdef\n\n". The pushed-back 'Z' was lost and the last byte came back twice. FreeBSD's fflush() writes out only a stream that is writing and leaves every other stream as it is, lib/libc/stdio/fflush.c: if ((fp->_flags & (__SWR | __SRW)) == 0) retval = 0; else retval = __sflush(fp); and __sflush(): t = fp->_flags; if ((t & __SWR) == 0) return (0); fflush() on Linux now returns 0 without calling the host for a stream whose last operation was not a write (__fwriting() is 0). fflush(NULL) and streams that are writing are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 10 +++++++++- core/libs/tests/GuestStandardStreams.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 87c352e2b..45212dc36 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -18,6 +18,10 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" +#ifdef __linux__ +#include +#endif + struct GuestFileMode { std::string native; bool writes = false; @@ -272,7 +276,11 @@ int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { } int APS5_VABI fflush_nid_postfix(FileStream* stream) { - if (std::fflush(stream ? GetNativeStream(stream) : nullptr) != 0) throw std::runtime_error("fflush: flush failed"); + std::FILE* native = stream ? GetNativeStream(stream) : nullptr; +#ifdef __linux__ + if (native && !__fwriting(native)) return 0; +#endif + if (std::fflush(native) != 0) throw std::runtime_error("fflush: flush failed"); return 0; } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 3173ea8d9..953f513e8 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -53,6 +53,7 @@ int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); void APS5_VABI setbuf_nid_postfix(FileStream*, char*); FileStream* APS5_VABI fdopen_nid_postfix(int, const char*); int APS5_VABI fclose_nid_postfix(FileStream*); +int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI _Getmbcurmax_nid_postfix(); int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI flockfile_nid_postfix(FileStream*); @@ -280,6 +281,28 @@ static void CheckRewind() { #endif } +static void CheckFlushWhileReading() { + const auto filename = "anyps5-flush-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { std::ofstream seed(filename, std::ios::binary); seed << "abcdef\n"; Require(seed.good()); } + for (const char* mode : {"r", "r+"}) { + FileStream* stream = fopen_nid_postfix(filename.c_str(), mode); + Require(stream != nullptr); + Require(fgetc_nid_postfix(stream) == 'a' && ungetc_nid_postfix('Z', stream) == 'Z'); + Require(fflush_nid_postfix(stream) == 0); + char rest[16]{}; + const auto count = fread_nid_postfix(rest, 1, sizeof(rest) - 1, stream); + if (count != 7 || std::strcmp(rest, "Zbcdef\n") != 0) { + std::string shown; + for (std::size_t index = 0; index < count; ++index) shown += rest[index] == '\n' ? std::string("\\n") : std::string(1, rest[index]); + std::fprintf(stderr, "fflush of a \"%s\" stream that is reading changed the data read next: \"%s\" (%zu bytes), expected \"Zbcdef\\n\" (7 bytes)\n", + mode, shown.c_str(), count); + std::abort(); + } + Require(fclose_nid_postfix(stream) == 0); + } + std::filesystem::remove(filename); +} + static bool CheckBinaryModes() { const auto directory = "anyps5-byte-stream-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); Require(std::filesystem::create_directory(directory)); @@ -336,6 +359,7 @@ static bool CheckBinaryModes() { int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); + CheckFlushWhileReading(); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); Require(fdopen_nid_postfix(0, nullptr) == nullptr && *__error_nid_postfix() == 22); Require(fdopen_nid_postfix(0, "invalid") == nullptr && *__error_nid_postfix() == 22);