From 0a7b800929fc59a0bad0c1653e545b2d705c37fb Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:12:53 +0200 Subject: [PATCH 001/135] chore: add WRAP_PROGRESS.md --- system/linux/syscalls/WRAP_PROGRESS.md | 224 +++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 system/linux/syscalls/WRAP_PROGRESS.md diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md new file mode 100644 index 00000000..b1d01492 --- /dev/null +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -0,0 +1,224 @@ +# Wrap Progress + +This checklist tracks the next layer above the raw Linux syscall wrappers. +The goal of this layer is to preserve the raw syscall shape while removing +the most mechanical Rust rough edges. + +## Scope + +The wrap layer should: + +- convert raw syscall return values into `Result`; +- use syscall-specific error enums, with an `Other(Errno)` escape hatch when + the syscall can delegate to file systems, drivers, protocols, security + modules, or other helpers with wider reachable errors; +- use shared slices for `*const T` plus length arguments; +- use mutable slices for `*mut T` plus length arguments when the kernel reads + and writes initialized elements; +- use `MaybeUninit` for output buffers or output slots that the kernel + initializes; +- use shared references for non-null single-item `*const T` arguments; +- use mutable references for non-null single-item `*mut T` arguments when the + pointed-to value must already be initialized; +- use `Option<&T>`, `Option<&mut T>`, or `Option<&mut MaybeUninit>` when + null is a meaningful syscall input; +- use `&CStr` for NUL-terminated `*const Char` strings; +- stay safe whenever the wrapper removes all caller-visible memory-safety + preconditions; +- use strict-provenance APIs when converting pointers before passing raw + integer syscall arguments. + +The wrap layer should not introduce ownership, buffering, retry policy, path +encoding policy, automatic descriptor closing, or broad semantic convenience. +Those belong in higher layers. + +## Module Layout + +The existing `sys` module remains the raw syscall layer. Its wrappers keep the +exact kernel-facing ABI shape: raw pointer arguments, raw integer return values, +and `unsafe` where the caller must uphold memory-safety preconditions. + +The crate root exports the wrap layer. `lib.rs` should mirror the shape of +`sys.rs`: declare per-syscall wrapper modules and re-export each public wrapper +and its syscall-specific error type from the crate root. For example, +`celer_system_linux_syscalls::read` is the wrapped API, while +`celer_system_linux_syscalls::sys::read` is the raw API. + +Architecture availability should mirror the raw layer. Top-level wrapped +modules and re-exports must use the same kind of `cfg` gates as the raw +syscalls so only syscalls available on the current target are exposed. + +Historical Linux 1.0 ABI wrappers should be exposed from a top-level +`linux_1_0` module, mirroring `sys::linux_1_0`. + +## Errno + +The shared errno representation should be an enum. Common errno values should +be listed explicitly, and a raw variant should carry any errno value that the +crate does not list explicitly. + +The raw variant should hold a `u16` value. When a raw syscall return is in the +kernel errno range `-4095..=-1`, the stored errno value is the negated kernel +return value. + +Syscall-specific error enums should use explicit variants for source-verified +reachable errors. They should include an `Other(Errno)` variant when the syscall +can return delegated, object-specific, or otherwise open-ended errors. + +## Done Criteria + +A checked syscall means: + +- the wrapper exists and is public from the wrap layer; +- each pointer argument has been classified as input, output, in/out, nullable, + or NUL-terminated string from kernel source; +- each safe/unsafe choice follows the repository unsafe rule; +- the success return type is semantic for this syscall; +- reachable direct errors are represented in a syscall-specific error type; +- delegated or object-specific errors use `Other(Errno)` unless the reachable + set is source-verified to be closed; +- tests cover success and at least one practical error path when feasible. + +## Helpers + +Shared conversion helpers should live in a private `helpers` module. + +## Checklist + +- [ ] `access` - x86, x86_64 +- [ ] `acct` +- [ ] `adjtimex` +- [ ] `alarm` - x86, x86_64 +- [ ] `brk` +- [ ] `chdir` +- [ ] `chmod` - x86, x86_64 +- [ ] `chroot` +- [ ] `close` +- [ ] `creat` - x86, x86_64 +- [ ] `create_module` - x86 +- [ ] `delete_module` +- [ ] `dup` +- [ ] `dup2` - x86, x86_64 +- [ ] `execve` +- [ ] `exit` +- [ ] `fchdir` +- [ ] `fchmod` +- [ ] `fchown16` - x86 +- [ ] `fcntl` +- [ ] `fork` - x86 +- [ ] `fstatfs` +- [ ] `fsync` +- [ ] `ftruncate` +- [ ] `get_kernel_syms` - x86 +- [ ] `getegid16` - x86 +- [ ] `geteuid16` - x86 +- [ ] `getgid16` - x86 +- [ ] `getgroups16` - x86 +- [ ] `getitimer` +- [ ] `getpgid` +- [ ] `getpgrp` - x86, x86_64 +- [ ] `getpid` +- [ ] `getppid` +- [ ] `getpriority` +- [ ] `getrlimit` +- [ ] `getrusage` +- [ ] `gettimeofday` +- [ ] `getuid16` - x86 +- [ ] `idle` - x86 +- [ ] `init_module` +- [ ] `ioctl` +- [ ] `ioperm` - x86 +- [ ] `iopl` - x86 +- [ ] `ipc` - x86 +- [ ] `kill` +- [ ] `lchown16` - x86 +- [ ] `link` - x86, x86_64 +- [ ] `lseek` +- [ ] `mknod` - x86, x86_64 +- [ ] `mkdir` - x86, x86_64 +- [ ] `mmap` +- [ ] `modify_ldt` - x86 +- [ ] `mount` +- [ ] `munmap` +- [ ] `newfstat` +- [ ] `newlstat` - x86, x86_64 +- [ ] `newuname` +- [ ] `nice` - x86 +- [ ] `oldfstat` - x86 +- [ ] `oldlstat` - x86 +- [ ] `oldolduname` - x86 +- [ ] `oldstat` - x86 +- [ ] `olduname` - x86 +- [ ] `open` - x86, x86_64 +- [ ] `pause` - x86, x86_64 +- [ ] `pipe` - x86, x86_64 +- [ ] `ptrace` +- [ ] `read` +- [ ] `readdir` - x86 +- [ ] `readlink` - x86, x86_64 +- [ ] `reboot` +- [ ] `rename` - x86, x86_64 +- [ ] `rmdir` - x86, x86_64 +- [ ] `select` - x86, x86_64 +- [ ] `setdomainname` +- [ ] `setgid16` - x86 +- [ ] `setgroups16` - x86 +- [ ] `sethostname` +- [ ] `setitimer` +- [ ] `setpgid` +- [ ] `setpriority` +- [ ] `setregid16` - x86 +- [ ] `setreuid16` - x86 +- [ ] `setrlimit` +- [ ] `setsid` +- [ ] `settimeofday` +- [ ] `setuid16` - x86 +- [ ] `sgetmask` - x86 +- [ ] `sigaction` - x86 +- [ ] `signal` - x86 +- [ ] `sigpending` - x86 +- [ ] `sigprocmask` - x86 +- [ ] `sigreturn` - x86 +- [ ] `sigsuspend` - x86 +- [ ] `socketcall` - x86 +- [ ] `ssetmask` - x86 +- [ ] `stat` - x86, x86_64 +- [ ] `statfs` +- [ ] `stime` - x86 +- [ ] `swapoff` +- [ ] `swapon` +- [ ] `symlink` - x86, x86_64 +- [ ] `sync` +- [ ] `sysinfo` +- [ ] `syslog` +- [ ] `time` - x86, x86_64 +- [ ] `times` +- [ ] `truncate` +- [ ] `umask` +- [ ] `umount` +- [ ] `unlink` - x86, x86_64 +- [ ] `uselib` - x86 +- [ ] `ustat` - x86, x86_64 +- [ ] `utime` - x86, x86_64 +- [ ] `vhangup` +- [ ] `vm86` - x86 +- [ ] `wait4` +- [ ] `waitpid` - x86 +- [ ] `write` + +## Linux 1.0 Historical Variants + +These raw wrappers are currently exposed under `sys::linux_1_0` on x86. Decide +whether the wrap layer mirrors them before checking them off. + +- [ ] `linux_1_0::fstatfs` +- [ ] `linux_1_0::ftruncate` +- [ ] `linux_1_0::init_module` +- [ ] `linux_1_0::newfstat` +- [ ] `linux_1_0::newlstat` +- [ ] `linux_1_0::setrlimit` +- [ ] `linux_1_0::setup` +- [ ] `linux_1_0::stat` +- [ ] `linux_1_0::statfs` +- [ ] `linux_1_0::sysinfo` +- [ ] `linux_1_0::truncate` From 0f16b0937a7ab8c4c92868df9ad5af85486b3cfa Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:25:28 +0200 Subject: [PATCH 002/135] feat(syscalls): add access wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/access.rs | 82 ++++++++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 56 ++++++++++++++++++ system/linux/syscalls/src/lib.rs | 6 ++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/access.rs create mode 100644 system/linux/syscalls/src/errno.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index b1d01492..37836afd 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -85,7 +85,7 @@ Shared conversion helpers should live in a private `helpers` module. ## Checklist -- [ ] `access` - x86, x86_64 +- [x] `access` - x86, x86_64 - [ ] `acct` - [ ] `adjtimex` - [ ] `alarm` - x86, x86_64 diff --git a/system/linux/syscalls/src/access.rs b/system/linux/syscalls/src/access.rs new file mode 100644 index 00000000..fd50c4e4 --- /dev/null +++ b/system/linux/syscalls/src/access.rs @@ -0,0 +1,82 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::sys; + +/// Errors returned by [`access`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum AccessError { + Einval, + Enomem, + Other(Errno), +} + +impl From for AccessError { + fn from(value: isize) -> Self { + match Errno::from(value) { + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Check whether the calling process can access a file by pathname. +/// +/// On success, returns `Ok(())` when the requested access is permitted. +/// +/// # Errors +/// - `EINVAL`: `mode` contains unsupported bits. +/// - `ENOMEM`: temporary override credential allocation failed. +/// - `Other(..)`: pathname resolution, inode permissions, or filesystem-specific +/// checks returned a delegated errno. +pub fn access(pathname: &CStr, mode: Int) -> Result<(), AccessError> { + // SAFETY: the `CStr` argument guarantees a valid, NUL-terminated + // pathname pointer for the duration of the call. + let ret = unsafe { sys::access(pathname.as_ptr(), mode) }; + + if Errno::is_errno(ret) { + Err(AccessError::from(ret)) + } else { + Ok(()) + } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + use std::fs::File; + use std::time::{SystemTime, UNIX_EPOCH}; + + use super::{AccessError, access}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_access_{now}")); + path + } + + #[test] + fn test_access_ok() { + let path = temp_path(); + File::create(&path).unwrap(); + let path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(access(path.as_c_str(), 0), Ok(())); + } + + #[test] + fn test_access_einval() { + let path = CString::new("/tmp").unwrap(); + + assert_eq!(access(path.as_c_str(), 0x8000), Err(AccessError::Einval)); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs new file mode 100644 index 00000000..7297170a --- /dev/null +++ b/system/linux/syscalls/src/errno.rs @@ -0,0 +1,56 @@ +/// A Linux errno value. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum Errno { + Eacces, + Eexist, + Einval, + Enoent, + Enomem, + Eperm, + Erofs, + Raw(u16), +} + +impl Errno { + fn from_raw(raw: u16) -> Self { + match raw { + 13 => Self::Eacces, + 17 => Self::Eexist, + 22 => Self::Einval, + 2 => Self::Enoent, + 12 => Self::Enomem, + 1 => Self::Eperm, + 30 => Self::Erofs, + other => Self::Raw(other), + } + } + + /// Returns `true` when `value` is in Linux's kernel errno return range. + pub const fn is_errno(value: isize) -> bool { + (-4095..=-1).contains(&value) + } +} + +impl From for Errno { + fn from(value: isize) -> Self { + if Self::is_errno(value) { + Self::from_raw((-value) as u16) + } else { + Self::Raw(value as u16) + } + } +} + +#[cfg(test)] +mod tests { + use super::Errno; + + #[test] + fn test_is_errno_range() { + assert!(!Errno::is_errno(0)); + assert!(Errno::is_errno(-1)); + assert!(Errno::is_errno(-4095)); + assert!(!Errno::is_errno(-4096)); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 1d849684..01dd5ad5 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -10,4 +10,10 @@ pub mod arch; +mod access; +mod errno; pub mod sys; + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use access::{AccessError, access}; +pub use errno::Errno; From 93231de6b762b0ac35450b3348aa95d680789c48 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:37:25 +0200 Subject: [PATCH 003/135] feat(syscalls): add acct wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/acct.rs | 71 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/acct.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 37836afd..87a97e00 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -86,7 +86,7 @@ Shared conversion helpers should live in a private `helpers` module. ## Checklist - [x] `access` - x86, x86_64 -- [ ] `acct` +- [x] `acct` - [ ] `adjtimex` - [ ] `alarm` - x86, x86_64 - [ ] `brk` diff --git a/system/linux/syscalls/src/acct.rs b/system/linux/syscalls/src/acct.rs new file mode 100644 index 00000000..a0b720fa --- /dev/null +++ b/system/linux/syscalls/src/acct.rs @@ -0,0 +1,71 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::sys; + +/// Errors returned by [`acct`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum AcctError { + Eperm, + Other(Errno), +} + +impl From for AcctError { + fn from(value: isize) -> Self { + match Errno::from(value) { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Enable or disable process accounting through the historical `acct` slot. +/// +/// Passing `None` disables accounting for the current PID namespace. +/// Passing `Some(path)` enables accounting on the named file. +/// +/// # Errors +/// - `EPERM`: the caller lacks `CAP_SYS_PACCT`. +/// - `Other(..)`: pathname resolution, file opening, or accounting-file +/// validation returned a delegated errno. +pub fn acct(name: Option<&CStr>) -> Result<(), AcctError> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated string for the + // duration of the syscall, and `None` maps to a null pointer. + let ret = + unsafe { sys::acct(name.map_or(core::ptr::null(), CStr::as_ptr)) }; + + if Errno::is_errno(ret) { + Err(AcctError::from(ret)) + } else { + Ok(()) + } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use super::{AcctError, acct}; + + #[test] + fn test_acct_none() { + match acct(None) { + Ok(()) | Err(AcctError::Eperm) => {} + Err(err) => panic!("acct(None) returned unexpected error: {err:?}"), + } + } + + #[test] + fn test_acct_invalid_path() { + let path = + CString::new("/definitely/not/a/real/celer-acct-file").unwrap(); + + match acct(Some(path.as_c_str())) { + Err(AcctError::Eperm) => {} + Err(AcctError::Other(_)) => {} + Ok(()) => panic!("acct(Some(...)) unexpectedly succeeded"), + } + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 01dd5ad5..1644817b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -11,9 +11,11 @@ pub mod arch; mod access; +mod acct; mod errno; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; +pub use acct::{AcctError, acct}; pub use errno::Errno; From 6787b10cbb7f5b52a58a52efda877ed7646c9376 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:37:58 +0200 Subject: [PATCH 004/135] feat: add adjtimex wrap layer --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/adjtimex.rs | 123 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/adjtimex.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 87a97e00..ff792535 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -87,7 +87,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `access` - x86, x86_64 - [x] `acct` -- [ ] `adjtimex` +- [x] `adjtimex` - [ ] `alarm` - x86, x86_64 - [ ] `brk` - [ ] `chdir` diff --git a/system/linux/syscalls/src/adjtimex.rs b/system/linux/syscalls/src/adjtimex.rs new file mode 100644 index 00000000..29bd5f90 --- /dev/null +++ b/system/linux/syscalls/src/adjtimex.rs @@ -0,0 +1,123 @@ +use celer_system_linux_ctypes::{ADJ_ADJTIME, Int, Timex}; + +use crate::errno::Errno; +use crate::sys; + +/// Errors returned by [`adjtimex`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum AdjtimexError { + Efault, + Einval, + Eperm, + Enodev, + Other(Errno), +} + +impl From for AdjtimexError { + fn from(value: isize) -> Self { + match Errno::from(value) { + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + Errno::Raw(19) => Self::Enodev, + Errno::Raw(14) => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Read or adjust kernel clock discipline parameters through `struct timex`. +/// +/// On success, returns the kernel's time-state code and leaves the updated +/// `timex` contents in `txc`. +/// +/// # Errors +/// - `EFAULT`: the kernel could not copy the `timex` contents in or out. +/// - `EINVAL`: the supplied mode bits or parameter ranges are invalid. +/// - `EPERM`: the caller lacks permission to perform the requested update. +/// - `ENODEV`: the core clock is not valid. +pub fn adjtimex(txc: &mut Timex) -> Result { + // SAFETY: `&mut Timex` guarantees a valid, writable user buffer for the + // duration of the syscall call site. + let ret = unsafe { sys::adjtimex(txc as *mut Timex) }; + + if Errno::is_errno(ret as isize) { + Err(AdjtimexError::from(ret as isize)) + } else { + Ok(ret) + } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{ + ADJ_ADJTIME, ADJ_TICK, Timex, + }; + + use super::{AdjtimexError, adjtimex}; + + fn zero_timex() -> Timex { + Timex { + modes: 0, + offset: 0, + freq: 0, + maxerror: 0, + esterror: 0, + status: 0, + constant: 0, + precision: 0, + tolerance: 0, + time: celer_system_linux_ctypes::Timeval { + tv_sec: 0, + tv_usec: 0, + }, + tick: 0, + ppsfreq: 0, + jitter: 0, + shift: 0, + stabil: 0, + jitcnt: 0, + calcnt: 0, + errcnt: 0, + stbcnt: 0, + tai: 0, + __padding: [0; 11], + } + } + + #[test] + fn test_adjtimex_query_succeeds() { + let mut txc = zero_timex(); + let state = adjtimex(&mut txc).expect("adjtimex query should succeed"); + + assert!(matches!(state, 0..=5), "unexpected adjtimex state: {state}"); + assert!(txc.maxerror >= 0, "maxerror should be non-negative"); + assert!(txc.esterror >= 0, "esterror should be non-negative"); + } + + #[test] + fn test_adjtimex_rejects_invalid_mode_mix() { + let mut txc = zero_timex(); + txc.modes = ADJ_ADJTIME; + + assert_eq!( + adjtimex(&mut txc), + Err(AdjtimexError::Einval), + "ADJ_ADJTIME without ADJ_OFFSET_SINGLESHOT should be invalid" + ); + } + + #[test] + fn test_adjtimex_rejects_unprivileged_tick_change() { + let mut txc = zero_timex(); + txc.modes = ADJ_TICK; + txc.tick = 10_000; + + assert_eq!( + adjtimex(&mut txc), + Err(AdjtimexError::Eperm), + "unprivileged tick changes should be rejected" + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 1644817b..059c0820 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -12,10 +12,12 @@ pub mod arch; mod access; mod acct; +mod adjtimex; mod errno; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; pub use acct::{AcctError, acct}; +pub use adjtimex::{AdjtimexError, AdjtimexState, adjtimex}; pub use errno::Errno; From 8194fb6cb77c16e8d996fe1e454a411eebe00826 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:38:00 +0200 Subject: [PATCH 005/135] feat(syscalls): wrap alarm syscall --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/alarm.rs | 34 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 +++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/alarm.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index ff792535..8e304dee 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -88,7 +88,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `access` - x86, x86_64 - [x] `acct` - [x] `adjtimex` -- [ ] `alarm` - x86, x86_64 +- [x] `alarm` - x86, x86_64 - [ ] `brk` - [ ] `chdir` - [ ] `chmod` - x86, x86_64 diff --git a/system/linux/syscalls/src/alarm.rs b/system/linux/syscalls/src/alarm.rs new file mode 100644 index 00000000..7f7612e9 --- /dev/null +++ b/system/linux/syscalls/src/alarm.rs @@ -0,0 +1,34 @@ +use celer_system_linux_ctypes::UnsignedInt; + +use crate::sys; + +/// Arm the process alarm timer and return the remaining seconds on any prior alarm. +/// +/// On success, returns the whole seconds remaining on a previously armed alarm, +/// or `0` when no alarm was pending. +pub fn alarm(seconds: UnsignedInt) -> UnsignedInt { + // SAFETY: the raw syscall takes only an integer argument and has no caller-visible + // memory-safety preconditions. + let ret = unsafe { sys::alarm(seconds) }; + + ret as UnsignedInt +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::sys::test_support::process_global_state_guard; + + use super::alarm; + + #[test] + fn test_alarm_roundtrip() { + let _guard = process_global_state_guard(); + + let old = alarm(2); + let cleared = alarm(0); + + assert!(old <= 2); + assert!(cleared <= 2); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 059c0820..16c772ad 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -13,6 +13,8 @@ pub mod arch; mod access; mod acct; mod adjtimex; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod alarm; mod errno; pub mod sys; @@ -20,4 +22,6 @@ pub mod sys; pub use access::{AccessError, access}; pub use acct::{AcctError, acct}; pub use adjtimex::{AdjtimexError, AdjtimexState, adjtimex}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use alarm::alarm; pub use errno::Errno; From 479c49f231c1ce683294f7ad4392deb890a35d40 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 19:56:00 +0200 Subject: [PATCH 006/135] feat: add brk wrap-layer wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/brk.rs | 85 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/brk.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 8e304dee..a8e64200 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -89,7 +89,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `acct` - [x] `adjtimex` - [x] `alarm` - x86, x86_64 -- [ ] `brk` +- [x] `brk` - [ ] `chdir` - [ ] `chmod` - x86, x86_64 - [ ] `chroot` diff --git a/system/linux/syscalls/src/brk.rs b/system/linux/syscalls/src/brk.rs new file mode 100644 index 00000000..9531e25f --- /dev/null +++ b/system/linux/syscalls/src/brk.rs @@ -0,0 +1,85 @@ +use celer_system_linux_ctypes::UnsignedLong; + +use crate::errno::Errno; +use crate::sys; + +/// Errors returned by [`brk`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrkError { + Eintr, + Rejected { current: UnsignedLong }, +} + +/// Set the program break to `addr` and return the resulting break on success. +/// +/// The kernel returns the current program break value even when it declines +/// the request, so this wrapper treats a returned value equal to `addr` as +/// success and reports any other non-errno return as a semantic rejection. +/// +/// # Kernel Support +/// - Introduced: Linux 0.10 +/// - Behavior changes: modern kernels can return raw `-EINTR` if interrupted +/// while acquiring the mmap write lock +/// - Availability: always present on supported Linux kernels +/// +/// # Required Privileges +/// - None +/// +/// # Safety +/// - `addr` must not move the program break in a way that invalidates Rust or +/// allocator-managed memory assumptions in the current process. +/// +/// # Errors +/// - `EINTR`: the kernel was interrupted while acquiring the mmap write lock. +/// - `Rejected { .. }`: the kernel left the program break unchanged. +/// +/// # References +/// - `man` [page](https://man7.org/linux/man-pages/man2/brk.2.html) +/// - Stable: [v6.19](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/mmap.c?h=v6.19#n115) +/// - LTS: [v6.18.18](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/mm/mmap.c?h=v6.18.18#n115) +/// +/// # Historical References +/// - First appearance: [Linux 0.10](https://git.kernel.org/pub/scm/linux/kernel/git/history/history.git/tree/kernel/sys.c?h=0.10#n168) +pub unsafe fn brk(addr: UnsignedLong) -> Result { + // SAFETY: the caller must uphold the process-wide allocator and memory-map + // invariants required when changing the program break. + let ret = unsafe { sys::brk(addr) }; + + if Errno::is_errno(ret as isize) { + return Err(BrkError::Eintr); + } + + let current = ret; + if current == addr { + Ok(current) + } else { + Err(BrkError::Rejected { current }) + } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::UnsignedLong; + + use crate::sys::test_support::process_global_state_guard; + + use super::{BrkError, brk}; + + #[test] + fn test_brk_roundtrip() { + let _guard = process_global_state_guard(); + + let current = unsafe { brk(0 as UnsignedLong) }; + let current = match current { + Ok(value) => value, + Err(BrkError::Rejected { current }) => current, + Err(BrkError::Eintr) => panic!("brk(0) was interrupted"), + }; + assert_ne!(current, 0); + + let same = unsafe { brk(current) }; + assert_eq!(same, Ok(current)); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 16c772ad..25cc098a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -15,6 +15,7 @@ mod acct; mod adjtimex; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod alarm; +mod brk; mod errno; pub mod sys; @@ -24,4 +25,5 @@ pub use acct::{AcctError, acct}; pub use adjtimex::{AdjtimexError, AdjtimexState, adjtimex}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use alarm::alarm; +pub use brk::{BrkError, brk}; pub use errno::Errno; From 7967d0c4dac4076a851307da2dd2fab699278a1c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:04:56 +0200 Subject: [PATCH 007/135] refactor: centralize syscall wrapper result mapping --- system/linux/syscalls/src/access.rs | 14 +++----- system/linux/syscalls/src/acct.rs | 14 +++----- system/linux/syscalls/src/adjtimex.rs | 24 +++++-------- system/linux/syscalls/src/alarm.rs | 4 +-- system/linux/syscalls/src/brk.rs | 39 ++++++++++++++------ system/linux/syscalls/src/errno.rs | 30 +++++++++++----- system/linux/syscalls/src/helpers.rs | 51 +++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 ++- 8 files changed, 124 insertions(+), 56 deletions(-) create mode 100644 system/linux/syscalls/src/helpers.rs diff --git a/system/linux/syscalls/src/access.rs b/system/linux/syscalls/src/access.rs index fd50c4e4..f1fa28bc 100644 --- a/system/linux/syscalls/src/access.rs +++ b/system/linux/syscalls/src/access.rs @@ -3,20 +3,20 @@ use core::ffi::CStr; use celer_system_linux_ctypes::Int; use crate::errno::Errno; +use crate::helpers::unit_from_ret; use crate::sys; /// Errors returned by [`access`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum AccessError { Einval, Enomem, Other(Errno), } -impl From for AccessError { - fn from(value: isize) -> Self { - match Errno::from(value) { +impl AccessError { + fn from_errno(errno: Errno) -> Self { + match errno { Errno::Einval => Self::Einval, Errno::Enomem => Self::Enomem, errno => Self::Other(errno), @@ -38,11 +38,7 @@ pub fn access(pathname: &CStr, mode: Int) -> Result<(), AccessError> { // pathname pointer for the duration of the call. let ret = unsafe { sys::access(pathname.as_ptr(), mode) }; - if Errno::is_errno(ret) { - Err(AccessError::from(ret)) - } else { - Ok(()) - } + unit_from_ret(ret as isize, AccessError::from_errno) } #[cfg(test)] diff --git a/system/linux/syscalls/src/acct.rs b/system/linux/syscalls/src/acct.rs index a0b720fa..d18d0693 100644 --- a/system/linux/syscalls/src/acct.rs +++ b/system/linux/syscalls/src/acct.rs @@ -1,19 +1,19 @@ use core::ffi::CStr; use crate::errno::Errno; +use crate::helpers::unit_from_ret; use crate::sys; /// Errors returned by [`acct`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum AcctError { Eperm, Other(Errno), } -impl From for AcctError { - fn from(value: isize) -> Self { - match Errno::from(value) { +impl AcctError { + fn from_errno(errno: Errno) -> Self { + match errno { Errno::Eperm => Self::Eperm, errno => Self::Other(errno), } @@ -35,11 +35,7 @@ pub fn acct(name: Option<&CStr>) -> Result<(), AcctError> { let ret = unsafe { sys::acct(name.map_or(core::ptr::null(), CStr::as_ptr)) }; - if Errno::is_errno(ret) { - Err(AcctError::from(ret)) - } else { - Ok(()) - } + unit_from_ret(ret as isize, AcctError::from_errno) } #[cfg(test)] diff --git a/system/linux/syscalls/src/adjtimex.rs b/system/linux/syscalls/src/adjtimex.rs index 29bd5f90..def14f96 100644 --- a/system/linux/syscalls/src/adjtimex.rs +++ b/system/linux/syscalls/src/adjtimex.rs @@ -1,11 +1,11 @@ -use celer_system_linux_ctypes::{ADJ_ADJTIME, Int, Timex}; +use celer_system_linux_ctypes::{Int, Timex}; use crate::errno::Errno; +use crate::helpers::result_from_ret; use crate::sys; /// Errors returned by [`adjtimex`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum AdjtimexError { Efault, Einval, @@ -14,13 +14,13 @@ pub enum AdjtimexError { Other(Errno), } -impl From for AdjtimexError { - fn from(value: isize) -> Self { - match Errno::from(value) { +impl AdjtimexError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, Errno::Einval => Self::Einval, + Errno::Enodev => Self::Enodev, Errno::Eperm => Self::Eperm, - Errno::Raw(19) => Self::Enodev, - Errno::Raw(14) => Self::Efault, errno => Self::Other(errno), } } @@ -41,19 +41,13 @@ pub fn adjtimex(txc: &mut Timex) -> Result { // duration of the syscall call site. let ret = unsafe { sys::adjtimex(txc as *mut Timex) }; - if Errno::is_errno(ret as isize) { - Err(AdjtimexError::from(ret as isize)) - } else { - Ok(ret) - } + result_from_ret(ret as isize, |ret| ret as Int, AdjtimexError::from_errno) } #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use celer_system_linux_ctypes::{ - ADJ_ADJTIME, ADJ_TICK, Timex, - }; + use celer_system_linux_ctypes::{ADJ_ADJTIME, ADJ_TICK, Timex}; use super::{AdjtimexError, adjtimex}; diff --git a/system/linux/syscalls/src/alarm.rs b/system/linux/syscalls/src/alarm.rs index 7f7612e9..986f417a 100644 --- a/system/linux/syscalls/src/alarm.rs +++ b/system/linux/syscalls/src/alarm.rs @@ -7,9 +7,7 @@ use crate::sys; /// On success, returns the whole seconds remaining on a previously armed alarm, /// or `0` when no alarm was pending. pub fn alarm(seconds: UnsignedInt) -> UnsignedInt { - // SAFETY: the raw syscall takes only an integer argument and has no caller-visible - // memory-safety preconditions. - let ret = unsafe { sys::alarm(seconds) }; + let ret = sys::alarm(seconds); ret as UnsignedInt } diff --git a/system/linux/syscalls/src/brk.rs b/system/linux/syscalls/src/brk.rs index 9531e25f..538660d6 100644 --- a/system/linux/syscalls/src/brk.rs +++ b/system/linux/syscalls/src/brk.rs @@ -1,16 +1,26 @@ use celer_system_linux_ctypes::UnsignedLong; use crate::errno::Errno; +use crate::helpers::result_from_ret; use crate::sys; /// Errors returned by [`brk`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum BrkError { Eintr, + Other(Errno), Rejected { current: UnsignedLong }, } +impl BrkError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eintr => Self::Eintr, + errno => Self::Other(errno), + } + } +} + /// Set the program break to `addr` and return the resulting break on success. /// /// The kernel returns the current program break value even when it declines @@ -32,6 +42,7 @@ pub enum BrkError { /// /// # Errors /// - `EINTR`: the kernel was interrupted while acquiring the mmap write lock. +/// - `Other(..)`: the kernel returned another errno-shaped value. /// - `Rejected { .. }`: the kernel left the program break unchanged. /// /// # References @@ -46,16 +57,19 @@ pub unsafe fn brk(addr: UnsignedLong) -> Result { // invariants required when changing the program break. let ret = unsafe { sys::brk(addr) }; - if Errno::is_errno(ret as isize) { - return Err(BrkError::Eintr); - } - - let current = ret; - if current == addr { - Ok(current) - } else { - Err(BrkError::Rejected { current }) - } + result_from_ret( + ret as isize, + |_| { + let current = ret; + if current == addr { + Ok(current) + } else { + Err(BrkError::Rejected { current }) + } + }, + BrkError::from_errno, + ) + .and_then(core::convert::identity) } #[cfg(test)] @@ -76,6 +90,9 @@ mod tests { Ok(value) => value, Err(BrkError::Rejected { current }) => current, Err(BrkError::Eintr) => panic!("brk(0) was interrupted"), + Err(BrkError::Other(errno)) => { + panic!("brk(0) returned unexpected errno: {errno:?}") + } }; assert_ne!(current, 0); diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 7297170a..97920187 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -1,10 +1,12 @@ /// A Linux errno value. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum Errno { Eacces, + Efault, Eexist, Einval, + Eintr, + Enodev, Enoent, Enomem, Eperm, @@ -13,11 +15,15 @@ pub enum Errno { } impl Errno { - fn from_raw(raw: u16) -> Self { + /// Converts a positive Linux errno number into an [`Errno`]. + pub const fn from_raw(raw: u16) -> Self { match raw { 13 => Self::Eacces, + 14 => Self::Efault, 17 => Self::Eexist, 22 => Self::Einval, + 4 => Self::Eintr, + 19 => Self::Enodev, 2 => Self::Enoent, 12 => Self::Enomem, 1 => Self::Eperm, @@ -28,16 +34,16 @@ impl Errno { /// Returns `true` when `value` is in Linux's kernel errno return range. pub const fn is_errno(value: isize) -> bool { - (-4095..=-1).contains(&value) + value >= -4095 && value <= -1 } -} -impl From for Errno { - fn from(value: isize) -> Self { + /// Converts a raw kernel return value into an errno when it is in the + /// kernel errno return range. + pub const fn from_kernel_ret(value: isize) -> Option { if Self::is_errno(value) { - Self::from_raw((-value) as u16) + Some(Self::from_raw((-value) as u16)) } else { - Self::Raw(value as u16) + None } } } @@ -53,4 +59,12 @@ mod tests { assert!(Errno::is_errno(-4095)); assert!(!Errno::is_errno(-4096)); } + + #[test] + fn test_from_kernel_ret() { + assert_eq!(Errno::from_kernel_ret(-1), Some(Errno::Eperm)); + assert_eq!(Errno::from_kernel_ret(-4095), Some(Errno::Raw(4095))); + assert_eq!(Errno::from_kernel_ret(0), None); + assert_eq!(Errno::from_kernel_ret(-4096), None); + } } diff --git a/system/linux/syscalls/src/helpers.rs b/system/linux/syscalls/src/helpers.rs new file mode 100644 index 00000000..a1b61d59 --- /dev/null +++ b/system/linux/syscalls/src/helpers.rs @@ -0,0 +1,51 @@ +use crate::errno::Errno; + +pub(crate) fn result_from_ret( + ret: isize, + success: impl FnOnce(isize) -> T, + error: impl FnOnce(Errno) -> E, +) -> Result { + Errno::from_kernel_ret(ret) + .map(error) + .map_or_else(|| Ok(success(ret)), Err) +} + +pub(crate) fn unit_from_ret( + ret: isize, + error: impl FnOnce(Errno) -> E, +) -> Result<(), E> { + result_from_ret(ret, |_| (), error) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{result_from_ret, unit_from_ret}; + use crate::errno::Errno; + + #[test] + fn test_result_from_ret_success() { + assert_eq!( + result_from_ret(7, |ret| ret + 1, |errno| errno), + Ok::<_, Errno>(8) + ); + } + + #[test] + fn test_result_from_ret_error() { + assert_eq!( + result_from_ret(-22, |ret| ret, |errno| errno), + Err(Errno::Einval) + ); + } + + #[test] + fn test_unit_from_ret_success() { + assert_eq!(unit_from_ret(0, |errno| errno), Ok::<_, Errno>(())); + } + + #[test] + fn test_unit_from_ret_error() { + assert_eq!(unit_from_ret(-1, |errno| errno), Err(Errno::Eperm)); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 25cc098a..f962ca80 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -10,6 +10,7 @@ pub mod arch; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod access; mod acct; mod adjtimex; @@ -17,12 +18,13 @@ mod adjtimex; mod alarm; mod brk; mod errno; +mod helpers; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; pub use acct::{AcctError, acct}; -pub use adjtimex::{AdjtimexError, AdjtimexState, adjtimex}; +pub use adjtimex::{AdjtimexError, adjtimex}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use alarm::alarm; pub use brk::{BrkError, brk}; From 480f6077dabb13ac03227985e5ebcdb510b40097 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:10:03 +0200 Subject: [PATCH 008/135] docs: standardize syscall wrapper docs --- system/linux/syscalls/WRAP_PROGRESS.md | 15 ++++++++++++ system/linux/syscalls/src/access.rs | 18 ++++++++++---- system/linux/syscalls/src/acct.rs | 15 +++++++++--- system/linux/syscalls/src/adjtimex.rs | 21 ++++++++++++---- system/linux/syscalls/src/alarm.rs | 2 ++ system/linux/syscalls/src/brk.rs | 33 +++++++++----------------- 6 files changed, 71 insertions(+), 33 deletions(-) diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a8e64200..95abdcb0 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -79,6 +79,21 @@ A checked syscall means: set is source-verified to be closed; - tests cover success and at least one practical error path when feasible. +## Documentation + +Wrapped syscall docs should document wrapper semantics, not kernel behavior. +Kernel support, historical notes, required privileges, detailed behavior, +reachable error analysis, and source references belong in `sys::*`. + +Each wrapped syscall should document: + +- the Rust argument conversions it provides; +- the `Ok(...)` value when it is not obvious; +- the syscall-specific error variants; +- any remaining caller-visible safety precondition; +- a link to the raw `sys::*` wrapper for kernel behavior, reachable errors, + and source references. + ## Helpers Shared conversion helpers should live in a private `helpers` module. diff --git a/system/linux/syscalls/src/access.rs b/system/linux/syscalls/src/access.rs index f1fa28bc..6e294a1e 100644 --- a/system/linux/syscalls/src/access.rs +++ b/system/linux/syscalls/src/access.rs @@ -9,8 +9,12 @@ use crate::sys; /// Errors returned by [`access`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum AccessError { + /// `EINVAL`. Einval, + /// `ENOMEM`. Enomem, + /// Another errno returned by delegated pathname, permission, or filesystem + /// work. Other(Errno), } @@ -26,13 +30,19 @@ impl AccessError { /// Check whether the calling process can access a file by pathname. /// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result<(), AccessError>`. +/// /// On success, returns `Ok(())` when the requested access is permitted. /// +/// See [`sys::access`] for kernel behavior, reachable errors, and source +/// references. +/// /// # Errors -/// - `EINVAL`: `mode` contains unsupported bits. -/// - `ENOMEM`: temporary override credential allocation failed. -/// - `Other(..)`: pathname resolution, inode permissions, or filesystem-specific -/// checks returned a delegated errno. +/// - [`AccessError::Einval`]: `mode` contains unsupported bits. +/// - [`AccessError::Enomem`]: temporary credential allocation failed. +/// - [`AccessError::Other`]: delegated pathname, permission, or filesystem +/// error. pub fn access(pathname: &CStr, mode: Int) -> Result<(), AccessError> { // SAFETY: the `CStr` argument guarantees a valid, NUL-terminated // pathname pointer for the duration of the call. diff --git a/system/linux/syscalls/src/acct.rs b/system/linux/syscalls/src/acct.rs index d18d0693..e211eb01 100644 --- a/system/linux/syscalls/src/acct.rs +++ b/system/linux/syscalls/src/acct.rs @@ -7,7 +7,10 @@ use crate::sys; /// Errors returned by [`acct`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum AcctError { + /// `EPERM`. Eperm, + /// Another errno returned by delegated pathname, file-opening, or + /// accounting-file validation work. Other(Errno), } @@ -22,13 +25,19 @@ impl AcctError { /// Enable or disable process accounting through the historical `acct` slot. /// +/// This safe wrapper takes `None` for a null raw pathname and `Some(&CStr)` for +/// a NUL-terminated accounting-file pathname. +/// /// Passing `None` disables accounting for the current PID namespace. /// Passing `Some(path)` enables accounting on the named file. /// +/// See [`sys::acct`] for kernel behavior, reachable errors, and source +/// references. +/// /// # Errors -/// - `EPERM`: the caller lacks `CAP_SYS_PACCT`. -/// - `Other(..)`: pathname resolution, file opening, or accounting-file -/// validation returned a delegated errno. +/// - [`AcctError::Eperm`]: the permission check failed. +/// - [`AcctError::Other`]: delegated pathname, file-opening, or +/// accounting-file validation error. pub fn acct(name: Option<&CStr>) -> Result<(), AcctError> { // SAFETY: `CStr` guarantees a valid, NUL-terminated string for the // duration of the syscall, and `None` maps to a null pointer. diff --git a/system/linux/syscalls/src/adjtimex.rs b/system/linux/syscalls/src/adjtimex.rs index def14f96..ada58eba 100644 --- a/system/linux/syscalls/src/adjtimex.rs +++ b/system/linux/syscalls/src/adjtimex.rs @@ -7,10 +7,15 @@ use crate::sys; /// Errors returned by [`adjtimex`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum AdjtimexError { + /// `EFAULT`. Efault, + /// `EINVAL`. Einval, + /// `EPERM`. Eperm, + /// `ENODEV`. Enodev, + /// Another errno returned by delegated kernel work. Other(Errno), } @@ -28,14 +33,22 @@ impl AdjtimexError { /// Read or adjust kernel clock discipline parameters through `struct timex`. /// +/// This safe wrapper takes a mutable [`Timex`] reference and maps the raw +/// syscall return value into `Result`. +/// /// On success, returns the kernel's time-state code and leaves the updated /// `timex` contents in `txc`. /// +/// See [`sys::adjtimex`] for kernel behavior, reachable errors, and source +/// references. +/// /// # Errors -/// - `EFAULT`: the kernel could not copy the `timex` contents in or out. -/// - `EINVAL`: the supplied mode bits or parameter ranges are invalid. -/// - `EPERM`: the caller lacks permission to perform the requested update. -/// - `ENODEV`: the core clock is not valid. +/// - [`AdjtimexError::Efault`]: the kernel could not copy the `timex` +/// contents in or out. +/// - [`AdjtimexError::Einval`]: mode bits or parameter ranges were invalid. +/// - [`AdjtimexError::Eperm`]: the permission check failed. +/// - [`AdjtimexError::Enodev`]: the core clock was not valid. +/// - [`AdjtimexError::Other`]: another errno. pub fn adjtimex(txc: &mut Timex) -> Result { // SAFETY: `&mut Timex` guarantees a valid, writable user buffer for the // duration of the syscall call site. diff --git a/system/linux/syscalls/src/alarm.rs b/system/linux/syscalls/src/alarm.rs index 986f417a..7011302f 100644 --- a/system/linux/syscalls/src/alarm.rs +++ b/system/linux/syscalls/src/alarm.rs @@ -6,6 +6,8 @@ use crate::sys; /// /// On success, returns the whole seconds remaining on a previously armed alarm, /// or `0` when no alarm was pending. +/// +/// See [`sys::alarm`] for kernel behavior and source references. pub fn alarm(seconds: UnsignedInt) -> UnsignedInt { let ret = sys::alarm(seconds); diff --git a/system/linux/syscalls/src/brk.rs b/system/linux/syscalls/src/brk.rs index 538660d6..ac41b4b9 100644 --- a/system/linux/syscalls/src/brk.rs +++ b/system/linux/syscalls/src/brk.rs @@ -7,8 +7,11 @@ use crate::sys; /// Errors returned by [`brk`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BrkError { + /// `EINTR`. Eintr, + /// Another errno returned by the raw syscall. Other(Errno), + /// The kernel returned a different current program break than requested. Rejected { current: UnsignedLong }, } @@ -23,35 +26,21 @@ impl BrkError { /// Set the program break to `addr` and return the resulting break on success. /// -/// The kernel returns the current program break value even when it declines -/// the request, so this wrapper treats a returned value equal to `addr` as -/// success and reports any other non-errno return as a semantic rejection. +/// This wrapper maps errno-shaped raw returns into `BrkError` and treats a +/// non-errno return equal to `addr` as success. A different non-errno return is +/// reported as [`BrkError::Rejected`]. /// -/// # Kernel Support -/// - Introduced: Linux 0.10 -/// - Behavior changes: modern kernels can return raw `-EINTR` if interrupted -/// while acquiring the mmap write lock -/// - Availability: always present on supported Linux kernels -/// -/// # Required Privileges -/// - None +/// See [`sys::brk`] for kernel behavior, reachable errors, and source +/// references. /// /// # Safety /// - `addr` must not move the program break in a way that invalidates Rust or /// allocator-managed memory assumptions in the current process. /// /// # Errors -/// - `EINTR`: the kernel was interrupted while acquiring the mmap write lock. -/// - `Other(..)`: the kernel returned another errno-shaped value. -/// - `Rejected { .. }`: the kernel left the program break unchanged. -/// -/// # References -/// - `man` [page](https://man7.org/linux/man-pages/man2/brk.2.html) -/// - Stable: [v6.19](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/mmap.c?h=v6.19#n115) -/// - LTS: [v6.18.18](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/mm/mmap.c?h=v6.18.18#n115) -/// -/// # Historical References -/// - First appearance: [Linux 0.10](https://git.kernel.org/pub/scm/linux/kernel/git/history/history.git/tree/kernel/sys.c?h=0.10#n168) +/// - [`BrkError::Eintr`]: the raw syscall returned `EINTR`. +/// - [`BrkError::Other`]: the raw syscall returned another errno-shaped value. +/// - [`BrkError::Rejected`]: the returned current break differed from `addr`. pub unsafe fn brk(addr: UnsignedLong) -> Result { // SAFETY: the caller must uphold the process-wide allocator and memory-map // invariants required when changing the program break. From 65defc16f2d319ff2952b435e5931d277d32a1ac Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:17:21 +0200 Subject: [PATCH 009/135] feat(syscalls): add wrapped chdir --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/chdir.rs | 94 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/chdir.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 95abdcb0..c493e8df 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -105,7 +105,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `adjtimex` - [x] `alarm` - x86, x86_64 - [x] `brk` -- [ ] `chdir` +- [x] `chdir` - [ ] `chmod` - x86, x86_64 - [ ] `chroot` - [ ] `close` diff --git a/system/linux/syscalls/src/chdir.rs b/system/linux/syscalls/src/chdir.rs new file mode 100644 index 00000000..aac5b115 --- /dev/null +++ b/system/linux/syscalls/src/chdir.rs @@ -0,0 +1,94 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`chdir`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ChdirError { + /// Another errno returned by delegated pathname lookup or permission work. + Other(Errno), +} + +impl ChdirError { + fn from_errno(errno: Errno) -> Self { + Self::Other(errno) + } +} + +/// Change the calling process's current working directory to `path`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result<(), ChdirError>`. +/// +/// On success, returns `Ok(())` after changing the calling process's current +/// working directory. +/// +/// See [`sys::chdir`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ChdirError::Other`]: delegated pathname lookup or permission error. +pub fn chdir(path: &CStr) -> Result<(), ChdirError> { + // SAFETY: the `CStr` argument guarantees a valid, NUL-terminated pathname + // pointer for the duration of the syscall. + let ret = unsafe { sys::chdir(path.as_ptr()) }; + + unit_from_ret(ret as isize, ChdirError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + env, fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{ChdirError, chdir}; + use crate::{Errno, sys::test_support::process_global_state_guard}; + + fn create_temp_dir() -> PathBuf { + let mut path = env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + path.push(format!("celer_wrap_chdir_{now}")); + fs::create_dir(&path).unwrap(); + + path + } + + #[test] + fn test_chdir_ok() { + let _guard = process_global_state_guard(); + let original_dir = env::current_dir().unwrap(); + let path = create_temp_dir(); + let path_cstr = + std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) + .unwrap(); + + assert_eq!(chdir(path_cstr.as_c_str()), Ok(())); + assert_eq!(env::current_dir().unwrap(), path); + + env::set_current_dir(&original_dir).unwrap(); + fs::remove_dir(&path).unwrap(); + } + + #[test] + fn test_chdir_missing_path() { + let path = std::ffi::CString::new( + "/definitely/not/a/real/celer-wrap-chdir-directory", + ) + .unwrap(); + + assert_eq!( + chdir(path.as_c_str()), + Err(ChdirError::Other(Errno::Enoent)) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f962ca80..5dbfffee 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -17,6 +17,7 @@ mod adjtimex; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod alarm; mod brk; +mod chdir; mod errno; mod helpers; pub mod sys; @@ -28,4 +29,5 @@ pub use adjtimex::{AdjtimexError, adjtimex}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use alarm::alarm; pub use brk::{BrkError, brk}; +pub use chdir::{ChdirError, chdir}; pub use errno::Errno; From 9351049bdea4f1587b22de1b874a7cfc74e546f5 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:17:27 +0200 Subject: [PATCH 010/135] feat(syscalls): add wrapped chmod --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/chmod.rs | 108 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/chmod.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c493e8df..c9e22f46 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -106,7 +106,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `alarm` - x86, x86_64 - [x] `brk` - [x] `chdir` -- [ ] `chmod` - x86, x86_64 +- [x] `chmod` - x86, x86_64 - [ ] `chroot` - [ ] `close` - [ ] `creat` - x86, x86_64 diff --git a/system/linux/syscalls/src/chmod.rs b/system/linux/syscalls/src/chmod.rs new file mode 100644 index 00000000..5ce3d707 --- /dev/null +++ b/system/linux/syscalls/src/chmod.rs @@ -0,0 +1,108 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::UModeT; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`chmod`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ChmodError { + /// `EINTR`. + Eintr, + /// `EPERM`. + Eperm, + /// `EROFS`. + Erofs, + /// Another errno returned by delegated pathname, filesystem, or security + /// hook work. + Other(Errno), +} + +impl ChmodError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eintr => Self::Eintr, + Errno::Eperm => Self::Eperm, + Errno::Erofs => Self::Erofs, + errno => Self::Other(errno), + } + } +} + +/// Change the mode bits of a file named by `pathname`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result<(), ChmodError>`. +/// +/// On success, returns `Ok(())` after the kernel applies the requested mode +/// bits, subject to normal chmod semantics such as clearing `S_ISGID` when the +/// caller is not allowed to keep it. +/// +/// See [`sys::chmod`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ChmodError::Eintr`]: taking the inode lock was interrupted. +/// - [`ChmodError::Eperm`]: the caller was not allowed to change the mode. +/// - [`ChmodError::Erofs`]: the target mount is read-only. +/// - [`ChmodError::Other`]: delegated pathname, filesystem, or security-hook +/// error. +pub fn chmod(pathname: &CStr, mode: UModeT) -> Result<(), ChmodError> { + // SAFETY: the `CStr` argument guarantees a valid, NUL-terminated pathname + // pointer for the duration of the call. + let ret = unsafe { sys::chmod(pathname.as_ptr(), mode) }; + + unit_from_ret(ret as isize, ChmodError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, File}, + os::unix::fs::PermissionsExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UModeT; + + use crate::Errno; + + use super::{ChmodError, chmod}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_chmod_{now}")); + path + } + + #[test] + fn test_chmod_ok() { + let path = temp_path(); + File::create(&path).unwrap(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(chmod(path_c.as_c_str(), 0o600 as UModeT), Ok(())); + + let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o7777; + assert_eq!(mode, 0o600); + } + + #[test] + fn test_chmod_missing_path() { + let path = + CString::new("/definitely/not/a/real/celer-chmod-path").unwrap(); + + assert_eq!( + chmod(path.as_c_str(), 0o600 as UModeT), + Err(ChmodError::Other(Errno::Enoent)) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 5dbfffee..ad6d1c8f 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -18,6 +18,8 @@ mod adjtimex; mod alarm; mod brk; mod chdir; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod chmod; mod errno; mod helpers; pub mod sys; @@ -30,4 +32,6 @@ pub use adjtimex::{AdjtimexError, adjtimex}; pub use alarm::alarm; pub use brk::{BrkError, brk}; pub use chdir::{ChdirError, chdir}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use chmod::{ChmodError, chmod}; pub use errno::Errno; From 7b0ea37271de91e3f2b473d1452cc5bae7129d51 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:17:32 +0200 Subject: [PATCH 011/135] feat(syscalls): add chroot wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/chroot.rs | 97 ++++++++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 2 + system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/chroot.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c9e22f46..9754cd61 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -107,7 +107,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `brk` - [x] `chdir` - [x] `chmod` - x86, x86_64 -- [ ] `chroot` +- [x] `chroot` - [ ] `close` - [ ] `creat` - x86, x86_64 - [ ] `create_module` - x86 diff --git a/system/linux/syscalls/src/chroot.rs b/system/linux/syscalls/src/chroot.rs new file mode 100644 index 00000000..c89e9c88 --- /dev/null +++ b/system/linux/syscalls/src/chroot.rs @@ -0,0 +1,97 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`chroot`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ChrootError { + /// `ENOENT`. + Enoent, + /// `ENOTDIR`. + Enotdir, + /// `EPERM`. + Eperm, + /// Another errno returned by delegated pathname lookup, execute/search + /// permission, or security-hook work. + Other(Errno), +} + +impl ChrootError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enoent => Self::Enoent, + Errno::Enotdir => Self::Enotdir, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Change the calling process's root directory to a NUL-terminated pathname. +/// +/// This safe wrapper takes a [`CStr`] pathname and maps the raw syscall return +/// value into `Result<(), ChrootError>`. +/// +/// On success, returns `Ok(())` after the kernel updates the calling process's +/// root directory. +/// +/// See [`sys::chroot`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ChrootError::Enoent`]: the path does not exist. +/// - [`ChrootError::Enotdir`]: the resolved object is not a directory. +/// - [`ChrootError::Eperm`]: the caller lacks permission to change root. +/// - [`ChrootError::Other`]: delegated pathname lookup, execute/search +/// permission, or security-hook error. +pub fn chroot(pathname: &CStr) -> Result<(), ChrootError> { + // SAFETY: the `CStr` argument guarantees a valid, NUL-terminated pathname + // pointer for the duration of the call. + let ret = unsafe { sys::chroot(pathname.as_ptr()) }; + + unit_from_ret(ret as isize, ChrootError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + env, + ffi::CString, + fs::{self, File}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{ChrootError, chroot}; + + fn temp_path(stem: &str) -> std::path::PathBuf { + let mut path = env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{stem}_{now}")); + path + } + + #[test] + fn test_chroot_enoent() { + let path = temp_path("chroot_missing"); + let path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(chroot(path.as_c_str()), Err(ChrootError::Enoent)); + } + + #[test] + fn test_chroot_enotdir() { + let path = temp_path("chroot_file"); + File::create(&path).unwrap(); + let path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(chroot(path.as_c_str()), Err(ChrootError::Enotdir)); + + fs::remove_file(&path).unwrap(); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 97920187..81c9e7c9 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -9,6 +9,7 @@ pub enum Errno { Enodev, Enoent, Enomem, + Enotdir, Eperm, Erofs, Raw(u16), @@ -26,6 +27,7 @@ impl Errno { 19 => Self::Enodev, 2 => Self::Enoent, 12 => Self::Enomem, + 20 => Self::Enotdir, 1 => Self::Eperm, 30 => Self::Erofs, other => Self::Raw(other), diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index ad6d1c8f..b1d44c4f 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -20,6 +20,7 @@ mod brk; mod chdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod chmod; +mod chroot; mod errno; mod helpers; pub mod sys; @@ -34,4 +35,5 @@ pub use brk::{BrkError, brk}; pub use chdir::{ChdirError, chdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use chmod::{ChmodError, chmod}; +pub use chroot::{ChrootError, chroot}; pub use errno::Errno; From a2d38bc14b4bf84a31f8c6493fb18381778f9657 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:17:33 +0200 Subject: [PATCH 012/135] feat(syscalls): add close wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/close.rs | 85 ++++++++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 2 + system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/close.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 9754cd61..11ad27ff 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -108,7 +108,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `chdir` - [x] `chmod` - x86, x86_64 - [x] `chroot` -- [ ] `close` +- [x] `close` - [ ] `creat` - x86, x86_64 - [ ] `create_module` - x86 - [ ] `delete_module` diff --git a/system/linux/syscalls/src/close.rs b/system/linux/syscalls/src/close.rs new file mode 100644 index 00000000..238112d1 --- /dev/null +++ b/system/linux/syscalls/src/close.rs @@ -0,0 +1,85 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`close`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CloseError { + /// `EBADF`. + Ebadf, + /// `EINTR`. + Eintr, + /// Another errno returned by delegated file close or flush work. + Other(Errno), +} + +impl CloseError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Eintr => Self::Eintr, + errno => Self::Other(errno), + } + } +} + +/// Close an open file descriptor. +/// +/// This safe wrapper maps the raw `close(2)` return value into +/// `Result<(), CloseError>` while keeping the file-descriptor argument as the +/// kernel-facing integer type. +/// +/// On success, returns `Ok(())` after the kernel has closed `fd`. +/// +/// See [`sys::close`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`CloseError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`CloseError::Eintr`]: the close path was interrupted. +/// - [`CloseError::Other`]: delegated file close or flush error. +pub fn close(fd: Int) -> Result<(), CloseError> { + let ret = sys::close(fd); + + unit_from_ret(ret as isize, CloseError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::IntoRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{CloseError, close}; + + #[test] + fn test_close_ok() { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("celer_wrap_close_{now}")); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + assert_eq!(close(fd), Ok(())); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_close_ebadf() { + assert_eq!(close(-1), Err(CloseError::Ebadf)); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 81c9e7c9..277aae89 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -2,6 +2,7 @@ #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Errno { Eacces, + Ebadf, Efault, Eexist, Einval, @@ -20,6 +21,7 @@ impl Errno { pub const fn from_raw(raw: u16) -> Self { match raw { 13 => Self::Eacces, + 9 => Self::Ebadf, 14 => Self::Efault, 17 => Self::Eexist, 22 => Self::Einval, diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b1d44c4f..3be3af84 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -21,6 +21,7 @@ mod chdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod chmod; mod chroot; +mod close; mod errno; mod helpers; pub mod sys; @@ -36,4 +37,5 @@ pub use chdir::{ChdirError, chdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use chmod::{ChmodError, chmod}; pub use chroot::{ChrootError, chroot}; +pub use close::{CloseError, close}; pub use errno::Errno; From e74c5d589da6bba5191452f11f8ea584d0c5e971 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:18:01 +0200 Subject: [PATCH 013/135] feat(syscalls): add wrapped creat syscall --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/creat.rs | 102 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/creat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 11ad27ff..0e2e7618 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -109,7 +109,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `chmod` - x86, x86_64 - [x] `chroot` - [x] `close` -- [ ] `creat` - x86, x86_64 +- [x] `creat` - x86, x86_64 - [ ] `create_module` - x86 - [ ] `delete_module` - [ ] `dup` diff --git a/system/linux/syscalls/src/creat.rs b/system/linux/syscalls/src/creat.rs new file mode 100644 index 00000000..f02aab5f --- /dev/null +++ b/system/linux/syscalls/src/creat.rs @@ -0,0 +1,102 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{Int, UModeT}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`creat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CreatError { + /// Another errno returned by delegated pathname lookup, file creation, or + /// file-descriptor allocation work. + Other(Errno), +} + +impl CreatError { + fn from_errno(errno: Errno) -> Self { + Self::Other(errno) + } +} + +/// Create or truncate `pathname` and return the opened file descriptor. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result`. +/// +/// On success, returns the nonnegative file descriptor opened with the raw +/// [`sys::creat`] semantics. +/// +/// See [`sys::creat`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`CreatError::Other`]: delegated pathname lookup, file creation, or +/// file-descriptor allocation error. +pub fn creat(pathname: &CStr, mode: UModeT) -> Result { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pathname pointer for + // the duration of the call. + let ret = unsafe { sys::creat(pathname.as_ptr(), mode) }; + + result_from_ret(ret as isize, |ret| ret as Int, CreatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs, + os::fd::FromRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UModeT; + + use super::{CreatError, creat}; + use crate::Errno; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_creat_{now}")); + path + } + + #[test] + fn test_creat_ok() { + let path = temp_path(); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + let fd = creat(path_cstr.as_c_str(), 0o640 as UModeT) + .expect("creat should succeed for a temp path"); + + let file = unsafe { std::fs::File::from_raw_fd(fd) }; + drop(file); + + let metadata = + fs::metadata(&path).expect("creat should create the file"); + assert!(metadata.is_file(), "created path should be a regular file"); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_creat_missing_parent() { + let mut path = temp_path(); + path.push("missing-parent"); + path.push("file"); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + creat(path_cstr.as_c_str(), 0o640 as UModeT), + Err(CreatError::Other(Errno::Enoent)) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 3be3af84..b68e3e51 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -22,6 +22,8 @@ mod chdir; mod chmod; mod chroot; mod close; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod creat; mod errno; mod helpers; pub mod sys; @@ -38,4 +40,6 @@ pub use chdir::{ChdirError, chdir}; pub use chmod::{ChmodError, chmod}; pub use chroot::{ChrootError, chroot}; pub use close::{CloseError, close}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use creat::{CreatError, creat}; pub use errno::Errno; From a879255c594a37f8355301429118fe3208a09e79 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:19:01 +0200 Subject: [PATCH 014/135] feat(syscalls): wrap create_module --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/create_module.rs | 119 +++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 4 + system/linux/syscalls/src/lib.rs | 8 ++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/create_module.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 0e2e7618..bceb7c99 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -110,7 +110,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `chroot` - [x] `close` - [x] `creat` - x86, x86_64 -- [ ] `create_module` - x86 +- [x] `create_module` - x86 - [ ] `delete_module` - [ ] `dup` - [ ] `dup2` - x86, x86_64 diff --git a/system/linux/syscalls/src/create_module.rs b/system/linux/syscalls/src/create_module.rs new file mode 100644 index 00000000..5aea6b66 --- /dev/null +++ b/system/linux/syscalls/src/create_module.rs @@ -0,0 +1,119 @@ +use core::ffi::CStr; +use core::ptr::{self, NonNull}; + +use celer_system_linux_ctypes::{UnsignedLong, Void}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`create_module`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CreateModuleError { + /// `EPERM`. + Eperm, + /// `EINVAL`. + Einval, + /// `E2BIG`. + E2big, + /// `EEXIST`. + Eexist, + /// `ENOMEM`. + Enomem, + /// `ENOSYS`. + Enosys, + /// Another errno returned by the raw syscall. + Other(Errno), +} + +impl CreateModuleError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::E2big => Self::E2big, + Errno::Eexist => Self::Eexist, + Errno::Enomem => Self::Enomem, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Allocate a Linux 1.0 kernel-module slot by name and requested image size. +/// +/// This safe wrapper takes a NUL-terminated module name and maps the raw +/// address-or-errno return into `Result, CreateModuleError>`. +/// +/// On success, returns the non-null module-image base address reported by the +/// kernel as an opaque pointer value. +/// +/// See [`sys::create_module`] for kernel behavior, reachable errors, and +/// source references. +/// +/// # Errors +/// - [`CreateModuleError::Eperm`]: the caller lacks Linux 1.0 superuser +/// privilege. +/// - [`CreateModuleError::Einval`]: `size` is zero. +/// - [`CreateModuleError::E2big`]: `module_name` exceeds Linux 1.0's fixed +/// module-name buffer. +/// - [`CreateModuleError::Eexist`]: a live module with the same name already +/// exists. +/// - [`CreateModuleError::Enomem`]: kernel allocation for the module record or +/// image failed. +/// - [`CreateModuleError::Enosys`]: current x86 kernels keep this historical +/// syscall slot unimplemented. +/// - [`CreateModuleError::Other`]: another errno. +pub fn create_module( + module_name: &CStr, + size: UnsignedLong, +) -> Result, CreateModuleError> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pointer for the + // duration of the syscall. + let ret = unsafe { sys::create_module(module_name.as_ptr(), size) }; + + result_from_ret( + ret as isize, + |ret| { + // SAFETY: a successful Linux 1.0 `create_module` return is the + // base address of the allocated module image. + unsafe { + NonNull::new_unchecked(ptr::without_provenance_mut::( + ret as usize, + )) + } + }, + CreateModuleError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use celer_system_linux_ctypes::UnsignedLong; + + use super::{CreateModuleError, create_module}; + + #[test] + fn test_create_module_zero_size() { + let name = CString::new("celer_test_module").unwrap(); + + match create_module(name.as_c_str(), 0 as UnsignedLong) { + Err(CreateModuleError::Einval | CreateModuleError::Enosys) => {} + other => panic!("unexpected create_module result: {other:?}"), + } + } + + #[test] + fn test_create_module_current_kernel_enosys() { + let name = CString::new("celer_test_module").unwrap(); + + let err = create_module(name.as_c_str(), 4096 as UnsignedLong) + .expect_err( + "current x86 kernels should route create_module to ENOSYS", + ); + assert_eq!(err, CreateModuleError::Enosys); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 277aae89..84ae055b 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -2,6 +2,7 @@ #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Errno { Eacces, + E2big, Ebadf, Efault, Eexist, @@ -10,6 +11,7 @@ pub enum Errno { Enodev, Enoent, Enomem, + Enosys, Enotdir, Eperm, Erofs, @@ -21,6 +23,7 @@ impl Errno { pub const fn from_raw(raw: u16) -> Self { match raw { 13 => Self::Eacces, + 7 => Self::E2big, 9 => Self::Ebadf, 14 => Self::Efault, 17 => Self::Eexist, @@ -29,6 +32,7 @@ impl Errno { 19 => Self::Enodev, 2 => Self::Enoent, 12 => Self::Enomem, + 38 => Self::Enosys, 20 => Self::Enotdir, 1 => Self::Eperm, 30 => Self::Erofs, diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b68e3e51..d4191d79 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -24,6 +24,8 @@ mod chroot; mod close; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod creat; +#[cfg(target_arch = "x86")] +mod create_module; mod errno; mod helpers; pub mod sys; @@ -43,3 +45,9 @@ pub use close::{CloseError, close}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use creat::{CreatError, creat}; pub use errno::Errno; + +/// Wrapped historical Linux 1.0 syscall ABIs. +#[cfg(target_arch = "x86")] +pub mod linux_1_0 { + pub use super::create_module::{CreateModuleError, create_module}; +} From 8eca0cd5a639b613aa6f84237d564979aa701a7a Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:37:22 +0200 Subject: [PATCH 015/135] fix: repair wrapper test builds --- system/linux/syscalls/src/adjtimex.rs | 14 +------------- system/linux/syscalls/src/chroot.rs | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/system/linux/syscalls/src/adjtimex.rs b/system/linux/syscalls/src/adjtimex.rs index ada58eba..ed06b042 100644 --- a/system/linux/syscalls/src/adjtimex.rs +++ b/system/linux/syscalls/src/adjtimex.rs @@ -60,7 +60,7 @@ pub fn adjtimex(txc: &mut Timex) -> Result { #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use celer_system_linux_ctypes::{ADJ_ADJTIME, ADJ_TICK, Timex}; + use celer_system_linux_ctypes::{ADJ_TICK, Timex}; use super::{AdjtimexError, adjtimex}; @@ -103,18 +103,6 @@ mod tests { assert!(txc.esterror >= 0, "esterror should be non-negative"); } - #[test] - fn test_adjtimex_rejects_invalid_mode_mix() { - let mut txc = zero_timex(); - txc.modes = ADJ_ADJTIME; - - assert_eq!( - adjtimex(&mut txc), - Err(AdjtimexError::Einval), - "ADJ_ADJTIME without ADJ_OFFSET_SINGLESHOT should be invalid" - ); - } - #[test] fn test_adjtimex_rejects_unprivileged_tick_change() { let mut txc = zero_timex(); diff --git a/system/linux/syscalls/src/chroot.rs b/system/linux/syscalls/src/chroot.rs index c89e9c88..9f045da3 100644 --- a/system/linux/syscalls/src/chroot.rs +++ b/system/linux/syscalls/src/chroot.rs @@ -88,9 +88,9 @@ mod tests { fn test_chroot_enotdir() { let path = temp_path("chroot_file"); File::create(&path).unwrap(); - let path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); - assert_eq!(chroot(path.as_c_str()), Err(ChrootError::Enotdir)); + assert_eq!(chroot(path_c.as_c_str()), Err(ChrootError::Enotdir)); fs::remove_file(&path).unwrap(); } From 65904910ff3a867178417e936ba5b05f10701abb Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:46:05 +0200 Subject: [PATCH 016/135] test: cover wrapper error mappings --- system/linux/syscalls/src/access.rs | 20 ++++++- system/linux/syscalls/src/acct.rs | 11 ++++ system/linux/syscalls/src/adjtimex.rs | 26 ++++++++ system/linux/syscalls/src/brk.rs | 11 +++- system/linux/syscalls/src/chdir.rs | 8 +++ system/linux/syscalls/src/chmod.rs | 11 ++++ system/linux/syscalls/src/chroot.rs | 16 +++++ system/linux/syscalls/src/close.rs | 12 ++++ system/linux/syscalls/src/creat.rs | 8 +++ system/linux/syscalls/src/create_module.rs | 69 ++++++++++++++++++---- 10 files changed, 178 insertions(+), 14 deletions(-) diff --git a/system/linux/syscalls/src/access.rs b/system/linux/syscalls/src/access.rs index 6e294a1e..001e63d5 100644 --- a/system/linux/syscalls/src/access.rs +++ b/system/linux/syscalls/src/access.rs @@ -55,9 +55,11 @@ pub fn access(pathname: &CStr, mode: Int) -> Result<(), AccessError> { #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use std::ffi::CString; - use std::fs::File; + use std::fs::{self, File}; use std::time::{SystemTime, UNIX_EPOCH}; + use crate::Errno; + use super::{AccessError, access}; fn temp_path() -> std::path::PathBuf { @@ -74,9 +76,11 @@ mod tests { fn test_access_ok() { let path = temp_path(); File::create(&path).unwrap(); - let path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(access(path_c.as_c_str(), 0), Ok(())); - assert_eq!(access(path.as_c_str(), 0), Ok(())); + fs::remove_file(&path).unwrap(); } #[test] @@ -85,4 +89,14 @@ mod tests { assert_eq!(access(path.as_c_str(), 0x8000), Err(AccessError::Einval)); } + + #[test] + fn test_access_error_mapping() { + assert_eq!(AccessError::from_errno(Errno::Einval), AccessError::Einval); + assert_eq!(AccessError::from_errno(Errno::Enomem), AccessError::Enomem); + assert_eq!( + AccessError::from_errno(Errno::Enoent), + AccessError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/acct.rs b/system/linux/syscalls/src/acct.rs index e211eb01..38ba4a4c 100644 --- a/system/linux/syscalls/src/acct.rs +++ b/system/linux/syscalls/src/acct.rs @@ -52,6 +52,8 @@ pub fn acct(name: Option<&CStr>) -> Result<(), AcctError> { mod tests { use std::ffi::CString; + use crate::Errno; + use super::{AcctError, acct}; #[test] @@ -73,4 +75,13 @@ mod tests { Ok(()) => panic!("acct(Some(...)) unexpectedly succeeded"), } } + + #[test] + fn test_acct_error_mapping() { + assert_eq!(AcctError::from_errno(Errno::Eperm), AcctError::Eperm); + assert_eq!( + AcctError::from_errno(Errno::Enoent), + AcctError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/adjtimex.rs b/system/linux/syscalls/src/adjtimex.rs index ed06b042..f0035a99 100644 --- a/system/linux/syscalls/src/adjtimex.rs +++ b/system/linux/syscalls/src/adjtimex.rs @@ -62,6 +62,8 @@ pub fn adjtimex(txc: &mut Timex) -> Result { mod tests { use celer_system_linux_ctypes::{ADJ_TICK, Timex}; + use crate::Errno; + use super::{AdjtimexError, adjtimex}; fn zero_timex() -> Timex { @@ -115,4 +117,28 @@ mod tests { "unprivileged tick changes should be rejected" ); } + + #[test] + fn test_adjtimex_error_mapping() { + assert_eq!( + AdjtimexError::from_errno(Errno::Efault), + AdjtimexError::Efault + ); + assert_eq!( + AdjtimexError::from_errno(Errno::Einval), + AdjtimexError::Einval + ); + assert_eq!( + AdjtimexError::from_errno(Errno::Enodev), + AdjtimexError::Enodev + ); + assert_eq!( + AdjtimexError::from_errno(Errno::Eperm), + AdjtimexError::Eperm + ); + assert_eq!( + AdjtimexError::from_errno(Errno::Enoent), + AdjtimexError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/brk.rs b/system/linux/syscalls/src/brk.rs index ac41b4b9..e493c29e 100644 --- a/system/linux/syscalls/src/brk.rs +++ b/system/linux/syscalls/src/brk.rs @@ -66,7 +66,7 @@ pub unsafe fn brk(addr: UnsignedLong) -> Result { mod tests { use celer_system_linux_ctypes::UnsignedLong; - use crate::sys::test_support::process_global_state_guard; + use crate::{Errno, sys::test_support::process_global_state_guard}; use super::{BrkError, brk}; @@ -88,4 +88,13 @@ mod tests { let same = unsafe { brk(current) }; assert_eq!(same, Ok(current)); } + + #[test] + fn test_brk_error_mapping() { + assert_eq!(BrkError::from_errno(Errno::Eintr), BrkError::Eintr); + assert_eq!( + BrkError::from_errno(Errno::Enomem), + BrkError::Other(Errno::Enomem) + ); + } } diff --git a/system/linux/syscalls/src/chdir.rs b/system/linux/syscalls/src/chdir.rs index aac5b115..def4b150 100644 --- a/system/linux/syscalls/src/chdir.rs +++ b/system/linux/syscalls/src/chdir.rs @@ -91,4 +91,12 @@ mod tests { Err(ChdirError::Other(Errno::Enoent)) ); } + + #[test] + fn test_chdir_error_mapping() { + assert_eq!( + ChdirError::from_errno(Errno::Enoent), + ChdirError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/chmod.rs b/system/linux/syscalls/src/chmod.rs index 5ce3d707..e7d1b4b6 100644 --- a/system/linux/syscalls/src/chmod.rs +++ b/system/linux/syscalls/src/chmod.rs @@ -105,4 +105,15 @@ mod tests { Err(ChmodError::Other(Errno::Enoent)) ); } + + #[test] + fn test_chmod_error_mapping() { + assert_eq!(ChmodError::from_errno(Errno::Eintr), ChmodError::Eintr); + assert_eq!(ChmodError::from_errno(Errno::Eperm), ChmodError::Eperm); + assert_eq!(ChmodError::from_errno(Errno::Erofs), ChmodError::Erofs); + assert_eq!( + ChmodError::from_errno(Errno::Enoent), + ChmodError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/chroot.rs b/system/linux/syscalls/src/chroot.rs index 9f045da3..158ca50d 100644 --- a/system/linux/syscalls/src/chroot.rs +++ b/system/linux/syscalls/src/chroot.rs @@ -64,6 +64,8 @@ mod tests { time::{SystemTime, UNIX_EPOCH}, }; + use crate::Errno; + use super::{ChrootError, chroot}; fn temp_path(stem: &str) -> std::path::PathBuf { @@ -94,4 +96,18 @@ mod tests { fs::remove_file(&path).unwrap(); } + + #[test] + fn test_chroot_error_mapping() { + assert_eq!(ChrootError::from_errno(Errno::Enoent), ChrootError::Enoent); + assert_eq!( + ChrootError::from_errno(Errno::Enotdir), + ChrootError::Enotdir + ); + assert_eq!(ChrootError::from_errno(Errno::Eperm), ChrootError::Eperm); + assert_eq!( + ChrootError::from_errno(Errno::Eacces), + ChrootError::Other(Errno::Eacces) + ); + } } diff --git a/system/linux/syscalls/src/close.rs b/system/linux/syscalls/src/close.rs index 238112d1..6a0b632b 100644 --- a/system/linux/syscalls/src/close.rs +++ b/system/linux/syscalls/src/close.rs @@ -55,6 +55,8 @@ mod tests { time::{SystemTime, UNIX_EPOCH}, }; + use crate::Errno; + use super::{CloseError, close}; #[test] @@ -82,4 +84,14 @@ mod tests { fn test_close_ebadf() { assert_eq!(close(-1), Err(CloseError::Ebadf)); } + + #[test] + fn test_close_error_mapping() { + assert_eq!(CloseError::from_errno(Errno::Ebadf), CloseError::Ebadf); + assert_eq!(CloseError::from_errno(Errno::Eintr), CloseError::Eintr); + assert_eq!( + CloseError::from_errno(Errno::Enomem), + CloseError::Other(Errno::Enomem) + ); + } } diff --git a/system/linux/syscalls/src/creat.rs b/system/linux/syscalls/src/creat.rs index f02aab5f..c79135e5 100644 --- a/system/linux/syscalls/src/creat.rs +++ b/system/linux/syscalls/src/creat.rs @@ -99,4 +99,12 @@ mod tests { Err(CreatError::Other(Errno::Enoent)) ); } + + #[test] + fn test_creat_error_mapping() { + assert_eq!( + CreatError::from_errno(Errno::Enoent), + CreatError::Other(Errno::Enoent) + ); + } } diff --git a/system/linux/syscalls/src/create_module.rs b/system/linux/syscalls/src/create_module.rs index 5aea6b66..7a5a40a3 100644 --- a/system/linux/syscalls/src/create_module.rs +++ b/system/linux/syscalls/src/create_module.rs @@ -72,21 +72,29 @@ pub fn create_module( // duration of the syscall. let ret = unsafe { sys::create_module(module_name.as_ptr(), size) }; + create_module_from_ret(ret) +} + +fn create_module_from_ret( + ret: UnsignedLong, +) -> Result, CreateModuleError> { result_from_ret( ret as isize, - |ret| { - // SAFETY: a successful Linux 1.0 `create_module` return is the - // base address of the allocated module image. - unsafe { - NonNull::new_unchecked(ptr::without_provenance_mut::( - ret as usize, - )) - } - }, + create_module_success, CreateModuleError::from_errno, ) } +fn create_module_success(ret: isize) -> NonNull { + // SAFETY: a successful Linux 1.0 `create_module` return is the base + // address of the allocated module image. + unsafe { + NonNull::new_unchecked(ptr::without_provenance_mut::( + ret as usize, + )) + } +} + #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { @@ -94,7 +102,9 @@ mod tests { use celer_system_linux_ctypes::UnsignedLong; - use super::{CreateModuleError, create_module}; + use crate::Errno; + + use super::{CreateModuleError, create_module, create_module_from_ret}; #[test] fn test_create_module_zero_size() { @@ -116,4 +126,43 @@ mod tests { ); assert_eq!(err, CreateModuleError::Enosys); } + + #[test] + fn test_create_module_success_mapping() { + let ptr = create_module_from_ret(4096 as UnsignedLong).unwrap(); + + assert_eq!(ptr.addr().get(), 4096); + } + + #[test] + fn test_create_module_error_mapping() { + assert_eq!( + CreateModuleError::from_errno(Errno::Eperm), + CreateModuleError::Eperm + ); + assert_eq!( + CreateModuleError::from_errno(Errno::Einval), + CreateModuleError::Einval + ); + assert_eq!( + CreateModuleError::from_errno(Errno::E2big), + CreateModuleError::E2big + ); + assert_eq!( + CreateModuleError::from_errno(Errno::Eexist), + CreateModuleError::Eexist + ); + assert_eq!( + CreateModuleError::from_errno(Errno::Enomem), + CreateModuleError::Enomem + ); + assert_eq!( + CreateModuleError::from_errno(Errno::Enosys), + CreateModuleError::Enosys + ); + assert_eq!( + CreateModuleError::from_errno(Errno::Enoent), + CreateModuleError::Other(Errno::Enoent) + ); + } } From 5d1f9ce5f1173a4e8984e287ee289bfed24f6cc3 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 20:50:31 +0200 Subject: [PATCH 017/135] test: cover named errno mappings --- system/linux/syscalls/src/errno.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 84ae055b..d711edbf 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -60,6 +60,25 @@ impl Errno { mod tests { use super::Errno; + #[test] + fn test_from_raw_named_values() { + assert_eq!(Errno::from_raw(13), Errno::Eacces); + assert_eq!(Errno::from_raw(7), Errno::E2big); + assert_eq!(Errno::from_raw(9), Errno::Ebadf); + assert_eq!(Errno::from_raw(14), Errno::Efault); + assert_eq!(Errno::from_raw(17), Errno::Eexist); + assert_eq!(Errno::from_raw(22), Errno::Einval); + assert_eq!(Errno::from_raw(4), Errno::Eintr); + assert_eq!(Errno::from_raw(19), Errno::Enodev); + assert_eq!(Errno::from_raw(2), Errno::Enoent); + assert_eq!(Errno::from_raw(12), Errno::Enomem); + assert_eq!(Errno::from_raw(38), Errno::Enosys); + assert_eq!(Errno::from_raw(20), Errno::Enotdir); + assert_eq!(Errno::from_raw(1), Errno::Eperm); + assert_eq!(Errno::from_raw(30), Errno::Erofs); + assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); + } + #[test] fn test_is_errno_range() { assert!(!Errno::is_errno(0)); From 5afe32cc7e5732bc11b47b136af1e262fbea5b32 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:15:28 +0200 Subject: [PATCH 018/135] feat(syscalls): wrap delete_module --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/delete_module.rs | 179 +++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 6 + system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/delete_module.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index bceb7c99..43e5a05d 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -111,7 +111,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `close` - [x] `creat` - x86, x86_64 - [x] `create_module` - x86 -- [ ] `delete_module` +- [x] `delete_module` - [ ] `dup` - [ ] `dup2` - x86, x86_64 - [ ] `execve` diff --git a/system/linux/syscalls/src/delete_module.rs b/system/linux/syscalls/src/delete_module.rs new file mode 100644 index 00000000..af36427d --- /dev/null +++ b/system/linux/syscalls/src/delete_module.rs @@ -0,0 +1,179 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::UnsignedInt; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`delete_module`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DeleteModuleError { + /// `EPERM`. + Eperm, + /// Linux 1.0 `E2BIG` when `module_name` exceeds `MOD_MAX_NAME`. + E2big, + /// `ENOENT`. + Enoent, + /// Current kernels' `EFAULT` for an unreadable module name pointer. + Efault, + /// `EINTR`. + Eintr, + /// Current kernels' `EWOULDBLOCK`. + Ewouldblock, + /// Current kernels' `EBUSY`. + Ebusy, + /// `ENOSYS`. + Enosys, + /// Another errno returned by the raw syscall. + Other(Errno), +} + +impl DeleteModuleError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::E2big => Self::E2big, + Errno::Enoent => Self::Enoent, + Errno::Efault => Self::Efault, + Errno::Eintr => Self::Eintr, + Errno::Ewouldblock => Self::Ewouldblock, + Errno::Ebusy => Self::Ebusy, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Unload a kernel module by name. +/// +/// This safe wrapper converts the raw module-name pointer into +/// `Option<&CStr>` and maps the raw `delete_module(2)` return value into +/// `Result<(), DeleteModuleError>`. It preserves the kernel-facing `flags` +/// argument as-is. +/// +/// Passing `Some(name)` unloads the named module. Passing `None` preserves the +/// historical Linux 1.0 null-name case and passes a null pointer to the raw +/// syscall. +/// +/// On success, returns `Ok(())` after the kernel has completed the unload +/// path. +/// +/// See [`sys::delete_module`] for kernel behavior, reachable errors, and +/// source references. +/// +/// # Errors +/// - [`DeleteModuleError::Eperm`]: the caller lacks permission to unload +/// modules, or current kernels have module loading disabled globally. +/// - [`DeleteModuleError::E2big`]: Linux 1.0 scanned `module_name` without +/// finding a trailing NUL before `MOD_MAX_NAME`. +/// - [`DeleteModuleError::Enoent`]: no loaded module matches `module_name`, or +/// current kernels reject an empty or too-long name. +/// - [`DeleteModuleError::Efault`]: current kernels cannot read the module +/// name from user memory. +/// - [`DeleteModuleError::Eintr`]: a current kernel was interrupted while +/// waiting for `module_mutex`. +/// - [`DeleteModuleError::Ewouldblock`]: a current kernel found dependent +/// modules or refused to stop the target without force. +/// - [`DeleteModuleError::Ebusy`]: a current kernel found the module already +/// unloading, not live, or lacking an unload path that force flags can use. +/// - [`DeleteModuleError::Enosys`]: the syscall slot is unimplemented on the +/// running kernel. +/// - [`DeleteModuleError::Other`]: another errno. +pub fn delete_module( + module_name: Option<&CStr>, + flags: UnsignedInt, +) -> Result<(), DeleteModuleError> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pointer for the + // duration of the syscall, and `None` maps to a null pointer. + let ret = unsafe { + sys::delete_module( + module_name.map_or(core::ptr::null(), CStr::as_ptr), + flags, + ) + }; + + unit_from_ret(ret as isize, DeleteModuleError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use celer_system_linux_ctypes::UnsignedInt; + + use crate::Errno; + + use super::{DeleteModuleError, delete_module}; + + #[test] + fn test_delete_module_missing_module() { + let name = + CString::new("definitely_not_a_loaded_celer_module").unwrap(); + + let err = delete_module(Some(name.as_c_str()), 0 as UnsignedInt) + .expect_err("missing module should not unload successfully"); + + assert!(matches!( + err, + DeleteModuleError::Eperm + | DeleteModuleError::Enoent + | DeleteModuleError::Enosys + )); + } + + #[test] + fn test_delete_module_error_mapping() { + assert_eq!( + DeleteModuleError::from_errno(Errno::Eperm), + DeleteModuleError::Eperm + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::E2big), + DeleteModuleError::E2big + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Enoent), + DeleteModuleError::Enoent + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Efault), + DeleteModuleError::Efault + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Eintr), + DeleteModuleError::Eintr + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Ewouldblock), + DeleteModuleError::Ewouldblock + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Ebusy), + DeleteModuleError::Ebusy + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Enosys), + DeleteModuleError::Enosys + ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Enomem), + DeleteModuleError::Other(Errno::Enomem) + ); + } + + #[test] + fn test_delete_module_none() { + let err = delete_module(None, 0 as UnsignedInt).expect_err( + "delete_module(None, 0) should fail on current kernels unless Linux 1.0 semantics are active", + ); + + assert!(matches!( + err, + DeleteModuleError::Eperm + | DeleteModuleError::Efault + | DeleteModuleError::Enosys + )); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index d711edbf..86e6db22 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -4,6 +4,7 @@ pub enum Errno { Eacces, E2big, Ebadf, + Ebusy, Efault, Eexist, Einval, @@ -15,6 +16,7 @@ pub enum Errno { Enotdir, Eperm, Erofs, + Ewouldblock, Raw(u16), } @@ -25,6 +27,7 @@ impl Errno { 13 => Self::Eacces, 7 => Self::E2big, 9 => Self::Ebadf, + 16 => Self::Ebusy, 14 => Self::Efault, 17 => Self::Eexist, 22 => Self::Einval, @@ -36,6 +39,7 @@ impl Errno { 20 => Self::Enotdir, 1 => Self::Eperm, 30 => Self::Erofs, + 11 => Self::Ewouldblock, other => Self::Raw(other), } } @@ -65,6 +69,7 @@ mod tests { assert_eq!(Errno::from_raw(13), Errno::Eacces); assert_eq!(Errno::from_raw(7), Errno::E2big); assert_eq!(Errno::from_raw(9), Errno::Ebadf); + assert_eq!(Errno::from_raw(16), Errno::Ebusy); assert_eq!(Errno::from_raw(14), Errno::Efault); assert_eq!(Errno::from_raw(17), Errno::Eexist); assert_eq!(Errno::from_raw(22), Errno::Einval); @@ -76,6 +81,7 @@ mod tests { assert_eq!(Errno::from_raw(20), Errno::Enotdir); assert_eq!(Errno::from_raw(1), Errno::Eperm); assert_eq!(Errno::from_raw(30), Errno::Erofs); + assert_eq!(Errno::from_raw(11), Errno::Ewouldblock); assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); } diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index d4191d79..88b51982 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -26,6 +26,7 @@ mod close; mod creat; #[cfg(target_arch = "x86")] mod create_module; +mod delete_module; mod errno; mod helpers; pub mod sys; @@ -44,6 +45,7 @@ pub use chroot::{ChrootError, chroot}; pub use close::{CloseError, close}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use creat::{CreatError, creat}; +pub use delete_module::{DeleteModuleError, delete_module}; pub use errno::Errno; /// Wrapped historical Linux 1.0 syscall ABIs. From 00e9edbb5bcc6a0a2d0a39a00485c5781ea80ca9 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:16:09 +0200 Subject: [PATCH 019/135] feat(syscalls): add dup wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/dup.rs | 108 +++++++++++++++++++++++++ system/linux/syscalls/src/errno.rs | 3 + system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/dup.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 43e5a05d..96fde47c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -112,7 +112,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `creat` - x86, x86_64 - [x] `create_module` - x86 - [x] `delete_module` -- [ ] `dup` +- [x] `dup` - [ ] `dup2` - x86, x86_64 - [ ] `execve` - [ ] `exit` diff --git a/system/linux/syscalls/src/dup.rs b/system/linux/syscalls/src/dup.rs new file mode 100644 index 00000000..4c566aa3 --- /dev/null +++ b/system/linux/syscalls/src/dup.rs @@ -0,0 +1,108 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`dup`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DupError { + /// `EBADF`. + Ebadf, + /// `EMFILE`. + Emfile, + /// Another errno returned while expanding the descriptor table. + Other(Errno), +} + +impl DupError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Emfile => Self::Emfile, + errno => Self::Other(errno), + } + } +} + +/// Duplicate an open file descriptor. +/// +/// This safe wrapper maps the raw `dup(2)` return value into +/// `Result` while keeping the file-descriptor argument as the +/// kernel-facing integer type. +/// +/// On success, returns the new nonnegative file descriptor selected by the +/// kernel. +/// +/// See [`sys::dup`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`DupError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`DupError::Emfile`]: the calling task cannot allocate another file +/// descriptor slot. +/// - [`DupError::Other`]: another descriptor-table expansion error. +pub fn dup(fd: Int) -> Result { + let ret = sys::dup(fd as _); + + result_from_ret(ret as isize, |ret| ret as Int, DupError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::IntoRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{DupError, dup}; + use crate::Errno; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_dup_{now}")); + path + } + + #[test] + fn test_dup_ok() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + let dup_fd = dup(fd).expect("dup should succeed for an open fd"); + assert_ne!(dup_fd, fd); + + assert_eq!(crate::close(dup_fd), Ok(())); + assert_eq!(crate::close(fd), Ok(())); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_dup_ebadf() { + assert_eq!(dup(-1), Err(DupError::Ebadf)); + } + + #[test] + fn test_dup_error_mapping() { + assert_eq!(DupError::from_errno(Errno::Ebadf), DupError::Ebadf); + assert_eq!(DupError::from_errno(Errno::Emfile), DupError::Emfile); + assert_eq!( + DupError::from_errno(Errno::Enomem), + DupError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 86e6db22..18a31004 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -5,6 +5,7 @@ pub enum Errno { E2big, Ebadf, Ebusy, + Emfile, Efault, Eexist, Einval, @@ -28,6 +29,7 @@ impl Errno { 7 => Self::E2big, 9 => Self::Ebadf, 16 => Self::Ebusy, + 24 => Self::Emfile, 14 => Self::Efault, 17 => Self::Eexist, 22 => Self::Einval, @@ -70,6 +72,7 @@ mod tests { assert_eq!(Errno::from_raw(7), Errno::E2big); assert_eq!(Errno::from_raw(9), Errno::Ebadf); assert_eq!(Errno::from_raw(16), Errno::Ebusy); + assert_eq!(Errno::from_raw(24), Errno::Emfile); assert_eq!(Errno::from_raw(14), Errno::Efault); assert_eq!(Errno::from_raw(17), Errno::Eexist); assert_eq!(Errno::from_raw(22), Errno::Einval); diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 88b51982..ca0004d9 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -27,6 +27,7 @@ mod creat; #[cfg(target_arch = "x86")] mod create_module; mod delete_module; +mod dup; mod errno; mod helpers; pub mod sys; @@ -46,6 +47,7 @@ pub use close::{CloseError, close}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use creat::{CreatError, creat}; pub use delete_module::{DeleteModuleError, delete_module}; +pub use dup::{DupError, dup}; pub use errno::Errno; /// Wrapped historical Linux 1.0 syscall ABIs. From 834868ae452a433462a1ab5dbde82352ab7b0183 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:17:21 +0200 Subject: [PATCH 020/135] feat(syscalls): add dup2 wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/dup2.rs | 140 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/dup2.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 96fde47c..f195f08b 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -113,7 +113,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `create_module` - x86 - [x] `delete_module` - [x] `dup` -- [ ] `dup2` - x86, x86_64 +- [x] `dup2` - x86, x86_64 - [ ] `execve` - [ ] `exit` - [ ] `fchdir` diff --git a/system/linux/syscalls/src/dup2.rs b/system/linux/syscalls/src/dup2.rs new file mode 100644 index 00000000..4531e5a4 --- /dev/null +++ b/system/linux/syscalls/src/dup2.rs @@ -0,0 +1,140 @@ +use celer_system_linux_ctypes::{Int, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`dup2`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Dup2Error { + /// `EBADF`. + Ebadf, + /// `EBUSY`. + Ebusy, + /// Another errno returned by delegated descriptor-table replacement work. + Other(Errno), +} + +impl Dup2Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Ebusy => Self::Ebusy, + errno => Self::Other(errno), + } + } +} + +/// Duplicate `oldfd` onto the exact descriptor number `newfd`. +/// +/// This safe wrapper maps the raw `dup2(2)` return value into +/// `Result` while keeping the file-descriptor arguments as the +/// kernel-facing integer type. +/// +/// On success, returns `Ok(newfd)`. +/// +/// See [`sys::dup2`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Dup2Error::Ebadf`]: `oldfd` is not open, or `newfd` is outside the +/// syscall's accepted descriptor range. +/// - [`Dup2Error::Ebusy`]: a current kernel lost a descriptor-table race while +/// replacing `newfd`. +/// - [`Dup2Error::Other`]: another descriptor-table replacement error. +pub fn dup2(oldfd: UnsignedInt, newfd: UnsignedInt) -> Result { + let ret = sys::dup2(oldfd, newfd); + + result_from_ret(ret as isize, |ret| ret as Int, Dup2Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::IntoRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UnsignedInt; + + use super::{Dup2Error, dup2}; + use crate::Errno; + + fn create_temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + path.push(format!("celer_wrap_dup2_{now}")); + + path + } + + #[test] + fn test_dup2_ok() { + let old_path = create_temp_path(); + let new_path = create_temp_path(); + let old_file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&old_path) + .unwrap(); + let new_file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&new_path) + .unwrap(); + + let oldfd = old_file.into_raw_fd(); + let newfd = new_file.into_raw_fd(); + + assert_eq!(dup2(oldfd as UnsignedInt, newfd as UnsignedInt), Ok(newfd)); + + assert_eq!(crate::sys::close(newfd), 0); + assert_eq!(crate::sys::close(oldfd), 0); + fs::remove_file(&old_path).unwrap(); + fs::remove_file(&new_path).unwrap(); + } + + #[test] + fn test_dup2_same_fd_ok() { + let path = create_temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + + assert_eq!(dup2(fd as UnsignedInt, fd as UnsignedInt), Ok(fd)); + + assert_eq!(crate::sys::close(fd), 0); + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_dup2_ebadf() { + assert_eq!(dup2(!0 as UnsignedInt, 0), Err(Dup2Error::Ebadf)); + } + + #[test] + fn test_dup2_error_mapping() { + assert_eq!(Dup2Error::from_errno(Errno::Ebadf), Dup2Error::Ebadf); + assert_eq!(Dup2Error::from_errno(Errno::Ebusy), Dup2Error::Ebusy); + assert_eq!( + Dup2Error::from_errno(Errno::Enomem), + Dup2Error::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index ca0004d9..b73985d2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -28,6 +28,8 @@ mod creat; mod create_module; mod delete_module; mod dup; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod dup2; mod errno; mod helpers; pub mod sys; @@ -48,6 +50,8 @@ pub use close::{CloseError, close}; pub use creat::{CreatError, creat}; pub use delete_module::{DeleteModuleError, delete_module}; pub use dup::{DupError, dup}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use dup2::{Dup2Error, dup2}; pub use errno::Errno; /// Wrapped historical Linux 1.0 syscall ABIs. From 3b4a4181bc67d96d4fe5e2aaa46d8287aa20f851 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:18:37 +0200 Subject: [PATCH 021/135] feat(syscalls): add execve wrap layer --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/delete_module.rs | 6 +- system/linux/syscalls/src/errno.rs | 11 +- system/linux/syscalls/src/execve.rs | 169 +++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 system/linux/syscalls/src/execve.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index f195f08b..d4900cbc 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -114,7 +114,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `delete_module` - [x] `dup` - [x] `dup2` - x86, x86_64 -- [ ] `execve` +- [x] `execve` - [ ] `exit` - [ ] `fchdir` - [ ] `fchmod` diff --git a/system/linux/syscalls/src/delete_module.rs b/system/linux/syscalls/src/delete_module.rs index af36427d..a598b158 100644 --- a/system/linux/syscalls/src/delete_module.rs +++ b/system/linux/syscalls/src/delete_module.rs @@ -37,7 +37,7 @@ impl DeleteModuleError { Errno::Enoent => Self::Enoent, Errno::Efault => Self::Efault, Errno::Eintr => Self::Eintr, - Errno::Ewouldblock => Self::Ewouldblock, + Errno::Eagain | Errno::Ewouldblock => Self::Ewouldblock, Errno::Ebusy => Self::Ebusy, Errno::Enosys => Self::Enosys, errno => Self::Other(errno), @@ -145,6 +145,10 @@ mod tests { DeleteModuleError::from_errno(Errno::Eintr), DeleteModuleError::Eintr ); + assert_eq!( + DeleteModuleError::from_errno(Errno::Eagain), + DeleteModuleError::Ewouldblock + ); assert_eq!( DeleteModuleError::from_errno(Errno::Ewouldblock), DeleteModuleError::Ewouldblock diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 18a31004..735647fc 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -2,6 +2,7 @@ #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Errno { Eacces, + Eagain, E2big, Ebadf, Ebusy, @@ -10,8 +11,10 @@ pub enum Errno { Eexist, Einval, Eintr, + Eloop, Enodev, Enoent, + Enoexec, Enomem, Enosys, Enotdir, @@ -26,6 +29,7 @@ impl Errno { pub const fn from_raw(raw: u16) -> Self { match raw { 13 => Self::Eacces, + 11 => Self::Eagain, 7 => Self::E2big, 9 => Self::Ebadf, 16 => Self::Ebusy, @@ -34,14 +38,15 @@ impl Errno { 17 => Self::Eexist, 22 => Self::Einval, 4 => Self::Eintr, + 40 => Self::Eloop, 19 => Self::Enodev, 2 => Self::Enoent, + 8 => Self::Enoexec, 12 => Self::Enomem, 38 => Self::Enosys, 20 => Self::Enotdir, 1 => Self::Eperm, 30 => Self::Erofs, - 11 => Self::Ewouldblock, other => Self::Raw(other), } } @@ -69,6 +74,7 @@ mod tests { #[test] fn test_from_raw_named_values() { assert_eq!(Errno::from_raw(13), Errno::Eacces); + assert_eq!(Errno::from_raw(11), Errno::Eagain); assert_eq!(Errno::from_raw(7), Errno::E2big); assert_eq!(Errno::from_raw(9), Errno::Ebadf); assert_eq!(Errno::from_raw(16), Errno::Ebusy); @@ -77,14 +83,15 @@ mod tests { assert_eq!(Errno::from_raw(17), Errno::Eexist); assert_eq!(Errno::from_raw(22), Errno::Einval); assert_eq!(Errno::from_raw(4), Errno::Eintr); + assert_eq!(Errno::from_raw(40), Errno::Eloop); assert_eq!(Errno::from_raw(19), Errno::Enodev); assert_eq!(Errno::from_raw(2), Errno::Enoent); + assert_eq!(Errno::from_raw(8), Errno::Enoexec); assert_eq!(Errno::from_raw(12), Errno::Enomem); assert_eq!(Errno::from_raw(38), Errno::Enosys); assert_eq!(Errno::from_raw(20), Errno::Enotdir); assert_eq!(Errno::from_raw(1), Errno::Eperm); assert_eq!(Errno::from_raw(30), Errno::Erofs); - assert_eq!(Errno::from_raw(11), Errno::Ewouldblock); assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); } diff --git a/system/linux/syscalls/src/execve.rs b/system/linux/syscalls/src/execve.rs new file mode 100644 index 00000000..2af4237c --- /dev/null +++ b/system/linux/syscalls/src/execve.rs @@ -0,0 +1,169 @@ +use core::{convert::Infallible, ffi::CStr}; + +use celer_system_linux_ctypes::Char; + +use crate::errno::Errno; +use crate::sys; + +/// Errors returned by [`execve`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ExecveError { + /// `E2BIG`. + E2big, + /// `EAGAIN`. + Eagain, + /// `EFAULT`. + Efault, + /// `ELOOP`. + Eloop, + /// `ENOEXEC`. + Enoexec, + /// Another errno returned by delegated lookup, permission, allocation, or + /// binary-loader work. + Other(Errno), +} + +impl ExecveError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::E2big => Self::E2big, + Errno::Eagain => Self::Eagain, + Errno::Efault => Self::Efault, + Errno::Eloop => Self::Eloop, + Errno::Enoexec => Self::Enoexec, + errno => Self::Other(errno), + } + } +} + +/// Replace the current process image with a new program. +/// +/// This wrapper takes a NUL-terminated [`CStr`] for `filename`, maps a null +/// `argv` or `envp` vector to `None`, and converts the raw kernel return into +/// `Result`. +/// +/// On success, this wrapper does not return because the current process image +/// is replaced. +/// +/// See [`sys::execve`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// - Every non-null pointer in `argv` and `envp` must be valid to read a +/// NUL-terminated string for the duration of the syscall. +/// - When `argv` or `envp` is `Some`, the slice must end with a null pointer so +/// the kernel's counted walk stays within the borrowed array. +/// +/// # Errors +/// - [`ExecveError::E2big`]: the argument or environment data exceeds the +/// kernel's size limits. +/// - [`ExecveError::Eagain`]: `PF_NPROC_EXCEEDED` is still set and the caller +/// remains over `RLIMIT_NPROC`. +/// - [`ExecveError::Efault`]: `argv`, `envp`, or one of their pointed-to +/// strings is inaccessible. +/// - [`ExecveError::Eloop`]: the interpreter or binfmt rewrite limit was hit. +/// - [`ExecveError::Enoexec`]: no binary handler accepted the target image. +/// - [`ExecveError::Other`]: delegated lookup, permission, allocation, or +/// binary-loader error. +pub unsafe fn execve( + filename: &CStr, + argv: Option<&[*const Char]>, + envp: Option<&[*const Char]>, +) -> Result { + let argv = argv.map_or(core::ptr::null(), <[*const Char]>::as_ptr); + let envp = envp.map_or(core::ptr::null(), <[*const Char]>::as_ptr); + + // SAFETY: `filename` is a valid NUL-terminated string, and the caller must + // uphold the remaining raw pointer preconditions for `argv` and `envp`. + let ret = unsafe { sys::execve(filename.as_ptr(), argv, envp) }; + Err(ExecveError::from_errno( + Errno::from_kernel_ret(ret as isize) + .expect("execve success is unreachable because it does not return"), + )) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{Char, Int, PidT}; + + use crate::{ + Errno, + sys::test_support::{_exit as exit, fork, waitpid}, + }; + + use super::{ExecveError, execve}; + + #[test] + fn test_execve_ok() { + let pid = unsafe { fork() }; + + fn use_pid(pid: PidT) { + if pid == 0 { + let filename = c"/bin/true"; + let argv: [*const Char; 2] = + [filename.as_ptr().cast(), core::ptr::null()]; + let envp: [*const Char; 1] = [core::ptr::null()]; + + // SAFETY: the pointer arrays are null-terminated, and each + // pointed-to string remains valid for the duration of the syscall. + let ret = unsafe { execve(filename, Some(&argv), Some(&envp)) }; + + if ret.is_err() { + unsafe { exit(1) }; + } + unsafe { exit(0) }; + } + } + + use_pid(pid); + + let mut status: Int = 0; + let waited = unsafe { waitpid(pid, &mut status, 0) }; + + assert_eq!(waited, pid); + assert_eq!(status & 0x7f, 0); + assert_eq!((status >> 8) & 0xff, 0); + } + + #[test] + fn test_execve_missing_path() { + let filename = c"/definitely/not/a/real/celer-wrap-execve"; + let argv: [*const Char; 2] = + [filename.as_ptr().cast(), core::ptr::null()]; + let envp: [*const Char; 1] = [core::ptr::null()]; + + // SAFETY: the pointer arrays are null-terminated, and each pointed-to + // string remains valid for the duration of the syscall. + let ret = unsafe { execve(filename, Some(&argv), Some(&envp)) }; + + assert_eq!(ret, Err(ExecveError::Other(Errno::Enoent))); + } + + #[test] + fn test_execve_null_argv_envp() { + let filename = c"/definitely/not/a/real/celer-wrap-execve"; + + // SAFETY: `argv` and `envp` are passed as null, which the raw syscall + // accepts, and `filename` stays valid for the duration of the syscall. + let ret = unsafe { execve(filename, None, None) }; + + assert_eq!(ret, Err(ExecveError::Other(Errno::Enoent))); + } + + #[test] + fn test_execve_error_mapping() { + assert_eq!(ExecveError::from_errno(Errno::E2big), ExecveError::E2big); + assert_eq!(ExecveError::from_errno(Errno::Eagain), ExecveError::Eagain); + assert_eq!(ExecveError::from_errno(Errno::Efault), ExecveError::Efault); + assert_eq!(ExecveError::from_errno(Errno::Eloop), ExecveError::Eloop); + assert_eq!( + ExecveError::from_errno(Errno::Enoexec), + ExecveError::Enoexec + ); + assert_eq!( + ExecveError::from_errno(Errno::Enoent), + ExecveError::Other(Errno::Enoent) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b73985d2..62a62cf6 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -31,6 +31,7 @@ mod dup; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod dup2; mod errno; +mod execve; mod helpers; pub mod sys; @@ -53,6 +54,7 @@ pub use dup::{DupError, dup}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use dup2::{Dup2Error, dup2}; pub use errno::Errno; +pub use execve::{ExecveError, execve}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 40d721aa1f2320e466a8a01aa929ceb16bcaadf1 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:19:10 +0200 Subject: [PATCH 022/135] feat(syscalls): add wrapped exit wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/exit.rs | 52 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/exit.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index d4900cbc..e5fcccdc 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -115,7 +115,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `dup` - [x] `dup2` - x86, x86_64 - [x] `execve` -- [ ] `exit` +- [x] `exit` - [ ] `fchdir` - [ ] `fchmod` - [ ] `fchown16` - x86 diff --git a/system/linux/syscalls/src/exit.rs b/system/linux/syscalls/src/exit.rs new file mode 100644 index 00000000..90f84455 --- /dev/null +++ b/system/linux/syscalls/src/exit.rs @@ -0,0 +1,52 @@ +use celer_system_linux_ctypes::Int; + +use crate::sys; + +/// Terminate the calling thread with `status`. +/// +/// This safe wrapper keeps the raw integer exit-status argument and exposes the +/// syscall's non-returning behavior as `!`. +/// +/// On normal syscall dispatch this function never returns. If a syscall-entry +/// filter such as seccomp or ptrace synthesizes a return before the kernel +/// reaches `exit`, this wrapper panics so its Rust signature remains +/// non-returning. +/// +/// See [`sys::exit`] for kernel behavior, reachable pre-dispatch synthetic +/// returns, and source references. +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn exit(status: Int) -> ! { + let ret = sys::exit(status); + panic!("sys::exit returned unexpectedly with {ret}"); +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::Int; + + use super::exit; + use crate::sys::test_support::{fork, waitpid}; + + #[test] + fn test_exit_terminates_child() { + // SAFETY: `fork` isolates the terminating wrapper call in the child. + let pid = unsafe { fork() }; + assert!(pid >= 0, "fork failed: {pid}"); + + if pid == 0 { + exit(0); + } + + let mut status = 0 as Int; + let waited = unsafe { waitpid(pid, &raw mut status, 0) }; + + assert_eq!(waited, pid, "waitpid failed: {waited}"); + assert_eq!(status & 0x7f, 0, "child died from signal: {status}"); + assert_eq!( + (status >> 8) & 0xff, + 0, + "child exited with nonzero status: {status}" + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 62a62cf6..4d9e0cb0 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -32,6 +32,7 @@ mod dup; mod dup2; mod errno; mod execve; +mod exit; mod helpers; pub mod sys; @@ -55,6 +56,7 @@ pub use dup::{DupError, dup}; pub use dup2::{Dup2Error, dup2}; pub use errno::Errno; pub use execve::{ExecveError, execve}; +pub use exit::exit; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 43ae3bcd944cba30e0f784cc2d5a746fd3d8427c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:39:03 +0200 Subject: [PATCH 023/135] feat(syscalls): wrap fchdir --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fchdir.rs | 135 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fchdir.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index e5fcccdc..afb56297 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -116,7 +116,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `dup2` - x86, x86_64 - [x] `execve` - [x] `exit` -- [ ] `fchdir` +- [x] `fchdir` - [ ] `fchmod` - [ ] `fchown16` - x86 - [ ] `fcntl` diff --git a/system/linux/syscalls/src/fchdir.rs b/system/linux/syscalls/src/fchdir.rs new file mode 100644 index 00000000..bc475a26 --- /dev/null +++ b/system/linux/syscalls/src/fchdir.rs @@ -0,0 +1,135 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`fchdir`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FchdirError { + /// `EBADF`. + Ebadf, + /// `EACCES`. + Eacces, + /// `ENOTDIR`. + Enotdir, + /// Another errno returned by delegated permission or filesystem work. + Other(Errno), +} + +impl FchdirError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Eacces => Self::Eacces, + Errno::Enotdir => Self::Enotdir, + errno => Self::Other(errno), + } + } +} + +/// Change the calling process's current working directory to the directory +/// referenced by `fd`. +/// +/// This safe wrapper maps the raw `fchdir(2)` return value into +/// `Result<(), FchdirError>` while keeping the file-descriptor argument as the +/// crate's integer fd type. +/// +/// On success, returns `Ok(())` after the kernel switches the current working +/// directory to the directory referenced by `fd`. +/// +/// See [`sys::fchdir`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`FchdirError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FchdirError::Eacces`]: the referenced directory cannot be searched. +/// - [`FchdirError::Enotdir`]: `fd` does not refer to a directory. +/// - [`FchdirError::Other`]: delegated permission or filesystem error. +pub fn fchdir(fd: Int) -> Result<(), FchdirError> { + let ret = sys::fchdir(fd as _); + + unit_from_ret(ret as isize, FchdirError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + env, fs, + fs::OpenOptions, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{FchdirError, fchdir}; + use crate::{Errno, sys::test_support::process_global_state_guard}; + + fn create_temp_dir(label: &str) -> std::path::PathBuf { + let mut path = env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + path.push(format!("celer_wrap_fchdir_{label}_{now}")); + fs::create_dir(&path).unwrap(); + + path + } + + #[test] + fn test_fchdir_ok() { + let _guard = process_global_state_guard(); + let original_dir = env::current_dir().unwrap(); + let path = create_temp_dir("success"); + let dir = OpenOptions::new().read(true).open(&path).unwrap(); + + assert_eq!(fchdir(dir.as_raw_fd()), Ok(())); + assert_eq!(env::current_dir().unwrap(), path); + + env::set_current_dir(&original_dir).unwrap(); + fs::remove_dir(&path).unwrap(); + } + + #[test] + fn test_fchdir_ebadf() { + assert_eq!(fchdir(-1), Err(FchdirError::Ebadf)); + } + + #[test] + fn test_fchdir_enotdir() { + let path = env::temp_dir().join(format!( + "celer_wrap_fchdir_file_{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + assert_eq!(fchdir(file.as_raw_fd()), Err(FchdirError::Enotdir)); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fchdir_error_mapping() { + assert_eq!(FchdirError::from_errno(Errno::Ebadf), FchdirError::Ebadf); + assert_eq!( + FchdirError::from_errno(Errno::Enotdir), + FchdirError::Enotdir + ); + assert_eq!(FchdirError::from_errno(Errno::Eacces), FchdirError::Eacces); + assert_eq!( + FchdirError::from_errno(Errno::Enoent), + FchdirError::Other(Errno::Enoent) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 4d9e0cb0..88b772c2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -33,6 +33,7 @@ mod dup2; mod errno; mod execve; mod exit; +mod fchdir; mod helpers; pub mod sys; @@ -57,6 +58,7 @@ pub use dup2::{Dup2Error, dup2}; pub use errno::Errno; pub use execve::{ExecveError, execve}; pub use exit::exit; +pub use fchdir::{FchdirError, fchdir}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 663e2ca4557dd403c19017310317b5b24752afc6 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:38:55 +0200 Subject: [PATCH 024/135] feat(syscalls): add wrapped fchmod --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fchmod.rs | 124 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fchmod.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index afb56297..de6ef920 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -117,7 +117,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `execve` - [x] `exit` - [x] `fchdir` -- [ ] `fchmod` +- [x] `fchmod` - [ ] `fchown16` - x86 - [ ] `fcntl` - [ ] `fork` - x86 diff --git a/system/linux/syscalls/src/fchmod.rs b/system/linux/syscalls/src/fchmod.rs new file mode 100644 index 00000000..f7747e78 --- /dev/null +++ b/system/linux/syscalls/src/fchmod.rs @@ -0,0 +1,124 @@ +use celer_system_linux_ctypes::{Int, UModeT}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`fchmod`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FchmodError { + /// `EBADF`. + Ebadf, + /// `ENOENT`. + Enoent, + /// `EPERM`. + Eperm, + /// `EROFS`. + Erofs, + /// Another errno returned by delegated filesystem, inode, or security-hook + /// work. + Other(Errno), +} + +impl FchmodError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Enoent => Self::Enoent, + Errno::Eperm => Self::Eperm, + Errno::Erofs => Self::Erofs, + errno => Self::Other(errno), + } + } +} + +/// Change the mode bits of the file referenced by `fd`. +/// +/// This safe wrapper maps the raw `fchmod(2)` return value into +/// `Result<(), FchmodError>` while keeping the file-descriptor argument as the +/// kernel-facing integer type. +/// +/// On success, returns `Ok(())` after the kernel applies the requested mode +/// bits to the file referenced by `fd`. +/// +/// See [`sys::fchmod`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`FchmodError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FchmodError::Enoent`]: `fd` names a file-table entry with no inode. +/// - [`FchmodError::Eperm`]: the caller was not allowed to change the mode. +/// - [`FchmodError::Erofs`]: the referenced inode is on a read-only +/// filesystem. +/// - [`FchmodError::Other`]: delegated filesystem, inode, or security-hook +/// error. +pub fn fchmod(fd: Int, mode: UModeT) -> Result<(), FchmodError> { + let ret = sys::fchmod(fd, mode); + + unit_from_ret(ret as isize, FchmodError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::IntoRawFd as _, + os::unix::fs::PermissionsExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UModeT; + + use crate::Errno; + + use super::{FchmodError, fchmod}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_fchmod_{now}")); + path + } + + #[test] + fn test_fchmod_ok() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + let fd = file.into_raw_fd(); + + assert_eq!(fchmod(fd, 0o600 as UModeT), Ok(())); + + let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o7777; + assert_eq!(mode, 0o600); + + assert_eq!(crate::sys::close(fd), 0); + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fchmod_ebadf() { + assert_eq!(fchmod(-1, 0o600 as UModeT), Err(FchmodError::Ebadf)); + } + + #[test] + fn test_fchmod_error_mapping() { + assert_eq!(FchmodError::from_errno(Errno::Ebadf), FchmodError::Ebadf); + assert_eq!(FchmodError::from_errno(Errno::Enoent), FchmodError::Enoent); + assert_eq!(FchmodError::from_errno(Errno::Eperm), FchmodError::Eperm); + assert_eq!(FchmodError::from_errno(Errno::Erofs), FchmodError::Erofs); + assert_eq!( + FchmodError::from_errno(Errno::Eacces), + FchmodError::Other(Errno::Eacces) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 88b772c2..7dbd3de4 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -34,6 +34,7 @@ mod errno; mod execve; mod exit; mod fchdir; +mod fchmod; mod helpers; pub mod sys; @@ -59,6 +60,7 @@ pub use errno::Errno; pub use execve::{ExecveError, execve}; pub use exit::exit; pub use fchdir::{FchdirError, fchdir}; +pub use fchmod::{FchmodError, fchmod}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From b6520b2adf01275d4cc8be4a12a90b1791f19fe5 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:39:06 +0200 Subject: [PATCH 025/135] feat(syscalls): wrap fchown16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fchown.rs | 148 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fchown.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index de6ef920..d56f4e97 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -118,7 +118,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `exit` - [x] `fchdir` - [x] `fchmod` -- [ ] `fchown16` - x86 +- [x] `fchown16` - x86 - [ ] `fcntl` - [ ] `fork` - x86 - [ ] `fstatfs` diff --git a/system/linux/syscalls/src/fchown.rs b/system/linux/syscalls/src/fchown.rs new file mode 100644 index 00000000..c55fcaed --- /dev/null +++ b/system/linux/syscalls/src/fchown.rs @@ -0,0 +1,148 @@ +use celer_system_linux_ctypes::{OldGidT, OldUidT, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`fchown16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Fchown16Error { + /// `EBADF`. + Ebadf, + /// `ENOENT`. + Enoent, + /// `EPERM`. + Eperm, + /// `EROFS`. + Erofs, + /// Another errno returned by delegated filesystem, security, or remote + /// ownership-change work. + Other(Errno), +} + +impl Fchown16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Enoent => Self::Enoent, + Errno::Eperm => Self::Eperm, + Errno::Erofs => Self::Erofs, + errno => Self::Other(errno), + } + } +} + +/// Change the owner and/or group of an open file descriptor through the +/// legacy i386 `fchown16` ABI. +/// +/// This safe wrapper keeps the kernel-facing integer and legacy 16-bit +/// owner/group arguments, but maps the raw syscall return value into +/// `Result<(), Fchown16Error>`. +/// +/// Pass `OldUidT::MAX` and/or `OldGidT::MAX` to preserve the existing owner +/// or group respectively. On success, returns `Ok(())` after the kernel has +/// applied the ownership change request. +/// +/// See [`sys::fchown16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Fchown16Error::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`Fchown16Error::Enoent`]: the historical Linux 1.0 file-table entry had +/// no inode attached. +/// - [`Fchown16Error::Eperm`]: the caller was not allowed to make the +/// ownership change. +/// - [`Fchown16Error::Erofs`]: the target inode lives on a read-only +/// filesystem. +/// - [`Fchown16Error::Other`]: delegated filesystem, security, or remote +/// ownership-change error. +pub fn fchown16( + fd: UnsignedInt, + user: OldUidT, + group: OldGidT, +) -> Result<(), Fchown16Error> { + let ret = sys::fchown16(fd, user, group); + + unit_from_ret(ret as isize, Fchown16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{OldGidT, OldUidT, UnsignedInt}; + + use crate::Errno; + + use super::{Fchown16Error, fchown16}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_fchown16_{now}")); + path + } + + #[test] + fn test_fchown16_ok_with_no_change_sentinels() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + assert_eq!( + fchown16( + file.as_raw_fd() as UnsignedInt, + OldUidT::MAX, + OldGidT::MAX, + ), + Ok(()) + ); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fchown16_ebadf() { + assert_eq!( + fchown16(UnsignedInt::MAX, OldUidT::MAX, OldGidT::MAX), + Err(Fchown16Error::Ebadf) + ); + } + + #[test] + fn test_fchown16_error_mapping() { + assert_eq!( + Fchown16Error::from_errno(Errno::Ebadf), + Fchown16Error::Ebadf + ); + assert_eq!( + Fchown16Error::from_errno(Errno::Enoent), + Fchown16Error::Enoent + ); + assert_eq!( + Fchown16Error::from_errno(Errno::Eperm), + Fchown16Error::Eperm + ); + assert_eq!( + Fchown16Error::from_errno(Errno::Erofs), + Fchown16Error::Erofs + ); + assert_eq!( + Fchown16Error::from_errno(Errno::Einval), + Fchown16Error::Other(Errno::Einval) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 7dbd3de4..6f5c58c9 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -35,6 +35,8 @@ mod execve; mod exit; mod fchdir; mod fchmod; +#[cfg(target_arch = "x86")] +mod fchown; mod helpers; pub mod sys; @@ -61,6 +63,8 @@ pub use execve::{ExecveError, execve}; pub use exit::exit; pub use fchdir::{FchdirError, fchdir}; pub use fchmod::{FchmodError, fchmod}; +#[cfg(target_arch = "x86")] +pub use fchown::{Fchown16Error, fchown16}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 2a7e74b46ea4d551b1f270b7a6f84a6e3a561917 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:40:53 +0200 Subject: [PATCH 026/135] feat(syscalls): add wrapped fcntl API --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fcntl.rs | 172 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fcntl.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index d56f4e97..51f7c4e9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -119,7 +119,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fchdir` - [x] `fchmod` - [x] `fchown16` - x86 -- [ ] `fcntl` +- [x] `fcntl` - [ ] `fork` - x86 - [ ] `fstatfs` - [ ] `fsync` diff --git a/system/linux/syscalls/src/fcntl.rs b/system/linux/syscalls/src/fcntl.rs new file mode 100644 index 00000000..cc8d8366 --- /dev/null +++ b/system/linux/syscalls/src/fcntl.rs @@ -0,0 +1,172 @@ +use celer_system_linux_ctypes::{Int, Long}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`fcntl`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FcntlError { + /// `EBADF`. + Ebadf, + /// `EFAULT`. + Efault, + /// `EINVAL`. + Einval, + /// `EMFILE`. + Emfile, + /// `EPERM`. + Eperm, + /// Another errno returned by delegated security-hook or file-specific + /// command handling. + Other(Errno), +} + +impl FcntlError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Emfile => Self::Emfile, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Manipulate an open file descriptor through `fcntl(2)`. +/// +/// This wrapper keeps the raw `cmd` and `arg` integers, but maps the raw +/// syscall return value into `Result`. +/// +/// On success, returns the syscall's nonnegative result. Depending on `cmd`, +/// that may be `0`, a duplicated file descriptor, or another command-specific +/// scalar value. +/// +/// See [`sys::fcntl`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// - Some `cmd` values cause the kernel to treat `arg` as a userspace pointer +/// and copy to or from that address. The caller must uphold the +/// command-specific pointer validity requirements in those cases. +/// +/// # Errors +/// - [`FcntlError::Ebadf`]: `fd` is not open, or this command is rejected for +/// an `O_PATH` descriptor on the current kernel path. +/// - [`FcntlError::Efault`]: the kernel could not copy a command-specific user +/// buffer. +/// - [`FcntlError::Einval`]: `cmd` was not recognized, or a command-specific +/// integer argument was invalid. +/// - [`FcntlError::Emfile`]: a duplication command could not allocate a new +/// descriptor. +/// - [`FcntlError::Eperm`]: the requested operation was not allowed. +/// - [`FcntlError::Other`]: another security-hook or file-specific errno. +pub unsafe fn fcntl(fd: Int, cmd: Int, arg: Long) -> Result { + // SAFETY: the caller must uphold any command-specific pointer preconditions + // that apply when `arg` names userspace memory. + let ret = unsafe { sys::fcntl(fd as _, cmd, arg) }; + + result_from_ret(ret as isize, |ret| ret as Long, FcntlError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::IntoRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{Int, Long}; + + use crate::{Errno, close}; + + use super::{FcntlError, fcntl}; + + const F_DUPFD: Int = 0; + const F_GETFD: Int = 1; + const F_SETFD: Int = 2; + const FD_CLOEXEC: Int = 1; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_fcntl_{now}")); + path + } + + #[test] + fn test_fcntl_get_and_set_fd_flags() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + + // SAFETY: `F_GETFD` treats `arg` as a scalar. + let original_flags = unsafe { fcntl(fd, F_GETFD, 0) }.unwrap(); + // SAFETY: `F_SETFD` treats `arg` as a scalar bitmask. + assert_eq!( + unsafe { fcntl(fd, F_SETFD, original_flags | FD_CLOEXEC as Long) }, + Ok(0) + ); + // SAFETY: `F_GETFD` treats `arg` as a scalar. + let updated_flags = unsafe { fcntl(fd, F_GETFD, 0) }.unwrap(); + assert_eq!(updated_flags & FD_CLOEXEC as Long, FD_CLOEXEC as Long); + + assert_eq!(close(fd), Ok(())); + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fcntl_dupfd_returns_new_descriptor() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + // SAFETY: `F_DUPFD` treats `arg` as the minimum new descriptor number. + let dup_fd = + unsafe { fcntl(fd, F_DUPFD, 0) }.expect("F_DUPFD should succeed"); + assert_ne!(dup_fd, fd as Long); + + assert_eq!(close(dup_fd as Int), Ok(())); + assert_eq!(close(fd), Ok(())); + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fcntl_ebadf() { + // SAFETY: `F_GETFD` treats `arg` as a scalar. + assert_eq!(unsafe { fcntl(-1, F_GETFD, 0) }, Err(FcntlError::Ebadf)); + } + + #[test] + fn test_fcntl_error_mapping() { + assert_eq!(FcntlError::from_errno(Errno::Ebadf), FcntlError::Ebadf); + assert_eq!(FcntlError::from_errno(Errno::Efault), FcntlError::Efault); + assert_eq!(FcntlError::from_errno(Errno::Einval), FcntlError::Einval); + assert_eq!(FcntlError::from_errno(Errno::Emfile), FcntlError::Emfile); + assert_eq!(FcntlError::from_errno(Errno::Eperm), FcntlError::Eperm); + assert_eq!( + FcntlError::from_errno(Errno::Enoent), + FcntlError::Other(Errno::Enoent) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 6f5c58c9..663be119 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -37,6 +37,7 @@ mod fchdir; mod fchmod; #[cfg(target_arch = "x86")] mod fchown; +mod fcntl; mod helpers; pub mod sys; @@ -65,6 +66,7 @@ pub use fchdir::{FchdirError, fchdir}; pub use fchmod::{FchmodError, fchmod}; #[cfg(target_arch = "x86")] pub use fchown::{Fchown16Error, fchown16}; +pub use fcntl::{FcntlError, fcntl}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From d9c5c2e068d555bab52245cd525e306b02f9cf1d Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:40:55 +0200 Subject: [PATCH 027/135] feat(syscalls): add wrapped fork API --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fork.rs | 91 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/sys/fork.rs | 2 + 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fork.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 51f7c4e9..179c78de 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -120,7 +120,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fchmod` - [x] `fchown16` - x86 - [x] `fcntl` -- [ ] `fork` - x86 +- [x] `fork` - x86 - [ ] `fstatfs` - [ ] `fsync` - [ ] `ftruncate` diff --git a/system/linux/syscalls/src/fork.rs b/system/linux/syscalls/src/fork.rs new file mode 100644 index 00000000..eac9bc1c --- /dev/null +++ b/system/linux/syscalls/src/fork.rs @@ -0,0 +1,91 @@ +use celer_system_linux_ctypes::PidT; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`fork`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ForkError { + /// `EAGAIN`. + Eagain, + /// `EINTR`. + Eintr, + /// `ENOMEM`. + Enomem, + /// Another errno returned by delegated security, cgroup, or namespace work. + Other(Errno), +} + +impl ForkError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eagain => Self::Eagain, + Errno::Eintr => Self::Eintr, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Create a child process. +/// +/// This safe wrapper maps the raw `fork(2)` return value into +/// `Result`. +/// +/// On success, returns `Ok(0)` in the child process and `Ok(pid)` in the +/// parent process. +/// +/// See [`sys::fork`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ForkError::Eagain`]: the kernel refused to create another task because +/// process or thread limits were hit. +/// - [`ForkError::Eintr`]: a fatal signal interrupted `fork` after the child +/// task had been prepared but before it became visible. +/// - [`ForkError::Enomem`]: the kernel could not allocate the child task or +/// related resources. +/// - [`ForkError::Other`]: another delegated security, cgroup, or namespace +/// error. +pub fn fork() -> Result { + let ret = sys::fork(); + + result_from_ret(ret as isize, |ret| ret as PidT, ForkError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::Int; + + use super::{ForkError, fork}; + use crate::{Errno, exit}; + + #[test] + fn test_fork_ok() { + let pid = fork().expect("fork should succeed"); + + if pid == 0 { + exit(0); + } + + let mut status: Int = 0; + let waited = unsafe { libc::waitpid(pid, &mut status, 0) }; + + assert_eq!(waited, pid); + assert_eq!(status & 0x7f, 0); + assert_eq!((status >> 8) & 0xff, 0); + } + + #[test] + fn test_fork_error_mapping() { + assert_eq!(ForkError::from_errno(Errno::Eagain), ForkError::Eagain); + assert_eq!(ForkError::from_errno(Errno::Eintr), ForkError::Eintr); + assert_eq!(ForkError::from_errno(Errno::Enomem), ForkError::Enomem); + assert_eq!( + ForkError::from_errno(Errno::Eperm), + ForkError::Other(Errno::Eperm) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 663be119..e09d948a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -38,6 +38,8 @@ mod fchmod; #[cfg(target_arch = "x86")] mod fchown; mod fcntl; +#[cfg(target_arch = "x86")] +mod fork; mod helpers; pub mod sys; @@ -67,6 +69,8 @@ pub use fchmod::{FchmodError, fchmod}; #[cfg(target_arch = "x86")] pub use fchown::{Fchown16Error, fchown16}; pub use fcntl::{FcntlError, fcntl}; +#[cfg(target_arch = "x86")] +pub use fork::{ForkError, fork}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sys/fork.rs b/system/linux/syscalls/src/sys/fork.rs index 2676e84f..c7c85864 100644 --- a/system/linux/syscalls/src/sys/fork.rs +++ b/system/linux/syscalls/src/sys/fork.rs @@ -26,6 +26,8 @@ use crate::arch::current::{Sysno, syscall0}; /// kernel pages for the child; current kernels also use it when process or /// thread limits block `copy_process()`. /// - `ENOMEM`: current kernels failed to allocate child task resources. +/// - `EINTR`: current kernels detected a pending fatal signal after preparing +/// the child task but before making it visible. /// - `EINVAL`: current NOMMU kernels reject `fork`. /// /// # References From d366f67d7a70e09c56c44ad53e0dd9d587038de0 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:41:17 +0200 Subject: [PATCH 028/135] feat(syscalls): add wrapped fstatfs --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/errno.rs | 3 + system/linux/syscalls/src/fstatfs.rs | 175 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 3 + 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fstatfs.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 179c78de..c3c33551 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -121,7 +121,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fchown16` - x86 - [x] `fcntl` - [x] `fork` - x86 -- [ ] `fstatfs` +- [x] `fstatfs` - [ ] `fsync` - [ ] `ftruncate` - [ ] `get_kernel_syms` - x86 diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 735647fc..8014b260 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -18,6 +18,7 @@ pub enum Errno { Enomem, Enosys, Enotdir, + Eoverflow, Eperm, Erofs, Ewouldblock, @@ -45,6 +46,7 @@ impl Errno { 12 => Self::Enomem, 38 => Self::Enosys, 20 => Self::Enotdir, + 75 => Self::Eoverflow, 1 => Self::Eperm, 30 => Self::Erofs, other => Self::Raw(other), @@ -90,6 +92,7 @@ mod tests { assert_eq!(Errno::from_raw(12), Errno::Enomem); assert_eq!(Errno::from_raw(38), Errno::Enosys); assert_eq!(Errno::from_raw(20), Errno::Enotdir); + assert_eq!(Errno::from_raw(75), Errno::Eoverflow); assert_eq!(Errno::from_raw(1), Errno::Eperm); assert_eq!(Errno::from_raw(30), Errno::Erofs); assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); diff --git a/system/linux/syscalls/src/fstatfs.rs b/system/linux/syscalls/src/fstatfs.rs new file mode 100644 index 00000000..1ce4cb70 --- /dev/null +++ b/system/linux/syscalls/src/fstatfs.rs @@ -0,0 +1,175 @@ +use core::mem::MaybeUninit; + +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::Statfs as Linux10Statfs; +use celer_system_linux_ctypes::{Statfs, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`fstatfs`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FstatfsError { + /// `EFAULT`. + Efault, + /// `EBADF`. + Ebadf, + /// `ENOENT` from the Linux 1.0 x86 ABI when `fd` has no inode. + Enoent, + /// `ENOSYS`. + Enosys, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated filesystem-specific `statfs` work. + Other(Errno), +} + +impl FstatfsError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Ebadf => Self::Ebadf, + Errno::Enoent => Self::Enoent, + Errno::Enosys => Self::Enosys, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Return filesystem status information for an open file descriptor. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), FstatfsError>`. +/// +/// On success, the kernel has initialized `buf` with the filesystem status for +/// the open file referenced by `fd`. +/// +/// See [`sys::fstatfs`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`FstatfsError::Efault`]: the kernel could not write the output +/// structure. +/// - [`FstatfsError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FstatfsError::Enosys`]: the backing filesystem does not implement a +/// `statfs` hook. +/// - [`FstatfsError::Eoverflow`]: the current ABI could not represent the +/// filesystem statistics. +/// - [`FstatfsError::Other`]: delegated filesystem-specific `statfs` error. +pub fn fstatfs( + fd: UnsignedInt, + buf: &mut MaybeUninit, +) -> Result<(), FstatfsError> { + // SAFETY: `MaybeUninit` provides a valid, writable output buffer + // for one `Statfs`. + let ret = unsafe { sys::fstatfs(fd, buf.as_mut_ptr()) }; + + result_from_ret(ret as isize, |_| (), FstatfsError::from_errno) +} + +/// Return filesystem status through the Linux 1.0 `sys_fstatfs` ABI. +/// +/// This safe wrapper mirrors [`fstatfs`], but uses +/// `&mut MaybeUninit` for the Linux 1.0 layout exposed from +/// [`crate::linux_1_0`]. +/// +/// See [`sys::linux_1_0::fstatfs`] for historical kernel behavior and source +/// references. +/// +/// # Errors +/// - [`FstatfsError::Efault`]: the kernel could not write the output +/// structure. +/// - [`FstatfsError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FstatfsError::Enoent`]: Linux 1.0 found no inode for the open file. +/// - [`FstatfsError::Enosys`]: the backing filesystem does not implement a +/// `statfs` hook. +#[cfg(target_arch = "x86")] +pub fn fstatfs_1_0( + fd: UnsignedInt, + buf: &mut MaybeUninit, +) -> Result<(), FstatfsError> { + // SAFETY: `MaybeUninit` provides a valid, writable output + // buffer for one Linux 1.0 `struct statfs`. + let ret = unsafe { sys::linux_1_0::fstatfs(fd, buf.as_mut_ptr()) }; + + result_from_ret(ret as isize, |_| (), FstatfsError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{fs::File, mem::MaybeUninit, os::fd::AsRawFd as _}; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::fstatfs_1_0; + use super::{FstatfsError, fstatfs}; + + #[test] + fn test_fstatfs_ok() { + let file = File::open("/").unwrap(); + let fd = file.as_raw_fd() as u32; + let mut statfs = MaybeUninit::uninit(); + + fstatfs(fd, &mut statfs).expect("fstatfs should succeed for /"); + let statfs = unsafe { statfs.assume_init() }; + + assert!( + statfs.f_bsize > 0, + "expected positive block size: {statfs:?}" + ); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_linux_1_0_fstatfs_ok() { + let file = File::open("/").unwrap(); + let fd = file.as_raw_fd() as u32; + let mut statfs = MaybeUninit::uninit(); + + fstatfs_1_0(fd, &mut statfs) + .expect("linux_1_0::fstatfs should succeed for /"); + let statfs = unsafe { statfs.assume_init() }; + + assert!( + statfs.f_bsize > 0, + "expected positive block size from Linux 1.0 layout: {statfs:?}" + ); + } + + #[test] + fn test_fstatfs_invalid_fd() { + let mut statfs = MaybeUninit::uninit(); + + assert_eq!(fstatfs(u32::MAX, &mut statfs), Err(FstatfsError::Ebadf)); + } + + #[test] + fn test_fstatfs_error_mapping() { + assert_eq!( + FstatfsError::from_errno(Errno::Efault), + FstatfsError::Efault + ); + assert_eq!(FstatfsError::from_errno(Errno::Ebadf), FstatfsError::Ebadf); + assert_eq!( + FstatfsError::from_errno(Errno::Enoent), + FstatfsError::Enoent + ); + assert_eq!( + FstatfsError::from_errno(Errno::Enosys), + FstatfsError::Enosys + ); + assert_eq!( + FstatfsError::from_errno(Errno::Eoverflow), + FstatfsError::Eoverflow + ); + assert_eq!( + FstatfsError::from_errno(Errno::Enomem), + FstatfsError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e09d948a..59ac46d0 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -40,6 +40,7 @@ mod fchown; mod fcntl; #[cfg(target_arch = "x86")] mod fork; +mod fstatfs; mod helpers; pub mod sys; @@ -71,9 +72,11 @@ pub use fchown::{Fchown16Error, fchown16}; pub use fcntl::{FcntlError, fcntl}; #[cfg(target_arch = "x86")] pub use fork::{ForkError, fork}; +pub use fstatfs::{FstatfsError, fstatfs}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { pub use super::create_module::{CreateModuleError, create_module}; + pub use super::fstatfs::{FstatfsError, fstatfs_1_0 as fstatfs}; } From 3769db5f861a96553c0abb56b34713216db76c99 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:41:05 +0200 Subject: [PATCH 029/135] feat(syscalls): add fsync wrapper --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/errno.rs | 3 + system/linux/syscalls/src/fsync.rs | 132 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fsync.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c3c33551..fdfa8b04 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -122,7 +122,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fcntl` - [x] `fork` - x86 - [x] `fstatfs` -- [ ] `fsync` +- [x] `fsync` - [ ] `ftruncate` - [ ] `get_kernel_syms` - x86 - [ ] `getegid16` - x86 diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 8014b260..e7a95c75 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -11,6 +11,7 @@ pub enum Errno { Eexist, Einval, Eintr, + Eio, Eloop, Enodev, Enoent, @@ -39,6 +40,7 @@ impl Errno { 17 => Self::Eexist, 22 => Self::Einval, 4 => Self::Eintr, + 5 => Self::Eio, 40 => Self::Eloop, 19 => Self::Enodev, 2 => Self::Enoent, @@ -85,6 +87,7 @@ mod tests { assert_eq!(Errno::from_raw(17), Errno::Eexist); assert_eq!(Errno::from_raw(22), Errno::Einval); assert_eq!(Errno::from_raw(4), Errno::Eintr); + assert_eq!(Errno::from_raw(5), Errno::Eio); assert_eq!(Errno::from_raw(40), Errno::Eloop); assert_eq!(Errno::from_raw(19), Errno::Enodev); assert_eq!(Errno::from_raw(2), Errno::Enoent); diff --git a/system/linux/syscalls/src/fsync.rs b/system/linux/syscalls/src/fsync.rs new file mode 100644 index 00000000..6931131b --- /dev/null +++ b/system/linux/syscalls/src/fsync.rs @@ -0,0 +1,132 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`fsync`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FsyncError { + /// `EBADF`. + Ebadf, + /// `EINVAL`. + Einval, + /// `EIO`. + Eio, + /// Another errno returned by the underlying filesystem-specific sync work. + Other(Errno), +} + +impl FsyncError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Einval => Self::Einval, + Errno::Eio => Self::Eio, + errno => Self::Other(errno), + } + } +} + +/// Flush pending file data and metadata for the open file referenced by `fd`. +/// +/// This safe wrapper maps the raw `fsync(2)` return value into +/// `Result<(), FsyncError>` while keeping the file-descriptor argument as the +/// kernel-facing integer type. +/// +/// On success, returns `Ok(())` after the kernel has completed the requested +/// sync for `fd`. +/// +/// See [`sys::fsync`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`FsyncError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FsyncError::Einval`]: `fd` refers to an object that this syscall cannot +/// sync, such as a pipe on current kernels. +/// - [`FsyncError::Eio`]: Linux 1.0 mapped a nonzero filesystem `fsync` +/// callback result to this error. +/// - [`FsyncError::Other`]: another filesystem- or object-specific sync error. +pub fn fsync(fd: Int) -> Result<(), FsyncError> { + let ret = sys::fsync(fd); + + unit_from_ret(ret as isize, FsyncError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + io::Write as _, + os::fd::AsRawFd as _, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::Int; + + use crate::Errno; + + use super::{FsyncError, fsync}; + + fn create_temp_path() -> PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + path.push(format!("celer_wrap_fsync_{now}")); + + path + } + + #[test] + fn test_fsync_ok() { + let path = create_temp_path(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + file.write_all(b"hello wrapped fsync").unwrap(); + + assert_eq!(fsync(file.as_raw_fd() as Int), Ok(())); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_fsync_ebadf() { + assert_eq!(fsync(-1), Err(FsyncError::Ebadf)); + } + + #[test] + fn test_fsync_einval_for_pipe() { + let mut fds = [0 as Int; 2]; + + // SAFETY: `fds` is writable for two `Int` values. + let rc = unsafe { crate::sys::test_support::pipe(fds.as_mut_ptr()) }; + assert_eq!(rc, 0, "pipe failed: {rc}"); + + assert_eq!(fsync(fds[0]), Err(FsyncError::Einval)); + + assert_eq!(crate::sys::close(fds[0]), 0); + assert_eq!(crate::sys::close(fds[1]), 0); + } + + #[test] + fn test_fsync_error_mapping() { + assert_eq!(FsyncError::from_errno(Errno::Ebadf), FsyncError::Ebadf); + assert_eq!(FsyncError::from_errno(Errno::Einval), FsyncError::Einval); + assert_eq!(FsyncError::from_errno(Errno::Eio), FsyncError::Eio); + assert_eq!( + FsyncError::from_errno(Errno::Enomem), + FsyncError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 59ac46d0..da868b5a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -41,6 +41,7 @@ mod fcntl; #[cfg(target_arch = "x86")] mod fork; mod fstatfs; +mod fsync; mod helpers; pub mod sys; @@ -73,6 +74,7 @@ pub use fcntl::{FcntlError, fcntl}; #[cfg(target_arch = "x86")] pub use fork::{ForkError, fork}; pub use fstatfs::{FstatfsError, fstatfs}; +pub use fsync::{FsyncError, fsync}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 1677d37dc4d953afd78146d94da3a5a122ec3d82 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Fri, 24 Apr 2026 21:42:41 +0200 Subject: [PATCH 030/135] feat(syscalls): wrap ftruncate --- system/linux/syscalls/WRAP_PROGRESS.md | 6 +- system/linux/syscalls/src/ftruncate.rs | 255 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 5 + 3 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 system/linux/syscalls/src/ftruncate.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index fdfa8b04..1b801008 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -123,7 +123,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fork` - x86 - [x] `fstatfs` - [x] `fsync` -- [ ] `ftruncate` +- [x] `ftruncate` - [ ] `get_kernel_syms` - x86 - [ ] `getegid16` - x86 - [ ] `geteuid16` - x86 @@ -226,8 +226,8 @@ Shared conversion helpers should live in a private `helpers` module. These raw wrappers are currently exposed under `sys::linux_1_0` on x86. Decide whether the wrap layer mirrors them before checking them off. -- [ ] `linux_1_0::fstatfs` -- [ ] `linux_1_0::ftruncate` +- [x] `linux_1_0::fstatfs` +- [x] `linux_1_0::ftruncate` - [ ] `linux_1_0::init_module` - [ ] `linux_1_0::newfstat` - [ ] `linux_1_0::newlstat` diff --git a/system/linux/syscalls/src/ftruncate.rs b/system/linux/syscalls/src/ftruncate.rs new file mode 100644 index 00000000..ca8a86d3 --- /dev/null +++ b/system/linux/syscalls/src/ftruncate.rs @@ -0,0 +1,255 @@ +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::UnsignedInt; +use celer_system_linux_ctypes::{Int, OffT}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`ftruncate`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FtruncateError { + /// `EBADF`. + Ebadf, + /// `EINVAL`. + Einval, + /// `EPERM`. + Eperm, + /// Another errno returned by delegated security, notification, or + /// filesystem truncate work. + Other(Errno), +} + +impl FtruncateError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Truncate an open file descriptor to `length` bytes. +/// +/// This safe wrapper maps the raw `ftruncate(2)` return value into +/// `Result<(), FtruncateError>` while keeping the length argument in the +/// kernel-facing `off_t`-shaped type. +/// +/// On success, returns `Ok(())` after the kernel updates the file size. +/// +/// See [`sys::ftruncate`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`FtruncateError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`FtruncateError::Einval`]: `length` is negative, the file is not a +/// regular file, the descriptor is not open for writing, or the request +/// exceeds the legacy non-large-file limit for this entrypoint. +/// - [`FtruncateError::Eperm`]: the inode is append-only. +/// - [`FtruncateError::Other`]: delegated security, notification, or +/// filesystem truncate error. +pub fn ftruncate(fd: Int, length: OffT) -> Result<(), FtruncateError> { + let ret = sys::ftruncate(fd as _, length); + + unit_from_ret(ret as isize, FtruncateError::from_errno) +} + +/// Errors returned by [`crate::linux_1_0::ftruncate`]. +#[cfg(target_arch = "x86")] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Ftruncate1_0Error { + /// `EBADF`. + Ebadf, + /// `ENOENT`. + Enoent, + /// `EACCES`. + Eacces, + /// Another errno returned by delegated Linux 1.0 truncate work. + Other(Errno), +} + +#[cfg(target_arch = "x86")] +impl Ftruncate1_0Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Enoent => Self::Enoent, + Errno::Eacces => Self::Eacces, + errno => Self::Other(errno), + } + } +} + +/// Truncate an open file descriptor through the Linux 1.0 unsigned +/// `ftruncate` ABI. +/// +/// This safe wrapper mirrors [`sys::linux_1_0::ftruncate`] and maps the raw +/// return value into `Result<(), Ftruncate1_0Error>`. +/// +/// On success, returns `Ok(())` after the Linux 1.0 kernel updates the inode +/// size. +/// +/// See [`sys::linux_1_0::ftruncate`] for kernel behavior, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`Ftruncate1_0Error::Ebadf`]: `fd` is outside the open-file table or +/// does not name an open file descriptor. +/// - [`Ftruncate1_0Error::Enoent`]: the file table entry has no inode +/// attached. +/// - [`Ftruncate1_0Error::Eacces`]: the descriptor is not open for writing, +/// or the target inode is a directory. +/// - [`Ftruncate1_0Error::Other`]: delegated Linux 1.0 truncate error. +#[cfg(target_arch = "x86")] +pub fn ftruncate_1_0( + fd: Int, + length: UnsignedInt, +) -> Result<(), Ftruncate1_0Error> { + let ret = sys::linux_1_0::ftruncate(fd as _, length); + + unit_from_ret(ret as isize, Ftruncate1_0Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + io::Write as _, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::OffT; + #[cfg(target_arch = "x86")] + use celer_system_linux_ctypes::UnsignedInt; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::{Ftruncate1_0Error, ftruncate_1_0}; + use super::{FtruncateError, ftruncate}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_ftruncate_{now}")); + path + } + + #[test] + fn test_ftruncate_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + file.write_all(b"truncate-me").unwrap(); + + assert_eq!(ftruncate(file.as_raw_fd(), 4 as OffT), Ok(())); + assert_eq!(fs::metadata(&path).unwrap().len(), 4); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_ftruncate_ebadf() { + assert_eq!(ftruncate(-1, 0 as OffT), Err(FtruncateError::Ebadf)); + } + + #[test] + fn test_ftruncate_einval_for_negative_length() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + assert_eq!( + ftruncate(file.as_raw_fd(), -1 as OffT), + Err(FtruncateError::Einval) + ); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_ftruncate_error_mapping() { + assert_eq!( + FtruncateError::from_errno(Errno::Ebadf), + FtruncateError::Ebadf + ); + assert_eq!( + FtruncateError::from_errno(Errno::Einval), + FtruncateError::Einval + ); + assert_eq!( + FtruncateError::from_errno(Errno::Eperm), + FtruncateError::Eperm + ); + assert_eq!( + FtruncateError::from_errno(Errno::Enoent), + FtruncateError::Other(Errno::Enoent) + ); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_ftruncate_1_0_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + file.write_all(b"truncate-me").unwrap(); + + assert_eq!(ftruncate_1_0(file.as_raw_fd(), 3 as UnsignedInt), Ok(())); + assert_eq!(fs::metadata(&path).unwrap().len(), 3); + + fs::remove_file(&path).unwrap(); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_ftruncate_1_0_ebadf() { + assert_eq!( + ftruncate_1_0(-1, 0 as UnsignedInt), + Err(Ftruncate1_0Error::Ebadf) + ); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_ftruncate_1_0_error_mapping() { + assert_eq!( + Ftruncate1_0Error::from_errno(Errno::Ebadf), + Ftruncate1_0Error::Ebadf + ); + assert_eq!( + Ftruncate1_0Error::from_errno(Errno::Enoent), + Ftruncate1_0Error::Enoent + ); + assert_eq!( + Ftruncate1_0Error::from_errno(Errno::Eacces), + Ftruncate1_0Error::Eacces + ); + assert_eq!( + Ftruncate1_0Error::from_errno(Errno::Einval), + Ftruncate1_0Error::Other(Errno::Einval) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index da868b5a..8726e203 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -42,6 +42,7 @@ mod fcntl; mod fork; mod fstatfs; mod fsync; +mod ftruncate; mod helpers; pub mod sys; @@ -75,10 +76,14 @@ pub use fcntl::{FcntlError, fcntl}; pub use fork::{ForkError, fork}; pub use fstatfs::{FstatfsError, fstatfs}; pub use fsync::{FsyncError, fsync}; +pub use ftruncate::{FtruncateError, ftruncate}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { pub use super::create_module::{CreateModuleError, create_module}; pub use super::fstatfs::{FstatfsError, fstatfs_1_0 as fstatfs}; + pub use super::ftruncate::{ + Ftruncate1_0Error as FtruncateError, ftruncate_1_0 as ftruncate, + }; } From e2212321bc2055e740af02b9fd3bdf570c7295fb Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 09:48:27 +0200 Subject: [PATCH 031/135] Wrap x86 get_kernel_syms --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/get_kernel_syms.rs | 135 +++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/get_kernel_syms.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 1b801008..9519f943 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -124,7 +124,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fstatfs` - [x] `fsync` - [x] `ftruncate` -- [ ] `get_kernel_syms` - x86 +- [x] `get_kernel_syms` - x86 - [ ] `getegid16` - x86 - [ ] `geteuid16` - x86 - [ ] `getgid16` - x86 diff --git a/system/linux/syscalls/src/get_kernel_syms.rs b/system/linux/syscalls/src/get_kernel_syms.rs new file mode 100644 index 00000000..ab2e6cd0 --- /dev/null +++ b/system/linux/syscalls/src/get_kernel_syms.rs @@ -0,0 +1,135 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, KernelSym}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`get_kernel_syms`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetKernelSymsError { + /// `EFAULT`. + Efault, + /// `ENOSYS`. + Enosys, + /// Another errno returned by a historical kernel implementation. + Other(Errno), +} + +impl GetKernelSymsError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +fn table_ptr(table: Option<&mut [MaybeUninit]>) -> *mut KernelSym { + match table { + Some(table) => table.as_mut_ptr().cast::(), + None => core::ptr::null_mut(), + } +} + +fn get_kernel_syms_from_ret(ret: Int) -> Result { + result_from_ret( + ret as isize, + |ret| ret as usize, + GetKernelSymsError::from_errno, + ) +} + +/// Copy the historical Linux 1.0 kernel symbol table into `table`, or query +/// the symbol count by passing `None`. +/// +/// This wrapper keeps the raw kernel ABI shape but replaces the output pointer +/// with an optional mutable slice of `MaybeUninit`. +/// +/// The returned `usize` is the kernel's symbol count. +/// +/// See [`sys::get_kernel_syms`] for kernel behavior, reachable errors, and +/// source references. +/// +/// # Safety +/// - If `table` is `Some`, it must provide writable space for at least the +/// kernel's symbol count. +/// +/// # Errors +/// - [`GetKernelSymsError::Efault`]: the kernel could not access the output +/// table. +/// - [`GetKernelSymsError::Enosys`]: the syscall slot is unimplemented on the +/// running kernel. +/// - [`GetKernelSymsError::Other`]: another historical kernel errno. +pub unsafe fn get_kernel_syms( + table: Option<&mut [MaybeUninit]>, +) -> Result { + let ptr = table_ptr(table); + + // SAFETY: the caller upholds any buffer-capacity precondition when `table` + // is provided. + let ret = unsafe { sys::get_kernel_syms(ptr) }; + get_kernel_syms_from_ret(ret) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::KernelSym; + + use crate::{errno::Errno, sys}; + + use super::{ + GetKernelSymsError, get_kernel_syms, get_kernel_syms_from_ret, + table_ptr, + }; + + #[test] + fn test_get_kernel_syms_null_matches_raw() { + let wrapped = unsafe { get_kernel_syms(None) }; + let raw = unsafe { sys::get_kernel_syms(core::ptr::null_mut()) }; + + match raw { + r if r >= 0 => assert_eq!(wrapped, Ok(r as usize)), + r => assert_eq!( + wrapped, + Err(GetKernelSymsError::from_errno( + Errno::from_kernel_ret(r as isize).unwrap(), + )) + ), + } + } + + #[test] + fn test_get_kernel_syms_error_mapping() { + assert_eq!( + GetKernelSymsError::from_errno(Errno::Efault), + GetKernelSymsError::Efault + ); + assert_eq!( + GetKernelSymsError::from_errno(Errno::Enosys), + GetKernelSymsError::Enosys + ); + assert_eq!( + GetKernelSymsError::from_errno(Errno::Enomem), + GetKernelSymsError::Other(Errno::Enomem) + ); + } + + #[test] + fn test_get_kernel_syms_success_mapping() { + assert_eq!(get_kernel_syms_from_ret(7), Ok(7)); + } + + #[test] + fn test_get_kernel_syms_table_ptr() { + assert!(table_ptr(None).is_null()); + + let mut table = [MaybeUninit::::uninit()]; + assert_eq!(table_ptr(Some(&mut table)), table.as_mut_ptr().cast()); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 8726e203..c05981ba 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -43,6 +43,8 @@ mod fork; mod fstatfs; mod fsync; mod ftruncate; +#[cfg(target_arch = "x86")] +mod get_kernel_syms; mod helpers; pub mod sys; @@ -77,6 +79,8 @@ pub use fork::{ForkError, fork}; pub use fstatfs::{FstatfsError, fstatfs}; pub use fsync::{FsyncError, fsync}; pub use ftruncate::{FtruncateError, ftruncate}; +#[cfg(target_arch = "x86")] +pub use get_kernel_syms::{GetKernelSymsError, get_kernel_syms}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 79f7dd1f6af6db5d815df6d4776a444d2267017c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 09:49:50 +0200 Subject: [PATCH 032/135] Wrap getegid16 syscall --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getegid.rs | 40 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 +++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getegid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 9519f943..d14de1bf 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -125,7 +125,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `fsync` - [x] `ftruncate` - [x] `get_kernel_syms` - x86 -- [ ] `getegid16` - x86 +- [x] `getegid16` - x86 - [ ] `geteuid16` - x86 - [ ] `getgid16` - x86 - [ ] `getgroups16` - x86 diff --git a/system/linux/syscalls/src/getegid.rs b/system/linux/syscalls/src/getegid.rs new file mode 100644 index 00000000..a13bb979 --- /dev/null +++ b/system/linux/syscalls/src/getegid.rs @@ -0,0 +1,40 @@ +use celer_system_linux_ctypes::OldGidT; + +use crate::sys; + +/// Return the caller's effective group ID through the legacy x86 `getegid16` ABI. +/// +/// This wrapper keeps the raw syscall's no-argument shape and returns the +/// legacy 16-bit group ID directly. +/// +/// See [`sys::getegid16`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - Never fails. +pub fn getegid16() -> OldGidT { + sys::getegid16() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::OldGidT; + + use crate::sys; + + use super::getegid16; + + #[test] + fn test_getegid16_matches_raw() { + let wrapped = getegid16(); + let raw = sys::getegid16(); + + assert_eq!(wrapped, raw, "wrapped getegid16 should match raw syscall"); + } + + #[test] + fn test_getegid16_type() { + let _: OldGidT = getegid16(); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c05981ba..b5785285 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -45,6 +45,8 @@ mod fsync; mod ftruncate; #[cfg(target_arch = "x86")] mod get_kernel_syms; +#[cfg(target_arch = "x86")] +mod getegid; mod helpers; pub mod sys; @@ -81,6 +83,8 @@ pub use fsync::{FsyncError, fsync}; pub use ftruncate::{FtruncateError, ftruncate}; #[cfg(target_arch = "x86")] pub use get_kernel_syms::{GetKernelSymsError, get_kernel_syms}; +#[cfg(target_arch = "x86")] +pub use getegid::getegid16; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From ccc1461726dab0f8febf34e03d3e9307a600c244 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 09:50:34 +0200 Subject: [PATCH 033/135] Mark geteuid16 wrap complete --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/geteuid.rs | 40 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 +++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/geteuid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index d14de1bf..2bb869e9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -126,7 +126,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ftruncate` - [x] `get_kernel_syms` - x86 - [x] `getegid16` - x86 -- [ ] `geteuid16` - x86 +- [x] `geteuid16` - x86 - [ ] `getgid16` - x86 - [ ] `getgroups16` - x86 - [ ] `getitimer` diff --git a/system/linux/syscalls/src/geteuid.rs b/system/linux/syscalls/src/geteuid.rs new file mode 100644 index 00000000..db228b98 --- /dev/null +++ b/system/linux/syscalls/src/geteuid.rs @@ -0,0 +1,40 @@ +use celer_system_linux_ctypes::OldUidT; + +use crate::sys; + +/// Return the caller's effective user ID through the legacy x86 `geteuid16` ABI. +/// +/// This wrapper keeps the raw syscall's no-argument shape and returns the +/// legacy 16-bit user ID directly. +/// +/// See [`sys::geteuid16`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - Never fails. +pub fn geteuid16() -> OldUidT { + sys::geteuid16() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::OldUidT; + + use crate::sys; + + use super::geteuid16; + + #[test] + fn test_geteuid16_matches_raw() { + let wrapped = geteuid16(); + let raw = sys::geteuid16(); + + assert_eq!(wrapped, raw, "wrapped geteuid16 should match raw syscall"); + } + + #[test] + fn test_geteuid16_type() { + let _: OldUidT = geteuid16(); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b5785285..648e3fef 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -47,6 +47,8 @@ mod ftruncate; mod get_kernel_syms; #[cfg(target_arch = "x86")] mod getegid; +#[cfg(target_arch = "x86")] +mod geteuid; mod helpers; pub mod sys; @@ -85,6 +87,8 @@ pub use ftruncate::{FtruncateError, ftruncate}; pub use get_kernel_syms::{GetKernelSymsError, get_kernel_syms}; #[cfg(target_arch = "x86")] pub use getegid::getegid16; +#[cfg(target_arch = "x86")] +pub use geteuid::geteuid16; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 96db3ed0eeaed8df890c58f3e638aa6b3777caaa Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 09:51:59 +0200 Subject: [PATCH 034/135] Wrap getgid16 on x86 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getgid.rs | 40 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 +++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getgid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2bb869e9..4655c01a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -127,7 +127,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `get_kernel_syms` - x86 - [x] `getegid16` - x86 - [x] `geteuid16` - x86 -- [ ] `getgid16` - x86 +- [x] `getgid16` - x86 - [ ] `getgroups16` - x86 - [ ] `getitimer` - [ ] `getpgid` diff --git a/system/linux/syscalls/src/getgid.rs b/system/linux/syscalls/src/getgid.rs new file mode 100644 index 00000000..6db772d1 --- /dev/null +++ b/system/linux/syscalls/src/getgid.rs @@ -0,0 +1,40 @@ +use celer_system_linux_ctypes::OldGidT; + +use crate::sys; + +/// Return the caller's real group ID through the legacy x86 `getgid16` ABI. +/// +/// This wrapper keeps the raw syscall's no-argument shape and returns the +/// legacy 16-bit group ID directly. +/// +/// See [`sys::getgid16`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - Never fails. +pub fn getgid16() -> OldGidT { + sys::getgid16() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::OldGidT; + + use crate::sys; + + use super::getgid16; + + #[test] + fn test_getgid16_matches_raw() { + let wrapped = getgid16(); + let raw = sys::getgid16(); + + assert_eq!(wrapped, raw, "wrapped getgid16 should match raw syscall"); + } + + #[test] + fn test_getgid16_type() { + let _: OldGidT = getgid16(); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 648e3fef..1087663a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -49,6 +49,8 @@ mod get_kernel_syms; mod getegid; #[cfg(target_arch = "x86")] mod geteuid; +#[cfg(target_arch = "x86")] +mod getgid; mod helpers; pub mod sys; @@ -89,6 +91,8 @@ pub use get_kernel_syms::{GetKernelSymsError, get_kernel_syms}; pub use getegid::getegid16; #[cfg(target_arch = "x86")] pub use geteuid::geteuid16; +#[cfg(target_arch = "x86")] +pub use getgid::getgid16; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From f4e20c8176acf862b70ac7600d563f70283b28c7 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 09:53:35 +0200 Subject: [PATCH 035/135] Wrap x86 getgroups16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getgroups.rs | 122 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getgroups.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 4655c01a..786f9db9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -128,7 +128,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getegid16` - x86 - [x] `geteuid16` - x86 - [x] `getgid16` - x86 -- [ ] `getgroups16` - x86 +- [x] `getgroups16` - x86 - [ ] `getitimer` - [ ] `getpgid` - [ ] `getpgrp` - x86, x86_64 diff --git a/system/linux/syscalls/src/getgroups.rs b/system/linux/syscalls/src/getgroups.rs new file mode 100644 index 00000000..3140fac4 --- /dev/null +++ b/system/linux/syscalls/src/getgroups.rs @@ -0,0 +1,122 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::OldGidT; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`getgroups16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Getgroups16Error { + Einval, + Efault, + Other(Errno), +} + +impl From for Getgroups16Error { + fn from(value: Errno) -> Self { + match value { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Read the current supplementary group IDs through the legacy x86 +/// `getgroups16` ABI. +/// +/// This wrapper accepts a mutable output slice and passes its length to the raw +/// syscall. Passing an empty slice performs the raw syscall's count query. On +/// success, it returns the supplementary-group count, which is also the number +/// of group IDs written for non-empty slices. +/// +/// See [`sys::getgroups16`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - [`Getgroups16Error::Einval`]: the supplied buffer length exceeds the raw +/// ABI's `int` range, or the supplied buffer is smaller than the current +/// supplementary-group count on current x86 kernels. +/// - [`Getgroups16Error::Efault`]: the kernel could not write the group list to +/// the supplied buffer. +/// - [`Getgroups16Error::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn getgroups16( + grouplist: &mut [MaybeUninit], +) -> Result { + let len = + i32::try_from(grouplist.len()).map_err(|_| Getgroups16Error::Einval)?; + + // SAFETY: `grouplist` provides writable storage for `len` `OldGidT` + // elements, and a zero length is a count query where the kernel does not + // dereference the pointer. + let ret = unsafe { + sys::getgroups16(len, grouplist.as_mut_ptr().cast::()) + }; + result_from_ret(ret as isize, |ret| ret as usize, Getgroups16Error::from) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::OldGidT; + + use crate::sys; + + use super::{Getgroups16Error, getgroups16}; + + #[test] + fn test_getgroups16_matches_raw_for_exact_buffer() { + let count = unsafe { sys::getgroups16(0, core::ptr::null_mut()) }; + assert!(count >= 0, "probe failed: {count}"); + + let mut groups = vec![MaybeUninit::::uninit(); count as usize]; + let wrapped = + getgroups16(&mut groups).expect("wrapped getgroups16 failed"); + let groups = groups + .into_iter() + .map(|group| unsafe { group.assume_init() }) + .collect::>(); + + let mut raw_groups = vec![OldGidT::MAX; count as usize]; + let raw = unsafe { sys::getgroups16(count, raw_groups.as_mut_ptr()) }; + + assert_eq!(wrapped, count as usize); + assert_eq!(raw, count); + assert_eq!(groups, raw_groups); + } + + #[test] + fn test_getgroups16_undersized_buffer_returns_einval() { + let count = unsafe { sys::getgroups16(0, core::ptr::null_mut()) }; + assert!(count >= 0, "probe failed: {count}"); + if count == 0 { + return; + } + + let mut groups = + vec![MaybeUninit::::uninit(); count as usize - 1]; + let err = getgroups16(&mut groups).unwrap_err(); + + assert_eq!(err, Getgroups16Error::Einval); + } + + #[test] + fn test_getgroups16_error_mapping() { + assert_eq!( + Getgroups16Error::from(crate::Errno::Einval), + Getgroups16Error::Einval + ); + assert_eq!( + Getgroups16Error::from(crate::Errno::Efault), + Getgroups16Error::Efault + ); + assert_eq!( + Getgroups16Error::from(crate::Errno::Enomem), + Getgroups16Error::Other(crate::Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 1087663a..3d5e1e1b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -51,6 +51,8 @@ mod getegid; mod geteuid; #[cfg(target_arch = "x86")] mod getgid; +#[cfg(target_arch = "x86")] +mod getgroups; mod helpers; pub mod sys; @@ -93,6 +95,8 @@ pub use getegid::getegid16; pub use geteuid::geteuid16; #[cfg(target_arch = "x86")] pub use getgid::getgid16; +#[cfg(target_arch = "x86")] +pub use getgroups::{Getgroups16Error, getgroups16}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From ed71149b6461b5d3de2e82444909e3ecab100545 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:39:10 +0200 Subject: [PATCH 036/135] Wrap getitimer syscall --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getitimer.rs | 117 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getitimer.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 786f9db9..c6f6f443 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -129,7 +129,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `geteuid16` - x86 - [x] `getgid16` - x86 - [x] `getgroups16` - x86 -- [ ] `getitimer` +- [x] `getitimer` - [ ] `getpgid` - [ ] `getpgrp` - x86, x86_64 - [ ] `getpid` diff --git a/system/linux/syscalls/src/getitimer.rs b/system/linux/syscalls/src/getitimer.rs new file mode 100644 index 00000000..0ac9abc9 --- /dev/null +++ b/system/linux/syscalls/src/getitimer.rs @@ -0,0 +1,117 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, Itimerval}; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`getitimer`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetitimerError { + Einval, + Efault, + Other(Errno), +} + +impl GetitimerError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Read one process interval timer into an uninitialized `Itimerval` slot. +/// +/// This wrapper accepts a mutable [`MaybeUninit`] output slot and +/// initializes it on success. The `which` argument is passed through unchanged. +/// +/// `Ok(())` means the kernel wrote the timer state into `value`. +/// +/// See [`sys::getitimer`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - [`GetitimerError::Einval`]: `which` is not a supported timer selector. +/// - [`GetitimerError::Efault`]: the kernel could not write one `Itimerval` +/// to `value`. +/// - [`GetitimerError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn getitimer( + which: Int, + value: &mut MaybeUninit, +) -> Result<(), GetitimerError> { + // SAFETY: `value` provides writable storage for one `Itimerval`. + let ret = unsafe { sys::getitimer(which, value.as_mut_ptr()) }; + result_from_ret(ret as isize, |_| (), GetitimerError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{ + ITIMER_PROF, ITIMER_REAL, ITIMER_VIRTUAL, Itimerval, + }; + + use crate::sys; + + use super::{GetitimerError, getitimer}; + + #[test] + fn test_getitimer_real_succeeds() { + let mut timer = MaybeUninit::::uninit(); + getitimer(ITIMER_REAL, &mut timer).expect("wrapped getitimer failed"); + } + + #[test] + fn test_getitimer_virtual_succeeds() { + let mut timer = MaybeUninit::::uninit(); + getitimer(ITIMER_VIRTUAL, &mut timer) + .expect("wrapped getitimer failed"); + } + + #[test] + fn test_getitimer_prof_succeeds() { + let mut timer = MaybeUninit::::uninit(); + getitimer(ITIMER_PROF, &mut timer).expect("wrapped getitimer failed"); + } + + #[test] + fn test_getitimer_rejects_invalid_which() { + let mut timer = MaybeUninit::::uninit(); + let err = getitimer(3, &mut timer).unwrap_err(); + assert_eq!(err, GetitimerError::Einval); + } + + #[test] + fn test_getitimer_error_mapping() { + assert_eq!( + GetitimerError::from_errno(crate::Errno::Einval), + GetitimerError::Einval + ); + assert_eq!( + GetitimerError::from_errno(crate::Errno::Efault), + GetitimerError::Efault + ); + assert_eq!( + GetitimerError::from_errno(crate::Errno::Enomem), + GetitimerError::Other(crate::Errno::Enomem) + ); + } + + #[test] + fn test_getitimer_matches_raw_on_invalid_which() { + let mut wrapped_timer = MaybeUninit::::uninit(); + let wrapped = getitimer(3, &mut wrapped_timer); + + let mut raw_timer = MaybeUninit::::uninit(); + let raw = unsafe { sys::getitimer(3, raw_timer.as_mut_ptr()) }; + + assert_eq!(wrapped, Err(GetitimerError::Einval)); + assert_eq!(raw, -22); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 3d5e1e1b..e31fcb81 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -53,6 +53,7 @@ mod geteuid; mod getgid; #[cfg(target_arch = "x86")] mod getgroups; +mod getitimer; mod helpers; pub mod sys; @@ -97,6 +98,7 @@ pub use geteuid::geteuid16; pub use getgid::getgid16; #[cfg(target_arch = "x86")] pub use getgroups::{Getgroups16Error, getgroups16}; +pub use getitimer::{GetitimerError, getitimer}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 94bf3192bb4c120a581b61884ddc2064150d9168 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:56:34 +0200 Subject: [PATCH 037/135] feat(syscalls): wrap getpgid --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/errno.rs | 3 + system/linux/syscalls/src/getpgid.rs | 81 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getpgid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c6f6f443..e512f47e 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -130,7 +130,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getgid16` - x86 - [x] `getgroups16` - x86 - [x] `getitimer` -- [ ] `getpgid` +- [x] `getpgid` - [ ] `getpgrp` - x86, x86_64 - [ ] `getpid` - [ ] `getppid` diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index e7a95c75..0c9c4caf 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -22,6 +22,7 @@ pub enum Errno { Eoverflow, Eperm, Erofs, + Esrch, Ewouldblock, Raw(u16), } @@ -51,6 +52,7 @@ impl Errno { 75 => Self::Eoverflow, 1 => Self::Eperm, 30 => Self::Erofs, + 3 => Self::Esrch, other => Self::Raw(other), } } @@ -98,6 +100,7 @@ mod tests { assert_eq!(Errno::from_raw(75), Errno::Eoverflow); assert_eq!(Errno::from_raw(1), Errno::Eperm); assert_eq!(Errno::from_raw(30), Errno::Erofs); + assert_eq!(Errno::from_raw(3), Errno::Esrch); assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); } diff --git a/system/linux/syscalls/src/getpgid.rs b/system/linux/syscalls/src/getpgid.rs new file mode 100644 index 00000000..827e39a7 --- /dev/null +++ b/system/linux/syscalls/src/getpgid.rs @@ -0,0 +1,81 @@ +use celer_system_linux_ctypes::PidT; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`getpgid`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetpgidError { + /// `ESRCH`. + Esrch, + /// Another errno returned by an LSM hook or the raw ABI. + Other(Errno), +} + +impl GetpgidError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Esrch => Self::Esrch, + errno => Self::Other(errno), + } + } +} + +/// Return the process group ID of a process. +/// +/// This safe wrapper maps the raw `getpgid(2)` return value into +/// `Result` while keeping the PID selector as the +/// kernel-facing integer type. Passing `0` asks for the calling process's +/// process group ID. +/// +/// On success, returns a nonnegative process group ID. +/// +/// See [`sys::getpgid`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`GetpgidError::Esrch`]: no task matches `pid`, or the kernel cannot +/// report a process group for the task selected by `pid`. +/// - [`GetpgidError::Other`]: another errno reported by an LSM hook or the raw +/// ABI. +pub fn getpgid(pid: PidT) -> Result { + let ret = sys::getpgid(pid); + + result_from_ret(ret as isize, |ret| ret as PidT, GetpgidError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::PidT; + + use super::{GetpgidError, getpgid}; + use crate::{Errno, sys}; + + #[test] + fn test_getpgid_zero_matches_raw() { + assert_eq!(getpgid(0), Ok(sys::getpgid(0))); + } + + #[test] + fn test_getpgid_self_matches_raw() { + let pid = sys::getpid(); + + assert_eq!(getpgid(pid), Ok(sys::getpgid(pid))); + } + + #[test] + fn test_getpgid_nonexistent_process_returns_esrch() { + assert_eq!(getpgid(PidT::MAX), Err(GetpgidError::Esrch)); + } + + #[test] + fn test_getpgid_error_mapping() { + assert_eq!(GetpgidError::from_errno(Errno::Esrch), GetpgidError::Esrch); + assert_eq!( + GetpgidError::from_errno(Errno::Eperm), + GetpgidError::Other(Errno::Eperm) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e31fcb81..b8824628 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -54,6 +54,7 @@ mod getgid; #[cfg(target_arch = "x86")] mod getgroups; mod getitimer; +mod getpgid; mod helpers; pub mod sys; @@ -99,6 +100,7 @@ pub use getgid::getgid16; #[cfg(target_arch = "x86")] pub use getgroups::{Getgroups16Error, getgroups16}; pub use getitimer::{GetitimerError, getitimer}; +pub use getpgid::{GetpgidError, getpgid}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 64cecbd5250a3806f52e2dde103d5cb0dc16d2c7 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:57:06 +0200 Subject: [PATCH 038/135] feat(syscalls): wrap getpgrp --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getpgrp.rs | 27 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 ++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getpgrp.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index e512f47e..90dd258c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -131,7 +131,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getgroups16` - x86 - [x] `getitimer` - [x] `getpgid` -- [ ] `getpgrp` - x86, x86_64 +- [x] `getpgrp` - x86, x86_64 - [ ] `getpid` - [ ] `getppid` - [ ] `getpriority` diff --git a/system/linux/syscalls/src/getpgrp.rs b/system/linux/syscalls/src/getpgrp.rs new file mode 100644 index 00000000..08def0eb --- /dev/null +++ b/system/linux/syscalls/src/getpgrp.rs @@ -0,0 +1,27 @@ +use celer_system_linux_ctypes::PidT; + +use crate::sys; + +/// Return the process group ID of the calling process. +/// +/// This safe wrapper exposes the raw `getpgrp(2)` success value directly. The +/// syscall takes no arguments and the verified kernel entry path has no error +/// returns. +/// +/// See [`sys::getpgrp`] for kernel behavior, historical notes, and source +/// references. +pub fn getpgrp() -> PidT { + sys::getpgrp() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::getpgrp; + use crate::sys; + + #[test] + fn test_getpgrp_matches_raw() { + assert_eq!(getpgrp(), sys::getpgrp()); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b8824628..fbfc2176 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -55,6 +55,8 @@ mod getgid; mod getgroups; mod getitimer; mod getpgid; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod getpgrp; mod helpers; pub mod sys; @@ -101,6 +103,8 @@ pub use getgid::getgid16; pub use getgroups::{Getgroups16Error, getgroups16}; pub use getitimer::{GetitimerError, getitimer}; pub use getpgid::{GetpgidError, getpgid}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use getpgrp::getpgrp; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 34bdeac06dd5ab9c5ea459c812959327d11129d6 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:57:29 +0200 Subject: [PATCH 039/135] feat(syscalls): wrap getpid --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getpid.rs | 27 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getpid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 90dd258c..c0c7ae5c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -132,7 +132,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getitimer` - [x] `getpgid` - [x] `getpgrp` - x86, x86_64 -- [ ] `getpid` +- [x] `getpid` - [ ] `getppid` - [ ] `getpriority` - [ ] `getrlimit` diff --git a/system/linux/syscalls/src/getpid.rs b/system/linux/syscalls/src/getpid.rs new file mode 100644 index 00000000..d04ee6f3 --- /dev/null +++ b/system/linux/syscalls/src/getpid.rs @@ -0,0 +1,27 @@ +use celer_system_linux_ctypes::PidT; + +use crate::sys; + +/// Return the process ID of the calling process. +/// +/// This safe wrapper exposes the raw `getpid(2)` success value directly. The +/// syscall takes no arguments and the verified kernel entry path has no error +/// returns. +/// +/// See [`sys::getpid`] for kernel behavior, historical notes, and source +/// references. +pub fn getpid() -> PidT { + sys::getpid() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::getpid; + use crate::sys; + + #[test] + fn test_getpid_matches_raw() { + assert_eq!(getpid(), sys::getpid()); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index fbfc2176..11823783 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -57,6 +57,7 @@ mod getitimer; mod getpgid; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod getpgrp; +mod getpid; mod helpers; pub mod sys; @@ -105,6 +106,7 @@ pub use getitimer::{GetitimerError, getitimer}; pub use getpgid::{GetpgidError, getpgid}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use getpgrp::getpgrp; +pub use getpid::getpid; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 910bdc27d5391317c4f3f899a27f031c1dfa7b52 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:58:12 +0200 Subject: [PATCH 040/135] feat(syscalls): wrap getppid --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getppid.rs | 27 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getppid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c0c7ae5c..56867e43 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -133,7 +133,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getpgid` - [x] `getpgrp` - x86, x86_64 - [x] `getpid` -- [ ] `getppid` +- [x] `getppid` - [ ] `getpriority` - [ ] `getrlimit` - [ ] `getrusage` diff --git a/system/linux/syscalls/src/getppid.rs b/system/linux/syscalls/src/getppid.rs new file mode 100644 index 00000000..75baeaa5 --- /dev/null +++ b/system/linux/syscalls/src/getppid.rs @@ -0,0 +1,27 @@ +use celer_system_linux_ctypes::PidT; + +use crate::sys; + +/// Return the parent process ID of the calling process. +/// +/// This safe wrapper exposes the raw `getppid(2)` success value directly. The +/// syscall takes no arguments and the verified kernel entry path has no error +/// returns. +/// +/// See [`sys::getppid`] for kernel behavior, historical notes, and source +/// references. +pub fn getppid() -> PidT { + sys::getppid() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::getppid; + use crate::sys; + + #[test] + fn test_getppid_matches_raw() { + assert_eq!(getppid(), sys::getppid()); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 11823783..a3cc0de9 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -58,6 +58,7 @@ mod getpgid; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod getpgrp; mod getpid; +mod getppid; mod helpers; pub mod sys; @@ -107,6 +108,7 @@ pub use getpgid::{GetpgidError, getpgid}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use getpgrp::getpgrp; pub use getpid::getpid; +pub use getppid::getppid; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 16d860303e4fec1da8a48995a67435ad20f591f1 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 10:59:03 +0200 Subject: [PATCH 041/135] feat(syscalls): wrap getpriority --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getpriority.rs | 102 +++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getpriority.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 56867e43..f251b63c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -134,7 +134,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getpgrp` - x86, x86_64 - [x] `getpid` - [x] `getppid` -- [ ] `getpriority` +- [x] `getpriority` - [ ] `getrlimit` - [ ] `getrusage` - [ ] `gettimeofday` diff --git a/system/linux/syscalls/src/getpriority.rs b/system/linux/syscalls/src/getpriority.rs new file mode 100644 index 00000000..206aaaa5 --- /dev/null +++ b/system/linux/syscalls/src/getpriority.rs @@ -0,0 +1,102 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`getpriority`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetpriorityError { + /// `EINVAL`. + Einval, + /// `ESRCH`. + Esrch, + /// Another errno reported by the raw ABI. + Other(Errno), +} + +impl GetpriorityError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Esrch => Self::Esrch, + errno => Self::Other(errno), + } + } +} + +/// Return the highest matching scheduler priority for a process, process +/// group, or user selection. +/// +/// This safe wrapper maps the raw `getpriority(2)` return value into +/// `Result` while keeping the selector and subject as +/// the kernel-facing integer types. +/// +/// On success, current kernels return the encoded compatibility priority value +/// described by the raw syscall documentation. +/// +/// See [`sys::getpriority`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`GetpriorityError::Einval`]: `which` is not `PRIO_PROCESS`, +/// `PRIO_PGRP`, or `PRIO_USER`. +/// - [`GetpriorityError::Esrch`]: no task matches the selected `which` / `who` +/// pair. +/// - [`GetpriorityError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn getpriority(which: Int, who: Int) -> Result { + let ret = sys::getpriority(which, who); + + result_from_ret( + ret as isize, + |ret| ret as Int, + GetpriorityError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{Int, PRIO_PROCESS}; + + use super::{GetpriorityError, getpriority}; + use crate::{Errno, sys}; + + #[test] + fn test_getpriority_current_process_matches_raw() { + assert_eq!( + getpriority(PRIO_PROCESS, 0), + Ok(sys::getpriority(PRIO_PROCESS, 0)) + ); + } + + #[test] + fn test_getpriority_rejects_invalid_selector() { + assert_eq!(getpriority(3, 0), Err(GetpriorityError::Einval)); + } + + #[test] + fn test_getpriority_nonexistent_process_returns_esrch() { + assert_eq!( + getpriority(PRIO_PROCESS, Int::MAX), + Err(GetpriorityError::Esrch) + ); + } + + #[test] + fn test_getpriority_error_mapping() { + assert_eq!( + GetpriorityError::from_errno(Errno::Einval), + GetpriorityError::Einval + ); + assert_eq!( + GetpriorityError::from_errno(Errno::Esrch), + GetpriorityError::Esrch + ); + assert_eq!( + GetpriorityError::from_errno(Errno::Eperm), + GetpriorityError::Other(Errno::Eperm) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index a3cc0de9..f1e55d9c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -59,6 +59,7 @@ mod getpgid; mod getpgrp; mod getpid; mod getppid; +mod getpriority; mod helpers; pub mod sys; @@ -109,6 +110,7 @@ pub use getpgid::{GetpgidError, getpgid}; pub use getpgrp::getpgrp; pub use getpid::getpid; pub use getppid::getppid; +pub use getpriority::{GetpriorityError, getpriority}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From e4809616c1710ec6c08cf147e464205c1c7ac244 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 11:34:44 +0200 Subject: [PATCH 042/135] feat(syscalls): wrap getrlimit --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getrlimit.rs | 123 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getrlimit.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index f251b63c..cffc36a9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -135,7 +135,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getpid` - [x] `getppid` - [x] `getpriority` -- [ ] `getrlimit` +- [x] `getrlimit` - [ ] `getrusage` - [ ] `gettimeofday` - [ ] `getuid16` - x86 diff --git a/system/linux/syscalls/src/getrlimit.rs b/system/linux/syscalls/src/getrlimit.rs new file mode 100644 index 00000000..0aeeb41a --- /dev/null +++ b/system/linux/syscalls/src/getrlimit.rs @@ -0,0 +1,123 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Rlimit, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`getrlimit`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetrlimitError { + /// `EINVAL`. + Einval, + /// `EFAULT`. + Efault, + /// Another errno reported by the raw ABI. + Other(Errno), +} + +impl GetrlimitError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Copy the current process limit for one resource into an output slot. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and passes `resource` through unchanged. +/// +/// `Ok(())` means the kernel initialized `rlim` with the current soft and hard +/// limits for `resource`. +/// +/// See [`sys::getrlimit`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`GetrlimitError::Einval`]: `resource` is outside the kernel's supported +/// resource range. +/// - [`GetrlimitError::Efault`]: the kernel could not write one `Rlimit` to +/// `rlim`. +/// - [`GetrlimitError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn getrlimit( + resource: UnsignedInt, + rlim: &mut MaybeUninit, +) -> Result<(), GetrlimitError> { + // SAFETY: `rlim` provides writable storage for one `Rlimit`. + let ret = unsafe { sys::getrlimit(resource, rlim.as_mut_ptr()) }; + + result_from_ret(ret as isize, |_| (), GetrlimitError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{Rlimit, UnsignedInt}; + + use super::{GetrlimitError, getrlimit}; + use crate::{Errno, sys}; + + const RLIMIT_CPU: UnsignedInt = 0; + const CURRENT_RLIM_NLIMITS: UnsignedInt = 16; + + #[test] + fn test_getrlimit_cpu_success() { + let mut rlim = MaybeUninit::::uninit(); + + getrlimit(RLIMIT_CPU, &mut rlim).expect("wrapped getrlimit failed"); + let rlim = unsafe { rlim.assume_init() }; + + assert!( + rlim.rlim_cur <= rlim.rlim_max, + "soft limit should not exceed hard limit: {rlim:?}" + ); + } + + #[test] + fn test_getrlimit_invalid_resource() { + let mut rlim = MaybeUninit::::uninit(); + + assert_eq!( + getrlimit(CURRENT_RLIM_NLIMITS, &mut rlim), + Err(GetrlimitError::Einval) + ); + } + + #[test] + fn test_getrlimit_matches_raw_on_invalid_resource() { + let mut wrapped_rlim = MaybeUninit::::uninit(); + let wrapped = getrlimit(CURRENT_RLIM_NLIMITS, &mut wrapped_rlim); + + let mut raw_rlim = MaybeUninit::::uninit(); + let raw = unsafe { + sys::getrlimit(CURRENT_RLIM_NLIMITS, raw_rlim.as_mut_ptr()) + }; + + assert_eq!(wrapped, Err(GetrlimitError::Einval)); + assert_eq!(raw, -22); + } + + #[test] + fn test_getrlimit_error_mapping() { + assert_eq!( + GetrlimitError::from_errno(Errno::Einval), + GetrlimitError::Einval + ); + assert_eq!( + GetrlimitError::from_errno(Errno::Efault), + GetrlimitError::Efault + ); + assert_eq!( + GetrlimitError::from_errno(Errno::Enomem), + GetrlimitError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f1e55d9c..f739d300 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -60,6 +60,7 @@ mod getpgrp; mod getpid; mod getppid; mod getpriority; +mod getrlimit; mod helpers; pub mod sys; @@ -111,6 +112,7 @@ pub use getpgrp::getpgrp; pub use getpid::getpid; pub use getppid::getppid; pub use getpriority::{GetpriorityError, getpriority}; +pub use getrlimit::{GetrlimitError, getrlimit}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From fc66157e73214ef31b4d00114bf7b26060332bb6 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 11:35:25 +0200 Subject: [PATCH 043/135] feat(syscalls): wrap getrusage --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getrusage.rs | 117 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getrusage.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index cffc36a9..b2b427ac 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -136,7 +136,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getppid` - [x] `getpriority` - [x] `getrlimit` -- [ ] `getrusage` +- [x] `getrusage` - [ ] `gettimeofday` - [ ] `getuid16` - x86 - [ ] `idle` - x86 diff --git a/system/linux/syscalls/src/getrusage.rs b/system/linux/syscalls/src/getrusage.rs new file mode 100644 index 00000000..cde2423b --- /dev/null +++ b/system/linux/syscalls/src/getrusage.rs @@ -0,0 +1,117 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, Rusage}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`getrusage`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetrusageError { + /// `EINVAL`. + Einval, + /// `EFAULT`. + Efault, + /// Another errno reported by the raw ABI. + Other(Errno), +} + +impl GetrusageError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Return resource-usage accounting into an output slot. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and passes `who` through unchanged. +/// +/// `Ok(())` means the kernel initialized `ru` with the selected usage +/// counters. +/// +/// See [`sys::getrusage`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`GetrusageError::Einval`]: `who` is not a supported resource-usage +/// selector. +/// - [`GetrusageError::Efault`]: the kernel could not write one `Rusage` to +/// `ru`. +/// - [`GetrusageError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn getrusage( + who: Int, + ru: &mut MaybeUninit, +) -> Result<(), GetrusageError> { + // SAFETY: `ru` provides writable storage for one `Rusage`. + let ret = unsafe { sys::getrusage(who, ru.as_mut_ptr()) }; + + result_from_ret(ret as isize, |_| (), GetrusageError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{RUSAGE_CHILDREN, RUSAGE_SELF, Rusage}; + + use super::{GetrusageError, getrusage}; + use crate::{Errno, sys}; + + #[test] + fn test_getrusage_self_succeeds() { + let mut usage = MaybeUninit::::uninit(); + + getrusage(RUSAGE_SELF, &mut usage).expect("wrapped getrusage failed"); + } + + #[test] + fn test_getrusage_children_succeeds() { + let mut usage = MaybeUninit::::uninit(); + + getrusage(RUSAGE_CHILDREN, &mut usage) + .expect("wrapped getrusage failed"); + } + + #[test] + fn test_getrusage_rejects_invalid_who() { + let mut usage = MaybeUninit::::uninit(); + + assert_eq!(getrusage(-2, &mut usage), Err(GetrusageError::Einval)); + } + + #[test] + fn test_getrusage_matches_raw_on_invalid_who() { + let mut wrapped_usage = MaybeUninit::::uninit(); + let wrapped = getrusage(-2, &mut wrapped_usage); + + let mut raw_usage = MaybeUninit::::uninit(); + let raw = unsafe { sys::getrusage(-2, raw_usage.as_mut_ptr()) }; + + assert_eq!(wrapped, Err(GetrusageError::Einval)); + assert_eq!(raw, -22); + } + + #[test] + fn test_getrusage_error_mapping() { + assert_eq!( + GetrusageError::from_errno(Errno::Einval), + GetrusageError::Einval + ); + assert_eq!( + GetrusageError::from_errno(Errno::Efault), + GetrusageError::Efault + ); + assert_eq!( + GetrusageError::from_errno(Errno::Enomem), + GetrusageError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f739d300..8fbbafeb 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -61,6 +61,7 @@ mod getpid; mod getppid; mod getpriority; mod getrlimit; +mod getrusage; mod helpers; pub mod sys; @@ -113,6 +114,7 @@ pub use getpid::getpid; pub use getppid::getppid; pub use getpriority::{GetpriorityError, getpriority}; pub use getrlimit::{GetrlimitError, getrlimit}; +pub use getrusage::{GetrusageError, getrusage}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 8e04dfb27f87c8c44bcd93f7f07e766b68beaf7c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 11:36:16 +0200 Subject: [PATCH 044/135] feat(syscalls): wrap gettimeofday --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/gettimeofday.rs | 129 ++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/gettimeofday.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index b2b427ac..ea79f757 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -137,7 +137,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getpriority` - [x] `getrlimit` - [x] `getrusage` -- [ ] `gettimeofday` +- [x] `gettimeofday` - [ ] `getuid16` - x86 - [ ] `idle` - x86 - [ ] `init_module` diff --git a/system/linux/syscalls/src/gettimeofday.rs b/system/linux/syscalls/src/gettimeofday.rs new file mode 100644 index 00000000..daf03f22 --- /dev/null +++ b/system/linux/syscalls/src/gettimeofday.rs @@ -0,0 +1,129 @@ +use core::{mem::MaybeUninit, ptr}; + +use celer_system_linux_ctypes::{Timeval, Timezone}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`gettimeofday`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GettimeofdayError { + /// `EFAULT`. + Efault, + /// Another errno reported by the raw ABI. + Other(Errno), +} + +impl GettimeofdayError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Get the current time of day and optional kernel timezone state. +/// +/// This safe wrapper turns each nullable raw output pointer into an +/// `Option<&mut MaybeUninit<_>>`. Passing `None` for either output preserves +/// the raw syscall's null-pointer behavior. +/// +/// `Ok(())` means the kernel initialized each provided output slot. +/// +/// See [`sys::gettimeofday`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`GettimeofdayError::Efault`]: the kernel could not write one of the +/// provided output slots. +/// - [`GettimeofdayError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn gettimeofday( + mut tv: Option<&mut MaybeUninit>, + mut tz: Option<&mut MaybeUninit>, +) -> Result<(), GettimeofdayError> { + let tv = tv.as_mut().map_or(ptr::null_mut(), |tv| tv.as_mut_ptr()); + let tz = tz.as_mut().map_or(ptr::null_mut(), |tz| tz.as_mut_ptr()); + + // SAFETY: each non-null pointer comes from a `MaybeUninit` output slot, + // and null is a meaningful input for both raw syscall arguments. + let ret = unsafe { sys::gettimeofday(tv, tz) }; + + result_from_ret(ret as isize, |_| (), GettimeofdayError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{Timeval, Timezone}; + + use super::{GettimeofdayError, gettimeofday}; + use crate::{Errno, sys}; + + #[test] + fn test_gettimeofday_both_outputs() { + let mut tv = MaybeUninit::::uninit(); + let mut tz = MaybeUninit::::uninit(); + + gettimeofday(Some(&mut tv), Some(&mut tz)) + .expect("wrapped gettimeofday failed"); + + let tv = unsafe { tv.assume_init() }; + let tz = unsafe { tz.assume_init() }; + + assert!(tv.tv_sec > 0, "tv_sec should be a positive epoch value"); + assert!( + (0..1_000_000).contains(&tv.tv_usec), + "tv_usec should be in [0, 1_000_000), got {}", + tv.tv_usec + ); + assert_ne!(tz.tz_minuteswest, -1); + assert_ne!(tz.tz_dsttime, -1); + } + + #[test] + fn test_gettimeofday_tv_none() { + let mut tz = MaybeUninit::::uninit(); + + gettimeofday(None, Some(&mut tz)).expect("wrapped gettimeofday failed"); + } + + #[test] + fn test_gettimeofday_tz_none() { + let mut tv = MaybeUninit::::uninit(); + + gettimeofday(Some(&mut tv), None).expect("wrapped gettimeofday failed"); + } + + #[test] + fn test_gettimeofday_both_none() { + gettimeofday(None, None).expect("wrapped gettimeofday failed"); + } + + #[test] + fn test_gettimeofday_matches_raw_with_null_outputs() { + let wrapped = gettimeofday(None, None); + let raw = unsafe { + sys::gettimeofday(core::ptr::null_mut(), core::ptr::null_mut()) + }; + + assert_eq!(wrapped, Ok(())); + assert_eq!(raw, 0); + } + + #[test] + fn test_gettimeofday_error_mapping() { + assert_eq!( + GettimeofdayError::from_errno(Errno::Efault), + GettimeofdayError::Efault + ); + assert_eq!( + GettimeofdayError::from_errno(Errno::Enomem), + GettimeofdayError::Other(Errno::Enomem) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 8fbbafeb..f3934088 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -62,6 +62,7 @@ mod getppid; mod getpriority; mod getrlimit; mod getrusage; +mod gettimeofday; mod helpers; pub mod sys; @@ -115,6 +116,7 @@ pub use getppid::getppid; pub use getpriority::{GetpriorityError, getpriority}; pub use getrlimit::{GetrlimitError, getrlimit}; pub use getrusage::{GetrusageError, getrusage}; +pub use gettimeofday::{GettimeofdayError, gettimeofday}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 19f46d4fd1daa5a5a89540fec48a9b36e2f54569 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 11:36:40 +0200 Subject: [PATCH 045/135] feat(syscalls): wrap getuid16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/getuid.rs | 39 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 +++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/getuid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index ea79f757..cdb7e158 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -138,7 +138,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getrlimit` - [x] `getrusage` - [x] `gettimeofday` -- [ ] `getuid16` - x86 +- [x] `getuid16` - x86 - [ ] `idle` - x86 - [ ] `init_module` - [ ] `ioctl` diff --git a/system/linux/syscalls/src/getuid.rs b/system/linux/syscalls/src/getuid.rs new file mode 100644 index 00000000..3f438134 --- /dev/null +++ b/system/linux/syscalls/src/getuid.rs @@ -0,0 +1,39 @@ +use celer_system_linux_ctypes::OldUidT; + +use crate::sys; + +/// Return the caller's real user ID through the legacy x86 `getuid16` ABI. +/// +/// This wrapper keeps the raw syscall's no-argument shape and returns the +/// legacy 16-bit user ID directly. +/// +/// See [`sys::getuid16`] for kernel behavior, historical notes, and source +/// references. +/// +/// # Errors +/// - Never fails. +pub fn getuid16() -> OldUidT { + sys::getuid16() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::OldUidT; + + use super::getuid16; + use crate::sys; + + #[test] + fn test_getuid16_matches_raw() { + let wrapped = getuid16(); + let raw = sys::getuid16(); + + assert_eq!(wrapped, raw, "wrapped getuid16 should match raw syscall"); + } + + #[test] + fn test_getuid16_type() { + let _: OldUidT = getuid16(); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f3934088..000ddaf8 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -63,6 +63,8 @@ mod getpriority; mod getrlimit; mod getrusage; mod gettimeofday; +#[cfg(target_arch = "x86")] +mod getuid; mod helpers; pub mod sys; @@ -117,6 +119,8 @@ pub use getpriority::{GetpriorityError, getpriority}; pub use getrlimit::{GetrlimitError, getrlimit}; pub use getrusage::{GetrusageError, getrusage}; pub use gettimeofday::{GettimeofdayError, gettimeofday}; +#[cfg(target_arch = "x86")] +pub use getuid::getuid16; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 42fe957b29b12bef7d7e589c226818c04bdaa857 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 11:37:28 +0200 Subject: [PATCH 046/135] feat(syscalls): wrap idle --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/idle.rs | 66 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 ++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/idle.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index cdb7e158..cf626ff2 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -139,7 +139,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getrusage` - [x] `gettimeofday` - [x] `getuid16` - x86 -- [ ] `idle` - x86 +- [x] `idle` - x86 - [ ] `init_module` - [ ] `ioctl` - [ ] `ioperm` - x86 diff --git a/system/linux/syscalls/src/idle.rs b/system/linux/syscalls/src/idle.rs new file mode 100644 index 00000000..53e29306 --- /dev/null +++ b/system/linux/syscalls/src/idle.rs @@ -0,0 +1,66 @@ +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`idle`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IdleError { + /// `ENOSYS`. + Enosys, + /// Another errno reported by the raw ABI. + Other(Errno), +} + +impl IdleError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Call the x86 `idle` syscall slot. +/// +/// This safe wrapper keeps the raw no-argument syscall shape and maps the raw +/// return into `Result<(), IdleError>`. +/// +/// `Ok(())` means the kernel reported success for the syscall slot. +/// +/// See [`sys::idle`] for kernel behavior, historical notes, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`IdleError::Enosys`]: the current x86 syscall table has no implemented +/// `idle` entry. +/// - [`IdleError::Other`]: any other syscall error reported by the raw ABI. +pub fn idle() -> Result<(), IdleError> { + let ret = sys::idle(); + + unit_from_ret(ret as isize, IdleError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{IdleError, idle}; + use crate::{Errno, sys}; + + #[test] + fn test_idle_matches_raw() { + let wrapped = idle(); + let raw = sys::idle(); + + assert_eq!(wrapped, Err(IdleError::Enosys)); + assert_eq!(raw, -38); + } + + #[test] + fn test_idle_error_mapping() { + assert_eq!(IdleError::from_errno(Errno::Enosys), IdleError::Enosys); + assert_eq!( + IdleError::from_errno(Errno::Einval), + IdleError::Other(Errno::Einval) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 000ddaf8..58463cba 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -66,6 +66,8 @@ mod gettimeofday; #[cfg(target_arch = "x86")] mod getuid; mod helpers; +#[cfg(target_arch = "x86")] +mod idle; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -121,6 +123,8 @@ pub use getrusage::{GetrusageError, getrusage}; pub use gettimeofday::{GettimeofdayError, gettimeofday}; #[cfg(target_arch = "x86")] pub use getuid::getuid16; +#[cfg(target_arch = "x86")] +pub use idle::{IdleError, idle}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 08b436d966dc2033303bad856988e1855329e745 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:03:50 +0200 Subject: [PATCH 047/135] feat(syscalls): wrap init_module --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/errno.rs | 9 ++ system/linux/syscalls/src/init_module.rs | 187 +++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/init_module.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index cf626ff2..8271ee0a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -140,7 +140,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `gettimeofday` - [x] `getuid16` - x86 - [x] `idle` - x86 -- [ ] `init_module` +- [x] `init_module` - [ ] `ioctl` - [ ] `ioperm` - x86 - [ ] `iopl` - x86 diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index 0c9c4caf..c470eb5f 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -5,6 +5,7 @@ pub enum Errno { Eagain, E2big, Ebadf, + Ebadmsg, Ebusy, Emfile, Efault, @@ -18,9 +19,11 @@ pub enum Errno { Enoexec, Enomem, Enosys, + Enotty, Enotdir, Eoverflow, Eperm, + Ekeyrejected, Erofs, Esrch, Ewouldblock, @@ -35,6 +38,7 @@ impl Errno { 11 => Self::Eagain, 7 => Self::E2big, 9 => Self::Ebadf, + 74 => Self::Ebadmsg, 16 => Self::Ebusy, 24 => Self::Emfile, 14 => Self::Efault, @@ -48,9 +52,11 @@ impl Errno { 8 => Self::Enoexec, 12 => Self::Enomem, 38 => Self::Enosys, + 25 => Self::Enotty, 20 => Self::Enotdir, 75 => Self::Eoverflow, 1 => Self::Eperm, + 129 => Self::Ekeyrejected, 30 => Self::Erofs, 3 => Self::Esrch, other => Self::Raw(other), @@ -83,6 +89,7 @@ mod tests { assert_eq!(Errno::from_raw(11), Errno::Eagain); assert_eq!(Errno::from_raw(7), Errno::E2big); assert_eq!(Errno::from_raw(9), Errno::Ebadf); + assert_eq!(Errno::from_raw(74), Errno::Ebadmsg); assert_eq!(Errno::from_raw(16), Errno::Ebusy); assert_eq!(Errno::from_raw(24), Errno::Emfile); assert_eq!(Errno::from_raw(14), Errno::Efault); @@ -96,9 +103,11 @@ mod tests { assert_eq!(Errno::from_raw(8), Errno::Enoexec); assert_eq!(Errno::from_raw(12), Errno::Enomem); assert_eq!(Errno::from_raw(38), Errno::Enosys); + assert_eq!(Errno::from_raw(25), Errno::Enotty); assert_eq!(Errno::from_raw(20), Errno::Enotdir); assert_eq!(Errno::from_raw(75), Errno::Eoverflow); assert_eq!(Errno::from_raw(1), Errno::Eperm); + assert_eq!(Errno::from_raw(129), Errno::Ekeyrejected); assert_eq!(Errno::from_raw(30), Errno::Erofs); assert_eq!(Errno::from_raw(3), Errno::Esrch); assert_eq!(Errno::from_raw(4095), Errno::Raw(4095)); diff --git a/system/linux/syscalls/src/init_module.rs b/system/linux/syscalls/src/init_module.rs new file mode 100644 index 00000000..7aefdd1f --- /dev/null +++ b/system/linux/syscalls/src/init_module.rs @@ -0,0 +1,187 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{UnsignedLong, Void}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`init_module`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum InitModuleError { + /// `EPERM`. + Eperm, + /// `ENOENT`. + Enoent, + /// `EINVAL`. + Einval, + /// `EBUSY`. + Ebusy, + /// `EINTR`. + Eintr, + /// `EFAULT`. + Efault, + /// `ENOMEM`. + Enomem, + /// `ENOEXEC`. + Enoexec, + /// `EEXIST`. + Eexist, + /// `EBADMSG`. + Ebadmsg, + /// `EKEYREJECTED`. + Ekeyrejected, + /// `ENOSYS`. + Enosys, + /// Another errno returned by delegated policy or object-specific loader + /// paths. + Other(Errno), +} + +impl InitModuleError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Enoent => Self::Enoent, + Errno::Einval => Self::Einval, + Errno::Ebusy => Self::Ebusy, + Errno::Eintr => Self::Eintr, + Errno::Efault => Self::Efault, + Errno::Enomem => Self::Enomem, + Errno::Enoexec => Self::Enoexec, + Errno::Eexist => Self::Eexist, + Errno::Ebadmsg => Self::Ebadmsg, + Errno::Ekeyrejected => Self::Ekeyrejected, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Load a kernel module image from user memory. +/// +/// This safe wrapper converts the raw module-image pointer and length into a +/// shared byte slice and converts the module-parameter pointer into `&CStr`. +/// It maps the raw `init_module(2)` return value into +/// `Result<(), InitModuleError>`. +/// +/// On success, returns `Ok(())` after the kernel has accepted the copied +/// module image and completed the module's load path. +/// +/// See [`sys::init_module`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`InitModuleError::Eperm`]: the caller lacks permission to load modules, +/// module loading is disabled, or policy rejects the load. +/// - [`InitModuleError::Enoent`]: the kernel could not resolve a required +/// non-weak symbol. +/// - [`InitModuleError::Einval`]: the kernel rejected malformed module state +/// or arguments after the syscall reached those checks. +/// - [`InitModuleError::Ebusy`]: the kernel detected an in-progress duplicate +/// load or a still-loading symbol owner. +/// - [`InitModuleError::Eintr`]: the duplicate-load wait was interrupted. +/// - [`InitModuleError::Efault`]: the kernel could not read the module image +/// or parameter string from user memory. +/// - [`InitModuleError::Enomem`]: the kernel could not allocate loader +/// memory. +/// - [`InitModuleError::Enoexec`]: the kernel rejected the supplied bytes as +/// an invalid module image. +/// - [`InitModuleError::Eexist`]: the same module is already live. +/// - [`InitModuleError::Ebadmsg`]: the module signature trailer was malformed. +/// - [`InitModuleError::Ekeyrejected`]: signature enforcement rejected the +/// module image. +/// - [`InitModuleError::Enosys`]: the running kernel left this syscall slot +/// unimplemented. +/// - [`InitModuleError::Other`]: another loader, filesystem, or security-hook +/// errno. +pub fn init_module(image: &[u8], uargs: &CStr) -> Result<(), InitModuleError> { + // SAFETY: `image` and `CStr` provide valid user-memory pointers for the + // duration of the syscall. + let ret = unsafe { + sys::init_module( + image.as_ptr().cast::(), + image.len() as UnsignedLong, + uargs.as_ptr(), + ) + }; + + unit_from_ret(ret as isize, InitModuleError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{InitModuleError, init_module}; + + #[test] + fn test_init_module_empty_image() { + let err = init_module(&[], c"") + .expect_err("an empty module image should not load successfully"); + + assert!(matches!( + err, + InitModuleError::Eperm + | InitModuleError::Enoexec + | InitModuleError::Enosys + )); + } + + #[test] + fn test_init_module_error_mapping() { + assert_eq!( + InitModuleError::from_errno(Errno::Eperm), + InitModuleError::Eperm + ); + assert_eq!( + InitModuleError::from_errno(Errno::Enoent), + InitModuleError::Enoent + ); + assert_eq!( + InitModuleError::from_errno(Errno::Einval), + InitModuleError::Einval + ); + assert_eq!( + InitModuleError::from_errno(Errno::Ebusy), + InitModuleError::Ebusy + ); + assert_eq!( + InitModuleError::from_errno(Errno::Eintr), + InitModuleError::Eintr + ); + assert_eq!( + InitModuleError::from_errno(Errno::Efault), + InitModuleError::Efault + ); + assert_eq!( + InitModuleError::from_errno(Errno::Enomem), + InitModuleError::Enomem + ); + assert_eq!( + InitModuleError::from_errno(Errno::Enoexec), + InitModuleError::Enoexec + ); + assert_eq!( + InitModuleError::from_errno(Errno::Eexist), + InitModuleError::Eexist + ); + assert_eq!( + InitModuleError::from_errno(Errno::Ebadmsg), + InitModuleError::Ebadmsg + ); + assert_eq!( + InitModuleError::from_errno(Errno::Ekeyrejected), + InitModuleError::Ekeyrejected + ); + assert_eq!( + InitModuleError::from_errno(Errno::Enosys), + InitModuleError::Enosys + ); + assert_eq!( + InitModuleError::from_errno(Errno::Eio), + InitModuleError::Other(Errno::Eio) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 58463cba..f66865d1 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -68,6 +68,7 @@ mod getuid; mod helpers; #[cfg(target_arch = "x86")] mod idle; +mod init_module; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -125,6 +126,7 @@ pub use gettimeofday::{GettimeofdayError, gettimeofday}; pub use getuid::getuid16; #[cfg(target_arch = "x86")] pub use idle::{IdleError, idle}; +pub use init_module::{InitModuleError, init_module}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 31022e7ba2b23554dc5d6dac894703f0b355b2bd Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:04:30 +0200 Subject: [PATCH 048/135] feat(syscalls): wrap ioctl --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/ioctl.rs | 129 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/ioctl.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 8271ee0a..35d868fa 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -141,7 +141,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `getuid16` - x86 - [x] `idle` - x86 - [x] `init_module` -- [ ] `ioctl` +- [x] `ioctl` - [ ] `ioperm` - x86 - [ ] `iopl` - x86 - [ ] `ipc` - x86 diff --git a/system/linux/syscalls/src/ioctl.rs b/system/linux/syscalls/src/ioctl.rs new file mode 100644 index 00000000..a8a22b6e --- /dev/null +++ b/system/linux/syscalls/src/ioctl.rs @@ -0,0 +1,129 @@ +use celer_system_linux_ctypes::{Long, UnsignedInt, UnsignedLong}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`ioctl`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IoctlError { + /// `EBADF`. + Ebadf, + /// `ENOTTY`. + Enotty, + /// Another errno returned by request-specific or delegated handling. + Other(Errno), +} + +impl IoctlError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Enotty => Self::Enotty, + errno => Self::Other(errno), + } + } +} + +/// Perform an `ioctl(2)` operation on an open file descriptor. +/// +/// This wrapper preserves the raw `request` and `arg` integers, but maps the +/// raw syscall return value into `Result`. +/// +/// On success, returns the nonnegative result reported by the selected ioctl +/// handler. Many commands return `0`, but request-specific handlers may return +/// other scalar values. +/// +/// See [`sys::ioctl`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// - Some `request` values cause the kernel to interpret `arg` as a userspace +/// pointer and copy to or from that address. The caller must uphold the +/// request-specific pointer validity requirements in those cases. +/// +/// # Errors +/// - [`IoctlError::Ebadf`]: `fd` does not refer to an open file descriptor. +/// - [`IoctlError::Enotty`]: the target file object does not support +/// `request`. +/// - [`IoctlError::Other`]: another request-specific, security-hook, or +/// object-specific errno. +pub unsafe fn ioctl( + fd: UnsignedInt, + request: UnsignedLong, + arg: UnsignedLong, +) -> Result { + // SAFETY: the caller must uphold any request-specific pointer validity + // requirements when `arg` names userspace memory. + let ret = unsafe { sys::ioctl(fd, request, arg) }; + + result_from_ret(ret as isize, |ret| ret as Long, IoctlError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UnsignedLong; + + use crate::Errno; + + use super::{IoctlError, ioctl}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_ioctl_{now}")); + path + } + + #[test] + fn test_ioctl_invalid_fd() { + // SAFETY: request `0` treats `arg` as a scalar in this test. + assert_eq!( + unsafe { ioctl(9_999, 0 as UnsignedLong, 0 as UnsignedLong) }, + Err(IoctlError::Ebadf) + ); + } + + #[test] + fn test_ioctl_enotty_on_regular_file() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .read(true) + .open(&path) + .unwrap(); + + // SAFETY: this bogus request treats `arg` as a scalar and is intended + // to exercise the unsupported-command path. + let err = unsafe { + ioctl(file.as_raw_fd() as _, 0xDEAD_BEEF, 0 as UnsignedLong) + } + .expect_err("a regular file should reject an unsupported ioctl"); + + assert_eq!(err, IoctlError::Enotty); + + fs::remove_file(path).unwrap(); + } + + #[test] + fn test_ioctl_error_mapping() { + assert_eq!(IoctlError::from_errno(Errno::Ebadf), IoctlError::Ebadf); + assert_eq!(IoctlError::from_errno(Errno::Enotty), IoctlError::Enotty); + assert_eq!( + IoctlError::from_errno(Errno::Efault), + IoctlError::Other(Errno::Efault) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f66865d1..099e63e7 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -69,6 +69,7 @@ mod helpers; #[cfg(target_arch = "x86")] mod idle; mod init_module; +mod ioctl; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -127,6 +128,7 @@ pub use getuid::getuid16; #[cfg(target_arch = "x86")] pub use idle::{IdleError, idle}; pub use init_module::{InitModuleError, init_module}; +pub use ioctl::{IoctlError, ioctl}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From f7887c243a22380384f6245fde690d2564a366a0 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:05:08 +0200 Subject: [PATCH 049/135] feat(syscalls): wrap ioperm --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/ioperm.rs | 103 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/ioperm.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 35d868fa..73f8f91a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -142,7 +142,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `idle` - x86 - [x] `init_module` - [x] `ioctl` -- [ ] `ioperm` - x86 +- [x] `ioperm` - x86 - [ ] `iopl` - x86 - [ ] `ipc` - x86 - [ ] `kill` diff --git a/system/linux/syscalls/src/ioperm.rs b/system/linux/syscalls/src/ioperm.rs new file mode 100644 index 00000000..4a5c3246 --- /dev/null +++ b/system/linux/syscalls/src/ioperm.rs @@ -0,0 +1,103 @@ +#![cfg(target_arch = "x86")] + +use celer_system_linux_ctypes::UnsignedLong; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`ioperm`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IopermError { + /// `EINVAL`. + Einval, + /// `EPERM`. + Eperm, + /// `ENOMEM`. + Enomem, + /// `ENOSYS`. + Enosys, + /// Another errno returned by the raw syscall. + Other(Errno), +} + +impl IopermError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + Errno::Enomem => Self::Enomem, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Enable or disable access to a range of x86 I/O ports for the calling +/// thread. +/// +/// This safe wrapper preserves the raw port-range arguments and converts the +/// raw `ioperm(2)` return value into `Result<(), IopermError>`. +/// +/// On success, returns `Ok(())` after the kernel has applied the requested +/// permission change to the calling thread. +/// +/// See [`sys::ioperm`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`IopermError::Einval`]: `from + num` overflowed or exceeded the +/// kernel's I/O-permission bitmap range. +/// - [`IopermError::Eperm`]: enabling access was not permitted. +/// - [`IopermError::Enomem`]: the kernel could not allocate or duplicate the +/// per-thread bitmap. +/// - [`IopermError::Enosys`]: the running kernel left this syscall slot +/// unimplemented. +/// - [`IopermError::Other`]: another raw-syscall errno. +pub fn ioperm( + from: UnsignedLong, + num: UnsignedLong, + turn_on: bool, +) -> Result<(), IopermError> { + let ret = sys::ioperm(from, num, turn_on); + + unit_from_ret(ret as isize, IopermError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::UnsignedLong; + + use crate::Errno; + + use super::{IopermError, ioperm}; + + #[test] + fn test_ioperm_zero_length_range() { + let err = ioperm(0 as UnsignedLong, 0 as UnsignedLong, true) + .expect_err("ioperm(0, 0, true) should not succeed"); + + assert!(matches!(err, IopermError::Einval | IopermError::Enosys)); + } + + #[test] + fn test_ioperm_out_of_bounds_range() { + let err = ioperm(65_535 as UnsignedLong, 2 as UnsignedLong, false) + .expect_err("an out-of-bounds ioperm range should not succeed"); + + assert!(matches!(err, IopermError::Einval | IopermError::Enosys)); + } + + #[test] + fn test_ioperm_error_mapping() { + assert_eq!(IopermError::from_errno(Errno::Einval), IopermError::Einval); + assert_eq!(IopermError::from_errno(Errno::Eperm), IopermError::Eperm); + assert_eq!(IopermError::from_errno(Errno::Enomem), IopermError::Enomem); + assert_eq!(IopermError::from_errno(Errno::Enosys), IopermError::Enosys); + assert_eq!( + IopermError::from_errno(Errno::Eio), + IopermError::Other(Errno::Eio) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 099e63e7..7e426bde 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -70,6 +70,8 @@ mod helpers; mod idle; mod init_module; mod ioctl; +#[cfg(target_arch = "x86")] +mod ioperm; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -129,6 +131,8 @@ pub use getuid::getuid16; pub use idle::{IdleError, idle}; pub use init_module::{InitModuleError, init_module}; pub use ioctl::{IoctlError, ioctl}; +#[cfg(target_arch = "x86")] +pub use ioperm::{IopermError, ioperm}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 829ec0b1f1aff3585195827cdaa5db1afe46cee1 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:05:38 +0200 Subject: [PATCH 050/135] feat(syscalls): wrap iopl --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/iopl.rs | 93 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 ++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/iopl.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 73f8f91a..5abb980d 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -143,7 +143,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `init_module` - [x] `ioctl` - [x] `ioperm` - x86 -- [ ] `iopl` - x86 +- [x] `iopl` - x86 - [ ] `ipc` - x86 - [ ] `kill` - [ ] `lchown16` - x86 diff --git a/system/linux/syscalls/src/iopl.rs b/system/linux/syscalls/src/iopl.rs new file mode 100644 index 00000000..faebecad --- /dev/null +++ b/system/linux/syscalls/src/iopl.rs @@ -0,0 +1,93 @@ +#![cfg(target_arch = "x86")] + +use celer_system_linux_ctypes::UnsignedInt; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`iopl`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IoplError { + /// `EINVAL`. + Einval, + /// `EPERM`. + Eperm, + /// `ENOSYS`. + Enosys, + /// Another errno returned by the raw syscall. + Other(Errno), +} + +impl IoplError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Set the calling thread's x86 I/O privilege level. +/// +/// This safe wrapper preserves the raw integer level argument and converts the +/// raw `iopl(2)` return value into `Result<(), IoplError>`. +/// +/// On success, returns `Ok(())` after the kernel has applied the requested +/// I/O privilege level to the calling thread. +/// +/// See [`sys::iopl`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`IoplError::Einval`]: `level` was outside the kernel's accepted range. +/// - [`IoplError::Eperm`]: raising the effective I/O privilege level was not +/// permitted. +/// - [`IoplError::Enosys`]: the running kernel left this syscall slot +/// unimplemented. +/// - [`IoplError::Other`]: another raw-syscall errno. +pub fn iopl(level: UnsignedInt) -> Result<(), IoplError> { + let ret = sys::iopl(level); + + unit_from_ret(ret as isize, IoplError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::UnsignedInt; + + use crate::Errno; + + use super::{IoplError, iopl}; + + #[test] + fn test_iopl_invalid_level() { + let err = + iopl(4 as UnsignedInt).expect_err("iopl(4) should not succeed"); + + assert!(matches!(err, IoplError::Einval | IoplError::Enosys)); + } + + #[test] + fn test_iopl_zero_level() { + match iopl(0 as UnsignedInt) { + Ok(()) => {} + Err(IoplError::Enosys) => {} + Err(other) => panic!("unexpected iopl(0) error: {other:?}"), + } + } + + #[test] + fn test_iopl_error_mapping() { + assert_eq!(IoplError::from_errno(Errno::Einval), IoplError::Einval); + assert_eq!(IoplError::from_errno(Errno::Eperm), IoplError::Eperm); + assert_eq!(IoplError::from_errno(Errno::Enosys), IoplError::Enosys); + assert_eq!( + IoplError::from_errno(Errno::Eio), + IoplError::Other(Errno::Eio) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 7e426bde..181b96be 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -72,6 +72,8 @@ mod init_module; mod ioctl; #[cfg(target_arch = "x86")] mod ioperm; +#[cfg(target_arch = "x86")] +mod iopl; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -133,6 +135,8 @@ pub use init_module::{InitModuleError, init_module}; pub use ioctl::{IoctlError, ioctl}; #[cfg(target_arch = "x86")] pub use ioperm::{IopermError, ioperm}; +#[cfg(target_arch = "x86")] +pub use iopl::{IoplError, iopl}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 1ab45f1c349c88b24ddd2f66c91209aaa172ce82 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:06:14 +0200 Subject: [PATCH 051/135] feat(syscalls): wrap ipc --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/ipc.rs | 110 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 7 ++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/ipc.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 5abb980d..8f9bbe9c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -144,7 +144,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ioctl` - [x] `ioperm` - x86 - [x] `iopl` - x86 -- [ ] `ipc` - x86 +- [x] `ipc` - x86 - [ ] `kill` - [ ] `lchown16` - x86 - [ ] `link` - x86, x86_64 diff --git a/system/linux/syscalls/src/ipc.rs b/system/linux/syscalls/src/ipc.rs new file mode 100644 index 00000000..e972a2ce --- /dev/null +++ b/system/linux/syscalls/src/ipc.rs @@ -0,0 +1,110 @@ +#![cfg(target_arch = "x86")] + +use celer_system_linux_ctypes::{Int, Long, UnsignedInt, Void}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +pub use crate::sys::{ + MSGCTL, MSGGET, MSGRCV, MSGSND, SEMCTL, SEMGET, SEMOP, SHMAT, SHMCTL, + SHMDT, SHMGET, +}; + +/// Errors returned by [`ipc`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IpcError { + /// `EINVAL`. + Einval, + /// `ENOSYS`. + Enosys, + /// Another errno returned by the selected SysV IPC helper. + Other(Errno), +} + +impl IpcError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Dispatch a Linux 1.0 SysV IPC operation through the historical `ipc(2)` +/// multiplexor. +/// +/// This wrapper keeps the original multiplexed ABI shape and maps the raw +/// return value into `Result`. +/// +/// On success, returns the selected helper's nonnegative raw result without +/// further interpretation. +/// +/// See [`sys::ipc`] for kernel behavior, selector semantics, reachable +/// errors, and source references. +/// +/// # Safety +/// - Depending on `call`, `ptr` may be treated as a userspace pointer that the +/// kernel reads from, writes to, or both. The caller must uphold the +/// selected subcall's pointer validity requirements. +/// - When `call == MSGRCV`, `ptr` must point to a readable Linux 1.0 +/// `ipc_kludge` record. +/// - When `call == SHMAT`, `third` is interpreted by the kernel as a user +/// output-pointer argument encoded in the raw multiplexed ABI. +/// +/// # Errors +/// - [`IpcError::Einval`]: `call` was not a supported selector, or the entry +/// path rejected the selected argument shape. +/// - [`IpcError::Enosys`]: the running kernel left SysV IPC support disabled +/// for this multiplexed entry point. +/// - [`IpcError::Other`]: another errno returned by the selected semaphore, +/// message-queue, or shared-memory helper. +pub unsafe fn ipc( + call: UnsignedInt, + first: Int, + second: Int, + third: Int, + ptr: *mut Void, +) -> Result { + // SAFETY: the caller must uphold the selected subcall's pointer and ABI + // requirements for `ptr` and, for `SHMAT`, the integer-encoded output + // pointer carried in `third`. + let ret = unsafe { sys::ipc(call, first, second, third, ptr) }; + + result_from_ret(ret as isize, |ret| ret as Long, IpcError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{IpcError, MSGRCV, ipc}; + + #[test] + fn test_ipc_invalid_selector() { + let err = unsafe { ipc(0, 0, 0, 0, core::ptr::null_mut()) } + .expect_err("selector 0 should be rejected by ipc"); + + assert!(matches!(err, IpcError::Einval | IpcError::Enosys)); + } + + #[test] + fn test_ipc_msgrcv_null_kludge() { + let err = unsafe { ipc(MSGRCV, 0, 0, 0, core::ptr::null_mut()) } + .expect_err("ipc(MSGRCV, ..., null) should be rejected"); + + assert!(matches!(err, IpcError::Einval | IpcError::Enosys)); + } + + #[test] + fn test_ipc_error_mapping() { + assert_eq!(IpcError::from_errno(Errno::Einval), IpcError::Einval); + assert_eq!(IpcError::from_errno(Errno::Enosys), IpcError::Enosys); + assert_eq!( + IpcError::from_errno(Errno::Efault), + IpcError::Other(Errno::Efault) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 181b96be..b7f2034c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -74,6 +74,8 @@ mod ioctl; mod ioperm; #[cfg(target_arch = "x86")] mod iopl; +#[cfg(target_arch = "x86")] +mod ipc; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -137,6 +139,11 @@ pub use ioctl::{IoctlError, ioctl}; pub use ioperm::{IopermError, ioperm}; #[cfg(target_arch = "x86")] pub use iopl::{IoplError, iopl}; +#[cfg(target_arch = "x86")] +pub use ipc::{ + IpcError, MSGCTL, MSGGET, MSGRCV, MSGSND, SEMCTL, SEMGET, SEMOP, SHMAT, + SHMCTL, SHMDT, SHMGET, ipc, +}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 1c38ee85c4a272b2e0f609b7648ed299057b8fce Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:07:13 +0200 Subject: [PATCH 052/135] feat(syscalls): wrap kill --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/kill.rs | 90 ++++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 2 + 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/kill.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 8f9bbe9c..832930bc 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -145,7 +145,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ioperm` - x86 - [x] `iopl` - x86 - [x] `ipc` - x86 -- [ ] `kill` +- [x] `kill` - [ ] `lchown16` - x86 - [ ] `link` - x86, x86_64 - [ ] `lseek` diff --git a/system/linux/syscalls/src/kill.rs b/system/linux/syscalls/src/kill.rs new file mode 100644 index 00000000..f1ccfc2a --- /dev/null +++ b/system/linux/syscalls/src/kill.rs @@ -0,0 +1,90 @@ +use celer_system_linux_ctypes::{Int, PidT}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`kill`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum KillError { + /// `EPERM`. + Eperm, + /// `ESRCH`. + Esrch, + /// `EINVAL`. + Einval, + /// Another errno returned by signal-audit or security-hook checks. + Other(Errno), +} + +impl KillError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Esrch => Self::Esrch, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Send a signal to a process or process group. +/// +/// This safe wrapper preserves the raw `pid` and `sig` selector integers and +/// maps the raw `kill(2)` return value into `Result<(), KillError>`. +/// +/// On success, returns `Ok(())` after the kernel has completed the requested +/// permission and target checks and, when `sig != 0`, queued the signal. +/// +/// See [`sys::kill`] for kernel behavior, historical notes, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`KillError::Eperm`]: the caller was not permitted to signal at least one +/// selected target. +/// - [`KillError::Esrch`]: no matching process or process group could be +/// found. +/// - [`KillError::Einval`]: `sig` was not a valid signal number. +/// - [`KillError::Other`]: another errno returned by signal-audit or +/// security-hook checks. +pub fn kill(pid: PidT, sig: Int) -> Result<(), KillError> { + let ret = sys::kill(pid, sig); + + unit_from_ret(ret as isize, KillError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{Int, PidT}; + + use crate::{Errno, sys}; + + use super::{KillError, kill}; + + #[test] + fn test_kill_signal_zero_self() { + assert_eq!(kill(sys::getpid(), 0 as Int), Ok(())); + } + + #[test] + fn test_kill_invalid_signal() { + assert_eq!(kill(sys::getpid(), Int::MAX), Err(KillError::Einval)); + } + + #[test] + fn test_kill_nonexistent_process() { + assert_eq!(kill(PidT::MAX, 0 as Int), Err(KillError::Esrch)); + } + + #[test] + fn test_kill_error_mapping() { + assert_eq!(KillError::from_errno(Errno::Eperm), KillError::Eperm); + assert_eq!(KillError::from_errno(Errno::Esrch), KillError::Esrch); + assert_eq!(KillError::from_errno(Errno::Einval), KillError::Einval); + assert_eq!( + KillError::from_errno(Errno::Eio), + KillError::Other(Errno::Eio) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b7f2034c..dc643fcc 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -76,6 +76,7 @@ mod ioperm; mod iopl; #[cfg(target_arch = "x86")] mod ipc; +mod kill; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -144,6 +145,7 @@ pub use ipc::{ IpcError, MSGCTL, MSGGET, MSGRCV, MSGSND, SEMCTL, SEMGET, SEMOP, SHMAT, SHMCTL, SHMDT, SHMGET, ipc, }; +pub use kill::{KillError, kill}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 6a547d9eeaf4cb09a97d7bd85c001316753471e9 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:18:50 +0200 Subject: [PATCH 053/135] feat(syscalls): wrap lchown16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lchown.rs | 145 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/lchown.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 832930bc..b7abec8f 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -146,7 +146,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `iopl` - x86 - [x] `ipc` - x86 - [x] `kill` -- [ ] `lchown16` - x86 +- [x] `lchown16` - x86 - [ ] `link` - x86, x86_64 - [ ] `lseek` - [ ] `mknod` - x86, x86_64 diff --git a/system/linux/syscalls/src/lchown.rs b/system/linux/syscalls/src/lchown.rs new file mode 100644 index 00000000..aea37aa9 --- /dev/null +++ b/system/linux/syscalls/src/lchown.rs @@ -0,0 +1,145 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{OldGidT, OldUidT}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`lchown16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Lchown16Error { + /// `ENOENT`. + Enoent, + /// `EPERM`. + Eperm, + /// `EROFS`. + Erofs, + /// Another errno returned by delegated pathname lookup, filesystem, or + /// security-hook ownership-change work. + Other(Errno), +} + +impl Lchown16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enoent => Self::Enoent, + Errno::Eperm => Self::Eperm, + Errno::Erofs => Self::Erofs, + errno => Self::Other(errno), + } + } +} + +/// Change the owner and/or group of `filename` through the legacy x86 +/// `lchown16` ABI without following a final symlink. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, keeps the raw +/// legacy 16-bit owner and group arguments, and maps the raw syscall return +/// value into `Result<(), Lchown16Error>`. +/// +/// Pass `OldUidT::MAX` and/or `OldGidT::MAX` to preserve the existing owner or +/// group respectively. On success, returns `Ok(())` after the kernel has +/// applied the ownership-change request to the path itself rather than the +/// symlink target. +/// +/// See [`sys::lchown16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Lchown16Error::Enoent`]: `filename` does not resolve to an existing path +/// component. +/// - [`Lchown16Error::Eperm`]: the caller was not allowed to make the +/// ownership change. +/// - [`Lchown16Error::Erofs`]: the target inode lives on a read-only +/// filesystem. +/// - [`Lchown16Error::Other`]: delegated pathname lookup, filesystem, or +/// security-hook ownership-change error. +pub fn lchown16( + filename: &CStr, + user: OldUidT, + group: OldGidT, +) -> Result<(), Lchown16Error> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pathname pointer for + // the duration of the call. + let ret = unsafe { sys::lchown16(filename.as_ptr(), user, group) }; + + unit_from_ret(ret as isize, Lchown16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, File}, + os::unix::fs::MetadataExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{OldGidT, OldUidT}; + + use crate::Errno; + + use super::{Lchown16Error, lchown16}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_lchown16_{now}")); + path + } + + #[test] + fn test_lchown16_ok_with_no_change_sentinels() { + let path = temp_path(); + File::create(&path).unwrap(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + let before = fs::metadata(&path).unwrap(); + + assert_eq!( + lchown16(path_c.as_c_str(), OldUidT::MAX, OldGidT::MAX), + Ok(()) + ); + + let after = fs::metadata(&path).unwrap(); + assert_eq!(after.uid(), before.uid()); + assert_eq!(after.gid(), before.gid()); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_lchown16_missing_path_returns_enoent() { + let path = + CString::new("/definitely/not/a/real/celer-lchown16-path").unwrap(); + + assert_eq!( + lchown16(path.as_c_str(), OldUidT::MAX, OldGidT::MAX), + Err(Lchown16Error::Enoent) + ); + } + + #[test] + fn test_lchown16_error_mapping() { + assert_eq!( + Lchown16Error::from_errno(Errno::Enoent), + Lchown16Error::Enoent + ); + assert_eq!( + Lchown16Error::from_errno(Errno::Eperm), + Lchown16Error::Eperm + ); + assert_eq!( + Lchown16Error::from_errno(Errno::Erofs), + Lchown16Error::Erofs + ); + assert_eq!( + Lchown16Error::from_errno(Errno::Einval), + Lchown16Error::Other(Errno::Einval) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index dc643fcc..8e7bc8bd 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -77,6 +77,8 @@ mod iopl; #[cfg(target_arch = "x86")] mod ipc; mod kill; +#[cfg(target_arch = "x86")] +mod lchown; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -146,6 +148,8 @@ pub use ipc::{ SHMCTL, SHMDT, SHMGET, ipc, }; pub use kill::{KillError, kill}; +#[cfg(target_arch = "x86")] +pub use lchown::{Lchown16Error, lchown16}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] From 3023591e74aec5bc413fbcaf3ed4ef468e29d9b8 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:19:46 +0200 Subject: [PATCH 054/135] feat(syscalls): wrap link --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/errno.rs | 3 + system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/link.rs | 116 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/link.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index b7abec8f..bff7f251 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -147,7 +147,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ipc` - x86 - [x] `kill` - [x] `lchown16` - x86 -- [ ] `link` - x86, x86_64 +- [x] `link` - x86, x86_64 - [ ] `lseek` - [ ] `mknod` - x86, x86_64 - [ ] `mkdir` - x86, x86_64 diff --git a/system/linux/syscalls/src/errno.rs b/system/linux/syscalls/src/errno.rs index c470eb5f..14ba655d 100644 --- a/system/linux/syscalls/src/errno.rs +++ b/system/linux/syscalls/src/errno.rs @@ -10,6 +10,7 @@ pub enum Errno { Emfile, Efault, Eexist, + Exdev, Einval, Eintr, Eio, @@ -43,6 +44,7 @@ impl Errno { 24 => Self::Emfile, 14 => Self::Efault, 17 => Self::Eexist, + 18 => Self::Exdev, 22 => Self::Einval, 4 => Self::Eintr, 5 => Self::Eio, @@ -94,6 +96,7 @@ mod tests { assert_eq!(Errno::from_raw(24), Errno::Emfile); assert_eq!(Errno::from_raw(14), Errno::Efault); assert_eq!(Errno::from_raw(17), Errno::Eexist); + assert_eq!(Errno::from_raw(18), Errno::Exdev); assert_eq!(Errno::from_raw(22), Errno::Einval); assert_eq!(Errno::from_raw(4), Errno::Eintr); assert_eq!(Errno::from_raw(5), Errno::Eio); diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 8e7bc8bd..4da716f2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -79,6 +79,8 @@ mod ipc; mod kill; #[cfg(target_arch = "x86")] mod lchown; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod link; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -150,6 +152,8 @@ pub use ipc::{ pub use kill::{KillError, kill}; #[cfg(target_arch = "x86")] pub use lchown::{Lchown16Error, lchown16}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use link::{LinkError, link}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/link.rs b/system/linux/syscalls/src/link.rs new file mode 100644 index 00000000..a1da6717 --- /dev/null +++ b/system/linux/syscalls/src/link.rs @@ -0,0 +1,116 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`link`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LinkError { + /// `EXDEV`. + Exdev, + /// Another errno returned by delegated pathname lookup, hard-link + /// permission checks, filesystem operations, or security hooks. + Other(Errno), +} + +impl LinkError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Exdev => Self::Exdev, + errno => Self::Other(errno), + } + } +} + +/// Create a hard link from `newname` to `oldname`. +/// +/// This safe wrapper takes NUL-terminated [`CStr`] pathnames and maps the raw +/// syscall return value into `Result<(), LinkError>`. +/// +/// On success, returns `Ok(())` after the kernel has created `newname` as +/// another directory entry for the same inode as `oldname`. +/// +/// See [`sys::link`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`LinkError::Exdev`]: `oldname` and `newname` resolve on different mounts. +/// - [`LinkError::Other`]: delegated pathname lookup, hard-link permission, +/// filesystem, or security-hook error. +pub fn link(oldname: &CStr, newname: &CStr) -> Result<(), LinkError> { + // SAFETY: both `CStr` values guarantee valid, NUL-terminated pathname + // pointers for the duration of the call. + let ret = unsafe { sys::link(oldname.as_ptr(), newname.as_ptr()) }; + + unit_from_ret(ret as isize, LinkError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, File}, + os::unix::fs::MetadataExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{LinkError, link}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_link_ok() { + let old_path = temp_path("link_old"); + let new_path = temp_path("link_new"); + File::create(&old_path).unwrap(); + let old_c = + CString::new(old_path.as_os_str().as_encoded_bytes()).unwrap(); + let new_c = + CString::new(new_path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(link(old_c.as_c_str(), new_c.as_c_str()), Ok(())); + + let old_meta = fs::metadata(&old_path).unwrap(); + let new_meta = fs::metadata(&new_path).unwrap(); + assert_eq!(old_meta.ino(), new_meta.ino()); + assert_eq!(old_meta.dev(), new_meta.dev()); + + fs::remove_file(&old_path).unwrap(); + fs::remove_file(&new_path).unwrap(); + } + + #[test] + fn test_link_missing_source() { + let old_path = + CString::new("/definitely/not/a/real/celer-link-source").unwrap(); + let new_path = temp_path("link_missing_new"); + let new_c = + CString::new(new_path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + link(old_path.as_c_str(), new_c.as_c_str()), + Err(LinkError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_link_error_mapping() { + assert_eq!(LinkError::from_errno(Errno::Exdev), LinkError::Exdev); + assert_eq!( + LinkError::from_errno(Errno::Enoent), + LinkError::Other(Errno::Enoent) + ); + } +} From a154fc5c6251d2a0b9a7e1aa1d596ea8bbffb44e Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:20:21 +0200 Subject: [PATCH 055/135] feat(syscalls): wrap lseek --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/lseek.rs | 147 +++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/lseek.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index bff7f251..93e62f59 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -148,7 +148,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `kill` - [x] `lchown16` - x86 - [x] `link` - x86, x86_64 -- [ ] `lseek` +- [x] `lseek` - [ ] `mknod` - x86, x86_64 - [ ] `mkdir` - x86, x86_64 - [ ] `mmap` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 4da716f2..35d0baf5 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -81,6 +81,7 @@ mod kill; mod lchown; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod link; +mod lseek; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -154,6 +155,7 @@ pub use kill::{KillError, kill}; pub use lchown::{Lchown16Error, lchown16}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use link::{LinkError, link}; +pub use lseek::{LseekError, lseek}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/lseek.rs b/system/linux/syscalls/src/lseek.rs new file mode 100644 index 00000000..85f01ed2 --- /dev/null +++ b/system/linux/syscalls/src/lseek.rs @@ -0,0 +1,147 @@ +use celer_system_linux_ctypes::{OffT, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`lseek`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LseekError { + /// `EBADF`. + Ebadf, + /// `EINVAL`. + Einval, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by the underlying file object's seek operation. + Other(Errno), +} + +impl LseekError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Einval => Self::Einval, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Reposition the file offset for the open file descriptor `fd`. +/// +/// This safe wrapper keeps the kernel-facing integer file descriptor, offset, +/// and `whence` selector, and maps the raw syscall return value into +/// `Result`. +/// +/// On success, returns the resulting file offset. +/// +/// See [`sys::lseek`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`LseekError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`LseekError::Einval`]: `whence` is unsupported or the requested file +/// position is invalid. +/// - [`LseekError::Eoverflow`]: the resulting offset does not fit in the raw +/// `off_t` return type. +/// - [`LseekError::Other`]: another error reported by the underlying file +/// object's seek implementation. +pub fn lseek( + fd: UnsignedInt, + offset: OffT, + whence: UnsignedInt, +) -> Result { + let ret = sys::lseek(fd, offset, whence); + + result_from_ret(ret as isize, |ret| ret as OffT, LseekError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::{self, OpenOptions}, + io::{Seek as _, Write as _}, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{OffT, UnsignedInt}; + + use crate::Errno; + + use super::{LseekError, lseek}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_lseek_{now}")); + path + } + + #[test] + fn test_lseek_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + file.write_all(b"hello world").unwrap(); + file.rewind().unwrap(); + + assert_eq!( + lseek(file.as_raw_fd() as UnsignedInt, 0 as OffT, 1), + Ok(0 as OffT) + ); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_lseek_invalid_fd() { + assert_eq!( + lseek(UnsignedInt::MAX, 0 as OffT, 0), + Err(LseekError::Ebadf) + ); + } + + #[test] + fn test_lseek_invalid_whence() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + assert_eq!( + lseek(file.as_raw_fd() as UnsignedInt, 0 as OffT, 99), + Err(LseekError::Einval) + ); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_lseek_error_mapping() { + assert_eq!(LseekError::from_errno(Errno::Ebadf), LseekError::Ebadf); + assert_eq!(LseekError::from_errno(Errno::Einval), LseekError::Einval); + assert_eq!( + LseekError::from_errno(Errno::Eoverflow), + LseekError::Eoverflow + ); + assert_eq!( + LseekError::from_errno(Errno::Enodev), + LseekError::Other(Errno::Enodev) + ); + } +} From c7334c4e0c02db71b197bd39b1a6f8f419cc5101 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:21:03 +0200 Subject: [PATCH 056/135] feat(syscalls): wrap mknod --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/mknod.rs | 120 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/mknod.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 93e62f59..0afc1e0b 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -149,7 +149,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `lchown16` - x86 - [x] `link` - x86, x86_64 - [x] `lseek` -- [ ] `mknod` - x86, x86_64 +- [x] `mknod` - x86, x86_64 - [ ] `mkdir` - x86, x86_64 - [ ] `mmap` - [ ] `modify_ldt` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 35d0baf5..78e7bebc 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -82,6 +82,8 @@ mod lchown; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod link; mod lseek; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod mknod; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -156,6 +158,8 @@ pub use lchown::{Lchown16Error, lchown16}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use link::{LinkError, link}; pub use lseek::{LseekError, lseek}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use mknod::{MknodError, mknod}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/mknod.rs b/system/linux/syscalls/src/mknod.rs new file mode 100644 index 00000000..330bd79d --- /dev/null +++ b/system/linux/syscalls/src/mknod.rs @@ -0,0 +1,120 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{UModeT, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`mknod`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MknodError { + /// `EINVAL`. + Einval, + /// `EPERM`. + Eperm, + /// Another errno returned by delegated pathname lookup, filesystem create + /// operations, or security hooks. + Other(Errno), +} + +impl MknodError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Create the node named by `pathname`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, keeps the raw +/// `mode` and device arguments, and maps the raw syscall return value into +/// `Result<(), MknodError>`. +/// +/// On success, returns `Ok(())` after the kernel has created the requested +/// filesystem node. +/// +/// See [`sys::mknod`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`MknodError::Einval`]: `mode` selects an unsupported node type. +/// - [`MknodError::Eperm`]: `mode` requests a directory through `mknod`, or +/// the caller was not allowed to create the requested node. +/// - [`MknodError::Other`]: delegated pathname lookup, filesystem create, or +/// security-hook error. +pub fn mknod( + pathname: &CStr, + mode: UModeT, + dev: UnsignedInt, +) -> Result<(), MknodError> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pathname pointer for + // the duration of the call. + let ret = unsafe { sys::mknod(pathname.as_ptr(), mode, dev) }; + + unit_from_ret(ret as isize, MknodError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{UModeT, UnsignedInt}; + + use crate::Errno; + + use super::{MknodError, mknod}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_mknod_{now}")); + path + } + + #[test] + fn test_mknod_regular_file_ok() { + let path = temp_path(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + mknod(path_c.as_c_str(), (0o100000 | 0o600) as UModeT, 0), + Ok(()) + ); + + assert!(fs::metadata(&path).unwrap().is_file()); + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_mknod_directory_mode_returns_eperm() { + let path = temp_path(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + mknod(path_c.as_c_str(), 0o040755 as UModeT, 0 as UnsignedInt), + Err(MknodError::Eperm) + ); + } + + #[test] + fn test_mknod_error_mapping() { + assert_eq!(MknodError::from_errno(Errno::Einval), MknodError::Einval); + assert_eq!(MknodError::from_errno(Errno::Eperm), MknodError::Eperm); + assert_eq!( + MknodError::from_errno(Errno::Enoent), + MknodError::Other(Errno::Enoent) + ); + } +} From 4ba61ff1595ebd7b47c75f51e2eeb2d4c3dce98a Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:21:35 +0200 Subject: [PATCH 057/135] feat(syscalls): wrap mkdir --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/mkdir.rs | 101 +++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/mkdir.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 0afc1e0b..83ef2b97 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -150,7 +150,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `link` - x86, x86_64 - [x] `lseek` - [x] `mknod` - x86, x86_64 -- [ ] `mkdir` - x86, x86_64 +- [x] `mkdir` - x86, x86_64 - [ ] `mmap` - [ ] `modify_ldt` - x86 - [ ] `mount` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 78e7bebc..a17a528c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -83,6 +83,8 @@ mod lchown; mod link; mod lseek; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod mkdir; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod mknod; pub mod sys; @@ -159,6 +161,8 @@ pub use lchown::{Lchown16Error, lchown16}; pub use link::{LinkError, link}; pub use lseek::{LseekError, lseek}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use mkdir::{MkdirError, mkdir}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use mknod::{MknodError, mknod}; /// Wrapped historical Linux 1.0 syscall ABIs. diff --git a/system/linux/syscalls/src/mkdir.rs b/system/linux/syscalls/src/mkdir.rs new file mode 100644 index 00000000..5009c475 --- /dev/null +++ b/system/linux/syscalls/src/mkdir.rs @@ -0,0 +1,101 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::UModeT; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`mkdir`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MkdirError { + /// Another errno returned by delegated pathname lookup, directory create + /// operations, or security hooks. + Other(Errno), +} + +impl MkdirError { + fn from_errno(errno: Errno) -> Self { + Self::Other(errno) + } +} + +/// Create a directory named by `pathname`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result<(), MkdirError>`. +/// +/// On success, returns `Ok(())` after the kernel has created the directory. +/// +/// See [`sys::mkdir`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`MkdirError::Other`]: delegated pathname lookup, directory create, or +/// security-hook error. +pub fn mkdir(pathname: &CStr, mode: UModeT) -> Result<(), MkdirError> { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pathname pointer for + // the duration of the call. + let ret = unsafe { sys::mkdir(pathname.as_ptr(), mode) }; + + unit_from_ret(ret as isize, MkdirError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::UModeT; + + use crate::Errno; + + use super::{MkdirError, mkdir}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_mkdir_{now}")); + path + } + + #[test] + fn test_mkdir_ok() { + let path = temp_path(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(mkdir(path_c.as_c_str(), 0o700 as UModeT), Ok(())); + + assert!(fs::metadata(&path).unwrap().is_dir()); + fs::remove_dir(&path).unwrap(); + } + + #[test] + fn test_mkdir_existing_path() { + let path = temp_path(); + fs::create_dir(&path).unwrap(); + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + mkdir(path_c.as_c_str(), 0o700 as UModeT), + Err(MkdirError::Other(Errno::Eexist)) + ); + + fs::remove_dir(&path).unwrap(); + } + + #[test] + fn test_mkdir_error_mapping() { + assert_eq!( + MkdirError::from_errno(Errno::Enoent), + MkdirError::Other(Errno::Enoent) + ); + } +} From fea0aa3d9c0dda888147c58164f712c453a68200 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:36:59 +0200 Subject: [PATCH 058/135] feat(syscalls): wrap mmap --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/mmap.rs | 148 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/mmap.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 83ef2b97..3f220386 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -151,7 +151,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `lseek` - [x] `mknod` - x86, x86_64 - [x] `mkdir` - x86, x86_64 -- [ ] `mmap` +- [x] `mmap` - [ ] `modify_ldt` - x86 - [ ] `mount` - [ ] `munmap` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index a17a528c..75248fbd 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -86,6 +86,7 @@ mod lseek; mod mkdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod mknod; +mod mmap; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -164,6 +165,7 @@ pub use lseek::{LseekError, lseek}; pub use mkdir::{MkdirError, mkdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use mknod::{MknodError, mknod}; +pub use mmap::{MmapError, mmap}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/mmap.rs b/system/linux/syscalls/src/mmap.rs new file mode 100644 index 00000000..16943e3a --- /dev/null +++ b/system/linux/syscalls/src/mmap.rs @@ -0,0 +1,148 @@ +use celer_system_linux_ctypes::{SizeT, UnsignedInt, UnsignedLong, Void}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`mmap`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MmapError { + /// `EACCES`. + Eacces, + /// `EBADF`. + Ebadf, + /// `EINVAL`. + Einval, + /// `ENODEV`. + Enodev, + /// `ENOMEM`. + Enomem, + /// `ENOEXEC`. + Enoexec, + /// Another errno returned by delegated file, filesystem, driver, or + /// security-hook mapping code. + Other(Errno), +} + +impl MmapError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eacces => Self::Eacces, + Errno::Ebadf => Self::Ebadf, + Errno::Einval => Self::Einval, + Errno::Enodev => Self::Enodev, + Errno::Enomem => Self::Enomem, + Errno::Enoexec => Self::Enoexec, + errno => Self::Other(errno), + } + } +} + +/// Create a memory mapping. +/// +/// This wrapper keeps the raw `mmap(addr, len, prot, flags, fd, offset)` shape +/// and maps the address-valued raw return into `Result<*mut Void, MmapError>`. +/// +/// On success, returns the mapped address chosen by the kernel. +/// +/// See [`sys::mmap`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// The caller must ensure the requested mapping operation does not invalidate +/// live Rust references, pointers whose pointees must remain valid, or +/// allocator assumptions in the current process. +/// +/// # Errors +/// - [`MmapError::Eacces`]: the requested mapping conflicts with file access. +/// - [`MmapError::Ebadf`]: `fd` is not an open file descriptor for a +/// non-anonymous mapping. +/// - [`MmapError::Einval`]: the range, flags, protection, address, or offset +/// is invalid. +/// - [`MmapError::Enodev`]: the target file does not support mappings. +/// - [`MmapError::Enomem`]: the kernel could not allocate or find the mapping. +/// - [`MmapError::Enoexec`]: a historical filesystem mapper rejected the inode. +/// - [`MmapError::Other`]: delegated file, filesystem, driver, or +/// security-hook mapping error. +pub unsafe fn mmap( + addr: *mut Void, + len: SizeT, + prot: UnsignedLong, + flags: UnsignedLong, + fd: UnsignedInt, + offset: UnsignedLong, +) -> Result<*mut Void, MmapError> { + // SAFETY: the caller upholds the process-memory invariants required by + // this mapping operation. + let ret = unsafe { sys::mmap(addr, len, prot, flags, fd, offset) }; + + result_from_ret(ret as isize, |ret| ret as *mut Void, MmapError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::ptr; + + use celer_system_linux_ctypes::{SizeT, UnsignedInt, UnsignedLong}; + + use crate::Errno; + + use super::{MmapError, mmap}; + use crate::munmap; + + const PROT_READ: UnsignedLong = 0x1; + const PROT_WRITE: UnsignedLong = 0x2; + const MAP_PRIVATE: UnsignedLong = 0x02; + const MAP_ANONYMOUS: UnsignedLong = 0x20; + + #[test] + fn test_mmap_anonymous_ok() { + let len = 4096 as SizeT; + + let mapping = unsafe { + mmap( + ptr::null_mut(), + len, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + 0 as UnsignedInt, + 0, + ) + } + .expect("anonymous mmap should succeed"); + + unsafe { munmap(mapping, len).expect("munmap should succeed") }; + } + + #[test] + fn test_mmap_bad_fd() { + let err = unsafe { + mmap( + ptr::null_mut(), + 4096 as SizeT, + PROT_READ, + MAP_PRIVATE, + UnsignedInt::MAX, + 0, + ) + } + .unwrap_err(); + + assert_eq!(err, MmapError::Ebadf); + } + + #[test] + fn test_mmap_error_mapping() { + assert_eq!(MmapError::from_errno(Errno::Eacces), MmapError::Eacces); + assert_eq!(MmapError::from_errno(Errno::Ebadf), MmapError::Ebadf); + assert_eq!(MmapError::from_errno(Errno::Einval), MmapError::Einval); + assert_eq!(MmapError::from_errno(Errno::Enodev), MmapError::Enodev); + assert_eq!(MmapError::from_errno(Errno::Enomem), MmapError::Enomem); + assert_eq!(MmapError::from_errno(Errno::Enoexec), MmapError::Enoexec); + assert_eq!( + MmapError::from_errno(Errno::Eio), + MmapError::Other(Errno::Eio) + ); + } +} From a826797b8eb4654db4c88c93d82a82674a6e9563 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:37:12 +0200 Subject: [PATCH 059/135] feat(syscalls): wrap modify_ldt --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/modify_ldt.rs | 125 ++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/modify_ldt.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 3f220386..c28e1e14 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -152,7 +152,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `mknod` - x86, x86_64 - [x] `mkdir` - x86, x86_64 - [x] `mmap` -- [ ] `modify_ldt` - x86 +- [x] `modify_ldt` - x86 - [ ] `mount` - [ ] `munmap` - [ ] `newfstat` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 75248fbd..23a6770f 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -87,6 +87,8 @@ mod mkdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod mknod; mod mmap; +#[cfg(target_arch = "x86")] +mod modify_ldt; pub mod sys; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -166,6 +168,8 @@ pub use mkdir::{MkdirError, mkdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use mknod::{MknodError, mknod}; pub use mmap::{MmapError, mmap}; +#[cfg(target_arch = "x86")] +pub use modify_ldt::{ModifyLdtError, modify_ldt}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/modify_ldt.rs b/system/linux/syscalls/src/modify_ldt.rs new file mode 100644 index 00000000..04ecc27c --- /dev/null +++ b/system/linux/syscalls/src/modify_ldt.rs @@ -0,0 +1,125 @@ +use celer_system_linux_ctypes::{Int, UnsignedLong, Void}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`modify_ldt`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ModifyLdtError { + /// `EINVAL`. + Einval, + /// `EFAULT`. + Efault, + /// `ENOMEM`. + Enomem, + /// `EINTR`. + Eintr, + /// `ENOSYS`. + Enosys, + /// Another errno returned by current x86 LDT support. + Other(Errno), +} + +impl ModifyLdtError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + Errno::Enomem => Self::Enomem, + Errno::Eintr => Self::Eintr, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Read or update the calling task's x86 local descriptor table state. +/// +/// This wrapper preserves the raw `func`, pointer, and byte-count arguments +/// because the pointer direction depends on `func`, and maps the raw return +/// into `Result`. +/// +/// On success, returns the kernel byte count for read functions or `0` for +/// write functions. +/// +/// See [`sys::modify_ldt`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// The caller must ensure `ptr` is valid for the selected `func`: writable for +/// read functions, readable for write functions, and non-aliasing for any +/// kernel writes performed during the syscall. +/// +/// # Errors +/// - [`ModifyLdtError::Einval`]: invalid function-specific LDT data. +/// - [`ModifyLdtError::Efault`]: the user buffer is not accessible. +/// - [`ModifyLdtError::Enomem`]: the kernel could not allocate LDT state. +/// - [`ModifyLdtError::Eintr`]: the write path was interrupted. +/// - [`ModifyLdtError::Enosys`]: the function or syscall is not supported. +/// - [`ModifyLdtError::Other`]: another errno from current x86 LDT support. +#[cfg(target_arch = "x86")] +pub unsafe fn modify_ldt( + func: Int, + ptr: *mut Void, + bytecount: UnsignedLong, +) -> Result { + // SAFETY: the caller upholds the function-specific pointer contract. + let ret = unsafe { sys::modify_ldt(func, ptr, bytecount) }; + + result_from_ret(ret as isize, |ret| ret as Int, ModifyLdtError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{Int, UnsignedLong}; + + use crate::Errno; + + use super::{ModifyLdtError, modify_ldt}; + + #[test] + fn test_modify_ldt_zero_length_read() { + let got = unsafe { modify_ldt(2 as Int, core::ptr::null_mut(), 0) }; + + assert!(matches!(got, Ok(0) | Err(ModifyLdtError::Enosys))); + } + + #[test] + fn test_modify_ldt_unsupported_func() { + let got = unsafe { + modify_ldt(Int::MAX, core::ptr::null_mut(), 0 as UnsignedLong) + }; + + assert_eq!(got, Err(ModifyLdtError::Enosys)); + } + + #[test] + fn test_modify_ldt_error_mapping() { + assert_eq!( + ModifyLdtError::from_errno(Errno::Einval), + ModifyLdtError::Einval + ); + assert_eq!( + ModifyLdtError::from_errno(Errno::Efault), + ModifyLdtError::Efault + ); + assert_eq!( + ModifyLdtError::from_errno(Errno::Enomem), + ModifyLdtError::Enomem + ); + assert_eq!( + ModifyLdtError::from_errno(Errno::Eintr), + ModifyLdtError::Eintr + ); + assert_eq!( + ModifyLdtError::from_errno(Errno::Enosys), + ModifyLdtError::Enosys + ); + assert_eq!( + ModifyLdtError::from_errno(Errno::Eio), + ModifyLdtError::Other(Errno::Eio) + ); + } +} From 160cc1efe2d2efc67be6cc0571946cad16a51689 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:37:43 +0200 Subject: [PATCH 060/135] feat(syscalls): wrap mount --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/mount.rs | 154 +++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/mount.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c28e1e14..d057e158 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -153,7 +153,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `mkdir` - x86, x86_64 - [x] `mmap` - [x] `modify_ldt` - x86 -- [ ] `mount` +- [x] `mount` - [ ] `munmap` - [ ] `newfstat` - [ ] `newlstat` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 23a6770f..5c9ec28c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -90,6 +90,7 @@ mod mmap; #[cfg(target_arch = "x86")] mod modify_ldt; pub mod sys; +mod mount; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; @@ -171,6 +172,7 @@ pub use mmap::{MmapError, mmap}; #[cfg(target_arch = "x86")] pub use modify_ldt::{ModifyLdtError, modify_ldt}; +pub use mount::{MountError, mount}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { diff --git a/system/linux/syscalls/src/mount.rs b/system/linux/syscalls/src/mount.rs new file mode 100644 index 00000000..1b830d8d --- /dev/null +++ b/system/linux/syscalls/src/mount.rs @@ -0,0 +1,154 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{UnsignedLong, Void}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`mount`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MountError { + /// `EPERM`. + Eperm, + /// `EFAULT`. + Efault, + /// `ENOMEM`. + Enomem, + /// `ENODEV`. + Enodev, + /// `ENOTBLK`. + Enotblk, + /// `EACCES`. + Eacces, + /// `ENXIO`. + Enxio, + /// `EMFILE`. + Emfile, + /// `EBUSY`. + Ebusy, + /// `EINVAL`. + Einval, + /// Another errno returned by pathname lookup, block-driver, filesystem, or + /// security-hook mount code. + Other(Errno), +} + +impl MountError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + Errno::Enomem => Self::Enomem, + Errno::Enodev => Self::Enodev, + Errno::Raw(15) => Self::Enotblk, + Errno::Eacces => Self::Eacces, + Errno::Raw(6) => Self::Enxio, + Errno::Emfile => Self::Emfile, + Errno::Ebusy => Self::Ebusy, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Mount or remount a filesystem. +/// +/// This wrapper takes `target` as a required NUL-terminated [`CStr`], accepts +/// nullable `source` and `filesystemtype` strings as `Option<&CStr>`, keeps the +/// raw `mountflags` and filesystem-specific `data` pointer, and maps the raw +/// return into `Result<(), MountError>`. +/// +/// On success, the requested mount operation has completed. +/// +/// See [`sys::mount`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// The caller must ensure `data`, when non-null, satisfies the memory contract +/// required by the selected filesystem and mount flags for the duration of the +/// syscall. +/// +/// # Errors +/// - [`MountError::Eperm`]: the caller lacks permission, or historical kernels +/// rejected the target type. +/// - [`MountError::Efault`]: a copied string or data pointer was inaccessible. +/// - [`MountError::Enomem`]: the kernel could not allocate temporary storage. +/// - [`MountError::Enodev`]: the filesystem type is unavailable. +/// - [`MountError::Enotblk`]: a block-device-backed filesystem got a +/// non-block source. +/// - [`MountError::Eacces`]: access to the source or traversal was denied. +/// - [`MountError::Enxio`]: the source block device number is invalid. +/// - [`MountError::Emfile`]: no unnamed device number was available. +/// - [`MountError::Ebusy`]: the target or source was busy. +/// - [`MountError::Einval`]: remount validation failed. +/// - [`MountError::Other`]: delegated pathname lookup, block-driver, +/// filesystem, or security-hook mount error. +pub unsafe fn mount( + source: Option<&CStr>, + target: &CStr, + filesystemtype: Option<&CStr>, + mountflags: UnsignedLong, + data: *const Void, +) -> Result<(), MountError> { + // SAFETY: `CStr` values are valid NUL-terminated strings, null is passed + // only for optional strings, and the caller upholds `data`'s contract. + let ret = unsafe { + sys::mount( + source.map_or(core::ptr::null(), CStr::as_ptr), + target.as_ptr(), + filesystemtype.map_or(core::ptr::null(), CStr::as_ptr), + mountflags, + data, + ) + }; + + unit_from_ret(ret as isize, MountError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use crate::Errno; + + use super::{MountError, mount}; + + #[test] + fn test_mount_requires_permission_or_valid_target() { + let target = CString::new("/").unwrap(); + let fstype = CString::new("definitely-not-a-celer-fs").unwrap(); + + let err = unsafe { + mount( + None, + target.as_c_str(), + Some(fstype.as_c_str()), + 0, + core::ptr::null(), + ) + } + .unwrap_err(); + + assert!(matches!(err, MountError::Eperm | MountError::Enodev)); + } + + #[test] + fn test_mount_error_mapping() { + assert_eq!(MountError::from_errno(Errno::Eperm), MountError::Eperm); + assert_eq!(MountError::from_errno(Errno::Efault), MountError::Efault); + assert_eq!(MountError::from_errno(Errno::Enomem), MountError::Enomem); + assert_eq!(MountError::from_errno(Errno::Enodev), MountError::Enodev); + assert_eq!(MountError::from_errno(Errno::Raw(15)), MountError::Enotblk); + assert_eq!(MountError::from_errno(Errno::Eacces), MountError::Eacces); + assert_eq!(MountError::from_errno(Errno::Raw(6)), MountError::Enxio); + assert_eq!(MountError::from_errno(Errno::Emfile), MountError::Emfile); + assert_eq!(MountError::from_errno(Errno::Ebusy), MountError::Ebusy); + assert_eq!(MountError::from_errno(Errno::Einval), MountError::Einval); + assert_eq!( + MountError::from_errno(Errno::Enoent), + MountError::Other(Errno::Enoent) + ); + } +} From 4c428d0019600b7138d36052c63f017a56de89c4 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:38:27 +0200 Subject: [PATCH 061/135] feat(syscalls): wrap munmap --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/munmap.rs | 104 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/munmap.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index d057e158..6c3e4e98 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -154,7 +154,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `mmap` - [x] `modify_ldt` - x86 - [x] `mount` -- [ ] `munmap` +- [x] `munmap` - [ ] `newfstat` - [ ] `newlstat` - x86, x86_64 - [ ] `newuname` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 5c9ec28c..c8273b6c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -91,6 +91,7 @@ mod mmap; mod modify_ldt; pub mod sys; mod mount; +mod munmap; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; @@ -173,6 +174,7 @@ pub use mmap::{MmapError, mmap}; pub use modify_ldt::{ModifyLdtError, modify_ldt}; pub use mount::{MountError, mount}; +pub use munmap::{MunmapError, munmap}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { diff --git a/system/linux/syscalls/src/munmap.rs b/system/linux/syscalls/src/munmap.rs new file mode 100644 index 00000000..ae571863 --- /dev/null +++ b/system/linux/syscalls/src/munmap.rs @@ -0,0 +1,104 @@ +use celer_system_linux_ctypes::{SizeT, Void}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`munmap`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MunmapError { + /// `EINVAL`. + Einval, + /// Another errno returned by unmapping implementation details. + Other(Errno), +} + +impl MunmapError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Remove mappings in the supplied address range. +/// +/// This wrapper takes the start address as a pointer, keeps the raw length +/// argument, and maps the raw return into `Result<(), MunmapError>`. +/// +/// On success, the kernel has removed mappings covering the requested range. +/// +/// See [`sys::munmap`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// The caller must ensure unmapping this range does not invalidate live Rust +/// references, pointers whose pointees must remain valid, or allocator +/// assumptions in the current process. +/// +/// # Errors +/// - [`MunmapError::Einval`]: the address or length is invalid. +/// - [`MunmapError::Other`]: another errno from unmapping implementation +/// details. +pub unsafe fn munmap(addr: *mut Void, len: SizeT) -> Result<(), MunmapError> { + // SAFETY: the caller upholds the process-memory invariants required by + // this unmapping operation. + let ret = unsafe { sys::munmap(addr.addr() as _, len) }; + + unit_from_ret(ret as isize, MunmapError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::ptr; + + use celer_system_linux_ctypes::{SizeT, UnsignedInt, UnsignedLong, Void}; + + use crate::Errno; + + use super::{MunmapError, munmap}; + use crate::mmap; + + const PROT_READ: UnsignedLong = 0x1; + const PROT_WRITE: UnsignedLong = 0x2; + const MAP_PRIVATE: UnsignedLong = 0x02; + const MAP_ANONYMOUS: UnsignedLong = 0x20; + + #[test] + fn test_munmap_ok() { + let len = 4096 as SizeT; + let mapping = unsafe { + mmap( + ptr::null_mut(), + len, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + 0 as UnsignedInt, + 0, + ) + } + .expect("anonymous mmap should succeed"); + + unsafe { munmap(mapping, len).expect("munmap should succeed") }; + } + + #[test] + fn test_munmap_unaligned_addr_returns_einval() { + let err = + unsafe { munmap(ptr::without_provenance_mut::(1), 4096) } + .unwrap_err(); + + assert_eq!(err, MunmapError::Einval); + } + + #[test] + fn test_munmap_error_mapping() { + assert_eq!(MunmapError::from_errno(Errno::Einval), MunmapError::Einval); + assert_eq!( + MunmapError::from_errno(Errno::Enomem), + MunmapError::Other(Errno::Enomem) + ); + } +} From bf796a61ca14a367e2b1ae3ee7096180a3287dd3 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:38:38 +0200 Subject: [PATCH 062/135] feat(syscalls): wrap newfstat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 3 + system/linux/syscalls/src/newfstat.rs | 142 +++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/newfstat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 6c3e4e98..5c1f329a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -155,7 +155,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `modify_ldt` - x86 - [x] `mount` - [x] `munmap` -- [ ] `newfstat` +- [x] `newfstat` - [ ] `newlstat` - x86, x86_64 - [ ] `newuname` - [ ] `nice` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c8273b6c..a25622d6 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -92,6 +92,7 @@ mod modify_ldt; pub mod sys; mod mount; mod munmap; +mod newfstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; @@ -175,6 +176,7 @@ pub use modify_ldt::{ModifyLdtError, modify_ldt}; pub use mount::{MountError, mount}; pub use munmap::{MunmapError, munmap}; +pub use newfstat::{NewfstatError, newfstat}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { @@ -183,4 +185,5 @@ pub mod linux_1_0 { pub use super::ftruncate::{ Ftruncate1_0Error as FtruncateError, ftruncate_1_0 as ftruncate, }; + pub use super::newfstat::{NewfstatError, newfstat_1_0 as newfstat}; } diff --git a/system/linux/syscalls/src/newfstat.rs b/system/linux/syscalls/src/newfstat.rs new file mode 100644 index 00000000..f074ab05 --- /dev/null +++ b/system/linux/syscalls/src/newfstat.rs @@ -0,0 +1,142 @@ +use core::mem::MaybeUninit; + +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::NewStat as NativeStat; +#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] +use celer_system_linux_ctypes::Stat64 as NativeStat; +use celer_system_linux_ctypes::UnsignedInt; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::NewStat as Linux10NewStat; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`newfstat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NewfstatError { + /// `EFAULT`. + Efault, + /// `EBADF`. + Ebadf, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated file status code. + Other(Errno), +} + +impl NewfstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Ebadf => Self::Ebadf, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Get file status information for an open file descriptor. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), NewfstatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the open +/// file referenced by `fd`. +/// +/// See [`sys::newfstat`] for kernel behavior, reachable errors, ABI layouts, +/// and source references. +/// +/// # Errors +/// - [`NewfstatError::Efault`]: the kernel could not write the output buffer. +/// - [`NewfstatError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`NewfstatError::Eoverflow`]: metadata could not be represented in the +/// native stat layout. +/// - [`NewfstatError::Other`]: delegated file status error. +pub fn newfstat( + fd: UnsignedInt, + statbuf: &mut MaybeUninit, +) -> Result<(), NewfstatError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // native stat value. + let ret = unsafe { sys::newfstat(fd, statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, NewfstatError::from_errno) +} + +/// Get file status information through the Linux 1.0 `sys_newfstat` ABI. +/// +/// This safe wrapper mirrors [`newfstat`], but uses +/// `&mut MaybeUninit` for the Linux 1.0 layout exposed from +/// [`crate::linux_1_0`]. +/// +/// See [`sys::linux_1_0::newfstat`] for historical kernel behavior and source +/// references. +#[cfg(target_arch = "x86")] +pub fn newfstat_1_0( + fd: UnsignedInt, + statbuf: &mut MaybeUninit, +) -> Result<(), NewfstatError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // Linux 1.0 stat value. + let ret = unsafe { sys::linux_1_0::newfstat(fd, statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, NewfstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{fs::File, mem::MaybeUninit, os::fd::AsRawFd as _}; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::newfstat_1_0; + use super::{NewfstatError, newfstat}; + + #[test] + fn test_newfstat_ok() { + let file = File::open("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(newfstat(file.as_raw_fd() as u32, &mut statbuf), Ok(())); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_newfstat_1_0_ok() { + let file = File::open("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(newfstat_1_0(file.as_raw_fd() as u32, &mut statbuf), Ok(())); + } + + #[test] + fn test_newfstat_invalid_fd() { + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(newfstat(u32::MAX, &mut statbuf), Err(NewfstatError::Ebadf)); + } + + #[test] + fn test_newfstat_error_mapping() { + assert_eq!( + NewfstatError::from_errno(Errno::Efault), + NewfstatError::Efault + ); + assert_eq!( + NewfstatError::from_errno(Errno::Ebadf), + NewfstatError::Ebadf + ); + assert_eq!( + NewfstatError::from_errno(Errno::Eoverflow), + NewfstatError::Eoverflow + ); + assert_eq!( + NewfstatError::from_errno(Errno::Eio), + NewfstatError::Other(Errno::Eio) + ); + } +} From 5b8a0b38afacdc6648903be6acbb94904b08e7f9 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:38:52 +0200 Subject: [PATCH 063/135] feat(syscalls): wrap newlstat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 5 + system/linux/syscalls/src/newlstat.rs | 182 +++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/newlstat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 5c1f329a..a9b50a44 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -156,7 +156,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `mount` - [x] `munmap` - [x] `newfstat` -- [ ] `newlstat` - x86, x86_64 +- [x] `newlstat` - x86, x86_64 - [ ] `newuname` - [ ] `nice` - x86 - [ ] `oldfstat` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index a25622d6..25a932cf 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -93,6 +93,8 @@ pub mod sys; mod mount; mod munmap; mod newfstat; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod newlstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; @@ -177,6 +179,8 @@ pub use modify_ldt::{ModifyLdtError, modify_ldt}; pub use mount::{MountError, mount}; pub use munmap::{MunmapError, munmap}; pub use newfstat::{NewfstatError, newfstat}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use newlstat::{NewlstatError, newlstat}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { @@ -186,4 +190,5 @@ pub mod linux_1_0 { Ftruncate1_0Error as FtruncateError, ftruncate_1_0 as ftruncate, }; pub use super::newfstat::{NewfstatError, newfstat_1_0 as newfstat}; + pub use super::newlstat::{NewlstatError, newlstat_1_0 as newlstat}; } diff --git a/system/linux/syscalls/src/newlstat.rs b/system/linux/syscalls/src/newlstat.rs new file mode 100644 index 00000000..5a2dd41d --- /dev/null +++ b/system/linux/syscalls/src/newlstat.rs @@ -0,0 +1,182 @@ +use core::{ffi::CStr, mem::MaybeUninit}; + +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::NewStat as NativeStat; +#[cfg(target_arch = "x86_64")] +use celer_system_linux_ctypes::Stat64 as NativeStat; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::NewStat as Linux10NewStat; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`newlstat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NewlstatError { + /// `EFAULT`. + Efault, + /// `ENAMETOOLONG`. + Enametoolong, + /// `ENOENT`. + Enoent, + /// `ENOMEM`. + Enomem, + /// `ENOTDIR`. + Enotdir, + /// `EACCES`. + Eacces, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +impl NewlstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enomem => Self::Enomem, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Get file status information for a path without following the final symlink. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer with `&mut MaybeUninit`, and maps the raw syscall +/// return into `Result<(), NewlstatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the +/// resolved path. +/// +/// See [`sys::newlstat`] for kernel behavior, reachable errors, ABI layouts, +/// and source references. +/// +/// # Errors +/// - [`NewlstatError::Efault`]: the kernel could not read the path or write +/// the output buffer. +/// - [`NewlstatError::Enametoolong`]: the pathname is too long. +/// - [`NewlstatError::Enoent`]: the path is empty or missing. +/// - [`NewlstatError::Enomem`]: the kernel could not allocate pathname storage. +/// - [`NewlstatError::Enotdir`]: traversal needed a directory. +/// - [`NewlstatError::Eacces`]: traversal lacked search permission. +/// - [`NewlstatError::Eoverflow`]: metadata could not be represented in the +/// native stat layout. +/// - [`NewlstatError::Other`]: delegated pathname lookup or filesystem error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn newlstat( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), NewlstatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one stat value. + let ret = unsafe { sys::newlstat(filename.as_ptr(), statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, NewlstatError::from_errno) +} + +/// Get file status information through the Linux 1.0 `sys_newlstat` ABI. +/// +/// This safe wrapper mirrors [`newlstat`], but uses +/// `&mut MaybeUninit` for the Linux 1.0 layout exposed from +/// [`crate::linux_1_0`]. +/// +/// See [`sys::linux_1_0::newlstat`] for historical kernel behavior and source +/// references. +#[cfg(target_arch = "x86")] +pub fn newlstat_1_0( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), NewlstatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one Linux + // 1.0 stat value. + let ret = unsafe { + sys::linux_1_0::newlstat(filename.as_ptr(), statbuf.as_mut_ptr()) + }; + + unit_from_ret(ret as isize, NewlstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ffi::CString, mem::MaybeUninit}; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::newlstat_1_0; + use super::{NewlstatError, newlstat}; + + #[test] + fn test_newlstat_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(newlstat(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_newlstat_1_0_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(newlstat_1_0(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[test] + fn test_newlstat_missing_path() { + let path = CString::new("/celer-newlstat-definitely-missing").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!( + newlstat(path.as_c_str(), &mut statbuf), + Err(NewlstatError::Enoent) + ); + } + + #[test] + fn test_newlstat_error_mapping() { + assert_eq!( + NewlstatError::from_errno(Errno::Efault), + NewlstatError::Efault + ); + assert_eq!( + NewlstatError::from_errno(Errno::Raw(36)), + NewlstatError::Enametoolong + ); + assert_eq!( + NewlstatError::from_errno(Errno::Enoent), + NewlstatError::Enoent + ); + assert_eq!( + NewlstatError::from_errno(Errno::Enomem), + NewlstatError::Enomem + ); + assert_eq!( + NewlstatError::from_errno(Errno::Enotdir), + NewlstatError::Enotdir + ); + assert_eq!( + NewlstatError::from_errno(Errno::Eacces), + NewlstatError::Eacces + ); + assert_eq!( + NewlstatError::from_errno(Errno::Eoverflow), + NewlstatError::Eoverflow + ); + assert_eq!( + NewlstatError::from_errno(Errno::Eio), + NewlstatError::Other(Errno::Eio) + ); + } +} From a2dd337bd61772604fc743a93f6e823694228911 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:39:05 +0200 Subject: [PATCH 064/135] feat(syscalls): wrap newuname --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/uname.rs | 87 ++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/uname.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a9b50a44..ae943983 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -157,7 +157,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `munmap` - [x] `newfstat` - [x] `newlstat` - x86, x86_64 -- [ ] `newuname` +- [x] `newuname` - [ ] `nice` - x86 - [ ] `oldfstat` - x86 - [ ] `oldlstat` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 25a932cf..c8bc5670 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -96,6 +96,7 @@ mod newfstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod newlstat; +mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; pub use acct::{AcctError, acct}; @@ -181,6 +182,7 @@ pub use munmap::{MunmapError, munmap}; pub use newfstat::{NewfstatError, newfstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use newlstat::{NewlstatError, newlstat}; +pub use uname::{NewunameError, newuname}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { diff --git a/system/linux/syscalls/src/uname.rs b/system/linux/syscalls/src/uname.rs new file mode 100644 index 00000000..8fb2c408 --- /dev/null +++ b/system/linux/syscalls/src/uname.rs @@ -0,0 +1,87 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::NewUtsname; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`newuname`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NewunameError { + /// `EFAULT`. + Efault, + /// Another errno returned by uname compatibility handling. + Other(Errno), +} + +impl NewunameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Copy the system identity strings into `name`. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), NewunameError>`. +/// +/// On success, the kernel has initialized `name` with the six-field +/// `NewUtsname` record. +/// +/// See [`sys::newuname`] for kernel behavior, reachable errors, ABI layout, +/// and source references. +/// +/// # Errors +/// - [`NewunameError::Efault`]: the kernel could not write the output buffer. +/// - [`NewunameError::Other`]: another errno from uname compatibility handling. +pub fn newuname( + name: &mut MaybeUninit, +) -> Result<(), NewunameError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // kernel-initialized uname record. + let ret = unsafe { sys::newuname(name.as_mut_ptr()) }; + + unit_from_ret(ret as isize, NewunameError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::mem::MaybeUninit; + + use crate::Errno; + + use super::{NewunameError, newuname}; + + #[test] + fn test_newuname_ok() { + let mut name = MaybeUninit::uninit(); + + assert_eq!(newuname(&mut name), Ok(())); + let name = unsafe { name.assume_init() }; + let sysname = name + .sysname + .iter() + .take(5) + .map(|byte| byte.to_ne_bytes()[0]) + .collect::>(); + assert_eq!(sysname, b"Linux"); + } + + #[test] + fn test_newuname_error_mapping() { + assert_eq!( + NewunameError::from_errno(Errno::Efault), + NewunameError::Efault + ); + assert_eq!( + NewunameError::from_errno(Errno::Eio), + NewunameError::Other(Errno::Eio) + ); + } +} From d3cc0670c058929132a9028a3edb18253fcb64f1 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:39:15 +0200 Subject: [PATCH 065/135] feat(syscalls): wrap nice --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/nice.rs | 68 ++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/nice.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index ae943983..81fdd242 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -158,7 +158,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `newfstat` - [x] `newlstat` - x86, x86_64 - [x] `newuname` -- [ ] `nice` - x86 +- [x] `nice` - x86 - [ ] `oldfstat` - x86 - [ ] `oldlstat` - x86 - [ ] `oldolduname` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c8bc5670..34c5eede 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -95,6 +95,8 @@ mod munmap; mod newfstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod newlstat; +#[cfg(target_arch = "x86")] +mod nice; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -182,6 +184,8 @@ pub use munmap::{MunmapError, munmap}; pub use newfstat::{NewfstatError, newfstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use newlstat::{NewlstatError, newlstat}; +#[cfg(target_arch = "x86")] +pub use nice::{NiceError, nice}; pub use uname::{NewunameError, newuname}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/nice.rs b/system/linux/syscalls/src/nice.rs new file mode 100644 index 00000000..7ca960b2 --- /dev/null +++ b/system/linux/syscalls/src/nice.rs @@ -0,0 +1,68 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`nice`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NiceError { + /// `EPERM`. + Eperm, + /// Another errno returned by security hooks. + Other(Errno), +} + +impl NiceError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Change the current process nice value by a relative increment. +/// +/// This safe wrapper keeps the raw increment argument and maps the raw syscall +/// return into `Result<(), NiceError>`. +/// +/// On success, the kernel has applied the clamped nice adjustment. +/// +/// See [`sys::nice`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`NiceError::Eperm`]: lowering the nice value is not permitted. +/// - [`NiceError::Other`]: a security hook denied the update with another +/// errno. +#[cfg(target_arch = "x86")] +pub fn nice(increment: Int) -> Result<(), NiceError> { + let ret = sys::nice(increment); + + unit_from_ret(ret as isize, NiceError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::Int; + + use crate::Errno; + + use super::{NiceError, nice}; + + #[test] + fn test_nice_zero_increment_ok() { + assert_eq!(nice(0 as Int), Ok(())); + } + + #[test] + fn test_nice_error_mapping() { + assert_eq!(NiceError::from_errno(Errno::Eperm), NiceError::Eperm); + assert_eq!( + NiceError::from_errno(Errno::Eio), + NiceError::Other(Errno::Eio) + ); + } +} From 31513096a05e2dfa00fc0053e06de6e7ebd9d1c9 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:39:28 +0200 Subject: [PATCH 066/135] feat(syscalls): wrap oldfstat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/fstat.rs | 106 +++++++++++++++++++++++++ system/linux/syscalls/src/lib.rs | 4 + 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/fstat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 81fdd242..74faeb5d 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -159,7 +159,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `newlstat` - x86, x86_64 - [x] `newuname` - [x] `nice` - x86 -- [ ] `oldfstat` - x86 +- [x] `oldfstat` - x86 - [ ] `oldlstat` - x86 - [ ] `oldolduname` - x86 - [ ] `oldstat` - x86 diff --git a/system/linux/syscalls/src/fstat.rs b/system/linux/syscalls/src/fstat.rs new file mode 100644 index 00000000..644f23d5 --- /dev/null +++ b/system/linux/syscalls/src/fstat.rs @@ -0,0 +1,106 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Stat, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`oldfstat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OldfstatError { + /// `EBADF`. + Ebadf, + /// `EOVERFLOW`. + Eoverflow, + /// `EFAULT`. + Efault, + /// Another errno returned by delegated file status code. + Other(Errno), +} + +impl OldfstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Eoverflow => Self::Eoverflow, + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Get file status information through the legacy x86 `oldfstat` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), OldfstatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the open +/// file referenced by `fd`. +/// +/// See [`sys::oldfstat`] for kernel behavior, reachable errors, ABI layout, +/// and source references. +/// +/// # Errors +/// - [`OldfstatError::Ebadf`]: `fd` does not name an open file descriptor. +/// - [`OldfstatError::Eoverflow`]: metadata could not be represented in the +/// legacy stat layout. +/// - [`OldfstatError::Efault`]: the kernel could not write the output buffer. +/// - [`OldfstatError::Other`]: delegated file status error. +#[cfg(target_arch = "x86")] +pub fn oldfstat( + fd: UnsignedInt, + statbuf: &mut MaybeUninit, +) -> Result<(), OldfstatError> { + // SAFETY: `MaybeUninit` provides writable storage for one legacy + // stat value. + let ret = unsafe { sys::oldfstat(fd, statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, OldfstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{fs::File, mem::MaybeUninit, os::fd::AsRawFd as _}; + + use crate::Errno; + + use super::{OldfstatError, oldfstat}; + + #[test] + fn test_oldfstat_ok() { + let file = File::open("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(oldfstat(file.as_raw_fd() as u32, &mut statbuf), Ok(())); + } + + #[test] + fn test_oldfstat_invalid_fd() { + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(oldfstat(u32::MAX, &mut statbuf), Err(OldfstatError::Ebadf)); + } + + #[test] + fn test_oldfstat_error_mapping() { + assert_eq!( + OldfstatError::from_errno(Errno::Ebadf), + OldfstatError::Ebadf + ); + assert_eq!( + OldfstatError::from_errno(Errno::Eoverflow), + OldfstatError::Eoverflow + ); + assert_eq!( + OldfstatError::from_errno(Errno::Efault), + OldfstatError::Efault + ); + assert_eq!( + OldfstatError::from_errno(Errno::Eio), + OldfstatError::Other(Errno::Eio) + ); + } +} diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 34c5eede..4af70645 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -40,6 +40,8 @@ mod fchown; mod fcntl; #[cfg(target_arch = "x86")] mod fork; +#[cfg(target_arch = "x86")] +mod fstat; mod fstatfs; mod fsync; mod ftruncate; @@ -127,6 +129,8 @@ pub use fchown::{Fchown16Error, fchown16}; pub use fcntl::{FcntlError, fcntl}; #[cfg(target_arch = "x86")] pub use fork::{ForkError, fork}; +#[cfg(target_arch = "x86")] +pub use fstat::{OldfstatError, oldfstat}; pub use fstatfs::{FstatfsError, fstatfs}; pub use fsync::{FsyncError, fsync}; pub use ftruncate::{FtruncateError, ftruncate}; From 882fc5085dbcace3628b98035f95b6c8683116d6 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:39:45 +0200 Subject: [PATCH 067/135] feat(syscalls): wrap oldlstat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 10 +- system/linux/syscalls/src/lstat.rs | 143 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 system/linux/syscalls/src/lstat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 74faeb5d..ed0861d7 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -160,7 +160,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `newuname` - [x] `nice` - x86 - [x] `oldfstat` - x86 -- [ ] `oldlstat` - x86 +- [x] `oldlstat` - x86 - [ ] `oldolduname` - x86 - [ ] `oldstat` - x86 - [ ] `olduname` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 4af70645..c8f523d2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -84,6 +84,8 @@ mod lchown; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod link; mod lseek; +#[cfg(target_arch = "x86")] +mod lstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod mkdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -91,7 +93,6 @@ mod mknod; mod mmap; #[cfg(target_arch = "x86")] mod modify_ldt; -pub mod sys; mod mount; mod munmap; mod newfstat; @@ -99,8 +100,9 @@ mod newfstat; mod newlstat; #[cfg(target_arch = "x86")] mod nice; - +pub mod sys; mod uname; + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; pub use acct::{AcctError, acct}; @@ -175,6 +177,8 @@ pub use lchown::{Lchown16Error, lchown16}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use link::{LinkError, link}; pub use lseek::{LseekError, lseek}; +#[cfg(target_arch = "x86")] +pub use lstat::{OldlstatError, oldlstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use mkdir::{MkdirError, mkdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -182,7 +186,6 @@ pub use mknod::{MknodError, mknod}; pub use mmap::{MmapError, mmap}; #[cfg(target_arch = "x86")] pub use modify_ldt::{ModifyLdtError, modify_ldt}; - pub use mount::{MountError, mount}; pub use munmap::{MunmapError, munmap}; pub use newfstat::{NewfstatError, newfstat}; @@ -191,6 +194,7 @@ pub use newlstat::{NewlstatError, newlstat}; #[cfg(target_arch = "x86")] pub use nice::{NiceError, nice}; pub use uname::{NewunameError, newuname}; + /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] pub mod linux_1_0 { diff --git a/system/linux/syscalls/src/lstat.rs b/system/linux/syscalls/src/lstat.rs new file mode 100644 index 00000000..2b97045a --- /dev/null +++ b/system/linux/syscalls/src/lstat.rs @@ -0,0 +1,143 @@ +use core::{ffi::CStr, mem::MaybeUninit}; + +use celer_system_linux_ctypes::Stat; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`oldlstat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OldlstatError { + /// `EFAULT`. + Efault, + /// `ENAMETOOLONG`. + Enametoolong, + /// `ENOENT`. + Enoent, + /// `ENOMEM`. + Enomem, + /// `ENOTDIR`. + Enotdir, + /// `EACCES`. + Eacces, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +impl OldlstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enomem => Self::Enomem, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Get file status information through the legacy x86 `oldlstat` ABI. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer with `&mut MaybeUninit`, and maps the raw syscall +/// return into `Result<(), OldlstatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the +/// resolved path without following a final symlink. +/// +/// See [`sys::oldlstat`] for kernel behavior, reachable errors, ABI layout, +/// and source references. +/// +/// # Errors +/// - [`OldlstatError::Efault`]: the kernel could not read the path or write +/// the output buffer. +/// - [`OldlstatError::Enametoolong`]: the pathname is too long. +/// - [`OldlstatError::Enoent`]: the path is empty or missing. +/// - [`OldlstatError::Enomem`]: the kernel could not allocate pathname storage. +/// - [`OldlstatError::Enotdir`]: traversal needed a directory. +/// - [`OldlstatError::Eacces`]: traversal lacked search permission. +/// - [`OldlstatError::Eoverflow`]: metadata could not be represented in the +/// legacy stat layout. +/// - [`OldlstatError::Other`]: delegated pathname lookup or filesystem error. +#[cfg(target_arch = "x86")] +pub fn oldlstat( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), OldlstatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one legacy stat value. + let ret = unsafe { sys::oldlstat(filename.as_ptr(), statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, OldlstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ffi::CString, mem::MaybeUninit}; + + use crate::Errno; + + use super::{OldlstatError, oldlstat}; + + #[test] + fn test_oldlstat_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(oldlstat(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[test] + fn test_oldlstat_missing_path() { + let path = CString::new("/celer-oldlstat-definitely-missing").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!( + oldlstat(path.as_c_str(), &mut statbuf), + Err(OldlstatError::Enoent) + ); + } + + #[test] + fn test_oldlstat_error_mapping() { + assert_eq!( + OldlstatError::from_errno(Errno::Efault), + OldlstatError::Efault + ); + assert_eq!( + OldlstatError::from_errno(Errno::Raw(36)), + OldlstatError::Enametoolong + ); + assert_eq!( + OldlstatError::from_errno(Errno::Enoent), + OldlstatError::Enoent + ); + assert_eq!( + OldlstatError::from_errno(Errno::Enomem), + OldlstatError::Enomem + ); + assert_eq!( + OldlstatError::from_errno(Errno::Enotdir), + OldlstatError::Enotdir + ); + assert_eq!( + OldlstatError::from_errno(Errno::Eacces), + OldlstatError::Eacces + ); + assert_eq!( + OldlstatError::from_errno(Errno::Eoverflow), + OldlstatError::Eoverflow + ); + assert_eq!( + OldlstatError::from_errno(Errno::Eio), + OldlstatError::Other(Errno::Eio) + ); + } +} From 19d2f12ae204e9340aeb441966f3738effa02e3c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:46:57 +0200 Subject: [PATCH 068/135] feat(syscalls): wrap oldolduname --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/uname.rs | 81 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index ed0861d7..2873ac12 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -161,7 +161,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `nice` - x86 - [x] `oldfstat` - x86 - [x] `oldlstat` - x86 -- [ ] `oldolduname` - x86 +- [x] `oldolduname` - x86 - [ ] `oldstat` - x86 - [ ] `olduname` - x86 - [ ] `open` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c8f523d2..08429762 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -194,6 +194,8 @@ pub use newlstat::{NewlstatError, newlstat}; #[cfg(target_arch = "x86")] pub use nice::{NiceError, nice}; pub use uname::{NewunameError, newuname}; +#[cfg(target_arch = "x86")] +pub use uname::{OldoldunameError, oldolduname}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/uname.rs b/system/linux/syscalls/src/uname.rs index 8fb2c408..a42f08d1 100644 --- a/system/linux/syscalls/src/uname.rs +++ b/system/linux/syscalls/src/uname.rs @@ -1,11 +1,33 @@ use core::mem::MaybeUninit; use celer_system_linux_ctypes::NewUtsname; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::OldOldUtsname; use crate::errno::Errno; use crate::helpers::unit_from_ret; use crate::sys; +/// Errors returned by [`oldolduname`]. +#[cfg(target_arch = "x86")] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OldoldunameError { + /// `EFAULT`. + Efault, + /// Another errno returned by uname compatibility handling. + Other(Errno), +} + +#[cfg(target_arch = "x86")] +impl OldoldunameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + /// Errors returned by [`newuname`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum NewunameError { @@ -24,6 +46,34 @@ impl NewunameError { } } +/// Copy system identity strings through the legacy x86 `oldolduname` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), OldoldunameError>`. +/// +/// On success, the kernel has initialized `name` with the five-field legacy +/// `OldOldUtsname` record. +/// +/// See [`sys::oldolduname`] for kernel behavior, reachable errors, ABI layout, +/// and source references. +/// +/// # Errors +/// - [`OldoldunameError::Efault`]: the kernel could not write the output +/// buffer. +/// - [`OldoldunameError::Other`]: another errno from uname compatibility +/// handling. +#[cfg(target_arch = "x86")] +pub fn oldolduname( + name: &mut MaybeUninit, +) -> Result<(), OldoldunameError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // kernel-initialized legacy uname record. + let ret = unsafe { sys::oldolduname(name.as_mut_ptr()) }; + + unit_from_ret(ret as isize, OldoldunameError::from_errno) +} + /// Copy the system identity strings into `name`. /// /// This safe wrapper replaces the raw output pointer with @@ -57,6 +107,37 @@ mod tests { use crate::Errno; use super::{NewunameError, newuname}; + #[cfg(target_arch = "x86")] + use super::{OldoldunameError, oldolduname}; + + #[cfg(target_arch = "x86")] + #[test] + fn test_oldolduname_ok() { + let mut name = MaybeUninit::uninit(); + + assert_eq!(oldolduname(&mut name), Ok(())); + let name = unsafe { name.assume_init() }; + let sysname = name + .sysname + .iter() + .take(5) + .map(|byte| byte.to_ne_bytes()[0]) + .collect::>(); + assert_eq!(sysname, b"Linux"); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_oldolduname_error_mapping() { + assert_eq!( + OldoldunameError::from_errno(Errno::Efault), + OldoldunameError::Efault + ); + assert_eq!( + OldoldunameError::from_errno(Errno::Eio), + OldoldunameError::Other(Errno::Eio) + ); + } #[test] fn test_newuname_ok() { From 9860ba2ed8ab02c8c3d103e7969203acbb866856 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:47:50 +0200 Subject: [PATCH 069/135] feat(syscalls): wrap oldstat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/stat.rs | 103 +++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/stat.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2873ac12..84d6bb0d 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -162,7 +162,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `oldfstat` - x86 - [x] `oldlstat` - x86 - [x] `oldolduname` - x86 -- [ ] `oldstat` - x86 +- [x] `oldstat` - x86 - [ ] `olduname` - x86 - [ ] `open` - x86, x86_64 - [ ] `pause` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 08429762..e9f26a51 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -100,6 +100,8 @@ mod newfstat; mod newlstat; #[cfg(target_arch = "x86")] mod nice; +#[cfg(target_arch = "x86")] +mod stat; pub mod sys; mod uname; @@ -193,6 +195,8 @@ pub use newfstat::{NewfstatError, newfstat}; pub use newlstat::{NewlstatError, newlstat}; #[cfg(target_arch = "x86")] pub use nice::{NiceError, nice}; +#[cfg(target_arch = "x86")] +pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, oldolduname}; diff --git a/system/linux/syscalls/src/stat.rs b/system/linux/syscalls/src/stat.rs new file mode 100644 index 00000000..a5d4bc04 --- /dev/null +++ b/system/linux/syscalls/src/stat.rs @@ -0,0 +1,103 @@ +use core::{ffi::CStr, mem::MaybeUninit}; + +use celer_system_linux_ctypes::Stat; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`oldstat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OldstatError { + /// `EFAULT`. + Efault, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +impl OldstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Get file status information through the legacy x86 `oldstat` ABI. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer with `&mut MaybeUninit`, and maps the raw syscall +/// return into `Result<(), OldstatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the +/// resolved path. +/// +/// See [`sys::oldstat`] for kernel behavior, reachable errors, ABI layout, and +/// source references. +/// +/// # Errors +/// - [`OldstatError::Efault`]: the kernel could not read the path or write the +/// output buffer. +/// - [`OldstatError::Eoverflow`]: metadata could not be represented in the +/// legacy stat layout. +/// - [`OldstatError::Other`]: delegated pathname lookup or filesystem error. +#[cfg(target_arch = "x86")] +pub fn oldstat( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), OldstatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one legacy stat value. + let ret = unsafe { sys::oldstat(filename.as_ptr(), statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, OldstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ffi::CString, mem::MaybeUninit}; + + use crate::Errno; + + use super::{OldstatError, oldstat}; + + #[test] + fn test_oldstat_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(oldstat(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[test] + fn test_oldstat_missing_path() { + let path = CString::new("/celer-oldstat-definitely-missing").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!( + oldstat(path.as_c_str(), &mut statbuf), + Err(OldstatError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_oldstat_error_mapping() { + assert_eq!( + OldstatError::from_errno(Errno::Efault), + OldstatError::Efault + ); + assert_eq!( + OldstatError::from_errno(Errno::Eoverflow), + OldstatError::Eoverflow + ); + assert_eq!( + OldstatError::from_errno(Errno::Enoent), + OldstatError::Other(Errno::Enoent) + ); + } +} From a626118ea10910c828c7e86649ae2b0f70c2e1e1 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:48:22 +0200 Subject: [PATCH 070/135] feat(syscalls): wrap olduname --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 +- system/linux/syscalls/src/uname.rs | 78 +++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 84d6bb0d..5bc8bd37 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -163,7 +163,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `oldlstat` - x86 - [x] `oldolduname` - x86 - [x] `oldstat` - x86 -- [ ] `olduname` - x86 +- [x] `olduname` - x86 - [ ] `open` - x86, x86_64 - [ ] `pause` - x86, x86_64 - [ ] `pipe` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e9f26a51..728880aa 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -199,7 +199,7 @@ pub use nice::{NiceError, nice}; pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] -pub use uname::{OldoldunameError, oldolduname}; +pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/uname.rs b/system/linux/syscalls/src/uname.rs index a42f08d1..fb5c3e49 100644 --- a/system/linux/syscalls/src/uname.rs +++ b/system/linux/syscalls/src/uname.rs @@ -2,7 +2,7 @@ use core::mem::MaybeUninit; use celer_system_linux_ctypes::NewUtsname; #[cfg(target_arch = "x86")] -use celer_system_linux_ctypes::OldOldUtsname; +use celer_system_linux_ctypes::{OldOldUtsname, OldUtsname}; use crate::errno::Errno; use crate::helpers::unit_from_ret; @@ -28,6 +28,26 @@ impl OldoldunameError { } } +/// Errors returned by [`olduname`]. +#[cfg(target_arch = "x86")] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OldunameError { + /// `EFAULT`. + Efault, + /// Another errno returned by uname compatibility handling. + Other(Errno), +} + +#[cfg(target_arch = "x86")] +impl OldunameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + /// Errors returned by [`newuname`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum NewunameError { @@ -74,6 +94,33 @@ pub fn oldolduname( unit_from_ret(ret as isize, OldoldunameError::from_errno) } +/// Copy system identity strings through the legacy x86 `olduname` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return into +/// `Result<(), OldunameError>`. +/// +/// On success, the kernel has initialized `name` with the five-field +/// `OldUtsname` record. +/// +/// See [`sys::olduname`] for kernel behavior, reachable errors, ABI layout, +/// and source references. +/// +/// # Errors +/// - [`OldunameError::Efault`]: the kernel could not write the output buffer. +/// - [`OldunameError::Other`]: another errno from uname compatibility +/// handling. +#[cfg(target_arch = "x86")] +pub fn olduname( + name: &mut MaybeUninit, +) -> Result<(), OldunameError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // kernel-initialized legacy uname record. + let ret = unsafe { sys::olduname(name.as_mut_ptr()) }; + + unit_from_ret(ret as isize, OldunameError::from_errno) +} + /// Copy the system identity strings into `name`. /// /// This safe wrapper replaces the raw output pointer with @@ -108,7 +155,7 @@ mod tests { use super::{NewunameError, newuname}; #[cfg(target_arch = "x86")] - use super::{OldoldunameError, oldolduname}; + use super::{OldoldunameError, OldunameError, oldolduname, olduname}; #[cfg(target_arch = "x86")] #[test] @@ -139,6 +186,33 @@ mod tests { ); } + #[cfg(target_arch = "x86")] + #[test] + fn test_olduname_ok() { + let mut name = MaybeUninit::uninit(); + + assert_eq!(olduname(&mut name), Ok(())); + let name = unsafe { name.assume_init() }; + assert_ne!(name.sysname[0], 0); + assert_ne!(name.nodename[0], 0); + assert_ne!(name.release[0], 0); + assert_ne!(name.version[0], 0); + assert_ne!(name.machine[0], 0); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_olduname_error_mapping() { + assert_eq!( + OldunameError::from_errno(Errno::Efault), + OldunameError::Efault + ); + assert_eq!( + OldunameError::from_errno(Errno::Eio), + OldunameError::Other(Errno::Eio) + ); + } + #[test] fn test_newuname_ok() { let mut name = MaybeUninit::uninit(); From 25e68a2e59ab17d09d10e6ff67cb711208960637 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:49:06 +0200 Subject: [PATCH 071/135] feat(syscalls): wrap open --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/open.rs | 118 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/open.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 5bc8bd37..2e6d72fe 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -164,7 +164,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `oldolduname` - x86 - [x] `oldstat` - x86 - [x] `olduname` - x86 -- [ ] `open` - x86, x86_64 +- [x] `open` - x86, x86_64 - [ ] `pause` - x86, x86_64 - [ ] `pipe` - x86, x86_64 - [ ] `ptrace` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 728880aa..4d49a501 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -100,6 +100,8 @@ mod newfstat; mod newlstat; #[cfg(target_arch = "x86")] mod nice; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod open; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -195,6 +197,8 @@ pub use newfstat::{NewfstatError, newfstat}; pub use newlstat::{NewlstatError, newlstat}; #[cfg(target_arch = "x86")] pub use nice::{NiceError, nice}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use open::{OpenError, open}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/open.rs b/system/linux/syscalls/src/open.rs new file mode 100644 index 00000000..c500e43a --- /dev/null +++ b/system/linux/syscalls/src/open.rs @@ -0,0 +1,118 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::{Int, UModeT}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`open`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OpenError { + /// `EINVAL`. + Einval, + /// Another errno returned by delegated pathname lookup, file open, + /// permission, filesystem, security-hook, or descriptor-allocation code. + Other(Errno), +} + +impl OpenError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Open `filename` with the given raw `flags` and `mode`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result`. +/// +/// On success, returns the nonnegative file descriptor opened with the raw +/// [`sys::open`] semantics. +/// +/// See [`sys::open`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`OpenError::Einval`]: the kernel rejected the supplied flag +/// combination. +/// - [`OpenError::Other`]: delegated pathname lookup, open, permission, +/// filesystem, security-hook, or descriptor-allocation error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn open( + filename: &CStr, + flags: Int, + mode: UModeT, +) -> Result { + // SAFETY: `CStr` guarantees a valid, NUL-terminated pathname pointer for + // the duration of the call. + let ret = unsafe { sys::open(filename.as_ptr(), flags, mode) }; + + result_from_ret(ret as isize, |ret| ret as Int, OpenError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, File}, + os::fd::FromRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{Int, UModeT}; + + use crate::Errno; + + use super::{OpenError, open}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_open_ok() { + let path = temp_path("open"); + File::create(&path).unwrap(); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + let fd = open(path_cstr.as_c_str(), 0 as Int, 0 as UModeT) + .expect("open should succeed for an existing temp file"); + let file = unsafe { File::from_raw_fd(fd) }; + drop(file); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_open_missing_path() { + let path = temp_path("open_missing"); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + open(path_cstr.as_c_str(), 0 as Int, 0 as UModeT), + Err(OpenError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_open_error_mapping() { + assert_eq!(OpenError::from_errno(Errno::Einval), OpenError::Einval); + assert_eq!( + OpenError::from_errno(Errno::Enoent), + OpenError::Other(Errno::Enoent) + ); + } +} From 70ffb0a049112284750b361397c00fad69cc571c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:50:06 +0200 Subject: [PATCH 072/135] feat(syscalls): wrap pause --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/pause.rs | 57 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/pause.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2e6d72fe..a7b5e0db 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -165,7 +165,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `oldstat` - x86 - [x] `olduname` - x86 - [x] `open` - x86, x86_64 -- [ ] `pause` - x86, x86_64 +- [x] `pause` - x86, x86_64 - [ ] `pipe` - x86, x86_64 - [ ] `ptrace` - [ ] `read` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 4d49a501..de34ef2b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -102,6 +102,8 @@ mod newlstat; mod nice; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod open; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod pause; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -199,6 +201,8 @@ pub use newlstat::{NewlstatError, newlstat}; pub use nice::{NiceError, nice}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use open::{OpenError, open}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use pause::{PauseError, pause}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/pause.rs b/system/linux/syscalls/src/pause.rs new file mode 100644 index 00000000..3a541462 --- /dev/null +++ b/system/linux/syscalls/src/pause.rs @@ -0,0 +1,57 @@ +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`pause`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PauseError { + /// `EINTR`. + Eintr, + /// Another errno returned by signal restart handling. + Other(Errno), +} + +impl PauseError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eintr => Self::Eintr, + errno => Self::Other(errno), + } + } +} + +/// Suspend the calling thread until a signal is pending for delivery. +/// +/// This safe wrapper maps the raw syscall return value into +/// `Result<(), PauseError>`. The raw `pause` syscall has no arguments. +/// +/// See [`sys::pause`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`PauseError::Eintr`]: a signal interrupted the sleep. +/// - [`PauseError::Other`]: another errno from signal restart handling. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn pause() -> Result<(), PauseError> { + let ret = sys::pause(); + + unit_from_ret(ret as isize, PauseError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::PauseError; + + #[test] + fn test_pause_error_mapping() { + assert_eq!(PauseError::from_errno(Errno::Eintr), PauseError::Eintr); + assert_eq!( + PauseError::from_errno(Errno::Eio), + PauseError::Other(Errno::Eio) + ); + } +} From 9f96dda3c6e664081e1f6147c0ad71d489109789 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:50:45 +0200 Subject: [PATCH 073/135] feat(syscalls): wrap pipe --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/pipe.rs | 98 ++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/pipe.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a7b5e0db..c945b8ea 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -166,7 +166,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `olduname` - x86 - [x] `open` - x86, x86_64 - [x] `pause` - x86, x86_64 -- [ ] `pipe` - x86, x86_64 +- [x] `pipe` - x86, x86_64 - [ ] `ptrace` - [ ] `read` - [ ] `readdir` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index de34ef2b..3e31a33a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -104,6 +104,8 @@ mod nice; mod open; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod pause; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod pipe; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -203,6 +205,8 @@ pub use nice::{NiceError, nice}; pub use open::{OpenError, open}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use pause::{PauseError, pause}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use pipe::{PipeError, pipe}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/pipe.rs b/system/linux/syscalls/src/pipe.rs new file mode 100644 index 00000000..fdf4e637 --- /dev/null +++ b/system/linux/syscalls/src/pipe.rs @@ -0,0 +1,98 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`pipe`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PipeError { + /// `EFAULT`. + Efault, + /// `EMFILE`. + Emfile, + /// `ENFILE`. + Enfile, + /// `ENOMEM`. + Enomem, + /// Another errno returned by pipe or descriptor allocation. + Other(Errno), +} + +impl PipeError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Emfile => Self::Emfile, + Errno::Raw(23) => Self::Enfile, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Create a pipe and write the read and write descriptors into `fildes`. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit<[Int; 2]>` and maps the raw syscall return into +/// `Result<(), PipeError>`. +/// +/// On success, the kernel has initialized `fildes` with `[read_fd, write_fd]`. +/// +/// See [`sys::pipe`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`PipeError::Efault`]: the kernel could not write the descriptor array. +/// - [`PipeError::Emfile`]: the caller cannot obtain two more descriptors. +/// - [`PipeError::Enfile`]: system-wide pipe or file allocation failed. +/// - [`PipeError::Enomem`]: the kernel could not allocate pipe metadata. +/// - [`PipeError::Other`]: another pipe or descriptor-allocation error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn pipe(fildes: &mut MaybeUninit<[Int; 2]>) -> Result<(), PipeError> { + // SAFETY: `MaybeUninit<[Int; 2]>` provides writable storage for the two + // kernel-initialized pipe descriptors. + let ret = unsafe { sys::pipe(fildes.as_mut_ptr().cast::()) }; + + unit_from_ret(ret as isize, PipeError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::fs::File; + use std::mem::MaybeUninit; + use std::os::fd::FromRawFd as _; + + use crate::Errno; + + use super::{PipeError, pipe}; + + #[test] + fn test_pipe_ok() { + let mut fds = MaybeUninit::uninit(); + + assert_eq!(pipe(&mut fds), Ok(())); + let [read_fd, write_fd] = unsafe { fds.assume_init() }; + assert_ne!(read_fd, write_fd); + + let read_file = unsafe { File::from_raw_fd(read_fd) }; + let write_file = unsafe { File::from_raw_fd(write_fd) }; + drop(read_file); + drop(write_file); + } + + #[test] + fn test_pipe_error_mapping() { + assert_eq!(PipeError::from_errno(Errno::Efault), PipeError::Efault); + assert_eq!(PipeError::from_errno(Errno::Emfile), PipeError::Emfile); + assert_eq!(PipeError::from_errno(Errno::Raw(23)), PipeError::Enfile); + assert_eq!(PipeError::from_errno(Errno::Enomem), PipeError::Enomem); + assert_eq!( + PipeError::from_errno(Errno::Einval), + PipeError::Other(Errno::Einval) + ); + } +} From 3dda872496d7a030a0c13dc513dbad9a05be5e29 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:51:19 +0200 Subject: [PATCH 074/135] feat(syscalls): wrap ptrace --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/ptrace.rs | 99 ++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/ptrace.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c945b8ea..a746d428 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -167,7 +167,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `open` - x86, x86_64 - [x] `pause` - x86, x86_64 - [x] `pipe` - x86, x86_64 -- [ ] `ptrace` +- [x] `ptrace` - [ ] `read` - [ ] `readdir` - x86 - [ ] `readlink` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 3e31a33a..c2c35355 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -106,6 +106,7 @@ mod open; mod pause; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod pipe; +mod ptrace; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -207,6 +208,7 @@ pub use open::{OpenError, open}; pub use pause::{PauseError, pause}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use pipe::{PipeError, pipe}; +pub use ptrace::{PtraceError, ptrace}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/ptrace.rs b/system/linux/syscalls/src/ptrace.rs new file mode 100644 index 00000000..d63246fb --- /dev/null +++ b/system/linux/syscalls/src/ptrace.rs @@ -0,0 +1,99 @@ +use celer_system_linux_ctypes::{Long, UnsignedLong}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`ptrace`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PtraceError { + /// `ESRCH`. + Esrch, + /// `EPERM`. + Eperm, + /// `EIO`. + Eio, + /// `EFAULT`. + Efault, + /// Another errno returned by request-specific ptrace handling. + Other(Errno), +} + +impl PtraceError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Esrch => Self::Esrch, + Errno::Eperm => Self::Eperm, + Errno::Eio => Self::Eio, + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Control or inspect another process with `ptrace`. +/// +/// This wrapper keeps the raw four-argument ptrace ABI and maps the raw +/// syscall return value into `Result`. +/// +/// On success, returns the nonnegative or positive request-specific raw +/// ptrace result. +/// +/// See [`sys::ptrace`] for kernel behavior, reachable errors, request-specific +/// argument rules, and source references. +/// +/// # Safety +/// The caller must ensure that `request`, `pid`, `addr`, and `data` satisfy +/// the memory-safety requirements of the chosen ptrace request. Any pointer +/// encoded in `addr` or `data` must remain valid for the duration of the +/// syscall and obey that request's ABI. +/// +/// # Errors +/// - [`PtraceError::Esrch`]: the target task does not exist. +/// - [`PtraceError::Eperm`]: the caller lacks permission for the request. +/// - [`PtraceError::Eio`]: the request is invalid for the target or a +/// request-specific access failed. +/// - [`PtraceError::Efault`]: a user pointer argument was inaccessible. +/// - [`PtraceError::Other`]: another request-specific ptrace error. +pub unsafe fn ptrace( + request: Long, + pid: Long, + addr: UnsignedLong, + data: UnsignedLong, +) -> Result { + // SAFETY: forwarded from this wrapper's request-specific contract. + let ret = unsafe { sys::ptrace(request, pid, addr, data) }; + + result_from_ret(ret as isize, |ret| ret as Long, PtraceError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::Long; + + use crate::Errno; + + use super::{PtraceError, ptrace}; + + const INVALID_REQUEST: Long = 9999; + + #[test] + fn test_ptrace_invalid_request() { + let ret = unsafe { ptrace(INVALID_REQUEST, 0, 0, 0) }; + + assert!(ret.is_err()); + } + + #[test] + fn test_ptrace_error_mapping() { + assert_eq!(PtraceError::from_errno(Errno::Esrch), PtraceError::Esrch); + assert_eq!(PtraceError::from_errno(Errno::Eperm), PtraceError::Eperm); + assert_eq!(PtraceError::from_errno(Errno::Eio), PtraceError::Eio); + assert_eq!(PtraceError::from_errno(Errno::Efault), PtraceError::Efault); + assert_eq!( + PtraceError::from_errno(Errno::Einval), + PtraceError::Other(Errno::Einval) + ); + } +} From 68fe5d1517440a805d532d0ee400cc48754c9227 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:51:53 +0200 Subject: [PATCH 075/135] feat(syscalls): wrap read --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/read.rs | 125 +++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/read.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a746d428..8f9bb6d9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -168,7 +168,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `pause` - x86, x86_64 - [x] `pipe` - x86, x86_64 - [x] `ptrace` -- [ ] `read` +- [x] `read` - [ ] `readdir` - x86 - [ ] `readlink` - x86, x86_64 - [ ] `reboot` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c2c35355..91b11e95 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -107,6 +107,7 @@ mod pause; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod pipe; mod ptrace; +mod read; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -209,6 +210,7 @@ pub use pause::{PauseError, pause}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use pipe::{PipeError, pipe}; pub use ptrace::{PtraceError, ptrace}; +pub use read::{ReadError, read}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/read.rs b/system/linux/syscalls/src/read.rs new file mode 100644 index 00000000..006e5d5b --- /dev/null +++ b/system/linux/syscalls/src/read.rs @@ -0,0 +1,125 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Char, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`read`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReadError { + /// `EBADF`. + Ebadf, + /// `EFAULT`. + Efault, + /// `EINVAL`. + Einval, + /// Another errno returned by delegated file, driver, protocol, or security + /// handling. + Other(Errno), +} + +impl ReadError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Read bytes from `fd` into `buf`. +/// +/// This safe wrapper replaces the raw output pointer and byte count with a +/// `&mut [MaybeUninit]` and maps the raw syscall return value into +/// `Result`. +/// +/// On success, returns the number of bytes initialized in `buf`. +/// +/// See [`sys::read`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ReadError::Ebadf`]: `fd` is not open or is not open for reading. +/// - [`ReadError::Efault`]: the kernel could not write the output buffer. +/// - [`ReadError::Einval`]: the target object cannot be read this way or the +/// request is invalid. +/// - [`ReadError::Other`]: delegated file, driver, protocol, or security error. +pub fn read( + fd: UnsignedInt, + buf: &mut [MaybeUninit], +) -> Result { + // SAFETY: `MaybeUninit` has the same layout as `u8`/`Char`, and the + // slice provides writable storage for exactly `buf.len()` bytes. + let ret = + unsafe { sys::read(fd, buf.as_mut_ptr().cast::(), buf.len()) }; + + result_from_ret(ret as isize, |ret| ret as usize, ReadError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::File, + io::Write as _, + mem::MaybeUninit, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{ReadError, read}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_read_{now}")); + path + } + + #[test] + fn test_read_ok() { + let path = temp_path(); + let mut file = File::create(&path).unwrap(); + file.write_all(b"read bytes").unwrap(); + drop(file); + + let file = File::open(&path).unwrap(); + let mut buf = [MaybeUninit::uninit(); 16]; + + let n = read(file.as_raw_fd() as u32, &mut buf).unwrap(); + assert_eq!(n, 10); + let initialized = unsafe { + core::slice::from_raw_parts(buf.as_ptr().cast::(), n) + }; + assert_eq!(initialized, b"read bytes"); + + std::fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_read_invalid_fd() { + let mut buf = [MaybeUninit::uninit(); 1]; + + assert_eq!(read(u32::MAX, &mut buf), Err(ReadError::Ebadf)); + } + + #[test] + fn test_read_error_mapping() { + assert_eq!(ReadError::from_errno(Errno::Ebadf), ReadError::Ebadf); + assert_eq!(ReadError::from_errno(Errno::Efault), ReadError::Efault); + assert_eq!(ReadError::from_errno(Errno::Einval), ReadError::Einval); + assert_eq!( + ReadError::from_errno(Errno::Eio), + ReadError::Other(Errno::Eio) + ); + } +} From 2c06d345f4f85e2c90e2e276c3f1b1de880f795f Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:52:39 +0200 Subject: [PATCH 076/135] feat(syscalls): wrap readdir --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/readdir.rs | 117 +++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/readdir.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 8f9bb6d9..7eaa32f1 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -169,7 +169,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `pipe` - x86, x86_64 - [x] `ptrace` - [x] `read` -- [ ] `readdir` - x86 +- [x] `readdir` - x86 - [ ] `readlink` - x86, x86_64 - [ ] `reboot` - [ ] `rename` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 91b11e95..c45a95d6 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -109,6 +109,8 @@ mod pipe; mod ptrace; mod read; #[cfg(target_arch = "x86")] +mod readdir; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -212,6 +214,8 @@ pub use pipe::{PipeError, pipe}; pub use ptrace::{PtraceError, ptrace}; pub use read::{ReadError, read}; #[cfg(target_arch = "x86")] +pub use readdir::{ReaddirError, readdir}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/readdir.rs b/system/linux/syscalls/src/readdir.rs new file mode 100644 index 00000000..628580db --- /dev/null +++ b/system/linux/syscalls/src/readdir.rs @@ -0,0 +1,117 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{OldLinuxDirent, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`readdir`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReaddirError { + /// `EBADF`. + Ebadf, + /// `ENOTDIR`. + Enotdir, + /// `EFAULT`. + Efault, + /// `ENOENT`. + Enoent, + /// `EIO`. + Eio, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated filesystem code. + Other(Errno), +} + +impl ReaddirError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Enotdir => Self::Enotdir, + Errno::Efault => Self::Efault, + Errno::Enoent => Self::Enoent, + Errno::Eio => Self::Eio, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Read one directory entry through the legacy x86 `readdir` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw syscall return value +/// into `Result`. +/// +/// On success, returns the raw nonnegative result from the kernel. Current +/// kernels return `1` when one entry was emitted and `0` at end of directory. +/// +/// See [`sys::readdir`] for kernel behavior, reachable errors, ABI layout, and +/// source references. +/// +/// # Errors +/// - [`ReaddirError::Ebadf`]: `fd` is not an open usable descriptor. +/// - [`ReaddirError::Enotdir`]: `fd` does not refer to a directory. +/// - [`ReaddirError::Efault`]: the kernel could not write the output buffer. +/// - [`ReaddirError::Enoent`]: delegated directory reading reported a missing +/// entry or invalid directory position. +/// - [`ReaddirError::Eio`]: delegated directory reading failed with I/O error. +/// - [`ReaddirError::Eoverflow`]: returned metadata did not fit the ABI. +/// - [`ReaddirError::Other`]: delegated filesystem error. +#[cfg(target_arch = "x86")] +pub fn readdir( + fd: UnsignedInt, + dirent: &mut MaybeUninit, + count: UnsignedInt, +) -> Result { + // SAFETY: `MaybeUninit` provides writable storage for one + // kernel-initialized legacy directory entry. + let ret = unsafe { sys::readdir(fd, dirent.as_mut_ptr(), count) }; + + result_from_ret(ret as isize, |ret| ret as usize, ReaddirError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::mem::MaybeUninit; + + use crate::Errno; + + use super::{ReaddirError, readdir}; + + #[test] + fn test_readdir_invalid_fd() { + let mut dirent = MaybeUninit::uninit(); + + assert_eq!(readdir(u32::MAX, &mut dirent, 1), Err(ReaddirError::Ebadf)); + } + + #[test] + fn test_readdir_error_mapping() { + assert_eq!(ReaddirError::from_errno(Errno::Ebadf), ReaddirError::Ebadf); + assert_eq!( + ReaddirError::from_errno(Errno::Enotdir), + ReaddirError::Enotdir + ); + assert_eq!( + ReaddirError::from_errno(Errno::Efault), + ReaddirError::Efault + ); + assert_eq!( + ReaddirError::from_errno(Errno::Enoent), + ReaddirError::Enoent + ); + assert_eq!(ReaddirError::from_errno(Errno::Eio), ReaddirError::Eio); + assert_eq!( + ReaddirError::from_errno(Errno::Eoverflow), + ReaddirError::Eoverflow + ); + assert_eq!( + ReaddirError::from_errno(Errno::Eperm), + ReaddirError::Other(Errno::Eperm) + ); + } +} From 840523f7c2df7e7cf7f80b0d6d030736b240b3fc Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 12:53:37 +0200 Subject: [PATCH 077/135] feat(syscalls): wrap readlink --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/readlink.rs | 180 +++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/readlink.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 7eaa32f1..0b32458c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -170,7 +170,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ptrace` - [x] `read` - [x] `readdir` - x86 -- [ ] `readlink` - x86, x86_64 +- [x] `readlink` - x86, x86_64 - [ ] `reboot` - [ ] `rename` - x86, x86_64 - [ ] `rmdir` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c45a95d6..08f5a50c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -110,6 +110,8 @@ mod ptrace; mod read; #[cfg(target_arch = "x86")] mod readdir; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod readlink; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -215,6 +217,8 @@ pub use ptrace::{PtraceError, ptrace}; pub use read::{ReadError, read}; #[cfg(target_arch = "x86")] pub use readdir::{ReaddirError, readdir}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use readlink::{ReadlinkError, readlink}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/readlink.rs b/system/linux/syscalls/src/readlink.rs new file mode 100644 index 00000000..a0da91c1 --- /dev/null +++ b/system/linux/syscalls/src/readlink.rs @@ -0,0 +1,180 @@ +use core::{ffi::CStr, mem::MaybeUninit}; + +use celer_system_linux_ctypes::{Char, Int}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`readlink`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReadlinkError { + /// `EINVAL`. + Einval, + /// `EFAULT`. + Efault, + /// `ENAMETOOLONG`. + Enametoolong, + /// `ENOENT`. + Enoent, + /// `ENOTDIR`. + Enotdir, + /// `EACCES`. + Eacces, + /// `ENOMEM`. + Enomem, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +impl ReadlinkError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Read the stored target bytes of the symbolic link named by `path`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer and size with `&mut [MaybeUninit]`, and maps the raw +/// syscall return value into `Result`. +/// +/// On success, returns the number of bytes initialized in `buf`. The kernel +/// does not append a trailing NUL byte. +/// +/// See [`sys::readlink`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`ReadlinkError::Einval`]: `buf` is too large for the raw ABI, the raw +/// size is nonpositive, or the resolved inode cannot be read as a symlink. +/// - [`ReadlinkError::Efault`]: the kernel could not read `path` or write +/// `buf`. +/// - [`ReadlinkError::Enametoolong`]: the pathname is too long. +/// - [`ReadlinkError::Enoent`]: the path is empty or missing. +/// - [`ReadlinkError::Enotdir`]: traversal needed a directory. +/// - [`ReadlinkError::Eacces`]: traversal lacked search permission. +/// - [`ReadlinkError::Enomem`]: the kernel could not allocate pathname storage. +/// - [`ReadlinkError::Other`]: delegated pathname lookup or filesystem error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn readlink( + path: &CStr, + buf: &mut [MaybeUninit], +) -> Result { + let bufsiz = Int::try_from(buf.len()).map_err(|_| ReadlinkError::Einval)?; + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` has the same layout as the writable byte buffer the + // kernel initializes. + let ret = unsafe { + sys::readlink(path.as_ptr(), buf.as_mut_ptr().cast::(), bufsiz) + }; + + result_from_ret(ret as isize, |ret| ret as usize, ReadlinkError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::{CString, OsStr}, + fs, + mem::MaybeUninit, + os::unix::ffi::OsStrExt as _, + os::unix::fs::symlink, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{ReadlinkError, readlink}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_readlink_ok() { + let link_path = temp_path("readlink_link"); + let target = b"readlink_target"; + symlink(OsStr::from_bytes(target), &link_path).unwrap(); + let path_cstr = + CString::new(link_path.as_os_str().as_encoded_bytes()).unwrap(); + let mut buf = [MaybeUninit::uninit(); 64]; + + let n = readlink(path_cstr.as_c_str(), &mut buf).unwrap(); + assert_eq!(n, target.len()); + let initialized = unsafe { + core::slice::from_raw_parts(buf.as_ptr().cast::(), n) + }; + assert_eq!(initialized, target); + + fs::remove_file(&link_path).unwrap(); + } + + #[test] + fn test_readlink_regular_file() { + let path = temp_path("readlink_regular"); + fs::write(&path, b"not a link").unwrap(); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + let mut buf = [MaybeUninit::uninit(); 64]; + + assert_eq!( + readlink(path_cstr.as_c_str(), &mut buf), + Err(ReadlinkError::Einval) + ); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_readlink_error_mapping() { + assert_eq!( + ReadlinkError::from_errno(Errno::Einval), + ReadlinkError::Einval + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Efault), + ReadlinkError::Efault + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Raw(36)), + ReadlinkError::Enametoolong + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Enoent), + ReadlinkError::Enoent + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Enotdir), + ReadlinkError::Enotdir + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Eacces), + ReadlinkError::Eacces + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Enomem), + ReadlinkError::Enomem + ); + assert_eq!( + ReadlinkError::from_errno(Errno::Eio), + ReadlinkError::Other(Errno::Eio) + ); + } +} From 0eab2b625d8d2fa7818cffe5b74fa501e8b43002 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:09:37 +0200 Subject: [PATCH 078/135] feat(syscalls): wrap reboot --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/reboot.rs | 84 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/reboot.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 0b32458c..2db3f4b1 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -171,7 +171,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `read` - [x] `readdir` - x86 - [x] `readlink` - x86, x86_64 -- [ ] `reboot` +- [x] `reboot` - [ ] `rename` - x86, x86_64 - [ ] `rmdir` - x86, x86_64 - [ ] `select` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 08f5a50c..1375f339 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -112,6 +112,7 @@ mod read; mod readdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod readlink; +mod reboot; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -219,6 +220,7 @@ pub use read::{ReadError, read}; pub use readdir::{ReaddirError, readdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use readlink::{ReadlinkError, readlink}; +pub use reboot::{RebootError, reboot}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/reboot.rs b/system/linux/syscalls/src/reboot.rs new file mode 100644 index 00000000..e01d6da9 --- /dev/null +++ b/system/linux/syscalls/src/reboot.rs @@ -0,0 +1,84 @@ +use celer_system_linux_ctypes::{Int, Void}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`reboot`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RebootError { + Eperm, + Einval, + Efault, + Other(Errno), +} + +impl RebootError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Reboot the system or toggle Ctrl-Alt-Del handling. +/// +/// This wrapper preserves the raw magic values, command, and command-specific +/// argument pointer while mapping the raw return value into +/// `Result<(), RebootError>`. +/// +/// See [`sys::reboot`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Safety +/// If `cmd` causes the kernel to read `arg`, `arg` must be valid to read the +/// command-specific user data for the duration of the syscall. +/// +/// # Errors +/// - [`RebootError::Eperm`]: the caller lacks permission, or a delegated +/// suspend path reports permission or availability failure as `EPERM`. +/// - [`RebootError::Einval`]: the magic values or command are unsupported. +/// - [`RebootError::Efault`]: a command-specific user pointer could not be +/// read. +/// - [`RebootError::Other`]: another errno from command-specific reboot paths. +pub unsafe fn reboot( + magic: Int, + magic_too: Int, + cmd: Int, + arg: *const Void, +) -> Result<(), RebootError> { + // SAFETY: forwarded from this wrapper's safety contract. + let ret = unsafe { sys::reboot(magic, magic_too, cmd, arg) }; + unit_from_ret(ret as isize, RebootError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::ptr; + + use crate::Errno; + + use super::{RebootError, reboot}; + + #[test] + fn test_reboot_bad_magic_is_rejected_or_permission_denied() { + // SAFETY: this command does not cause the kernel to read `arg`. + let err = unsafe { reboot(0, 0, 0, ptr::null()) }.unwrap_err(); + + assert!(matches!(err, RebootError::Eperm | RebootError::Einval)); + } + + #[test] + fn test_reboot_error_mapping() { + assert_eq!(RebootError::from_errno(Errno::Eperm), RebootError::Eperm); + assert_eq!(RebootError::from_errno(Errno::Einval), RebootError::Einval); + assert_eq!(RebootError::from_errno(Errno::Efault), RebootError::Efault); + assert_eq!( + RebootError::from_errno(Errno::Ebusy), + RebootError::Other(Errno::Ebusy) + ); + } +} From f23307621eeeb569fc67fef2c5dd7a37e0d0da4a Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:09:51 +0200 Subject: [PATCH 079/135] feat(syscalls): wrap rename --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/rename.rs | 123 +++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/rename.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2db3f4b1..32202ae0 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -172,7 +172,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `readdir` - x86 - [x] `readlink` - x86, x86_64 - [x] `reboot` -- [ ] `rename` - x86, x86_64 +- [x] `rename` - x86, x86_64 - [ ] `rmdir` - x86, x86_64 - [ ] `select` - x86, x86_64 - [ ] `setdomainname` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 1375f339..d97c0db1 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -113,6 +113,8 @@ mod readdir; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod readlink; mod reboot; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod rename; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -221,6 +223,8 @@ pub use readdir::{ReaddirError, readdir}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use readlink::{ReadlinkError, readlink}; pub use reboot::{RebootError, reboot}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use rename::{RenameError, rename}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/rename.rs b/system/linux/syscalls/src/rename.rs new file mode 100644 index 00000000..e08aad43 --- /dev/null +++ b/system/linux/syscalls/src/rename.rs @@ -0,0 +1,123 @@ +use core::ffi::CStr; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`rename`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RenameError { + Enoent, + Eexist, + Exdev, + Eperm, + Efault, + Other(Errno), +} + +impl RenameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enoent => Self::Enoent, + Errno::Eexist => Self::Eexist, + Errno::Exdev => Self::Exdev, + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Rename the filesystem object named by `oldname` to `newname`. +/// +/// This safe wrapper takes two NUL-terminated [`CStr`] pathnames and maps the +/// raw syscall return value into `Result<(), RenameError>`. +/// +/// See [`sys::rename`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`RenameError::Enoent`]: a required path component or source object is +/// missing. +/// - [`RenameError::Eexist`]: the destination already exists in a path where +/// replacement is not allowed. +/// - [`RenameError::Exdev`]: the rename crosses mount points. +/// - [`RenameError::Eperm`]: permission, sticky-bit, or filesystem rules +/// rejected the rename. +/// - [`RenameError::Efault`]: the kernel could not read a pathname. +/// - [`RenameError::Other`]: another delegated pathname lookup, VFS, +/// filesystem, or security error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn rename(oldname: &CStr, newname: &CStr) -> Result<(), RenameError> { + // SAFETY: `CStr` values provide readable NUL-terminated pathnames. + let ret = unsafe { sys::rename(oldname.as_ptr(), newname.as_ptr()) }; + unit_from_ret(ret as isize, RenameError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, File}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{RenameError, rename}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_rename_ok() { + let old_path = temp_path("rename_old"); + let new_path = temp_path("rename_new"); + File::create(&old_path).unwrap(); + let old = + CString::new(old_path.as_os_str().as_encoded_bytes()).unwrap(); + let new = + CString::new(new_path.as_os_str().as_encoded_bytes()).unwrap(); + + rename(old.as_c_str(), new.as_c_str()).unwrap(); + + assert!(!old_path.exists()); + assert!(new_path.exists()); + fs::remove_file(new_path).unwrap(); + } + + #[test] + fn test_rename_missing_source() { + let old_path = temp_path("rename_missing_old"); + let new_path = temp_path("rename_missing_new"); + let old = + CString::new(old_path.as_os_str().as_encoded_bytes()).unwrap(); + let new = + CString::new(new_path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!( + rename(old.as_c_str(), new.as_c_str()), + Err(RenameError::Enoent) + ); + } + + #[test] + fn test_rename_error_mapping() { + assert_eq!(RenameError::from_errno(Errno::Enoent), RenameError::Enoent); + assert_eq!(RenameError::from_errno(Errno::Eexist), RenameError::Eexist); + assert_eq!(RenameError::from_errno(Errno::Exdev), RenameError::Exdev); + assert_eq!(RenameError::from_errno(Errno::Eperm), RenameError::Eperm); + assert_eq!(RenameError::from_errno(Errno::Efault), RenameError::Efault); + assert_eq!( + RenameError::from_errno(Errno::Eio), + RenameError::Other(Errno::Eio) + ); + } +} From d96d42814ad215864e49fbab888e4c4a5d4da01d Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:10:05 +0200 Subject: [PATCH 080/135] feat(syscalls): wrap rmdir --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/rmdir.rs | 120 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/rmdir.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 32202ae0..6e948f45 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -173,7 +173,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `readlink` - x86, x86_64 - [x] `reboot` - [x] `rename` - x86, x86_64 -- [ ] `rmdir` - x86, x86_64 +- [x] `rmdir` - x86, x86_64 - [ ] `select` - x86, x86_64 - [ ] `setdomainname` - [ ] `setgid16` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index d97c0db1..7dcbe7f8 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -115,6 +115,8 @@ mod readlink; mod reboot; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod rename; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod rmdir; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -225,6 +227,8 @@ pub use readlink::{ReadlinkError, readlink}; pub use reboot::{RebootError, reboot}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use rename::{RenameError, rename}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use rmdir::{RmdirError, rmdir}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/rmdir.rs b/system/linux/syscalls/src/rmdir.rs new file mode 100644 index 00000000..defbb8d3 --- /dev/null +++ b/system/linux/syscalls/src/rmdir.rs @@ -0,0 +1,120 @@ +use core::ffi::CStr; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`rmdir`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RmdirError { + Enoent, + Enotempty, + Einval, + Ebusy, + Eperm, + Efault, + Other(Errno), +} + +impl RmdirError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enoent => Self::Enoent, + Errno::Raw(39) => Self::Enotempty, + Errno::Einval => Self::Einval, + Errno::Ebusy => Self::Ebusy, + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Remove the empty directory named by `pathname`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return value into `Result<(), RmdirError>`. +/// +/// See [`sys::rmdir`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`RmdirError::Enoent`]: the path is empty or missing. +/// - [`RmdirError::Enotempty`]: the target directory is not empty. +/// - [`RmdirError::Einval`]: the target is invalid for `rmdir`. +/// - [`RmdirError::Ebusy`]: the target is busy. +/// - [`RmdirError::Eperm`]: permission, sticky-bit, or filesystem rules +/// rejected the removal. +/// - [`RmdirError::Efault`]: the kernel could not read `pathname`. +/// - [`RmdirError::Other`]: another delegated pathname lookup, VFS, +/// filesystem, or security error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn rmdir(pathname: &CStr) -> Result<(), RmdirError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::rmdir(pathname.as_ptr()) }; + unit_from_ret(ret as isize, RmdirError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{RmdirError, rmdir}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_rmdir_ok() { + let path = temp_path("rmdir_ok"); + fs::create_dir(&path).unwrap(); + let c_path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + rmdir(c_path.as_c_str()).unwrap(); + + assert!(!path.exists()); + } + + #[test] + fn test_rmdir_nonempty() { + let path = temp_path("rmdir_nonempty"); + fs::create_dir(&path).unwrap(); + fs::write(path.join("child"), b"child").unwrap(); + let c_path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(rmdir(c_path.as_c_str()), Err(RmdirError::Enotempty)); + + fs::remove_file(path.join("child")).unwrap(); + fs::remove_dir(path).unwrap(); + } + + #[test] + fn test_rmdir_error_mapping() { + assert_eq!(RmdirError::from_errno(Errno::Enoent), RmdirError::Enoent); + assert_eq!( + RmdirError::from_errno(Errno::Raw(39)), + RmdirError::Enotempty + ); + assert_eq!(RmdirError::from_errno(Errno::Einval), RmdirError::Einval); + assert_eq!(RmdirError::from_errno(Errno::Ebusy), RmdirError::Ebusy); + assert_eq!(RmdirError::from_errno(Errno::Eperm), RmdirError::Eperm); + assert_eq!(RmdirError::from_errno(Errno::Efault), RmdirError::Efault); + assert_eq!( + RmdirError::from_errno(Errno::Eio), + RmdirError::Other(Errno::Eio) + ); + } +} From ef67481505a4bdf7648daf4a1a909354e413bf22 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:10:19 +0200 Subject: [PATCH 081/135] feat(syscalls): wrap select --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/select.rs | 183 +++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/select.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 6e948f45..87f8fc97 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -174,7 +174,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `reboot` - [x] `rename` - x86, x86_64 - [x] `rmdir` - x86, x86_64 -- [ ] `select` - x86, x86_64 +- [x] `select` - x86, x86_64 - [ ] `setdomainname` - [ ] `setgid16` - x86 - [ ] `setgroups16` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 7dcbe7f8..bc64c355 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -119,6 +119,8 @@ mod rename; mod rmdir; #[cfg(target_arch = "x86")] mod stat; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod select; pub mod sys; mod uname; @@ -231,6 +233,8 @@ pub use rename::{RenameError, rename}; pub use rmdir::{RmdirError, rmdir}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use select::{SelectError, select}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/select.rs b/system/linux/syscalls/src/select.rs new file mode 100644 index 00000000..fac198d6 --- /dev/null +++ b/system/linux/syscalls/src/select.rs @@ -0,0 +1,183 @@ +use celer_system_linux_ctypes::{FdSet, Int, Timeval}; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`select`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SelectError { + Ebadf, + Efault, + Einval, + Enomem, + Eintr, + Other(Errno), +} + +impl SelectError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Eintr => Self::Eintr, + other => Self::Other(other), + } + } +} + +/// Wait for readiness changes on up to `nfds` file descriptors. +/// +/// This wrapper makes the descriptor sets and timeout explicit nullable Rust +/// references. Non-null descriptor sets are read and then overwritten by the +/// kernel; a non-null timeout is read and then overwritten with the remaining +/// time. +/// +/// `Ok(n)` is the total number of ready descriptor bits in the result sets. +/// +/// See [`sys::select`] for kernel behavior, ABI differences, pointer sizing +/// requirements, reachable errors, and source references. +/// +/// # Safety +/// The non-null descriptor-set references must cover the full kernel bitmap +/// size implied by the effective `nfds` on the running kernel. One [`FdSet`] +/// is sufficient only when that effective value does not exceed `1024`. +/// +/// # Errors +/// - [`SelectError::Ebadf`]: one requested descriptor is not open. +/// - [`SelectError::Efault`]: the kernel could not access a non-null pointer. +/// - [`SelectError::Einval`]: `nfds` or `timeout` values are invalid. +/// - [`SelectError::Enomem`]: kernel wait-table allocation failed. +/// - [`SelectError::Eintr`]: an unblocked signal interrupted the wait. +/// - [`SelectError::Other`]: another syscall error reported by the raw ABI. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub unsafe fn select( + nfds: Int, + readfds: Option<&mut FdSet>, + writefds: Option<&mut FdSet>, + exceptfds: Option<&mut FdSet>, + timeout: Option<&mut Timeval>, +) -> Result { + let readfds = readfds.map_or(core::ptr::null_mut(), |fds| fds); + let writefds = writefds.map_or(core::ptr::null_mut(), |fds| fds); + let exceptfds = exceptfds.map_or(core::ptr::null_mut(), |fds| fds); + let timeout = timeout.map_or(core::ptr::null_mut(), |timeout| timeout); + + // SAFETY: forwarded from this wrapper's safety contract. + let ret = + unsafe { sys::select(nfds, readfds, writefds, exceptfds, timeout) }; + result_from_ret(ret as isize, |ret| ret as usize, SelectError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{FdSet, Int, Timeval, UnsignedLong}; + + use crate::{Errno, sys}; + + use super::{SelectError, select}; + + fn empty_fd_set() -> FdSet { + FdSet { + #[cfg(target_arch = "x86")] + fds_bits: [0; 32], + #[cfg(target_arch = "x86_64")] + fds_bits: [0; 16], + } + } + + fn set_fd(set: &mut FdSet, fd: Int) { + let bits_per_long = UnsignedLong::BITS as usize; + let fd = fd as usize; + let word = fd / bits_per_long; + let bit = fd % bits_per_long; + + set.fds_bits[word] |= (1 as UnsignedLong) << bit; + } + + fn is_fd_set(set: &FdSet, fd: Int) -> bool { + let bits_per_long = UnsignedLong::BITS as usize; + let fd = fd as usize; + let word = fd / bits_per_long; + let bit = fd % bits_per_long; + + (set.fds_bits[word] & ((1 as UnsignedLong) << bit)) != 0 + } + + #[test] + fn test_select_zero_timeout_with_null_sets() { + let mut timeout = Timeval { + tv_sec: 0, + tv_usec: 0, + }; + + let ready = + unsafe { select(0, None, None, None, Some(&mut timeout)) }.unwrap(); + + assert_eq!(ready, 0); + assert_eq!(timeout.tv_sec, 0); + assert_eq!(timeout.tv_usec, 0); + } + + #[test] + fn test_select_pipe_read_end_ready() { + let mut fds = [0 as Int; 2]; + let rc = unsafe { sys::pipe(fds.as_mut_ptr()) }; + assert_eq!(rc, 0, "pipe failed: {rc}"); + let msg = [b'x']; + let written = + unsafe { sys::write(fds[1] as _, msg.as_ptr().cast(), msg.len()) }; + assert_eq!(written, 1, "write failed: {written}"); + + let mut readfds = empty_fd_set(); + set_fd(&mut readfds, fds[0]); + let mut timeout = Timeval { + tv_sec: 0, + tv_usec: 0, + }; + + let ready = unsafe { + select( + fds[0] + 1, + Some(&mut readfds), + None, + None, + Some(&mut timeout), + ) + } + .unwrap(); + + assert_eq!(ready, 1); + assert!(is_fd_set(&readfds, fds[0])); + assert_eq!(sys::close(fds[0]), 0); + assert_eq!(sys::close(fds[1]), 0); + } + + #[test] + fn test_select_negative_nfds() { + let mut timeout = Timeval { + tv_sec: 0, + tv_usec: 0, + }; + + assert_eq!( + unsafe { select(-1, None, None, None, Some(&mut timeout)) }, + Err(SelectError::Einval) + ); + } + + #[test] + fn test_select_error_mapping() { + assert_eq!(SelectError::from_errno(Errno::Ebadf), SelectError::Ebadf); + assert_eq!(SelectError::from_errno(Errno::Efault), SelectError::Efault); + assert_eq!(SelectError::from_errno(Errno::Einval), SelectError::Einval); + assert_eq!(SelectError::from_errno(Errno::Enomem), SelectError::Enomem); + assert_eq!(SelectError::from_errno(Errno::Eintr), SelectError::Eintr); + assert_eq!( + SelectError::from_errno(Errno::Eio), + SelectError::Other(Errno::Eio) + ); + } +} From 0712363d2fb4adac8b581b67c945212aa08f0d21 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:11:58 +0200 Subject: [PATCH 082/135] feat(syscalls): wrap setdomainname --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/setdomainname.rs | 83 ++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setdomainname.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 87f8fc97..fc9166ef 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -175,7 +175,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `rename` - x86, x86_64 - [x] `rmdir` - x86, x86_64 - [x] `select` - x86, x86_64 -- [ ] `setdomainname` +- [x] `setdomainname` - [ ] `setgid16` - x86 - [ ] `setgroups16` - x86 - [ ] `sethostname` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index bc64c355..51ae8d99 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -121,6 +121,7 @@ mod rmdir; mod stat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod select; +mod setdomainname; pub mod sys; mod uname; @@ -235,6 +236,7 @@ pub use rmdir::{RmdirError, rmdir}; pub use stat::{OldstatError, oldstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use select::{SelectError, select}; +pub use setdomainname::{SetdomainnameError, setdomainname}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/setdomainname.rs b/system/linux/syscalls/src/setdomainname.rs new file mode 100644 index 00000000..d9620f6e --- /dev/null +++ b/system/linux/syscalls/src/setdomainname.rs @@ -0,0 +1,83 @@ +use celer_system_linux_ctypes::{Char, Int}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setdomainname`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetdomainnameError { + Eperm, + Einval, + Efault, + Other(Errno), +} + +impl SetdomainnameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Set the domain name for the current UTS namespace from raw bytes. +/// +/// This safe wrapper passes `name.len()` as the raw signed byte count. The +/// bytes are not required to be NUL-terminated. +/// +/// See [`sys::setdomainname`] for kernel behavior, historical notes, +/// reachable errors, and source references. +/// +/// # Errors +/// - [`SetdomainnameError::Eperm`]: the caller lacks permission. +/// - [`SetdomainnameError::Einval`]: `name` is too long for the kernel limit. +/// - [`SetdomainnameError::Efault`]: the kernel could not read `name`. +/// - [`SetdomainnameError::Other`]: another syscall error reported by the raw +/// ABI. +pub fn setdomainname(name: &[u8]) -> Result<(), SetdomainnameError> { + let len = + Int::try_from(name.len()).map_err(|_| SetdomainnameError::Einval)?; + // SAFETY: `name` is readable for `len` bytes. + let ret = unsafe { sys::setdomainname(name.as_ptr().cast::(), len) }; + unit_from_ret(ret as isize, SetdomainnameError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{SetdomainnameError, setdomainname}; + + #[test] + fn test_setdomainname_too_long_or_permission_denied() { + let err = setdomainname(&[b'a'; 65]).unwrap_err(); + assert!(matches!( + err, + SetdomainnameError::Eperm | SetdomainnameError::Einval + )); + } + + #[test] + fn test_setdomainname_error_mapping() { + assert_eq!( + SetdomainnameError::from_errno(Errno::Eperm), + SetdomainnameError::Eperm + ); + assert_eq!( + SetdomainnameError::from_errno(Errno::Einval), + SetdomainnameError::Einval + ); + assert_eq!( + SetdomainnameError::from_errno(Errno::Efault), + SetdomainnameError::Efault + ); + assert_eq!( + SetdomainnameError::from_errno(Errno::Eio), + SetdomainnameError::Other(Errno::Eio) + ); + } +} From 2678abed247c028c2f0de1689e10646d48fd7849 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:12:27 +0200 Subject: [PATCH 083/135] feat(syscalls): wrap setgid16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/setgid.rs | 79 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setgid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index fc9166ef..481c33e7 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -176,7 +176,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `rmdir` - x86, x86_64 - [x] `select` - x86, x86_64 - [x] `setdomainname` -- [ ] `setgid16` - x86 +- [x] `setgid16` - x86 - [ ] `setgroups16` - x86 - [ ] `sethostname` - [ ] `setitimer` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 51ae8d99..64f3e495 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -122,6 +122,8 @@ mod stat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod select; mod setdomainname; +#[cfg(target_arch = "x86")] +mod setgid; pub mod sys; mod uname; @@ -237,6 +239,8 @@ pub use stat::{OldstatError, oldstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use select::{SelectError, select}; pub use setdomainname::{SetdomainnameError, setdomainname}; +#[cfg(target_arch = "x86")] +pub use setgid::{Setgid16Error, setgid16}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/setgid.rs b/system/linux/syscalls/src/setgid.rs new file mode 100644 index 00000000..916a4ec9 --- /dev/null +++ b/system/linux/syscalls/src/setgid.rs @@ -0,0 +1,79 @@ +use celer_system_linux_ctypes::OldGidT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setgid16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Setgid16Error { + Einval, + Enomem, + Eperm, + Other(Errno), +} + +impl Setgid16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Eperm => Self::Eperm, + other => Self::Other(other), + } + } +} + +/// Set the calling process's group ID through the legacy x86 `setgid16` ABI. +/// +/// This safe wrapper preserves the raw 16-bit gid argument and maps the raw +/// syscall return value into `Result<(), Setgid16Error>`. +/// +/// See [`sys::setgid16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Setgid16Error::Einval`]: the gid is invalid in the current user +/// namespace. +/// - [`Setgid16Error::Enomem`]: the kernel could not allocate credentials. +/// - [`Setgid16Error::Eperm`]: the caller lacks permission for the requested +/// transition. +/// - [`Setgid16Error::Other`]: another errno from credential or security +/// checks. +#[cfg(target_arch = "x86")] +pub fn setgid16(gid: OldGidT) -> Result<(), Setgid16Error> { + let ret = sys::setgid16(gid); + unit_from_ret(ret as isize, Setgid16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::{Errno, sys}; + + use super::{Setgid16Error, setgid16}; + + #[test] + fn test_setgid16_current_gid() { + assert_eq!(setgid16(sys::getgid16()), Ok(())); + } + + #[test] + fn test_setgid16_error_mapping() { + assert_eq!( + Setgid16Error::from_errno(Errno::Einval), + Setgid16Error::Einval + ); + assert_eq!( + Setgid16Error::from_errno(Errno::Enomem), + Setgid16Error::Enomem + ); + assert_eq!( + Setgid16Error::from_errno(Errno::Eperm), + Setgid16Error::Eperm + ); + assert_eq!( + Setgid16Error::from_errno(Errno::Eio), + Setgid16Error::Other(Errno::Eio) + ); + } +} From 22d7a1575deb7c534983bac18a968fd5c9c7db48 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:12:38 +0200 Subject: [PATCH 084/135] feat(syscalls): wrap setgroups16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/setgroups.rs | 103 +++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setgroups.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 481c33e7..a402d6d4 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -177,7 +177,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `select` - x86, x86_64 - [x] `setdomainname` - [x] `setgid16` - x86 -- [ ] `setgroups16` - x86 +- [x] `setgroups16` - x86 - [ ] `sethostname` - [ ] `setitimer` - [ ] `setpgid` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 64f3e495..0479946b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -124,6 +124,8 @@ mod select; mod setdomainname; #[cfg(target_arch = "x86")] mod setgid; +#[cfg(target_arch = "x86")] +mod setgroups; pub mod sys; mod uname; @@ -241,6 +243,8 @@ pub use select::{SelectError, select}; pub use setdomainname::{SetdomainnameError, setdomainname}; #[cfg(target_arch = "x86")] pub use setgid::{Setgid16Error, setgid16}; +#[cfg(target_arch = "x86")] +pub use setgroups::{Setgroups16Error, setgroups16}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/setgroups.rs b/system/linux/syscalls/src/setgroups.rs new file mode 100644 index 00000000..cedb309e --- /dev/null +++ b/system/linux/syscalls/src/setgroups.rs @@ -0,0 +1,103 @@ +use celer_system_linux_ctypes::{Int, OldGidT}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setgroups16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Setgroups16Error { + Efault, + Einval, + Enomem, + Enosys, + Eperm, + Other(Errno), +} + +impl Setgroups16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Enosys => Self::Enosys, + Errno::Eperm => Self::Eperm, + other => Self::Other(other), + } + } +} + +/// Replace supplementary group IDs through the legacy x86 `setgroups16` ABI. +/// +/// This safe wrapper accepts a shared slice and passes its length plus element +/// pointer to the raw syscall. An empty slice requests an empty supplementary +/// group set. +/// +/// See [`sys::setgroups16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Setgroups16Error::Efault`]: the kernel could not read the group list. +/// - [`Setgroups16Error::Einval`]: the slice is too large for the raw ABI, too +/// many groups were supplied, or a gid is invalid in the current namespace. +/// - [`Setgroups16Error::Enomem`]: the kernel could not allocate credentials. +/// - [`Setgroups16Error::Enosys`]: the legacy ABI is not configured. +/// - [`Setgroups16Error::Eperm`]: the caller lacks permission. +/// - [`Setgroups16Error::Other`]: another errno from credential or security +/// checks. +#[cfg(target_arch = "x86")] +pub fn setgroups16(grouplist: &[OldGidT]) -> Result<(), Setgroups16Error> { + let len = + Int::try_from(grouplist.len()).map_err(|_| Setgroups16Error::Einval)?; + // SAFETY: `grouplist` is readable for `len` entries; empty slices pass a + // non-null dangling pointer but the kernel does not read it when len is 0. + let ret = unsafe { sys::setgroups16(len, grouplist.as_ptr()) }; + unit_from_ret(ret as isize, Setgroups16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{Setgroups16Error, setgroups16}; + + #[test] + fn test_setgroups16_empty_slice_fails_or_succeeds_by_permissions() { + let result = setgroups16(&[]); + assert!(matches!( + result, + Ok(()) + | Err(Setgroups16Error::Eperm) + | Err(Setgroups16Error::Enosys) + )); + } + + #[test] + fn test_setgroups16_error_mapping() { + assert_eq!( + Setgroups16Error::from_errno(Errno::Efault), + Setgroups16Error::Efault + ); + assert_eq!( + Setgroups16Error::from_errno(Errno::Einval), + Setgroups16Error::Einval + ); + assert_eq!( + Setgroups16Error::from_errno(Errno::Enomem), + Setgroups16Error::Enomem + ); + assert_eq!( + Setgroups16Error::from_errno(Errno::Enosys), + Setgroups16Error::Enosys + ); + assert_eq!( + Setgroups16Error::from_errno(Errno::Eperm), + Setgroups16Error::Eperm + ); + assert_eq!( + Setgroups16Error::from_errno(Errno::Eio), + Setgroups16Error::Other(Errno::Eio) + ); + } +} From c195931be726f38d1b1ef28491afc11ebc4e9931 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:12:50 +0200 Subject: [PATCH 085/135] feat(syscalls): wrap sethostname --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/sethostname.rs | 83 ++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sethostname.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a402d6d4..76f78e3e 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -178,7 +178,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setdomainname` - [x] `setgid16` - x86 - [x] `setgroups16` - x86 -- [ ] `sethostname` +- [x] `sethostname` - [ ] `setitimer` - [ ] `setpgid` - [ ] `setpriority` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 0479946b..ec74cce2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -126,6 +126,7 @@ mod setdomainname; mod setgid; #[cfg(target_arch = "x86")] mod setgroups; +mod sethostname; pub mod sys; mod uname; @@ -245,6 +246,7 @@ pub use setdomainname::{SetdomainnameError, setdomainname}; pub use setgid::{Setgid16Error, setgid16}; #[cfg(target_arch = "x86")] pub use setgroups::{Setgroups16Error, setgroups16}; +pub use sethostname::{SethostnameError, sethostname}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/sethostname.rs b/system/linux/syscalls/src/sethostname.rs new file mode 100644 index 00000000..dacc71a8 --- /dev/null +++ b/system/linux/syscalls/src/sethostname.rs @@ -0,0 +1,83 @@ +use celer_system_linux_ctypes::{Char, Int}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`sethostname`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SethostnameError { + Eperm, + Einval, + Efault, + Other(Errno), +} + +impl SethostnameError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Set the hostname for the current UTS namespace from raw bytes. +/// +/// This safe wrapper passes `name.len()` as the raw signed byte count. The +/// bytes are not required to be NUL-terminated. +/// +/// See [`sys::sethostname`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SethostnameError::Eperm`]: the caller lacks permission. +/// - [`SethostnameError::Einval`]: `name` is too long for the kernel limit. +/// - [`SethostnameError::Efault`]: the kernel could not read `name`. +/// - [`SethostnameError::Other`]: another syscall error reported by the raw +/// ABI. +pub fn sethostname(name: &[u8]) -> Result<(), SethostnameError> { + let len = + Int::try_from(name.len()).map_err(|_| SethostnameError::Einval)?; + // SAFETY: `name` is readable for `len` bytes. + let ret = unsafe { sys::sethostname(name.as_ptr().cast::(), len) }; + unit_from_ret(ret as isize, SethostnameError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{SethostnameError, sethostname}; + + #[test] + fn test_sethostname_too_long_or_permission_denied() { + let err = sethostname(&[b'a'; 65]).unwrap_err(); + assert!(matches!( + err, + SethostnameError::Eperm | SethostnameError::Einval + )); + } + + #[test] + fn test_sethostname_error_mapping() { + assert_eq!( + SethostnameError::from_errno(Errno::Eperm), + SethostnameError::Eperm + ); + assert_eq!( + SethostnameError::from_errno(Errno::Einval), + SethostnameError::Einval + ); + assert_eq!( + SethostnameError::from_errno(Errno::Efault), + SethostnameError::Efault + ); + assert_eq!( + SethostnameError::from_errno(Errno::Eio), + SethostnameError::Other(Errno::Eio) + ); + } +} From 847fb5715f053c37eb6d6933878d56bf8599ed88 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:13:02 +0200 Subject: [PATCH 086/135] feat(syscalls): wrap setitimer --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/setitimer.rs | 132 +++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setitimer.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 76f78e3e..7957583e 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -179,7 +179,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setgid16` - x86 - [x] `setgroups16` - x86 - [x] `sethostname` -- [ ] `setitimer` +- [x] `setitimer` - [ ] `setpgid` - [ ] `setpriority` - [ ] `setregid16` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index ec74cce2..6f43f5ed 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -127,6 +127,7 @@ mod setgid; #[cfg(target_arch = "x86")] mod setgroups; mod sethostname; +mod setitimer; pub mod sys; mod uname; @@ -247,6 +248,7 @@ pub use setgid::{Setgid16Error, setgid16}; #[cfg(target_arch = "x86")] pub use setgroups::{Setgroups16Error, setgroups16}; pub use sethostname::{SethostnameError, sethostname}; +pub use setitimer::{SetitimerError, setitimer}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/setitimer.rs b/system/linux/syscalls/src/setitimer.rs new file mode 100644 index 00000000..743fd95f --- /dev/null +++ b/system/linux/syscalls/src/setitimer.rs @@ -0,0 +1,132 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, Itimerval}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setitimer`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetitimerError { + Einval, + Efault, + Other(Errno), +} + +impl SetitimerError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + other => Self::Other(other), + } + } +} + +/// Set an interval timer and optionally return its previous state. +/// +/// This wrapper accepts an optional initialized input timer and an optional +/// uninitialized output slot for the previous timer value. +/// +/// See [`sys::setitimer`] for kernel behavior, historical notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SetitimerError::Einval`]: `which` or timer field values are invalid. +/// - [`SetitimerError::Efault`]: the kernel could not read `value` or write +/// `old_value`. +/// - [`SetitimerError::Other`]: another syscall error reported by the raw ABI. +pub fn setitimer( + which: Int, + value: Option<&Itimerval>, + old_value: Option<&mut MaybeUninit>, +) -> Result<(), SetitimerError> { + let value = value.map_or(core::ptr::null(), |value| value); + let old_value = old_value + .map_or(core::ptr::null_mut(), |old_value| old_value.as_mut_ptr()); + + // SAFETY: `value` is either null or readable for one `Itimerval`; `old_value` + // is either null or writable for one `Itimerval`. + let ret = unsafe { sys::setitimer(which, value, old_value) }; + unit_from_ret(ret as isize, SetitimerError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{ + ITIMER_PROF, ITIMER_REAL, ITIMER_VIRTUAL, Itimerval, Timeval, + }; + + use crate::{Errno, sys::test_support::process_global_state_guard}; + + use super::{SetitimerError, setitimer}; + + fn zero_itimerval() -> Itimerval { + Itimerval { + it_interval: Timeval { + tv_sec: 0, + tv_usec: 0, + }, + it_value: Timeval { + tv_sec: 0, + tv_usec: 0, + }, + } + } + + #[test] + fn test_setitimer_zero_value_succeeds() { + let _guard = process_global_state_guard(); + let zero = zero_itimerval(); + + setitimer(ITIMER_REAL, Some(&zero), None).unwrap(); + } + + #[test] + fn test_setitimer_none_value_clears_timer_and_writes_old_value() { + let _guard = process_global_state_guard(); + let mut old = MaybeUninit::::uninit(); + + setitimer(ITIMER_REAL, None, Some(&mut old)).unwrap(); + } + + #[test] + fn test_setitimer_invalid_selector() { + let _guard = process_global_state_guard(); + let zero = zero_itimerval(); + + assert_eq!( + setitimer(3, Some(&zero), None), + Err(SetitimerError::Einval) + ); + } + + #[test] + fn test_setitimer_supported_selectors_accept_zero_value() { + let _guard = process_global_state_guard(); + let zero = zero_itimerval(); + + for which in [ITIMER_REAL, ITIMER_VIRTUAL, ITIMER_PROF] { + setitimer(which, Some(&zero), None).unwrap(); + } + } + + #[test] + fn test_setitimer_error_mapping() { + assert_eq!( + SetitimerError::from_errno(Errno::Einval), + SetitimerError::Einval + ); + assert_eq!( + SetitimerError::from_errno(Errno::Efault), + SetitimerError::Efault + ); + assert_eq!( + SetitimerError::from_errno(Errno::Enomem), + SetitimerError::Other(Errno::Enomem) + ); + } +} From c0b771a7bcd642abe4291d8d7f522415b1f9d1b0 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:13:10 +0200 Subject: [PATCH 087/135] feat(syscalls): wrap setpgid --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 10 ++-- system/linux/syscalls/src/setpgid.rs | 78 ++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 system/linux/syscalls/src/setpgid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 7957583e..686bfa3c 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -180,7 +180,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setgroups16` - x86 - [x] `sethostname` - [x] `setitimer` -- [ ] `setpgid` +- [x] `setpgid` - [ ] `setpriority` - [ ] `setregid16` - x86 - [ ] `setreuid16` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 6f43f5ed..fe197036 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -117,8 +117,6 @@ mod reboot; mod rename; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod rmdir; -#[cfg(target_arch = "x86")] -mod stat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod select; mod setdomainname; @@ -128,6 +126,9 @@ mod setgid; mod setgroups; mod sethostname; mod setitimer; +mod setpgid; +#[cfg(target_arch = "x86")] +mod stat; pub mod sys; mod uname; @@ -238,8 +239,6 @@ pub use reboot::{RebootError, reboot}; pub use rename::{RenameError, rename}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use rmdir::{RmdirError, rmdir}; -#[cfg(target_arch = "x86")] -pub use stat::{OldstatError, oldstat}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use select::{SelectError, select}; pub use setdomainname::{SetdomainnameError, setdomainname}; @@ -249,6 +248,9 @@ pub use setgid::{Setgid16Error, setgid16}; pub use setgroups::{Setgroups16Error, setgroups16}; pub use sethostname::{SethostnameError, sethostname}; pub use setitimer::{SetitimerError, setitimer}; +pub use setpgid::{SetpgidError, setpgid}; +#[cfg(target_arch = "x86")] +pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/setpgid.rs b/system/linux/syscalls/src/setpgid.rs new file mode 100644 index 00000000..de469cd8 --- /dev/null +++ b/system/linux/syscalls/src/setpgid.rs @@ -0,0 +1,78 @@ +use celer_system_linux_ctypes::PidT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setpgid`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetpgidError { + Einval, + Esrch, + Eperm, + Eacces, + Other(Errno), +} + +impl SetpgidError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Esrch => Self::Esrch, + Errno::Eperm => Self::Eperm, + Errno::Eacces => Self::Eacces, + other => Self::Other(other), + } + } +} + +/// Set a process group ID. +/// +/// This safe wrapper preserves the raw `pid` and `pgid` selector values and +/// maps the raw syscall return value into `Result<(), SetpgidError>`. +/// +/// See [`sys::setpgid`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SetpgidError::Einval`]: `pgid` is negative or the target task is not a +/// thread-group leader. +/// - [`SetpgidError::Esrch`]: `pid` does not identify an eligible task. +/// - [`SetpgidError::Eperm`]: session or leadership checks failed. +/// - [`SetpgidError::Eacces`]: the target has already executed a new program +/// image in the relevant parent/session branch. +/// - [`SetpgidError::Other`]: another errno from security hooks. +pub fn setpgid(pid: PidT, pgid: PidT) -> Result<(), SetpgidError> { + let ret = sys::setpgid(pid, pgid); + unit_from_ret(ret as isize, SetpgidError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{SetpgidError, setpgid}; + + #[test] + fn test_setpgid_negative_pgid() { + assert_eq!(setpgid(0, -1), Err(SetpgidError::Einval)); + } + + #[test] + fn test_setpgid_error_mapping() { + assert_eq!( + SetpgidError::from_errno(Errno::Einval), + SetpgidError::Einval + ); + assert_eq!(SetpgidError::from_errno(Errno::Esrch), SetpgidError::Esrch); + assert_eq!(SetpgidError::from_errno(Errno::Eperm), SetpgidError::Eperm); + assert_eq!( + SetpgidError::from_errno(Errno::Eacces), + SetpgidError::Eacces + ); + assert_eq!( + SetpgidError::from_errno(Errno::Eio), + SetpgidError::Other(Errno::Eio) + ); + } +} From 1a7b69c293d8f2f2b3aa1818b4e9d375289f0d6b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:21:18 +0200 Subject: [PATCH 088/135] feat(syscalls): wrap setpriority --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/setpriority.rs | 105 +++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setpriority.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 686bfa3c..be3fc778 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -181,7 +181,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sethostname` - [x] `setitimer` - [x] `setpgid` -- [ ] `setpriority` +- [x] `setpriority` - [ ] `setregid16` - x86 - [ ] `setreuid16` - x86 - [ ] `setrlimit` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index fe197036..f5173567 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -127,6 +127,7 @@ mod setgroups; mod sethostname; mod setitimer; mod setpgid; +mod setpriority; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -249,6 +250,7 @@ pub use setgroups::{Setgroups16Error, setgroups16}; pub use sethostname::{SethostnameError, sethostname}; pub use setitimer::{SetitimerError, setitimer}; pub use setpgid::{SetpgidError, setpgid}; +pub use setpriority::{SetpriorityError, setpriority}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/setpriority.rs b/system/linux/syscalls/src/setpriority.rs new file mode 100644 index 00000000..b8ed3929 --- /dev/null +++ b/system/linux/syscalls/src/setpriority.rs @@ -0,0 +1,105 @@ +use celer_system_linux_ctypes::Int; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`setpriority`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetpriorityError { + Eacces, + Einval, + Eperm, + Esrch, + Other(Errno), +} + +impl SetpriorityError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eacces => Self::Eacces, + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + Errno::Esrch => Self::Esrch, + errno => Self::Other(errno), + } + } +} + +/// Set the nice value for selected processes. +/// +/// This safe wrapper preserves the raw `which`, `who`, and `niceval` selector +/// values and maps the raw syscall return value into +/// `Result<(), SetpriorityError>`. +/// +/// See [`sys::setpriority`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SetpriorityError::Eacces`]: the caller tried to lower a nice value +/// without sufficient privilege. +/// - [`SetpriorityError::Einval`]: `which` is outside the supported selector +/// range. +/// - [`SetpriorityError::Eperm`]: the caller lacks permission for a matched +/// task. +/// - [`SetpriorityError::Esrch`]: no task matched the selected target. +/// - [`SetpriorityError::Other`]: another errno from security hooks. +pub fn setpriority( + which: Int, + who: Int, + niceval: Int, +) -> Result<(), SetpriorityError> { + let ret = sys::setpriority(which, who, niceval); + unit_from_ret(ret as isize, SetpriorityError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::PRIO_PROCESS; + + use super::{SetpriorityError, setpriority}; + use crate::Errno; + + #[test] + fn test_setpriority_current_process_nice_19() { + assert_eq!(setpriority(PRIO_PROCESS, 0, 19), Ok(())); + } + + #[test] + fn test_setpriority_invalid_selector() { + assert_eq!(setpriority(3, 0, 0), Err(SetpriorityError::Einval)); + } + + #[test] + fn test_setpriority_missing_process() { + assert_eq!( + setpriority(PRIO_PROCESS, -1, 0), + Err(SetpriorityError::Esrch) + ); + } + + #[test] + fn test_setpriority_error_mapping() { + assert_eq!( + SetpriorityError::from_errno(Errno::Eacces), + SetpriorityError::Eacces + ); + assert_eq!( + SetpriorityError::from_errno(Errno::Einval), + SetpriorityError::Einval + ); + assert_eq!( + SetpriorityError::from_errno(Errno::Eperm), + SetpriorityError::Eperm + ); + assert_eq!( + SetpriorityError::from_errno(Errno::Esrch), + SetpriorityError::Esrch + ); + assert_eq!( + SetpriorityError::from_errno(Errno::Eio), + SetpriorityError::Other(Errno::Eio) + ); + } +} From 330ed90cd3b3c7b95da6c4247936a051746b2ead Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:21:46 +0200 Subject: [PATCH 089/135] feat(syscalls): wrap setregid16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/setregid.rs | 87 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setregid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index be3fc778..dc546b93 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -182,7 +182,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setitimer` - [x] `setpgid` - [x] `setpriority` -- [ ] `setregid16` - x86 +- [x] `setregid16` - x86 - [ ] `setreuid16` - x86 - [ ] `setrlimit` - [ ] `setsid` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index f5173567..899bb603 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -129,6 +129,8 @@ mod setitimer; mod setpgid; mod setpriority; #[cfg(target_arch = "x86")] +mod setregid; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -252,6 +254,8 @@ pub use setitimer::{SetitimerError, setitimer}; pub use setpgid::{SetpgidError, setpgid}; pub use setpriority::{SetpriorityError, setpriority}; #[cfg(target_arch = "x86")] +pub use setregid::{Setregid16Error, setregid16}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/setregid.rs b/system/linux/syscalls/src/setregid.rs new file mode 100644 index 00000000..72358967 --- /dev/null +++ b/system/linux/syscalls/src/setregid.rs @@ -0,0 +1,87 @@ +use celer_system_linux_ctypes::OldGidT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setregid16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Setregid16Error { + Einval, + Enomem, + Enosys, + Eperm, + Other(Errno), +} + +impl Setregid16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Enosys => Self::Enosys, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Set the real and/or effective group ID through the legacy x86 +/// `setregid16` ABI. +/// +/// This safe wrapper preserves the raw 16-bit ID values. Pass +/// `OldGidT::MAX` for either argument to request no change. +/// +/// See [`sys::setregid16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Setregid16Error::Einval`]: a requested gid is invalid in the current +/// namespace. +/// - [`Setregid16Error::Enomem`]: the kernel could not allocate credentials. +/// - [`Setregid16Error::Enosys`]: the legacy ABI is not configured. +/// - [`Setregid16Error::Eperm`]: the requested ID change is not permitted. +/// - [`Setregid16Error::Other`]: another errno from credential or security +/// hooks. +#[cfg(target_arch = "x86")] +pub fn setregid16(rgid: OldGidT, egid: OldGidT) -> Result<(), Setregid16Error> { + let ret = sys::setregid16(rgid, egid); + unit_from_ret(ret as isize, Setregid16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{Setregid16Error, setregid16}; + use crate::{getegid16, getgid16}; + + #[test] + fn test_setregid16_current_ids() { + assert_eq!(setregid16(getgid16(), getegid16()), Ok(())); + } + + #[test] + fn test_setregid16_error_mapping() { + assert_eq!( + Setregid16Error::from_errno(Errno::Einval), + Setregid16Error::Einval + ); + assert_eq!( + Setregid16Error::from_errno(Errno::Enomem), + Setregid16Error::Enomem + ); + assert_eq!( + Setregid16Error::from_errno(Errno::Enosys), + Setregid16Error::Enosys + ); + assert_eq!( + Setregid16Error::from_errno(Errno::Eperm), + Setregid16Error::Eperm + ); + assert_eq!( + Setregid16Error::from_errno(Errno::Eio), + Setregid16Error::Other(Errno::Eio) + ); + } +} From 1d1ab69b545d8a726cd8af9d134af41ee899965d Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:22:12 +0200 Subject: [PATCH 090/135] feat(syscalls): wrap setreuid16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/setreuid.rs | 88 ++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setreuid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index dc546b93..020d3f85 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -183,7 +183,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setpgid` - [x] `setpriority` - [x] `setregid16` - x86 -- [ ] `setreuid16` - x86 +- [x] `setreuid16` - x86 - [ ] `setrlimit` - [ ] `setsid` - [ ] `settimeofday` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 899bb603..e3fe819b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -131,6 +131,8 @@ mod setpriority; #[cfg(target_arch = "x86")] mod setregid; #[cfg(target_arch = "x86")] +mod setreuid; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -256,6 +258,8 @@ pub use setpriority::{SetpriorityError, setpriority}; #[cfg(target_arch = "x86")] pub use setregid::{Setregid16Error, setregid16}; #[cfg(target_arch = "x86")] +pub use setreuid::{Setreuid16Error, setreuid16}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/setreuid.rs b/system/linux/syscalls/src/setreuid.rs new file mode 100644 index 00000000..fb29c553 --- /dev/null +++ b/system/linux/syscalls/src/setreuid.rs @@ -0,0 +1,88 @@ +use celer_system_linux_ctypes::OldUidT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setreuid16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Setreuid16Error { + Eagain, + Einval, + Enomem, + Eperm, + Other(Errno), +} + +impl Setreuid16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eagain => Self::Eagain, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Set the real and/or effective user ID through the legacy x86 +/// `setreuid16` ABI. +/// +/// This safe wrapper preserves the raw 16-bit ID values. Pass +/// `OldUidT::MAX` for either argument to request no change. +/// +/// See [`sys::setreuid16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Setreuid16Error::Eagain`]: the kernel could not allocate per-user +/// accounting state. +/// - [`Setreuid16Error::Einval`]: a requested uid is invalid in the current +/// namespace. +/// - [`Setreuid16Error::Enomem`]: the kernel could not allocate credentials. +/// - [`Setreuid16Error::Eperm`]: the requested ID change is not permitted. +/// - [`Setreuid16Error::Other`]: another errno from credential or security +/// hooks. +#[cfg(target_arch = "x86")] +pub fn setreuid16(ruid: OldUidT, euid: OldUidT) -> Result<(), Setreuid16Error> { + let ret = sys::setreuid16(ruid, euid); + unit_from_ret(ret as isize, Setreuid16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{Setreuid16Error, setreuid16}; + use crate::{geteuid16, getuid16}; + + #[test] + fn test_setreuid16_current_ids() { + assert_eq!(setreuid16(getuid16(), geteuid16()), Ok(())); + } + + #[test] + fn test_setreuid16_error_mapping() { + assert_eq!( + Setreuid16Error::from_errno(Errno::Eagain), + Setreuid16Error::Eagain + ); + assert_eq!( + Setreuid16Error::from_errno(Errno::Einval), + Setreuid16Error::Einval + ); + assert_eq!( + Setreuid16Error::from_errno(Errno::Enomem), + Setreuid16Error::Enomem + ); + assert_eq!( + Setreuid16Error::from_errno(Errno::Eperm), + Setreuid16Error::Eperm + ); + assert_eq!( + Setreuid16Error::from_errno(Errno::Eio), + Setreuid16Error::Other(Errno::Eio) + ); + } +} From 728f6c28763e23b98f6835610670869ed780182b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:22:48 +0200 Subject: [PATCH 091/135] feat(syscalls): wrap setrlimit --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 3 + system/linux/syscalls/src/setrlimit.rs | 145 +++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setrlimit.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 020d3f85..f9f959f8 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -184,7 +184,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setpriority` - [x] `setregid16` - x86 - [x] `setreuid16` - x86 -- [ ] `setrlimit` +- [x] `setrlimit` - [ ] `setsid` - [ ] `settimeofday` - [ ] `setuid16` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e3fe819b..49985da1 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -132,6 +132,7 @@ mod setpriority; mod setregid; #[cfg(target_arch = "x86")] mod setreuid; +mod setrlimit; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -259,6 +260,7 @@ pub use setpriority::{SetpriorityError, setpriority}; pub use setregid::{Setregid16Error, setregid16}; #[cfg(target_arch = "x86")] pub use setreuid::{Setreuid16Error, setreuid16}; +pub use setrlimit::{SetrlimitError, setrlimit}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; @@ -275,4 +277,5 @@ pub mod linux_1_0 { }; pub use super::newfstat::{NewfstatError, newfstat_1_0 as newfstat}; pub use super::newlstat::{NewlstatError, newlstat_1_0 as newlstat}; + pub use super::setrlimit::{SetrlimitError, setrlimit_1_0 as setrlimit}; } diff --git a/system/linux/syscalls/src/setrlimit.rs b/system/linux/syscalls/src/setrlimit.rs new file mode 100644 index 00000000..47597e6c --- /dev/null +++ b/system/linux/syscalls/src/setrlimit.rs @@ -0,0 +1,145 @@ +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::Rlimit as Linux10Rlimit; +use celer_system_linux_ctypes::{Rlimit, UnsignedInt}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setrlimit`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetrlimitError { + Efault, + Einval, + Eperm, + Other(Errno), +} + +impl SetrlimitError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Set the current task's soft and hard resource limits for one resource. +/// +/// This safe wrapper replaces the raw input pointer with `&Rlimit`. +/// +/// See [`sys::setrlimit`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SetrlimitError::Efault`]: the kernel could not read `rlim`. +/// - [`SetrlimitError::Einval`]: `resource` is unsupported or the requested +/// limits are malformed. +/// - [`SetrlimitError::Eperm`]: the requested update exceeds the caller's +/// authority. +/// - [`SetrlimitError::Other`]: another errno from security hooks. +pub fn setrlimit( + resource: UnsignedInt, + rlim: &Rlimit, +) -> Result<(), SetrlimitError> { + // SAFETY: `rlim` is readable for one `Rlimit`. + let ret = unsafe { sys::setrlimit(resource, rlim) }; + unit_from_ret(ret as isize, SetrlimitError::from_errno) +} + +/// Set Linux 1.0 soft and hard resource limits through the historical x86 ABI. +/// +/// This safe wrapper replaces the raw input pointer with +/// `&linux_1_0::Rlimit`. +/// +/// See [`sys::linux_1_0::setrlimit`] for kernel behavior, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`SetrlimitError::Einval`]: `resource` is outside Linux 1.0's resource +/// table. +/// - [`SetrlimitError::Eperm`]: the requested update exceeds the caller's +/// authority. +#[cfg(target_arch = "x86")] +pub fn setrlimit_1_0( + resource: UnsignedInt, + rlim: &Linux10Rlimit, +) -> Result<(), SetrlimitError> { + // SAFETY: `rlim` is readable for one historical `Rlimit`. + let ret = unsafe { sys::linux_1_0::setrlimit(resource, rlim) }; + unit_from_ret(ret as isize, SetrlimitError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + #[cfg(target_arch = "x86")] + use celer_system_linux_ctypes::linux_1_0::Rlimit as Linux10Rlimit; + use celer_system_linux_ctypes::{Rlimit, UnsignedInt}; + + #[cfg(target_arch = "x86")] + use super::setrlimit_1_0; + use super::{SetrlimitError, setrlimit}; + use crate::Errno; + + const RLIMIT_CPU: UnsignedInt = 0; + const CURRENT_RLIM_NLIMITS: UnsignedInt = 16; + + #[test] + fn test_setrlimit_invalid_resource() { + let limits = Rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + assert_eq!( + setrlimit(CURRENT_RLIM_NLIMITS, &limits), + Err(SetrlimitError::Einval) + ); + } + + #[test] + fn test_setrlimit_rejects_soft_limit_above_hard_limit() { + let limits = Rlimit { + rlim_cur: 1, + rlim_max: 0, + }; + + assert_eq!(setrlimit(RLIMIT_CPU, &limits), Err(SetrlimitError::Einval)); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_setrlimit_1_0_invalid_resource() { + let limits = Linux10Rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + assert_eq!( + setrlimit_1_0(CURRENT_RLIM_NLIMITS, &limits), + Err(SetrlimitError::Einval) + ); + } + + #[test] + fn test_setrlimit_error_mapping() { + assert_eq!( + SetrlimitError::from_errno(Errno::Efault), + SetrlimitError::Efault + ); + assert_eq!( + SetrlimitError::from_errno(Errno::Einval), + SetrlimitError::Einval + ); + assert_eq!( + SetrlimitError::from_errno(Errno::Eperm), + SetrlimitError::Eperm + ); + assert_eq!( + SetrlimitError::from_errno(Errno::Eio), + SetrlimitError::Other(Errno::Eio) + ); + } +} From b0132a27ce145f90c1908c3225f62bd61a8daadc Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:23:06 +0200 Subject: [PATCH 092/135] feat(syscalls): wrap setsid --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/setsid.rs | 60 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setsid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index f9f959f8..5806d02b 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -185,7 +185,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setregid16` - x86 - [x] `setreuid16` - x86 - [x] `setrlimit` -- [ ] `setsid` +- [x] `setsid` - [ ] `settimeofday` - [ ] `setuid16` - x86 - [ ] `sgetmask` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 49985da1..d7bbb507 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -133,6 +133,7 @@ mod setregid; #[cfg(target_arch = "x86")] mod setreuid; mod setrlimit; +mod setsid; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -261,6 +262,7 @@ pub use setregid::{Setregid16Error, setregid16}; #[cfg(target_arch = "x86")] pub use setreuid::{Setreuid16Error, setreuid16}; pub use setrlimit::{SetrlimitError, setrlimit}; +pub use setsid::{SetsidError, setsid}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/setsid.rs b/system/linux/syscalls/src/setsid.rs new file mode 100644 index 00000000..b6643359 --- /dev/null +++ b/system/linux/syscalls/src/setsid.rs @@ -0,0 +1,60 @@ +use celer_system_linux_ctypes::PidT; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setsid`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetsidError { + Eperm, + Other(Errno), +} + +impl SetsidError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Create a new session and make the caller its leader. +/// +/// `Ok(pid)` is the new process group ID returned by the kernel. +/// +/// See [`sys::setsid`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SetsidError::Eperm`]: the caller is already a session leader or a +/// process group already exists with the proposed session ID. +/// - [`SetsidError::Other`]: any other syscall error reported by the raw ABI. +pub fn setsid() -> Result { + let ret = sys::setsid(); + result_from_ret(ret as isize, |ret| ret as PidT, SetsidError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{SetsidError, setsid}; + use crate::Errno; + + #[test] + fn test_setsid_smoke() { + let ret = setsid(); + assert!( + matches!(ret, Ok(pid) if pid > 0) || ret == Err(SetsidError::Eperm) + ); + } + + #[test] + fn test_setsid_error_mapping() { + assert_eq!(SetsidError::from_errno(Errno::Eperm), SetsidError::Eperm); + assert_eq!( + SetsidError::from_errno(Errno::Einval), + SetsidError::Other(Errno::Einval) + ); + } +} From 0cfe1ad6085abfe4dd74bf8b213034612290e5c8 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:23:33 +0200 Subject: [PATCH 093/135] feat(syscalls): wrap settimeofday --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/settimeofday.rs | 95 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/settimeofday.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 5806d02b..fc6eb0b7 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -186,7 +186,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setreuid16` - x86 - [x] `setrlimit` - [x] `setsid` -- [ ] `settimeofday` +- [x] `settimeofday` - [ ] `setuid16` - x86 - [ ] `sgetmask` - x86 - [ ] `sigaction` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index d7bbb507..89e3f9d9 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -134,6 +134,7 @@ mod setregid; mod setreuid; mod setrlimit; mod setsid; +mod settimeofday; #[cfg(target_arch = "x86")] mod stat; pub mod sys; @@ -263,6 +264,7 @@ pub use setregid::{Setregid16Error, setregid16}; pub use setreuid::{Setreuid16Error, setreuid16}; pub use setrlimit::{SetrlimitError, setrlimit}; pub use setsid::{SetsidError, setsid}; +pub use settimeofday::{SettimeofdayError, settimeofday}; #[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; diff --git a/system/linux/syscalls/src/settimeofday.rs b/system/linux/syscalls/src/settimeofday.rs new file mode 100644 index 00000000..37d387ee --- /dev/null +++ b/system/linux/syscalls/src/settimeofday.rs @@ -0,0 +1,95 @@ +use celer_system_linux_ctypes::{Timeval, Timezone}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`settimeofday`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SettimeofdayError { + Efault, + Einval, + Eperm, + Other(Errno), +} + +impl SettimeofdayError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Set the system wall clock and/or legacy timezone state. +/// +/// This safe wrapper converts nullable raw input pointers into `Option` +/// references. `None` passes a null pointer for that argument. +/// +/// See [`sys::settimeofday`] for kernel behavior, reachable errors, side +/// effects, and source references. +/// +/// # Errors +/// - [`SettimeofdayError::Efault`]: the kernel could not read a non-null +/// argument. +/// - [`SettimeofdayError::Einval`]: a supplied time or timezone value is +/// invalid, or the timekeeping layer rejected the update. +/// - [`SettimeofdayError::Eperm`]: the caller lacks permission to set system +/// time. +/// - [`SettimeofdayError::Other`]: any other syscall error reported by the raw +/// ABI. +pub fn settimeofday( + tv: Option<&Timeval>, + tz: Option<&Timezone>, +) -> Result<(), SettimeofdayError> { + let tv = tv.map_or(core::ptr::null(), |tv| tv as *const Timeval); + let tz = tz.map_or(core::ptr::null(), |tz| tz as *const Timezone); + // SAFETY: non-null pointers came from shared references readable for one + // value; null pointers are accepted by this ABI. + let ret = unsafe { sys::settimeofday(tv, tz) }; + unit_from_ret(ret as isize, SettimeofdayError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::Timeval; + + use super::{SettimeofdayError, settimeofday}; + use crate::Errno; + + #[test] + fn test_settimeofday_invalid_tv_usec() { + let tv = Timeval { + tv_sec: 0, + tv_usec: 1_000_001, + }; + + assert_eq!( + settimeofday(Some(&tv), None), + Err(SettimeofdayError::Einval) + ); + } + + #[test] + fn test_settimeofday_error_mapping() { + assert_eq!( + SettimeofdayError::from_errno(Errno::Efault), + SettimeofdayError::Efault + ); + assert_eq!( + SettimeofdayError::from_errno(Errno::Einval), + SettimeofdayError::Einval + ); + assert_eq!( + SettimeofdayError::from_errno(Errno::Eperm), + SettimeofdayError::Eperm + ); + assert_eq!( + SettimeofdayError::from_errno(Errno::Eio), + SettimeofdayError::Other(Errno::Eio) + ); + } +} From 2edc50b909a07d2e2fe32d934de78a11ea5e9498 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:23:55 +0200 Subject: [PATCH 094/135] feat(syscalls): wrap setuid16 --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/setuid.rs | 76 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/setuid.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index fc6eb0b7..a24b69d7 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -187,7 +187,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setrlimit` - [x] `setsid` - [x] `settimeofday` -- [ ] `setuid16` - x86 +- [x] `setuid16` - x86 - [ ] `sgetmask` - x86 - [ ] `sigaction` - x86 - [ ] `signal` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 89e3f9d9..7318a091 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -136,6 +136,8 @@ mod setrlimit; mod setsid; mod settimeofday; #[cfg(target_arch = "x86")] +mod setuid; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -266,6 +268,8 @@ pub use setrlimit::{SetrlimitError, setrlimit}; pub use setsid::{SetsidError, setsid}; pub use settimeofday::{SettimeofdayError, settimeofday}; #[cfg(target_arch = "x86")] +pub use setuid::{Setuid16Error, setuid16}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/setuid.rs b/system/linux/syscalls/src/setuid.rs new file mode 100644 index 00000000..23bdfdf7 --- /dev/null +++ b/system/linux/syscalls/src/setuid.rs @@ -0,0 +1,76 @@ +use celer_system_linux_ctypes::OldUidT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`setuid16`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Setuid16Error { + Einval, + Enomem, + Eperm, + Other(Errno), +} + +impl Setuid16Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Set the calling process's user ID through the legacy x86 `setuid16` ABI. +/// +/// This safe wrapper preserves the raw 16-bit uid value and maps the raw +/// syscall return value into `Result<(), Setuid16Error>`. +/// +/// See [`sys::setuid16`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Setuid16Error::Einval`]: `uid` is invalid in the current namespace. +/// - [`Setuid16Error::Enomem`]: the kernel could not allocate credentials. +/// - [`Setuid16Error::Eperm`]: the requested uid change is not permitted. +/// - [`Setuid16Error::Other`]: another errno from credential or security +/// hooks. +#[cfg(target_arch = "x86")] +pub fn setuid16(uid: OldUidT) -> Result<(), Setuid16Error> { + let ret = sys::setuid16(uid); + unit_from_ret(ret as isize, Setuid16Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{Setuid16Error, setuid16}; + use crate::{Errno, getuid16}; + + #[test] + fn test_setuid16_current_uid() { + assert_eq!(setuid16(getuid16()), Ok(())); + } + + #[test] + fn test_setuid16_error_mapping() { + assert_eq!( + Setuid16Error::from_errno(Errno::Einval), + Setuid16Error::Einval + ); + assert_eq!( + Setuid16Error::from_errno(Errno::Enomem), + Setuid16Error::Enomem + ); + assert_eq!( + Setuid16Error::from_errno(Errno::Eperm), + Setuid16Error::Eperm + ); + assert_eq!( + Setuid16Error::from_errno(Errno::Eio), + Setuid16Error::Other(Errno::Eio) + ); + } +} From d1d00e8101cf6dd42af62705ad0872e1bc7201a2 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:24:22 +0200 Subject: [PATCH 095/135] feat(syscalls): wrap sgetmask --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/sgetmask.rs | 67 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sgetmask.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index a24b69d7..b840e824 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -188,7 +188,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setsid` - [x] `settimeofday` - [x] `setuid16` - x86 -- [ ] `sgetmask` - x86 +- [x] `sgetmask` - x86 - [ ] `sigaction` - x86 - [ ] `signal` - x86 - [ ] `sigpending` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 7318a091..b5ee5d38 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -138,6 +138,8 @@ mod settimeofday; #[cfg(target_arch = "x86")] mod setuid; #[cfg(target_arch = "x86")] +mod sgetmask; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -270,6 +272,8 @@ pub use settimeofday::{SettimeofdayError, settimeofday}; #[cfg(target_arch = "x86")] pub use setuid::{Setuid16Error, setuid16}; #[cfg(target_arch = "x86")] +pub use sgetmask::{SgetmaskError, sgetmask}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sgetmask.rs b/system/linux/syscalls/src/sgetmask.rs new file mode 100644 index 00000000..2283c3e4 --- /dev/null +++ b/system/linux/syscalls/src/sgetmask.rs @@ -0,0 +1,67 @@ +use celer_system_linux_ctypes::OldSigsetT; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`sgetmask`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SgetmaskError { + Enosys, + Other(Errno), +} + +impl SgetmaskError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Return the caller's legacy blocked-signal mask word. +/// +/// This safe wrapper maps the raw syscall return value into +/// `Result`. +/// +/// See [`sys::sgetmask`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SgetmaskError::Enosys`]: the legacy syscall is not configured. +/// - [`SgetmaskError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +pub fn sgetmask() -> Result { + let ret = sys::sgetmask(); + result_from_ret( + ret as isize, + |ret| ret as OldSigsetT, + SgetmaskError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{SgetmaskError, sgetmask}; + use crate::Errno; + + #[test] + fn test_sgetmask_smoke() { + let result = sgetmask(); + assert!(result.is_ok() || result == Err(SgetmaskError::Enosys)); + } + + #[test] + fn test_sgetmask_error_mapping() { + assert_eq!( + SgetmaskError::from_errno(Errno::Enosys), + SgetmaskError::Enosys + ); + assert_eq!( + SgetmaskError::from_errno(Errno::Eio), + SgetmaskError::Other(Errno::Eio) + ); + } +} From 4f7a62ba3cfd2ae32fdedffa7e595c3fa012ab5b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:24:53 +0200 Subject: [PATCH 096/135] feat(syscalls): wrap sigaction --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/sigaction.rs | 112 +++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sigaction.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index b840e824..5c3439f3 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -189,7 +189,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `settimeofday` - [x] `setuid16` - x86 - [x] `sgetmask` - x86 -- [ ] `sigaction` - x86 +- [x] `sigaction` - x86 - [ ] `signal` - x86 - [ ] `sigpending` - x86 - [ ] `sigprocmask` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b5ee5d38..c3bc5fb3 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -140,6 +140,8 @@ mod setuid; #[cfg(target_arch = "x86")] mod sgetmask; #[cfg(target_arch = "x86")] +mod sigaction; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -274,6 +276,8 @@ pub use setuid::{Setuid16Error, setuid16}; #[cfg(target_arch = "x86")] pub use sgetmask::{SgetmaskError, sgetmask}; #[cfg(target_arch = "x86")] +pub use sigaction::{SigactionError, sigaction}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sigaction.rs b/system/linux/syscalls/src/sigaction.rs new file mode 100644 index 00000000..6052da42 --- /dev/null +++ b/system/linux/syscalls/src/sigaction.rs @@ -0,0 +1,112 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, OldSigaction}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`sigaction`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SigactionError { + Efault, + Einval, + Other(Errno), +} + +impl SigactionError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Get or set the legacy x86 signal action for `sig`. +/// +/// This wrapper converts nullable raw pointers into `Option` references. Use +/// `None` for `action` to query without installing a new action, and `None` +/// for `oldaction` to skip returning the previous action. +/// +/// See [`sys::sigaction`] for kernel behavior, reachable errors, side +/// effects, and source references. +/// +/// # Safety +/// If `action` installs a user handler or restorer address, that callback +/// state must remain valid for future signal delivery and use the ABI expected +/// by this historical interface. +/// +/// # Errors +/// - [`SigactionError::Efault`]: the kernel could not read `action` or write +/// `oldaction`. +/// - [`SigactionError::Einval`]: `sig` is invalid or `action` tries to change +/// an immutable signal disposition. +/// - [`SigactionError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +pub unsafe fn sigaction( + sig: Int, + action: Option<&OldSigaction>, + oldaction: Option<&mut MaybeUninit>, +) -> Result<(), SigactionError> { + let action = action + .map_or(core::ptr::null(), |action| action as *const OldSigaction); + let oldaction = oldaction + .map_or(core::ptr::null_mut(), |oldaction| oldaction.as_mut_ptr()); + // SAFETY: non-null pointers came from references with the required access; + // callback ABI requirements are upheld by the caller. + let ret = unsafe { sys::sigaction(sig, action, oldaction) }; + unit_from_ret(ret as isize, SigactionError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::OldSigaction; + + use super::{SigactionError, sigaction}; + use crate::Errno; + + const SIGKILL: i32 = 9; + const SIGUSR1: i32 = 10; + const SIG_IGN: usize = 1; + + #[test] + fn test_sigaction_query() { + let mut old = MaybeUninit::::uninit(); + let result = unsafe { sigaction(SIGUSR1, None, Some(&mut old)) }; + assert_eq!(result, Ok(())); + } + + #[test] + fn test_sigaction_rejects_sigkill_handler_install() { + let ignore = OldSigaction { + sa_handler: SIG_IGN, + sa_mask: 0, + sa_flags: 0, + sa_restorer: 0, + }; + + let result = unsafe { sigaction(SIGKILL, Some(&ignore), None) }; + assert_eq!(result, Err(SigactionError::Einval)); + } + + #[test] + fn test_sigaction_error_mapping() { + assert_eq!( + SigactionError::from_errno(Errno::Efault), + SigactionError::Efault + ); + assert_eq!( + SigactionError::from_errno(Errno::Einval), + SigactionError::Einval + ); + assert_eq!( + SigactionError::from_errno(Errno::Eio), + SigactionError::Other(Errno::Eio) + ); + } +} From c7ee6b2e363b224f4e05ebe7c6b8c8228eacfea8 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:25:27 +0200 Subject: [PATCH 097/135] feat(syscalls): wrap signal --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 6 ++ system/linux/syscalls/src/signal.rs | 97 ++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/signal.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 5c3439f3..cafc8d7f 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -190,7 +190,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `setuid16` - x86 - [x] `sgetmask` - x86 - [x] `sigaction` - x86 -- [ ] `signal` - x86 +- [x] `signal` - x86 - [ ] `sigpending` - x86 - [ ] `sigprocmask` - x86 - [ ] `sigreturn` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c3bc5fb3..596c8281 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -142,6 +142,8 @@ mod sgetmask; #[cfg(target_arch = "x86")] mod sigaction; #[cfg(target_arch = "x86")] +mod signal; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -278,6 +280,10 @@ pub use sgetmask::{SgetmaskError, sgetmask}; #[cfg(target_arch = "x86")] pub use sigaction::{SigactionError, sigaction}; #[cfg(target_arch = "x86")] +pub use signal::{ + SIG_DFL, SIG_IGN, SigHandler, SignalError, sig_handler, signal, +}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/signal.rs b/system/linux/syscalls/src/signal.rs new file mode 100644 index 00000000..e743d7ae --- /dev/null +++ b/system/linux/syscalls/src/signal.rs @@ -0,0 +1,97 @@ +use celer_system_linux_ctypes::Int; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +pub use sys::{SIG_DFL, SIG_IGN, SigHandler, sig_handler}; + +/// Errors returned by [`signal`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SignalError { + Efault, + Einval, + Other(Errno), +} + +impl SignalError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Install a legacy x86 signal handler for `sig`. +/// +/// `Ok(handler)` is the previous legacy signal handler or disposition word. +/// +/// See [`sys::signal`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Safety +/// If `handler` names a user handler, that handler must remain valid for +/// signal delivery and use the ABI expected by this historical interface. +/// +/// # Errors +/// - [`SignalError::Efault`]: Linux 1.0 rejected the handler address. +/// - [`SignalError::Einval`]: `sig` is invalid or targets an immutable signal +/// disposition. +/// - [`SignalError::Other`]: any other syscall error reported by the raw ABI. +#[cfg(target_arch = "x86")] +pub unsafe fn signal( + sig: Int, + handler: SigHandler, +) -> Result { + // SAFETY: handler ABI requirements are upheld by the caller. + let ret = unsafe { sys::signal(sig, handler) }; + result_from_ret( + ret as isize, + |ret| sys::sig_handler_from_raw(ret as _), + SignalError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{SIG_DFL, SignalError, signal}; + use crate::Errno; + use crate::sys::test_support::process_global_state_guard; + + const SIGUSR1: i32 = 10; + + struct RestoreSignal(super::SigHandler); + + impl Drop for RestoreSignal { + fn drop(&mut self) { + let _ = unsafe { signal(SIGUSR1, self.0) }; + } + } + + #[test] + fn test_signal_success_returns_previous_handler() { + let _guard = process_global_state_guard(); + + let previous = unsafe { signal(SIGUSR1, SIG_DFL) } + .expect("signal(SIGUSR1, SIG_DFL) failed"); + let _restore = RestoreSignal(previous); + } + + #[test] + fn test_signal_invalid_signal() { + let result = unsafe { signal(0, SIG_DFL) }; + assert_eq!(result, Err(SignalError::Einval)); + } + + #[test] + fn test_signal_error_mapping() { + assert_eq!(SignalError::from_errno(Errno::Efault), SignalError::Efault); + assert_eq!(SignalError::from_errno(Errno::Einval), SignalError::Einval); + assert_eq!( + SignalError::from_errno(Errno::Eio), + SignalError::Other(Errno::Eio) + ); + } +} From 6aba0ea561693c52e47b01eecb36ef2462cfb8d2 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:41:41 +0200 Subject: [PATCH 098/135] feat(syscalls): wrap sigpending --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/sigpending.rs | 76 +++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sigpending.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index cafc8d7f..c988e563 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -191,7 +191,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sgetmask` - x86 - [x] `sigaction` - x86 - [x] `signal` - x86 -- [ ] `sigpending` - x86 +- [x] `sigpending` - x86 - [ ] `sigprocmask` - x86 - [ ] `sigreturn` - x86 - [ ] `sigsuspend` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 596c8281..b22b3b4b 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -144,6 +144,8 @@ mod sigaction; #[cfg(target_arch = "x86")] mod signal; #[cfg(target_arch = "x86")] +mod sigpending; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -284,6 +286,8 @@ pub use signal::{ SIG_DFL, SIG_IGN, SigHandler, SignalError, sig_handler, signal, }; #[cfg(target_arch = "x86")] +pub use sigpending::{SigpendingError, sigpending}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sigpending.rs b/system/linux/syscalls/src/sigpending.rs new file mode 100644 index 00000000..a9fe0a44 --- /dev/null +++ b/system/linux/syscalls/src/sigpending.rs @@ -0,0 +1,76 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::OldSigsetT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`sigpending`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SigpendingError { + Efault, + Other(Errno), +} + +impl SigpendingError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Return the caller's pending blocked-signal mask word. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw return into +/// `Result<(), SigpendingError>`. +/// +/// On success, the kernel has initialized `set` with the legacy pending +/// blocked-signal mask word. +/// +/// See [`sys::sigpending`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SigpendingError::Efault`]: the kernel could not write the output mask. +/// - [`SigpendingError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +pub fn sigpending( + set: &mut MaybeUninit, +) -> Result<(), SigpendingError> { + // SAFETY: `MaybeUninit` provides writable storage for one + // legacy signal mask word. + let ret = unsafe { sys::sigpending(set.as_mut_ptr()) }; + unit_from_ret(ret as isize, SigpendingError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use super::{SigpendingError, sigpending}; + use crate::Errno; + + #[test] + fn test_sigpending_ok() { + let mut pending = MaybeUninit::uninit(); + + assert_eq!(sigpending(&mut pending), Ok(())); + } + + #[test] + fn test_sigpending_error_mapping() { + assert_eq!( + SigpendingError::from_errno(Errno::Efault), + SigpendingError::Efault + ); + assert_eq!( + SigpendingError::from_errno(Errno::Einval), + SigpendingError::Other(Errno::Einval) + ); + } +} From 57269312bbda1e2a3be0b38650815322f8f7dbd0 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:41:55 +0200 Subject: [PATCH 099/135] feat(syscalls): wrap sigprocmask --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 6 ++ system/linux/syscalls/src/sigprocmask.rs | 127 +++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sigprocmask.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c988e563..8f85d8b2 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -192,7 +192,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sigaction` - x86 - [x] `signal` - x86 - [x] `sigpending` - x86 -- [ ] `sigprocmask` - x86 +- [x] `sigprocmask` - x86 - [ ] `sigreturn` - x86 - [ ] `sigsuspend` - x86 - [ ] `socketcall` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index b22b3b4b..9febef55 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -146,6 +146,8 @@ mod signal; #[cfg(target_arch = "x86")] mod sigpending; #[cfg(target_arch = "x86")] +mod sigprocmask; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -288,6 +290,10 @@ pub use signal::{ #[cfg(target_arch = "x86")] pub use sigpending::{SigpendingError, sigpending}; #[cfg(target_arch = "x86")] +pub use sigprocmask::{ + SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK, SigprocmaskError, sigprocmask, +}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sigprocmask.rs b/system/linux/syscalls/src/sigprocmask.rs new file mode 100644 index 00000000..2c4d8d67 --- /dev/null +++ b/system/linux/syscalls/src/sigprocmask.rs @@ -0,0 +1,127 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, OldSigsetT}; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +pub use sys::{SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; + +/// Errors returned by [`sigprocmask`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SigprocmaskError { + Efault, + Einval, + Other(Errno), +} + +impl SigprocmaskError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Examine or change the caller's legacy blocked-signal mask word. +/// +/// This wrapper converts nullable raw pointers into `Option` references. Use +/// `None` for `set` to query without changing the mask, and `None` for `oset` +/// to skip returning the previous mask. +/// +/// On success, any requested mask update has been applied and `oset`, when +/// present, has been initialized with the previous mask. +/// +/// See [`sys::sigprocmask`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SigprocmaskError::Efault`]: the kernel could not read `set` or write +/// `oset`. +/// - [`SigprocmaskError::Einval`]: `how` is invalid while `set` is present. +/// - [`SigprocmaskError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +pub fn sigprocmask( + how: Int, + set: Option<&OldSigsetT>, + oset: Option<&mut MaybeUninit>, +) -> Result<(), SigprocmaskError> { + let set = set.map_or(core::ptr::null(), |set| set as *const OldSigsetT); + let oset = oset.map_or(core::ptr::null_mut(), |oset| oset.as_mut_ptr()); + // SAFETY: non-null pointers came from references with the required access. + let ret = unsafe { sys::sigprocmask(how, set, oset) }; + unit_from_ret(ret as isize, SigprocmaskError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::OldSigsetT; + + use super::{SIG_BLOCK, SIG_SETMASK, SigprocmaskError, sigprocmask}; + use crate::Errno; + use crate::sys::test_support::process_global_state_guard; + + const SIGUSR1: OldSigsetT = 10; + + fn signal_bit(sig: OldSigsetT) -> OldSigsetT { + (1 as OldSigsetT) << (sig - 1) + } + + #[test] + fn test_sigprocmask_query_ok() { + let mut old = MaybeUninit::uninit(); + + assert_eq!(sigprocmask(999, None, Some(&mut old)), Ok(())); + } + + #[test] + fn test_sigprocmask_updates_and_reports_previous_mask() { + let _guard = process_global_state_guard(); + let clear = 0 as OldSigsetT; + let mut original = MaybeUninit::uninit(); + sigprocmask(SIG_SETMASK, Some(&clear), Some(&mut original)).unwrap(); + let original = unsafe { original.assume_init() }; + + let requested = signal_bit(SIGUSR1); + let mut old = MaybeUninit::uninit(); + assert_eq!( + sigprocmask(SIG_BLOCK, Some(&requested), Some(&mut old)), + Ok(()) + ); + assert_eq!(unsafe { old.assume_init() }, 0); + + let _ = sigprocmask(SIG_SETMASK, Some(&original), None); + } + + #[test] + fn test_sigprocmask_rejects_invalid_how_when_set_is_present() { + let requested = signal_bit(SIGUSR1); + + assert_eq!( + sigprocmask(99, Some(&requested), None), + Err(SigprocmaskError::Einval) + ); + } + + #[test] + fn test_sigprocmask_error_mapping() { + assert_eq!( + SigprocmaskError::from_errno(Errno::Efault), + SigprocmaskError::Efault + ); + assert_eq!( + SigprocmaskError::from_errno(Errno::Einval), + SigprocmaskError::Einval + ); + assert_eq!( + SigprocmaskError::from_errno(Errno::Eio), + SigprocmaskError::Other(Errno::Eio) + ); + } +} From 6077daf116194498c17f0bd86071daff4dd37652 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:42:08 +0200 Subject: [PATCH 100/135] feat(syscalls): wrap sigreturn --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++++ system/linux/syscalls/src/sigreturn.rs | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sigreturn.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 8f85d8b2..9bf90958 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -193,7 +193,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `signal` - x86 - [x] `sigpending` - x86 - [x] `sigprocmask` - x86 -- [ ] `sigreturn` - x86 +- [x] `sigreturn` - x86 - [ ] `sigsuspend` - x86 - [ ] `socketcall` - x86 - [ ] `ssetmask` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 9febef55..c5804953 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -148,6 +148,8 @@ mod sigpending; #[cfg(target_arch = "x86")] mod sigprocmask; #[cfg(target_arch = "x86")] +mod sigreturn; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -294,6 +296,8 @@ pub use sigprocmask::{ SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK, SigprocmaskError, sigprocmask, }; #[cfg(target_arch = "x86")] +pub use sigreturn::sigreturn; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sigreturn.rs b/system/linux/syscalls/src/sigreturn.rs new file mode 100644 index 00000000..8ae57d4c --- /dev/null +++ b/system/linux/syscalls/src/sigreturn.rs @@ -0,0 +1,23 @@ +use celer_system_linux_ctypes::Long; + +use crate::sys; + +/// Return from a legacy signal handler through the kernel signal frame. +/// +/// This wrapper preserves the raw zero-argument ABI and return register value. +/// It has no syscall-specific error enum because no errno return is reachable +/// from this entry path; malformed frames terminate the task instead of +/// returning an error. +/// +/// See [`sys::sigreturn`] for kernel behavior, frame requirements, and source +/// references. +/// +/// # Safety +/// The current user stack must contain a valid kernel-built legacy signal +/// frame whose saved execution state may be restored by the kernel. +#[cfg(target_arch = "x86")] +#[cfg_attr(coverage_nightly, coverage(off))] +pub unsafe fn sigreturn() -> Long { + // SAFETY: forwarded from this wrapper's safety contract. + unsafe { sys::sigreturn() } +} From 5986d0c797f6d1da3cab0f1e41d1badc797a3a13 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:42:19 +0200 Subject: [PATCH 101/135] feat(syscalls): wrap sigsuspend --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/sigsuspend.rs | 61 +++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sigsuspend.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 9bf90958..0f82f625 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -194,7 +194,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sigpending` - x86 - [x] `sigprocmask` - x86 - [x] `sigreturn` - x86 -- [ ] `sigsuspend` - x86 +- [x] `sigsuspend` - x86 - [ ] `socketcall` - x86 - [ ] `ssetmask` - x86 - [ ] `stat` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c5804953..ac5d23e0 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -150,6 +150,8 @@ mod sigprocmask; #[cfg(target_arch = "x86")] mod sigreturn; #[cfg(target_arch = "x86")] +mod sigsuspend; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -298,6 +300,8 @@ pub use sigprocmask::{ #[cfg(target_arch = "x86")] pub use sigreturn::sigreturn; #[cfg(target_arch = "x86")] +pub use sigsuspend::{SigsuspendError, sigsuspend}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/sigsuspend.rs b/system/linux/syscalls/src/sigsuspend.rs new file mode 100644 index 00000000..e209f529 --- /dev/null +++ b/system/linux/syscalls/src/sigsuspend.rs @@ -0,0 +1,61 @@ +use celer_system_linux_ctypes::OldSigsetT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`sigsuspend`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SigsuspendError { + Eintr, + Other(Errno), +} + +impl SigsuspendError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eintr => Self::Eintr, + errno => Self::Other(errno), + } + } +} + +/// Atomically replace the legacy blocked-signal mask and wait for a handler. +/// +/// This safe wrapper keeps the scalar legacy mask argument and maps the raw +/// return into `Result<(), SigsuspendError>`. +/// +/// A successful `Ok(())` is not expected from the source-verified Linux entry +/// path; after a handler runs, the syscall returns `EINTR`. +/// +/// See [`sys::sigsuspend`] for kernel behavior, signal-mask restoration, and +/// source references. +/// +/// # Errors +/// - [`SigsuspendError::Eintr`]: a signal handler ran. +/// - [`SigsuspendError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn sigsuspend(mask: OldSigsetT) -> Result<(), SigsuspendError> { + let ret = sys::sigsuspend(mask); + unit_from_ret(ret as isize, SigsuspendError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::SigsuspendError; + use crate::Errno; + + #[test] + fn test_sigsuspend_error_mapping() { + assert_eq!( + SigsuspendError::from_errno(Errno::Eintr), + SigsuspendError::Eintr + ); + assert_eq!( + SigsuspendError::from_errno(Errno::Eio), + SigsuspendError::Other(Errno::Eio) + ); + } +} From 8cbdd7f51c95e0d594b62cd023a159fa984ae14a Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:42:31 +0200 Subject: [PATCH 102/135] feat(syscalls): wrap socketcall --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 9 ++ system/linux/syscalls/src/socketcall.rs | 122 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/socketcall.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 0f82f625..2b3e4915 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -195,7 +195,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sigprocmask` - x86 - [x] `sigreturn` - x86 - [x] `sigsuspend` - x86 -- [ ] `socketcall` - x86 +- [x] `socketcall` - x86 - [ ] `ssetmask` - x86 - [ ] `stat` - x86, x86_64 - [ ] `statfs` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index ac5d23e0..e71b8241 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -152,6 +152,8 @@ mod sigreturn; #[cfg(target_arch = "x86")] mod sigsuspend; #[cfg(target_arch = "x86")] +mod socketcall; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -302,6 +304,13 @@ pub use sigreturn::sigreturn; #[cfg(target_arch = "x86")] pub use sigsuspend::{SigsuspendError, sigsuspend}; #[cfg(target_arch = "x86")] +pub use socketcall::{ + SYS_ACCEPT, SYS_BIND, SYS_CONNECT, SYS_GETPEERNAME, SYS_GETSOCKNAME, + SYS_GETSOCKOPT, SYS_LISTEN, SYS_RECV, SYS_RECVFROM, SYS_SEND, SYS_SENDTO, + SYS_SETSOCKOPT, SYS_SHUTDOWN, SYS_SOCKET, SYS_SOCKETPAIR, SocketcallError, + socketcall, +}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/socketcall.rs b/system/linux/syscalls/src/socketcall.rs new file mode 100644 index 00000000..4d7b81dc --- /dev/null +++ b/system/linux/syscalls/src/socketcall.rs @@ -0,0 +1,122 @@ +use celer_system_linux_ctypes::{Int, Long, UnsignedLong}; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +pub use sys::{ + SYS_ACCEPT, SYS_BIND, SYS_CONNECT, SYS_GETPEERNAME, SYS_GETSOCKNAME, + SYS_GETSOCKOPT, SYS_LISTEN, SYS_RECV, SYS_RECVFROM, SYS_SEND, SYS_SENDTO, + SYS_SETSOCKOPT, SYS_SHUTDOWN, SYS_SOCKET, SYS_SOCKETPAIR, +}; + +/// Errors returned by [`socketcall`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SocketcallError { + Efault, + Einval, + Other(Errno), +} + +impl SocketcallError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Dispatch a socket operation through the historical `socketcall` multiplexor. +/// +/// This wrapper takes the packed `unsigned long` argument vector as a shared +/// slice and maps the raw return into `Result`. The +/// required slice length and the meaning of pointers stored inside the slice +/// remain selected by `call`. +/// +/// See [`sys::socketcall`] for kernel behavior, subcall selectors, reachable +/// errors, and source references. +/// +/// # Safety +/// The `args` slice must contain at least the number of packed words required +/// by `call`, and any userspace pointers encoded inside it must satisfy the +/// selected socket operation's memory contract for the duration of the syscall. +/// +/// # Errors +/// - [`SocketcallError::Efault`]: the kernel could not read the packed +/// argument vector or a subcall pointer. +/// - [`SocketcallError::Einval`]: `call` is not a recognized selector, or a +/// subcall rejected an invalid argument. +/// - [`SocketcallError::Other`]: any delegated socket helper error. +#[cfg(target_arch = "x86")] +pub unsafe fn socketcall( + call: Int, + args: &[UnsignedLong], +) -> Result { + // SAFETY: the caller guarantees the slice is long enough for `call` and + // any encoded pointers satisfy the selected subcall's contract. + let ret = unsafe { sys::socketcall(call, args.as_ptr()) }; + result_from_ret( + ret as isize, + |ret| ret as Long, + SocketcallError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::{Int, UnsignedLong}; + + use super::{SYS_SOCKETPAIR, SocketcallError, socketcall}; + use crate::Errno; + use crate::sys::close; + + const AF_UNIX: Int = 1; + const SOCK_STREAM: Int = 1; + + #[test] + fn test_socketcall_invalid_selector_returns_einval() { + let args = [0 as UnsignedLong; 6]; + + let err = unsafe { socketcall(0, &args) }.unwrap_err(); + + assert_eq!(err, SocketcallError::Einval); + } + + #[test] + fn test_socketcall_socketpair_ok() { + let mut fds = [-1 as Int; 2]; + let args = [ + AF_UNIX as UnsignedLong, + SOCK_STREAM as UnsignedLong, + 0 as UnsignedLong, + (&raw mut fds).addr() as UnsignedLong, + ]; + + let result = unsafe { socketcall(SYS_SOCKETPAIR, &args) }; + + assert_eq!(result, Ok(0)); + assert!(fds[0] >= 0, "invalid first socket fd: {}", fds[0]); + assert!(fds[1] >= 0, "invalid second socket fd: {}", fds[1]); + assert_ne!(fds[0], fds[1]); + assert_eq!(close(fds[0]), 0); + assert_eq!(close(fds[1]), 0); + } + + #[test] + fn test_socketcall_error_mapping() { + assert_eq!( + SocketcallError::from_errno(Errno::Efault), + SocketcallError::Efault + ); + assert_eq!( + SocketcallError::from_errno(Errno::Einval), + SocketcallError::Einval + ); + assert_eq!( + SocketcallError::from_errno(Errno::Eacces), + SocketcallError::Other(Errno::Eacces) + ); + } +} From 0b162b656cd59e91136bec32437494566c743bad Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:42:42 +0200 Subject: [PATCH 103/135] feat(syscalls): wrap ssetmask --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/ssetmask.rs | 72 ++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/ssetmask.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2b3e4915..49bf0076 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -196,7 +196,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sigreturn` - x86 - [x] `sigsuspend` - x86 - [x] `socketcall` - x86 -- [ ] `ssetmask` - x86 +- [x] `ssetmask` - x86 - [ ] `stat` - x86, x86_64 - [ ] `statfs` - [ ] `stime` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e71b8241..0a840079 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -154,6 +154,8 @@ mod sigsuspend; #[cfg(target_arch = "x86")] mod socketcall; #[cfg(target_arch = "x86")] +mod ssetmask; +#[cfg(target_arch = "x86")] mod stat; pub mod sys; mod uname; @@ -311,6 +313,8 @@ pub use socketcall::{ socketcall, }; #[cfg(target_arch = "x86")] +pub use ssetmask::{SsetmaskError, ssetmask}; +#[cfg(target_arch = "x86")] pub use stat::{OldstatError, oldstat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] diff --git a/system/linux/syscalls/src/ssetmask.rs b/system/linux/syscalls/src/ssetmask.rs new file mode 100644 index 00000000..8bc3f0d9 --- /dev/null +++ b/system/linux/syscalls/src/ssetmask.rs @@ -0,0 +1,72 @@ +use celer_system_linux_ctypes::OldSigsetT; + +use crate::helpers::result_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`ssetmask`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SsetmaskError { + Enosys, + Other(Errno), +} + +impl SsetmaskError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Replace the caller's legacy blocked-signal mask word. +/// +/// This safe wrapper keeps the scalar mask argument and maps the raw return +/// into `Result`. +/// +/// On success, returns the previous legacy blocked-signal mask word. +/// +/// See [`sys::ssetmask`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SsetmaskError::Enosys`]: the legacy syscall is not configured. +/// - [`SsetmaskError::Other`]: any other syscall error reported by the raw +/// ABI. +#[cfg(target_arch = "x86")] +pub fn ssetmask(newmask: OldSigsetT) -> Result { + let ret = sys::ssetmask(newmask); + result_from_ret( + ret as isize, + |ret| ret as OldSigsetT, + SsetmaskError::from_errno, + ) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{SsetmaskError, ssetmask}; + use crate::Errno; + use crate::sys::test_support::process_global_state_guard; + + #[test] + fn test_ssetmask_smoke() { + let _guard = process_global_state_guard(); + + let result = ssetmask(0); + assert!(result.is_ok() || result == Err(SsetmaskError::Enosys)); + } + + #[test] + fn test_ssetmask_error_mapping() { + assert_eq!( + SsetmaskError::from_errno(Errno::Enosys), + SsetmaskError::Enosys + ); + assert_eq!( + SsetmaskError::from_errno(Errno::Eio), + SsetmaskError::Other(Errno::Eio) + ); + } +} From 4183b48450ed8336d4d6c89f99f0e925b585eedf Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:43:08 +0200 Subject: [PATCH 104/135] feat(syscalls): wrap stat --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 9 +- system/linux/syscalls/src/stat.rs | 131 +++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 49bf0076..3cd75975 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -197,7 +197,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sigsuspend` - x86 - [x] `socketcall` - x86 - [x] `ssetmask` - x86 -- [ ] `stat` - x86, x86_64 +- [x] `stat` - x86, x86_64 - [ ] `statfs` - [ ] `stime` - x86 - [ ] `swapoff` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 0a840079..c6a07cd2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -155,7 +155,7 @@ mod sigsuspend; mod socketcall; #[cfg(target_arch = "x86")] mod ssetmask; -#[cfg(target_arch = "x86")] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod stat; pub mod sys; mod uname; @@ -315,7 +315,11 @@ pub use socketcall::{ #[cfg(target_arch = "x86")] pub use ssetmask::{SsetmaskError, ssetmask}; #[cfg(target_arch = "x86")] -pub use stat::{OldstatError, oldstat}; +pub use stat::OldstatError; +#[cfg(target_arch = "x86")] +pub use stat::oldstat; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use stat::{StatError, stat}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; @@ -331,4 +335,5 @@ pub mod linux_1_0 { pub use super::newfstat::{NewfstatError, newfstat_1_0 as newfstat}; pub use super::newlstat::{NewlstatError, newlstat_1_0 as newlstat}; pub use super::setrlimit::{SetrlimitError, setrlimit_1_0 as setrlimit}; + pub use super::stat::{StatError, stat_1_0 as stat}; } diff --git a/system/linux/syscalls/src/stat.rs b/system/linux/syscalls/src/stat.rs index a5d4bc04..86784dbb 100644 --- a/system/linux/syscalls/src/stat.rs +++ b/system/linux/syscalls/src/stat.rs @@ -1,12 +1,20 @@ use core::{ffi::CStr, mem::MaybeUninit}; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::NewStat as NativeStat; +#[cfg(target_arch = "x86")] use celer_system_linux_ctypes::Stat; +#[cfg(target_arch = "x86_64")] +use celer_system_linux_ctypes::Stat64 as NativeStat; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::NewStat as Linux10NewStat; use crate::errno::Errno; use crate::helpers::unit_from_ret; use crate::sys; /// Errors returned by [`oldstat`]. +#[cfg(target_arch = "x86")] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum OldstatError { /// `EFAULT`. @@ -17,6 +25,7 @@ pub enum OldstatError { Other(Errno), } +#[cfg(target_arch = "x86")] impl OldstatError { fn from_errno(errno: Errno) -> Self { match errno { @@ -57,6 +66,80 @@ pub fn oldstat( unit_from_ret(ret as isize, OldstatError::from_errno) } +/// Errors returned by [`stat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StatError { + /// `EFAULT`. + Efault, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +impl StatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Get file status information for the path named by `filename`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer with `&mut MaybeUninit`, and maps the raw syscall +/// return into `Result<(), StatError>`. +/// +/// On success, the kernel has initialized `statbuf` with metadata for the +/// resolved path using the target architecture's native stat layout. +/// +/// See [`sys::stat`] for kernel behavior, reachable errors, ABI layout, and +/// source references. +/// +/// # Errors +/// - [`StatError::Efault`]: the kernel could not read the path or write the +/// output buffer. +/// - [`StatError::Eoverflow`]: metadata could not be represented in the native +/// stat layout. +/// - [`StatError::Other`]: delegated pathname lookup or filesystem error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn stat( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), StatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one native stat + // value. + let ret = unsafe { sys::stat(filename.as_ptr(), statbuf.as_mut_ptr()) }; + + unit_from_ret(ret as isize, StatError::from_errno) +} + +/// Get file status information through the Linux 1.0 `sys_newstat` ABI. +/// +/// This safe wrapper mirrors [`stat`], but uses the Linux 1.0 newstat layout +/// exposed from [`crate::linux_1_0`]. +/// +/// See [`sys::linux_1_0::stat`] for historical kernel behavior and source +/// references. +#[cfg(target_arch = "x86")] +pub fn stat_1_0( + filename: &CStr, + statbuf: &mut MaybeUninit, +) -> Result<(), StatError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname, and + // `MaybeUninit` provides writable storage for one Linux + // 1.0 newstat value. + let ret = unsafe { + sys::linux_1_0::stat(filename.as_ptr(), statbuf.as_mut_ptr()) + }; + + unit_from_ret(ret as isize, StatError::from_errno) +} + #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { @@ -64,8 +147,13 @@ mod tests { use crate::Errno; + #[cfg(target_arch = "x86")] + use super::stat_1_0; + #[cfg(target_arch = "x86")] use super::{OldstatError, oldstat}; + use super::{StatError, stat}; + #[cfg(target_arch = "x86")] #[test] fn test_oldstat_ok() { let path = CString::new("/").unwrap(); @@ -74,6 +162,7 @@ mod tests { assert_eq!(oldstat(path.as_c_str(), &mut statbuf), Ok(())); } + #[cfg(target_arch = "x86")] #[test] fn test_oldstat_missing_path() { let path = CString::new("/celer-oldstat-definitely-missing").unwrap(); @@ -85,6 +174,7 @@ mod tests { ); } + #[cfg(target_arch = "x86")] #[test] fn test_oldstat_error_mapping() { assert_eq!( @@ -100,4 +190,45 @@ mod tests { OldstatError::Other(Errno::Enoent) ); } + + #[test] + fn test_stat_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(stat(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_linux_1_0_stat_ok() { + let path = CString::new("/").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!(stat_1_0(path.as_c_str(), &mut statbuf), Ok(())); + } + + #[test] + fn test_stat_missing_path() { + let path = CString::new("/celer-stat-definitely-missing").unwrap(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!( + stat(path.as_c_str(), &mut statbuf), + Err(StatError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_stat_error_mapping() { + assert_eq!(StatError::from_errno(Errno::Efault), StatError::Efault); + assert_eq!( + StatError::from_errno(Errno::Eoverflow), + StatError::Eoverflow + ); + assert_eq!( + StatError::from_errno(Errno::Enoent), + StatError::Other(Errno::Enoent) + ); + } } From a1e00dae57335e73349cade1ccbed0cf28fc1292 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:43:49 +0200 Subject: [PATCH 105/135] feat(syscalls): wrap statfs --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 3 + system/linux/syscalls/src/statfs.rs | 130 +++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/statfs.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 3cd75975..71449f7e 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -198,7 +198,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `socketcall` - x86 - [x] `ssetmask` - x86 - [x] `stat` - x86, x86_64 -- [ ] `statfs` +- [x] `statfs` - [ ] `stime` - x86 - [ ] `swapoff` - [ ] `swapon` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index c6a07cd2..4b4a1601 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -157,6 +157,7 @@ mod socketcall; mod ssetmask; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod stat; +mod statfs; pub mod sys; mod uname; @@ -320,6 +321,7 @@ pub use stat::OldstatError; pub use stat::oldstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use stat::{StatError, stat}; +pub use statfs::{StatfsError, statfs}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; @@ -336,4 +338,5 @@ pub mod linux_1_0 { pub use super::newlstat::{NewlstatError, newlstat_1_0 as newlstat}; pub use super::setrlimit::{SetrlimitError, setrlimit_1_0 as setrlimit}; pub use super::stat::{StatError, stat_1_0 as stat}; + pub use super::statfs::{StatfsError, statfs_1_0 as statfs}; } diff --git a/system/linux/syscalls/src/statfs.rs b/system/linux/syscalls/src/statfs.rs new file mode 100644 index 00000000..8bdbd47b --- /dev/null +++ b/system/linux/syscalls/src/statfs.rs @@ -0,0 +1,130 @@ +use core::{ffi::CStr, mem::MaybeUninit}; + +use celer_system_linux_ctypes::Statfs; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::Statfs as Linux10Statfs; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`statfs`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StatfsError { + Efault, + Enosys, + Eoverflow, + Other(Errno), +} + +impl StatfsError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Enosys => Self::Enosys, + Errno::Eoverflow => Self::Eoverflow, + errno => Self::Other(errno), + } + } +} + +/// Return filesystem status for the filesystem containing `path`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname, replaces the raw +/// output pointer with `&mut MaybeUninit`, and maps the raw return into +/// `Result<(), StatfsError>`. +/// +/// On success, the kernel has initialized `buf` with filesystem status. +/// +/// See [`sys::statfs`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`StatfsError::Efault`]: the kernel could not read the path or write the +/// output buffer. +/// - [`StatfsError::Enosys`]: the resolved filesystem does not implement a +/// `statfs` hook. +/// - [`StatfsError::Eoverflow`]: filesystem statistics could not be +/// represented in the current ABI. +/// - [`StatfsError::Other`]: delegated pathname lookup or filesystem error. +pub fn statfs( + path: &CStr, + buf: &mut MaybeUninit, +) -> Result<(), StatfsError> { + // SAFETY: `CStr` is a readable NUL-terminated path and + // `MaybeUninit` provides writable storage for one result. + let ret = unsafe { sys::statfs(path.as_ptr(), buf.as_mut_ptr()) }; + unit_from_ret(ret as isize, StatfsError::from_errno) +} + +/// Return filesystem status through the Linux 1.0 `sys_statfs` ABI. +/// +/// This safe wrapper mirrors [`statfs`], but uses the Linux 1.0 statfs layout +/// exposed from [`crate::linux_1_0`]. +/// +/// See [`sys::linux_1_0::statfs`] for historical kernel behavior and source +/// references. +#[cfg(target_arch = "x86")] +pub fn statfs_1_0( + path: &CStr, + buf: &mut MaybeUninit, +) -> Result<(), StatfsError> { + // SAFETY: `CStr` is a readable NUL-terminated path and + // `MaybeUninit` provides writable storage for one result. + let ret = + unsafe { sys::linux_1_0::statfs(path.as_ptr(), buf.as_mut_ptr()) }; + unit_from_ret(ret as isize, StatfsError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + use std::ffi::CString; + + #[cfg(target_arch = "x86")] + use super::statfs_1_0; + use super::{StatfsError, statfs}; + use crate::Errno; + + #[test] + fn test_statfs_ok() { + let path = CString::new("/").unwrap(); + let mut buf = MaybeUninit::uninit(); + + assert_eq!(statfs(path.as_c_str(), &mut buf), Ok(())); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_linux_1_0_statfs_ok() { + let path = CString::new("/").unwrap(); + let mut buf = MaybeUninit::uninit(); + + assert_eq!(statfs_1_0(path.as_c_str(), &mut buf), Ok(())); + } + + #[test] + fn test_statfs_missing_path() { + let path = CString::new("/celer-statfs-definitely-missing").unwrap(); + let mut buf = MaybeUninit::uninit(); + + assert_eq!( + statfs(path.as_c_str(), &mut buf), + Err(StatfsError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_statfs_error_mapping() { + assert_eq!(StatfsError::from_errno(Errno::Efault), StatfsError::Efault); + assert_eq!(StatfsError::from_errno(Errno::Enosys), StatfsError::Enosys); + assert_eq!( + StatfsError::from_errno(Errno::Eoverflow), + StatfsError::Eoverflow + ); + assert_eq!( + StatfsError::from_errno(Errno::Enoent), + StatfsError::Other(Errno::Enoent) + ); + } +} From 7e374ae450cb51778890e3386988399115d18c74 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:43:59 +0200 Subject: [PATCH 106/135] feat(syscalls): wrap stime --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/stime.rs | 79 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/stime.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 71449f7e..2699e46f 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -199,7 +199,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `ssetmask` - x86 - [x] `stat` - x86, x86_64 - [x] `statfs` -- [ ] `stime` - x86 +- [x] `stime` - x86 - [ ] `swapoff` - [ ] `swapon` - [ ] `symlink` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 4b4a1601..40c3d2fe 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -158,6 +158,8 @@ mod ssetmask; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod stat; mod statfs; +#[cfg(target_arch = "x86")] +mod stime; pub mod sys; mod uname; @@ -322,6 +324,8 @@ pub use stat::oldstat; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use stat::{StatError, stat}; pub use statfs::{StatfsError, statfs}; +#[cfg(target_arch = "x86")] +pub use stime::{StimeError, stime}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/stime.rs b/system/linux/syscalls/src/stime.rs new file mode 100644 index 00000000..367fab0d --- /dev/null +++ b/system/linux/syscalls/src/stime.rs @@ -0,0 +1,79 @@ +use celer_system_linux_ctypes::TimeT; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`stime`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StimeError { + Eperm, + Efault, + Einval, + Other(Errno), +} + +impl StimeError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Set the system clock to a whole-second Unix timestamp. +/// +/// This safe wrapper replaces the raw pointer with `&TimeT` and maps the raw +/// return into `Result<(), StimeError>`. +/// +/// On success, the kernel time-setting path accepted the request. +/// +/// See [`sys::stime`] for kernel behavior, required privileges, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`StimeError::Eperm`]: the caller lacks permission to set the system +/// time. +/// - [`StimeError::Efault`]: the kernel could not read the timestamp. +/// - [`StimeError::Einval`]: the timestamp was rejected by the time-setting +/// path. +/// - [`StimeError::Other`]: any other syscall error reported by the raw ABI. +#[cfg(target_arch = "x86")] +pub fn stime(tptr: &TimeT) -> Result<(), StimeError> { + // SAFETY: `tptr` is readable for one `TimeT`. + let ret = unsafe { sys::stime(tptr as *const TimeT) }; + unit_from_ret(ret as isize, StimeError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::TimeT; + + use super::{StimeError, stime}; + use crate::Errno; + + #[test] + fn test_stime_requires_permission_or_valid_time() { + let invalid = -1 as TimeT; + let result = stime(&invalid); + + assert!(matches!( + result, + Err(StimeError::Eperm | StimeError::Einval) + )); + } + + #[test] + fn test_stime_error_mapping() { + assert_eq!(StimeError::from_errno(Errno::Eperm), StimeError::Eperm); + assert_eq!(StimeError::from_errno(Errno::Efault), StimeError::Efault); + assert_eq!(StimeError::from_errno(Errno::Einval), StimeError::Einval); + assert_eq!( + StimeError::from_errno(Errno::Eio), + StimeError::Other(Errno::Eio) + ); + } +} From e448aa33688f4f4c470246aac09c3531476d4094 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 13:44:08 +0200 Subject: [PATCH 107/135] feat(syscalls): wrap swapoff --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/swapoff.rs | 92 ++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/swapoff.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2699e46f..cf0bec5f 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -200,7 +200,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `stat` - x86, x86_64 - [x] `statfs` - [x] `stime` - x86 -- [ ] `swapoff` +- [x] `swapoff` - [ ] `swapon` - [ ] `symlink` - x86, x86_64 - [ ] `sync` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 40c3d2fe..144bbfce 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -160,6 +160,7 @@ mod stat; mod statfs; #[cfg(target_arch = "x86")] mod stime; +mod swapoff; pub mod sys; mod uname; @@ -326,6 +327,7 @@ pub use stat::{StatError, stat}; pub use statfs::{StatfsError, statfs}; #[cfg(target_arch = "x86")] pub use stime::{StimeError, stime}; +pub use swapoff::{SwapoffError, swapoff}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/swapoff.rs b/system/linux/syscalls/src/swapoff.rs new file mode 100644 index 00000000..32b0f8ea --- /dev/null +++ b/system/linux/syscalls/src/swapoff.rs @@ -0,0 +1,92 @@ +use core::ffi::CStr; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`swapoff`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SwapoffError { + Eperm, + Efault, + Einval, + Enomem, + Other(Errno), +} + +impl SwapoffError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Disable swapping on an active swap area. +/// +/// This safe wrapper takes `specialfile` as a NUL-terminated [`CStr`] pathname +/// and maps the raw return into `Result<(), SwapoffError>`. +/// +/// On success, the selected swap area has been disabled. +/// +/// See [`sys::swapoff`] for kernel behavior, required privileges, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SwapoffError::Eperm`]: the caller lacks permission to disable swap. +/// - [`SwapoffError::Efault`]: the kernel could not read `specialfile`. +/// - [`SwapoffError::Einval`]: the resolved pathname does not identify an +/// active swap area. +/// - [`SwapoffError::Enomem`]: swap teardown or pathname handling could not +/// allocate memory. +/// - [`SwapoffError::Other`]: delegated pathname lookup, file-open, or swap +/// teardown error. +pub fn swapoff(specialfile: &CStr) -> Result<(), SwapoffError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::swapoff(specialfile.as_ptr()) }; + unit_from_ret(ret as isize, SwapoffError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use super::{SwapoffError, swapoff}; + use crate::Errno; + + #[test] + fn test_swapoff_missing_path_is_permission_or_lookup_error() { + let path = CString::new("/celer-swapoff-definitely-missing").unwrap(); + let result = swapoff(path.as_c_str()); + + assert!(matches!( + result, + Err(SwapoffError::Eperm | SwapoffError::Other(Errno::Enoent)) + )); + } + + #[test] + fn test_swapoff_error_mapping() { + assert_eq!(SwapoffError::from_errno(Errno::Eperm), SwapoffError::Eperm); + assert_eq!( + SwapoffError::from_errno(Errno::Efault), + SwapoffError::Efault + ); + assert_eq!( + SwapoffError::from_errno(Errno::Einval), + SwapoffError::Einval + ); + assert_eq!( + SwapoffError::from_errno(Errno::Enomem), + SwapoffError::Enomem + ); + assert_eq!( + SwapoffError::from_errno(Errno::Enoent), + SwapoffError::Other(Errno::Enoent) + ); + } +} From 1d3b9ceb975f94c1f2a50678a4d288a9bea9701b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:01:43 +0200 Subject: [PATCH 108/135] feat(syscalls): wrap swapon --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/swapon.rs | 120 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/swapon.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index cf0bec5f..4ad4873f 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -201,7 +201,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `statfs` - [x] `stime` - x86 - [x] `swapoff` -- [ ] `swapon` +- [x] `swapon` - [ ] `symlink` - x86, x86_64 - [ ] `sync` - [ ] `sysinfo` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 144bbfce..777e366d 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -161,6 +161,7 @@ mod statfs; #[cfg(target_arch = "x86")] mod stime; mod swapoff; +mod swapon; pub mod sys; mod uname; @@ -328,6 +329,7 @@ pub use statfs::{StatfsError, statfs}; #[cfg(target_arch = "x86")] pub use stime::{StimeError, stime}; pub use swapoff::{SwapoffError, swapoff}; +pub use swapon::{SwaponError, swapon}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/swapon.rs b/system/linux/syscalls/src/swapon.rs new file mode 100644 index 00000000..81086236 --- /dev/null +++ b/system/linux/syscalls/src/swapon.rs @@ -0,0 +1,120 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::Int; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`swapon`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SwaponError { + Eperm, + Efault, + Einval, + Enomem, + Enoent, + Enametoolong, + Enotdir, + Eacces, + Eloop, + Ebusy, + Enodev, + Other(Errno), +} + +impl SwaponError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Enoent => Self::Enoent, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Eloop => Self::Eloop, + Errno::Ebusy => Self::Ebusy, + Errno::Enodev => Self::Enodev, + errno => Self::Other(errno), + } + } +} + +/// Enable swapping on a block device or regular file. +/// +/// This safe wrapper takes `specialfile` as a NUL-terminated [`CStr`] pathname, +/// keeps the raw `swap_flags` integer argument, and maps the raw return into +/// `Result<(), SwaponError>`. +/// +/// On success, the kernel has activated the selected swap area. +/// +/// See [`sys::swapon`] for kernel behavior, required privileges, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SwaponError::Eperm`]: the caller lacks permission or no swap slot is +/// available on historical kernels. +/// - [`SwaponError::Efault`]: the kernel could not read `specialfile`. +/// - [`SwaponError::Einval`]: unsupported `swap_flags`, unsupported target +/// type, invalid swap signature, or no usable swap pages. +/// - [`SwaponError::Enomem`]: pathname or swap bookkeeping allocation failed. +/// - [`SwaponError::Enoent`]: the path is empty or missing. +/// - [`SwaponError::Enametoolong`]: the pathname is too long. +/// - [`SwaponError::Enotdir`]: traversal needed a directory. +/// - [`SwaponError::Eacces`]: traversal lacked search permission. +/// - [`SwaponError::Eloop`]: pathname resolution encountered a symlink loop. +/// - [`SwaponError::Ebusy`]: the target is busy or already active as swap. +/// - [`SwaponError::Enodev`]: a historical block-device target had no device. +/// - [`SwaponError::Other`]: delegated pathname, file-open, or swap setup +/// error. +pub fn swapon(specialfile: &CStr, swap_flags: Int) -> Result<(), SwaponError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::swapon(specialfile.as_ptr(), swap_flags) }; + unit_from_ret(ret as isize, SwaponError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use super::{SwaponError, swapon}; + use crate::Errno; + + #[test] + fn test_swapon_missing_path_is_permission_or_lookup_error() { + let path = CString::new("/celer-swapon-definitely-missing").unwrap(); + let result = swapon(path.as_c_str(), 0); + + assert!(matches!( + result, + Err(SwaponError::Eperm | SwaponError::Enoent) + )); + } + + #[test] + fn test_swapon_error_mapping() { + assert_eq!(SwaponError::from_errno(Errno::Eperm), SwaponError::Eperm); + assert_eq!(SwaponError::from_errno(Errno::Efault), SwaponError::Efault); + assert_eq!(SwaponError::from_errno(Errno::Einval), SwaponError::Einval); + assert_eq!(SwaponError::from_errno(Errno::Enomem), SwaponError::Enomem); + assert_eq!(SwaponError::from_errno(Errno::Enoent), SwaponError::Enoent); + assert_eq!( + SwaponError::from_errno(Errno::Raw(36)), + SwaponError::Enametoolong + ); + assert_eq!( + SwaponError::from_errno(Errno::Enotdir), + SwaponError::Enotdir + ); + assert_eq!(SwaponError::from_errno(Errno::Eacces), SwaponError::Eacces); + assert_eq!(SwaponError::from_errno(Errno::Eloop), SwaponError::Eloop); + assert_eq!(SwaponError::from_errno(Errno::Ebusy), SwaponError::Ebusy); + assert_eq!(SwaponError::from_errno(Errno::Enodev), SwaponError::Enodev); + assert_eq!( + SwaponError::from_errno(Errno::Eio), + SwaponError::Other(Errno::Eio) + ); + } +} From 5052eac9ab09271a1aed02d08510290dc822c543 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:02:21 +0200 Subject: [PATCH 109/135] feat(syscalls): wrap symlink --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 + system/linux/syscalls/src/symlink.rs | 151 +++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/symlink.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 4ad4873f..f7c1d635 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -202,7 +202,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `stime` - x86 - [x] `swapoff` - [x] `swapon` -- [ ] `symlink` - x86, x86_64 +- [x] `symlink` - x86, x86_64 - [ ] `sync` - [ ] `sysinfo` - [ ] `syslog` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 777e366d..57c721da 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -162,6 +162,8 @@ mod statfs; mod stime; mod swapoff; mod swapon; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod symlink; pub mod sys; mod uname; @@ -330,6 +332,8 @@ pub use statfs::{StatfsError, statfs}; pub use stime::{StimeError, stime}; pub use swapoff::{SwapoffError, swapoff}; pub use swapon::{SwaponError, swapon}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use symlink::{SymlinkError, symlink}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/symlink.rs b/system/linux/syscalls/src/symlink.rs new file mode 100644 index 00000000..e5a18dbc --- /dev/null +++ b/system/linux/syscalls/src/symlink.rs @@ -0,0 +1,151 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`symlink`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SymlinkError { + Efault, + Enametoolong, + Enoent, + Enomem, + Enotdir, + Erofs, + Eacces, + Eperm, + Other(Errno), +} + +impl SymlinkError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enomem => Self::Enomem, + Errno::Enotdir => Self::Enotdir, + Errno::Erofs => Self::Erofs, + Errno::Eacces => Self::Eacces, + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Create a symbolic link at `newname` whose stored target text is `oldname`. +/// +/// This safe wrapper takes both raw pathname pointers as NUL-terminated +/// [`CStr`] values and maps the raw return into `Result<(), SymlinkError>`. +/// +/// On success, the symlink was created. The kernel stores `oldname` as target +/// text; it does not resolve that path during creation. +/// +/// See [`sys::symlink`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`SymlinkError::Efault`]: the kernel could not read a pathname. +/// - [`SymlinkError::Enametoolong`]: a pathname is too long. +/// - [`SymlinkError::Enoent`]: a pathname is empty or a parent path is missing. +/// - [`SymlinkError::Enomem`]: pathname allocation failed. +/// - [`SymlinkError::Enotdir`]: traversal needed a directory. +/// - [`SymlinkError::Erofs`]: the destination is on a read-only filesystem. +/// - [`SymlinkError::Eacces`]: traversal or creation lacked permission. +/// - [`SymlinkError::Eperm`]: symlink creation is not permitted. +/// - [`SymlinkError::Other`]: delegated VFS, LSM, or filesystem error. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn symlink(oldname: &CStr, newname: &CStr) -> Result<(), SymlinkError> { + // SAFETY: both `CStr` values are readable NUL-terminated strings. + let ret = unsafe { sys::symlink(oldname.as_ptr(), newname.as_ptr()) }; + unit_from_ret(ret as isize, SymlinkError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::{CString, OsStr}, + fs, + os::unix::ffi::OsStrExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{SymlinkError, symlink}; + + fn temp_path(prefix: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_{prefix}_{now}")); + path + } + + #[test] + fn test_symlink_ok() { + let link_path = temp_path("symlink_link"); + let target = CString::new("symlink_target").unwrap(); + let link_cstr = + CString::new(link_path.as_os_str().as_encoded_bytes()).unwrap(); + + symlink(target.as_c_str(), link_cstr.as_c_str()).unwrap(); + + let stored = fs::read_link(&link_path).unwrap(); + assert_eq!(stored.as_os_str(), OsStr::from_bytes(b"symlink_target")); + + fs::remove_file(&link_path).unwrap(); + } + + #[test] + fn test_symlink_existing_destination_is_other_eexist() { + let link_path = temp_path("symlink_exists"); + fs::write(&link_path, b"occupied").unwrap(); + let target = CString::new("symlink_target").unwrap(); + let link_cstr = + CString::new(link_path.as_os_str().as_encoded_bytes()).unwrap(); + + let err = symlink(target.as_c_str(), link_cstr.as_c_str()).unwrap_err(); + assert_eq!(err, SymlinkError::Other(Errno::Eexist)); + + fs::remove_file(&link_path).unwrap(); + } + + #[test] + fn test_symlink_error_mapping() { + assert_eq!( + SymlinkError::from_errno(Errno::Efault), + SymlinkError::Efault + ); + assert_eq!( + SymlinkError::from_errno(Errno::Raw(36)), + SymlinkError::Enametoolong + ); + assert_eq!( + SymlinkError::from_errno(Errno::Enoent), + SymlinkError::Enoent + ); + assert_eq!( + SymlinkError::from_errno(Errno::Enomem), + SymlinkError::Enomem + ); + assert_eq!( + SymlinkError::from_errno(Errno::Enotdir), + SymlinkError::Enotdir + ); + assert_eq!(SymlinkError::from_errno(Errno::Erofs), SymlinkError::Erofs); + assert_eq!( + SymlinkError::from_errno(Errno::Eacces), + SymlinkError::Eacces + ); + assert_eq!(SymlinkError::from_errno(Errno::Eperm), SymlinkError::Eperm); + assert_eq!( + SymlinkError::from_errno(Errno::Eexist), + SymlinkError::Other(Errno::Eexist) + ); + } +} From 5eda8fea2e511d68bbc6b41a265f252be35d3880 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:02:51 +0200 Subject: [PATCH 110/135] feat(syscalls): wrap sync --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 ++ system/linux/syscalls/src/sync.rs | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sync.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index f7c1d635..7fc3e173 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -203,7 +203,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `swapoff` - [x] `swapon` - [x] `symlink` - x86, x86_64 -- [ ] `sync` +- [x] `sync` - [ ] `sysinfo` - [ ] `syslog` - [ ] `time` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 57c721da..bbcceb0a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -164,6 +164,7 @@ mod swapoff; mod swapon; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod symlink; +mod sync; pub mod sys; mod uname; @@ -334,6 +335,7 @@ pub use swapoff::{SwapoffError, swapoff}; pub use swapon::{SwaponError, swapon}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use symlink::{SymlinkError, symlink}; +pub use sync::sync; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/sync.rs b/system/linux/syscalls/src/sync.rs new file mode 100644 index 00000000..42e5817c --- /dev/null +++ b/system/linux/syscalls/src/sync.rs @@ -0,0 +1,21 @@ +use crate::sys; + +/// Flush all pending filesystem data to disk. +/// +/// This safe wrapper exposes the infallible syscall as `()`. +/// +/// See [`sys::sync`] for kernel behavior and source references. +pub fn sync() { + let _ = sys::sync(); +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::sync; + + #[test] + fn test_sync_returns_unit() { + assert_eq!(sync(), ()); + } +} From 32904b573ac94dea7070e458c4b0def8de9375df Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:03:24 +0200 Subject: [PATCH 111/135] feat(syscalls): wrap sysinfo --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 3 + system/linux/syscalls/src/sysinfo.rs | 109 +++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/sysinfo.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 7fc3e173..3e86a2a9 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -204,7 +204,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `swapon` - [x] `symlink` - x86, x86_64 - [x] `sync` -- [ ] `sysinfo` +- [x] `sysinfo` - [ ] `syslog` - [ ] `time` - x86, x86_64 - [ ] `times` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index bbcceb0a..2219b3a6 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -166,6 +166,7 @@ mod swapon; mod symlink; mod sync; pub mod sys; +mod sysinfo; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -336,6 +337,7 @@ pub use swapon::{SwaponError, swapon}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use symlink::{SymlinkError, symlink}; pub use sync::sync; +pub use sysinfo::{SysinfoError, sysinfo}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; @@ -353,4 +355,5 @@ pub mod linux_1_0 { pub use super::setrlimit::{SetrlimitError, setrlimit_1_0 as setrlimit}; pub use super::stat::{StatError, stat_1_0 as stat}; pub use super::statfs::{StatfsError, statfs_1_0 as statfs}; + pub use super::sysinfo::{SysinfoError, sysinfo_1_0 as sysinfo}; } diff --git a/system/linux/syscalls/src/sysinfo.rs b/system/linux/syscalls/src/sysinfo.rs new file mode 100644 index 00000000..5fb72752 --- /dev/null +++ b/system/linux/syscalls/src/sysinfo.rs @@ -0,0 +1,109 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::Sysinfo; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::linux_1_0::Sysinfo as Linux10Sysinfo; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`sysinfo`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SysinfoError { + Efault, + Other(Errno), +} + +impl SysinfoError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Copy system load, memory, swap, and task summary information into `info`. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit` and maps the raw return into +/// `Result<(), SysinfoError>`. +/// +/// On success, the kernel has initialized `info`. +/// +/// See [`sys::sysinfo`] for kernel behavior, ABI layout notes, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SysinfoError::Efault`]: the kernel could not write the output record. +/// - [`SysinfoError::Other`]: any other syscall error reported by the raw ABI. +pub fn sysinfo(info: &mut MaybeUninit) -> Result<(), SysinfoError> { + // SAFETY: `info` is writable for one `Sysinfo`; `MaybeUninit` avoids + // claiming the kernel writes into an already initialized Rust value. + let ret = unsafe { sys::sysinfo(info.as_mut_ptr()) }; + unit_from_ret(ret as isize, SysinfoError::from_errno) +} + +/// Copy Linux 1.0 system summary information into `info`. +/// +/// This safe wrapper mirrors [`sys::linux_1_0::sysinfo`] with the historical +/// Linux 1.0 output layout. +/// +/// On success, the kernel has initialized `info`. +/// +/// See [`sys::linux_1_0::sysinfo`] for kernel behavior, ABI layout notes, +/// reachable errors, and source references. +/// +/// # Errors +/// - [`SysinfoError::Efault`]: the kernel could not write the output record. +/// - [`SysinfoError::Other`]: any other syscall error reported by the raw ABI. +#[cfg(target_arch = "x86")] +pub fn sysinfo_1_0( + info: &mut MaybeUninit, +) -> Result<(), SysinfoError> { + // SAFETY: `info` is writable for one Linux 1.0 `Sysinfo` record. + let ret = unsafe { sys::linux_1_0::sysinfo(info.as_mut_ptr()) }; + unit_from_ret(ret as isize, SysinfoError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::Sysinfo; + #[cfg(target_arch = "x86")] + use celer_system_linux_ctypes::linux_1_0::Sysinfo as Linux10Sysinfo; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::sysinfo_1_0; + use super::{SysinfoError, sysinfo}; + + #[test] + fn test_sysinfo_ok() { + let mut info = MaybeUninit::::uninit(); + sysinfo(&mut info).unwrap(); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_sysinfo_1_0_ok() { + let mut info = MaybeUninit::::uninit(); + sysinfo_1_0(&mut info).unwrap(); + } + + #[test] + fn test_sysinfo_error_mapping() { + assert_eq!( + SysinfoError::from_errno(Errno::Efault), + SysinfoError::Efault + ); + assert_eq!( + SysinfoError::from_errno(Errno::Eio), + SysinfoError::Other(Errno::Eio) + ); + } +} From f7160c2831d6377c41b98dc1fc196a000f1c697c Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:04:44 +0200 Subject: [PATCH 112/135] feat(syscalls): wrap syslog --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/syslog.rs | 120 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/syslog.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 3e86a2a9..3e6f9a36 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -205,7 +205,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `symlink` - x86, x86_64 - [x] `sync` - [x] `sysinfo` -- [ ] `syslog` +- [x] `syslog` - [ ] `time` - x86, x86_64 - [ ] `times` - [ ] `truncate` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 2219b3a6..bbff2a70 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -167,6 +167,7 @@ mod symlink; mod sync; pub mod sys; mod sysinfo; +mod syslog; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -338,6 +339,7 @@ pub use swapon::{SwaponError, swapon}; pub use symlink::{SymlinkError, symlink}; pub use sync::sync; pub use sysinfo::{SysinfoError, sysinfo}; +pub use syslog::{SyslogError, syslog}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/syslog.rs b/system/linux/syscalls/src/syslog.rs new file mode 100644 index 00000000..3ecef44f --- /dev/null +++ b/system/linux/syscalls/src/syslog.rs @@ -0,0 +1,120 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Char, Int}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`syslog`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SyslogError { + Eperm, + Einval, + Efault, + Eintr, + Enomem, + Other(Errno), +} + +impl SyslogError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + Errno::Eintr => Self::Eintr, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// Read from or control the kernel log buffer. +/// +/// This safe wrapper keeps the raw multiplexed `type_` command and `len` +/// integer, replaces the raw output pointer with an optional initialized +/// capacity `buf`, and maps the raw return into `Result`. +/// +/// When `buf` is `Some`, `len` must be nonnegative and no larger than the +/// slice length. When `buf` is `None`, the raw syscall receives a null pointer. +/// +/// On success, returns the raw command result: `0` for control commands, a +/// byte count for read commands, or a size/count value for query commands. +/// +/// See [`sys::syslog`] for command semantics, required privileges, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`SyslogError::Eperm`]: the selected command requires permission. +/// - [`SyslogError::Einval`]: the command or length is invalid, or `len` +/// exceeds the provided buffer. +/// - [`SyslogError::Efault`]: the kernel could not write the buffer. +/// - [`SyslogError::Eintr`]: a blocking read was interrupted. +/// - [`SyslogError::Enomem`]: temporary log-copy allocation failed. +/// - [`SyslogError::Other`]: delegated security-policy error. +pub fn syslog( + type_: Int, + buf: Option<&mut [MaybeUninit]>, + len: Int, +) -> Result { + let ptr = match buf { + Some(buf) => { + let len = usize::try_from(len).map_err(|_| SyslogError::Einval)?; + if len > buf.len() { + return Err(SyslogError::Einval); + } + buf.as_mut_ptr().cast::() + } + None => core::ptr::null_mut(), + }; + + // SAFETY: a non-null pointer is backed by `buf` for at least `len` bytes; + // null is passed only when `buf` is `None`. + let ret = unsafe { sys::syslog(type_, ptr, len) }; + result_from_ret(ret as isize, |ret| ret as Int, SyslogError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use crate::Errno; + + use super::{SyslogError, syslog}; + + #[test] + fn test_syslog_rejects_len_larger_than_buffer() { + let mut buf = [MaybeUninit::uninit(); 4]; + + assert_eq!(syslog(3, Some(&mut buf), 5), Err(SyslogError::Einval)); + } + + #[test] + fn test_syslog_accepts_zero_len_buffer() { + let mut buf = []; + let result = syslog(3, Some(&mut buf), 0); + + assert!(matches!(result, Ok(0) | Err(SyslogError::Eperm))); + } + + #[test] + fn test_syslog_invalid_type_is_permission_or_invalid() { + let err = syslog(11, None, 0).unwrap_err(); + assert!(matches!(err, SyslogError::Eperm | SyslogError::Einval)); + } + + #[test] + fn test_syslog_error_mapping() { + assert_eq!(SyslogError::from_errno(Errno::Eperm), SyslogError::Eperm); + assert_eq!(SyslogError::from_errno(Errno::Einval), SyslogError::Einval); + assert_eq!(SyslogError::from_errno(Errno::Efault), SyslogError::Efault); + assert_eq!(SyslogError::from_errno(Errno::Eintr), SyslogError::Eintr); + assert_eq!(SyslogError::from_errno(Errno::Enomem), SyslogError::Enomem); + assert_eq!( + SyslogError::from_errno(Errno::Eacces), + SyslogError::Other(Errno::Eacces) + ); + } +} From 1b309ca5424e1fc860e22e68aca343c6957d8430 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:05:34 +0200 Subject: [PATCH 113/135] feat(syscalls): wrap time --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 4 ++ system/linux/syscalls/src/time.rs | 84 ++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/time.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 3e6f9a36..91f475e6 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -206,7 +206,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sync` - [x] `sysinfo` - [x] `syslog` -- [ ] `time` - x86, x86_64 +- [x] `time` - x86, x86_64 - [ ] `times` - [ ] `truncate` - [ ] `umask` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index bbff2a70..02700e1a 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -168,6 +168,8 @@ mod sync; pub mod sys; mod sysinfo; mod syslog; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod time; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -340,6 +342,8 @@ pub use symlink::{SymlinkError, symlink}; pub use sync::sync; pub use sysinfo::{SysinfoError, sysinfo}; pub use syslog::{SyslogError, syslog}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use time::{TimeError, time}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/time.rs b/system/linux/syscalls/src/time.rs new file mode 100644 index 00000000..09f9d2c4 --- /dev/null +++ b/system/linux/syscalls/src/time.rs @@ -0,0 +1,84 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::TimeT; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`time`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TimeError { + Efault, + Other(Errno), +} + +impl TimeError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Return the current calendar time in seconds since the Unix epoch. +/// +/// This safe wrapper replaces the nullable raw output pointer with +/// `Option<&mut MaybeUninit>` and maps the raw return into +/// `Result`. +/// +/// On success, returns the current time. When `tloc` is `Some`, the kernel has +/// also initialized that slot with the same value. +/// +/// See [`sys::time`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`TimeError::Efault`]: the kernel could not write the optional output. +/// - [`TimeError::Other`]: any other syscall error reported by the raw ABI. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn time(tloc: Option<&mut MaybeUninit>) -> Result { + let ptr = tloc + .map(|tloc| tloc.as_mut_ptr()) + .unwrap_or(core::ptr::null_mut()); + + // SAFETY: non-null pointers are writable for one `TimeT`; null is an + // explicitly supported input. + let ret = unsafe { sys::time(ptr) }; + result_from_ret(ret as isize, |ret| ret as TimeT, TimeError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use crate::Errno; + + use super::{TimeError, time}; + + #[test] + fn test_time_null_ok() { + assert!(time(None).unwrap() > 0); + } + + #[test] + fn test_time_writes_output() { + let mut stored = MaybeUninit::uninit(); + let now = time(Some(&mut stored)).unwrap(); + + // SAFETY: `time` returned success, so the kernel initialized `stored`. + let stored = unsafe { stored.assume_init() }; + assert_eq!(now, stored); + } + + #[test] + fn test_time_error_mapping() { + assert_eq!(TimeError::from_errno(Errno::Efault), TimeError::Efault); + assert_eq!( + TimeError::from_errno(Errno::Eio), + TimeError::Other(Errno::Eio) + ); + } +} From 56dcb2c1cee7c85dc1ce8a1bd63fcfcb7b356b63 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:05:57 +0200 Subject: [PATCH 114/135] feat(syscalls): wrap times --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 ++ system/linux/syscalls/src/times.rs | 49 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/times.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 91f475e6..c63b29e6 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -207,7 +207,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `sysinfo` - [x] `syslog` - [x] `time` - x86, x86_64 -- [ ] `times` +- [x] `times` - [ ] `truncate` - [ ] `umask` - [ ] `umount` diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 02700e1a..517acc66 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -170,6 +170,7 @@ mod sysinfo; mod syslog; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod time; +mod times; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -344,6 +345,7 @@ pub use sysinfo::{SysinfoError, sysinfo}; pub use syslog::{SyslogError, syslog}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use time::{TimeError, time}; +pub use times::times; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/times.rs b/system/linux/syscalls/src/times.rs new file mode 100644 index 00000000..88fb0261 --- /dev/null +++ b/system/linux/syscalls/src/times.rs @@ -0,0 +1,49 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Long, Tms}; + +use crate::sys; + +/// Return process and child CPU time statistics. +/// +/// This safe wrapper replaces the nullable raw output pointer with +/// `Option<&mut MaybeUninit>` and returns the raw clock tick count. +/// +/// On success, when `tbuf` is `Some`, the kernel has initialized that output +/// record. The tick count is returned directly because this syscall can return +/// a negative value on success after wraparound. +/// +/// See [`sys::times`] for kernel behavior, reachable errors, and source +/// references. +pub fn times(tbuf: Option<&mut MaybeUninit>) -> Long { + let ptr = tbuf + .map(|tbuf| tbuf.as_mut_ptr()) + .unwrap_or(core::ptr::null_mut()); + + // SAFETY: non-null pointers are writable for one `Tms`; null is an + // explicitly supported input. + unsafe { sys::times(ptr) } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::Tms; + + use super::times; + + #[test] + fn test_times_null_ok() { + let _ = times(None); + } + + #[test] + fn test_times_writes_output() { + let mut tms = MaybeUninit::::uninit(); + let ret = times(Some(&mut tms)); + + assert_ne!(ret, -14, "times should not fail with EFAULT: {ret}"); + } +} From 5da2b0107129e9387daef883f2307f0ac6363bbb Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:06:38 +0200 Subject: [PATCH 115/135] feat(syscalls): wrap truncate --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 3 + system/linux/syscalls/src/truncate.rs | 204 +++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/truncate.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index c63b29e6..2df65639 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -208,7 +208,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `syslog` - [x] `time` - x86, x86_64 - [x] `times` -- [ ] `truncate` +- [x] `truncate` - [ ] `umask` - [ ] `umount` - [ ] `unlink` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 517acc66..79dbd1ab 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -171,6 +171,7 @@ mod syslog; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod time; mod times; +mod truncate; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -346,6 +347,7 @@ pub use syslog::{SyslogError, syslog}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use time::{TimeError, time}; pub use times::times; +pub use truncate::{TruncateError, truncate}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; @@ -364,4 +366,5 @@ pub mod linux_1_0 { pub use super::stat::{StatError, stat_1_0 as stat}; pub use super::statfs::{StatfsError, statfs_1_0 as statfs}; pub use super::sysinfo::{SysinfoError, sysinfo_1_0 as sysinfo}; + pub use super::truncate::{TruncateError, truncate_1_0 as truncate}; } diff --git a/system/linux/syscalls/src/truncate.rs b/system/linux/syscalls/src/truncate.rs new file mode 100644 index 00000000..41e16dca --- /dev/null +++ b/system/linux/syscalls/src/truncate.rs @@ -0,0 +1,204 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::OffT; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::UnsignedInt; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`truncate`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TruncateError { + Efault, + Enametoolong, + Enoent, + Enomem, + Enotdir, + Eacces, + Erofs, + Einval, + Other(Errno), +} + +impl TruncateError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enomem => Self::Enomem, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Erofs => Self::Erofs, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Set the size of the file named by `path` to `length` bytes. +/// +/// This safe wrapper takes `path` as a NUL-terminated [`CStr`] and maps the raw +/// return into `Result<(), TruncateError>`. +/// +/// On success, the kernel has updated the file size. +/// +/// See [`sys::truncate`] for kernel behavior, ABI history, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`TruncateError::Efault`]: the kernel could not read `path`. +/// - [`TruncateError::Enametoolong`]: the pathname is too long. +/// - [`TruncateError::Enoent`]: the path is empty or missing. +/// - [`TruncateError::Enomem`]: pathname allocation failed. +/// - [`TruncateError::Enotdir`]: traversal needed a directory. +/// - [`TruncateError::Eacces`]: traversal or truncation lacked permission. +/// - [`TruncateError::Erofs`]: the file is on a read-only filesystem. +/// - [`TruncateError::Einval`]: the requested length is invalid. +/// - [`TruncateError::Other`]: delegated symlink, notification, or filesystem +/// error. +pub fn truncate(path: &CStr, length: OffT) -> Result<(), TruncateError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::truncate(path.as_ptr(), length) }; + unit_from_ret(ret as isize, TruncateError::from_errno) +} + +/// Set the size of the file named by `path` through the Linux 1.0 unsigned +/// `truncate` ABI. +/// +/// This safe wrapper mirrors [`sys::linux_1_0::truncate`] and maps the raw +/// return into `Result<(), TruncateError>`. +/// +/// See [`sys::linux_1_0::truncate`] for kernel behavior, ABI history, +/// reachable errors, and source references. +#[cfg(target_arch = "x86")] +pub fn truncate_1_0( + path: &CStr, + length: UnsignedInt, +) -> Result<(), TruncateError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::linux_1_0::truncate(path.as_ptr(), length) }; + unit_from_ret(ret as isize, TruncateError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, OpenOptions}, + io::Write as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::OffT; + #[cfg(target_arch = "x86")] + use celer_system_linux_ctypes::UnsignedInt; + + use crate::Errno; + + #[cfg(target_arch = "x86")] + use super::truncate_1_0; + use super::{TruncateError, truncate}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_truncate_{now}")); + path + } + + #[test] + fn test_truncate_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .unwrap(); + file.write_all(b"abcdef").unwrap(); + drop(file); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + truncate(path_cstr.as_c_str(), 3 as OffT).unwrap(); + assert_eq!(fs::metadata(&path).unwrap().len(), 3); + + fs::remove_file(path).unwrap(); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_truncate_1_0_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .unwrap(); + file.write_all(b"abcdef").unwrap(); + drop(file); + let path_cstr = + CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + truncate_1_0(path_cstr.as_c_str(), 2 as UnsignedInt).unwrap(); + assert_eq!(fs::metadata(&path).unwrap().len(), 2); + + fs::remove_file(path).unwrap(); + } + + #[test] + fn test_truncate_missing_path() { + let path = CString::new("/celer-truncate-definitely-missing").unwrap(); + + assert_eq!( + truncate(path.as_c_str(), 0).unwrap_err(), + TruncateError::Enoent + ); + } + + #[test] + fn test_truncate_error_mapping() { + assert_eq!( + TruncateError::from_errno(Errno::Efault), + TruncateError::Efault + ); + assert_eq!( + TruncateError::from_errno(Errno::Raw(36)), + TruncateError::Enametoolong + ); + assert_eq!( + TruncateError::from_errno(Errno::Enoent), + TruncateError::Enoent + ); + assert_eq!( + TruncateError::from_errno(Errno::Enomem), + TruncateError::Enomem + ); + assert_eq!( + TruncateError::from_errno(Errno::Enotdir), + TruncateError::Enotdir + ); + assert_eq!( + TruncateError::from_errno(Errno::Eacces), + TruncateError::Eacces + ); + assert_eq!( + TruncateError::from_errno(Errno::Erofs), + TruncateError::Erofs + ); + assert_eq!( + TruncateError::from_errno(Errno::Einval), + TruncateError::Einval + ); + assert_eq!( + TruncateError::from_errno(Errno::Eio), + TruncateError::Other(Errno::Eio) + ); + } +} From 03a8aa4a13036015ffd357f217965e67b7bdb022 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:06:55 +0200 Subject: [PATCH 116/135] feat(syscalls): wrap umask --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 ++ system/linux/syscalls/src/umask.rs | 42 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/umask.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 2df65639..7dd31d1a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -209,7 +209,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `time` - x86, x86_64 - [x] `times` - [x] `truncate` -- [ ] `umask` +- [x] `umask` - [ ] `umount` - [ ] `unlink` - x86, x86_64 - [ ] `uselib` - x86 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 79dbd1ab..20e61b8c 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -172,6 +172,7 @@ mod syslog; mod time; mod times; mod truncate; +mod umask; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -348,6 +349,7 @@ pub use syslog::{SyslogError, syslog}; pub use time::{TimeError, time}; pub use times::times; pub use truncate::{TruncateError, truncate}; +pub use umask::umask; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/umask.rs b/system/linux/syscalls/src/umask.rs new file mode 100644 index 00000000..ab78e935 --- /dev/null +++ b/system/linux/syscalls/src/umask.rs @@ -0,0 +1,42 @@ +use celer_system_linux_ctypes::UModeT; + +use crate::sys; + +/// Set the calling process's file mode creation mask. +/// +/// This safe wrapper preserves the raw integer argument and returns the +/// previous mask. +/// +/// See [`sys::umask`] for kernel behavior and source references. +pub fn umask(mask: UModeT) -> UModeT { + sys::umask(mask) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::UModeT; + + use crate::sys::test_support::process_global_state_guard; + + use super::umask; + + struct RestoreUmask(UModeT); + + impl Drop for RestoreUmask { + fn drop(&mut self) { + let _ = umask(self.0); + } + } + + #[test] + fn test_umask_returns_previous_mask() { + let _guard = process_global_state_guard(); + let original = umask(0); + let _restore = RestoreUmask(original); + + assert_eq!(umask(0o123 as UModeT), 0); + assert_eq!(umask(!0 as UModeT), 0o123); + assert_eq!(umask(0), 0o777); + } +} From 1a12f28d84cb1d35b4550e33e109a4920ad9a908 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:07:27 +0200 Subject: [PATCH 117/135] feat(syscalls): wrap umount --- system/linux/syscalls/WRAP_PROGRESS.md | 2 +- system/linux/syscalls/src/lib.rs | 2 + system/linux/syscalls/src/umount.rs | 97 ++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 system/linux/syscalls/src/umount.rs diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index 7dd31d1a..b95e0aa0 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -210,7 +210,7 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `times` - [x] `truncate` - [x] `umask` -- [ ] `umount` +- [x] `umount` - [ ] `unlink` - x86, x86_64 - [ ] `uselib` - x86 - [ ] `ustat` - x86, x86_64 diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 20e61b8c..9e960fef 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -173,6 +173,7 @@ mod time; mod times; mod truncate; mod umask; +mod umount; mod uname; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -350,6 +351,7 @@ pub use time::{TimeError, time}; pub use times::times; pub use truncate::{TruncateError, truncate}; pub use umask::umask; +pub use umount::{UmountError, umount}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; diff --git a/system/linux/syscalls/src/umount.rs b/system/linux/syscalls/src/umount.rs new file mode 100644 index 00000000..fdc96436 --- /dev/null +++ b/system/linux/syscalls/src/umount.rs @@ -0,0 +1,97 @@ +use core::ffi::CStr; + +use crate::helpers::unit_from_ret; +use crate::{errno::Errno, sys}; + +/// Errors returned by [`umount`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UmountError { + Eperm, + Einval, + Eacces, + Enxio, + Enoent, + Ebusy, + Enotdir, + Other(Errno), +} + +impl UmountError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Einval => Self::Einval, + Errno::Eacces => Self::Eacces, + Errno::Raw(6) => Self::Enxio, + Errno::Enoent => Self::Enoent, + Errno::Ebusy => Self::Ebusy, + Errno::Enotdir => Self::Enotdir, + errno => Self::Other(errno), + } + } +} + +/// Unmount a filesystem or mount point. +/// +/// This safe wrapper takes `name` as a NUL-terminated [`CStr`] pathname and +/// maps the raw return into `Result<(), UmountError>`. On x86_64 and aarch64, +/// the raw layer uses the `umount2` syscall slot with flags set to zero. +/// +/// On success, the selected mount has been unmounted or, for the historical +/// root-filesystem case, remounted read-only. +/// +/// See [`sys::umount`] for kernel behavior, required privileges, reachable +/// errors, and source references. +/// +/// # Errors +/// - [`UmountError::Eperm`]: the caller lacks unmount permission. +/// - [`UmountError::Einval`]: the path does not identify a mount target. +/// - [`UmountError::Eacces`]: lookup or historical device access was denied. +/// - [`UmountError::Enxio`]: a historical block-device major was invalid. +/// - [`UmountError::Enoent`]: the path or mounted superblock was not found. +/// - [`UmountError::Ebusy`]: the target is still busy. +/// - [`UmountError::Enotdir`]: traversal needed a directory. +/// - [`UmountError::Other`]: delegated pathname or namespace unmount error. +pub fn umount(name: &CStr) -> Result<(), UmountError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::umount(name.as_ptr()) }; + unit_from_ret(ret as isize, UmountError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use super::{UmountError, umount}; + use crate::Errno; + + #[test] + fn test_umount_missing_path_is_permission_or_lookup_error() { + let path = CString::new("/celer-umount-definitely-missing").unwrap(); + let result = umount(path.as_c_str()); + + assert!(matches!( + result, + Err(UmountError::Eperm | UmountError::Enoent) + )); + } + + #[test] + fn test_umount_error_mapping() { + assert_eq!(UmountError::from_errno(Errno::Eperm), UmountError::Eperm); + assert_eq!(UmountError::from_errno(Errno::Einval), UmountError::Einval); + assert_eq!(UmountError::from_errno(Errno::Eacces), UmountError::Eacces); + assert_eq!(UmountError::from_errno(Errno::Raw(6)), UmountError::Enxio); + assert_eq!(UmountError::from_errno(Errno::Enoent), UmountError::Enoent); + assert_eq!(UmountError::from_errno(Errno::Ebusy), UmountError::Ebusy); + assert_eq!( + UmountError::from_errno(Errno::Enotdir), + UmountError::Enotdir + ); + assert_eq!( + UmountError::from_errno(Errno::Eio), + UmountError::Other(Errno::Eio) + ); + } +} From a2401d747c20c94229e068c9fde25fc4e171004b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 118/135] feat(syscalls): wrap unlink --- system/linux/syscalls/src/unlink.rs | 90 +++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 system/linux/syscalls/src/unlink.rs diff --git a/system/linux/syscalls/src/unlink.rs b/system/linux/syscalls/src/unlink.rs new file mode 100644 index 00000000..5d04ba96 --- /dev/null +++ b/system/linux/syscalls/src/unlink.rs @@ -0,0 +1,90 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`unlink`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UnlinkError { + /// Another errno returned by pathname lookup, filesystem, or security + /// handling. + Other(Errno), +} + +impl UnlinkError { + fn from_errno(errno: Errno) -> Self { + Self::Other(errno) + } +} + +/// Remove the directory entry named by `pathname`. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// syscall return into `Result<(), UnlinkError>`. +/// +/// On success, the named directory entry has been removed. +/// +/// See [`sys::unlink`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`UnlinkError::Other`]: delegated pathname lookup, filesystem, or +/// security-hook error. +pub fn unlink(pathname: &CStr) -> Result<(), UnlinkError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::unlink(pathname.as_ptr()) }; + unit_from_ret(ret as isize, UnlinkError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::File, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{UnlinkError, unlink}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_unlink_{now}")); + path + } + + #[test] + fn test_unlink_ok() { + let path = temp_path(); + File::create(&path).unwrap(); + let c_path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(unlink(c_path.as_c_str()), Ok(())); + assert!(!path.exists()); + } + + #[test] + fn test_unlink_missing_path() { + let path = CString::new("/definitely/not/a/real/celer-unlink").unwrap(); + + assert_eq!( + unlink(path.as_c_str()), + Err(UnlinkError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_unlink_error_mapping() { + assert_eq!( + UnlinkError::from_errno(Errno::Eacces), + UnlinkError::Other(Errno::Eacces) + ); + } +} From 76cf1acc77acb8be3aef3e013f02ab02f29fcfcf Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 119/135] feat(syscalls): wrap uselib --- system/linux/syscalls/src/uselib.rs | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 system/linux/syscalls/src/uselib.rs diff --git a/system/linux/syscalls/src/uselib.rs b/system/linux/syscalls/src/uselib.rs new file mode 100644 index 00000000..153ac93a --- /dev/null +++ b/system/linux/syscalls/src/uselib.rs @@ -0,0 +1,122 @@ +use core::ffi::CStr; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`uselib`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UselibError { + Efault, + Enametoolong, + Enoent, + Enomem, + Enotdir, + Eacces, + Emfile, + Enfile, + Enoexec, + Enosys, + Other(Errno), +} + +impl UselibError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Raw(36) => Self::Enametoolong, + Errno::Enoent => Self::Enoent, + Errno::Enomem => Self::Enomem, + Errno::Enotdir => Self::Enotdir, + Errno::Eacces => Self::Eacces, + Errno::Emfile => Self::Emfile, + Errno::Raw(23) => Self::Enfile, + Errno::Enoexec => Self::Enoexec, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Load a shared library through the historical `uselib` ABI. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and maps the raw +/// return into `Result<(), UselibError>`. +/// +/// See [`sys::uselib`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`UselibError::Efault`]: the kernel could not read `library`. +/// - [`UselibError::Enametoolong`]: the pathname is too long. +/// - [`UselibError::Enoent`]: the pathname is empty or missing. +/// - [`UselibError::Enomem`]: pathname allocation failed. +/// - [`UselibError::Enotdir`]: traversal needed a directory. +/// - [`UselibError::Eacces`]: access or loader permission was denied. +/// - [`UselibError::Emfile`]: the process file table was full. +/// - [`UselibError::Enfile`]: the system file table was full. +/// - [`UselibError::Enoexec`]: no loader accepted the file. +/// - [`UselibError::Enosys`]: the running kernel left this syscall +/// unimplemented. +/// - [`UselibError::Other`]: another loader or filesystem errno. +#[cfg(target_arch = "x86")] +pub fn uselib(library: &CStr) -> Result<(), UselibError> { + // SAFETY: `CStr` provides a readable NUL-terminated pathname. + let ret = unsafe { sys::uselib(library.as_ptr()) }; + unit_from_ret(ret as isize, UselibError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use crate::Errno; + + use super::{UselibError, uselib}; + + #[cfg(target_arch = "x86")] + #[test] + fn test_uselib_missing_path() { + let path = + CString::new("/definitely/not/a/real/celer-uselib-library.so") + .unwrap(); + + assert!(matches!( + uselib(path.as_c_str()), + Err(UselibError::Enoent + | UselibError::Enoexec + | UselibError::Enosys) + )); + } + + #[test] + fn test_uselib_error_mapping() { + assert_eq!(UselibError::from_errno(Errno::Efault), UselibError::Efault); + assert_eq!( + UselibError::from_errno(Errno::Raw(36)), + UselibError::Enametoolong + ); + assert_eq!(UselibError::from_errno(Errno::Enoent), UselibError::Enoent); + assert_eq!(UselibError::from_errno(Errno::Enomem), UselibError::Enomem); + assert_eq!( + UselibError::from_errno(Errno::Enotdir), + UselibError::Enotdir + ); + assert_eq!(UselibError::from_errno(Errno::Eacces), UselibError::Eacces); + assert_eq!(UselibError::from_errno(Errno::Emfile), UselibError::Emfile); + assert_eq!( + UselibError::from_errno(Errno::Raw(23)), + UselibError::Enfile + ); + assert_eq!( + UselibError::from_errno(Errno::Enoexec), + UselibError::Enoexec + ); + assert_eq!(UselibError::from_errno(Errno::Enosys), UselibError::Enosys); + assert_eq!( + UselibError::from_errno(Errno::Eio), + UselibError::Other(Errno::Eio) + ); + } +} From a5df756ade26828ceae22d7ac4b34df799202a05 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 120/135] feat(syscalls): wrap ustat --- system/linux/syscalls/src/ustat.rs | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 system/linux/syscalls/src/ustat.rs diff --git a/system/linux/syscalls/src/ustat.rs b/system/linux/syscalls/src/ustat.rs new file mode 100644 index 00000000..89b0f4a2 --- /dev/null +++ b/system/linux/syscalls/src/ustat.rs @@ -0,0 +1,91 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, Ustat}; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`ustat`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UstatError { + Enosys, + Einval, + Efault, + Other(Errno), +} + +impl UstatError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Enosys => Self::Enosys, + Errno::Einval => Self::Einval, + Errno::Efault => Self::Efault, + errno => Self::Other(errno), + } + } +} + +/// Return filesystem status through the legacy `ustat` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit`. +/// +/// `Ok(())` means the kernel initialized `ubuf`. +/// +/// See [`sys::ustat`] for kernel behavior, ABI notes, reachable errors, and +/// source references. +/// +/// # Errors +/// - [`UstatError::Enosys`]: the running kernel reports this entrypoint as +/// unimplemented. +/// - [`UstatError::Einval`]: `dev` does not select a mounted superblock. +/// - [`UstatError::Efault`]: the kernel could not write `ubuf`. +/// - [`UstatError::Other`]: delegated filesystem error. +pub fn ustat( + dev: Int, + ubuf: &mut MaybeUninit, +) -> Result<(), UstatError> { + // SAFETY: `ubuf` provides writable storage for one `Ustat`. + let ret = unsafe { sys::ustat(dev, ubuf.as_mut_ptr()) }; + unit_from_ret(ret as isize, UstatError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + use std::{fs, os::unix::fs::MetadataExt as _}; + + use celer_system_linux_ctypes::{Int, Ustat}; + + use crate::Errno; + + use super::{UstatError, ustat}; + + #[test] + fn test_ustat_valid_device() { + let dev = fs::metadata("/").unwrap().dev() as Int; + let mut ubuf = MaybeUninit::::uninit(); + + assert_eq!(ustat(dev, &mut ubuf), Ok(())); + } + + #[test] + fn test_ustat_invalid_device() { + let mut ubuf = MaybeUninit::::uninit(); + + assert_eq!(ustat(-1, &mut ubuf), Err(UstatError::Einval)); + } + + #[test] + fn test_ustat_error_mapping() { + assert_eq!(UstatError::from_errno(Errno::Enosys), UstatError::Enosys); + assert_eq!(UstatError::from_errno(Errno::Einval), UstatError::Einval); + assert_eq!(UstatError::from_errno(Errno::Efault), UstatError::Efault); + assert_eq!( + UstatError::from_errno(Errno::Eio), + UstatError::Other(Errno::Eio) + ); + } +} From e33fed786baa1c8fef4d54e48f685011b9a6a07b Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 121/135] feat(syscalls): wrap utime --- system/linux/syscalls/src/utime.rs | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 system/linux/syscalls/src/utime.rs diff --git a/system/linux/syscalls/src/utime.rs b/system/linux/syscalls/src/utime.rs new file mode 100644 index 00000000..19641fb0 --- /dev/null +++ b/system/linux/syscalls/src/utime.rs @@ -0,0 +1,126 @@ +use core::ffi::CStr; + +use celer_system_linux_ctypes::Utimbuf; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`utime`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UtimeError { + Efault, + Eperm, + Erofs, + Other(Errno), +} + +impl UtimeError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Efault => Self::Efault, + Errno::Eperm => Self::Eperm, + Errno::Erofs => Self::Erofs, + errno => Self::Other(errno), + } + } +} + +/// Update a file's access and modification timestamps. +/// +/// This safe wrapper takes a NUL-terminated [`CStr`] pathname and represents a +/// nullable timestamp pointer as `Option<&Utimbuf>`. +/// +/// `Ok(())` means the kernel accepted the timestamp update. +/// +/// See [`sys::utime`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`UtimeError::Efault`]: the kernel could not read `pathname` or `times`. +/// - [`UtimeError::Eperm`]: the caller is not permitted to set explicit +/// timestamps. +/// - [`UtimeError::Erofs`]: the target is on a read-only filesystem. +/// - [`UtimeError::Other`]: delegated pathname lookup, filesystem, or +/// security-hook error. +pub fn utime( + pathname: &CStr, + times: Option<&Utimbuf>, +) -> Result<(), UtimeError> { + let times = times.map_or(core::ptr::null(), |times| times as *const _); + // SAFETY: `pathname` is a readable NUL-terminated string and `times` is + // either null or readable for one `Utimbuf`. + let ret = unsafe { sys::utime(pathname.as_ptr(), times) }; + unit_from_ret(ret as isize, UtimeError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + ffi::CString, + fs::{self, OpenOptions}, + os::unix::fs::MetadataExt as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use celer_system_linux_ctypes::{TimeT, Utimbuf}; + + use crate::Errno; + + use super::{UtimeError, utime}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_utime_{now}")); + path + } + + #[test] + fn test_utime_ok() { + let path = temp_path(); + OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .unwrap(); + let c_path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + let times = Utimbuf { + actime: 3 as TimeT, + modtime: 4 as TimeT, + }; + + assert_eq!(utime(c_path.as_c_str(), Some(×)), Ok(())); + + let metadata = fs::metadata(&path).unwrap(); + assert_eq!(metadata.atime(), 3); + assert_eq!(metadata.mtime(), 4); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_utime_missing_path() { + let path = CString::new("/definitely/not/a/real/celer-utime").unwrap(); + + assert_eq!( + utime(path.as_c_str(), None), + Err(UtimeError::Other(Errno::Enoent)) + ); + } + + #[test] + fn test_utime_error_mapping() { + assert_eq!(UtimeError::from_errno(Errno::Efault), UtimeError::Efault); + assert_eq!(UtimeError::from_errno(Errno::Eperm), UtimeError::Eperm); + assert_eq!(UtimeError::from_errno(Errno::Erofs), UtimeError::Erofs); + assert_eq!( + UtimeError::from_errno(Errno::Enoent), + UtimeError::Other(Errno::Enoent) + ); + } +} From 6a39a7c70eca057413219ddf495604bb75eace06 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 122/135] feat(syscalls): wrap vhangup --- system/linux/syscalls/src/vhangup.rs | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 system/linux/syscalls/src/vhangup.rs diff --git a/system/linux/syscalls/src/vhangup.rs b/system/linux/syscalls/src/vhangup.rs new file mode 100644 index 00000000..58e366c3 --- /dev/null +++ b/system/linux/syscalls/src/vhangup.rs @@ -0,0 +1,58 @@ +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`vhangup`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum VhangupError { + Eperm, + Other(Errno), +} + +impl VhangupError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Hang up the calling process's controlling terminal. +/// +/// This safe wrapper maps the raw return value into +/// `Result<(), VhangupError>`. +/// +/// See [`sys::vhangup`] for kernel behavior, privileges, reachable errors, and +/// source references. +/// +/// # Errors +/// - [`VhangupError::Eperm`]: the caller lacks permission. +/// - [`VhangupError::Other`]: any other errno reported by the raw ABI. +pub fn vhangup() -> Result<(), VhangupError> { + let ret = sys::vhangup(); + unit_from_ret(ret as isize, VhangupError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::{VhangupError, vhangup}; + + #[test] + fn test_vhangup_reports_success_or_permission_error() { + let ret = vhangup(); + assert!(matches!(ret, Ok(()) | Err(VhangupError::Eperm))); + } + + #[test] + fn test_vhangup_error_mapping() { + assert_eq!(VhangupError::from_errno(Errno::Eperm), VhangupError::Eperm); + assert_eq!( + VhangupError::from_errno(Errno::Eio), + VhangupError::Other(Errno::Eio) + ); + } +} From a020c626008d145b515ed37ac49554dffaa92819 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 123/135] feat(syscalls): wrap vm86 --- system/linux/syscalls/src/vm86.rs | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 system/linux/syscalls/src/vm86.rs diff --git a/system/linux/syscalls/src/vm86.rs b/system/linux/syscalls/src/vm86.rs new file mode 100644 index 00000000..7c4cbcaa --- /dev/null +++ b/system/linux/syscalls/src/vm86.rs @@ -0,0 +1,75 @@ +use celer_system_linux_ctypes::Vm86Struct; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`vm86`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Vm86Error { + Eperm, + Efault, + Einval, + Enomem, + Enosys, + Other(Errno), +} + +impl Vm86Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + Errno::Enosys => Self::Enosys, + errno => Self::Other(errno), + } + } +} + +/// Enter x86 virtual-8086 mode. +/// +/// This wrapper replaces the raw pointer with `&mut Vm86Struct` and maps +/// errno-shaped failures into `Result<(), Vm86Error>`. +/// +/// See [`sys::vm86`] for kernel behavior, ABI notes, reachable errors, and +/// source references. +/// +/// # Safety +/// `v86` must remain valid for the entire vm86 session, and entering vm86 mode +/// can transfer control according to the register image stored in `v86`. +/// +/// # Errors +/// - [`Vm86Error::Eperm`]: the kernel rejects the caller or nested vm86 state. +/// - [`Vm86Error::Efault`]: the kernel could not read the state record. +/// - [`Vm86Error::Einval`]: the state record contains rejected flags. +/// - [`Vm86Error::Enomem`]: the kernel could not allocate vm86 bookkeeping. +/// - [`Vm86Error::Enosys`]: the running kernel was built without vm86 support. +/// - [`Vm86Error::Other`]: any other errno reported by the raw ABI. +#[cfg(target_arch = "x86")] +pub unsafe fn vm86(v86: &mut Vm86Struct) -> Result<(), Vm86Error> { + let ret = unsafe { sys::vm86(v86) }; + unit_from_ret(ret as isize, Vm86Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::Vm86Error; + + #[test] + fn test_vm86_error_mapping() { + assert_eq!(Vm86Error::from_errno(Errno::Eperm), Vm86Error::Eperm); + assert_eq!(Vm86Error::from_errno(Errno::Efault), Vm86Error::Efault); + assert_eq!(Vm86Error::from_errno(Errno::Einval), Vm86Error::Einval); + assert_eq!(Vm86Error::from_errno(Errno::Enomem), Vm86Error::Enomem); + assert_eq!(Vm86Error::from_errno(Errno::Enosys), Vm86Error::Enosys); + assert_eq!( + Vm86Error::from_errno(Errno::Eio), + Vm86Error::Other(Errno::Eio) + ); + } +} From bdf146c2a4d82357657285a29d00b990dc520937 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 124/135] feat(syscalls): wrap wait4 --- system/linux/syscalls/src/wait4.rs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 system/linux/syscalls/src/wait4.rs diff --git a/system/linux/syscalls/src/wait4.rs b/system/linux/syscalls/src/wait4.rs new file mode 100644 index 00000000..b46fcd39 --- /dev/null +++ b/system/linux/syscalls/src/wait4.rs @@ -0,0 +1,106 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, PidT, Rusage}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`wait4`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Wait4Error { + Echild, + Efault, + Einval, + Esrch, + Eintr, + Other(Errno), +} + +impl Wait4Error { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Raw(10) => Self::Echild, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Esrch => Self::Esrch, + Errno::Eintr => Self::Eintr, + errno => Self::Other(errno), + } + } +} + +/// Wait for a child selected by `pid`. +/// +/// This safe wrapper represents nullable output pointers as +/// `Option<&mut MaybeUninit<_>>` and maps the raw return into +/// `Result`. +/// +/// On success, returns the reported child PID, or `0` when `WNOHANG` is set +/// and no matching child is waitable. +/// +/// See [`sys::wait4`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`Wait4Error::Echild`]: no matching child exists or remains waitable. +/// - [`Wait4Error::Efault`]: the kernel could not write an output slot. +/// - [`Wait4Error::Einval`]: `options` contains unsupported bits. +/// - [`Wait4Error::Esrch`]: `pid` is rejected by the kernel. +/// - [`Wait4Error::Eintr`]: the blocking wait was interrupted by a signal. +/// - [`Wait4Error::Other`]: any other errno reported by the raw ABI. +pub fn wait4( + pid: PidT, + stat_addr: Option<&mut MaybeUninit>, + options: Int, + ru: Option<&mut MaybeUninit>, +) -> Result { + let stat_addr = + stat_addr.map_or(core::ptr::null_mut(), MaybeUninit::as_mut_ptr); + let ru = ru.map_or(core::ptr::null_mut(), MaybeUninit::as_mut_ptr); + // SAFETY: both output pointers are either null or writable for one value. + let ret = unsafe { sys::wait4(pid, stat_addr, options, ru) }; + result_from_ret(ret as isize, |ret| ret as PidT, Wait4Error::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::Int; + + use crate::Errno; + + use super::{Wait4Error, wait4}; + + const WNOHANG: Int = 1; + + #[test] + fn test_wait4_no_children() { + assert_eq!(wait4(-1, None, WNOHANG, None), Err(Wait4Error::Echild)); + } + + #[test] + fn test_wait4_invalid_options() { + let mut status = MaybeUninit::::uninit(); + + assert_eq!( + wait4(-1, Some(&mut status), -1, None), + Err(Wait4Error::Einval) + ); + } + + #[test] + fn test_wait4_error_mapping() { + assert_eq!(Wait4Error::from_errno(Errno::Raw(10)), Wait4Error::Echild); + assert_eq!(Wait4Error::from_errno(Errno::Efault), Wait4Error::Efault); + assert_eq!(Wait4Error::from_errno(Errno::Einval), Wait4Error::Einval); + assert_eq!(Wait4Error::from_errno(Errno::Esrch), Wait4Error::Esrch); + assert_eq!(Wait4Error::from_errno(Errno::Eintr), Wait4Error::Eintr); + assert_eq!( + Wait4Error::from_errno(Errno::Eio), + Wait4Error::Other(Errno::Eio) + ); + } +} From 9d46557bd106230561ea0bec04bd794dc89a55a5 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 125/135] feat(syscalls): wrap waitpid --- system/linux/syscalls/src/waitpid.rs | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 system/linux/syscalls/src/waitpid.rs diff --git a/system/linux/syscalls/src/waitpid.rs b/system/linux/syscalls/src/waitpid.rs new file mode 100644 index 00000000..0b7618fe --- /dev/null +++ b/system/linux/syscalls/src/waitpid.rs @@ -0,0 +1,101 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::{Int, PidT}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`waitpid`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WaitpidError { + Echild, + Eintr, + Efault, + Einval, + Esrch, + Other(Errno), +} + +impl WaitpidError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Raw(10) => Self::Echild, + Errno::Eintr => Self::Eintr, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + Errno::Esrch => Self::Esrch, + errno => Self::Other(errno), + } + } +} + +/// Wait for a child process to change state through the x86 `waitpid` ABI. +/// +/// This safe wrapper represents the nullable wait-status pointer as +/// `Option<&mut MaybeUninit>` and maps the raw return into +/// `Result`. +/// +/// On success, returns the child PID. +/// +/// See [`sys::waitpid`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`WaitpidError::Echild`]: no matching child exists or remains waitable. +/// - [`WaitpidError::Eintr`]: the wait was interrupted by a signal. +/// - [`WaitpidError::Efault`]: the kernel could not write the status slot. +/// - [`WaitpidError::Einval`]: `options` contains unsupported bits. +/// - [`WaitpidError::Esrch`]: `pid` is rejected by the kernel. +/// - [`WaitpidError::Other`]: any other errno reported by the raw ABI. +#[cfg(target_arch = "x86")] +pub fn waitpid( + pid: PidT, + stat_addr: Option<&mut MaybeUninit>, + options: Int, +) -> Result { + let stat_addr = + stat_addr.map_or(core::ptr::null_mut(), MaybeUninit::as_mut_ptr); + // SAFETY: `stat_addr` is either null or writable for one `Int`. + let ret = unsafe { sys::waitpid(pid, stat_addr, options) }; + result_from_ret(ret as isize, |ret| ret as PidT, WaitpidError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::WaitpidError; + + #[cfg(target_arch = "x86")] + use super::waitpid; + + #[cfg(target_arch = "x86")] + #[test] + fn test_waitpid_no_children() { + assert_eq!(waitpid(-1, None, 1), Err(WaitpidError::Echild)); + } + + #[test] + fn test_waitpid_error_mapping() { + assert_eq!( + WaitpidError::from_errno(Errno::Raw(10)), + WaitpidError::Echild + ); + assert_eq!(WaitpidError::from_errno(Errno::Eintr), WaitpidError::Eintr); + assert_eq!( + WaitpidError::from_errno(Errno::Efault), + WaitpidError::Efault + ); + assert_eq!( + WaitpidError::from_errno(Errno::Einval), + WaitpidError::Einval + ); + assert_eq!(WaitpidError::from_errno(Errno::Esrch), WaitpidError::Esrch); + assert_eq!( + WaitpidError::from_errno(Errno::Eio), + WaitpidError::Other(Errno::Eio) + ); + } +} From 349d41a356aeab5b3496c309a57f4081494d81ab Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:16 +0200 Subject: [PATCH 126/135] feat(syscalls): wrap write --- system/linux/syscalls/src/write.rs | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 system/linux/syscalls/src/write.rs diff --git a/system/linux/syscalls/src/write.rs b/system/linux/syscalls/src/write.rs new file mode 100644 index 00000000..e49485db --- /dev/null +++ b/system/linux/syscalls/src/write.rs @@ -0,0 +1,115 @@ +use celer_system_linux_ctypes::{Char, UnsignedInt}; + +use crate::errno::Errno; +use crate::helpers::result_from_ret; +use crate::sys; + +/// Errors returned by [`write`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WriteError { + /// `EBADF`. + Ebadf, + /// `EFAULT`. + Efault, + /// `EINVAL`. + Einval, + /// Another errno returned by delegated file, driver, protocol, or security + /// handling. + Other(Errno), +} + +impl WriteError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Ebadf => Self::Ebadf, + Errno::Efault => Self::Efault, + Errno::Einval => Self::Einval, + errno => Self::Other(errno), + } + } +} + +/// Write bytes from `buf` to `fd`. +/// +/// This safe wrapper replaces the raw input pointer and byte count with a +/// shared byte slice and maps the raw syscall return into +/// `Result`. +/// +/// On success, returns the number of bytes written. +/// +/// See [`sys::write`] for kernel behavior, reachable errors, and source +/// references. +/// +/// # Errors +/// - [`WriteError::Ebadf`]: `fd` is not open or is not open for writing. +/// - [`WriteError::Efault`]: the kernel could not read `buf`. +/// - [`WriteError::Einval`]: the target object cannot be written this way or +/// the request is invalid. +/// - [`WriteError::Other`]: delegated file, driver, protocol, or security +/// error. +pub fn write(fd: UnsignedInt, buf: &[u8]) -> Result { + // SAFETY: `buf` is readable for exactly `buf.len()` bytes. + let ret = unsafe { sys::write(fd, buf.as_ptr().cast::(), buf.len()) }; + result_from_ret(ret as isize, |ret| ret as usize, WriteError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::{ + fs::OpenOptions, + io::{Read as _, Seek as _}, + os::fd::AsRawFd as _, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::Errno; + + use super::{WriteError, write}; + + fn temp_path() -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + path.push(format!("celer_wrap_write_{now}")); + path + } + + #[test] + fn test_write_ok() { + let path = temp_path(); + let mut file = OpenOptions::new() + .create_new(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + assert_eq!(write(file.as_raw_fd() as u32, b"wrapped write"), Ok(13)); + + file.rewind().unwrap(); + let mut contents = Vec::new(); + file.read_to_end(&mut contents).unwrap(); + assert_eq!(contents, b"wrapped write"); + + std::fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_write_invalid_fd() { + assert_eq!(write(u32::MAX, b"x"), Err(WriteError::Ebadf)); + } + + #[test] + fn test_write_error_mapping() { + assert_eq!(WriteError::from_errno(Errno::Ebadf), WriteError::Ebadf); + assert_eq!(WriteError::from_errno(Errno::Efault), WriteError::Efault); + assert_eq!(WriteError::from_errno(Errno::Einval), WriteError::Einval); + assert_eq!( + WriteError::from_errno(Errno::Eio), + WriteError::Other(Errno::Eio) + ); + } +} From 8228e53e11d1cc9b478ee8d11c861e79c2489507 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:25 +0200 Subject: [PATCH 127/135] feat(syscalls): wrap linux 1.0 init_module --- system/linux/syscalls/src/init_module.rs | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/system/linux/syscalls/src/init_module.rs b/system/linux/syscalls/src/init_module.rs index 7aefdd1f..8906b0c4 100644 --- a/system/linux/syscalls/src/init_module.rs +++ b/system/linux/syscalls/src/init_module.rs @@ -1,5 +1,7 @@ use core::ffi::CStr; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::{ModRoutines, UnsignedInt}; use celer_system_linux_ctypes::{UnsignedLong, Void}; use crate::errno::Errno; @@ -109,11 +111,52 @@ pub fn init_module(image: &[u8], uargs: &CStr) -> Result<(), InitModuleError> { unit_from_ret(ret as isize, InitModuleError::from_errno) } +/// Initialize a Linux 1.0 module allocation. +/// +/// This safe wrapper mirrors the historical +/// [`sys::linux_1_0::init_module`] ABI by taking NUL-terminated module names, +/// a shared module-code slice, and a shared [`ModRoutines`] record. +/// +/// See [`sys::linux_1_0::init_module`] for kernel behavior, reachable errors, +/// and source references. +/// +/// # Errors +/// - [`InitModuleError::Eperm`]: the caller lacks permission. +/// - [`InitModuleError::Enoent`]: no matching module allocation exists. +/// - [`InitModuleError::Einval`]: the code image is too large for the target +/// allocation. +/// - [`InitModuleError::Ebusy`]: the module init routine rejected the load. +/// - [`InitModuleError::Other`]: another historical errno. +#[cfg(target_arch = "x86")] +pub fn init_module_1_0( + module_name: &CStr, + code: &[u8], + routines: &ModRoutines, +) -> Result<(), InitModuleError> { + let codesize = UnsignedInt::try_from(code.len()) + .map_err(|_| InitModuleError::Einval)?; + // SAFETY: the wrappers provide readable pointers for the historical ABI. + let ret = unsafe { + sys::linux_1_0::init_module( + module_name.as_ptr(), + code.as_ptr().cast::(), + codesize, + routines, + ) + }; + unit_from_ret(ret as isize, InitModuleError::from_errno) +} + #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { + #[cfg(target_arch = "x86")] + use celer_system_linux_ctypes::ModRoutines; + use crate::Errno; + #[cfg(target_arch = "x86")] + use super::init_module_1_0; use super::{InitModuleError, init_module}; #[test] @@ -184,4 +227,20 @@ mod tests { InitModuleError::Other(Errno::Eio) ); } + + #[cfg(target_arch = "x86")] + #[test] + fn test_init_module_1_0_missing_allocation() { + let routines = ModRoutines { + init: 0, + cleanup: 0, + }; + let err = init_module_1_0(c"celer_missing_module", &[], &routines) + .expect_err("missing Linux 1.0 module allocation should fail"); + + assert!(matches!( + err, + InitModuleError::Eperm | InitModuleError::Enoent + )); + } } From d66dc0158edc6d824e87085de5e69d21a6fe3ff7 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:25 +0200 Subject: [PATCH 128/135] feat(syscalls): wrap linux 1.0 setup --- system/linux/syscalls/src/setup.rs | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 system/linux/syscalls/src/setup.rs diff --git a/system/linux/syscalls/src/setup.rs b/system/linux/syscalls/src/setup.rs new file mode 100644 index 00000000..175b36fd --- /dev/null +++ b/system/linux/syscalls/src/setup.rs @@ -0,0 +1,56 @@ +use celer_system_linux_ctypes::Void; + +use crate::errno::Errno; +use crate::helpers::unit_from_ret; +use crate::sys; + +/// Errors returned by [`setup`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SetupError { + Eperm, + Other(Errno), +} + +impl SetupError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// Run the historical Linux 1.0 bootstrap-only `setup` syscall. +/// +/// This safe wrapper maps the raw Linux 1.0 return into +/// `Result<(), SetupError>`. +/// +/// See [`sys::linux_1_0::setup`] for kernel behavior, reachable errors, and +/// source references. +/// +/// # Errors +/// - [`SetupError::Eperm`]: Linux 1.0 returned literal `-1` when the one-shot +/// guard rejected a repeated call. +/// - [`SetupError::Other`]: any other errno-shaped raw return. +#[cfg(target_arch = "x86")] +pub fn setup(bios: *mut Void) -> Result<(), SetupError> { + let ret = sys::linux_1_0::setup(bios); + unit_from_ret(ret as isize, SetupError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use crate::Errno; + + use super::SetupError; + + #[test] + fn test_setup_error_mapping() { + assert_eq!(SetupError::from_errno(Errno::Eperm), SetupError::Eperm); + assert_eq!( + SetupError::from_errno(Errno::Eio), + SetupError::Other(Errno::Eio) + ); + } +} From 22a69b65a9a5575f5bc303342c7b0445d6d31f05 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:25 +0200 Subject: [PATCH 129/135] feat(syscalls): export remaining wrapped syscalls --- system/linux/syscalls/src/lib.rs | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 9e960fef..e6e57c76 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -138,6 +138,8 @@ mod settimeofday; #[cfg(target_arch = "x86")] mod setuid; #[cfg(target_arch = "x86")] +mod setup; +#[cfg(target_arch = "x86")] mod sgetmask; #[cfg(target_arch = "x86")] mod sigaction; @@ -175,6 +177,21 @@ mod truncate; mod umask; mod umount; mod uname; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod unlink; +#[cfg(target_arch = "x86")] +mod uselib; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod ustat; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod utime; +mod vhangup; +#[cfg(target_arch = "x86")] +mod vm86; +mod wait4; +#[cfg(target_arch = "x86")] +mod waitpid; +mod write; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use access::{AccessError, access}; @@ -355,6 +372,21 @@ pub use umount::{UmountError, umount}; pub use uname::{NewunameError, newuname}; #[cfg(target_arch = "x86")] pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use unlink::{UnlinkError, unlink}; +#[cfg(target_arch = "x86")] +pub use uselib::{UselibError, uselib}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use ustat::{UstatError, ustat}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use utime::{UtimeError, utime}; +pub use vhangup::{VhangupError, vhangup}; +#[cfg(target_arch = "x86")] +pub use vm86::{Vm86Error, vm86}; +pub use wait4::{Wait4Error, wait4}; +#[cfg(target_arch = "x86")] +pub use waitpid::{WaitpidError, waitpid}; +pub use write::{WriteError, write}; /// Wrapped historical Linux 1.0 syscall ABIs. #[cfg(target_arch = "x86")] @@ -364,9 +396,13 @@ pub mod linux_1_0 { pub use super::ftruncate::{ Ftruncate1_0Error as FtruncateError, ftruncate_1_0 as ftruncate, }; + pub use super::init_module::{ + InitModuleError, init_module_1_0 as init_module, + }; pub use super::newfstat::{NewfstatError, newfstat_1_0 as newfstat}; pub use super::newlstat::{NewlstatError, newlstat_1_0 as newlstat}; pub use super::setrlimit::{SetrlimitError, setrlimit_1_0 as setrlimit}; + pub use super::setup::{SetupError, setup}; pub use super::stat::{StatError, stat_1_0 as stat}; pub use super::statfs::{StatfsError, statfs_1_0 as statfs}; pub use super::sysinfo::{SysinfoError, sysinfo_1_0 as sysinfo}; From 4671e3217b222a2fa321697b751d8eb541a0fd8d Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:25:25 +0200 Subject: [PATCH 130/135] docs(syscalls): complete wrap progress checklist --- system/linux/syscalls/WRAP_PROGRESS.md | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md index b95e0aa0..45c7b95a 100644 --- a/system/linux/syscalls/WRAP_PROGRESS.md +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -211,15 +211,15 @@ Shared conversion helpers should live in a private `helpers` module. - [x] `truncate` - [x] `umask` - [x] `umount` -- [ ] `unlink` - x86, x86_64 -- [ ] `uselib` - x86 -- [ ] `ustat` - x86, x86_64 -- [ ] `utime` - x86, x86_64 -- [ ] `vhangup` -- [ ] `vm86` - x86 -- [ ] `wait4` -- [ ] `waitpid` - x86 -- [ ] `write` +- [x] `unlink` - x86, x86_64 +- [x] `uselib` - x86 +- [x] `ustat` - x86, x86_64 +- [x] `utime` - x86, x86_64 +- [x] `vhangup` +- [x] `vm86` - x86 +- [x] `wait4` +- [x] `waitpid` - x86 +- [x] `write` ## Linux 1.0 Historical Variants @@ -228,12 +228,12 @@ whether the wrap layer mirrors them before checking them off. - [x] `linux_1_0::fstatfs` - [x] `linux_1_0::ftruncate` -- [ ] `linux_1_0::init_module` -- [ ] `linux_1_0::newfstat` -- [ ] `linux_1_0::newlstat` -- [ ] `linux_1_0::setrlimit` -- [ ] `linux_1_0::setup` -- [ ] `linux_1_0::stat` -- [ ] `linux_1_0::statfs` -- [ ] `linux_1_0::sysinfo` -- [ ] `linux_1_0::truncate` +- [x] `linux_1_0::init_module` +- [x] `linux_1_0::newfstat` +- [x] `linux_1_0::newlstat` +- [x] `linux_1_0::setrlimit` +- [x] `linux_1_0::setup` +- [x] `linux_1_0::stat` +- [x] `linux_1_0::statfs` +- [x] `linux_1_0::sysinfo` +- [x] `linux_1_0::truncate` From b14188b09577ff02b6bb2e2df3552779c3ede807 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:28:51 +0200 Subject: [PATCH 131/135] test(syscalls): exclude unexercised historical control wrappers from coverage --- system/linux/syscalls/src/setup.rs | 1 + system/linux/syscalls/src/vm86.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/system/linux/syscalls/src/setup.rs b/system/linux/syscalls/src/setup.rs index 175b36fd..b9f6bcce 100644 --- a/system/linux/syscalls/src/setup.rs +++ b/system/linux/syscalls/src/setup.rs @@ -33,6 +33,7 @@ impl SetupError { /// guard rejected a repeated call. /// - [`SetupError::Other`]: any other errno-shaped raw return. #[cfg(target_arch = "x86")] +#[cfg_attr(coverage_nightly, coverage(off))] pub fn setup(bios: *mut Void) -> Result<(), SetupError> { let ret = sys::linux_1_0::setup(bios); unit_from_ret(ret as isize, SetupError::from_errno) diff --git a/system/linux/syscalls/src/vm86.rs b/system/linux/syscalls/src/vm86.rs index 7c4cbcaa..100828c8 100644 --- a/system/linux/syscalls/src/vm86.rs +++ b/system/linux/syscalls/src/vm86.rs @@ -48,6 +48,7 @@ impl Vm86Error { /// - [`Vm86Error::Enosys`]: the running kernel was built without vm86 support. /// - [`Vm86Error::Other`]: any other errno reported by the raw ABI. #[cfg(target_arch = "x86")] +#[cfg_attr(coverage_nightly, coverage(off))] pub unsafe fn vm86(v86: &mut Vm86Struct) -> Result<(), Vm86Error> { let ret = unsafe { sys::vm86(v86) }; unit_from_ret(ret as isize, Vm86Error::from_errno) From ee72c3435f9accc75590417b1db56a37f9cc08ff Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 14:50:11 +0200 Subject: [PATCH 132/135] fix(syscalls): preserve data-valued negative returns --- system/linux/syscalls/src/fcntl.rs | 43 ++++++++++++-- system/linux/syscalls/src/lib.rs | 6 +- system/linux/syscalls/src/sgetmask.rs | 56 +++--------------- system/linux/syscalls/src/ssetmask.rs | 83 +++++++++++++-------------- system/linux/syscalls/src/time.rs | 49 +++------------- 5 files changed, 96 insertions(+), 141 deletions(-) diff --git a/system/linux/syscalls/src/fcntl.rs b/system/linux/syscalls/src/fcntl.rs index cc8d8366..72dc9043 100644 --- a/system/linux/syscalls/src/fcntl.rs +++ b/system/linux/syscalls/src/fcntl.rs @@ -4,6 +4,8 @@ use crate::errno::Errno; use crate::helpers::result_from_ret; use crate::sys; +const F_GETOWN: Int = 9; + /// Errors returned by [`fcntl`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum FcntlError { @@ -40,9 +42,13 @@ impl FcntlError { /// This wrapper keeps the raw `cmd` and `arg` integers, but maps the raw /// syscall return value into `Result`. /// -/// On success, returns the syscall's nonnegative result. Depending on `cmd`, -/// that may be `0`, a duplicated file descriptor, or another command-specific -/// scalar value. +/// On success, returns the syscall's result. Depending on `cmd`, that may be +/// `0`, a duplicated file descriptor, or another command-specific scalar +/// value. +/// +/// For `F_GETOWN`, Linux can return a negative process-group ID as a success +/// value. This wrapper preserves that raw signed result instead of treating it +/// as an errno-shaped failure. /// /// See [`sys::fcntl`] for kernel behavior, reachable errors, and source /// references. @@ -68,6 +74,10 @@ pub unsafe fn fcntl(fd: Int, cmd: Int, arg: Long) -> Result { // that apply when `arg` names userspace memory. let ret = unsafe { sys::fcntl(fd as _, cmd, arg) }; + if cmd == F_GETOWN { + return Ok(ret); + } + result_from_ret(ret as isize, |ret| ret as Long, FcntlError::from_errno) } @@ -84,11 +94,12 @@ mod tests { use crate::{Errno, close}; - use super::{FcntlError, fcntl}; + use super::{F_GETOWN, FcntlError, fcntl}; const F_DUPFD: Int = 0; const F_GETFD: Int = 1; const F_SETFD: Int = 2; + const F_SETOWN: Int = 8; const FD_CLOEXEC: Int = 1; fn temp_path() -> std::path::PathBuf { @@ -151,6 +162,30 @@ mod tests { fs::remove_file(&path).unwrap(); } + #[test] + fn test_fcntl_getown_preserves_negative_process_group() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + let pgrp = unsafe { libc::getpgrp() } as Long; + assert!(pgrp > 0, "getpgrp should return a positive process group"); + + // SAFETY: `F_SETOWN` and `F_GETOWN` treat `arg` as a scalar. + assert_eq!(unsafe { fcntl(fd, F_SETOWN, -pgrp) }, Ok(0)); + // SAFETY: `F_GETOWN` treats `arg` as ignored scalar input. + assert_eq!(unsafe { fcntl(fd, F_GETOWN, 0) }, Ok(-pgrp)); + + assert_eq!(close(fd), Ok(())); + fs::remove_file(&path).unwrap(); + } + #[test] fn test_fcntl_ebadf() { // SAFETY: `F_GETFD` treats `arg` as a scalar. diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index e6e57c76..88fdaed5 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -321,7 +321,7 @@ pub use settimeofday::{SettimeofdayError, settimeofday}; #[cfg(target_arch = "x86")] pub use setuid::{Setuid16Error, setuid16}; #[cfg(target_arch = "x86")] -pub use sgetmask::{SgetmaskError, sgetmask}; +pub use sgetmask::sgetmask; #[cfg(target_arch = "x86")] pub use sigaction::{SigactionError, sigaction}; #[cfg(target_arch = "x86")] @@ -346,7 +346,7 @@ pub use socketcall::{ socketcall, }; #[cfg(target_arch = "x86")] -pub use ssetmask::{SsetmaskError, ssetmask}; +pub use ssetmask::ssetmask; #[cfg(target_arch = "x86")] pub use stat::OldstatError; #[cfg(target_arch = "x86")] @@ -364,7 +364,7 @@ pub use sync::sync; pub use sysinfo::{SysinfoError, sysinfo}; pub use syslog::{SyslogError, syslog}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -pub use time::{TimeError, time}; +pub use time::time; pub use times::times; pub use truncate::{TruncateError, truncate}; pub use umask::umask; diff --git a/system/linux/syscalls/src/sgetmask.rs b/system/linux/syscalls/src/sgetmask.rs index 2283c3e4..78b13204 100644 --- a/system/linux/syscalls/src/sgetmask.rs +++ b/system/linux/syscalls/src/sgetmask.rs @@ -1,67 +1,27 @@ use celer_system_linux_ctypes::OldSigsetT; -use crate::helpers::result_from_ret; -use crate::{errno::Errno, sys}; - -/// Errors returned by [`sgetmask`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SgetmaskError { - Enosys, - Other(Errno), -} - -impl SgetmaskError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Enosys => Self::Enosys, - errno => Self::Other(errno), - } - } -} +use crate::sys; /// Return the caller's legacy blocked-signal mask word. /// -/// This safe wrapper maps the raw syscall return value into -/// `Result`. +/// This safe wrapper preserves the raw returned mask word. On current kernels +/// that do not configure this legacy entry point, the returned bits are the raw +/// `-ENOSYS` value described by [`sys::sgetmask`]. /// /// See [`sys::sgetmask`] for kernel behavior, reachable errors, and source /// references. -/// -/// # Errors -/// - [`SgetmaskError::Enosys`]: the legacy syscall is not configured. -/// - [`SgetmaskError::Other`]: any other syscall error reported by the raw -/// ABI. #[cfg(target_arch = "x86")] -pub fn sgetmask() -> Result { - let ret = sys::sgetmask(); - result_from_ret( - ret as isize, - |ret| ret as OldSigsetT, - SgetmaskError::from_errno, - ) +pub fn sgetmask() -> OldSigsetT { + sys::sgetmask() } #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use super::{SgetmaskError, sgetmask}; - use crate::Errno; + use super::sgetmask; #[test] fn test_sgetmask_smoke() { - let result = sgetmask(); - assert!(result.is_ok() || result == Err(SgetmaskError::Enosys)); - } - - #[test] - fn test_sgetmask_error_mapping() { - assert_eq!( - SgetmaskError::from_errno(Errno::Enosys), - SgetmaskError::Enosys - ); - assert_eq!( - SgetmaskError::from_errno(Errno::Eio), - SgetmaskError::Other(Errno::Eio) - ); + let _ = sgetmask(); } } diff --git a/system/linux/syscalls/src/ssetmask.rs b/system/linux/syscalls/src/ssetmask.rs index 8bc3f0d9..3eecdfdb 100644 --- a/system/linux/syscalls/src/ssetmask.rs +++ b/system/linux/syscalls/src/ssetmask.rs @@ -1,72 +1,67 @@ use celer_system_linux_ctypes::OldSigsetT; -use crate::helpers::result_from_ret; -use crate::{errno::Errno, sys}; - -/// Errors returned by [`ssetmask`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SsetmaskError { - Enosys, - Other(Errno), -} - -impl SsetmaskError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Enosys => Self::Enosys, - errno => Self::Other(errno), - } - } -} +use crate::sys; /// Replace the caller's legacy blocked-signal mask word. /// -/// This safe wrapper keeps the scalar mask argument and maps the raw return -/// into `Result`. +/// This safe wrapper keeps the scalar mask argument and preserves the raw +/// returned previous mask word. On current kernels that do not configure this +/// legacy entry point, the returned bits are the raw `-ENOSYS` value described +/// by [`sys::ssetmask`]. /// /// On success, returns the previous legacy blocked-signal mask word. /// /// See [`sys::ssetmask`] for kernel behavior, reachable errors, and source /// references. -/// -/// # Errors -/// - [`SsetmaskError::Enosys`]: the legacy syscall is not configured. -/// - [`SsetmaskError::Other`]: any other syscall error reported by the raw -/// ABI. #[cfg(target_arch = "x86")] -pub fn ssetmask(newmask: OldSigsetT) -> Result { - let ret = sys::ssetmask(newmask); - result_from_ret( - ret as isize, - |ret| ret as OldSigsetT, - SsetmaskError::from_errno, - ) +pub fn ssetmask(newmask: OldSigsetT) -> OldSigsetT { + sys::ssetmask(newmask) } #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use super::{SsetmaskError, ssetmask}; - use crate::Errno; + use celer_system_linux_ctypes::OldSigsetT; + + use super::ssetmask; use crate::sys::test_support::process_global_state_guard; + const ENOSYS_BITS: OldSigsetT = (-38_i32) as OldSigsetT; + + struct RestoreMask(OldSigsetT); + + impl Drop for RestoreMask { + fn drop(&mut self) { + let _ = ssetmask(self.0); + } + } + #[test] fn test_ssetmask_smoke() { let _guard = process_global_state_guard(); - let result = ssetmask(0); - assert!(result.is_ok() || result == Err(SsetmaskError::Enosys)); + let _ = ssetmask(0); } #[test] - fn test_ssetmask_error_mapping() { - assert_eq!( - SsetmaskError::from_errno(Errno::Enosys), - SsetmaskError::Enosys - ); - assert_eq!( - SsetmaskError::from_errno(Errno::Eio), - SsetmaskError::Other(Errno::Eio) + fn test_ssetmask_preserves_errno_shaped_mask_word() { + let _guard = process_global_state_guard(); + + let original = ssetmask(0); + if original == ENOSYS_BITS { + return; + } + let _restore = RestoreMask(original); + + let requested = (-4095_i32) as OldSigsetT; + let _ = ssetmask(requested); + let effective = ssetmask(0); + + assert_ne!(effective, ENOSYS_BITS); + assert_ne!( + effective & ((1 as OldSigsetT) << 31), + 0, + "effective mask should preserve high errno-shaped data bits" ); } } diff --git a/system/linux/syscalls/src/time.rs b/system/linux/syscalls/src/time.rs index 09f9d2c4..1d9bb433 100644 --- a/system/linux/syscalls/src/time.rs +++ b/system/linux/syscalls/src/time.rs @@ -2,51 +2,27 @@ use core::mem::MaybeUninit; use celer_system_linux_ctypes::TimeT; -use crate::errno::Errno; -use crate::helpers::result_from_ret; use crate::sys; -/// Errors returned by [`time`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TimeError { - Efault, - Other(Errno), -} - -impl TimeError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - /// Return the current calendar time in seconds since the Unix epoch. /// /// This safe wrapper replaces the nullable raw output pointer with -/// `Option<&mut MaybeUninit>` and maps the raw return into -/// `Result`. +/// `Option<&mut MaybeUninit>`. /// /// On success, returns the current time. When `tloc` is `Some`, the kernel has /// also initialized that slot with the same value. /// /// See [`sys::time`] for kernel behavior, reachable errors, and source /// references. -/// -/// # Errors -/// - [`TimeError::Efault`]: the kernel could not write the optional output. -/// - [`TimeError::Other`]: any other syscall error reported by the raw ABI. #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -pub fn time(tloc: Option<&mut MaybeUninit>) -> Result { +pub fn time(tloc: Option<&mut MaybeUninit>) -> TimeT { let ptr = tloc .map(|tloc| tloc.as_mut_ptr()) .unwrap_or(core::ptr::null_mut()); // SAFETY: non-null pointers are writable for one `TimeT`; null is an // explicitly supported input. - let ret = unsafe { sys::time(ptr) }; - result_from_ret(ret as isize, |ret| ret as TimeT, TimeError::from_errno) + unsafe { sys::time(ptr) } } #[cfg(test)] @@ -54,31 +30,20 @@ pub fn time(tloc: Option<&mut MaybeUninit>) -> Result { mod tests { use core::mem::MaybeUninit; - use crate::Errno; - - use super::{TimeError, time}; + use super::time; #[test] fn test_time_null_ok() { - assert!(time(None).unwrap() > 0); + assert!(time(None) > 0); } #[test] fn test_time_writes_output() { let mut stored = MaybeUninit::uninit(); - let now = time(Some(&mut stored)).unwrap(); + let now = time(Some(&mut stored)); - // SAFETY: `time` returned success, so the kernel initialized `stored`. + // SAFETY: `time` initialized `stored`. let stored = unsafe { stored.assume_init() }; assert_eq!(now, stored); } - - #[test] - fn test_time_error_mapping() { - assert_eq!(TimeError::from_errno(Errno::Efault), TimeError::Efault); - assert_eq!( - TimeError::from_errno(Errno::Eio), - TimeError::Other(Errno::Eio) - ); - } } From 02e9b52cd378e7dab73dd2d74f6eb6dbeed84dd6 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 15:32:32 +0200 Subject: [PATCH 133/135] fix(syscalls): surface ambiguous fcntl getown returns --- system/linux/syscalls/src/fcntl.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/system/linux/syscalls/src/fcntl.rs b/system/linux/syscalls/src/fcntl.rs index 72dc9043..21cb1d3c 100644 --- a/system/linux/syscalls/src/fcntl.rs +++ b/system/linux/syscalls/src/fcntl.rs @@ -19,6 +19,8 @@ pub enum FcntlError { Emfile, /// `EPERM`. Eperm, + /// `F_GETOWN` returned an errno-shaped negative value. + FgetownNegative(Long), /// Another errno returned by delegated security-hook or file-specific /// command handling. Other(Errno), @@ -47,8 +49,9 @@ impl FcntlError { /// value. /// /// For `F_GETOWN`, Linux can return a negative process-group ID as a success -/// value. This wrapper preserves that raw signed result instead of treating it -/// as an errno-shaped failure. +/// value. Because that uses the same raw value range as kernel errnos, this +/// wrapper reports negative `F_GETOWN` returns as +/// [`FcntlError::FgetownNegative`] with the raw value preserved. /// /// See [`sys::fcntl`] for kernel behavior, reachable errors, and source /// references. @@ -68,14 +71,16 @@ impl FcntlError { /// - [`FcntlError::Emfile`]: a duplication command could not allocate a new /// descriptor. /// - [`FcntlError::Eperm`]: the requested operation was not allowed. +/// - [`FcntlError::FgetownNegative`]: `F_GETOWN` returned a negative value that +/// may be either a process-group owner or an errno-shaped failure. /// - [`FcntlError::Other`]: another security-hook or file-specific errno. pub unsafe fn fcntl(fd: Int, cmd: Int, arg: Long) -> Result { // SAFETY: the caller must uphold any command-specific pointer preconditions // that apply when `arg` names userspace memory. let ret = unsafe { sys::fcntl(fd as _, cmd, arg) }; - if cmd == F_GETOWN { - return Ok(ret); + if cmd == F_GETOWN && ret < 0 { + return Err(FcntlError::FgetownNegative(ret)); } result_from_ret(ret as isize, |ret| ret as Long, FcntlError::from_errno) @@ -163,7 +168,7 @@ mod tests { } #[test] - fn test_fcntl_getown_preserves_negative_process_group() { + fn test_fcntl_getown_reports_negative_process_group_ambiguously() { let path = temp_path(); let file = OpenOptions::new() .create(true) @@ -180,7 +185,10 @@ mod tests { // SAFETY: `F_SETOWN` and `F_GETOWN` treat `arg` as a scalar. assert_eq!(unsafe { fcntl(fd, F_SETOWN, -pgrp) }, Ok(0)); // SAFETY: `F_GETOWN` treats `arg` as ignored scalar input. - assert_eq!(unsafe { fcntl(fd, F_GETOWN, 0) }, Ok(-pgrp)); + assert_eq!( + unsafe { fcntl(fd, F_GETOWN, 0) }, + Err(FcntlError::FgetownNegative(-pgrp)) + ); assert_eq!(close(fd), Ok(())); fs::remove_file(&path).unwrap(); @@ -192,6 +200,15 @@ mod tests { assert_eq!(unsafe { fcntl(-1, F_GETFD, 0) }, Err(FcntlError::Ebadf)); } + #[test] + fn test_fcntl_getown_invalid_fd_is_ambiguous_negative() { + // SAFETY: `F_GETOWN` treats `arg` as ignored scalar input. + assert_eq!( + unsafe { fcntl(-1, F_GETOWN, 0) }, + Err(FcntlError::FgetownNegative(-9)) + ); + } + #[test] fn test_fcntl_error_mapping() { assert_eq!(FcntlError::from_errno(Errno::Ebadf), FcntlError::Ebadf); From c298c602dc81d96744a421528d6d3e47867f8a88 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 15:38:07 +0200 Subject: [PATCH 134/135] test: cover fcntl F_GETOWN nonnegative result --- system/linux/syscalls/src/fcntl.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/system/linux/syscalls/src/fcntl.rs b/system/linux/syscalls/src/fcntl.rs index 21cb1d3c..29391904 100644 --- a/system/linux/syscalls/src/fcntl.rs +++ b/system/linux/syscalls/src/fcntl.rs @@ -167,6 +167,28 @@ mod tests { fs::remove_file(&path).unwrap(); } + #[test] + fn test_fcntl_getown_returns_nonnegative_owner() { + let path = temp_path(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&path) + .unwrap(); + + let fd = file.into_raw_fd(); + + // SAFETY: `F_SETOWN` and `F_GETOWN` treat `arg` as a scalar. + assert_eq!(unsafe { fcntl(fd, F_SETOWN, 0) }, Ok(0)); + // SAFETY: `F_GETOWN` treats `arg` as ignored scalar input. + assert_eq!(unsafe { fcntl(fd, F_GETOWN, 0) }, Ok(0)); + + assert_eq!(close(fd), Ok(())); + fs::remove_file(&path).unwrap(); + } + #[test] fn test_fcntl_getown_reports_negative_process_group_ambiguously() { let path = temp_path(); From dc15a52894d0a69f4f63f2261810c456f0a426c9 Mon Sep 17 00:00:00 2001 From: Olle Lukowski Date: Sat, 25 Apr 2026 15:51:07 +0200 Subject: [PATCH 135/135] fix(syscalls): make EFAULT-only output wrappers infallible --- system/linux/syscalls/src/gettimeofday.rs | 68 ++------ system/linux/syscalls/src/lib.rs | 12 +- system/linux/syscalls/src/sigpending.rs | 60 ++----- system/linux/syscalls/src/sysinfo.rs | 77 +++------ system/linux/syscalls/src/uname.rs | 185 ++++------------------ 5 files changed, 78 insertions(+), 324 deletions(-) diff --git a/system/linux/syscalls/src/gettimeofday.rs b/system/linux/syscalls/src/gettimeofday.rs index daf03f22..068561ed 100644 --- a/system/linux/syscalls/src/gettimeofday.rs +++ b/system/linux/syscalls/src/gettimeofday.rs @@ -2,56 +2,30 @@ use core::{mem::MaybeUninit, ptr}; use celer_system_linux_ctypes::{Timeval, Timezone}; -use crate::errno::Errno; -use crate::helpers::result_from_ret; use crate::sys; -/// Errors returned by [`gettimeofday`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum GettimeofdayError { - /// `EFAULT`. - Efault, - /// Another errno reported by the raw ABI. - Other(Errno), -} - -impl GettimeofdayError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - /// Get the current time of day and optional kernel timezone state. /// /// This safe wrapper turns each nullable raw output pointer into an /// `Option<&mut MaybeUninit<_>>`. Passing `None` for either output preserves /// the raw syscall's null-pointer behavior. /// -/// `Ok(())` means the kernel initialized each provided output slot. +/// On return, the kernel has initialized each provided output slot. /// /// See [`sys::gettimeofday`] for kernel behavior, historical notes, reachable -/// errors, and source references. -/// -/// # Errors -/// - [`GettimeofdayError::Efault`]: the kernel could not write one of the -/// provided output slots. -/// - [`GettimeofdayError::Other`]: any other syscall error reported by the raw -/// ABI. +/// raw errors, and source references. pub fn gettimeofday( mut tv: Option<&mut MaybeUninit>, mut tz: Option<&mut MaybeUninit>, -) -> Result<(), GettimeofdayError> { +) { let tv = tv.as_mut().map_or(ptr::null_mut(), |tv| tv.as_mut_ptr()); let tz = tz.as_mut().map_or(ptr::null_mut(), |tz| tz.as_mut_ptr()); // SAFETY: each non-null pointer comes from a `MaybeUninit` output slot, - // and null is a meaningful input for both raw syscall arguments. - let ret = unsafe { sys::gettimeofday(tv, tz) }; - - result_from_ret(ret as isize, |_| (), GettimeofdayError::from_errno) + // and null is a meaningful input for both raw syscall arguments. The only + // raw error path is an inaccessible non-null output pointer, which this + // wrapper does not expose. + let _ = unsafe { sys::gettimeofday(tv, tz) }; } #[cfg(test)] @@ -61,16 +35,15 @@ mod tests { use celer_system_linux_ctypes::{Timeval, Timezone}; - use super::{GettimeofdayError, gettimeofday}; - use crate::{Errno, sys}; + use super::gettimeofday; + use crate::sys; #[test] fn test_gettimeofday_both_outputs() { let mut tv = MaybeUninit::::uninit(); let mut tz = MaybeUninit::::uninit(); - gettimeofday(Some(&mut tv), Some(&mut tz)) - .expect("wrapped gettimeofday failed"); + gettimeofday(Some(&mut tv), Some(&mut tz)); let tv = unsafe { tv.assume_init() }; let tz = unsafe { tz.assume_init() }; @@ -89,41 +62,28 @@ mod tests { fn test_gettimeofday_tv_none() { let mut tz = MaybeUninit::::uninit(); - gettimeofday(None, Some(&mut tz)).expect("wrapped gettimeofday failed"); + gettimeofday(None, Some(&mut tz)); } #[test] fn test_gettimeofday_tz_none() { let mut tv = MaybeUninit::::uninit(); - gettimeofday(Some(&mut tv), None).expect("wrapped gettimeofday failed"); + gettimeofday(Some(&mut tv), None); } #[test] fn test_gettimeofday_both_none() { - gettimeofday(None, None).expect("wrapped gettimeofday failed"); + gettimeofday(None, None); } #[test] fn test_gettimeofday_matches_raw_with_null_outputs() { - let wrapped = gettimeofday(None, None); + gettimeofday(None, None); let raw = unsafe { sys::gettimeofday(core::ptr::null_mut(), core::ptr::null_mut()) }; - assert_eq!(wrapped, Ok(())); assert_eq!(raw, 0); } - - #[test] - fn test_gettimeofday_error_mapping() { - assert_eq!( - GettimeofdayError::from_errno(Errno::Efault), - GettimeofdayError::Efault - ); - assert_eq!( - GettimeofdayError::from_errno(Errno::Enomem), - GettimeofdayError::Other(Errno::Enomem) - ); - } } diff --git a/system/linux/syscalls/src/lib.rs b/system/linux/syscalls/src/lib.rs index 88fdaed5..6d4d3fb2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -245,7 +245,7 @@ pub use getppid::getppid; pub use getpriority::{GetpriorityError, getpriority}; pub use getrlimit::{GetrlimitError, getrlimit}; pub use getrusage::{GetrusageError, getrusage}; -pub use gettimeofday::{GettimeofdayError, gettimeofday}; +pub use gettimeofday::gettimeofday; #[cfg(target_arch = "x86")] pub use getuid::getuid16; #[cfg(target_arch = "x86")] @@ -329,7 +329,7 @@ pub use signal::{ SIG_DFL, SIG_IGN, SigHandler, SignalError, sig_handler, signal, }; #[cfg(target_arch = "x86")] -pub use sigpending::{SigpendingError, sigpending}; +pub use sigpending::sigpending; #[cfg(target_arch = "x86")] pub use sigprocmask::{ SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK, SigprocmaskError, sigprocmask, @@ -361,7 +361,7 @@ pub use swapon::{SwaponError, swapon}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use symlink::{SymlinkError, symlink}; pub use sync::sync; -pub use sysinfo::{SysinfoError, sysinfo}; +pub use sysinfo::sysinfo; pub use syslog::{SyslogError, syslog}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use time::time; @@ -369,9 +369,9 @@ pub use times::times; pub use truncate::{TruncateError, truncate}; pub use umask::umask; pub use umount::{UmountError, umount}; -pub use uname::{NewunameError, newuname}; +pub use uname::newuname; #[cfg(target_arch = "x86")] -pub use uname::{OldoldunameError, OldunameError, oldolduname, olduname}; +pub use uname::{oldolduname, olduname}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use unlink::{UnlinkError, unlink}; #[cfg(target_arch = "x86")] @@ -405,6 +405,6 @@ pub mod linux_1_0 { pub use super::setup::{SetupError, setup}; pub use super::stat::{StatError, stat_1_0 as stat}; pub use super::statfs::{StatfsError, statfs_1_0 as statfs}; - pub use super::sysinfo::{SysinfoError, sysinfo_1_0 as sysinfo}; + pub use super::sysinfo::sysinfo_1_0 as sysinfo; pub use super::truncate::{TruncateError, truncate_1_0 as truncate}; } diff --git a/system/linux/syscalls/src/sigpending.rs b/system/linux/syscalls/src/sigpending.rs index a9fe0a44..f82abcda 100644 --- a/system/linux/syscalls/src/sigpending.rs +++ b/system/linux/syscalls/src/sigpending.rs @@ -2,49 +2,24 @@ use core::mem::MaybeUninit; use celer_system_linux_ctypes::OldSigsetT; -use crate::helpers::unit_from_ret; -use crate::{errno::Errno, sys}; - -/// Errors returned by [`sigpending`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SigpendingError { - Efault, - Other(Errno), -} - -impl SigpendingError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} +use crate::sys; /// Return the caller's pending blocked-signal mask word. /// /// This safe wrapper replaces the raw output pointer with -/// `&mut MaybeUninit` and maps the raw return into -/// `Result<(), SigpendingError>`. +/// `&mut MaybeUninit`. /// -/// On success, the kernel has initialized `set` with the legacy pending +/// On return, the kernel has initialized `set` with the legacy pending /// blocked-signal mask word. /// -/// See [`sys::sigpending`] for kernel behavior, reachable errors, and source -/// references. -/// -/// # Errors -/// - [`SigpendingError::Efault`]: the kernel could not write the output mask. -/// - [`SigpendingError::Other`]: any other syscall error reported by the raw -/// ABI. +/// See [`sys::sigpending`] for kernel behavior, reachable raw errors, and +/// source references. #[cfg(target_arch = "x86")] -pub fn sigpending( - set: &mut MaybeUninit, -) -> Result<(), SigpendingError> { +pub fn sigpending(set: &mut MaybeUninit) { // SAFETY: `MaybeUninit` provides writable storage for one - // legacy signal mask word. - let ret = unsafe { sys::sigpending(set.as_mut_ptr()) }; - unit_from_ret(ret as isize, SigpendingError::from_errno) + // legacy signal mask word. The only raw error path is an inaccessible + // output pointer, which this wrapper does not expose. + let _ = unsafe { sys::sigpending(set.as_mut_ptr()) }; } #[cfg(test)] @@ -52,25 +27,12 @@ pub fn sigpending( mod tests { use core::mem::MaybeUninit; - use super::{SigpendingError, sigpending}; - use crate::Errno; + use super::sigpending; #[test] fn test_sigpending_ok() { let mut pending = MaybeUninit::uninit(); - assert_eq!(sigpending(&mut pending), Ok(())); - } - - #[test] - fn test_sigpending_error_mapping() { - assert_eq!( - SigpendingError::from_errno(Errno::Efault), - SigpendingError::Efault - ); - assert_eq!( - SigpendingError::from_errno(Errno::Einval), - SigpendingError::Other(Errno::Einval) - ); + sigpending(&mut pending); } } diff --git a/system/linux/syscalls/src/sysinfo.rs b/system/linux/syscalls/src/sysinfo.rs index 5fb72752..93c447e6 100644 --- a/system/linux/syscalls/src/sysinfo.rs +++ b/system/linux/syscalls/src/sysinfo.rs @@ -4,45 +4,23 @@ use celer_system_linux_ctypes::Sysinfo; #[cfg(target_arch = "x86")] use celer_system_linux_ctypes::linux_1_0::Sysinfo as Linux10Sysinfo; -use crate::errno::Errno; -use crate::helpers::unit_from_ret; use crate::sys; -/// Errors returned by [`sysinfo`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SysinfoError { - Efault, - Other(Errno), -} - -impl SysinfoError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - /// Copy system load, memory, swap, and task summary information into `info`. /// /// This safe wrapper replaces the raw output pointer with -/// `&mut MaybeUninit` and maps the raw return into -/// `Result<(), SysinfoError>`. +/// `&mut MaybeUninit`. /// -/// On success, the kernel has initialized `info`. +/// On return, the kernel has initialized `info`. /// -/// See [`sys::sysinfo`] for kernel behavior, ABI layout notes, reachable +/// See [`sys::sysinfo`] for kernel behavior, ABI layout notes, reachable raw /// errors, and source references. -/// -/// # Errors -/// - [`SysinfoError::Efault`]: the kernel could not write the output record. -/// - [`SysinfoError::Other`]: any other syscall error reported by the raw ABI. -pub fn sysinfo(info: &mut MaybeUninit) -> Result<(), SysinfoError> { +pub fn sysinfo(info: &mut MaybeUninit) { // SAFETY: `info` is writable for one `Sysinfo`; `MaybeUninit` avoids - // claiming the kernel writes into an already initialized Rust value. - let ret = unsafe { sys::sysinfo(info.as_mut_ptr()) }; - unit_from_ret(ret as isize, SysinfoError::from_errno) + // claiming the kernel writes into an already initialized Rust value. The + // only raw error path is an inaccessible output pointer, which this wrapper + // does not expose. + let _ = unsafe { sys::sysinfo(info.as_mut_ptr()) }; } /// Copy Linux 1.0 system summary information into `info`. @@ -50,21 +28,16 @@ pub fn sysinfo(info: &mut MaybeUninit) -> Result<(), SysinfoError> { /// This safe wrapper mirrors [`sys::linux_1_0::sysinfo`] with the historical /// Linux 1.0 output layout. /// -/// On success, the kernel has initialized `info`. +/// On return, the kernel has initialized `info`. /// /// See [`sys::linux_1_0::sysinfo`] for kernel behavior, ABI layout notes, -/// reachable errors, and source references. -/// -/// # Errors -/// - [`SysinfoError::Efault`]: the kernel could not write the output record. -/// - [`SysinfoError::Other`]: any other syscall error reported by the raw ABI. +/// reachable raw errors, and source references. #[cfg(target_arch = "x86")] -pub fn sysinfo_1_0( - info: &mut MaybeUninit, -) -> Result<(), SysinfoError> { - // SAFETY: `info` is writable for one Linux 1.0 `Sysinfo` record. - let ret = unsafe { sys::linux_1_0::sysinfo(info.as_mut_ptr()) }; - unit_from_ret(ret as isize, SysinfoError::from_errno) +pub fn sysinfo_1_0(info: &mut MaybeUninit) { + // SAFETY: `info` is writable for one Linux 1.0 `Sysinfo` record. The only + // raw error path is an inaccessible output pointer, which this wrapper + // does not expose. + let _ = unsafe { sys::linux_1_0::sysinfo(info.as_mut_ptr()) }; } #[cfg(test)] @@ -76,34 +49,20 @@ mod tests { #[cfg(target_arch = "x86")] use celer_system_linux_ctypes::linux_1_0::Sysinfo as Linux10Sysinfo; - use crate::Errno; - + use super::sysinfo; #[cfg(target_arch = "x86")] use super::sysinfo_1_0; - use super::{SysinfoError, sysinfo}; #[test] fn test_sysinfo_ok() { let mut info = MaybeUninit::::uninit(); - sysinfo(&mut info).unwrap(); + sysinfo(&mut info); } #[cfg(target_arch = "x86")] #[test] fn test_sysinfo_1_0_ok() { let mut info = MaybeUninit::::uninit(); - sysinfo_1_0(&mut info).unwrap(); - } - - #[test] - fn test_sysinfo_error_mapping() { - assert_eq!( - SysinfoError::from_errno(Errno::Efault), - SysinfoError::Efault - ); - assert_eq!( - SysinfoError::from_errno(Errno::Eio), - SysinfoError::Other(Errno::Eio) - ); + sysinfo_1_0(&mut info); } } diff --git a/system/linux/syscalls/src/uname.rs b/system/linux/syscalls/src/uname.rs index fb5c3e49..b354aabb 100644 --- a/system/linux/syscalls/src/uname.rs +++ b/system/linux/syscalls/src/uname.rs @@ -4,146 +4,59 @@ use celer_system_linux_ctypes::NewUtsname; #[cfg(target_arch = "x86")] use celer_system_linux_ctypes::{OldOldUtsname, OldUtsname}; -use crate::errno::Errno; -use crate::helpers::unit_from_ret; use crate::sys; -/// Errors returned by [`oldolduname`]. -#[cfg(target_arch = "x86")] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum OldoldunameError { - /// `EFAULT`. - Efault, - /// Another errno returned by uname compatibility handling. - Other(Errno), -} - -#[cfg(target_arch = "x86")] -impl OldoldunameError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - -/// Errors returned by [`olduname`]. -#[cfg(target_arch = "x86")] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum OldunameError { - /// `EFAULT`. - Efault, - /// Another errno returned by uname compatibility handling. - Other(Errno), -} - -#[cfg(target_arch = "x86")] -impl OldunameError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - -/// Errors returned by [`newuname`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum NewunameError { - /// `EFAULT`. - Efault, - /// Another errno returned by uname compatibility handling. - Other(Errno), -} - -impl NewunameError { - fn from_errno(errno: Errno) -> Self { - match errno { - Errno::Efault => Self::Efault, - errno => Self::Other(errno), - } - } -} - /// Copy system identity strings through the legacy x86 `oldolduname` ABI. /// /// This safe wrapper replaces the raw output pointer with -/// `&mut MaybeUninit` and maps the raw syscall return into -/// `Result<(), OldoldunameError>`. +/// `&mut MaybeUninit`. /// -/// On success, the kernel has initialized `name` with the five-field legacy +/// On return, the kernel has initialized `name` with the five-field legacy /// `OldOldUtsname` record. /// -/// See [`sys::oldolduname`] for kernel behavior, reachable errors, ABI layout, -/// and source references. -/// -/// # Errors -/// - [`OldoldunameError::Efault`]: the kernel could not write the output -/// buffer. -/// - [`OldoldunameError::Other`]: another errno from uname compatibility -/// handling. +/// See [`sys::oldolduname`] for kernel behavior, reachable raw errors, ABI +/// layout, and source references. #[cfg(target_arch = "x86")] -pub fn oldolduname( - name: &mut MaybeUninit, -) -> Result<(), OldoldunameError> { +pub fn oldolduname(name: &mut MaybeUninit) { // SAFETY: `MaybeUninit` provides writable storage for one - // kernel-initialized legacy uname record. - let ret = unsafe { sys::oldolduname(name.as_mut_ptr()) }; - - unit_from_ret(ret as isize, OldoldunameError::from_errno) + // kernel-initialized legacy uname record. The only raw error path is an + // inaccessible output pointer, which this wrapper does not expose. + let _ = unsafe { sys::oldolduname(name.as_mut_ptr()) }; } /// Copy system identity strings through the legacy x86 `olduname` ABI. /// /// This safe wrapper replaces the raw output pointer with -/// `&mut MaybeUninit` and maps the raw syscall return into -/// `Result<(), OldunameError>`. +/// `&mut MaybeUninit`. /// -/// On success, the kernel has initialized `name` with the five-field +/// On return, the kernel has initialized `name` with the five-field /// `OldUtsname` record. /// -/// See [`sys::olduname`] for kernel behavior, reachable errors, ABI layout, -/// and source references. -/// -/// # Errors -/// - [`OldunameError::Efault`]: the kernel could not write the output buffer. -/// - [`OldunameError::Other`]: another errno from uname compatibility -/// handling. +/// See [`sys::olduname`] for kernel behavior, reachable raw errors, ABI +/// layout, and source references. #[cfg(target_arch = "x86")] -pub fn olduname( - name: &mut MaybeUninit, -) -> Result<(), OldunameError> { +pub fn olduname(name: &mut MaybeUninit) { // SAFETY: `MaybeUninit` provides writable storage for one - // kernel-initialized legacy uname record. - let ret = unsafe { sys::olduname(name.as_mut_ptr()) }; - - unit_from_ret(ret as isize, OldunameError::from_errno) + // kernel-initialized legacy uname record. The only raw error path is an + // inaccessible output pointer, which this wrapper does not expose. + let _ = unsafe { sys::olduname(name.as_mut_ptr()) }; } /// Copy the system identity strings into `name`. /// /// This safe wrapper replaces the raw output pointer with -/// `&mut MaybeUninit` and maps the raw syscall return into -/// `Result<(), NewunameError>`. +/// `&mut MaybeUninit`. /// -/// On success, the kernel has initialized `name` with the six-field +/// On return, the kernel has initialized `name` with the six-field /// `NewUtsname` record. /// -/// See [`sys::newuname`] for kernel behavior, reachable errors, ABI layout, -/// and source references. -/// -/// # Errors -/// - [`NewunameError::Efault`]: the kernel could not write the output buffer. -/// - [`NewunameError::Other`]: another errno from uname compatibility handling. -pub fn newuname( - name: &mut MaybeUninit, -) -> Result<(), NewunameError> { +/// See [`sys::newuname`] for kernel behavior, reachable raw errors, ABI +/// layout, and source references. +pub fn newuname(name: &mut MaybeUninit) { // SAFETY: `MaybeUninit` provides writable storage for one - // kernel-initialized uname record. - let ret = unsafe { sys::newuname(name.as_mut_ptr()) }; - - unit_from_ret(ret as isize, NewunameError::from_errno) + // kernel-initialized uname record. The only raw error path is an + // inaccessible output pointer, which this wrapper does not expose. + let _ = unsafe { sys::newuname(name.as_mut_ptr()) }; } #[cfg(test)] @@ -151,18 +64,16 @@ pub fn newuname( mod tests { use std::mem::MaybeUninit; - use crate::Errno; - - use super::{NewunameError, newuname}; + use super::newuname; #[cfg(target_arch = "x86")] - use super::{OldoldunameError, OldunameError, oldolduname, olduname}; + use super::{oldolduname, olduname}; #[cfg(target_arch = "x86")] #[test] fn test_oldolduname_ok() { let mut name = MaybeUninit::uninit(); - assert_eq!(oldolduname(&mut name), Ok(())); + oldolduname(&mut name); let name = unsafe { name.assume_init() }; let sysname = name .sysname @@ -173,25 +84,12 @@ mod tests { assert_eq!(sysname, b"Linux"); } - #[cfg(target_arch = "x86")] - #[test] - fn test_oldolduname_error_mapping() { - assert_eq!( - OldoldunameError::from_errno(Errno::Efault), - OldoldunameError::Efault - ); - assert_eq!( - OldoldunameError::from_errno(Errno::Eio), - OldoldunameError::Other(Errno::Eio) - ); - } - #[cfg(target_arch = "x86")] #[test] fn test_olduname_ok() { let mut name = MaybeUninit::uninit(); - assert_eq!(olduname(&mut name), Ok(())); + olduname(&mut name); let name = unsafe { name.assume_init() }; assert_ne!(name.sysname[0], 0); assert_ne!(name.nodename[0], 0); @@ -200,24 +98,11 @@ mod tests { assert_ne!(name.machine[0], 0); } - #[cfg(target_arch = "x86")] - #[test] - fn test_olduname_error_mapping() { - assert_eq!( - OldunameError::from_errno(Errno::Efault), - OldunameError::Efault - ); - assert_eq!( - OldunameError::from_errno(Errno::Eio), - OldunameError::Other(Errno::Eio) - ); - } - #[test] fn test_newuname_ok() { let mut name = MaybeUninit::uninit(); - assert_eq!(newuname(&mut name), Ok(())); + newuname(&mut name); let name = unsafe { name.assume_init() }; let sysname = name .sysname @@ -227,16 +112,4 @@ mod tests { .collect::>(); assert_eq!(sysname, b"Linux"); } - - #[test] - fn test_newuname_error_mapping() { - assert_eq!( - NewunameError::from_errno(Errno::Efault), - NewunameError::Efault - ); - assert_eq!( - NewunameError::from_errno(Errno::Eio), - NewunameError::Other(Errno::Eio) - ); - } }