Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ List the syscall names supported by this build:
pinchy --list-syscalls
```

Follow child processes created by the tracee (like `strace -f`):
```shell
pinchy -f -- sh -c 'ls | wc -l'
```
The daemon learns about new children from the parent's fork/clone exit, so a
child's very first syscalls may be missed.

Other knobs:

- `--format one-line|multi-line`: trace line formatting (default `one-line`)
Expand Down
4 changes: 2 additions & 2 deletions lupa/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ fn main() -> Result<()> {
let (pid, fd) = tokio::runtime::Runtime::new()?.block_on(async move {
let syscalls = vec![SYS_openat, SYS_close, SYS_read, SYS_write];
let (pid, fd) = if let Some(pid) = args.pid {
let fd = pinchy_client::attach(pid, syscalls).await;
let fd = pinchy_client::attach(pid, syscalls, false).await;
(pid, fd)
} else if let Some(command) = args.command {
let (pid, fd) = pinchy_client::trace_child(command, syscalls).await;
let (pid, fd) = pinchy_client::trace_child(command, syscalls, false).await;
(pid as u32, fd)
} else {
anyhow::bail!("Need one of -p <pid> or command")
Expand Down
19 changes: 14 additions & 5 deletions pinchy-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,31 @@ use zbus::{Error as ZBusError, fdo, names::WellKnownName, proxy};

#[proxy(interface = "org.pinchy.Service", default_path = "/org/pinchy/Service")]
pub trait Pinchy {
fn trace_pid(&self, pid: u32, syscalls: Vec<i64>) -> zbus::Result<zbus::zvariant::OwnedFd>;
fn trace_pid(
&self,
pid: u32,
syscalls: Vec<i64>,
follow_forks: bool,
) -> zbus::Result<zbus::zvariant::OwnedFd>;
}

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.

let proxy = match connect_to_server().await {
Ok(proxy) => proxy,
Err(e) => handle_dbus_error(e),
};

match proxy.trace_pid(pid, syscalls).await {
match proxy.trace_pid(pid, syscalls, follow_forks).await {
Ok(fd) => OwnedFd::from(fd),
Err(e) => handle_dbus_error(e),
}
}

pub async fn trace_child(command: Vec<OsString>, syscalls: Vec<i64>) -> (i32, OwnedFd) {
pub async fn trace_child(
command: Vec<OsString>,
syscalls: Vec<i64>,
follow_forks: bool,
) -> (i32, OwnedFd) {
let proxy = match connect_to_server().await {
Ok(proxy) => proxy,
Err(e) => handle_dbus_error(e),
Expand Down Expand Up @@ -107,7 +116,7 @@ pub async fn trace_child(command: Vec<OsString>, syscalls: Vec<i64>) -> (i32, Ow
std::process::exit(1);
}
}
let fd = match proxy.trace_pid(pid as u32, syscalls).await {
let fd = match proxy.trace_pid(pid as u32, syscalls, follow_forks).await {
Ok(fd) => OwnedFd::from(fd),
Err(e) => handle_dbus_error(e),
};
Expand Down
32 changes: 32 additions & 0 deletions pinchy/src/bin/test-helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ fn main() -> anyhow::Result<()> {
"rt_sigaction_realtime" => rt_sigaction_realtime(),
"rt_sigaction_standard" => rt_sigaction_standard(),
"fcntl_test" => fcntl_test(),
"fork_test" => fork_test(),
"fchdir_test" => fchdir_test(),
"network_test" => network_test(),
"accept_test" => accept_test(),
Expand Down Expand Up @@ -1665,6 +1666,37 @@ fn fchdir_test() -> anyhow::Result<()> {
Ok(())
}

fn fork_test() -> anyhow::Result<()> {
unsafe {
let pid = libc::fork();
assert!(pid >= 0, "fork failed");

if pid == 0 {
// Child: do a recognizable syscall so a follow-forks trace can
// assert on it, then exit without running any libc cleanup. The
// daemon learns about the child from the parent's clone exit, so
// retry over a few hundred milliseconds to let it catch up.
for _ in 0..5 {
libc::usleep(100_000);

let fd = libc::openat(libc::AT_FDCWD, c"/dev/null".as_ptr(), libc::O_RDONLY);

if fd >= 0 {
libc::close(fd);
}
}

libc::_exit(0);
}

let mut status = 0;
let ret = libc::waitpid(pid, &mut status, 0);
assert_eq!(ret, pid, "waitpid failed");
}

Ok(())
}

fn fcntl_test() -> anyhow::Result<()> {
unsafe {
// Open a file to test fcntl on
Expand Down
15 changes: 12 additions & 3 deletions pinchy/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ mod tests;

#[proxy(interface = "org.pinchy.Service", default_path = "/org/pinchy/Service")]
trait Pinchy {
fn trace_pid(&self, pid: u32, syscalls: Vec<i64>) -> zbus::Result<zbus::zvariant::OwnedFd>;
fn trace_pid(
&self,
pid: u32,
syscalls: Vec<i64>,
follow_forks: bool,
) -> zbus::Result<zbus::zvariant::OwnedFd>;
}

const DEFAULT_STDOUT_FLUSH_BYTES: usize = 1;
Expand Down Expand Up @@ -141,6 +146,10 @@ struct Args {
#[arg(long = "list-syscalls")]
list_syscalls: bool,

/// Follow forks: also trace child processes created by the tracee
#[arg(short = 'f', long = "follow-forks")]
follow_forks: bool,

/// Write trace output to FILE instead of stderr
#[arg(short = 'o', long = "output")]
output: Option<std::path::PathBuf>,
Expand Down Expand Up @@ -180,15 +189,15 @@ async fn main() -> Result<()> {
let style = args.style;

if let Some(command) = args.command {
let (pid, fd) = pinchy_client::trace_child(command, syscalls).await;
let (pid, fd) = pinchy_client::trace_child(command, syscalls, args.follow_forks).await;

// Read everything there is to read, the server will close the write end
// of the pipe
relay_to_sink(fd, style, args.output).await?;

pinchy_client::cleanup_and_quit(pid);
} else if let Some(pid) = args.pid {
let fd = pinchy_client::attach(pid, syscalls).await;
let fd = pinchy_client::attach(pid, syscalls, args.follow_forks).await;

relay_to_sink(fd, style, args.output).await?;
} else {
Expand Down
3 changes: 2 additions & 1 deletion pinchy/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ impl PinchyDBus {
#[zbus(connection)] conn: &zbus::Connection,
pid: u32,
syscalls: Vec<i64>,
follow_forks: bool,
) -> zbus::fdo::Result<Fd<'_>> {
// The eBPF SYSCALL_FILTER is a 512-bit bitmap indexed by syscall
// number; reject anything outside that range before it reaches the
Expand Down Expand Up @@ -164,7 +165,7 @@ impl PinchyDBus {
.dispatch
.write()
.await
.register_client(pid, writer, syscalls, Some(pidfd), caller_uid)
.register_client(pid, writer, syscalls, Some(pidfd), caller_uid, follow_forks)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;

Expand Down
Loading
Loading