Repository navigation
fix(libkernel): fail sceKernelMprotect over an unmapped range with EINVAL instead of aborting - #5143
Conversation
db8b06a to
fecb9cb
Compare
…NVAL instead of aborting The allocation tracker threw a runtime_error (a hole in the protected range) from inside sceKernelMprotect, which terminated the title. mprotect already turned the exception into EINVAL. sceKernelMprotect and sceKernelMtypeprotect now check that the range is registered first and return EINVAL; the retype of direct mappings is skipped on failure. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
fecb9cb to
05ffb45
Compare
# Conflicts: # core/libs/prx/libkernel/CMakeLists.txt
CnRJay
left a comment
There was a problem hiding this comment.
Skipping the holes matches vm_map_protect for a range that starts in a mapping. One ordering difference, though: the new loop applies protection span by span, and a later span can still throw (RequireUnpinned, a host mprotect failure). The earlier spans then keep their new protection while the call fails.
FreeBSD's vm_map_protect works in two passes over the entries. The first pass checks every entry in the range (max_protection, submap) and returns KERN_PROTECTION_FAILURE before changing anything. Only the second pass applies the change. So a failed mprotect leaves the whole range unchanged. Collecting the spans (already done), running the checks that can fail (RequireUnpinned per span) for all of them first, and only then calling Protect would keep that property. Otherwise a title that retries after an error sees part of the range already changed.
On the evidence: as the description says, the PPSA03001 logs show the throw but not the call, and the remap path is the other caller of Protect. A stack (APS5_TRACE_*, or the crash report's guest frames) from one of those runs would confirm that it is sceKernelMprotect over a gap before this is counted as the fix for that title.
… protecting the first Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks, both points are right. Two passes: Evidence: I ran PPSA03001 on plain AI-assisted (Claude Code). |
|
Checked on Linux: scoped build and ctest green ( |
What
sceKernelMprotectover a range that is not completely registered in the allocation tracker (an unmapped gap inside or at the end of the range) threwguest protection or unmap range has a hole(or... is not registered) fromGuestAllocations::Protect; nothing caught it, so the title was terminated. The POSIXmprotectwrapper caught the exception and returned-1withEINVAL.FreeBSD 11.4's
vm_map_protecthas no check for holes: its loop runs over the map entries that exist, so the gaps of a range are skipped and the mapped parts are protected.DoMprotectnow does the same: a newGuestAllocations::ForRegisteredhands out the registered parts of the range,DoMprotectprotects each of them and returns 0; a range with no registered part at all returnsSCE_KERNEL_ERROR_EINVAL(mprotect:EINVAL), as the existingGuestMemory.cpptest for an unmapped start expects. FreeBSD does not answerEINVALthere: with nothing mapped in the rangevm_map_protectfinds no entry and returnsKERN_SUCCESS, whichkern_mprotectturns into 0 (both read inrelease/11.4.0); theEINVALstays because that test pins it, not because of the source.DoMtypeprotectreturned the result ofDoMprotectonly at the very end after retyping the direct mappings; it now returns early on failure (no test covers that failure path). Every part is first checked for a GPU or image pin (RequireUnpinned), so a pinned part fails the call before the first part changes; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (listed in the TechnicalDebt entry for libkernel together with the missing console measurement).GuestAllocations::Protectitself is unchanged and still throws on a hole;libSceAgcDriver/tests/GuestAllocations.cpp:159keeps pinning that.A range whose start is unmapped keeps answering
EINVALeven if later pages are mapped (pinned by the existingGuestMemory.cpptest). FreeBSD would take the next entry there (vm_map_protect:entry = entry->nextwhen the start lies in no entry) and protect the later pages, so only a gap inside or at the end of a range follows the FreeBSD source; the unmapped start is a deliberate difference.Not covered:
DoMprotectstill throwsinvalid_argumentfor address 0, length 0 or an overflowing range (first lines of the function), sosceKernelMprotect(NULL, ...)or a zero length ends the title as before (the POSIXmprotectwrapper catches it); a pinned range still throws fromRequireUnpinned; the guest-stack branch runs before the new code.Title evidence, and what is NOT shown: in saved runs of PPSA03001 the title stops with exactly
what(): guest protection or unmap range has a hole(11 run logs, 22 counted lines, e.g.PPSA03001_r/run_021704.err:4). The logs have no guest frame and not the arguments of the call, so that it is asceKernelMprotectover an unmapped gap is a hypothesis (the other caller ofProtect, the fixed remap path, checksCoversfirst). With this change such a call protects the mapped parts and returns 0; whether PPSA03001 then runs on is not tested.Tested
Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, first run on
main0f2683cb7(the re-run with the current head andmainis the bullet "Re-run on 14 October 2026" below):guest_mprotect_unmapped(new): three pages mapped, the middle one unmapped:sceKernelMprotect,mprotectandsceKernelMtypeprotectover all three return 0 andsceKernelVirtualQueryshows the protection changed on both sides of the gap; unaligned ranges inside and across the gap work; a range that is completely unmapped returnsEINVAL(22 formprotect); in a second mapping, a range that ends in an unmapped page protects the mapped page.DirectMemory.cpp(sceKernelMprotect threw: guest protection or unmap range has a hole, the message of the title run).guest_memorypasses.git merge-tree: feat(libkernel): count committed ranges in sceKernelGetPageTableStats #4725 conflicts only in the test registration ofcore/libs/prx/libkernel/CMakeLists.txt(both add a test there); feat(memory): track changes to mapped address ranges #1360 and perf(agc): cache page permissions within registered allocations #1330 already conflict withmainon their own; feat(libkernel): support sceKernelMprotect on foreign guest module images on Windows #5109 (sceKernelMprotecton foreign module images on Windows) editsIsGuestModuleImagein the same file for the Windows branch ofDoMprotect;git merge-treeof the two heads is clean (not built together), and the two changes look independent (it decides which images may be protected, this PR what happens to an unregistered gap inside a range); no other open PR touches the function. fix(libkernel): inherit floating-point environment on Windows threads #4981 (floating-point environment for Windows threads) conflicts with this PR indocs/dev/debt/libkernel.md(git merge-treeof the two heads, re-checked; each is clean againstmain30f961266); whichever lands second rebases the debt entry.-j4), aftermainfeddf7f4cwas merged into the head (merge commite528ac9c5; the only conflict was the two independent test registrations incore/libs/prx/libkernel/CMakeLists.txtnext toguest_memory,guest_mprotect_unmappedand the newguest_flexible_memory_*tests, both kept and built):guest_mprotect_unmappedpasses (0.03 s, ctest) together withguest_memory, the fiveguest_flexible_memory_*cases,guest_mlock,guest_kernel_sysandguest_filesystem(10 of 10, 0 failed); withDirectMemory.cpptaken back tomainand the new test kept it fails (sceKernelMprotect threw: guest protection or unmap range has a hole, exit code0xc0000409). The commit238fe1eec(pin check for every part before the first change, after a review) was built and run afterwards on the samemain: the same eight tests (guest_mprotect_unmapped,guest_memory, the fiveguest_flexible_memory_*,guest_mlock) pass (8 of 8); it has no test of its own, because a pinned part next to a free part needs a GPU lease that only the AGC driver tests create.python tools/check_conventions.py --base upstream/mainpasses. This replaces the earlier figures. Not run: the socket tests, Linux, a console, a title.docs/dev/debt/libkernel.md(git merge-treeof the two heads, re-checked againstmaince5c83953; both heads are clean againstmain); whichever lands second rebases the debt entry.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); no silent stub added.mdfiles or images added to the repositoryAI-assisted (Claude Code).
🤖 Generated with Claude Code