diff --git a/system/linux/syscalls/WRAP_PROGRESS.md b/system/linux/syscalls/WRAP_PROGRESS.md new file mode 100644 index 00000000..45c7b95a --- /dev/null +++ b/system/linux/syscalls/WRAP_PROGRESS.md @@ -0,0 +1,239 @@ +# 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. + +## 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. + +## Checklist + +- [x] `access` - x86, x86_64 +- [x] `acct` +- [x] `adjtimex` +- [x] `alarm` - x86, x86_64 +- [x] `brk` +- [x] `chdir` +- [x] `chmod` - x86, x86_64 +- [x] `chroot` +- [x] `close` +- [x] `creat` - x86, x86_64 +- [x] `create_module` - x86 +- [x] `delete_module` +- [x] `dup` +- [x] `dup2` - x86, x86_64 +- [x] `execve` +- [x] `exit` +- [x] `fchdir` +- [x] `fchmod` +- [x] `fchown16` - x86 +- [x] `fcntl` +- [x] `fork` - x86 +- [x] `fstatfs` +- [x] `fsync` +- [x] `ftruncate` +- [x] `get_kernel_syms` - x86 +- [x] `getegid16` - x86 +- [x] `geteuid16` - x86 +- [x] `getgid16` - x86 +- [x] `getgroups16` - x86 +- [x] `getitimer` +- [x] `getpgid` +- [x] `getpgrp` - x86, x86_64 +- [x] `getpid` +- [x] `getppid` +- [x] `getpriority` +- [x] `getrlimit` +- [x] `getrusage` +- [x] `gettimeofday` +- [x] `getuid16` - x86 +- [x] `idle` - x86 +- [x] `init_module` +- [x] `ioctl` +- [x] `ioperm` - x86 +- [x] `iopl` - x86 +- [x] `ipc` - x86 +- [x] `kill` +- [x] `lchown16` - x86 +- [x] `link` - x86, x86_64 +- [x] `lseek` +- [x] `mknod` - x86, x86_64 +- [x] `mkdir` - x86, x86_64 +- [x] `mmap` +- [x] `modify_ldt` - x86 +- [x] `mount` +- [x] `munmap` +- [x] `newfstat` +- [x] `newlstat` - x86, x86_64 +- [x] `newuname` +- [x] `nice` - x86 +- [x] `oldfstat` - x86 +- [x] `oldlstat` - x86 +- [x] `oldolduname` - x86 +- [x] `oldstat` - x86 +- [x] `olduname` - x86 +- [x] `open` - x86, x86_64 +- [x] `pause` - x86, x86_64 +- [x] `pipe` - x86, x86_64 +- [x] `ptrace` +- [x] `read` +- [x] `readdir` - x86 +- [x] `readlink` - x86, x86_64 +- [x] `reboot` +- [x] `rename` - x86, x86_64 +- [x] `rmdir` - x86, x86_64 +- [x] `select` - x86, x86_64 +- [x] `setdomainname` +- [x] `setgid16` - x86 +- [x] `setgroups16` - x86 +- [x] `sethostname` +- [x] `setitimer` +- [x] `setpgid` +- [x] `setpriority` +- [x] `setregid16` - x86 +- [x] `setreuid16` - x86 +- [x] `setrlimit` +- [x] `setsid` +- [x] `settimeofday` +- [x] `setuid16` - x86 +- [x] `sgetmask` - x86 +- [x] `sigaction` - x86 +- [x] `signal` - x86 +- [x] `sigpending` - x86 +- [x] `sigprocmask` - x86 +- [x] `sigreturn` - x86 +- [x] `sigsuspend` - x86 +- [x] `socketcall` - x86 +- [x] `ssetmask` - x86 +- [x] `stat` - x86, x86_64 +- [x] `statfs` +- [x] `stime` - x86 +- [x] `swapoff` +- [x] `swapon` +- [x] `symlink` - x86, x86_64 +- [x] `sync` +- [x] `sysinfo` +- [x] `syslog` +- [x] `time` - x86, x86_64 +- [x] `times` +- [x] `truncate` +- [x] `umask` +- [x] `umount` +- [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 + +These raw wrappers are currently exposed under `sys::linux_1_0` on x86. Decide +whether the wrap layer mirrors them before checking them off. + +- [x] `linux_1_0::fstatfs` +- [x] `linux_1_0::ftruncate` +- [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` diff --git a/system/linux/syscalls/src/access.rs b/system/linux/syscalls/src/access.rs new file mode 100644 index 00000000..001e63d5 --- /dev/null +++ b/system/linux/syscalls/src/access.rs @@ -0,0 +1,102 @@ +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)] +pub enum AccessError { + /// `EINVAL`. + Einval, + /// `ENOMEM`. + Enomem, + /// Another errno returned by delegated pathname, permission, or filesystem + /// work. + Other(Errno), +} + +impl AccessError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Einval => Self::Einval, + Errno::Enomem => Self::Enomem, + errno => Self::Other(errno), + } + } +} + +/// 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 +/// - [`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. + let ret = unsafe { sys::access(pathname.as_ptr(), mode) }; + + unit_from_ret(ret as isize, AccessError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + use std::fs::{self, File}; + use std::time::{SystemTime, UNIX_EPOCH}; + + use crate::Errno; + + 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_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(access(path_c.as_c_str(), 0), Ok(())); + + fs::remove_file(&path).unwrap(); + } + + #[test] + fn test_access_einval() { + let path = CString::new("/tmp").unwrap(); + + 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 new file mode 100644 index 00000000..38ba4a4c --- /dev/null +++ b/system/linux/syscalls/src/acct.rs @@ -0,0 +1,87 @@ +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)] +pub enum AcctError { + /// `EPERM`. + Eperm, + /// Another errno returned by delegated pathname, file-opening, or + /// accounting-file validation work. + Other(Errno), +} + +impl AcctError { + fn from_errno(errno: Errno) -> Self { + match errno { + Errno::Eperm => Self::Eperm, + errno => Self::Other(errno), + } + } +} + +/// 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 +/// - [`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. + let ret = + unsafe { sys::acct(name.map_or(core::ptr::null(), CStr::as_ptr)) }; + + unit_from_ret(ret as isize, AcctError::from_errno) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::ffi::CString; + + use crate::Errno; + + 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"), + } + } + + #[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 new file mode 100644 index 00000000..f0035a99 --- /dev/null +++ b/system/linux/syscalls/src/adjtimex.rs @@ -0,0 +1,144 @@ +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)] +pub enum AdjtimexError { + /// `EFAULT`. + Efault, + /// `EINVAL`. + Einval, + /// `EPERM`. + Eperm, + /// `ENODEV`. + Enodev, + /// Another errno returned by delegated kernel work. + Other(Errno), +} + +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 => Self::Other(errno), + } + } +} + +/// 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 +/// - [`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. + let ret = unsafe { sys::adjtimex(txc as *mut Timex) }; + + 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_TICK, Timex}; + + use crate::Errno; + + 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_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" + ); + } + + #[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/alarm.rs b/system/linux/syscalls/src/alarm.rs new file mode 100644 index 00000000..7011302f --- /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. +/// +/// See [`sys::alarm`] for kernel behavior and source references. +pub fn alarm(seconds: UnsignedInt) -> UnsignedInt { + let ret = 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/brk.rs b/system/linux/syscalls/src/brk.rs new file mode 100644 index 00000000..e493c29e --- /dev/null +++ b/system/linux/syscalls/src/brk.rs @@ -0,0 +1,100 @@ +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)] +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 }, +} + +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. +/// +/// 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`]. +/// +/// 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 +/// - [`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. + let ret = unsafe { sys::brk(addr) }; + + 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)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use celer_system_linux_ctypes::UnsignedLong; + + use crate::{Errno, 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"), + Err(BrkError::Other(errno)) => { + panic!("brk(0) returned unexpected errno: {errno:?}") + } + }; + assert_ne!(current, 0); + + 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 new file mode 100644 index 00000000..def4b150 --- /dev/null +++ b/system/linux/syscalls/src/chdir.rs @@ -0,0 +1,102 @@ +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)) + ); + } + + #[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 new file mode 100644 index 00000000..e7d1b4b6 --- /dev/null +++ b/system/linux/syscalls/src/chmod.rs @@ -0,0 +1,119 @@ +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)) + ); + } + + #[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 new file mode 100644 index 00000000..158ca50d --- /dev/null +++ b/system/linux/syscalls/src/chroot.rs @@ -0,0 +1,113 @@ +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 crate::Errno; + + 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_c = CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); + + assert_eq!(chroot(path_c.as_c_str()), Err(ChrootError::Enotdir)); + + 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 new file mode 100644 index 00000000..6a0b632b --- /dev/null +++ b/system/linux/syscalls/src/close.rs @@ -0,0 +1,97 @@ +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 crate::Errno; + + 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)); + } + + #[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 new file mode 100644 index 00000000..c79135e5 --- /dev/null +++ b/system/linux/syscalls/src/creat.rs @@ -0,0 +1,110 @@ +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)) + ); + } + + #[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 new file mode 100644 index 00000000..7a5a40a3 --- /dev/null +++ b/system/linux/syscalls/src/create_module.rs @@ -0,0 +1,168 @@ +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) }; + + create_module_from_ret(ret) +} + +fn create_module_from_ret( + ret: UnsignedLong, +) -> Result, CreateModuleError> { + result_from_ret( + ret as isize, + 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 { + use std::ffi::CString; + + use celer_system_linux_ctypes::UnsignedLong; + + use crate::Errno; + + use super::{CreateModuleError, create_module, create_module_from_ret}; + + #[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); + } + + #[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) + ); + } +} diff --git a/system/linux/syscalls/src/delete_module.rs b/system/linux/syscalls/src/delete_module.rs new file mode 100644 index 00000000..a598b158 --- /dev/null +++ b/system/linux/syscalls/src/delete_module.rs @@ -0,0 +1,183 @@ +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::Eagain | 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::Eagain), + DeleteModuleError::Ewouldblock + ); + 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/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/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/errno.rs b/system/linux/syscalls/src/errno.rs new file mode 100644 index 00000000..14ba655d --- /dev/null +++ b/system/linux/syscalls/src/errno.rs @@ -0,0 +1,134 @@ +/// A Linux errno value. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Errno { + Eacces, + Eagain, + E2big, + Ebadf, + Ebadmsg, + Ebusy, + Emfile, + Efault, + Eexist, + Exdev, + Einval, + Eintr, + Eio, + Eloop, + Enodev, + Enoent, + Enoexec, + Enomem, + Enosys, + Enotty, + Enotdir, + Eoverflow, + Eperm, + Ekeyrejected, + Erofs, + Esrch, + Ewouldblock, + Raw(u16), +} + +impl Errno { + /// Converts a positive Linux errno number into an [`Errno`]. + pub const fn from_raw(raw: u16) -> Self { + match raw { + 13 => Self::Eacces, + 11 => Self::Eagain, + 7 => Self::E2big, + 9 => Self::Ebadf, + 74 => Self::Ebadmsg, + 16 => Self::Ebusy, + 24 => Self::Emfile, + 14 => Self::Efault, + 17 => Self::Eexist, + 18 => Self::Exdev, + 22 => Self::Einval, + 4 => Self::Eintr, + 5 => Self::Eio, + 40 => Self::Eloop, + 19 => Self::Enodev, + 2 => Self::Enoent, + 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), + } + } + + /// Returns `true` when `value` is in Linux's kernel errno return range. + pub const fn is_errno(value: isize) -> bool { + value >= -4095 && value <= -1 + } + + /// 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) { + Some(Self::from_raw((-value) as u16)) + } else { + None + } + } +} + +#[cfg(test)] +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(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); + 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); + 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(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)); + } + + #[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)); + } + + #[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/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/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/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/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/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/fcntl.rs b/system/linux/syscalls/src/fcntl.rs new file mode 100644 index 00000000..29391904 --- /dev/null +++ b/system/linux/syscalls/src/fcntl.rs @@ -0,0 +1,246 @@ +use celer_system_linux_ctypes::{Int, Long}; + +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 { + /// `EBADF`. + Ebadf, + /// `EFAULT`. + Efault, + /// `EINVAL`. + Einval, + /// `EMFILE`. + 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), +} + +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 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. 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. +/// +/// # 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::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 && ret < 0 { + return Err(FcntlError::FgetownNegative(ret)); + } + + 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::{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 { + 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_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(); + 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) }, + Err(FcntlError::FgetownNegative(-pgrp)) + ); + + 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_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); + 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/gettimeofday.rs b/system/linux/syscalls/src/gettimeofday.rs new file mode 100644 index 00000000..068561ed --- /dev/null +++ b/system/linux/syscalls/src/gettimeofday.rs @@ -0,0 +1,89 @@ +use core::{mem::MaybeUninit, ptr}; + +use celer_system_linux_ctypes::{Timeval, Timezone}; + +use crate::sys; + +/// 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. +/// +/// On return, the kernel has initialized each provided output slot. +/// +/// See [`sys::gettimeofday`] for kernel behavior, historical notes, reachable +/// raw errors, and source references. +pub fn gettimeofday( + mut tv: Option<&mut MaybeUninit>, + mut tz: Option<&mut MaybeUninit>, +) { + 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. 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)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use celer_system_linux_ctypes::{Timeval, Timezone}; + + 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)); + + 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)); + } + + #[test] + fn test_gettimeofday_tz_none() { + let mut tv = MaybeUninit::::uninit(); + + gettimeofday(Some(&mut tv), None); + } + + #[test] + fn test_gettimeofday_both_none() { + gettimeofday(None, None); + } + + #[test] + fn test_gettimeofday_matches_raw_with_null_outputs() { + gettimeofday(None, None); + let raw = unsafe { + sys::gettimeofday(core::ptr::null_mut(), core::ptr::null_mut()) + }; + + assert_eq!(raw, 0); + } +} 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/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/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/init_module.rs b/system/linux/syscalls/src/init_module.rs new file mode 100644 index 00000000..8906b0c4 --- /dev/null +++ b/system/linux/syscalls/src/init_module.rs @@ -0,0 +1,246 @@ +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; +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) +} + +/// 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] + 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) + ); + } + + #[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 + )); + } +} 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/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/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/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/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/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 1d849684..6d4d3fb2 100644 --- a/system/linux/syscalls/src/lib.rs +++ b/system/linux/syscalls/src/lib.rs @@ -10,4 +10,401 @@ pub mod arch; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod access; +mod acct; +mod adjtimex; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod alarm; +mod brk; +mod chdir; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod chmod; +mod chroot; +mod close; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod creat; +#[cfg(target_arch = "x86")] +mod create_module; +mod delete_module; +mod dup; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod dup2; +mod errno; +mod execve; +mod exit; +mod fchdir; +mod fchmod; +#[cfg(target_arch = "x86")] +mod fchown; +mod fcntl; +#[cfg(target_arch = "x86")] +mod fork; +#[cfg(target_arch = "x86")] +mod fstat; +mod fstatfs; +mod fsync; +mod ftruncate; +#[cfg(target_arch = "x86")] +mod get_kernel_syms; +#[cfg(target_arch = "x86")] +mod getegid; +#[cfg(target_arch = "x86")] +mod geteuid; +#[cfg(target_arch = "x86")] +mod getgid; +#[cfg(target_arch = "x86")] +mod getgroups; +mod getitimer; +mod getpgid; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod getpgrp; +mod getpid; +mod getppid; +mod getpriority; +mod getrlimit; +mod getrusage; +mod gettimeofday; +#[cfg(target_arch = "x86")] +mod getuid; +mod helpers; +#[cfg(target_arch = "x86")] +mod idle; +mod init_module; +mod ioctl; +#[cfg(target_arch = "x86")] +mod ioperm; +#[cfg(target_arch = "x86")] +mod iopl; +#[cfg(target_arch = "x86")] +mod ipc; +mod kill; +#[cfg(target_arch = "x86")] +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"))] +mod mknod; +mod mmap; +#[cfg(target_arch = "x86")] +mod modify_ldt; +mod mount; +mod munmap; +mod newfstat; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod newlstat; +#[cfg(target_arch = "x86")] +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(any(target_arch = "x86", target_arch = "x86_64"))] +mod pipe; +mod ptrace; +mod read; +#[cfg(target_arch = "x86")] +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(any(target_arch = "x86", target_arch = "x86_64"))] +mod rmdir; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod select; +mod setdomainname; +#[cfg(target_arch = "x86")] +mod setgid; +#[cfg(target_arch = "x86")] +mod setgroups; +mod sethostname; +mod setitimer; +mod setpgid; +mod setpriority; +#[cfg(target_arch = "x86")] +mod setregid; +#[cfg(target_arch = "x86")] +mod setreuid; +mod setrlimit; +mod setsid; +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; +#[cfg(target_arch = "x86")] +mod signal; +#[cfg(target_arch = "x86")] +mod sigpending; +#[cfg(target_arch = "x86")] +mod sigprocmask; +#[cfg(target_arch = "x86")] +mod sigreturn; +#[cfg(target_arch = "x86")] +mod sigsuspend; +#[cfg(target_arch = "x86")] +mod socketcall; +#[cfg(target_arch = "x86")] +mod ssetmask; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod stat; +mod statfs; +#[cfg(target_arch = "x86")] +mod stime; +mod swapoff; +mod swapon; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod symlink; +mod sync; pub mod sys; +mod sysinfo; +mod syslog; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod time; +mod times; +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}; +pub use acct::{AcctError, acct}; +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}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +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 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; +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}; +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}; +#[cfg(target_arch = "x86")] +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; +#[cfg(target_arch = "x86")] +pub use getgid::getgid16; +#[cfg(target_arch = "x86")] +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; +pub use getpid::getpid; +pub use getppid::getppid; +pub use getpriority::{GetpriorityError, getpriority}; +pub use getrlimit::{GetrlimitError, getrlimit}; +pub use getrusage::{GetrusageError, getrusage}; +pub use gettimeofday::gettimeofday; +#[cfg(target_arch = "x86")] +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}; +#[cfg(target_arch = "x86")] +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, +}; +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}; +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"))] +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}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +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(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}; +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}; +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(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}; +#[cfg(target_arch = "x86")] +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 setregid::{Setregid16Error, setregid16}; +#[cfg(target_arch = "x86")] +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 setuid::{Setuid16Error, setuid16}; +#[cfg(target_arch = "x86")] +pub use sgetmask::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 sigpending::sigpending; +#[cfg(target_arch = "x86")] +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 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 ssetmask::ssetmask; +#[cfg(target_arch = "x86")] +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 statfs::{StatfsError, statfs}; +#[cfg(target_arch = "x86")] +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 sync::sync; +pub use sysinfo::sysinfo; +pub use syslog::{SyslogError, syslog}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use time::time; +pub use times::times; +pub use truncate::{TruncateError, truncate}; +pub use umask::umask; +pub use umount::{UmountError, umount}; +pub use uname::newuname; +#[cfg(target_arch = "x86")] +pub use uname::{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")] +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, + }; + 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::sysinfo_1_0 as sysinfo; + pub use super::truncate::{TruncateError, truncate_1_0 as truncate}; +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/setup.rs b/system/linux/syscalls/src/setup.rs new file mode 100644 index 00000000..b9f6bcce --- /dev/null +++ b/system/linux/syscalls/src/setup.rs @@ -0,0 +1,57 @@ +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")] +#[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) +} + +#[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) + ); + } +} diff --git a/system/linux/syscalls/src/sgetmask.rs b/system/linux/syscalls/src/sgetmask.rs new file mode 100644 index 00000000..78b13204 --- /dev/null +++ b/system/linux/syscalls/src/sgetmask.rs @@ -0,0 +1,27 @@ +use celer_system_linux_ctypes::OldSigsetT; + +use crate::sys; + +/// Return the caller's legacy blocked-signal mask word. +/// +/// 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. +#[cfg(target_arch = "x86")] +pub fn sgetmask() -> OldSigsetT { + sys::sgetmask() +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::sgetmask; + + #[test] + fn test_sgetmask_smoke() { + let _ = sgetmask(); + } +} 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) + ); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/sigpending.rs b/system/linux/syscalls/src/sigpending.rs new file mode 100644 index 00000000..f82abcda --- /dev/null +++ b/system/linux/syscalls/src/sigpending.rs @@ -0,0 +1,38 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::OldSigsetT; + +use crate::sys; + +/// Return the caller's pending blocked-signal mask word. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit`. +/// +/// On return, the kernel has initialized `set` with the legacy pending +/// blocked-signal mask word. +/// +/// See [`sys::sigpending`] for kernel behavior, reachable raw errors, and +/// source references. +#[cfg(target_arch = "x86")] +pub fn sigpending(set: &mut MaybeUninit) { + // SAFETY: `MaybeUninit` provides writable storage for one + // 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)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use super::sigpending; + + #[test] + fn test_sigpending_ok() { + let mut pending = MaybeUninit::uninit(); + + sigpending(&mut pending); + } +} 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) + ); + } +} 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() } +} 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) + ); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/ssetmask.rs b/system/linux/syscalls/src/ssetmask.rs new file mode 100644 index 00000000..3eecdfdb --- /dev/null +++ b/system/linux/syscalls/src/ssetmask.rs @@ -0,0 +1,67 @@ +use celer_system_linux_ctypes::OldSigsetT; + +use crate::sys; + +/// Replace the caller's legacy blocked-signal mask word. +/// +/// 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. +#[cfg(target_arch = "x86")] +pub fn ssetmask(newmask: OldSigsetT) -> OldSigsetT { + sys::ssetmask(newmask) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + 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 _ = ssetmask(0); + } + + #[test] + 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/stat.rs b/system/linux/syscalls/src/stat.rs new file mode 100644 index 00000000..86784dbb --- /dev/null +++ b/system/linux/syscalls/src/stat.rs @@ -0,0 +1,234 @@ +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`. + Efault, + /// `EOVERFLOW`. + Eoverflow, + /// Another errno returned by delegated pathname lookup or filesystem code. + Other(Errno), +} + +#[cfg(target_arch = "x86")] +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) +} + +/// 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 { + use std::{ffi::CString, mem::MaybeUninit}; + + 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(); + let mut statbuf = MaybeUninit::uninit(); + + 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(); + let mut statbuf = MaybeUninit::uninit(); + + assert_eq!( + oldstat(path.as_c_str(), &mut statbuf), + Err(OldstatError::Other(Errno::Enoent)) + ); + } + + #[cfg(target_arch = "x86")] + #[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) + ); + } + + #[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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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(), ()); + } +} 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 diff --git a/system/linux/syscalls/src/sysinfo.rs b/system/linux/syscalls/src/sysinfo.rs new file mode 100644 index 00000000..93c447e6 --- /dev/null +++ b/system/linux/syscalls/src/sysinfo.rs @@ -0,0 +1,68 @@ +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::sys; + +/// Copy system load, memory, swap, and task summary information into `info`. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit`. +/// +/// On return, the kernel has initialized `info`. +/// +/// See [`sys::sysinfo`] for kernel behavior, ABI layout notes, reachable raw +/// errors, and source references. +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. 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`. +/// +/// This safe wrapper mirrors [`sys::linux_1_0::sysinfo`] with the historical +/// Linux 1.0 output layout. +/// +/// On return, the kernel has initialized `info`. +/// +/// See [`sys::linux_1_0::sysinfo`] for kernel behavior, ABI layout notes, +/// reachable raw errors, and source references. +#[cfg(target_arch = "x86")] +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)] +#[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 super::sysinfo; + #[cfg(target_arch = "x86")] + use super::sysinfo_1_0; + + #[test] + fn test_sysinfo_ok() { + let mut info = MaybeUninit::::uninit(); + sysinfo(&mut info); + } + + #[cfg(target_arch = "x86")] + #[test] + fn test_sysinfo_1_0_ok() { + let mut info = MaybeUninit::::uninit(); + sysinfo_1_0(&mut info); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/time.rs b/system/linux/syscalls/src/time.rs new file mode 100644 index 00000000..1d9bb433 --- /dev/null +++ b/system/linux/syscalls/src/time.rs @@ -0,0 +1,49 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::TimeT; + +use crate::sys; + +/// Return the current calendar time in seconds since the Unix epoch. +/// +/// This safe wrapper replaces the nullable raw output pointer with +/// `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. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +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. + unsafe { sys::time(ptr) } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use core::mem::MaybeUninit; + + use super::time; + + #[test] + fn test_time_null_ok() { + assert!(time(None) > 0); + } + + #[test] + fn test_time_writes_output() { + let mut stored = MaybeUninit::uninit(); + let now = time(Some(&mut stored)); + + // SAFETY: `time` initialized `stored`. + let stored = unsafe { stored.assume_init() }; + assert_eq!(now, stored); + } +} 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}"); + } +} 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) + ); + } +} 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); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/uname.rs b/system/linux/syscalls/src/uname.rs new file mode 100644 index 00000000..b354aabb --- /dev/null +++ b/system/linux/syscalls/src/uname.rs @@ -0,0 +1,115 @@ +use core::mem::MaybeUninit; + +use celer_system_linux_ctypes::NewUtsname; +#[cfg(target_arch = "x86")] +use celer_system_linux_ctypes::{OldOldUtsname, OldUtsname}; + +use crate::sys; + +/// Copy system identity strings through the legacy x86 `oldolduname` ABI. +/// +/// This safe wrapper replaces the raw output pointer with +/// `&mut MaybeUninit`. +/// +/// On return, the kernel has initialized `name` with the five-field legacy +/// `OldOldUtsname` record. +/// +/// See [`sys::oldolduname`] for kernel behavior, reachable raw errors, ABI +/// layout, and source references. +#[cfg(target_arch = "x86")] +pub fn oldolduname(name: &mut MaybeUninit) { + // SAFETY: `MaybeUninit` provides writable storage for one + // 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`. +/// +/// On return, the kernel has initialized `name` with the five-field +/// `OldUtsname` record. +/// +/// See [`sys::olduname`] for kernel behavior, reachable raw errors, ABI +/// layout, and source references. +#[cfg(target_arch = "x86")] +pub fn olduname(name: &mut MaybeUninit) { + // SAFETY: `MaybeUninit` provides writable storage for one + // 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`. +/// +/// On return, the kernel has initialized `name` with the six-field +/// `NewUtsname` record. +/// +/// 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. 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)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use std::mem::MaybeUninit; + + use super::newuname; + #[cfg(target_arch = "x86")] + use super::{oldolduname, olduname}; + + #[cfg(target_arch = "x86")] + #[test] + fn test_oldolduname_ok() { + let mut name = MaybeUninit::uninit(); + + oldolduname(&mut name); + 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_olduname_ok() { + let mut name = MaybeUninit::uninit(); + + olduname(&mut name); + 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); + } + + #[test] + fn test_newuname_ok() { + let mut name = MaybeUninit::uninit(); + + newuname(&mut name); + 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"); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +} diff --git a/system/linux/syscalls/src/vm86.rs b/system/linux/syscalls/src/vm86.rs new file mode 100644 index 00000000..100828c8 --- /dev/null +++ b/system/linux/syscalls/src/vm86.rs @@ -0,0 +1,76 @@ +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")] +#[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) +} + +#[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) + ); + } +} 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) + ); + } +} 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) + ); + } +} 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) + ); + } +}