Skip to content

fix(libkernel): refuse read-only guest mappings for getdirentries on Windows - #7672

Merged
oneandonlydean merged 3 commits into
boykopovar:mainfrom
valenciana-nc:fix/getdirentries-guest-validation
Oct 19, 2026
Merged

oneandonlydean merged 3 commits into
boykopovar:mainfrom
valenciana-nc:fix/getdirentries-guest-validation

Conversation

@valenciana-nc

@valenciana-nc valenciana-nc commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

sceKernelGetdirentries now guards its buf and basep outputs with GuestArena::HostWrite in both the Windows and Linux branches of core/libs/prx/libkernel/File/src/Stdio.cpp. This matches the other output-buffer syscalls in that file. Both guards stay open until the call returns. They open after the descriptor checks, so a bad descriptor still fails with EBADF or EINVAL before EFAULT. FreeBSD 11.4's kern_getdirentries also checks the descriptor before reading.

What this does: on Windows, a buf or basep in a read-only guest mapping (for example, a read-only direct-memory mapping) now fails with EFAULT (0x8002000e). Previously the process crashed with an access violation.

What this does not do:

  • There is no guest-arena containment check. A host heap buffer is still written, and a reserved but uncommitted arena address still faults. HostWrite only refuses ranges that overlap a non-writable shared view; it does not establish that the pointer is in the guest arena.
  • Linux behaviour is unchanged for this case. GuestArenaBeginHostWrite always returns true there, so the read-only mapping still crashes on Linux. On Linux the guard only reports the write to the write watch.

Tested

Merged at 950eeb847 on 2026-10-19. The subsequent fresh-descriptor test improvement is tracked separately in #7892; it was not part of this merge.

Each build is this branch merged onto main a373e85e5, Release, BUILD_TESTING=ON.

  • Windows 11 (26300), WinLibs GCC 15.2.0 r7:
    • The 463 guest_* tests registered under core/libs: all pass, the same as main. guest_video_out_wqhd_detection_no_vulkan is skipped on both.
    • That includes guest_directory_buffer, guest_directory_rewind and guest_directory_close.
    • With Stdio.cpp from main, the original regression test fails at its read-only output check. As @wachi35676 clarified, the buf call was already at EOF; the distinct basep probe is the one that demonstrates the access violation. test(libkernel): exercise read-only getdirentries buffers before EOF #7892 opens a fresh descriptor so the buf check also exercises the crash.
  • Linux, Ubuntu 24.04 container (Docker Desktop, WSL2 kernel 6.6), GCC 14.2:
    • 417 of 419 guest_* tests pass, including guest_directory_entries, guest_filesystem, guest_kernel_sys and guest_kernel_app_archive_*.
    • The two failures, guest_system_load_exec_standard_error and guest_named_shm_concurrency, fail on main in the same container too; it runs as root.

CheckReadOnlyDestination in core/libs/tests/GuestDirectoryBuffer.cpp is part of the Windows-only test binary. It covers:

  • A writable direct-memory buf succeeds.
  • The same mapping made read-only fails with EFAULT, both as buf and as basep.
  • A bad descriptor with the read-only buf returns EBADF.

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 18, 2026 •

Copy link
Copy Markdown

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/libkernel/File/src/Stdio.cpp
PR 6044 core/libs/tests/GuestDirectoryBuffer.cpp
PR 6879 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6936 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6942 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6977 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 7121 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 7201 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 7299 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 7332 core/libs/prx/libkernel/File/src/Stdio.cpp

@LeandroLP23

LeandroLP23 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Windows build and run, since the description has a syntax check only. Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), Windows 11 26300, WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON. Build: main 8a456334f with #7677, #7673, #7671, #7666, #7676, #7675, #7672, #7668, #7664 and #7658 merged together (each merge clean, built once).

The build is clean. The full guest_* run on that build: 474 of 476 pass, with guest_video_out_wqhd_detection_no_vulkan skipped. The 2 failures, guest_sysarch and guest_offline_imports, come from #7658 in the same build: they fail with #7658 alone and pass without it. I didn't check which of them reach the changed function, and no test checks the new buffer validation itself.

AI-assisted

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

Linux run, since the description has a syntax check only and the existing run is Windows.

Machine: Linux x86_64, i7-8700, Ubuntu 26.04, GCC 15.2, Release, BUILD_TESTING=ON, full build.
Head 7709eda9a merged onto main 84c1a4d9a (clean merge).

  • Builds. The directory/filesystem tests pass (ctest -R "director|dirent|getdents|filesystem|kernel_sys", 8 tests), and the non-GPU suite (ctest -E "^agc|gpu|vulkan|video_out") is 739/739, same as main.
  • The new check does not fire on Linux. Outside Windows, GuestArenaBeginHostWrite_nid_postfix (GuestArena.cpp) only notifies the write watch and returns true, so HostWrite::Open() is always true and the EFAULT return is unreachable. A small probe (each call in a forked child, fresh descriptor per call, on an empty directory) gives the same result with and without this PR:
    • valid buf and basep: returns 24;
    • buf = 0x1000 (unmapped, outside the arena): SIGSEGV;
    • basep = 0x1000: SIGSEGV.

So on Linux this changes nothing, and the "forged pointer fails with EFAULT" part of the description holds only where BeginHostWrite rejects the range. @MarcZX10 raised the same point about HostWrite::Open() for #7675, which uses the same guard.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@wachi35676

Copy link
Copy Markdown
Contributor

Tested at 7709eda9a, built on Windows (GCC 15.2) and Linux (gcc 13.3) on main 8a456334f. The guest_directory* tests pass (4 on Windows, 2 on Linux).

The change helps on Windows. With buf (or basep) in a read-only direct-memory mapping (sceKernelMapDirectMemory, prot 1), main crashes with unhandled exception 0xc0000005 … write of 0x0000000200000000 in libkernel.prx, and this branch returns 0x8002000e.

The description says more than the change does, though. GuestArena::HostWrite only refuses ranges that overlap a non-writable shared view; it doesn't check that the pointer is in the guest arena. With this branch:

  • a host heap buffer (GuestArenaContains = 0) is still written, and 44 bytes are returned;
  • a reserved but uncommitted arena address still crashes with an access violation;
  • on Linux GuestArenaBeginHostWrite always returns true, so the read-only mapping still dumps core there.

Could the title and description say "refuses read-only guest mappings on Windows", and could the PR add a test for that case?

AI-assisted: yes (Claude Code); results above were run on this machine.

sceKernelGetdirentries/sceKernelGetdents wrote up to nbytes into buf
and 8 bytes into *basep with only null checks, unlike sceKernelRead,
pread and readv which all gate on GuestArena::HostWrite. Validate both
buffers the same way so a forged pointer fails with EFAULT instead of
writing to an arbitrary host address.
@valenciana-nc
valenciana-nc force-pushed the fix/getdirentries-guest-validation branch from 7709eda to 25d26b5 Compare October 19, 2026 00:22
@valenciana-nc valenciana-nc changed the title fix(libkernel): validate getdirentries output buffers in the guest arena fix(libkernel): refuse read-only guest mappings for getdirentries on Windows Oct 19, 2026
@valenciana-nc

Copy link
Copy Markdown
Contributor Author

Thanks for the independent build and test run — that was genuinely useful.

You're right that the description claimed more than the change does, so I've narrowed it: the PR is retitled to fix(libkernel): refuse read-only guest mappings for getdirentries on Windows, and the body now states the non-goals explicitly (no arena-containment check, Linux behavior unchanged).

I've also added the test you asked for: CheckReadOnlyDestination in core/libs/tests/GuestDirectoryBuffer.cpp (the Windows-only test binary, since the read-only refusal only manifests on Windows). It maps a page read/write via direct memory, verifies getdirentries succeeds into the writable mapping, then mprotects it read-only and asserts SCE_KERNEL_ERROR_EFAULT for both a read-only buf and a read-only basep.

I couldn't execute the test suite in this environment (no Windows toolchain here), so CI will have to run it — your finding that unpatched main crashes on this case is cited in the PR as the regression property the test pins down.

@wachi35676

Copy link
Copy Markdown
Contributor

Ran CheckReadOnlyDestination at 25d26b54a on Windows 11 (MinGW GCC 15.2), merged onto main ad502d717.

  • Windows: guest_directory_buffer, guest_directory, guest_directory_rewind and guest_directory_close pass. With Stdio.cpp from main, guest_directory_buffer fails at line 114.
  • Linux (gcc 13.3): guest_directory and guest_directory_entries pass, and a read-only mapping still dumps core with and without the PR, as the description now says.

One detail in the test: when the read-only buf call runs (line 114), the first call has already read the directory to the end. On main that call returns 0 and writes nothing, so the test fails on the return value rather than on the crash. The basep case is the one that crashes main (unhandled exception 0xc0000005 … write of 0x0000000200000000).

Opening a fresh descriptor, or rewinding, before the read-only buf call would cover that crash too. With this PR, a fresh descriptor also returns 0x8002000e.

AI-assisted: yes (Claude Code); results above were run on this machine.

@valenciana-nc

Copy link
Copy Markdown
Contributor Author

Updated in 950eeb8:

  • The basep guard was scoped to its if block, so it closed before *basep was written. Both guards now stay open until the call returns.
  • The guards now open after the descriptor checks, so a bad descriptor reports EBADF/EINVAL before EFAULT, as kern_getdirentries checks the descriptor first.

Built and ran on Windows and Linux; details are in the description.

  • On Windows, CheckReadOnlyDestination passes, and with Stdio.cpp from main it crashes.
  • Linux behaviour is unchanged, as described.

@oneandonlydean oneandonlydean self-assigned this Oct 19, 2026
@oneandonlydean
oneandonlydean merged commit b106f6f into boykopovar:main Oct 19, 2026
11 of 12 checks passed
@valenciana-nc

valenciana-nc commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

@wachi35676 thanks — #7672 merged at 950eeb847 while I was preparing the fresh-descriptor adjustment, so that improvement did not enter this merge. It is now a separate test-only follow-up: #7892 (413b1237b), based on the merge commit b106f6f8c.

The follow-up closes the descriptor after the writable control, opens a fresh one before the read-only buf call, then verifies entries remain unread after both refused outputs.

All four Windows directory tests pass. With only Stdio.cpp restored from pre-fix 8982791bc, the updated test segfaults writing to 0x200000000 in the read-only buf call, so it covers the crash rather than the EOF return. No runtime changes in the follow-up.

AI-assisted: yes.

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.

5 participants