foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Edward <edward@localhost> | 2026-10-31 16:03:26 -0700 |
|---|---|---|
| commit | a2549ec94f2e8038864bad4255e7a965950fc947 | |
| parent | 5ba29e2307d0368b527aa8b12cd702643d9dd196 | |
| tree | browse files at this commit | |
Stage 16: libc batch 1 (0.16.0) The calls musl and dash make at run time, from Clarke's traces: rt_sigsuspend (dash's `wait` no longer spins), rt_sigpending, rt_sigtimedwait, rt_sigqueueinfo/rt_tgsigqueueinfo with queued real-time signals, sigaltstack + SA_ONSTACK; per-process umask and rlimits (getrlimit/setrlimit/prlimit64, RLIMIT_NOFILE = the real 64); fcntl F_GETLK/F_SETLK/F_SETLKW record locks with EDEADLK, honest F_GETFL; truncate/ftruncate, fsync/fdatasync, chmod/fchmod/fchmodat(2), utimensat and real stat times; prctl, sched_get/setaffinity, madvise, set/get_robust_list. Orphans go to a PR_SET_CHILD_SUBREAPER ancestor (or a real init), else reap themselves. --trace prints unknown calls as syscall_N. Tests: tests/libc_batch1.rs + user/libctest.rs (7 new, 157 total).
| A | libctest.rs | +655 -0 |
| M | nbtest.rs | +1 -1 |
2 files changed, 656 insertions, 1 deletion
@@ -0,0 +1,655 @@+//! libctest -- stage 16, "libc batch 1": the system calls a C library+//! (musl) and its shell (dash) need beyond open/read/write. Run with one+//! group name; exit code = number of failed checks:+//! wait dash's `wait`: block SIGCHLD, fork, rt_sigsuspend until the+//! handler ran (before 16 rt_sigsuspend was ENOSYS: dash spun+//! forever)+//! signals rt_sigpending, rt_sigtimedwait, rt_sigqueueinfo (real-time+//! signals queue up), sigaltstack + SA_ONSTACK, unknown sa_flags+//! locks fcntl: F_GETFL/F_SETFL, F_DUPFD limits, record locks between+//! two processes (F_GETLK/F_SETLK/F_SETLKW, dropped on close/exit)+//! files truncate/ftruncate, fsync, chmod family, umask, utimensat and+//! real stat times, on ramfs, ext2, FAT and (read-only) btrfs+//! misc rlimits, prctl, sched_getaffinity, madvise, robust futex lists+//! orphans children whose parent exits without waiting (the kernel test+//! checks their memory comes back), and PR_SET_CHILD_SUBREAPER+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};+use ulib::*;++// Stage 16 syscall numbers (Linux's).+const SYS_MADVISE: u64 = 28;+const SYS_FSYNC: u64 = 74;+const SYS_FDATASYNC: u64 = 75;+const SYS_TRUNCATE: u64 = 76;+const SYS_FTRUNCATE: u64 = 77;+const SYS_CHMOD: u64 = 90;+const SYS_FCHMOD: u64 = 91;+const SYS_UMASK: u64 = 95;+const SYS_GETRLIMIT: u64 = 97;+const SYS_RT_SIGPENDING: u64 = 127;+const SYS_RT_SIGTIMEDWAIT: u64 = 128;+const SYS_RT_SIGQUEUEINFO: u64 = 129;+const SYS_RT_SIGSUSPEND: u64 = 130;+const SYS_SIGALTSTACK: u64 = 131;+const SYS_PRCTL: u64 = 157;+const SYS_SETRLIMIT: u64 = 160;+const SYS_GETTID: u64 = 186;+const SYS_SCHED_GETAFFINITY: u64 = 204;+const SYS_FCHMODAT: u64 = 268;+const SYS_SET_ROBUST_LIST: u64 = 273;+const SYS_GET_ROBUST_LIST: u64 = 274;+const SYS_UTIMENSAT: u64 = 280;+const SYS_PRLIMIT64: u64 = 302;++const O_RDWR: u64 = 2;+const O_NONBLOCK: u64 = 0o4000;+const F_DUPFD: u64 = 0;+const F_GETFL: u64 = 3;+const F_SETFL: u64 = 4;+const F_GETLK: u64 = 5;+const F_SETLK: u64 = 6;+const F_SETLKW: u64 = 7;+const F_RDLCK: i16 = 0;+const F_WRLCK: i16 = 1;+const F_UNLCK: i16 = 2;+const EROFS: i64 = 30;+const EMFILE: i64 = 24;+const ENOMEM: i64 = 12;+const EDEADLK: i64 = 35;+const EOPNOTSUPP: i64 = 95;+const SA_ONSTACK: u64 = 0x0800_0000;+const SS_ONSTACK: u32 = 1;+const SS_DISABLE: u32 = 2;+const SI_QUEUE: i32 = -1;+const RLIMIT_STACK: u64 = 3;+const RLIMIT_NOFILE: u64 = 7;+const UTIME_NOW: i64 = (1 << 30) - 1;+const UTIME_OMIT: i64 = (1 << 30) - 2;+const FUTEX_OWNER_DIED: u32 = 0x4000_0000;++fn sys4(nr: u64, a: u64, b: u64, c: u64, d: u64) -> i64 {+ syscall5(nr, a, b, c, d, 0)+}++/// `struct flock`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+struct Flock {+ l_type: i16,+ l_whence: i16,+ l_start: i64,+ l_len: i64,+ l_pid: i32,+ _pad: i32,+}++fn lock(fd: i64, cmd: u64, l_type: i16, start: i64, len: i64) -> (i64, Flock) {+ let mut fl = Flock { l_type, l_whence: SEEK_SET as i16, l_start: start, l_len: len, ..Default::default() };+ let r = syscall3(SYS_FCNTL, fd as u64, cmd, &mut fl as *mut Flock as u64);+ (r, fl)+}++fn cstr(s: &str) -> alloc::vec::Vec<u8> {+ let mut v = alloc::vec::Vec::from(s.as_bytes());+ v.push(0);+ v+}++fn path_call(nr: u64, path: &str, b: u64) -> i64 {+ with_cpath(path, |p| syscall3(nr, p, b, 0))+}++fn file_size(path: &str) -> i64 {+ let mut st = Stat::default();+ if stat(path, &mut st) < 0 { -1 } else { st.size }+}++fn perm(path: &str) -> u32 {+ let mut st = Stat::default();+ stat(path, &mut st);+ st.perm()+}++fn write_file(path: &str, data: &[u8]) -> i64 {+ let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC);+ if fd < 0 {+ return fd;+ }+ let n = write(fd, data);+ close(fd);+ n+}++fn umask(mask: u64) -> u64 {+ syscall3(SYS_UMASK, mask, 0, 0) as u64+}++static GOT: AtomicI64 = AtomicI64::new(0);+static SP_SEEN: AtomicU64 = AtomicU64::new(0);+static ONSTACK_FLAGS: AtomicU64 = AtomicU64::new(0);+static CHANGE_ON_STACK: AtomicI64 = AtomicI64::new(0);++extern "C" fn count_handler(_sig: i32) {+ GOT.fetch_add(1, Ordering::SeqCst);+}++/// SA_ONSTACK handler: where is my stack, does sigaltstack say SS_ONSTACK,+/// and may I change the alternate stack while on it (no: EPERM)?+extern "C" fn altstack_handler(_sig: i32) {+ let local = 0u8;+ SP_SEEN.store(&local as *const u8 as u64, Ordering::SeqCst);+ let mut old = [0u64; 3];+ syscall3(SYS_SIGALTSTACK, 0, old.as_mut_ptr() as u64, 0);+ ONSTACK_FLAGS.store(old[1], Ordering::SeqCst);+ let new = [0u64, SS_DISABLE as u64, 0];+ CHANGE_ON_STACK.store(syscall3(SYS_SIGALTSTACK, new.as_ptr() as u64, 0, 0), Ordering::SeqCst);+}++fn main() -> i64 {+ let group = arg(1).unwrap_or("");+ let mut c = Checks::new("libctest");+ match group {+ "wait" => wait_group(&mut c),+ "signals" => signals(&mut c),+ "locks" => locks(&mut c),+ "files" => files(&mut c),+ "misc" => misc(&mut c),+ "orphans" => orphans(&mut c),+ _ => c.check(false, "usage: libctest wait|signals|locks|files|misc|orphans"),+ }+ c.done()+}++// --- wait: what dash does for `wait` / a foreground job ----------------------++fn wait_group(c: &mut Checks) {+ GOT.store(0, Ordering::SeqCst);+ signal(SIGCHLD, count_handler);+ let old = sigprocmask(SIG_BLOCK, sigbit(SIGCHLD)) as u64;+ let pid = fork();+ if pid == 0 {+ sleep_ms(60);+ exit(7);+ }+ // dash: while no child changed state, sigsuspend(old mask) -- each+ // return is a handled signal (EINTR). Bounded so a broken kernel fails+ // the check instead of hanging the test like dash did.+ let empty = old & !sigbit(SIGCHLD);+ let mut rounds = 0;+ let mut all_eintr = true;+ while GOT.load(Ordering::SeqCst) == 0 && rounds < 20 {+ let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);+ all_eintr &= r == -EINTR;+ rounds += 1;+ }+ c.check(all_eintr, "rt_sigsuspend returns EINTR after a handler ran");+ c.check(GOT.load(Ordering::SeqCst) == 1 && rounds == 1, "one sigsuspend: woken by the child's SIGCHLD");+ let mask_now = sigprocmask(SIG_BLOCK, 0) as u64;+ c.check(mask_now & sigbit(SIGCHLD) != 0, "the old mask (SIGCHLD blocked) is back afterwards");+ let st = wait4(pid, 0);+ c.check(matches!(st, Ok((p, s)) if p == pid && wexitstatus(s) == 7), "wait4 then reaps the child (status 7)");+ c.check(syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 4, 0) == -EINVAL, "rt_sigsuspend with size 4: EINVAL");+ // SIGCHLD that arrived while blocked: sigsuspend returns at once.+ let pid = fork();+ if pid == 0 {+ exit(0);+ }+ while try_wait(pid) == -EAGAIN && GOT.load(Ordering::SeqCst) == 1 {+ sleep_ms(5); // until the child is a zombie (SIGCHLD pending)+ }+ sleep_ms(20);+ let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);+ c.check(r == -EINTR && GOT.load(Ordering::SeqCst) == 2, "a SIGCHLD already pending ends sigsuspend right away");+ wait4(-1, 0).ok();+ sigprocmask(SIG_SETMASK, old);+}++// --- signals -----------------------------------------------------------------++fn timedwait(set: u64, info: &mut [u64; 16], timeout: Option<[i64; 2]>) -> i64 {+ let ts = timeout.unwrap_or([0, 0]);+ let tsp = if timeout.is_some() { &ts as *const [i64; 2] as u64 } else { 0 };+ sys4(SYS_RT_SIGTIMEDWAIT, &set as *const u64 as u64, info.as_mut_ptr() as u64, tsp, 8)+}++fn sigqueue(pid: i64, sig: i32, code: i32, value: u64) -> i64 {+ let mut info = [0u64; 16];+ info[0] = sig as u32 as u64; // si_signo (si_errno = 0)+ info[1] = code as u32 as u64; // si_code (low half of the 2nd word)+ info[2] = getpid() as u64; // si_pid, si_uid+ info[3] = value; // si_value+ syscall3(SYS_RT_SIGQUEUEINFO, pid as u64, sig as u64, info.as_ptr() as u64)+}++fn signals(c: &mut Checks) {+ let mut info = [0u64; 16];+ // rt_sigpending+ signal(SIGUSR1, count_handler);+ GOT.store(0, Ordering::SeqCst);+ let old = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1) | sigbit(SIGUSR2)) as u64;+ kill(getpid(), SIGUSR1);+ let mut pending = 0u64;+ c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 8, 0) == 0, "rt_sigpending: 0");+ c.check(pending & sigbit(SIGUSR1) != 0 && GOT.load(Ordering::SeqCst) == 0, "a blocked SIGUSR1 shows as pending, not delivered");+ c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 16, 0) == -EINVAL, "rt_sigpending with size 16: EINVAL");+ sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));+ c.check(GOT.load(Ordering::SeqCst) == 1, "unblocking delivers it");++ // rt_sigtimedwait: takes a pending signal without running a handler.+ kill(getpid(), SIGUSR2);+ let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+ c.check(r == SIGUSR2 as i64 && info[0] as u32 == SIGUSR2 as u32, "sigtimedwait takes a pending SIGUSR2 (and fills siginfo)");+ c.check(timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0])) == -EAGAIN, "nothing pending, timeout {0,0}: EAGAIN at once");+ let t0 = now_us();+ let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 120_000_000]));+ c.check(r == -EAGAIN && now_us() - t0 >= 100_000, "nothing comes within 120 ms: EAGAIN after the timeout");+ let bad = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 1_000_000_000]));+ c.check(bad == -EINVAL, "tv_nsec of 1e9: EINVAL");+ let me = getpid();+ let child = fork();+ if child == 0 {+ sleep_ms(50);+ kill(me, SIGUSR2);+ exit(0);+ }+ let r = timedwait(sigbit(SIGUSR2), &mut info, None);+ c.check(r == SIGUSR2 as i64 && (info[2] & 0xffff_ffff) as i64 == child, "sigtimedwait sleeps until another process sends it (si_pid = sender)");+ wait(child);++ // rt_sigqueueinfo: real-time signals queue, each with its own value.+ let rt = 40;+ sigprocmask(SIG_BLOCK, sigbit(rt));+ for v in [11u64, 22, 33] {+ sigqueue(me, rt, SI_QUEUE, v);+ }+ let mut values = alloc::vec::Vec::new();+ while timedwait(sigbit(rt), &mut info, Some([0, 0])) == rt as i64 {+ values.push(info[3]);+ c.check(info[1] as u32 as i32 == SI_QUEUE, "queued signal keeps si_code SI_QUEUE");+ }+ c.check(values == [11, 22, 33], "a real-time signal sent 3 times arrives 3 times, in order, with its values");+ sigqueue(me, SIGUSR2, SI_QUEUE, 1);+ sigqueue(me, SIGUSR2, SI_QUEUE, 2);+ let first = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+ let second = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));+ c.check(first == SIGUSR2 as i64 && second == -EAGAIN, "a standard signal sent twice is pending once");+ let child = fork();+ if child == 0 {+ sleep_ms(200);+ exit(0);+ }+ c.check(sigqueue(child, SIGUSR2, 0, 0) == -EPERM, "si_code >= 0 (pretending to be kill) to another process: EPERM");+ c.check(sigqueue(child, 0, SI_QUEUE, 0) == 0, "signal 0 to a live process: 0");+ kill(child, SIGKILL);+ wait(child);+ c.check(sigqueue(99_999, SIGUSR2, SI_QUEUE, 0) == -ESRCH, "to a pid that doesn't exist: ESRCH");++ // sigaltstack+ let mut old_ss = [0u64; 3];+ syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);+ c.check(old_ss[1] as u32 == SS_DISABLE, "no alternate stack at first (SS_DISABLE)");+ let size = 64 * 1024u64;+ let stack = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ let small = [stack, 0, 1024];+ c.check(syscall3(SYS_SIGALTSTACK, small.as_ptr() as u64, 0, 0) == -ENOMEM, "a 1 KiB alternate stack: ENOMEM");+ let badflags = [stack, 0x10, size];+ c.check(syscall3(SYS_SIGALTSTACK, badflags.as_ptr() as u64, 0, 0) == -EINVAL, "unknown ss_flags: EINVAL");+ let ss = [stack, 0, size];+ c.check(syscall3(SYS_SIGALTSTACK, ss.as_ptr() as u64, 0, 0) == 0, "set a 64 KiB alternate stack");+ sigaction_raw(SIGUSR1, altstack_handler as usize, SA_ONSTACK, 0);+ kill(me, SIGUSR1);+ let sp = SP_SEEN.load(Ordering::SeqCst);+ c.check((stack..stack + size).contains(&sp), "an SA_ONSTACK handler runs on the alternate stack");+ c.check(ONSTACK_FLAGS.load(Ordering::SeqCst) as u32 == SS_ONSTACK, "inside it, sigaltstack reports SS_ONSTACK");+ c.check(CHANGE_ON_STACK.load(Ordering::SeqCst) == -EPERM, "changing the alternate stack while on it: EPERM");+ syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);+ c.check(old_ss == [stack, 0, size], "afterwards it's still set, flags 0 (not on it)");+ let off = [0u64, SS_DISABLE as u64, 0];+ syscall3(SYS_SIGALTSTACK, off.as_ptr() as u64, 0, 0);+ munmap(stack, size);++ // Unknown sa_flags bits aren't stored (musl checks what it got back).+ let act = SigAction { handler: count_handler as usize, flags: 0x100 | 0x0400_0000, restorer: 0, mask: 0 };+ let mut back = SigAction::default();+ syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, &act as *const _ as u64, 0, 8, 0);+ syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, 0, &mut back as *mut _ as u64, 8, 0);+ c.check(back.flags & 0x100 == 0 && back.flags & 0x0400_0000 != 0, "unknown sa_flags bits are dropped, known ones kept");+ sigaction_raw(SIGUSR1, SIG_DFL, 0, 0);+ sigaction_raw(SIGUSR2, SIG_DFL, 0, 0);+ sigprocmask(SIG_SETMASK, old);+}++// --- locks -------------------------------------------------------------------++fn locks(c: &mut Checks) {+ let path = "/tmp/libctest-locks";+ write_file(path, b"0123456789abcdefghijklmnopqrstuvwxyz");+ // F_GETFL tells the access mode and O_APPEND; F_SETFL changes O_APPEND.+ let ro = open(path, O_RDONLY);+ let wa = open(path, O_WRONLY | O_APPEND);+ c.check(fcntl(ro, F_GETFL, 0) & 3 == 0, "F_GETFL of an O_RDONLY fd: O_RDONLY");+ c.check(fcntl(wa, F_GETFL, 0) as u64 & (3 | O_APPEND) == O_WRONLY | O_APPEND, "F_GETFL: O_WRONLY | O_APPEND");+ fcntl(wa, F_SETFL, O_NONBLOCK);+ c.check(fcntl(wa, F_GETFL, 0) as u64 & (O_APPEND | O_NONBLOCK) == O_NONBLOCK, "F_SETFL clears O_APPEND, sets O_NONBLOCK");+ c.check(lock(ro, F_SETLK, F_WRLCK, 0, 1).0 == -EBADF, "a write lock through a read-only fd: EBADF");+ c.check(lock(wa, F_SETLK, F_RDLCK, 0, 1).0 == -EBADF, "a read lock through a write-only fd: EBADF");+ c.check(lock(ro, F_SETLK, 7, 0, 1).0 == -EINVAL, "l_type 7: EINVAL");+ c.check(lock(ro, F_SETLK, F_RDLCK, -5, 1).0 == -EINVAL, "a range starting before 0: EINVAL");+ close(ro);+ close(wa);++ // Two processes.+ let fd = open(path, O_RDWR);+ c.check(lock(fd, F_SETLK, F_WRLCK, 0, 10).0 == 0, "parent: write lock on bytes 0..10");+ let (r, fl) = lock(fd, F_GETLK, F_WRLCK, 0, 10);+ c.check(r == 0 && fl.l_type == F_UNLCK, "F_GETLK: my own locks never conflict");+ let parent = getpid();+ let (rd, wr) = pipe().unwrap();+ let child = fork();+ if child == 0 {+ let mut k = Checks::new("libctest-child");+ let fd = open(path, O_RDWR);+ let (r, fl) = lock(fd, F_GETLK, F_RDLCK, 5, 1);+ k.check(r == 0 && fl.l_type == F_WRLCK && fl.l_pid as i64 == parent && fl.l_start == 0 && fl.l_len == 10, "child: F_GETLK names the parent's lock");+ k.check(lock(fd, F_SETLK, F_RDLCK, 9, 5).0 == -EAGAIN, "child: overlapping F_SETLK: EAGAIN");+ k.check(lock(fd, F_SETLK, F_WRLCK, 10, 10).0 == 0, "child: bytes 10..20 are free");+ write(wr, b"x"); // tell the parent we're about to wait+ let t0 = now_us();+ let r = lock(fd, F_SETLKW, F_WRLCK, 0, 10).0;+ k.check(r == 0 && now_us() - t0 >= 50_000, "child: F_SETLKW waits until the parent's lock goes away");+ // The parent now asks for 10..20 (ours) with F_SETLKW and sleeps;+ // us asking for its 30..40 would close the circle: EDEADLK.+ sleep_ms(100);+ k.check(lock(fd, F_SETLKW, F_WRLCK, 30, 5).0 == -EDEADLK, "child: a lock wait that would deadlock: EDEADLK");+ exit(k.done()); // exiting drops all our locks+ }+ let mut b = [0u8; 1];+ read(rd, &mut b);+ sleep_ms(100);+ c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: write lock on 30..40");+ let fd2 = open(path, O_RDONLY);+ close(fd2); // closing *any* fd of the file drops my locks on it+ // (the child now gets 0..10; we wait for its 10..20, which it holds+ // until it exits)+ c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: 30..40 again (the close dropped it)");+ let r = lock(fd, F_SETLKW, F_WRLCK, 10, 10).0;+ c.check(r == 0, "parent: F_SETLKW on the child's range succeeds once it exits");+ c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "child's checks passed");+ c.check(lock(fd, F_SETLK, F_WRLCK, 0, 0).0 == 0, "the whole file is free after the child exited");+ c.check(lock(fd, F_SETLK, F_UNLCK, 0, 0).0 == 0, "F_UNLCK");+ close(fd);+ close(rd);+ close(wr);+ unlink(path);++ // RLIMIT_NOFILE is the fd table's real size; F_DUPFD honours it.+ let mut lim = [0u64; 2];+ syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, lim.as_mut_ptr() as u64, 0);+ c.check(lim == [64, 64], "RLIMIT_NOFILE is 64 (the fd table size)");+ c.check(fcntl(0, F_DUPFD, 64) == -EINVAL, "F_DUPFD at 64: EINVAL");+ let small = [8u64, 64];+ syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, small.as_ptr() as u64, 0);+ c.check(fcntl(0, F_DUPFD, 8) == -EINVAL, "after setrlimit(NOFILE, 8): F_DUPFD at 8 is EINVAL");+ let mut fds = alloc::vec::Vec::new();+ loop {+ let fd = dup(0);+ if fd < 0 {+ c.check(fd == -EMFILE && fds.iter().all(|&f| f < 8), "dup fails with EMFILE at the soft limit");+ break;+ }+ fds.push(fd);+ }+ for fd in fds {+ close(fd);+ }+ let back = [64u64, 64];+ c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, back.as_ptr() as u64, 0) == 0, "raising it back up to the hard limit");+}++// --- files -------------------------------------------------------------------++fn files(c: &mut Checks) {+ // truncate / ftruncate on ramfs, ext2, FAT.+ for dir in ["/tmp", "/mnt/disk1", "/mnt/disk0"] {+ let path = alloc::format!("{}/trunc16.txt", dir);+ write_file(&path, b"hello world");+ let fd = open(&path, O_RDWR);+ c.check(syscall3(SYS_FTRUNCATE, fd as u64, 5, 0) == 0 && file_size(&path) == 5, &alloc::format!("{}: ftruncate to 5 bytes", dir));+ c.check(syscall3(SYS_FTRUNCATE, fd as u64, 9000, 0) == 0 && file_size(&path) == 9000, &alloc::format!("{}: ftruncate grows to 9000", dir));+ let mut buf = [0xffu8; 16];+ let n = syscall6(SYS_PREAD64_NR, fd as u64, buf.as_mut_ptr() as u64, 16, 0, 0, 0);+ c.check(n == 16 && &buf[..5] == b"hello" && buf[5..].iter().all(|&b| b == 0), &alloc::format!("{}: the old bytes stay, the new ones are zeros", dir));+ c.check(syscall3(SYS_FSYNC, fd as u64, 0, 0) == 0 && syscall3(SYS_FDATASYNC, fd as u64, 0, 0) == 0, &alloc::format!("{}: fsync, fdatasync: 0", dir));+ close(fd);+ c.check(path_call(SYS_TRUNCATE, &path, 3) == 0 && file_size(&path) == 3, &alloc::format!("{}: truncate(path, 3)", dir));+ let ro = open(&path, O_RDONLY);+ c.check(syscall3(SYS_FTRUNCATE, ro as u64, 0, 0) == -EINVAL, &alloc::format!("{}: ftruncate of a read-only fd: EINVAL", dir));+ c.check(syscall3(SYS_FTRUNCATE, ro as u64, -1i64 as u64, 0) == -EINVAL, &alloc::format!("{}: negative length: EINVAL", dir));+ close(ro);+ unlink(&path);+ }+ c.check(path_call(SYS_TRUNCATE, "/tmp", 0) == -EISDIR, "truncate of a directory: EISDIR");+ c.check(path_call(SYS_TRUNCATE, "/mnt/disk3/hello.txt", 0) == -EROFS, "truncate on btrfs (read-only here): EROFS");+ c.check(path_call(SYS_TRUNCATE, "/tmp/nope16", 0) == -ENOENT, "truncate of a missing file: ENOENT");+ let (rd, wr) = pipe().unwrap();+ c.check(syscall3(SYS_FSYNC, rd as u64, 0, 0) == -EINVAL, "fsync of a pipe: EINVAL");+ c.check(syscall3(SYS_FTRUNCATE, wr as u64, 0, 0) == -EINVAL, "ftruncate of a pipe: EINVAL");+ close(rd);+ close(wr);++ // umask: taken away from open(O_CREAT) and mkdir modes; per process.+ let old = umask(0o077);+ c.check(old == 0o022, "umask starts as 022 and returns the old value");+ write_file("/tmp/mask16", b"x");+ c.check(perm("/tmp/mask16") == 0o600, "with umask 077, open(O_CREAT, 0644) makes 0600");+ with_cpath("/tmp/maskdir16", |p| syscall3(SYS_MKDIR, p, 0o777, 0));+ c.check(perm("/tmp/maskdir16") == 0o700, "and mkdir(0777) makes 0700");+ let child = fork();+ if child == 0 {+ exit(if umask(0) == 0o077 { 0 } else { 1 });+ }+ c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits the umask");+ c.check(umask(old) == 0o077, "umask(022) gives back 077");+ unlink("/tmp/mask16");+ with_cpath("/tmp/maskdir16", |p| syscall3(SYS_RMDIR, p, 0, 0));++ // chmod family.+ for dir in ["/tmp", "/mnt/disk1"] {+ let path = alloc::format!("{}/mode16", dir);+ write_file(&path, b"m");+ c.check(path_call(SYS_CHMOD, &path, 0o751) == 0 && perm(&path) == 0o751, &alloc::format!("{}: chmod 751", dir));+ let fd = open(&path, O_RDONLY);+ c.check(syscall3(SYS_FCHMOD, fd as u64, 0, 0) == 0 && perm(&path) == 0, &alloc::format!("{}: fchmod 000 (a real 0, not 'unknown')", dir));+ close(fd);+ let name = cstr("mode16");+ let dfd = open(dir, O_RDONLY | O_DIRECTORY);+ c.check(sys4(SYS_FCHMODAT, dfd as u64, name.as_ptr() as u64, 0o4644, 0) == 0 && perm(&path) == 0o4644, &alloc::format!("{}: fchmodat relative to a dir fd, setuid bit kept", dir));+ close(dfd);+ unlink(&path);+ }+ c.check(path_call(SYS_CHMOD, "/mnt/disk0/readme.txt", 0o600) == -EPERM, "chmod on FAT (no permissions to keep): EPERM");+ c.check(path_call(SYS_CHMOD, "/mnt/disk3/hello.txt", 0o600) == -EROFS, "chmod on btrfs: EROFS");+ // (Rustbelt can't make symlinks yet; btrfs has some.)+ let link = cstr("/mnt/disk3/hello-link");+ let r = sys4(452, AT_FDCWD as u64, link.as_ptr() as u64, 0o700, AT_SYMLINK_NOFOLLOW);+ c.check(r == -EOPNOTSUPP, "fchmodat2(AT_SYMLINK_NOFOLLOW) on a symlink: EOPNOTSUPP (like Linux)");++ // utimensat + stat times.+ for (dir, nsec_kept) in [("/tmp", true), ("/mnt/disk1", false)] {+ let path = alloc::format!("{}/times16", dir);+ write_file(&path, b"t");+ let mut st = Stat::default();+ stat(&path, &mut st);+ c.check(st.mtime[0] > 1_000_000_000, &alloc::format!("{}: a new file's mtime is now (not 1970)", dir));+ let p = cstr(&path);+ let times = [[1000i64, 5], [2000, 6]];+ let r = sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, times.as_ptr() as u64, 0);+ stat(&path, &mut st);+ let want = if nsec_kept { ([1000, 5], [2000, 6]) } else { ([1000, 0], [2000, 0]) };+ c.check(r == 0 && st.atime == want.0 && st.mtime == want.1, &alloc::format!("{}: utimensat sets atime and mtime", dir));+ c.check(st.ctime[0] > 1_000_000_000, &alloc::format!("{}: ... and ctime becomes now", dir));+ let omit = [[0i64, UTIME_OMIT], [3000, 0]];+ sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, omit.as_ptr() as u64, 0);+ stat(&path, &mut st);+ c.check(st.atime[0] == 1000 && st.mtime[0] == 3000, &alloc::format!("{}: UTIME_OMIT leaves atime alone", dir));+ let fd = open(&path, O_RDONLY);+ let now = [[0i64, UTIME_NOW], [0, UTIME_NOW]];+ c.check(sys4(SYS_UTIMENSAT, fd as u64, 0, now.as_ptr() as u64, 0) == 0, &alloc::format!("{}: futimens (NULL path, an fd): 0", dir));+ fstat(fd, &mut st);+ c.check(st.atime[0] > 1_000_000_000 && st.mtime[0] > 1_000_000_000, &alloc::format!("{}: UTIME_NOW, seen by fstat", dir));+ close(fd);+ unlink(&path);+ }+ let p = cstr("/tmp");+ let bad = [[0i64, 1_000_000_000], [0, 0]];+ c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, bad.as_ptr() as u64, 0) == -EINVAL, "utimensat with tv_nsec = 1e9: EINVAL");+ c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, 0, 0x8) == -EINVAL, "unknown flags: EINVAL");+ let h = cstr("/mnt/disk3/hello.txt");+ c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, h.as_ptr() as u64, 0, 0) == -EROFS, "utimensat on btrfs: EROFS");+ let mut st = Stat::default();+ stat("/mnt/disk3/hello.txt", &mut st);+ c.check(st.mtime[0] > 1_000_000_000, "btrfs files report their real mtime");+}++const SYS_PREAD64_NR: u64 = 17;++// --- misc --------------------------------------------------------------------++fn misc(c: &mut Checks) {+ // rlimits+ let mut lim = [0u64; 2];+ c.check(syscall3(SYS_GETRLIMIT, RLIMIT_STACK, lim.as_mut_ptr() as u64, 0) == 0 && lim[0] > 0 && lim[0] == lim[1], "RLIMIT_STACK: the real (fixed) stack size");+ c.check(syscall3(SYS_GETRLIMIT, 99, lim.as_mut_ptr() as u64, 0) == -EINVAL, "an unknown resource: EINVAL");+ let up = [64u64, 1000];+ c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, up.as_ptr() as u64, 0) == -EPERM, "raising NOFILE's hard limit past 64: EPERM");+ let inverted = [10u64, 5];+ c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, inverted.as_ptr() as u64, 0) == -EINVAL, "soft above hard: EINVAL");+ let new = [32u64, 64];+ let mut old = [0u64; 2];+ let r = sys4(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, new.as_ptr() as u64, old.as_mut_ptr() as u64);+ c.check(r == 0 && old == [64, 64], "prlimit64 sets and returns the old value");+ let child = fork();+ if child == 0 {+ let mut l = [0u64; 2];+ syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, l.as_mut_ptr() as u64, 0);+ exit(if l == [32, 64] { 0 } else { 1 });+ }+ c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits rlimits");+ let back = [64u64, 64];+ sys4(SYS_PRLIMIT64, getpid() as u64, RLIMIT_NOFILE, back.as_ptr() as u64, 0);+ c.check(sys4(SYS_PRLIMIT64, 99_999, RLIMIT_NOFILE, 0, old.as_mut_ptr() as u64) == -ESRCH, "prlimit64 of a missing pid: ESRCH");++ // prctl+ let name = cstr("libctest-worker-long");+ c.check(syscall3(SYS_PRCTL, 15, name.as_ptr() as u64, 0) == 0, "PR_SET_NAME");+ let mut got = [0xffu8; 16];+ syscall3(SYS_PRCTL, 16, got.as_mut_ptr() as u64, 0);+ c.check(&got[..15] == b"libctest-worker" && got[15] == 0, "PR_GET_NAME: cut to 15 bytes + NUL");+ let mut sig = -1i32;+ syscall3(SYS_PRCTL, 1, SIGTERM as u64, 0);+ syscall3(SYS_PRCTL, 2, &mut sig as *mut i32 as u64, 0);+ c.check(sig == SIGTERM, "PR_SET/GET_PDEATHSIG");+ c.check(syscall3(SYS_PRCTL, 1, 99, 0) == -EINVAL, "PR_SET_PDEATHSIG 99: EINVAL");+ syscall3(SYS_PRCTL, 1, 0, 0);+ c.check(syscall3(SYS_PRCTL, 3, 0, 0) == 1, "PR_GET_DUMPABLE: 1");+ c.check(syscall3(SYS_PRCTL, 4, 2, 0) == -EINVAL, "PR_SET_DUMPABLE 2: EINVAL");+ c.check(syscall5(SYS_PRCTL, 38, 1, 0, 0, 0) == 0 && syscall5(SYS_PRCTL, 39, 0, 0, 0, 0) == 1, "PR_SET/GET_NO_NEW_PRIVS");+ c.check(syscall3(SYS_PRCTL, 0x5555, 0, 0) == -EINVAL, "an unknown prctl: EINVAL");++ // sched_getaffinity: every CPU, returns the bytes written.+ let mut mask = [0u64; 2];+ let r = syscall3(SYS_SCHED_GETAFFINITY, 0, 16, mask.as_mut_ptr() as u64);+ let cpus = sysinfo().cpus as u32;+ c.check(r == 8 && mask[0].count_ones() == cpus && cpus >= 1, &alloc::format!("sched_getaffinity: {} CPU bits, returns 8", cpus));+ c.check(syscall3(SYS_SCHED_GETAFFINITY, 0, 4, mask.as_mut_ptr() as u64) == -EINVAL, "sched_getaffinity with 4 bytes: EINVAL");++ // madvise+ let page = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ unsafe { core::ptr::write_bytes(page as *mut u8, 0xaa, 8192) };+ c.check(syscall3(SYS_MADVISE, page, 8192, 3) == 0, "MADV_WILLNEED: 0 (a hint)");+ c.check(syscall3(SYS_MADVISE, page, 8192, 4) == 0, "MADV_DONTNEED: 0");+ let zero = unsafe { core::slice::from_raw_parts(page as *const u8, 8192) }.iter().all(|&b| b == 0);+ c.check(zero, "after MADV_DONTNEED private anonymous pages read as zeros");+ c.check(syscall3(SYS_MADVISE, page + 1, 4096, 4) == -EINVAL, "unaligned address: EINVAL");+ c.check(syscall3(SYS_MADVISE, page, 4096, 999) == -EINVAL, "unknown advice: EINVAL");+ munmap(page, 8192);+ c.check(syscall3(SYS_MADVISE, page, 4096, 4) == -ENOMEM, "on unmapped memory: ENOMEM");++ // Robust futex lists.+ let mut head = [0u64; 3];+ c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 23, 0) == -EINVAL, "set_robust_list with len 23: EINVAL");+ c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0) == 0, "set_robust_list");+ let (mut h, mut l) = (0u64, 0u64);+ syscall3(SYS_GET_ROBUST_LIST, 0, &mut h as *mut u64 as u64, &mut l as *mut u64 as u64);+ c.check(h == head.as_ptr() as u64 && l == 24, "get_robust_list returns it");+ syscall3(SYS_SET_ROBUST_LIST, 0, 24, 0);+ // A child dies holding a "robust mutex" in shared memory: the kernel+ // marks the lock word FUTEX_OWNER_DIED, so the next locker knows.+ let shared = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) as u64;+ let word = shared as *mut u32;+ let entry = shared + 64; // the mutex's list node; lock word at entry - 64+ let child = fork();+ if child == 0 {+ let tid = syscall3(SYS_GETTID, 0, 0, 0) as u32;+ head[0] = entry; // list: head -> entry -> head+ head[1] = (-64i64) as u64; // futex_offset: from the node to the word+ head[2] = 0;+ unsafe {+ *(entry as *mut u64) = head.as_ptr() as u64;+ word.write_volatile(tid); // "locked by me"+ }+ syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0);+ exit(0); // ... and die holding it+ }+ wait(child);+ let w = unsafe { word.read_volatile() };+ c.check(w & FUTEX_OWNER_DIED != 0 && w & 0x3fff_ffff == 0, "a robust lock held by a dead thread: FUTEX_OWNER_DIED, owner 0");+ munmap(shared, 4096);+ head[0] = 0;+}++// --- orphans -----------------------------------------------------------------++fn orphans(c: &mut Checks) {+ // A child that leaves a running grandchild and a zombie grandchild+ // behind and exits without waiting for either.+ let child = fork();+ if child == 0 {+ if fork() == 0 {+ exit(0); // becomes a zombie: its parent never waits+ }+ if fork() == 0 {+ sleep_ms(40); // still running when its parent exits+ exit(0);+ }+ sleep_ms(10);+ exit(0);+ }+ c.check(matches!(wait4(child, 0), Ok((p, _)) if p == child), "the child exits without waiting for its own children");++ // With PR_SET_CHILD_SUBREAPER we adopt the orphan and can wait for it.+ c.check(syscall3(SYS_PRCTL, 36, 1, 0) == 0, "PR_SET_CHILD_SUBREAPER");+ let me = getpid();+ let child = fork();+ if child == 0 {+ if fork() == 0 {+ sleep_ms(40);+ exit(if getppid() == me { 5 } else { 6 }); // adopted by the subreaper?+ }+ exit(0);+ }+ c.check(matches!(wait4(child, 0), Ok((p, s)) if p == child && wexitstatus(s) == 0), "the middle process exits");+ let r = wait4(-1, 0);+ c.check(matches!(r, Ok((p, s)) if p != child && wexitstatus(s) == 5), "the subreaper reaps the orphaned grandchild, which saw getppid() = subreaper");+ c.check(wait4(-1, WNOHANG) == Err(-ECHILD), "no children left");+ sleep_ms(80); // let the first orphan finish before we exit+}
@@ -137,7 +137,7 @@ fn main() -> i64 { c.check(n > 0 && cwd[0] == b'/', "getcwd gives an absolute path"); let mut ub = [0u8; 6 * 65]; let u = uname(&mut ub);- c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 15"), "uname: Rustbelt, stage 15");+ c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 16"), "uname: Rustbelt, stage 16"); let (secs, _) = clock_gettime(0); c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");