From d00aaddc3b4644324fcfb19ea5c35c113f607136 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Fri, 11 Sep 2026 17:04:41 -0700 Subject: [PATCH 1/3] core: build on musl targets The libc crate types ptrace requests as c_uint on glibc but c_int on musl, ioctl requests as c_ulong versus c_int, and msg_controllen as size_t versus u32. The explicit glibc-shaped casts made a musl build fail with 19 type errors, so users wanting a static binary for Alpine had to fall back to a glibc base image. Let the constants carry their per-target type, cast ioctl requests to libc::Ioctl, and fold the one helper that named the request type into a PTRACE_CONT-only resume so no code has to spell the type at all. The statx test constant is spelled locally because the libc crate only exports it for glibc. Signed-off-by: Cong Wang --- crates/sandlock-core/src/checkpoint/capture.rs | 4 ++-- crates/sandlock-core/src/chroot/dispatch.rs | 4 ++-- crates/sandlock-core/src/cow/dispatch.rs | 4 ++-- crates/sandlock-core/src/freeze.rs | 4 ++-- crates/sandlock-core/src/network/send_engine.rs | 2 +- crates/sandlock-core/src/resource.rs | 12 ++++++------ crates/sandlock-core/src/seccomp/notif.rs | 12 ++++++------ crates/sandlock-core/src/sys/fs.rs | 6 ++++-- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/crates/sandlock-core/src/checkpoint/capture.rs b/crates/sandlock-core/src/checkpoint/capture.rs index 9b7b445a..bb76c311 100644 --- a/crates/sandlock-core/src/checkpoint/capture.rs +++ b/crates/sandlock-core/src/checkpoint/capture.rs @@ -9,14 +9,14 @@ use crate::error::{SandlockError, SandboxRuntimeError}; pub(crate) fn ptrace_seize(pid: i32) -> io::Result<()> { let ret = unsafe { - libc::ptrace(libc::PTRACE_SEIZE as libc::c_uint, pid, 0, 0) + libc::ptrace(libc::PTRACE_SEIZE, pid, 0, 0) }; if ret < 0 { return Err(io::Error::last_os_error()); } // PTRACE_INTERRUPT stops the tracee without SIGSTOP side effects let ret = unsafe { - libc::ptrace(libc::PTRACE_INTERRUPT as libc::c_uint, pid, 0, 0) + libc::ptrace(libc::PTRACE_INTERRUPT, pid, 0, 0) }; if ret < 0 { return Err(io::Error::last_os_error()); diff --git a/crates/sandlock-core/src/chroot/dispatch.rs b/crates/sandlock-core/src/chroot/dispatch.rs index 9d76b78d..7b7ee140 100644 --- a/crates/sandlock-core/src/chroot/dispatch.rs +++ b/crates/sandlock-core/src/chroot/dispatch.rs @@ -1024,7 +1024,7 @@ pub(crate) async fn handle_chroot_exec( let child_interp_fd = unsafe { libc::ioctl( notif_fd, - SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, + SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd_interp as *const _, ) }; @@ -1073,7 +1073,7 @@ pub(crate) async fn handle_chroot_exec( let child_fd = unsafe { libc::ioctl( notif_fd, - SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, + SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd as *const _, ) }; diff --git a/crates/sandlock-core/src/cow/dispatch.rs b/crates/sandlock-core/src/cow/dispatch.rs index 2555e85e..e845ac99 100644 --- a/crates/sandlock-core/src/cow/dispatch.rs +++ b/crates/sandlock-core/src/cow/dispatch.rs @@ -1095,7 +1095,7 @@ pub(crate) async fn handle_cow_exec( let child_fd = unsafe { libc::ioctl( notif_fd, - crate::sys::structs::SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, + crate::sys::structs::SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd as *const _, ) }; @@ -1380,7 +1380,7 @@ pub(crate) async fn handle_cow_chdir( let child_fd = unsafe { libc::ioctl( notif_fd, - crate::sys::structs::SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, + crate::sys::structs::SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd as *const _, ) }; diff --git a/crates/sandlock-core/src/freeze.rs b/crates/sandlock-core/src/freeze.rs index 40bdb7a5..d1d57649 100644 --- a/crates/sandlock-core/src/freeze.rs +++ b/crates/sandlock-core/src/freeze.rs @@ -119,7 +119,7 @@ fn seize_and_interrupt(tid: i32) -> io::Result { } let ret = unsafe { - libc::ptrace(libc::PTRACE_SEIZE as libc::c_uint, tid, 0, 0) + libc::ptrace(libc::PTRACE_SEIZE, tid, 0, 0) }; if ret < 0 { let err = io::Error::last_os_error(); @@ -132,7 +132,7 @@ fn seize_and_interrupt(tid: i32) -> io::Result { // before returning so we don't leave the task traced-but-running. let ret = unsafe { - libc::ptrace(libc::PTRACE_INTERRUPT as libc::c_uint, tid, 0, 0) + libc::ptrace(libc::PTRACE_INTERRUPT, tid, 0, 0) }; if ret < 0 { let err = io::Error::last_os_error(); diff --git a/crates/sandlock-core/src/network/send_engine.rs b/crates/sandlock-core/src/network/send_engine.rs index cafc5236..268ec7ab 100644 --- a/crates/sandlock-core/src/network/send_engine.rs +++ b/crates/sandlock-core/src/network/send_engine.rs @@ -42,7 +42,7 @@ fn send_materialized_at(fd: RawFd, m: &MaterializedMsg, offset: usize, flags: i3 } if let Some(ref c) = m.control { msg.msg_control = c.as_ptr() as *mut libc::c_void; - msg.msg_controllen = c.len(); + msg.msg_controllen = c.len() as _; } } msg.msg_iov = &iov as *const libc::iovec as *mut libc::iovec; diff --git a/crates/sandlock-core/src/resource.rs b/crates/sandlock-core/src/resource.rs index 398d45a2..a4610f77 100644 --- a/crates/sandlock-core/src/resource.rs +++ b/crates/sandlock-core/src/resource.rs @@ -315,7 +315,7 @@ fn process_creation_worker( | libc::PTRACE_O_TRACEVFORK | libc::PTRACE_O_TRACECLONE | libc::PTRACE_O_TRACESYSGOOD) as libc::c_ulong; - let ret = unsafe { libc::ptrace(libc::PTRACE_SEIZE as libc::c_uint, caller_tid, 0, opts) }; + let ret = unsafe { libc::ptrace(libc::PTRACE_SEIZE, caller_tid, 0, opts) }; if ret < 0 { let errno = io::Error::last_os_error().raw_os_error().unwrap_or(libc::EPERM); let _ = attached_tx.send(Err(errno)); @@ -441,13 +441,13 @@ fn run_creation_event_loop(caller_tid: i32, ctx: &Arc) -> io::Res // Some other signal-delivery-stop in the window: forward the pending // signal and keep waiting for the fork event. let inject = if stopsig == libc::SIGTRAP { 0 } else { stopsig as libc::c_ulong }; - ptrace_resume(caller_tid, libc::PTRACE_CONT, inject)?; + ptrace_cont(caller_tid, inject)?; } } -/// `ptrace(request, tid, 0, data)` returning an error on failure. -fn ptrace_resume(tid: i32, request: libc::c_uint, data: libc::c_ulong) -> io::Result<()> { - let ret = unsafe { libc::ptrace(request, tid, 0, data) }; +/// Resume `tid` with `sig` injected (0 for none), failing on error. +fn ptrace_cont(tid: i32, sig: libc::c_ulong) -> io::Result<()> { + let ret = unsafe { libc::ptrace(libc::PTRACE_CONT, tid, 0, sig) }; if ret < 0 { return Err(io::Error::last_os_error()); } @@ -461,7 +461,7 @@ fn handle_fork_event(caller_tid: i32, ctx: &Arc) -> io::Result io::Result { let mut notif: SeccompNotif = unsafe { std::mem::zeroed() }; let ret = unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV as libc::c_ulong, &mut notif as *mut _) + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV as libc::Ioctl, &mut notif as *mut _) }; if ret < 0 { Err(io::Error::last_os_error()) @@ -945,7 +945,7 @@ fn inject_fd_and_send(fd: RawFd, id: u64, srcfd: RawFd, newfd_flags: u32) -> io: newfd_flags, }; let ret = unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _) + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd as *const _) }; if ret < 0 { Err(io::Error::last_os_error()) @@ -965,7 +965,7 @@ fn inject_fd(fd: RawFd, id: u64, srcfd: RawFd, targetfd: i32) -> io::Result<()> newfd_flags: 0, }; let ret = unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _) + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::Ioctl, &addfd as *const _) }; if ret < 0 { Err(io::Error::last_os_error()) @@ -977,7 +977,7 @@ fn inject_fd(fd: RawFd, id: u64, srcfd: RawFd, targetfd: i32) -> io::Result<()> /// Raw ioctl to send a notification response. fn send_resp_raw(fd: RawFd, resp: &SeccompNotifResp) -> io::Result<()> { let ret = unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND as libc::c_ulong, resp as *const _) + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND as libc::Ioctl, resp as *const _) }; if ret < 0 { Err(io::Error::last_os_error()) @@ -990,7 +990,7 @@ fn send_resp_raw(fd: RawFd, resp: &SeccompNotifResp) -> io::Result<()> { /// ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) pub(crate) fn id_valid(fd: RawFd, id: u64) -> io::Result<()> { let ret = unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID as libc::c_ulong, &id as *const _) + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID as libc::Ioctl, &id as *const _) }; if ret < 0 { Err(io::Error::last_os_error()) @@ -1003,7 +1003,7 @@ pub(crate) fn id_valid(fd: RawFd, id: u64) -> io::Result<()> { fn try_set_sync_wakeup(fd: RawFd) { let flags: u64 = SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP as u64; unsafe { - libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS as libc::c_ulong, &flags as *const _); + libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS as libc::Ioctl, &flags as *const _); } } diff --git a/crates/sandlock-core/src/sys/fs.rs b/crates/sandlock-core/src/sys/fs.rs index dd610677..e394278c 100644 --- a/crates/sandlock-core/src/sys/fs.rs +++ b/crates/sandlock-core/src/sys/fs.rs @@ -692,6 +692,8 @@ mod tests { #[test] fn statx_confines_symlinked_parent() { + // The libc crate only exposes this constant for glibc targets. + const STATX_BASIC_STATS: u32 = 0x7ff; let tmp = TempDir::new().unwrap(); let root = tmp.path(); std::fs::write(root.join("f"), "data").unwrap(); @@ -699,9 +701,9 @@ mod tests { let mut buf = vec![0u8; 256]; // In-tree file resolves; escaping parent does not. - skip_if_nosys!(statx_in_root(root, "f", 0, libc::STATX_BASIC_STATS, &mut buf)).unwrap(); + skip_if_nosys!(statx_in_root(root, "f", 0, STATX_BASIC_STATS, &mut buf)).unwrap(); assert_eq!( - statx_in_root(root, "dirlink/group", 0, libc::STATX_BASIC_STATS, &mut buf), + statx_in_root(root, "dirlink/group", 0, STATX_BASIC_STATS, &mut buf), Err(libc::ENOENT) ); } From 69abd73624b0a04550f17e70de462f4d38b63103 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Fri, 11 Sep 2026 17:14:43 -0700 Subject: [PATCH 2/3] landlock: open rule paths with open(2), not OpenOptions musl defines O_SEARCH as O_PATH and folds it into O_ACCMODE, and std's OpenOptions masks custom flags with !O_ACCMODE. On musl the Landlock rule open therefore lost O_PATH and became a real O_RDONLY open, so a read rule on a FIFO blocked child setup forever. Calling open(2) directly keeps O_PATH on every libc. Signed-off-by: Cong Wang --- crates/sandlock-core/src/landlock.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/sandlock-core/src/landlock.rs b/crates/sandlock-core/src/landlock.rs index 490e32b9..5cef470d 100644 --- a/crates/sandlock-core/src/landlock.rs +++ b/crates/sandlock-core/src/landlock.rs @@ -150,19 +150,21 @@ pub fn abi_version() -> Result { /// Open `path` and add a Landlock path-beneath rule to `ruleset_fd`. fn add_path_rule(ruleset_fd: &OwnedFd, path: &Path, access: u64) -> Result<(), ConfinementError> { - use std::os::unix::fs::OpenOptionsExt; + use std::os::fd::FromRawFd; // Reference the path with O_PATH rather than opening it for I/O: O_PATH does // not block on FIFOs and needs no read permission on the target, so a rule // on a FIFO or a write-only/no-read path neither hangs nor fails here. An // O_PATH fd still supports fstat (the file-type check below) and serves as a - // valid parent_fd for landlock_add_rule. - let file = std::fs::OpenOptions::new() - .read(true) - .custom_flags(libc::O_PATH | libc::O_CLOEXEC) - .open(path) - .map_err(|e| { - ConfinementError::Landlock(format!("open path {:?} failed: {}", path, e)) - })?; + // valid parent_fd for landlock_add_rule. Call open(2) directly: musl folds + // O_PATH into O_ACCMODE, which OpenOptions::custom_flags masks out. + let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) + .map_err(|e| ConfinementError::Landlock(format!("open path {:?} failed: {}", path, e)))?; + let raw = unsafe { libc::open(c_path.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) }; + if raw < 0 { + let e = std::io::Error::last_os_error(); + return Err(ConfinementError::Landlock(format!("open path {:?} failed: {}", path, e))); + } + let file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(raw) }); // Directory-only access rights (READ_DIR, MAKE_*, REMOVE_*, REFER) make // landlock_add_rule fail with EINVAL on a non-directory path. Mask the From b6043bd8258ba2b2d23f242d6e36d0752108d090 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Fri, 11 Sep 2026 17:14:43 -0700 Subject: [PATCH 3/3] seccomp: stop the exec rewrite scan at the first NUL The scan for argv/envp strings that could run into the execve path buffer read the whole span from each candidate up to the buffer, then looked for a NUL. glibc's brk heap keeps that span mapped, but musl's allocator places allocations in separate mmap chunks with unmapped pages between them, so the read hit a hole and the child's execve failed with EFAULT under chroot. Check each page for a NUL before reading the next, which also matches what the kernel would copy. Signed-off-by: Cong Wang --- crates/sandlock-core/src/seccomp/notif.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/sandlock-core/src/seccomp/notif.rs b/crates/sandlock-core/src/seccomp/notif.rs index 9bd2ded0..8241a89e 100644 --- a/crates/sandlock-core/src/seccomp/notif.rs +++ b/crates/sandlock-core/src/seccomp/notif.rs @@ -1256,20 +1256,24 @@ fn read_exec_ptr_array( } } -/// Read exactly `len` bytes starting at `addr`, chunked at page boundaries. -fn read_exec_range( +/// Whether the `len` bytes at `addr` hold no NUL. Checked one page at a time +/// so the scan stops at a string's terminator before touching a later page, +/// which may be unmapped: musl's allocator leaves gaps between chunks. +fn nul_free_run( read: &mut impl FnMut(u64, usize) -> Result, NotifError>, addr: u64, len: usize, -) -> Result, NotifError> { - let mut out = Vec::with_capacity(len); +) -> Result { + let end = addr + len as u64; let mut cur = addr; - while out.len() < len { - let chunk = ((4096 - cur % 4096) as usize).min(len - out.len()); - out.extend_from_slice(&read(cur, chunk)?); + while cur < end { + let chunk = ((4096 - cur % 4096) as usize).min((end - cur) as usize); + if read(cur, chunk)?.contains(&0) { + return Ok(false); + } cur += chunk as u64; } - Ok(out) + Ok(true) } /// Read a NUL-terminated string (NUL excluded) of at most @@ -1358,8 +1362,7 @@ fn plan_exec_rewrite( below.dedup(); let mut nul_free_from = path_ptr; for &p in below.iter().rev() { - let seg = read_exec_range(read, p, (nul_free_from - p) as usize)?; - if seg.contains(&0) { + if !nul_free_run(read, p, (nul_free_from - p) as usize)? { break; } relocate(&mut buf, &mut relocated, read, p)?;