Skip to content

Commit f385930

Browse files
branchseerclaude
andauthored
feat: add musl target support (#273)
## Summary - Always use seccomp in fspy to infer inputs - Add a dedicated `test-musl` CI job that runs the full test suite against `x86_64-unknown-linux-musl` https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d5f0d5b commit f385930

File tree

22 files changed

+244
-79
lines changed

22 files changed

+244
-79
lines changed

.cargo/config.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ rustflags = ["--cfg", "tokio_unstable", "-D", "warnings"]
44
[unstable]
55
bindeps = true
66

7-
# Linker wrappers for cross-compiling bindep targets (fspy_test_bin) via cargo-zigbuild.
8-
# On native Linux the system linker can handle musl targets; these are needed on non-Linux hosts.
7+
# Linker wrappers for musl targets. On Linux hosts these use the system cc directly;
8+
# on non-Linux hosts (macOS, Windows) they cross-compile via cargo-zigbuild.
99
[target.x86_64-unknown-linux-musl]
1010
rustflags = ["-C", "linker=.cargo/zigcc-x86_64-unknown-linux-musl"]
1111

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
#!/bin/sh
2+
# Linker wrapper for aarch64-unknown-linux-musl targets.
3+
# On Linux, use the system cc directly. On other hosts, cross-compile via cargo-zigbuild.
4+
if [ "$(uname -s)" = "Linux" ]; then
5+
exec cc "$@"
6+
fi
27
exec cargo-zigbuild zig cc -- -fno-sanitize=all -target aarch64-linux-musl "$@"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
#!/bin/sh
2+
# Linker wrapper for x86_64-unknown-linux-musl targets.
3+
# On Linux, use the system cc directly. On other hosts, cross-compile via cargo-zigbuild.
4+
if [ "$(uname -s)" = "Linux" ]; then
5+
exec cc "$@"
6+
fi
27
exec cargo-zigbuild zig cc -- -fno-sanitize=all -target x86_64-linux-musl "$@"

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,44 @@ jobs:
133133
- run: cargo-zigbuild test --target x86_64-unknown-linux-gnu.2.17
134134
if: ${{ matrix.os == 'ubuntu-latest' }}
135135

136+
test-musl:
137+
needs: detect-changes
138+
if: needs.detect-changes.outputs.code-changed == 'true'
139+
name: Test (musl)
140+
runs-on: ubuntu-latest
141+
container:
142+
image: node:22-alpine3.21
143+
options: --shm-size=256m # shm_io tests need bigger shared memory
144+
env:
145+
# Override all rustflags to skip the zig cross-linker from .cargo/config.toml.
146+
# Alpine's cc is already musl-native, so no custom linker is needed.
147+
# Must mirror [build].rustflags from .cargo/config.toml.
148+
RUSTFLAGS: --cfg tokio_unstable -D warnings
149+
steps:
150+
- name: Install Alpine dependencies
151+
shell: sh {0}
152+
run: apk add --no-cache bash curl git musl-dev gcc g++ python3
153+
154+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
155+
with:
156+
persist-credentials: false
157+
submodules: true
158+
159+
- name: Install rustup
160+
run: |
161+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
162+
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
163+
164+
- name: Install Rust toolchain
165+
run: rustup show
166+
167+
- name: Install pnpm and Node tools
168+
run: |
169+
corepack enable
170+
pnpm install
171+
172+
- run: cargo test
173+
136174
fmt:
137175
name: Format and Check Deps
138176
runs-on: ubuntu-latest
@@ -168,6 +206,7 @@ jobs:
168206
needs:
169207
- clippy
170208
- test
209+
- test-musl
171210
- fmt
172211
steps:
173212
- run: exit 1

crates/fspy/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] }
2828
nix = { workspace = true, features = ["uio"] }
2929
tokio = { workspace = true, features = ["bytes"] }
3030

31-
[target.'cfg(unix)'.dependencies]
31+
[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies]
3232
fspy_preload_unix = { workspace = true }
33+
34+
[target.'cfg(unix)'.dependencies]
3335
fspy_shared_unix = { workspace = true }
3436
nix = { workspace = true, features = ["fs", "process", "socket", "feature"] }
3537

crates/fspy/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Run a command and capture all the paths it tries to access.
44

5-
## macOS/Linux implementation
5+
## macOS/Linux (glibc) implementation
66

77
It uses `DYLD_INSERT_LIBRARIES` on macOS and `LD_PRELOAD` on Linux to inject a shared library that intercepts file system calls.
88
The injection process is almost identical on both platforms other than the environment variable name. The implementation is in `src/unix`.
@@ -11,6 +11,10 @@ The injection process is almost identical on both platforms other than the envir
1111

1212
For fully static binaries (such as `esbuild`), `LD_PRELOAD` does not work. In this case, `seccomp_unotify` is used to intercept direct system calls. The handler is implemented in `src/unix/syscall_handler`.
1313

14+
## Linux musl implementation
15+
16+
On musl targets, only `seccomp_unotify`-based tracking is used (no preload library).
17+
1418
## Windows implementation
1519

1620
It uses [Detours](https://github.com/microsoft/Detours) to intercept file system calls. The implementation is in `src/windows`.

crates/fspy/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#![feature(once_cell_try)]
33

44
// Persist the injected DLL/shared library somewhere in the filesystem.
5+
// Not needed on musl (seccomp-only tracking).
6+
#[cfg(not(target_env = "musl"))]
57
mod artifact;
68

79
pub mod error;

crates/fspy/src/unix/mod.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use std::{io, path::Path};
88

99
#[cfg(target_os = "linux")]
1010
use fspy_seccomp_unotify::supervisor::supervise;
11-
use fspy_shared::ipc::{NativeStr, PathAccess, channel::channel};
11+
#[cfg(not(target_env = "musl"))]
12+
use fspy_shared::ipc::NativeStr;
13+
use fspy_shared::ipc::{PathAccess, channel::channel};
1214
#[cfg(target_os = "macos")]
1315
use fspy_shared_unix::payload::Artifacts;
1416
use fspy_shared_unix::{
@@ -33,28 +35,39 @@ pub struct SpyImpl {
3335
#[cfg(target_os = "macos")]
3436
artifacts: Artifacts,
3537

38+
#[cfg(not(target_env = "musl"))]
3639
preload_path: Box<NativeStr>,
3740
}
3841

42+
#[cfg(not(target_env = "musl"))]
3943
const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX"));
4044

4145
impl SpyImpl {
42-
/// Initialize the fs access spy by writing the preload library on disk
43-
pub fn init_in(dir: &Path) -> io::Result<Self> {
44-
use const_format::formatcp;
45-
use xxhash_rust::const_xxh3::xxh3_128;
46-
47-
use crate::artifact::Artifact;
48-
49-
const PRELOAD_CDYLIB: Artifact = Artifact {
50-
name: "fspy_preload",
51-
content: PRELOAD_CDYLIB_BINARY,
52-
hash: formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)),
46+
/// Initialize the fs access spy by writing the preload library on disk.
47+
///
48+
/// On musl targets, we don't build a preload library —
49+
/// only seccomp-based tracking is used.
50+
pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result<Self> {
51+
#[cfg(not(target_env = "musl"))]
52+
let preload_path = {
53+
use const_format::formatcp;
54+
use xxhash_rust::const_xxh3::xxh3_128;
55+
56+
use crate::artifact::Artifact;
57+
58+
const PRELOAD_CDYLIB: Artifact = Artifact {
59+
name: "fspy_preload",
60+
content: PRELOAD_CDYLIB_BINARY,
61+
hash: formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)),
62+
};
63+
64+
let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?;
65+
preload_cdylib_path.as_path().into()
5366
};
5467

55-
let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?;
5668
Ok(Self {
57-
preload_path: preload_cdylib_path.as_path().into(),
69+
#[cfg(not(target_env = "musl"))]
70+
preload_path,
5871
#[cfg(target_os = "macos")]
5972
artifacts: {
6073
let coreutils_path = macos_artifacts::COREUTILS_BINARY.write_to(dir, "")?;
@@ -80,6 +93,7 @@ impl SpyImpl {
8093
#[cfg(target_os = "macos")]
8194
artifacts: self.artifacts.clone(),
8295

96+
#[cfg(not(target_env = "musl"))]
8397
preload_path: self.preload_path.clone(),
8498

8599
#[cfg(target_os = "linux")]

crates/fspy/src/unix/syscall_handler/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ impl_handler!(
9292
#[cfg(target_arch = "x86_64")] lstat,
9393
#[cfg(target_arch = "x86_64")] newfstatat,
9494
#[cfg(target_arch = "aarch64")] fstatat,
95+
statx,
96+
97+
#[cfg(target_arch = "x86_64")] access,
98+
faccessat,
99+
faccessat2,
95100

96101
execve,
97102
execveat,

crates/fspy/src/unix/syscall_handler/stat.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,37 @@ impl SyscallHandler {
3232
) -> io::Result<()> {
3333
self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY)
3434
}
35+
36+
/// statx(2) — modern replacement for stat/fstatat used by newer glibc.
37+
pub(super) fn statx(
38+
&mut self,
39+
caller: Caller,
40+
(dir_fd, path_ptr): (Fd, CStrPtr),
41+
) -> io::Result<()> {
42+
self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY)
43+
}
44+
45+
/// access(2) — check file accessibility (e.g. existsSync in Node.js).
46+
#[cfg(target_arch = "x86_64")]
47+
pub(super) fn access(&mut self, caller: Caller, (path,): (CStrPtr,)) -> io::Result<()> {
48+
self.handle_open(caller, Fd::cwd(), path, libc::O_RDONLY)
49+
}
50+
51+
/// faccessat(2) — check file accessibility relative to directory fd.
52+
pub(super) fn faccessat(
53+
&mut self,
54+
caller: Caller,
55+
(dir_fd, path_ptr): (Fd, CStrPtr),
56+
) -> io::Result<()> {
57+
self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY)
58+
}
59+
60+
/// faccessat2(2) — extended faccessat with flags parameter.
61+
pub(super) fn faccessat2(
62+
&mut self,
63+
caller: Caller,
64+
(dir_fd, path_ptr): (Fd, CStrPtr),
65+
) -> io::Result<()> {
66+
self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY)
67+
}
3568
}

0 commit comments

Comments
 (0)