Skip to content

Commit 6d1a6b2

Browse files
wan9chiclaude
andauthored
refactor(fspy): collect execl arguments in the call's bump allocator (#891)
## Motivation `execl`, `execlp` and `execle` pass their arguments as a variadic list, so the interposer has to build an argv array before it can handle the exec. It used a 32-slot stack array, fell back to libc `malloc` for longer lists, and failed with `E2BIG` past a hardcoded 4096 arguments. That split is where #865's out-of-bounds write lived, and the `malloc` fallback isn't async-signal-safe: exec may run in the child of `fork()`, where malloc's lock may be held by a thread that no longer exists. ## Change - A small `collect_argv` pushes `arg` and the variadic arguments, up to and including the NULL, into an `allocator_api2::vec::Vec` in the call's `pooled_bump()`, and leaves the `VaList` where `execle` reads `envp`. - Each of the three interposers creates one bump and passes it to both `collect_argv` and `handle_exec`, as `execveat` already does. - `with_argv.rs` is removed along with the stack/heap split and the 4096-argument limit; the real `execve` reports `E2BIG` instead. The rest of the exec path still allocates through libc `malloc` (`RawExec::to_exec`/`from_exec`, `fexecve`); the PR stacked on this one removes that. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 194ee9c commit 6d1a6b2

2 files changed

Lines changed: 49 additions & 109 deletions

File tree

‎crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs‎

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
mod with_argv;
1+
use std::{ffi::VaList, ptr::null};
22

3-
use allocator_api2::alloc::Allocator;
3+
use allocator_api2::{alloc::Allocator, vec::Vec};
44
use fspy_shared_unix::exec::ExecResolveConfig;
55
use libc::{c_char, c_int};
6-
use with_argv::with_argv;
76

87
use crate::{
98
client::{global_client, raw_exec::RawExec},
@@ -25,6 +24,32 @@ pub unsafe fn environ() -> *const *const c_char {
2524
unsafe { environ }
2625
}
2726

27+
/// Collects `arg` and the variadic arguments after it into the
28+
/// NULL-terminated argv array that `execl`, `execlp` and `execle` take as a
29+
/// list. The array lives in `allocator`, not libc's heap: exec may run in
30+
/// the child of `fork()`, where malloc's lock may be held by a thread that
31+
/// no longer exists. `va` is left past the terminator, where `execle` finds
32+
/// `envp`.
33+
///
34+
/// # Safety
35+
///
36+
/// Unless `arg` is NULL, `va` must hold `*const c_char` arguments up to a
37+
/// NULL pointer.
38+
unsafe fn collect_argv<A: Allocator>(
39+
allocator: A,
40+
mut arg: *const c_char,
41+
va: &mut VaList<'_>,
42+
) -> Vec<*const c_char, A> {
43+
let mut argv = Vec::new_in(allocator);
44+
while !arg.is_null() {
45+
argv.push(arg);
46+
// SAFETY: the caller guarantees the arguments continue up to a NULL pointer.
47+
arg = unsafe { va.next_arg() };
48+
}
49+
argv.push(null());
50+
argv
51+
}
52+
2853
fn handle_exec(
2954
allocator: impl Allocator,
3055
config: ExecResolveConfig,
@@ -77,67 +102,48 @@ unsafe extern "C" fn execve(
77102
}
78103

79104
intercept!(execl(64): unsafe extern "C" fn(path: *const c_char, arg0: *const c_char, ...) -> c_int);
80-
unsafe extern "C" fn execl(path: *const c_char, arg0: *const c_char, valist: ...) -> c_int {
105+
unsafe extern "C" fn execl(path: *const c_char, arg: *const c_char, mut valist: ...) -> c_int {
81106
#[expect(
82107
clippy::no_effect_underscore_binding,
83108
reason = "suppresses unused warning on *::original"
84109
)]
85110
let _unused = execl::original;
86-
// SAFETY: valist and arg0 are valid variadic arguments forwarded from the interposed execl function
87-
unsafe {
88-
with_argv(valist, arg0, |args, _remaining| {
89-
handle_exec(
90-
fspy_nostd_alloc::pooled_bump(),
91-
ExecResolveConfig::search_path_disabled(),
92-
path,
93-
args.as_ptr(),
94-
environ(),
95-
)
96-
})
97-
}
111+
let arena = fspy_nostd_alloc::pooled_bump();
112+
// SAFETY: valist holds execl's NULL-terminated argument list after arg,
113+
// and environ() returns the valid process environment pointer
114+
let (argv, envp) = unsafe { (collect_argv(&arena, arg, &mut valist), environ()) };
115+
handle_exec(&arena, ExecResolveConfig::search_path_disabled(), path, argv.as_ptr(), envp)
98116
}
99117

100118
intercept!(execlp(64): unsafe extern "C" fn(path: *const c_char, arg0: *const c_char, ...) -> c_int);
101-
unsafe extern "C" fn execlp(path: *const c_char, arg0: *const c_char, valist: ...) -> c_int {
119+
unsafe extern "C" fn execlp(path: *const c_char, arg: *const c_char, mut valist: ...) -> c_int {
102120
#[expect(
103121
clippy::no_effect_underscore_binding,
104122
reason = "suppresses unused warning on *::original"
105123
)]
106124
let _unused = execlp::original;
107-
// SAFETY: valist and arg0 are valid variadic arguments forwarded from the interposed execlp function
108-
unsafe {
109-
with_argv(valist, arg0, |args, _remaining| {
110-
handle_exec(
111-
fspy_nostd_alloc::pooled_bump(),
112-
ExecResolveConfig::search_path_enabled(None),
113-
path,
114-
args.as_ptr(),
115-
environ(),
116-
)
117-
})
118-
}
125+
let arena = fspy_nostd_alloc::pooled_bump();
126+
// SAFETY: valist holds execlp's NULL-terminated argument list after arg,
127+
// and environ() returns the valid process environment pointer
128+
let (argv, envp) = unsafe { (collect_argv(&arena, arg, &mut valist), environ()) };
129+
handle_exec(&arena, ExecResolveConfig::search_path_enabled(None), path, argv.as_ptr(), envp)
119130
}
120131

121132
intercept!(execle(64): unsafe extern "C" fn(path: *const c_char, arg0: *const c_char, ...) -> c_int);
122-
unsafe extern "C" fn execle(path: *const c_char, arg0: *const c_char, valist: ...) -> c_int {
133+
unsafe extern "C" fn execle(path: *const c_char, arg: *const c_char, mut valist: ...) -> c_int {
123134
#[expect(
124135
clippy::no_effect_underscore_binding,
125136
reason = "suppresses unused warning on *::original"
126137
)]
127138
let _unused = execle::original;
128-
// SAFETY: valist and arg0 are valid variadic arguments forwarded from the interposed execle function
129-
unsafe {
130-
with_argv(valist, arg0, |args, mut remaining| {
131-
let envp = remaining.next_arg::<*const *const c_char>();
132-
handle_exec(
133-
fspy_nostd_alloc::pooled_bump(),
134-
ExecResolveConfig::search_path_disabled(),
135-
path,
136-
args.as_ptr(),
137-
envp,
138-
)
139-
})
140-
}
139+
let arena = fspy_nostd_alloc::pooled_bump();
140+
// SAFETY: valist holds execle's NULL-terminated argument list after arg,
141+
// followed by envp
142+
let (argv, envp) = unsafe {
143+
let argv = collect_argv(&arena, arg, &mut valist);
144+
(argv, valist.next_arg::<*const *const c_char>())
145+
};
146+
handle_exec(&arena, ExecResolveConfig::search_path_disabled(), path, argv.as_ptr(), envp)
141147
}
142148

143149
intercept!(execv(64): unsafe extern "C" fn(path: *const c_char, argv: *const *const c_char) -> c_int);

‎crates/fspy_preload_unix/src/interceptions/spawn/exec/with_argv.rs‎

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)