Let kill reach a relative that is not a child - #388
Merged
Merged
Conversation
jserv
reviewed
Sep 18, 2026
jserv
reviewed
Sep 18, 2026
jserv
reviewed
Sep 18, 2026
jserv
reviewed
Sep 18, 2026
| * Returns the host pid, or -1 when the registry holds no live member with that | ||
| * guest pid. | ||
| */ | ||
| pid_t proc_namespace_host_pid(int64_t guest_pid); |
Contributor
There was a problem hiding this comment.
sys_pidfd_open and sys_pidfd_send_signal still resolve through proc_guest_to_host_pid alone, so after this change kill(getppid(), 0) succeeds while pidfd_open(getppid()) still returns ESRCH, and a pidfd on any relative that is not a descendant stays unreachable. Linux draws no such line. Worth a follow-up routing the three proc_guest_to_host_pid call sites in src/syscall/proc-pidfd.c through the same resolver.
Collaborator
Author
There was a problem hiding this comment.
Agreed, and out of scope here. The three call sites in proc-pidfd.c follow in a separate PR once this one lands.
jserv
reviewed
Sep 18, 2026
A guest process could not signal its own parent. sc_kill resolved a single pid through proc_guest_to_host_pid, which reads the child table, and that table holds descendants only, so kill(getppid(), sig) found nothing and returned ESRCH where Linux delivers. The fork-family registry already holds every live member, and the group and broadcast forms of kill already read it through proc_get_namespace_targets. This gives that walk a single-pid entry point and falls back to it when the child table has no answer, so the two forms resolve from the same source. tests/test-kill-parent.c has a forked child probe its parent with kill(getppid(), 0) and then deliver SIGUSR1 to the parent's handler. Both steps fail with ESRCH without this change.
A fork-family registry record outlives the member that wrote it until the next publish compacts the file, and registry_parse_cb kept any record whose host pid answered kill(pid, 0). Once macOS handed that pid to another elfuse process, the record passed both that probe and the proc_pidpath check in registry_collect, so kill(G, 0) for an exited guest pid G returned 0 instead of ESRCH. The group and broadcast forms of kill read the same records and had the same gap. Each record now carries the host process start time from proc_pidinfo, and registry_parse_cb keeps a record only while the live process at that pid has the same start time. The check runs at read time, so a member killed before it could clean up is covered too. The test-registry-stale-pid lane plants such a record and expects ESRCH; without this change kill(99, 0) finds it. The lane runs both elfuse processes under timeout, cleans up from an EXIT trap, and fails rather than reports OK when its own setup did not hold: no registry file, no READY from the family, or a holder that exited before the lookup.
registry_collect reads a guest_filter of 0 as "match every member", so proc_namespace_host_pid(0) returned the first non-self member rather than the -1 its header promises, and a negative pid did the same. sc_kill screens both before it calls, but the resolver is exported and the next caller has no reason to know it must screen.
registry_find_by_host_cb parsed three fields and trusted any record whose host pid matched, leaving proc_host_to_guest_pid to settle identity with proc_pidpath alone. That is the test a reused host pid passes: every elfuse process runs the same binary. F_GETLK then named an exited member as the guest holding a conflicting lock. The reverse lookup now compares the record's start time the way registry_parse_cb does.
The family id was the root's host pid, and macOS recycles those. Once a family outlived its root, a later elfuse root landing on that pid passed the owner test, deleted the orphan family's registry and published into the same path. The orphans then resolved guest pid 1 to that unrelated root, and proc_send_guest_signal let the signal through because the tag it checks was the same colliding value, so an orphan's kill(1, SIGKILL) killed a stranger. Pid-directed kill reached this through the registry only as of this branch; the group and broadcast forms always could. The root now mints a random id and carries it to children over the existing fork IPC field, and the four owner tests read a flag set by whoever minted rather than comparing the id against getpid. The id is the release store that publishes the family state and every reader acquires it, so a thread that observes the id observes the matching ownership; pthread_once serializes the mint. Two guest threads reaching the first fork together would otherwise let one skip the owner-only pid-sequence reset and then allocate from a sequence file the other unlinks. The stale-registry lane also pins that the registry file is not named after the root's pid.
xalestar
force-pushed
the
kill-parent-via-registry
branch
from
September 20, 2026 15:17
356ed00 to
a323958
Compare
Contributor
|
Thank @xalestar for contributing! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A forked guest process gets ESRCH from
kill(getppid(), sig), where Linux delivers the signal.sc_killresolves a single pid only through the child table, which holds the caller's descendants, so the parent is never found. The group and broadcast forms ofkillalready read the fork-family registry throughproc_get_namespace_targets; the first commit falls back to the same registry when the child table has no answer.Reading the registry from a pid-directed
killexposed three weaknesses that the group forms already had, and the rest of the series closes them.A record outlives the member that wrote it until the next publish compacts the file, and a reader kept any record whose host pid was alive. Once macOS reused that pid for another elfuse process,
killon the exited guest pid returned 0 instead of ESRCH. Each record now carries the host process start time fromproc_pidinfo, and a record survives only while the process at that pid has the same start time. The reverse lookup gets the same check, soF_GETLKstops naming an exited member as the holder of a conflicting lock.The family id was the root's host pid, which macOS recycles too. Once a family outlived its root, a later elfuse root landing on that pid passed the owner test, deleted the orphan family's registry and published into the same path. The orphans then resolved guest pid 1 to that unrelated root, and
proc_send_guest_signallet the signal through, because the tag the receiver checks was the same colliding value, so an orphan'skill(1, SIGKILL)reached a stranger. The root now mints a random id, children receive it over the fork IPC field that already carries it, and the owner tests read a flag set by whoever minted.Reproduction:
tests/test-kill-parent.chas the child callkill(getppid(), 0)and thenkill(getppid(), SIGUSR1); onmainboth return ESRCH (FAIL: 2 failed), with this series the parent's handler runs (PASS: 0 failed).make test-registry-stale-pidplants a record carrying a live elfuse host pid and a start time that process does not have, and expects ESRCH; it also pins that the registry file is not named after the root's pid. Both assertions fail without the commits they cover.Rebased on
mainat c0458b4.make checkpasses.make test-matrixreports two failures, both of which reproduce onmainwith the same binary: the Rosettaaudit-known-limitationscase (a Rosetta assertion inrt_sigreturn, rc=133) andtest-sigio, which is flaky at 7 failures in 10 runs onmainagainst 3 in 10 here.