Skip to content

Fix kill() broadcast and process-group signal#150

Merged
jserv merged 1 commit into
mainfrom
fix-kill
Jul 7, 2026
Merged

Fix kill() broadcast and process-group signal#150
jserv merged 1 commit into
mainfrom
fix-kill

Conversation

@jserv

@jserv jserv commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

sc_kill mishandled the negative-pid forms. Correct them to match Linux and the qemu-aarch64 ground truth:

  • kill(-1, sig): broadcast to the emulated fork children over the cross-process transport, excluding the caller. Per kill(2), Linux does not signal the calling process on kill(-1); the prior code did the opposite (self only, no children). Return 0 on any delivery, the first transport errno if children existed but every send failed, else ESRCH.
  • kill(0, sig) and kill(-pgid, sig): deliver to every process in the target group -- children, siblings, and the parent -- not just self. Because each guest process is a separate host process, group membership is shared through a per-fork-family registry file keyed by the abstract-socket namespace id, so independent sessions stay isolated. Membership is published at fork, setpgid (self and parent-side), and setsid; publish compacts the file in place so it stays bounded by live members rather than growing per event. A process syncs its own pgid from registry before acting on its group, so setsid's already-leader check and the kill group path observe the current group even after a parent-side setpgid(child).

Hardening:

  • setpgid rejects a negative group id (EINVAL) and negative pid (ESRCH), reading the args as 32-bit like sc_kill.
  • The signal transport tags each line with the sender's namespace so a recycled host pid on an unrelated session drops the delivery instead of applying it, and proc_send_guest_signal revalidates that the target is still this binary before signaling.

Close #131


Summary by cubic

Fixes kill() negative-PID semantics to match Linux. kill(-1) now broadcasts to every member of the emulated fork family except self, and kill(0)/kill(-pgid) signal the full process group across host processes via a shared registry. Hardens signal transport to block cross-session and cross-guest PID reuse.

  • Bug Fixes

    • kill(-1, sig) broadcasts to all fork-family peers and never to self.
    • kill(0, sig) and kill(-pgid, sig) reach the entire target group (children, siblings, parent) across processes.
    • Linux-compatible results: 0 on any delivery; ESRCH when no targets; for broadcast/group if all sends fail, return the first transport errno; sig==0 probes match Linux; setpgid rejects negative pgid (EINVAL) and negative pid (ESRCH).
  • New Features

    • Fork-family process-group registry keyed by the abstract-socket namespace: published at fork, setpgid (self and parent-side), and setsid; processes sync pgid before group ops; setpgid’s group-existence check reads the registry; file compacted in place.
    • Hardened signal transport: tag deliveries with namespace and target guest pid; verify the target runs this binary; reader drops mismatched sessions/guests and rejects garbage; owner cleans the registry only when last member; warn when full; log failed truncates.
    • Tests added and wired into CI: test-kill-broadcast, test-kill-pgroup.

Written for commit 20d1be2. Summary will update on new commits.

Review in cubic

@jserv jserv requested review from Max042004 and doanbaotrung July 4, 2026 14:43
cubic-dev-ai[bot]

This comment was marked as resolved.

@doanbaotrung

Copy link
Copy Markdown
Collaborator

I have tested the fix using standard native tools (bash and /bin/kill) without using any compiled test binaries:

1. Process Group Signaling (kill 0)

We spawned a background child subshell trapping SIGUSR1, ignored SIGUSR1 in the parent, and sent kill -USR1 0:

/bin/bash -c \
  'OUT=/tmp/sig_out.txt; rm -f $OUT; ( trap "echo CHILD_RCV_USR1 >> $OUT; exit 0" USR1; sleep 10 & wait $! ) & CHILD_PID=$!; sleep 0.5; trap "" USR1; kill -USR1 0; sleep 0.5; cat $OUT'

Output

CHILD_RCV_USR1
ELF entry   : 0x33840
Load range  : 0x0 – 0x193d80
Segments    : 2
  • Result: CHILD_RCV_USR1 (Exit code: 0)
  • Status: PASSED (The sibling/child process in the process group successfully received the signal).

2. Broadcast Signaling excluding Caller (kill -1)

We spawned a background child subshell trapping SIGUSR2 and sent kill -USR2 -1 via /bin/kill to deliver to all processes except the caller (parent):

/bin/bash -c \
  'OUT=/tmp/sig_out.txt; rm -f $OUT; ( trap "echo CHILD_RCV_USR2 >> $OUT; exit 0" USR2; sleep 10 & wait $! ) & CHILD_PID=$!; sleep 0.5; /bin/kill -USR2 -1; sleep 0.5; cat $OUT'

Output:

CHILD_RCV_USR2
ELF entry   : 0x33840
Load range  : 0x0 – 0x193d80
Segments    : 2
  • Result: CHILD_RCV_USR2 (Exit code: 0)
  • Status: PASSED (The child process successfully received SIGUSR2. The parent process was correctly excluded from the broadcast signal, surviving the command without a trap and terminating successfully).

sc_kill mishandled the negative-pid forms. Correct them to match Linux
and the qemu-aarch64 ground truth:
- kill(-1, sig): broadcast to every member of the emulated fork family
  over the cross-process transport, excluding the caller. Per kill(2),
  Linux does not signal the calling process on kill(-1); the prior code
  did the opposite (self only, no children). Return 0 on any delivery,
  the first transport errno if targets existed but every send failed,
  else ESRCH.
- kill(0, sig) and kill(-pgid, sig): deliver to every process in the
  target group -- children, siblings, and the parent -- not just self.

Because each guest process is a separate host process, group membership
is shared through a per-fork-family registry file keyed by the
abstract-socket namespace id, so independent sessions stay isolated.
Membership is published at fork, setpgid (self and parent-side), and
setsid; publish compacts the file in place so it stays bounded by live
members rather than growing per event. A process syncs its own pgid from
the registry before acting on its group, so setsid's already-leader
check and the kill group path observe the current group even after a
parent-side setpgid(child). The registry is the single source of group
membership: setpgid's group-existence check reads it directly rather
than a process-table copy that could go stale after a child changes its
own group.

The registry and signal transport share one record reader
(for_each_record) that parses newline-terminated "hostpid guestpid pgid"
and "namespace tgpid signum" lines, replacing two hand-rolled chunked
parsers. proc_get_namespace_targets returns (host_pid, guest_pid) pairs
and, with PROC_PGID_ANY, serves both the broadcast and group paths.

Recycled-pid safety:
- Each transport line is tagged with the sender's namespace and the
  intended guest pid. A recycled host pid on an unrelated session sees a
  mismatched namespace; a host pid recycled onto a different guest in
  this same session sees a mismatched guest pid. Either mismatch drops
  the delivery. The drain rejects trailing garbage so a forged same-user
  temp-dir record cannot be accepted as a bare signal.
- pgid sync matches on host pid AND guest pid so a stale record from a
  recycled pid cannot overwrite this process's group.
- proc_send_guest_signal revalidates that the target still runs this
  binary before signaling.

Registry lifecycle:
- The namespace owner unlinks the registry only once no other live
  member needs it, so an owner that exits while fork children survive
  does not blind their group signals.
- A full registry logs a warning instead of silently dropping a live
  member's registration.
- A failed transport truncate is logged (queued signals may re-deliver)
  rather than ignored.

Hardening:
- setpgid rejects a negative group id (EINVAL) and negative pid (ESRCH),
  reading the args as 32-bit like sc_kill.

Close #131
@jserv jserv merged commit e6b3455 into main Jul 7, 2026
5 checks passed
@jserv jserv deleted the fix-kill branch July 7, 2026 04:25
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.

Incorrect signal broadcast behavior and missing self-signaling on kill(-1)

2 participants