Skip to content

Add follow-forks support (-f) to trace child processes#136

Merged
kov merged 1 commit into
mainfrom
follow-forks
Jun 10, 2026
Merged

Add follow-forks support (-f) to trace child processes#136
kov merged 1 commit into
mainfrom
follow-forks

Conversation

@kov

@kov kov commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Fifth PR from the review pass (after #132-#135): follow child processes with -f/--follow-forks, like strace -f.

How it works — the daemon, not eBPF, drives child discovery. When any client requests follow-forks, the daemon always subscribes the fork family (clone, clone3, and fork/vfork on x86_64) the same way it already does execve for privilege revalidation. The dispatch loop watches successful fork-family exits in traced processes; the positive return value is the child PID. For each interested client the child is registered like a regular traced PID: eBPF PID-filter entry, its own pidfd lifecycle monitoring, events into the same output stream. clone/clone3 with CLONE_THREAD are skipped — threads already trace via the thread-group check. Authorization carries over (a child inherits the parent's credentials at fork; a later setuid exec is caught by the existing execve revalidation, which applies to children too).

Client bookkeeping for one-client-many-PIDs:

  • disconnect now cleans up every PID left without clients, not just the first found
  • the per-UID registration cap counts distinct clients rather than per-PID entries
  • a child whose process is already gone by the time the daemon sees the fork exit is skipped (pidfd_open fails)

Known limitation (documented in README): children are discovered from the parent's fork exit event, so a child's very first syscalls may be missed. Closing that race would need a sched_process_fork eBPF program with task_struct access; deferred.

Plumbing: the D-Bus trace_pid method gains a follow_forks argument; the client grows -f/--follow-forks. The integration harness gains a way to pass extra client flags through the UML runner (PINCHY_TEST_EXTRA_ARGS), and a fork_test workload whose child retries a recognizable openat over a few hundred ms so the single-CPU UML scheduler can't starve the daemon's filter update before the child runs.

🤖 Generated with Claude Code

Copilot AI left a comment

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.

Pull request overview

Adds strace -f-style fork-following to Pinchy by having the userspace daemon discover and auto-register child PIDs when clients opt in via -f/--follow-forks (plumbed over D-Bus).

Changes:

  • Extend the D-Bus trace_pid API and CLI to support follow_forks (-f/--follow-forks).
  • Update the daemon dispatch loop to detect successful fork-family exits and register child PIDs for interested clients.
  • Add an integration test + UML harness plumbing to pass extra client flags and validate fork-follow behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uml-kernel/uml-test-runner.sh Adds support for passing extra Pinchy client flags from kernel cmdline into the UML test invocation.
README.md Documents -f/--follow-forks behavior and its known “may miss earliest syscalls” limitation.
pinchy/tests/integration.rs Adds a follow_forks integration test asserting the child’s syscall is captured with -f.
pinchy/tests/common.rs Adds run_workload_with_args and passes extra client args through UML cmdline.
pinchy/src/tracing.rs Implements fork-child detection, follow-forks client bookkeeping, and forced fork-family syscall subscription when needed.
pinchy/src/server.rs Extends trace_pid D-Bus method signature to accept follow_forks.
pinchy/src/client.rs Adds CLI flag -f/--follow-forks and passes it through to the client library calls.
pinchy/src/bin/test-helper.rs Adds a fork_test workload used by integration tests.
pinchy-client/src/lib.rs Extends client-library API (attach/trace_child) and D-Bus proxy to pass follow_forks.

Comment thread pinchy/src/tracing.rs Outdated
Comment on lines +1041 to +1042
self.add_pid_to_filter(child)?;
self.start_pidfd_monitoring(child, pidfd).await?;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed and squashed: lupa call sites updated (good catch — lupa is outside default-members so CI never builds it), both rollback paths added, and printf replaces echo in the runner.

Comment thread uml-kernel/uml-test-runner.sh Outdated
Comment thread pinchy-client/src/lib.rs
}

pub async fn attach(pid: u32, syscalls: Vec<i64>) -> OwnedFd {
pub async fn attach(pid: u32, syscalls: Vec<i64>, follow_forks: bool) -> OwnedFd {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed and squashed: lupa call sites updated (good catch — lupa is outside default-members so CI never builds it), both rollback paths added, and printf replaces echo in the runner.

Comment thread pinchy/src/tracing.rs Outdated
Comment on lines +937 to +942
let is_new_pid = !self.clients_map.contains_key(&pid);
self.clients_map.entry(pid).or_default().push(client_info);

// Add PID to eBPF filter if this is the first client for this PID
if is_new_pid {
let mut pid_filter: aya::maps::HashMap<_, u32, u8> = self
.ebpf
.map_mut("PID_FILTER")
.ok_or_else(|| anyhow::anyhow!("PID_FILTER map not found"))?
.try_into()
.map_err(|e| anyhow::anyhow!("Map conversion failed: {e}"))?;
pid_filter
.insert(pid, 0, 0)
.map_err(|e| anyhow::anyhow!("Failed to insert PID: {e}"))?; // Start monitoring this PID if we got a pidfd
self.add_pid_to_filter(pid)?;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed and squashed: lupa call sites updated (good catch — lupa is outside default-members so CI never builds it), both rollback paths added, and printf replaces echo in the runner.

When a client requests follow-forks, the daemon always subscribes the
fork family (clone, clone3, and fork/vfork on x86_64) the same way it
already does for execve, watches their successful exits in the
dispatch loop, and registers the returned child PID for every
interested client: the child goes into the eBPF PID filter, gets its
own pidfd lifecycle monitoring, and its events flow into the same
output stream. clone/clone3 with CLONE_THREAD are skipped, since
threads already share the traced thread group.

Children are discovered from the parent's fork exit event, so a
child's very first syscalls may be missed; this is documented in the
README.

Client bookkeeping now copes with one client spanning several PIDs:
disconnect cleans up every PID left without clients, and the per-UID
registration cap counts distinct clients rather than per-PID entries.

The D-Bus trace_pid method gains a follow_forks argument, and the
client grows the -f/--follow-forks flag. The integration harness
gains a way to pass extra client flags through the UML runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kov kov merged commit 1832081 into main Jun 10, 2026
1 check passed
@kov kov deleted the follow-forks branch June 10, 2026 16:54
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