Conversation
A pidfd whose exit monitor could not be set up was completed through proc_pidfd_notify_exit, which matches by guest pid, so every other pidfd on the same live target also reported an exit. Other monitor failures completed nothing: a kqueue() failure, or the wait returning an error, left the fd unreadable forever. Each open now carries a generation number, and a failed monitor completes only the entry holding its own. The generation, not the guest fd, names the open, since the guest can close the fd and reuse the number before the failing monitor takes the lock. A registration failure with ESRCH still completes every pidfd on the target, because it means the target has exited. The wait retries on EINTR.
| int guest_fd; | ||
| int64_t guest_pid; | ||
| int write_end; | ||
| uint64_t gen; /* names this open across guest fd number reuse */ |
There was a problem hiding this comment.
pidfd_cleanup still finds its entry by guest_fd alone, so the reuse race this gen closes for the monitor is still open on the close path. fd_snapshot_and_close marks the slot closed under fd_lock, and then fd_cleanup_entry runs pidfd_cleanup with no lock held. A sibling thread's pidfd_open can get the same fd number in between, and if pidfd_find_free_entry places the new entry at a lower index, the old fd's cleanup tears down the new pidfd and leaves the stale entry active, so pidfd_send_signal on the new fd signals the old target. A follow-up could store the gen in the fd table entry (or pass it through the snapshot) and have cleanup match (guest_fd, gen).
There was a problem hiding this comment.
Confirmed: fd_snapshot_and_close releases fd_lock before fd_cleanup_entry calls pidfd_cleanup, and the lookup by guest_fd alone then picks the lower-index entry. Since the cleanup callback only receives the fd number, carrying gen there means storing it in fd_entry_t or passing the snapshot through, which reaches beyond this file. Filed as #403.
|
|
||
| int kq = kqueue(); | ||
| if (kq < 0) | ||
| if (kq < 0) { |
There was a problem hiding this comment.
The description says a guest cannot make kqueue or pthread_create fail, but closing a pidfd never stops its detached monitor. A guest that repeatedly opens and closes a pidfd on a long-lived child leaves one thread blocked in kevent plus one kqueue fd behind per open. The table holds only 32 active entries, but closing frees a slot, so these leaks have no limit. That makes these failure paths reachable from the guest, and that is the case that deserves a test. A follow-up could give each monitor a cancel path from pidfd_cleanup (for example an EVFILT_USER event on its kqueue that cleanup triggers) so a close also tears down the monitor.
There was a problem hiding this comment.
Right, the description was wrong on that point; the monitor leak in #400 makes these paths reachable, and I have corrected it. A test that drives the leak until kqueue() fails stops being possible once #400 is fixed, so I would pair the test with that fix: open and close pidfds on a live child in a loop and check the host thread count stays flat.
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/syscall/proc-pidfd.c">
<violation number="1" location="src/syscall/proc-pidfd.c:126">
P3: The `EV_ADD` registration is not retried on `EINTR`, but the blocking wait below it is. macOS kevent(2) documents that when a change-list call fails with `EINTR`, all changes have already been applied — so an `EINTR` here means the `EVFILT_PROC` filter is installed, yet this path closes `kq`, discards the working monitor, and completes only this pidfd while the target may still be alive. Retry the registration on `EINTR` (EV_ADD is idempotent) like the wait loop does; nothing else in this block changes.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * on it should see. Any other failure says nothing about the target, so | ||
| * only this fd is completed. | ||
| */ | ||
| bool gone = errno == ESRCH; |
There was a problem hiding this comment.
P3: The EV_ADD registration is not retried on EINTR, but the blocking wait below it is. macOS kevent(2) documents that when a change-list call fails with EINTR, all changes have already been applied — so an EINTR here means the EVFILT_PROC filter is installed, yet this path closes kq, discards the working monitor, and completes only this pidfd while the target may still be alive. Retry the registration on EINTR (EV_ADD is idempotent) like the wait loop does; nothing else in this block changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/syscall/proc-pidfd.c, line 126:
<comment>The `EV_ADD` registration is not retried on `EINTR`, but the blocking wait below it is. macOS kevent(2) documents that when a change-list call fails with `EINTR`, all changes have already been applied — so an `EINTR` here means the `EVFILT_PROC` filter is installed, yet this path closes `kq`, discards the working monitor, and completes only this pidfd while the target may still be alive. Retry the registration on `EINTR` (EV_ADD is idempotent) like the wait loop does; nothing else in this block changes.</comment>
<file context>
@@ -84,23 +111,38 @@ static void *pidfd_monitor_thread(void *arg)
+ * on it should see. Any other failure says nothing about the target, so
+ * only this fd is completed.
+ */
+ bool gone = errno == ESRCH;
close(kq);
- proc_pidfd_notify_exit(gpid);
</file context>
There was a problem hiding this comment.
The sentence about changes being applied on EINTR is from the FreeBSD page; the macOS kevent(2) page does not say it. This call passes nevents = 0, so it returns without waiting and has nothing for a signal to interrupt, and elfuse blocks its async signals on all threads except the sigwait thread. The final wait is retried because it does block.
Follow-up to the review on #392.
pidfd_createcompleted a pidfd whose monitor thread could not start by callingproc_pidfd_notify_exit(target_pid). That matches by guest pid, so every other pidfd already watching the same live target also reported an exit. On the monitor side, akqueue()failure returned without completing anything, which left the fd unreadable forever, the case the fallback exists to prevent. The same held when the finalkevent()wait returned an error.Each pidfd now gets a generation number when it is created, and a monitor that fails completes only the entry with its own generation. The generation, and not the guest fd, identifies the open, because the guest can close the fd and reuse the number before the failing monitor takes the lock. A registration failure with
ESRCHstill completes every pidfd on the target, since it means the target has already exited. The final wait now retries onEINTR.These paths are reachable through the monitor leak in #400. A test belongs with the fix for #400, which removes that leak.
Rebased on
mainat bcf6c5a.make check: all sections pass.make test-matrix:test-pidfdandtest-pidfd-targetspass. Two failures, both also present on unmodifiedmain:audit-known-limitationsin the Rosetta audit (anrt_sigreturnassertion inside Rosetta, failing 3 of 3 runs on either tree), andtest-sigioon the TCP out-of-band SIGURG case, which is intermittent (12 of 20 standalone runs failed on this branch and 12 of 20 onmain).Summary by cubic
Fixes pidfd cleanup when its exit monitor fails to start or errors out. A failed monitor now completes only the pidfd that owns it; previously one failure path completed every pidfd watching the same live target, while other failure paths completed nothing, leaving the fd unreadable forever.
ESRCHregistration failure still completes all pidfds on the target, because it means the target has already exited.keventwait now retries onEINTR.malloc,pthread_create,kqueue, orkeventfails, which a guest cannot cause.Written for commit 4e58d7e. Summary will update on new commits.