Skip to content

fix(fspy): run exec untracked when injection fails - #701

Open
lifeiscontent wants to merge 6 commits into
voidzero-dev:mainfrom
lifeiscontent:fix/fspy-untracked-fallback
Open

lifeiscontent wants to merge 6 commits into
voidzero-dev:mainfrom
lifeiscontent:fix/fspy-untracked-fallback

Conversation

@lifeiscontent

@lifeiscontent lifeiscontent commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Fixes #700.

Cached tasks run under fspy, which preloads a library into every spawned process. For statically linked binaries the preload installs a seccomp filter before exec. In sandboxes that deny the seccomp syscall (rootless bubblewrap under a restrictive profile, some CI environments) that install fails with EPERM, and the exec returned the error instead of running the program. Every spawn in a cached task failed before the child started.

Changes

  • Split handle_exec into resolve_exec and prepare_exec. Client::handle_exec now returns ExecInjectionError::Resolution or ExecInjectionError::Injection, so real exec errors like ENOENT and EACCES still reach the caller unchanged.
  • On an injection failure, exec and posix_spawn(p) mark the trace incomplete and run the call untracked.
  • Each exec variant falls back to its own libc original with its original arguments. execvp and execlp still search PATH, execveat keeps its dirfd and flags, and fexecve keeps its fd (so it doesn't need /proc). execl, execlp and execle can't forward their varargs, so they fall back to execv, execvp and execve with the collected argv, which POSIX defines as equivalent.
  • Add Sender::report_loss, which sets the channel's CLOSED bit so the run is treated as untracked and not cached from a partial trace.

Affected tasks still run but aren't cached. Setting input explicitly (without auto) brings caching back in those environments.

Split out from the original version of this PR:

@wan9chi wan9chi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These are great improvements. Thank you!

Could you try to split the PR into small independent ones? That'd make review easier and allow us to merge progressively.

// valid payload): run untracked by forwarding to the real exec.
// SAFETY: prog, argv, and envp are valid pointers forwarded from the
// interposed exec function.
return unsafe { execve::original()(prog, argv, envp) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Different types of exec* calls should fallback to different original versions. For example, if a execvp injection fails, the original execvp should be called, not execve::original. They have different behaviours.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Each exec variant now falls back to its own libc original, so execvp keeps its PATH search, execveat keeps its dirfd and flags, and fexecve keeps its fd. The variadic calls use their equivalent argv-based forms. Added a Linux regression test that forces injection to fail and checks that execvp still finds the program through PATH. That test and the static-binary fallback test both pass.

@lifeiscontent
lifeiscontent force-pushed the fix/fspy-untracked-fallback branch from eed0849 to 3dade77 Compare September 23, 2026 23:49
@lifeiscontent lifeiscontent changed the title fix(fspy): run processes untracked when injection fails fix(fspy): run exec untracked when injection fails Sep 23, 2026
@lifeiscontent
lifeiscontent force-pushed the fix/fspy-untracked-fallback branch 3 times, most recently from 00e5fd6 to f9a83aa Compare September 23, 2026 23:56
@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@wan9chi done! Thanks for the reviews :)

If the preload can't set up tracking for an exec (for example, the
seccomp filter for a static binary can't be installed because the
sandbox denies the syscall), mark the trace incomplete and run the
original call untracked instead of failing it. Resolution errors such
as ENOENT or EACCES are still returned as-is.

Each exec variant falls back to its own libc original with its original
arguments, so execvp and execlp still search PATH, execveat keeps its
dirfd and flags, and fexecve keeps its fd.

Refs voidzero-dev#700
@lifeiscontent
lifeiscontent force-pushed the fix/fspy-untracked-fallback branch from f9a83aa to 3beb6e9 Compare September 24, 2026 10:37
@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@wan9chi I've found several other bugs related to caching and work trees, are you open to handling a few more PRs? if so I'll try to push a few more fixes this week.

@wan9chi

wan9chi commented Sep 29, 2026

Copy link
Copy Markdown
Member

@wan9chi I've found several other bugs related to caching and work trees, are you open to handling a few more PRs? if so I'll try to push a few more fixes this week.

@lifeiscontent yes please keep them coming! Forgive me for being unresponsive recently. I'll review once I'm have time.

…llback

# Conflicts:
#	CHANGELOG.md
#	crates/fspy_client_unix/src/lib.rs
#	crates/fspy_shared_unix/src/spawn/mod.rs
@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@wan9chi I split this up as you asked. #752 is the untracked spawn fallback in vt, and #753 builds on this one. Each exec variant now falls back to its own libc original, so execvp calls the original execvp and keeps the PATH search. I also merged main again, so it is up to date. Could you take another look when you get a chance?

…llback

# Conflicts:
#	CHANGELOG.md
#	crates/fspy_client_unix/src/lib.rs
#	crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs
#	crates/fspy_preload_unix/src/interceptions/spawn/posix_spawn.rs
#	crates/fspy_shared_unix/src/spawn/mod.rs
@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@wan9chi merged current main and cleared the conflict. This still contains just the exec fallback fix; #752 is independent and #753 builds on this one.

The affected fspy tests and Clippy pass on macOS. Both Linux fallback regression tests pass, and Linux Clippy is clean too. Could you take another look and approve the workflow runs when you have time?

@wan9chi

wan9chi commented Nov 4, 2026

Copy link
Copy Markdown
Member

Thanks for the update, the per-variant fallbacks look right now. Two things before merging:

  1. On Linux, prepare_exec also opens the program and parses the ELF header, and that can fail in cases where the real exec fails too. For example, a script with no #! line gives ENOEXEC. main returns that errno as is, but this PR treats it as an injection failure and calls report_loss, so the task stops being cached. sh -c ./script-without-shebang succeeds on both, but here the channel reports RecordsLost.

    I think this is a different problem from Cached tasks cannot spawn child processes inside a rootless bubblewrap sandbox (EPERM) #700, which only needs the fallback when pre_exec.run() fails. If you want to get the seccomp fix in first, could you limit this PR to that and leave the other handle_exec errors as they are on main? The rest can go in another PR.

  2. report_loss sets the same bit as a full channel, so in a sandbox that blocks seccomp the user is told "this task used more files than automatic tracking can record". Can we change it to something that fits both cases, like "Not cached: automatic file tracking couldn't record everything this task did. Configure input and output manually to enable caching."? The TrackingIncomplete docs and the fspy_shm_capacity snapshot would need updating. Showing the actual cause is something we can design in a separate PR.

@wan9chi wan9chi added the linux backend The new Linux file tracking backend label Nov 4, 2026
Only a failure of PreExec::run, which installs the seccomp filter, now
makes an exec run untracked. Other handle_exec errors return their errno
as before. On Linux these can be real exec errors, such as ENOEXEC for a
script without a shebang, and they must not mark the trace incomplete.

The not-cached message now says that automatic file tracking couldn't
record everything the task did, so it fits both a full tracking channel
and a sandbox that blocks seccomp.
@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@wan9chi thanks. I limited the fallback to a failed pre_exec.run(). All other handle_exec errors now behave as on main. I added a Linux test for the shebang-less script case: it runs, and the channel no longer reports RecordsLost. The message now reads "automatic file tracking couldn't record everything this task did", and I updated the TrackingIncomplete docs and the fspy_shm_capacity snapshot.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linux backend The new Linux file tracking backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cached tasks cannot spawn child processes inside a rootless bubblewrap sandbox (EPERM)

2 participants