Skip to content

Commit d65b8db

Browse files
wan9chicodex
andcommitted
fix(fspy): rendezvous after exec event
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent b9f07f5 commit d65b8db

3 files changed

Lines changed: 37 additions & 12 deletions

File tree

docs/fspy-linux-sigsys-research.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ The preferred ptrace bridge is:
2121
2. The handler asks the existing fspy supervisor to attach to that thread with `PTRACE_SEIZE` and `PTRACE_O_TRACEEXEC`.
2222
3. The handler reissues the original syscall through a trusted gateway.
2323
4. Linux performs the requested exec and stops at `PTRACE_EVENT_EXEC` before target code runs.
24-
5. The supervisor maps the handler island into the new address space, reinstalls the physical `SIGSYS` action, and detaches.
25-
6. File-system syscalls run with no tracer attached. Their `SIGSYS` handling stays in process.
24+
5. The supervisor advances once to the pending exec syscall-exit stop.
25+
6. The supervisor maps the handler island into the new address space, reinstalls the physical `SIGSYS` action, and detaches.
26+
7. File-system syscalls run with no tracer attached. Their `SIGSYS` handling stays in process.
2627

2728
Keep the custom loader in-house. The reference loaders were useful for finding requirements, but neither is suitable for production. Current esbuild 0.28.1, Node, shells, glibc, static musl, and static Go all passed a pure userland handoff after correcting reference-loader defects.
2829

@@ -43,8 +44,9 @@ flowchart TD
4344
G --> H["Temporary PTRACE_SEIZE"]
4445
H --> I["Real kernel exec"]
4546
I --> J["PTRACE_EVENT_EXEC before target entry"]
46-
J --> K["Map handler, install SIGSYS action, detach"]
47-
K --> L["Target starts with no active tracer"]
47+
J --> K["Exec syscall-exit rendezvous"]
48+
K --> L["Map handler, install SIGSYS action, detach"]
49+
L --> P["Target starts with no active tracer"]
4850
4951
G -->|"ptrace denied or occupied"| M["Real exec of static fspy_host"]
5052
M --> N["Earliest-entry handler bootstrap"]
@@ -130,18 +132,19 @@ Copy target pointers with bounded self `process_vm_readv`. Direct loads can turn
130132

131133
## Real exec with a temporary ptrace attachment
132134

133-
`PTRACE_EVENT_EXEC` occurs after Linux has installed the new image and reset exec-owned state, but before the new program executes an instruction. The ordering is visible in [`fs/exec.c`](https://github.com/torvalds/linux/blob/master/fs/exec.c#L1747).
135+
`PTRACE_EVENT_EXEC` occurs after Linux has installed the new image and reset exec-owned state, but before the new program executes an instruction. It also occurs before the pending exec syscall finishes returning. The ordering is visible in [`fs/exec.c`](https://github.com/torvalds/linux/blob/master/fs/exec.c#L1747) and the [x86-64 syscall return path](https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscall_64.c).
134136

135137
The exec handler can use this sequence:
136138

137139
1. Write an exec request containing the current TID and logical signal state to a preinitialized channel.
138140
2. Wait for the supervisor to call `PTRACE_SEIZE` with `PTRACE_O_TRACEEXEC`.
139141
3. Reissue the original `execve` or `execveat` with the sixth-argument gateway marker. Preserve the original path, argv, environment, fd, and flags.
140142
4. On success, handle the exec event under the post-exec thread-group-leader TID. `PTRACE_GETEVENTMSG` reports the former TID for a nonleader exec.
141-
5. Remote-map a sealed, position-independent handler artifact and its state mapping.
142-
6. Install the physical `SIGSYS` action and force the physical mask to unblock `SIGSYS`.
143-
7. Restore the target's entry registers and any instruction bytes overwritten for injection.
144-
8. Detach with no delivered signal.
143+
5. Resume once with `PTRACE_SYSCALL` and `PTRACE_O_TRACESYSGOOD`, then require the pending exec syscall-exit stop. This prevents the late exec return from overwriting registers prepared for the first injected syscall; x86-64 uses `rax` for both the syscall number and return value.
144+
6. Remote-map a sealed, position-independent handler artifact and its state mapping.
145+
7. Install the physical `SIGSYS` action and force the physical mask to unblock `SIGSYS`.
146+
8. Restore the target's entry registers and any instruction bytes overwritten for injection.
147+
9. Detach with no delivered signal.
145148

146149
Remote injection syscalls are also evaluated by inherited seccomp filters. Set the gateway marker on remote `mmap`, `mprotect`, and `rt_sigaction` calls that fspy itself traps. A stronger target or outer filter can still reject an operation.
147150

research/ptrace-exec-prototype/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ This proves the kernel ordering needed by a hybrid fspy design:
66
`getpid` and then performs a real `execve`.
77
2. Its parent catches `PTRACE_EVENT_EXEC`, after the new image exists but before
88
its first user-space instruction.
9-
3. The parent executes remote `mmap`, `rt_sigaction`, and `mprotect` syscalls at
9+
3. The parent advances once to the pending `execve` syscall-exit stop.
10+
4. The parent executes remote `mmap`, `rt_sigaction`, and `mprotect` syscalls at
1011
the stopped entry PC, copies in a freestanding handler, and detaches.
11-
4. The target verifies `TracerPid: 0` and calls `getpid`. The injected in-process
12+
5. The target verifies `TracerPid: 0` and calls `getpid`. The injected in-process
1213
handler changes the saved return register to `0x51515151`.
1314

1415
The source has native AArch64 and x86-64 register/trampoline implementations.
1516
It uses only libc/kernel headers and is suitable for a native Linux CI job.
17+
The syscall-exit rendezvous is required because the exec event occurs before
18+
the original syscall finishes returning. In particular, the x86-64 return path
19+
would otherwise overwrite the first injected syscall number in `rax`.
1620

1721
## Run
1822

research/ptrace-exec-prototype/injector.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,28 @@ static void wait_for_exec_stop(pid_t child)
384384
}
385385
}
386386

387+
static void finish_exec_syscall(pid_t child)
388+
{
389+
int status;
390+
391+
/* PTRACE_EVENT_EXEC happens before the original execve returns. Wait for
392+
* its syscall-exit stop so that the kernel cannot overwrite registers
393+
* prepared for the first injected syscall. */
394+
if (ptrace(PTRACE_SYSCALL, child, NULL, NULL) < 0)
395+
fatal("PTRACE_SYSCALL after exec event");
396+
if (waitpid(child, &status, 0) < 0)
397+
fatal("waitpid exec syscall exit");
398+
if (!WIFSTOPPED(status) || WSTOPSIG(status) != (SIGTRAP | 0x80) ||
399+
(unsigned int)status >> 16 != 0)
400+
fatal_message("child did not reach the exec syscall-exit stop");
401+
}
402+
387403
int main(int argc, char **argv)
388404
{
389405
pid_t child;
390406
int status;
391-
unsigned long options = PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL;
407+
unsigned long options =
408+
PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL | PTRACE_O_TRACESYSGOOD;
392409

393410
if (argc != 2) {
394411
fprintf(stderr, "usage: %s /absolute/path/to/target\n", argv[0]);
@@ -413,6 +430,7 @@ int main(int argc, char **argv)
413430

414431
wait_for_exec_stop(child);
415432
puts("injector: caught PTRACE_EVENT_EXEC before target entry");
433+
finish_exec_syscall(child);
416434
inject_sigsys_handler(child);
417435

418436
if (ptrace(PTRACE_DETACH, child, NULL, NULL) < 0)

0 commit comments

Comments
 (0)