Skip to content

Commit 3af9426

Browse files
wan9chiclaude
andcommitted
fix(fspy): observe libuv's syscall(SYS_close) closes on Linux
On glibc Linux, libuv (and therefore Node) closes every descriptor via `syscall(SYS_close, fd)` rather than the `close` libc symbol — confirmed by tracing: a `fs.writeFileSync` triggers zero `close()`/`__close_nocancel()` calls and routes every close through the `syscall` wrapper. The `close` symbol interposition therefore never observed Node's closes, so the `Closing` callback never fired and worldline captured no writes (the Linux analog of the macOS `close$NOCANCEL` gap; musl is unaffected because its seccomp backend catches the close syscall directly). The preload already interposes the libc `syscall` wrapper (for SYS_statx) and forwards Node's SYS_close through it — it just didn't act on it. Fire the close callback for SYS_close there, mirroring the existing handling. The `close_callback_fires_for_node_write` regression test now passes on Linux too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4ca70cf commit 3af9426

1 file changed

Lines changed: 14 additions & 2 deletions

File tree

crates/fspy_preload_unix/src/interceptions/linux_syscall.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use fspy_shared::ipc::AccessMode;
22
use libc::{c_char, c_int, c_long};
33

44
use crate::{
5-
client::{convert::PathAt, handle_open},
5+
client::{convert::PathAt, handle_close, handle_open},
66
macros::intercept,
77
};
88

@@ -22,7 +22,19 @@ unsafe extern "C" fn syscall(syscall_no: c_long, mut args: ...) -> c_long {
2222
// SAFETY: extracting variadic arguments matching the syscall ABI
2323
let a5 = unsafe { args.next_arg::<c_long>() };
2424

25-
if syscall_no == libc::SYS_statx {
25+
if syscall_no == libc::SYS_close {
26+
// libuv (and therefore Node) closes descriptors via `syscall(SYS_close,
27+
// fd)` rather than the `close` libc symbol (see `uv__close_nocancel`),
28+
// so the `close` interposition never sees them. Observe the close here
29+
// instead, before the syscall runs while `fd` is still valid. (On macOS
30+
// the equivalent path is the `close$NOCANCEL` symbol.)
31+
#[expect(
32+
clippy::cast_possible_truncation,
33+
reason = "a file descriptor always fits in c_int per the syscall ABI"
34+
)]
35+
let fd = a0 as c_int;
36+
handle_close(fd);
37+
} else if syscall_no == libc::SYS_statx {
2638
// c-style conversion is expected: (4294967196 -> -100 aka libc::AT_FDCWD)
2739
#[expect(
2840
clippy::cast_possible_truncation,

0 commit comments

Comments
 (0)