Skip to content

fix(libc): keep host logging on the console when a guest reopens stdout or stderr - #13390

Open
medazizktata wants to merge 2 commits into
boykopovar:mainfrom
medazizktata:fix/freopen-host-log
Open

medazizktata wants to merge 2 commits into
boykopovar:mainfrom
medazizktata:fix/freopen-host-log

Conversation

@medazizktata

@medazizktata medazizktata commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What

After a guest freopen of stdout or stderr, AnyPS5's own log lines followed the guest stream into the title's file (#13328). On Windows both streams were affected, on Linux only stdout. A guest dup2 onto descriptor 1 or 2 already keeps the host streams first (#7807), and so does setvbuf (#10020); freopen did not, except for Linux stderr (#4792).

Changes, all in core/libs/prx/libc/include/FileStream.hpp:

  • FileStream::Reopen keeps the host stream before the guest's file replaces descriptor 1 or 2:
    • Windows: KeepHostStandardOutput_nid_no_patch() or KeepHostStandardError_nid_no_patch() when the stream is the C runtime's stdout or stderr;
    • Linux: KeepHostStandardOutput_nid_no_patch() for descriptor 1, next to the existing KeepHostStandardError_nid_no_patch() for descriptor 2.
  • On Windows, IsHostStandard() compares against __acrt_iob_func(0/1/2) instead of stdin/stdout/stderr. Inside libc those macros resolve, through the forced HostStandardStreams.hpp, to the kept host stream once a keep has run. After the first keep, a guest standard stream therefore no longer counted as standard, and a second freopen of it took the fclose path and closed the C runtime's stream. guest_standard_streams catches that.
  • Both Keep functions are declared on every platform. Before, KeepHostStandardError_nid_no_patch was declared outside Windows only.
  • Keeping the host stream is best effort on Linux: if a keep throws std::system_error (no free descriptor, no memory), Reopen restores errno and the guest's freopen goes ahead; only AnyPS5's own output then follows the guest's file. This covers the existing stderr keep too. On Windows a failed keep already returns -1 and is ignored.
  • The new Linux stdout keep is skipped under Emscripten, where KeepHostStandardOutput_nid_no_patch throws (General.cpp). A WebAssembly build keeps today's behaviour, and the WebAssembly entry in docs/dev/debt/libc.md now says that host stdout follows a guest freopen there.

docs/dev/GAME_TESTING.md: the line added by #13024 said run.log ends at the entry point for these titles. It now says that the title's own output, including its exit reason, is not in run.log; the pointer to app.log stays.

Test: new ctest guest_freopen_host_log (core/libs/tests/GuestFreopenHostLog.cpp, registered in core/libs/prx/libc/CMakeLists.txt, Windows and Linux).

  • It points descriptors 1 and 2 at a file standing in for the console, has the guest freopen its stdout and stderr to two files and write a line to each, then writes one host line to each stream.
  • It checks that each guest line is in its file, that no host line is in a guest file, and that both host lines reach the console file.

Second test, Linux only: guest_freopen_host_log_keep_failure, a mode of the same executable. In a fresh process it fills descriptors 3 and up under a lowered RLIMIT_NOFILE and closes descriptor 0, so the first stdout keep fails with EMFILE while the guest's file can still be opened. It checks that no exception leaves freopen, that freopen succeeds with the guest's line in its file, and that the host's stdout was not replaced.

Tested

All on main a5939f10a unless stated. The branch is now on f91137142e. Between the two, this code only gained the Emscripten guard, so on Linux I built libc.prx with and without it there: both builds are byte-identical, libc was recompiled for the second, and the test-only build differs from both. On f91137142e the test still fails 3/3 on the stdout checks without the fix, and the 9 related tests pass 5/5 with it.

After review, with the keep-failure catch, on f91137142e: guest_freopen_host_log_keep_failure fails 3/3 without the catch and passes 5/5 with it; Linux ctest -E agc 1872/1872, Windows 1954/1954.

test only (no fix) with this change
Windows 11 Pro 10.0.26200, i5-12600KF, WinLibs GCC 15.2.0 r7, Release: guest_freopen_host_log fails 3/3, all four host checks passes 5/5
Windows: 10 related tests (guest_freopen_host_log, guest_freopen_host_standard, the five guest_standard_streams*, guest_host_standard_error_windows, guest_host_standard_buffering_windows, guest_stdio_variadic) the other 9 pass pass 5/5
Windows: ctest -E agc — 1942/1942
Pop!_OS 24.04 LTS, GCC 13.3.0, Release: guest_freopen_host_log fails 3/3, stdout checks only passes 5/5, with the 8 other related tests that build on Linux
Pop!_OS: ctest -E agc — 1859/1859 (27 video-out tests skipped: no display)
  • Deliberate breaks, Windows:

    • removing the two Windows keep lines: guest_freopen_host_log fails 3/3 on all four host checks;
    • putting back the old IsHostStandard(): guest_standard_streams aborts 3/3 (0xc0000409).
  • Deliberate break, Linux: removing the new stdout keep makes guest_freopen_host_log fail 3/3 on the stdout checks.

  • Restoring the file byte-for-byte passes again on both. Build logs show libc relinked in every step.

  • Real titles, Windows, RTX 4060: Trophy Manager (PPSA99842) and PSCord (PPSA99190), each converted once and run in two copies that differ only in the libraries (test only / this change), with APS5_PROFILE_DRAW=1. All runs were still running when stopped.

    10 minutes, main 66b8e907e:

    console lines [present] on console AnyPS5 lines in the title's app.log
    Trophy Manager, test only 3 0 59 [present], 45 [src/…], 2 [pipeline-cache], plus [recipe], [draw-cache]…
    Trophy Manager, this change 1833 59 none
    PSCord, test only 3 0 58 [present], 202 [src/…], 2 [pipeline-cache]…
    PSCord, this change 2279 58 none

    With the change, the console and app.log share no line tag; app.log holds only the title's own lines ([TROPHY], [HUI], [ps5-gallium], [dc]…).

    120 s with ANYPS5_LOG_FILE (feat(libc): copy the runtime log into a file with ANYPS5_LOG_FILE #12715), main a5939f10a:

    console lines copy file lines AnyPS5 lines in app.log
    Trophy Manager, test only 3 4 11 [present], 45 [src/…]…
    Trophy Manager, this change 413 414 none
    PSCord, test only 3 4 11 [present], 142 [src/…]…
    PSCord, this change 571 572 none

    The extra line in each copy is its [log] ANYPS5_LOG_FILE: appending… header; every tag on the console is in the copy. This is also a first run of ANYPS5_LOG_FILE on Windows, which docs/user/USAGE.md lists as compiled but not run.

  • tools/check_conventions.py --base origin/main: passes.

Not tested:

  • a real title on Linux, and ANYPS5_LOG_FILE on Linux;
  • macOS (not built);
  • a WebAssembly build (not built; the guard only removes the new line there);
  • a failing fdopen in the Linux keep (ENOMEM), and a failing stderr keep; both take the same catch as the tested stdout case;
  • on Windows, a failed freopen of stdout or stderr after an earlier keep now detaches the C runtime's stream instead of closing it; the existing closed-stream tests pass, but there is no test for this case.

Checklist

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@medazizktata Reviewed at e5671a8. Please handle preservation failure at the new Linux stdout keep before reopening. FileStream.hpp:242 calls KeepHostStandardOutput_nid_no_patch; General.cpp:623-632 throws std::system_error on fcntl/fdopen failures such as EMFILE or ENOMEM. freopen_nid_postfix catches bad_alloc and filesystem_error, so this exception escapes the guest C ABI. The PR's failure limitation describes an ignored -1, but these failures throw instead. Please use an explicit failure policy that does not let a new host preservation failure escape as a C++ exception, and add a bounded failure-injection regression for the first stdout keep (guest outcome and host-stream state). Keep the normal guest-file/host-console separation test. No requirement to exhaust the review host's descriptor table.

@github-actions github-actions Bot added the fix error Closes an issue (Fixes #N) label Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 10755 core/libs/prx/libc/include/FileStream.hpp L193, L236
PR 12682 core/libs/prx/libc/include/FileStream.hpp L61-62, L193, L236

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 6085 core/libs/prx/libc/CMakeLists.txt
PR 7081 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7386 docs/dev/debt/libc.md
PR 7447 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 7715 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8150 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8965 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 9340 docs/dev/debt/libc.md
PR 9371 docs/dev/debt/libc.md
PR 9447 docs/dev/debt/libc.md
PR 9526 docs/dev/debt/libc.md
PR 9863 docs/dev/debt/libc.md
PR 10324 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 10405 core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 10729 docs/dev/debt/libc.md
PR 10780 docs/dev/debt/libc.md
PR 11332 docs/dev/debt/libc.md
PR 12057 core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 12363 docs/dev/debt/libc.md
PR 12688 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 12700 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 12920 core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md
PR 12965 core/libs/prx/libc/CMakeLists.txt
core/libs/prx/libc/include/FileStream.hpp
docs/dev/debt/libc.md

@adriadam10 adriadam10 self-assigned this Oct 30, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the approach and the shared-console test look right. One blocker, already raised by @oneandonlydean and not yet addressed:

  • FileStream.hpp:197-198 and :242 call KeepHostStandardOutput_nid_no_patch() / KeepHostStandardError_nid_no_patch() from Reopen. On Linux KeepHostStandardOutput_nid_no_patch (General.cpp:623-632) throws std::system_error on fcntl/fdopen failure (EMFILE, ENOMEM), it does not return -1. freopen_nid_postfix only catches bad_alloc and filesystem_error, so a failed keep escapes the guest C ABI. The "Not tested: a failing Keep returns -1" note in the body does not match the code.
    Please choose an explicit policy (for example catch the keep failure in Reopen and have the freopen fail with the errno, or continue without the keep) so no new exception can leave freopen, and state it in the PR.
  • Add a bounded regression for the failing first stdout keep that checks the guest outcome (return value, errno) and the host stream state afterwards. There is no need to exhaust the descriptor table; a hook or a closed descriptor 1 is enough.

Keep the existing guest-file/host-console separation test. Happy to re-review once pushed.

@medazizktata

Copy link
Copy Markdown
Contributor Author

@oneandonlydean @adriadam10 Thanks, you're both right: on Linux the keep throws, and my "returns -1 and is ignored" only held on Windows. The stderr keep from #4792 throws the same way.

Pushed 4cf81e1:

  • Failure policy (Linux Reopen): keeping the host stream is best effort. If either keep throws std::system_error, Reopen restores errno and the guest's freopen goes ahead unchanged; only AnyPS5's own output then follows the guest's file, as before this PR. I didn't make freopen fail instead, because the keep needs a descriptor that FreeBSD's freopen doesn't, so the guest would see a failure the console never gives. This is the same as Windows, where a -1 from the keep is already ignored.
  • Regression: guest_freopen_host_log_keep_failure (Linux only, a new mode of the same test executable). It runs in a fresh process, so the stdout keep it hits is the first one.
    • It lowers RLIMIT_NOFILE to 64 and fills descriptors 3 and up with /dev/null (at most 64 opens).
    • It closes descriptor 0, so the guest's file can still be opened while the keep's F_DUPFD_CLOEXEC from 3 fails with EMFILE.
    • It then reopens the guest stdout and restores everything.
    • Checks: the table was full; no exception left freopen; freopen returned the stream and the guest's line is in its file; the host's stdout was not replaced. errno isn't asserted: under this policy freopen succeeds, and errno is unspecified on success.
  • The normal guest-file/host-console test is unchanged.

Tested on main f91137142e:

this PR's previous head with the catch
Pop!_OS 24.04, GCC 13.3.0: guest_freopen_host_log_keep_failure fails 3/3: the exception escapes, freopen doesn't return the stream, the guest's line is missing passes 5/5, with the 9 related tests
Pop!_OS: ctest -E agc — 1872/1872
Windows (test not registered there): 10 related tests, ctest -E agc — pass 5/5; 1954/1954

Deliberately breaking the test so it stops filling the table after one descriptor makes it fail 3/3 on "the descriptor table was not filled" and "the host standard output was replaced". Restored, it passes.

Not tested: a failing fdopen (ENOMEM), which takes the same catch. The stderr keep failing is covered by the same try but has no test of its own.

@oneandonlydean oneandonlydean self-assigned this Oct 30, 2026

@malekaifa malekaifa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 4cf81e1d1d4aebae35b813dd8dc89a118d72beba.

The earlier keep-failure blocker is addressed in the current source: on non-Windows, Reopen catches std::system_error from preserving stdout/stderr, restores the prior errno, and continues with the documented best-effort policy instead of allowing an exception to escape the guest freopen ABI. The Linux-only regression exercises that path. On Windows, using the raw UCRT standard-stream identity for IsHostStandard() is consistent with keeping host logging on the preserved duplicate while allowing repeated guest reopens; the existing standard-stream suite covers successful redirect, failed reopen, closed/reopened stdout, and update mode.

Independent Windows 11 / WinLibs GCC 15.2.0 Release validation on this exact head:

  • built guest_freopen_host_log_tests cleanly;
  • guest_freopen_host_log: 5/5 consecutive passes;
  • built guest_standard_stream_tests and guest_freopen_host_standard_tests cleanly;
  • all five guest_standard_streams* cases: 3 repetitions each, 15/15 passes;
  • guest_freopen_host_standard: 3/3 passes;
  • tools/check_conventions.py --base origin/main: pass;
  • git diff --check origin/main...HEAD: pass;
  • git merge-tree --write-tree HEAD origin/main against current main 16345cad6a: clean.

Hosted checks on the exact head currently show Windows, conventions, library portability, report, and relinker jobs green; macOS write-watch jobs are still pending. I did not independently execute the Linux keep-failure test, so that part of this review is source-level plus the submitted regression rather than a local Linux run.

No remaining blocker found in this change.

AI-assisted: yes (OpenAI ChatGPT).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix error Closes an issue (Fixes #N)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libc: after a guest freopen of stdout or stderr, AnyPS5's own log goes into the title's file (Windows: both streams; Linux: stdout)

4 participants