//! 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 { 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 }