diff --git a/.cargo/config.toml b/.cargo/config.toml index b52768ea..e9ac52c2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,8 +4,8 @@ rustflags = ["--cfg", "tokio_unstable", "-D", "warnings"] [unstable] bindeps = true -# Linker wrappers for cross-compiling bindep targets (fspy_test_bin) via cargo-zigbuild. -# On native Linux the system linker can handle musl targets; these are needed on non-Linux hosts. +# Linker wrappers for musl targets. On Linux hosts these use the system cc directly; +# on non-Linux hosts (macOS, Windows) they cross-compile via cargo-zigbuild. [target.x86_64-unknown-linux-musl] rustflags = ["-C", "linker=.cargo/zigcc-x86_64-unknown-linux-musl"] diff --git a/.cargo/zigcc-aarch64-unknown-linux-musl b/.cargo/zigcc-aarch64-unknown-linux-musl index 2c638a92..6dba882b 100755 --- a/.cargo/zigcc-aarch64-unknown-linux-musl +++ b/.cargo/zigcc-aarch64-unknown-linux-musl @@ -1,2 +1,7 @@ #!/bin/sh +# Linker wrapper for aarch64-unknown-linux-musl targets. +# On Linux, use the system cc directly. On other hosts, cross-compile via cargo-zigbuild. +if [ "$(uname -s)" = "Linux" ]; then + exec cc "$@" +fi exec cargo-zigbuild zig cc -- -fno-sanitize=all -target aarch64-linux-musl "$@" diff --git a/.cargo/zigcc-x86_64-unknown-linux-musl b/.cargo/zigcc-x86_64-unknown-linux-musl index 51e21946..c7e04fda 100755 --- a/.cargo/zigcc-x86_64-unknown-linux-musl +++ b/.cargo/zigcc-x86_64-unknown-linux-musl @@ -1,2 +1,7 @@ #!/bin/sh +# Linker wrapper for x86_64-unknown-linux-musl targets. +# On Linux, use the system cc directly. On other hosts, cross-compile via cargo-zigbuild. +if [ "$(uname -s)" = "Linux" ]; then + exec cc "$@" +fi exec cargo-zigbuild zig cc -- -fno-sanitize=all -target x86_64-linux-musl "$@" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3f48649..106a3440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,44 @@ jobs: - run: cargo-zigbuild test --target x86_64-unknown-linux-gnu.2.17 if: ${{ matrix.os == 'ubuntu-latest' }} + test-musl: + needs: detect-changes + if: needs.detect-changes.outputs.code-changed == 'true' + name: Test (musl) + runs-on: ubuntu-latest + container: + image: node:22-alpine3.21 + options: --shm-size=256m # shm_io tests need bigger shared memory + env: + # Override all rustflags to skip the zig cross-linker from .cargo/config.toml. + # Alpine's cc is already musl-native, so no custom linker is needed. + # Must mirror [build].rustflags from .cargo/config.toml. + RUSTFLAGS: --cfg tokio_unstable -D warnings + steps: + - name: Install Alpine dependencies + shell: sh {0} + run: apk add --no-cache bash curl git musl-dev gcc g++ python3 + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + submodules: true + + - name: Install rustup + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Install Rust toolchain + run: rustup show + + - name: Install pnpm and Node tools + run: | + corepack enable + pnpm install + + - run: cargo test + fmt: name: Format and Check Deps runs-on: ubuntu-latest @@ -168,6 +206,7 @@ jobs: needs: - clippy - test + - test-musl - fmt steps: - run: exit 1 diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 988a9888..abcc4b4c 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -28,8 +28,10 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } nix = { workspace = true, features = ["uio"] } tokio = { workspace = true, features = ["bytes"] } -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies] fspy_preload_unix = { workspace = true } + +[target.'cfg(unix)'.dependencies] fspy_shared_unix = { workspace = true } nix = { workspace = true, features = ["fs", "process", "socket", "feature"] } diff --git a/crates/fspy/README.md b/crates/fspy/README.md index c671d73c..cf7fcba0 100644 --- a/crates/fspy/README.md +++ b/crates/fspy/README.md @@ -2,7 +2,7 @@ Run a command and capture all the paths it tries to access. -## macOS/Linux implementation +## macOS/Linux (glibc) implementation It uses `DYLD_INSERT_LIBRARIES` on macOS and `LD_PRELOAD` on Linux to inject a shared library that intercepts file system calls. 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 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`. +## Linux musl implementation + +On musl targets, only `seccomp_unotify`-based tracking is used (no preload library). + ## Windows implementation It uses [Detours](https://github.com/microsoft/Detours) to intercept file system calls. The implementation is in `src/windows`. diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index cfcdb78c..8a25ca06 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -2,6 +2,8 @@ #![feature(once_cell_try)] // Persist the injected DLL/shared library somewhere in the filesystem. +// Not needed on musl (seccomp-only tracking). +#[cfg(not(target_env = "musl"))] mod artifact; pub mod error; diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 96583f04..6e77f9cb 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -8,7 +8,9 @@ use std::{io, path::Path}; #[cfg(target_os = "linux")] use fspy_seccomp_unotify::supervisor::supervise; -use fspy_shared::ipc::{NativeStr, PathAccess, channel::channel}; +#[cfg(not(target_env = "musl"))] +use fspy_shared::ipc::NativeStr; +use fspy_shared::ipc::{PathAccess, channel::channel}; #[cfg(target_os = "macos")] use fspy_shared_unix::payload::Artifacts; use fspy_shared_unix::{ @@ -33,28 +35,39 @@ pub struct SpyImpl { #[cfg(target_os = "macos")] artifacts: Artifacts, + #[cfg(not(target_env = "musl"))] preload_path: Box, } +#[cfg(not(target_env = "musl"))] const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX")); impl SpyImpl { - /// Initialize the fs access spy by writing the preload library on disk - pub fn init_in(dir: &Path) -> io::Result { - use const_format::formatcp; - use xxhash_rust::const_xxh3::xxh3_128; - - use crate::artifact::Artifact; - - const PRELOAD_CDYLIB: Artifact = Artifact { - name: "fspy_preload", - content: PRELOAD_CDYLIB_BINARY, - hash: formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), + /// Initialize the fs access spy by writing the preload library on disk. + /// + /// On musl targets, we don't build a preload library — + /// only seccomp-based tracking is used. + pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { + #[cfg(not(target_env = "musl"))] + let preload_path = { + use const_format::formatcp; + use xxhash_rust::const_xxh3::xxh3_128; + + use crate::artifact::Artifact; + + const PRELOAD_CDYLIB: Artifact = Artifact { + name: "fspy_preload", + content: PRELOAD_CDYLIB_BINARY, + hash: formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), + }; + + let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; + preload_cdylib_path.as_path().into() }; - let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; Ok(Self { - preload_path: preload_cdylib_path.as_path().into(), + #[cfg(not(target_env = "musl"))] + preload_path, #[cfg(target_os = "macos")] artifacts: { let coreutils_path = macos_artifacts::COREUTILS_BINARY.write_to(dir, "")?; @@ -80,6 +93,7 @@ impl SpyImpl { #[cfg(target_os = "macos")] artifacts: self.artifacts.clone(), + #[cfg(not(target_env = "musl"))] preload_path: self.preload_path.clone(), #[cfg(target_os = "linux")] diff --git a/crates/fspy/src/unix/syscall_handler/mod.rs b/crates/fspy/src/unix/syscall_handler/mod.rs index bfed7d94..4b6f7947 100644 --- a/crates/fspy/src/unix/syscall_handler/mod.rs +++ b/crates/fspy/src/unix/syscall_handler/mod.rs @@ -92,6 +92,11 @@ impl_handler!( #[cfg(target_arch = "x86_64")] lstat, #[cfg(target_arch = "x86_64")] newfstatat, #[cfg(target_arch = "aarch64")] fstatat, + statx, + + #[cfg(target_arch = "x86_64")] access, + faccessat, + faccessat2, execve, execveat, diff --git a/crates/fspy/src/unix/syscall_handler/stat.rs b/crates/fspy/src/unix/syscall_handler/stat.rs index ae7da893..40d9f76f 100644 --- a/crates/fspy/src/unix/syscall_handler/stat.rs +++ b/crates/fspy/src/unix/syscall_handler/stat.rs @@ -32,4 +32,37 @@ impl SyscallHandler { ) -> io::Result<()> { self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) } + + /// statx(2) — modern replacement for stat/fstatat used by newer glibc. + pub(super) fn statx( + &mut self, + caller: Caller, + (dir_fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) + } + + /// access(2) — check file accessibility (e.g. existsSync in Node.js). + #[cfg(target_arch = "x86_64")] + pub(super) fn access(&mut self, caller: Caller, (path,): (CStrPtr,)) -> io::Result<()> { + self.handle_open(caller, Fd::cwd(), path, libc::O_RDONLY) + } + + /// faccessat(2) — check file accessibility relative to directory fd. + pub(super) fn faccessat( + &mut self, + caller: Caller, + (dir_fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) + } + + /// faccessat2(2) — extended faccessat with flags parameter. + pub(super) fn faccessat2( + &mut self, + caller: Caller, + (dir_fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) + } } diff --git a/crates/fspy/tests/rust_std.rs b/crates/fspy/tests/rust_std.rs index e16b6e25..7c5ed30a 100644 --- a/crates/fspy/tests/rust_std.rs +++ b/crates/fspy/tests/rust_std.rs @@ -52,7 +52,8 @@ async fn readdir() -> anyhow::Result<()> { let accesses = track_fn!(tmpdir_path.to_str().unwrap().to_owned(), |tmpdir_path: String| { std::env::set_current_dir(tmpdir_path).unwrap(); - let _ = std::fs::read_dir("hello_dir"); + // Consume at least one entry so that getdents64 fires on seccomp targets. + let _ = std::fs::read_dir("hello_dir").unwrap().next(); }) .await?; assert_contains(&accesses, tmpdir_path.join("hello_dir").as_path(), AccessMode::READ_DIR); diff --git a/crates/fspy_preload_unix/src/lib.rs b/crates/fspy_preload_unix/src/lib.rs index 2e5a6b5b..9728cd98 100644 --- a/crates/fspy_preload_unix/src/lib.rs +++ b/crates/fspy_preload_unix/src/lib.rs @@ -1,8 +1,12 @@ -#![cfg(unix)] -// required for defining inteposed `open`/`openat`(https://man7.org/linux/man-pages/man2/open.2.html) -#![feature(c_variadic)] +// On musl targets, fspy_preload_unix is not needed since we can track accesses via seccomp-only. +// Compile as an empty crate to avoid build failures from missing libc symbols. +#![cfg_attr(not(target_env = "musl"), feature(c_variadic))] +#[cfg(all(unix, not(target_env = "musl")))] mod client; +#[cfg(all(unix, not(target_env = "musl")))] mod interceptions; +#[cfg(all(unix, not(target_env = "musl")))] mod libc; +#[cfg(all(unix, not(target_env = "musl")))] mod macros; diff --git a/crates/fspy_seccomp_unotify/Cargo.toml b/crates/fspy_seccomp_unotify/Cargo.toml index be581839..9a4a2a08 100644 --- a/crates/fspy_seccomp_unotify/Cargo.toml +++ b/crates/fspy_seccomp_unotify/Cargo.toml @@ -33,3 +33,7 @@ workspace = true [lib] test = false doctest = false + +[[test]] +name = "arg_types" +required-features = ["supervisor", "target"] diff --git a/crates/fspy_seccomp_unotify/src/bindings/mod.rs b/crates/fspy_seccomp_unotify/src/bindings/mod.rs index cd8e7b2e..b6461a4f 100644 --- a/crates/fspy_seccomp_unotify/src/bindings/mod.rs +++ b/crates/fspy_seccomp_unotify/src/bindings/mod.rs @@ -45,7 +45,7 @@ pub fn notif_recv( notif_buf: &mut Alloced, ) -> nix::Result<()> { use std::os::fd::AsRawFd; - const SECCOMP_IOCTL_NOTIF_RECV: libc::c_ulong = 3_226_476_800; + const SECCOMP_IOCTL_NOTIF_RECV: libc::Ioctl = 3_226_476_800u64 as libc::Ioctl; // SAFETY: `notif_buf.zeroed()` returns a valid mutable pointer to a zeroed // `seccomp_notif` buffer with sufficient size for the kernel's notification struct let ret = unsafe { diff --git a/crates/fspy_seccomp_unotify/src/supervisor/listener.rs b/crates/fspy_seccomp_unotify/src/supervisor/listener.rs index 165949f2..0de6f89f 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/listener.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/listener.rs @@ -30,7 +30,7 @@ impl AsFd for NotifyListener { } } -const SECCOMP_IOCTL_NOTIF_SEND: libc::c_ulong = 3_222_806_785; +const SECCOMP_IOCTL_NOTIF_SEND: libc::Ioctl = 3_222_806_785u64 as libc::Ioctl; impl NotifyListener { /// Sends a `SECCOMP_USER_NOTIF_FLAG_CONTINUE` response for the given request ID. diff --git a/crates/fspy_seccomp_unotify/tests/arg_types.rs b/crates/fspy_seccomp_unotify/tests/arg_types.rs index 50aa83a5..810dd478 100644 --- a/crates/fspy_seccomp_unotify/tests/arg_types.rs +++ b/crates/fspy_seccomp_unotify/tests/arg_types.rs @@ -90,7 +90,7 @@ async fn run_in_pre_exec( async fn fd_and_path() -> Result<(), Box> { let syscalls = run_in_pre_exec(|| { set_current_dir("/")?; - let home_fd = nix::fcntl::open(c"/home", OFlag::O_PATH, Mode::empty())?; + let home_fd = openat(AT_FDCWD, c"/home", OFlag::O_PATH, Mode::empty())?; let _ = openat(home_fd, c"open_at_home", OFlag::O_RDONLY, Mode::empty()); let _ = openat(AT_FDCWD, c"openat_cwd", OFlag::O_RDONLY, Mode::empty()); Ok(()) diff --git a/crates/fspy_shared/Cargo.toml b/crates/fspy_shared/Cargo.toml index cc63d9cf..756e4512 100644 --- a/crates/fspy_shared/Cargo.toml +++ b/crates/fspy_shared/Cargo.toml @@ -32,3 +32,6 @@ workspace = true [lib] doctest = false + +[package.metadata.cargo-shear] +ignored = ["ctor"] diff --git a/crates/fspy_shared/src/ipc/channel/shm_io.rs b/crates/fspy_shared/src/ipc/channel/shm_io.rs index 3c6b8f3d..39a490d1 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io.rs @@ -323,7 +323,6 @@ impl> ShmReader { #[cfg(test)] mod tests { use std::{ - env::current_exe, process::{Child, Command}, sync::Arc, thread, @@ -669,43 +668,31 @@ mod tests { #[cfg(not(miri))] fn real_shm_across_processes() { use shared_memory::ShmemConf; + use subprocess_test::command_for_fn; - const CHILD_PROCESS_ENV: &str = "FSPY_SHM_IO_TEST_CHILD_PROCESS"; const CHILD_COUNT: usize = 12; const FRAME_COUNT_EACH_CHILD: usize = 100; - #[ctor::ctor] - fn child_process() { - if std::env::var_os(CHILD_PROCESS_ENV).is_none() { - return; - } - let mut args = std::env::args_os(); - args.next().unwrap(); // exe path - let shm_name = args.next().expect("shm name arg").into_string().unwrap(); - let child_index = args.next().expect("child name").into_string().unwrap(); - - let shm = ShmemConf::new().os_id(shm_name).open().unwrap(); - // SAFETY: `shm` is a freshly opened shared memory region with a valid pointer and size. - // Concurrent write access is safe because `ShmWriter` uses atomic operations. - let writer = unsafe { ShmWriter::new(shm) }; - for i in 0..FRAME_COUNT_EACH_CHILD { - let frame_data = format!("{child_index} {i}"); - assert!(writer.try_write_frame(frame_data.as_bytes())); - } - std::process::exit(0); - } - let shm = ShmemConf::new().size(1024 * 1024).create().unwrap(); - let shm_name = shm.get_os_id(); + let shm_name = shm.get_os_id().to_owned(); let children: Vec = (0..CHILD_COUNT) .map(|child_index| { - Command::new(current_exe().unwrap()) - .env(CHILD_PROCESS_ENV, "1") - .arg(shm_name) - .arg(child_index.to_string()) - .spawn() - .unwrap() + let cmd = command_for_fn!( + (shm_name.clone(), child_index), + |(shm_name, child_index): (String, usize)| { + let shm = ShmemConf::new().os_id(shm_name).open().unwrap(); + // SAFETY: `shm` is a freshly opened shared memory region with a valid + // pointer and size. Concurrent write access is safe because `ShmWriter` + // uses atomic operations. + let writer = unsafe { ShmWriter::new(shm) }; + for i in 0..FRAME_COUNT_EACH_CHILD { + let frame_data = std::format!("{child_index} {i}"); + assert!(writer.try_write_frame(frame_data.as_bytes())); + } + } + ); + Command::from(cmd).spawn().unwrap() }) .collect(); diff --git a/crates/fspy_shared_unix/src/payload.rs b/crates/fspy_shared_unix/src/payload.rs index d7369d0d..e23f7ce0 100644 --- a/crates/fspy_shared_unix/src/payload.rs +++ b/crates/fspy_shared_unix/src/payload.rs @@ -3,12 +3,15 @@ use std::os::unix::ffi::OsStringExt; use base64::{Engine as _, prelude::BASE64_STANDARD_NO_PAD}; use bincode::{Decode, Encode, config::standard}; use bstr::BString; -use fspy_shared::ipc::{NativeStr, channel::ChannelConf}; +#[cfg(not(target_env = "musl"))] +use fspy_shared::ipc::NativeStr; +use fspy_shared::ipc::channel::ChannelConf; #[derive(Debug, Encode, Decode)] pub struct Payload { pub ipc_channel_conf: ChannelConf, + #[cfg(not(target_env = "musl"))] pub preload_path: Box, #[cfg(target_os = "macos")] diff --git a/crates/fspy_shared_unix/src/spawn/linux/mod.rs b/crates/fspy_shared_unix/src/spawn/linux/mod.rs index 4d3faec7..0632999d 100644 --- a/crates/fspy_shared_unix/src/spawn/linux/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/linux/mod.rs @@ -1,12 +1,14 @@ +#[cfg(not(target_env = "musl"))] use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::Path}; use fspy_seccomp_unotify::{payload::SeccompPayload, target::install_target}; +#[cfg(not(target_env = "musl"))] use memmap2::Mmap; +#[cfg(not(target_env = "musl"))] +use crate::{elf, exec::ensure_env, open_exec::open_executable}; use crate::{ - elf, - exec::{Exec, ensure_env}, - open_exec::open_executable, + exec::Exec, payload::{EncodedPayload, PAYLOAD_ENV_NAME}, }; @@ -28,20 +30,26 @@ pub fn handle_exec( command: &mut Exec, encoded_payload: &EncodedPayload, ) -> nix::Result> { - let executable_fd = open_executable(Path::new(OsStr::from_bytes(&command.program)))?; - // SAFETY: The file descriptor is valid and we only read from the mapping. - let executable_mmap = unsafe { Mmap::map(&executable_fd) } - .map_err(|io_error| nix::Error::try_from(io_error).unwrap_or(nix::Error::UnknownErrno))?; - if elf::is_dynamically_linked_to_libc(executable_mmap)? { - ensure_env( - &mut command.envs, - LD_PRELOAD, - encoded_payload.payload.preload_path.as_os_str().as_bytes(), - )?; - ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, &encoded_payload.encoded_string)?; - Ok(None) - } else { - command.envs.retain(|(name, _)| name != LD_PRELOAD && name != PAYLOAD_ENV_NAME); - Ok(Some(PreExec(encoded_payload.payload.seccomp_payload.clone()))) + // On musl targets, LD_PRELOAD is not available (cdylib not supported). + // Always use seccomp-based tracking instead. + #[cfg(not(target_env = "musl"))] + { + let executable_fd = open_executable(Path::new(OsStr::from_bytes(&command.program)))?; + // SAFETY: The file descriptor is valid and we only read from the mapping. + let executable_mmap = unsafe { Mmap::map(&executable_fd) }.map_err(|io_error| { + nix::Error::try_from(io_error).unwrap_or(nix::Error::UnknownErrno) + })?; + if elf::is_dynamically_linked_to_libc(executable_mmap)? { + ensure_env( + &mut command.envs, + LD_PRELOAD, + encoded_payload.payload.preload_path.as_os_str().as_bytes(), + )?; + ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, &encoded_payload.encoded_string)?; + return Ok(None); + } } + + command.envs.retain(|(name, _)| name != LD_PRELOAD && name != PAYLOAD_ENV_NAME); + Ok(Some(PreExec(encoded_payload.payload.seccomp_payload.clone()))) } diff --git a/crates/subprocess_test/src/lib.rs b/crates/subprocess_test/src/lib.rs index ca418d77..c4d2bcd4 100644 --- a/crates/subprocess_test/src/lib.rs +++ b/crates/subprocess_test/src/lib.rs @@ -72,13 +72,57 @@ macro_rules! command_for_fn { }}; } +/// Read command-line arguments in a way that works during `.init_array`. +/// +/// On Linux, `std::env::args()` may return empty during `.init_array` +/// constructors (observed on musl targets) because the Rust runtime hasn't +/// initialized its argument storage yet. We fall back to reading +/// `/proc/self/cmdline` directly. +fn get_args() -> Vec { + let args: Vec = std::env::args().collect(); + if !args.is_empty() { + return args; + } + + // Fallback: read /proc/self/cmdline directly. + #[cfg(target_os = "linux")] + { + if let Some(args) = read_proc_cmdline() { + return args; + } + } + + args +} + +/// Read `/proc/self/cmdline` as a fallback that works before Rust runtime +/// initialization (during `.init_array` constructors). +#[cfg(target_os = "linux")] +fn read_proc_cmdline() -> Option> { + let buf = std::fs::read("/proc/self/cmdline").ok()?; + if buf.is_empty() { + return None; + } + + // /proc/self/cmdline has null-separated args with a trailing null. + // We must preserve empty args (e.g., empty base64 for `()` arg) but + // remove the trailing empty entry from the final null terminator. + let mut args: Vec = buf + .split(|&b| b == 0) + .filter_map(|s| std::str::from_utf8(s).ok().map(String::from)) + .collect(); + // Remove trailing empty string from the final null byte + if args.last().is_some_and(String::is_empty) { + args.pop(); + } + Some(args) +} + #[doc(hidden)] pub fn init_impl>(expected_id: &str, f: impl FnOnce(A)) { - let mut args = ::std::env::args(); + let args = get_args(); // - let (Some(_program), Some(current_id), Some(arg_base64)) = - (args.next(), args.next(), args.next()) - else { + let (Some(current_id), Some(arg_base64)) = (args.get(1), args.get(2)) else { return; }; if current_id != expected_id { diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index c944f88d..bbacf80f 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -316,10 +316,12 @@ pub async fn execute_spawn( // - path_accesses: only tracked when includes_auto is true (fspy inference) let (mut std_outputs, mut path_accesses, cache_metadata_and_inputs) = cache_metadata.map_or((None, None, None), |cache_metadata| { + // On musl targets, LD_PRELOAD-based tracking is unavailable but seccomp + // unotify provides equivalent file access tracing. let path_accesses = if cache_metadata.input_config.includes_auto { Some(TrackedPathAccesses::default()) } else { - None // Skip fspy when inference is disabled + None // Skip fspy when inference is disabled or unavailable }; (Some(Vec::new()), path_accesses, Some((cache_metadata, globbed_inputs))) });