RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorEdward <edward@localhost>2026-10-31 16:03:26 -0700
commita2549ec94f2e8038864bad4255e7a965950fc947
parent5ba29e2307d0368b527aa8b12cd702643d9dd196
treebrowse 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).
Alibctest.rs+655 -0
Mnbtest.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)");