Skip to content

Let kill reach a relative that is not a child - #388

Merged
jserv merged 5 commits into
sysprog21:mainfrom
xalestar:kill-parent-via-registry
Sep 20, 2026
Merged

jserv merged 5 commits into
sysprog21:mainfrom
xalestar:kill-parent-via-registry

Conversation

@xalestar

@xalestar xalestar commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

A forked guest process gets ESRCH from kill(getppid(), sig), where Linux delivers the signal. sc_kill resolves 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 of kill already read the fork-family registry through proc_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 kill exposed 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, kill on the exited guest pid returned 0 instead of ESRCH. Each record now carries the host process start time from proc_pidinfo, and a record survives only while the process at that pid has the same start time. The reverse lookup gets the same check, so F_GETLK stops 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_signal let the signal through, because the tag the receiver checks was the same colliding value, so an orphan's kill(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.c has the child call kill(getppid(), 0) and then kill(getppid(), SIGUSR1); on main both return ESRCH (FAIL: 2 failed), with this series the parent's handler runs (PASS: 0 failed). make test-registry-stale-pid plants 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 main at c0458b4. make check passes. make test-matrix reports two failures, both of which reproduce on main with the same binary: the Rosetta audit-known-limitations case (a Rosetta assertion in rt_sigreturn, rc=133) and test-sigio, which is flaky at 7 failures in 10 runs on main against 3 in 10 here.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

Comment thread mk/tests.mk
Comment thread src/syscall/proc.c
Comment thread src/syscall/proc.c
Comment thread src/syscall/proc.h
* 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);

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.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and out of scope here. The three call sites in proc-pidfd.c follow in a separate PR once this one lands.

Comment thread src/syscall/syscall.c
cubic-dev-ai[bot]

This comment was marked as resolved.

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
xalestar force-pushed the kill-parent-via-registry branch from 356ed00 to a323958 Compare September 20, 2026 15:17
@jserv
jserv merged commit 669ea25 into sysprog21:main Sep 20, 2026
16 checks passed
@jserv

jserv commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Thank @xalestar for contributing!

@xalestar
xalestar deleted the kill-parent-via-registry branch September 21, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants