Skip to content

fix(libkernel): return EFAULT from kevent for an unreadable changelist or timeout or an unwritable eventlist - #12503

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
babyhuey:fix/kevent-efault
Oct 29, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
babyhuey:fix/kevent-efault

Conversation

@babyhuey

@babyhuey babyhuey commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

kevent read the changelist and the timeout and wrote the eventlist in place, so a non-null pointer into unmapped or read-only memory crashed the host. FreeBSD's sys_kevent copies the timeout in first, kqueue_register copies each change in, and kqueue_scan copies events out, returning EFAULT for any failed copy.

  • The timeout is copied in with GuestCopyIn_nid_no_patch before the changes are applied; its values are still validated where they were.
  • Each change is copied in before it's registered, and each EV_ERROR / EV_RECEIPT entry is copied out with GuestCopyOut_nid_no_patch.
  • Triggered events come from the same single GetTriggeredEvents / WaitForEventsUntil call as before, now into a host buffer (on the stack up to 64 events, on the heap above that, and at most 1024 events whatever nevents asks for), and are copied out once. kevent may return fewer events than requested; any left in the queue come back on the next call. Keeping one queue call matters: level-triggered events are requeued after they're returned, so collecting in several calls would return them twice in one kevent.
  • A null eventlist keeps its existing behaviour (an 8-event scratch buffer, EFAULT if anything triggered).

Tested

BadPointersFaultAndBatchesFill in core/libs/tests/GuestKqueue.cpp, run last in main: with 12 level-triggered user events, polls with room for 16, 100 (the heap path), 9 and 5 return 12, 12, 9 and 5, and the 16-slot poll reports all 12 idents once. An unmapped changelist, timeout or eventlist fails with EFAULT; a read-only eventlist fails with EFAULT both for triggered events and for an EV_ERROR entry, and is left unchanged. A request for INT_MAX events on the 16-slot buffer returns the 12 pending events, and a queue of 1,030 events returns 1,024 per call.

  • Linux (anyps5-build CI image, Ubuntu 24.04, GCC 13, Release, BUILD_TESTING=ON), this head e73a56f8aba on main d4cb89300fe: guest_kqueue passes; the full ctest passes (2,712 of 2,712), and check_conventions.py passes. With main's Kqueue.cpp the new test segfaults.
  • Windows (Windows 11, Ryzen 7 5800X, RTX 3080, WinLibs GCC 15.2, Release, BUILD_TESTING=ON), this head: full build and libs succeed; guest_kqueue passes 3 of 3 alone, and guest_equeue_events passes alone; with main's Kqueue.cpp the new test crashes (0xc0000005). CI's test selection for this change (2,364 tests): 1,635 pass, 726 skip, and three failures unrelated to this change: guest_raise_exception, which also fails on main on this machine (libkernel (Windows): guest_raise_exception slowed about 3x and sometimes aborts at "address waking" since 540364a24 (#10666) #11813), and two load timeouts under --parallel 4, check_main_sync (120 s) and select_tests_SelectArgumentTests (30 s), which pass 3 of 3 alone on this head in at most 49 s and 15 s. check_conventions.py passes.

Cost. Each checked copy is a host system call. A kevent poll with 4 events ready goes from about 98 ns on main to about 2,800 ns here (Linux, same build). That's measured and discussed with the same question for clock_gettime on #12418; until that's settled this shouldn't merge.

The test function sits before CreateAndClose and is called after Timeout, away from the spots where #9575 and #10014 add tests; both of their current heads merge with this one without conflicts.

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

…t or timeout or an unwritable eventlist

kevent read the changelist and the timeout and wrote the eventlist in
place, so a non-null pointer into unmapped or read-only memory crashed
the host. It now copies the timeout in first, as FreeBSD's sys_kevent
does, copies each change in with GuestCopyIn_nid_no_patch and copies
EV_ERROR entries out one at a time. Triggered events are collected by
the same single queue call as before into a host buffer (on the stack
up to 64 events) and copied out with GuestCopyOut_nid_no_patch, so
level-triggered events are still reported once per call. Any failed
copy returns EFAULT.

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

I found one blocking issue in the new staging buffer. capacity is the caller-controlled nevents value after receipt slots are deducted, and when it exceeds 64 this allocates new KernelEvent[capacity] before asking the queue how many events are actually available. A guest can request INT_MAX events with a valid eventlist pointer and force an allocation proportional to roughly two billion KernelEvents; on constrained hosts that can throw std::bad_alloc out of this C ABI entry point or consume a very large virtual/committed address range. The queue may have only one pending event, so this cost is not tied to useful results.

Please bound the temporary buffer independently of the guest's nevents or handle allocation failure as a guest-visible error, and add a regression for an oversized nevents request that does not require allocating the full requested capacity. I have not run the focused guest_kqueue test locally: the full build configuration currently stops because the installed system Vulkan headers lack VK_KHR_maintenance8; your PR description reports Linux/Windows focused runs. I have not approved this PR.

@github-actions

github-actions Bot commented Oct 28, 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 10016 core/libs/tests/GuestKqueue.cpp 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 7088 core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
PR 7121 core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
PR 9843 core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp

@MarcZX10

Copy link
Copy Markdown
Contributor

The overlap bot identifies concurrent edits in the GuestKqueue.cpp insertion area from open PRs #9575 and #10014. The repository rule asks authors to agree on merge order for overlapping lines and record a Depends on relationship when needed. Could you coordinate with those authors and update the checklist if one branch needs to land first? I have not assumed the test insertions will compose cleanly.

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

The EFAULT handling matches FreeBSD's sys_kevent / kqueue_register / kqueue_scan, and the single queue call is the right call for level-triggered events. One blocking problem, the same one @MarcZX10 raised:

  • core/libs/prx/libkernel/Equeue/src/Kqueue.cpp:645: new KernelEvent[capacity] is sized by the guest's nevents (up to INT_MAX, about 64 GB) before the queue says how many events exist. That throws std::bad_alloc out of a C ABI entry point, or commits a huge range for one pending event. Bound the staging buffer independently of nevents. kevent may return fewer events than requested, and the events left in the queue are returned by the next call, so clamping capacity (for example to 1024) is enough. Alternatively, loop over fixed-size chunks only if you can keep the single-call requeue behaviour.
  • Add a case to BadPointersFaultAndBatchesFill with a valid eventlist and a very large nevents (for example INT_MAX on the 16-slot buffer's address, or the clamp boundary) showing it returns the pending events without a huge allocation.

The overlap bot lists #9575 and #10014 touching the same test insertion spot; that is only adjacency, so rebase when one lands.

The staging buffer was sized by the guest's nevents, so a request for
INT_MAX events allocated about 64 GiB before the queue reported how many
were pending. It's now bounded at 1024 events; kevent may return fewer
than requested, and events left in the queue come back on the next call.
The test covers an INT_MAX request and a 1030-event queue, and moves away
from the end of StandardInput and the start of main, where boykopovar#9575 and
boykopovar#10014 also add tests.
@babyhuey

Copy link
Copy Markdown
Contributor Author

@MarcZX10 @adriadam10 thanks, both right. e73a56f8aba bounds the staging buffer at 1024 events whatever nevents asks for (stack up to 64, heap above), as adriadam10 suggested; kevent may return fewer, and the rest come back on the next call. The test now covers an INT_MAX request on the 16-slot buffer (returns the 12 pending events) and a 1,030-event queue (returns 1,024 per call).

On the overlap with #9575 and #10014: I moved the test function to before CreateAndClose and its call to after Timeout, away from where both of them add tests, and both of their current heads now merge with this one without conflicts, so neither has to wait on this.

Linux and Windows results for this head, with the negative control, are in the description. One more thing I found while measuring: the checked copies make a kevent poll about 28 times slower (98 ns to ~2,800 ns). That's the same question as clock_gettime on #12418, so I've noted there that this one shouldn't merge until it's settled.

AI-assisted: yes (Claude Code)

@JJenkx

JJenkx commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Windows run of this head (e73a56f8ab), merged onto main 3a2b919816 (clean merge, local only). Windows 11 Enterprise 10.0.22621, Ryzen 9 5950X, WinLibs GCC 15.2.0 UCRT r7, Ninja, Release, BUILD_TESTING=ON, no Vulkan driver visible.

  • This PR: guest_kqueue passes 3 of 3 (ctest --repeat until-fail:3).
  • Negative control: with core/libs/prx/libkernel/Equeue/src/Kqueue.cpp put back to main's version and everything else from this PR, guest_kqueue fails.

AI-assisted: yes (Claude Code).

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

Re-checked — changes requested

The unbounded staging allocation is fixed and the added guest-memory fault coverage is useful. I still cannot approve the current head: the author’s own matched-path measurements show a substantial hot-poll regression, and the test addition still conflicts with open PR #10016.

What I ran

  • Reviewed exact head e73a56f against current main 52c09d5.
  • Ran git merge-tree --write-tree and git diff --check against current main.
  • Compared the current diff with open #7088, #7121, and #10016. The #10016 test insertion still conflicts in GuestKqueue.cpp; the implementation hunks I inspected for #7088 and #7121 are in separate registration/modify paths.
  • Inspected the bounded capacity logic and the author’s paired performance measurements. I did not run the local tests.

Results

Check Result
Current-main merge preview Clean
git diff --check Pass
Bounded staging Capacity is capped at 1,024 regardless of nevents; stack storage is used through 64
Author-reported Linux tests Full CTest 2,712/2,712, focused test passes, conventions pass; negative control segfaults with main Kqueue.cpp
Author/reviewer-reported Windows tests Focused guest_kqueue passes 3/3; negative control crashes with main Kqueue.cpp
Hosted checks Windows, conventions, eligibility, labels, and report pass; Linux, register-context, relinker, ASan, and write-watch jobs are skipped
Overlap preview Pairwise merge-tree with #10016 conflicts in GuestKqueue.cpp

Negative control

  • The author reports that the new test crashes when run with main’s Kqueue.cpp and passes with the PR implementation on Linux and Windows. I did not reproduce that locally.

Findings

Blocking

  1. Measured hot-path regression. The author reports a matched Linux measurement for four ready events: about 98 ns per call on main versus 2,773–2,861 ns on this head, roughly 28× slower. The author also explicitly concluded that #12503 should not merge with these guarded copies as-is, pending a fast-copy design decision. This is a measured cost on the operation’s polling path, not a theoretical concern. Please either address the per-event copy overhead without losing the EFAULT safety this PR adds, or get an explicit maintainer decision that accepts this measured cost before treating the change as ready.

  2. Unresolved overlap with #10016. The current overlap report identifies a conflict in GuestKqueue.cpp, and my pairwise merge-tree check still reports that conflict. The body says the test was moved away from #9575/#10014, but does not mention #10016. Please coordinate with that author and move/sequence the insertion so both changes can integrate cleanly; declare Depends on if an order is required.

I manually compared the current #7088 and #7121 hunks in Kqueue.cpp: they modify registration/modify paths, while this PR stages and copies results in kevent_nid_postfix, so I found no same-line dependency there.

Sources

Not verified

  • I did not independently rerun the Linux/Windows suites or reproduce the timing benchmark. Test and timing figures above are author/reviewer-reported.
  • The hosted product matrix is mostly skipped; the passing Windows check does not substitute for the skipped Linux and specialized jobs.

To get this to a good state

Resolve the measured copy cost with maintainers, coordinate the still-conflicting #10016 test insertion, and rerun focused tests and the paired benchmark on the integrated head.

@adriadam10
adriadam10 merged commit d820fef into boykopovar:main Oct 29, 2026
11 of 12 checks passed
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