Conversation
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 could pass without testing anything. A wrong registry path made the append create a fresh file the family never reads, and a family that never printed READY was waited out and then run anyway; both still left kill(99, 0) returning ESRCH. The recipe now fails when READY does not appear or the registry file does not already exist. An EXIT trap stops both elfuse runs and removes the scratch directory, so an interrupted lane no longer leaves the paused holder behind. The guest parent closes its copy of the readiness pipe's write end, so a child that dies before reporting gives EOF instead of a hang.
A holder that exited before the family's kill(99, 0) leaves a record for a dead pid, which registry_parse_cb drops on liveness alone, so the lane passed without reaching the start-time comparison. The recipe now fails unless the holder is still alive once the family has exited, which covers the whole window the lookup ran in.
| printf '%s 99 1 1\n' "$$xpid" >> "$$reg" || fail "cannot append to $$reg"; \ | ||
| echo go >&4; \ | ||
| exec 4>&-; \ | ||
| wait $$fpid; \ |
There was a problem hiding this comment.
wait $$fpid has no bound, so a regression that leaves the guest parked (the read(0, ...) on the fifo, or the forked child's read(release[0])) hangs this recipe instead of failing it. The EXIT trap never runs, so make check stops here with both elfuse roots still alive and the scratch directory left behind. Every other lane in this file reaches the guest through a runner that wraps the invocation in timeout; this recipe launches $(ELFUSE_BIN) directly. Wrapping both launches in timeout $(TEST_TIMEOUT) and failing when fpid outlives it keeps a hang reportable.
| * | ||
| * Returns false once @pid has exited. | ||
| */ | ||
| static bool host_start_us(pid_t pid, uint64_t *out) |
There was a problem hiding this comment.
Nothing on the reverse lookup uses this. registry_find_by_host_cb still parses three fields, and proc_host_to_guest_pid still accepts a hit on host pid plus a matching proc_pidpath, which is the exact identity test this commit demonstrates is not sufficient. Once macOS hands an exited member's host pid to another elfuse process, F_GETLK reports the exited member's guest pid as the conflicting lock holder, the same stale-record class the second commit closes for kill. Parsing the fourth field in registry_find_by_host_cb and comparing it against host_start_us closes it on that path too.
| pid_t proc_namespace_host_pid(int64_t guest_pid) | ||
| { | ||
| proc_signal_target_t target; |
There was a problem hiding this comment.
Two inputs break the contract proc.h states for this function. A guest_pid of 0 or negative leaves guest_filter > 0 false inside registry_collect, which disables the filter entirely and returns the first non-self member, so the caller gets an arbitrary host pid where the header promises -1. The caller's own guest pid returns -1 even though it is a live member, because registry_collect skips host_pid == self. sc_kill screens both cases before it calls, so nothing is wrong today, but this is exported as a general resolver and the next caller has no reason to know it must screen.
| pid_t proc_namespace_host_pid(int64_t guest_pid) | |
| { | |
| proc_signal_target_t target; | |
| pid_t proc_namespace_host_pid(int64_t guest_pid) | |
| { | |
| if (guest_pid <= 0) | |
| return -1; | |
| proc_signal_target_t target; |
| * 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); |
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.
| * exists only in the namespace registry, the same source the group and | ||
| * broadcast forms already read. | ||
| */ | ||
| static pid_t kill_resolve_host_pid(int64_t gpid) |
There was a problem hiding this comment.
This puts pid-directed kill on the registry, where before only the group and broadcast forms read it, and the registry's file name is keyed on absock_get_namespace_id(), which falls back to the root's host pid. macOS recycles that pid like any other: once a family's root exits with a child still running, a new elfuse root that lands on the reused pid passes the owner test in proc_registry_reset_if_owner, unlinks the orphan family's registry, and then publishes into the same path. The start time added by the second commit proves the target is live and proc_pidpath proves it runs the same binary, but neither proves it is in the same family, so the orphan's kill(1, SIGKILL) reaches the unrelated root, and proc_send_guest_signal passes it through because the namespace tag it checks is that same colliding value. Keying the registry on something that is not a recycled pid, a nonce carried through the existing fork IPC state for instance, would close it for the group forms as well.
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.The second commit closes a gap that registry already had. An exited member's record stays until the next publish compacts the file, and a reader kept any record whose host pid was alive, so 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 is kept only while the process at that pid has the same start time.Reproduction:
tests/test-kill-parent.chas the child callkill(getppid(), 0)and thenkill(getppid(), SIGUSR1); onmainboth return ESRCH (FAIL: 2 failed), with this change the parent's handler runs (PASS: 0 failed).make test-registry-stale-pidplants a record with a live elfuse host pid and a wrong start time; without the second commitkill(99, 0)finds it, with it the call returns ESRCH.Rebased on
mainat c0458b4.make checkpasses.make test-matrixpassestest-kill-parent,test-kill-pgroup, andtest-kill-broadcastin every mode; its one failure, the Rosettaaudit-known-limitationscase (a Rosetta assertion inrt_sigreturn, rc=133), fails the same way onmain.