Skip to content

fix(libc): open a directory with freopen in a read mode on Windows - #10433

Open
Retr0G28 wants to merge 2 commits into
boykopovar:mainfrom
Retr0G28:fix/freopen-directory
Open

Retr0G28 wants to merge 2 commits into
boykopovar:mainfrom
Retr0G28:fix/freopen-directory

Conversation

@Retr0G28

@Retr0G28 Retr0G28 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, freopen of a stream onto a directory in a read mode (r, rb, re) failed with EACCES, which is the gap #8550 left open. FreeBSD 11 freopen opens the path with _open (freopen.c), and vn_open_vnode refuses a directory only for FWRITE or O_TRUNC. #10251 measured on a PS5 (firmware 13.60) that freopen onto a directory in a read mode succeeds there too.

freopen now takes the same path fopen takes since #8550. For a read mode and a path that is a directory, it reopens the stream through OpenDirectoryHandle, which opens a libkernel directory descriptor and wraps it in a host FILE. The stream is then marked as a directory stream. OpenDirectoryHandle is the first half of the old OpenDirectoryStream, split out so that FileStream::Reopen can call it the way it calls OpenNative. fopen keeps calling OpenDirectoryStream, which still uses it.

The stream behaves as a directory stream from fopen does: a read fails with EISDIR and sets the error indicator, fstat on its fileno reports a directory, and fclose, or a second freopen, removes the descriptor from libkernel's table. A write or update mode keeps failing with EISDIR.

stdin, stdout and stderr keep their host descriptor numbers 0 to 2 across freopen, so they cannot hold a libkernel directory descriptor. freopen of one of them onto a directory in a read mode now throws instead of failing with EACCES. The debt entry says so, and its old sentence about EACCES is removed.

Test: guest_filesystem opens a file with fopen and reopens it onto a directory with r. fstat must report a directory, and fread must fail with EISDIR and set the error indicator. It then reopens the stream onto the file with rb. fstat of its descriptor must report a regular file, so the directory registration is gone, and the stream reads the file's data. It reopens the stream onto the directory with re and closes it. On Windows, open must then return the same descriptor number, as the fopen loop already checks. A reopen onto a directory with r+ must fail with EISDIR.

Tested

Windows 11, WinLibs MinGW-w64 GCC 15.2.0 r7, CMake + Ninja, Release, -DBUILD_TESTING=ON, on main a23f8fc.

  • With main's libc, the new guest_filesystem checks fail (exit 0xc0000409 from the failed Require). With this PR it passes.
  • guest_filesystem, file_streams, guest_freopen_host_standard, guest_fopen_long_host_path, guest_directory* and guest_file_lock_* pass. guest_filesystem, file_streams and guest_freopen_host_standard pass again on head 1e32d23, after the review follow-up.
  • python tools/check_conventions.py --base origin/main reports no errors.
  • Not run on Linux, where freopen already opened a directory and the new checks run unchanged. No title or homebrew run.

Overlap (tools/pr_overlap.py):

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions

github-actions Bot commented Oct 24, 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 9763 docs/dev/debt/libc.md L133

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 docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 6713 docs/dev/debt/libc.md
PR 7081 core/libs/prx/libc/src/FileAndHeap.cpp
docs/dev/debt/libc.md
PR 7201 core/libs/prx/libc/src/FileAndHeap.cpp
PR 7386 docs/dev/debt/libc.md
PR 7447 core/libs/prx/libc/src/FileAndHeap.cpp
docs/dev/debt/libc.md
PR 7544 docs/dev/debt/libc.md
PR 7648 docs/dev/debt/libc.md
PR 7715 docs/dev/debt/libc.md
PR 8150 docs/dev/debt/libc.md
PR 8417 docs/dev/debt/libc.md
PR 9371 core/libs/tests/GuestFilesystem.cpp
docs/dev/debt/libc.md
PR 9447 docs/dev/debt/libc.md
PR 9526 core/libs/prx/libc/src/FileAndHeap.cpp
docs/dev/debt/libc.md
PR 9669 docs/dev/debt/libc.md
PR 9929 docs/dev/debt/libc.md
PR 10007 docs/dev/debt/libc.md
PR 10319 core/libs/tests/GuestFilesystem.cpp
PR 10405 core/libs/prx/libc/src/FileAndHeap.cpp
docs/dev/debt/libc.md
PR 10467 docs/dev/debt/libc.md
PR 10742 docs/dev/debt/libc.md
PR 10755 core/libs/prx/libc/src/FileAndHeap.cpp
PR 10898 docs/dev/debt/libc.md
PR 10927 docs/dev/debt/libc.md

@MarcZX10 MarcZX10 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 the exact PR head 4080a15f229892ce756c1e53670eee4d8795cff7 against current main and the overlap notes. The change reuses the directory-handle path only for Windows read-mode freopen calls onto directories; write/update modes still fail, fopen retains its existing path, and the standard-stream limitation is recorded as debt. The regression checks directory fstat, EISDIR plus the error indicator on read, reopening the stream onto a regular file afterward, re mode, and update-mode failure.

The author reports the focused Windows tests on this head and a negative control against main; the required hosted checks pass. Linux product tests were not run by me, and the hosted Linux/Windows full-build jobs are skipped under repository policy. The #10251 overlap is documentation/measurement, while #9763 concerns adjacent closed-stream handling. I found no blocking issue.

@harisibrar1 harisibrar1 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.

Checked against main 11d6efa and FreeBSD 11.4 lib/libc/stdio/freopen.c:

  • This still merges cleanly with main. FileStream::Reopen on main already takes a (path, mode) open callback and already forgets the libkernel directory descriptor (std::exchange(directory, false)), so reusing OpenDirectoryHandle there fits well, and the split from OpenDirectoryStream keeps fopen's behaviour unchanged.
  • Checking is_directory before OpenNative gives the same result as fopen's "EACCES, then directory" order, since _wfopen never opens a directory on Windows.
  • Optional: the fopen loop checks on Windows that open reuses the directory's descriptor number after fclose. Doing the same after freopen(listed, "rb") and after the final fclose would cover the forget path in Reopen too.
  • Nit: the "fopen: libkernel has not registered…" message is now reached from freopen as well.

Reviewed with AI assistance (Claude Code); the checks above were run locally on my machine.

@Retr0G28

Copy link
Copy Markdown
Contributor Author

@harisibrar1 Thanks, I did both in 1e32d23.

  • After freopen(listed, "rb"), sceKernelFstat of the stream's descriptor must report a regular file, which shows Reopen dropped the directory registration. After the final reopen onto the directory and fclose, open on Windows must return the same number, as in the fopen loop.
  • The message now starts with OpenDirectoryHandle:, like the ForgetDirectoryStream: one next to it, so it is right from both callers.

guest_filesystem, file_streams and guest_freopen_host_standard pass on Windows.

@harisibrar1 harisibrar1 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.

Re-checked head 1e32d23 against main 6cd36e6:

  • After freopen(listed, "rb"), the test now checks that sceKernelFstat reports a regular file (GuestFilesystem.cpp:695). After the final reopen onto the directory and fclose, it checks on Windows that open reuses the same descriptor (:697-706). Together these cover the forget path in Reopen.
  • The error message now starts with OpenDirectoryHandle: (FileAndHeap.cpp:331), so it's right for both fopen and freopen.
  • It still merges cleanly into main. Thanks for the extra coverage, LGTM.

Reviewed with AI assistance (Claude Code); the checks above were run locally on my machine.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants