Skip to content
Draft
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
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,80 @@ cargo run --release --config 'target."cfg(all())".runner="sudo -E"'
Cargo build scripts are used to automatically build the eBPF correctly and include it in the
program.

## Usage

Pinchy can trace syscalls for a running process or launch a new process and trace it. You can specify which syscalls to trace using the `-e` or `--event` option.

### Basic Examples

Trace all syscalls for a command:
```shell
pinchy ls /tmp
```

Trace specific syscalls:
```shell
pinchy -e read,write,open ls /tmp
```

Attach to a running process:
```shell
pinchy -p <PID> -e open,close
```

### Syscall Aliases

Pinchy supports common syscall aliases that users might be familiar with from `libc` or other tools.

#### Signal Syscalls

On x86_64 and aarch64, signal-related syscalls use `rt_*` prefixes in the kernel, but you can use the more familiar names without the prefix:

```shell
# These are equivalent:
pinchy -e sigaction,sigprocmask ./myprogram
pinchy -e rt_sigaction,rt_sigprocmask ./myprogram
```

Supported signal aliases (all architectures):
- `sigaction` → `rt_sigaction`
- `sigprocmask` → `rt_sigprocmask`
- `sigreturn` → `rt_sigreturn`
- `sigpending` → `rt_sigpending`
- `sigtimedwait` → `rt_sigtimedwait`
- `sigqueueinfo` → `rt_sigqueueinfo`
- `sigsuspend` → `rt_sigsuspend`

#### Architecture-Specific Aliases

On **aarch64**, many traditional syscalls don't exist in the kernel but are provided by glibc as wrappers around newer `*at` variants. Pinchy supports these for convenience:

```shell
# On aarch64, these are equivalent:
pinchy -e open,stat ./myprogram
pinchy -e openat,newfstatat ./myprogram
```

Supported aarch64 aliases:
- `open` → `openat`
- `stat` → `newfstatat`
- `lstat` → `newfstatat`
- `poll` → `ppoll`
- `dup2` → `dup3`
- `pipe` → `pipe2`
- `access` → `faccessat`
- `chmod` → `fchmodat`
- `chown` → `fchownat`
- `link` → `linkat`
- `mkdir` → `mkdirat`
- `mknod` → `mknodat`
- `rename` → `renameat`
- `rmdir` → `unlinkat`
- `symlink` → `symlinkat`
- `unlink` → `unlinkat`

On **x86_64**, these traditional syscalls exist directly in the kernel, so no aliases are needed.

## Cross-compiling on macOS

Cross compilation should work on both Intel and Apple Silicon Macs.
Expand Down
29 changes: 29 additions & 0 deletions pinchy-common/src/syscalls/aarch64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,33 @@ declare_syscalls! {
SYS_futex_waitv = 449,
SYS_set_mempolicy_home_node = 450,
SYS_mseal = 462,
;
aliases:
// Signal-related syscalls that libc users might know by their non-rt_ names
"sigaction" => SYS_rt_sigaction,
"sigprocmask" => SYS_rt_sigprocmask,
"sigreturn" => SYS_rt_sigreturn,
"sigpending" => SYS_rt_sigpending,
"sigtimedwait" => SYS_rt_sigtimedwait,
"sigqueueinfo" => SYS_rt_sigqueueinfo,
"sigsuspend" => SYS_rt_sigsuspend,

// File/directory syscalls that don't exist on aarch64 but are provided by glibc
// These map to the *at variants with AT_FDCWD
"open" => SYS_openat,
"stat" => SYS_newfstatat,
"lstat" => SYS_newfstatat,
"poll" => SYS_ppoll,
"dup2" => SYS_dup3,
"pipe" => SYS_pipe2,
"access" => SYS_faccessat,
"chmod" => SYS_fchmodat,
"chown" => SYS_fchownat,
"link" => SYS_linkat,
"mkdir" => SYS_mkdirat,
"mknod" => SYS_mknodat,
"rename" => SYS_renameat,
"rmdir" => SYS_unlinkat,
"symlink" => SYS_symlinkat,
"unlink" => SYS_unlinkat,
}
11 changes: 11 additions & 0 deletions pinchy-common/src/syscalls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,20 @@ pub const SYS_generic_parse_test: i64 = i64::MAX; // for testing only
macro_rules! declare_syscalls {
(
$( $name:ident = $num:expr ),* $(,)?
$(; aliases: $( $alias:literal => $target:ident ),* $(,)? )?
) => {
$(pub const $name: i64 = $num;)*
pub fn syscall_nr_from_name(name: &str) -> Option<i64> {
// First check aliases
$(
$(
if name == $alias {
return Some($target);
}
)*
)?

// Then check canonical names
match name {
$( x if x == &stringify!($name)[4..] => Some($name), )*
_ => None,
Expand Down
10 changes: 10 additions & 0 deletions pinchy-common/src/syscalls/x86_64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,14 @@ declare_syscalls! {
SYS_set_mempolicy_home_node = 450,
SYS_fchmodat2 = 452,
SYS_mseal = 462,
;
aliases:
// Signal-related syscalls that libc users might know by their non-rt_ names
"sigaction" => SYS_rt_sigaction,
"sigprocmask" => SYS_rt_sigprocmask,
"sigreturn" => SYS_rt_sigreturn,
"sigpending" => SYS_rt_sigpending,
"sigtimedwait" => SYS_rt_sigtimedwait,
"sigqueueinfo" => SYS_rt_sigqueueinfo,
"sigsuspend" => SYS_rt_sigsuspend,
}
2 changes: 1 addition & 1 deletion pinchy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ bytes = "1.10.1"
zbus = { version = "5.7", features = ["tokio"] }
zbus_macros = "5.7"
clap = { workspace = true, default-features = true, features = ["derive"] }
nix = "0.30.1"
nix = { version = "0.30.1", features = ["user"] }

[build-dependencies]
anyhow = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion pinchy/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn parse_syscall_names(names: &[String]) -> Result<Vec<i64>, String> {
#[derive(Parser, Debug)]
#[command(author, version, about, trailing_var_arg = true)]
struct Args {
/// Syscall(s) to trace (can be repeated or comma-separated)
/// Syscall(s) to trace (can be repeated or comma-separated). Supports aliases like 'sigaction' for 'rt_sigaction'.
#[arg(short = 'e', long = "event", value_delimiter = ',', action = clap::ArgAction::Append)]
syscalls: Vec<String>,

Expand Down
117 changes: 117 additions & 0 deletions pinchy/src/tests/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Gustavo Noronha Silva <gustavo@noronha.dev.br>

use pinchy_common::syscalls::{syscall_nr_from_name, ALL_SYSCALLS};

fn parse_syscall_names(names: &[String]) -> Result<Vec<i64>, String> {
let mut out = Vec::new();
for name in names {
match syscall_nr_from_name(name) {
Some(nr) if ALL_SYSCALLS.contains(&nr) => out.push(nr),
Some(_) => return Err(format!("Syscall '{name}' is not supported by this build")),
None => return Err(format!("Unknown syscall name: {name}")),
}
}
Ok(out)
}

#[test]
fn test_parse_syscall_names_canonical() {
// Test canonical syscall names work
let result = parse_syscall_names(&["read".to_string(), "write".to_string()]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 2);
}

#[test]
fn test_parse_syscall_names_with_rt_prefix() {
// Test that rt_ prefixed names work
let result = parse_syscall_names(&["rt_sigaction".to_string()]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 1);
}

#[test]
fn test_parse_syscall_names_aliases() {
// Test that aliases work (non-rt_ versions should map to rt_ versions)
let result = parse_syscall_names(&["sigaction".to_string()]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 1);

// Verify it resolves to the same number as rt_sigaction
let rt_result = parse_syscall_names(&["rt_sigaction".to_string()]);
assert!(rt_result.is_ok());
assert_eq!(syscalls[0], rt_result.unwrap()[0]);
}

#[test]
fn test_parse_syscall_names_multiple_aliases() {
// Test multiple aliases at once
let result = parse_syscall_names(&[
"sigaction".to_string(),
"sigprocmask".to_string(),
"sigreturn".to_string(),
]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 3);
}

#[test]
fn test_parse_syscall_names_mixed() {
// Test mixing canonical names and aliases
let result = parse_syscall_names(&[
"read".to_string(),
"sigaction".to_string(),
"write".to_string(),
]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 3);
}

#[test]
fn test_parse_syscall_names_unknown() {
// Test that unknown syscall names produce an error
let result = parse_syscall_names(&["nonexistent_syscall".to_string()]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Unknown syscall name: nonexistent_syscall"));
}

#[test]
#[cfg(target_arch = "aarch64")]
fn test_parse_syscall_names_aarch64_aliases() {
// Test aarch64-specific aliases that map to *at variants
let result = parse_syscall_names(&[
"open".to_string(),
"stat".to_string(),
"poll".to_string(),
]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 3);

// Verify aliases resolve to the *at variants
let open_nr = syscall_nr_from_name("open");
let openat_nr = syscall_nr_from_name("openat");
assert_eq!(open_nr, openat_nr);
}

#[test]
#[cfg(target_arch = "x86_64")]
fn test_parse_syscall_names_x86_64_direct() {
// On x86_64, these syscalls exist directly (not as aliases)
let result = parse_syscall_names(&[
"open".to_string(),
"stat".to_string(),
"poll".to_string(),
]);
assert!(result.is_ok());
let syscalls = result.unwrap();
assert_eq!(syscalls.len(), 3);
}
1 change: 1 addition & 0 deletions pinchy/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) 2025 Gustavo Noronha Silva <gustavo@noronha.dev.br>

mod basic_io;
mod client;
mod filesystem;
mod ipc;
mod memory;
Expand Down
Loading