RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorEdward <edward@localhost>2026-10-31 16:49:32 -0700
commite6eb9917e47dda5cf57b32256ee0c85032c1c600
parenta2549ec94f2e8038864bad4255e7a965950fc947
treebrowse files at this commit
Stage 17: coreutils batch 2 (0.17.0)

The ENOSYS calls from Clarke's coreutils/busybox (tier 3) traces, in his
priority order. New calls live in src/syscall_ext.rs; timers in src/timer.rs.

- link/linkat, symlink/symlinkat (ramfs, ext2; EPERM on FAT/exFAT, EROFS on
  btrfs, EXDEV across mounts, EMLINK); native rename of full directories,
  renameat2 RENAME_NOREPLACE; real st_ino/st_nlink/st_dev/owner in stat
- chown/fchown/lchown/fchownat (stored on ramfs/ext2; vfat-like on FAT)
- statfs/fstatfs with Linux magic numbers, statx
- Linux sysinfo (99); ours stays 502, traced as rb_sysinfo
- times, getrusage, CPU-time clocks (10 ms sampling), clock_getres
- setsockopt options accepted/remembered, getsockopt, getsockname,
  getpeername, shutdown; socketpair -> EAFNOSUPPORT (no AF_UNIX)
- alarm, getitimer/setitimer (ITIMER_REAL), POSIX timers (timerd thread)
- real (unenforced) uid/gid sets, groups, fs ids, nice values,
  sethostname/setdomainname
- flock, preadv/pwritev, sendfile
- fixes: getdents64 skipped entries while deleting (rm -r); --trace shows
  the user-visible result of an interrupted call; sigsetsize checks for
  ppoll/pselect6/epoll_pwait; the headless frame report waits for all tasks
- user/coretest.rs + tests/coreutils_batch2.rs, README stage 17 chapter

Deferred: socketpair/AF_UNIX, timerfd, mremap, ITIMER_VIRTUAL/PROF.
Acoretest.rs+885 -0
Mnbtest.rs+1 -1
Mulib.rs+2 -2

3 files changed, 888 insertions, 3 deletions

@@ -0,0 +1,885 @@+//! coretest -- stage 17, "coreutils batch 2": the system calls coreutils+//! and busybox use beyond what libctest (stage 16) covers. Run with one+//! group name; exit code = number of failed checks:+//!   links    link/linkat, symlink/symlinkat on ramfs and ext2; EPERM on+//!            FAT/exFAT, EROFS on btrfs; rename over a second hard link+//!   owners   chown/fchown/lchown/fchownat (stored, not enforced)+//!   statfs   statfs/fstatfs magic numbers and sizes, statx, st_dev+//!   info     sysinfo (99), times, getrusage, CPU clocks, clock_getres+//!   sockets  getsockname/getpeername, get/setsockopt, shutdown, socketpair+//!   timers   alarm, setitimer/getitimer, timer_create & co.+//!   ids      setuid & co., groups, priority, sethostname+//!   misc     flock, preadv/pwritev, sendfile, ppoll/pselect6 masks,+//!            getdents while deleting+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::vec::Vec;+use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};+use ulib::*;++// Stage 17 syscall numbers (Linux's).+const SYS_SHUTDOWN: u64 = 48;+const SYS_GETSOCKNAME: u64 = 51;+const SYS_GETPEERNAME: u64 = 52;+const SYS_SOCKETPAIR: u64 = 53;+const SYS_GETSOCKOPT: u64 = 55;+const SYS_GETITIMER: u64 = 36;+const SYS_ALARM: u64 = 37;+const SYS_SETITIMER: u64 = 38;+const SYS_SENDFILE: u64 = 40;+const SYS_FLOCK: u64 = 73;+const SYS_LINK: u64 = 86;+const SYS_SYMLINK: u64 = 88;+const SYS_CHOWN: u64 = 92;+const SYS_FCHOWN: u64 = 93;+const SYS_LCHOWN: u64 = 94;+const SYS_GETRUSAGE: u64 = 98;+const SYS_SYSINFO: u64 = 99;+const SYS_TIMES: u64 = 100;+const SYS_GETGID: u64 = 104;+const SYS_SETUID: u64 = 105;+const SYS_SETGID: u64 = 106;+const SYS_GETEUID: u64 = 107;+const SYS_SETREUID: u64 = 113;+const SYS_GETGROUPS: u64 = 115;+const SYS_SETGROUPS: u64 = 116;+const SYS_SETRESUID: u64 = 117;+const SYS_GETRESUID: u64 = 118;+const SYS_GETRESGID: u64 = 120;+const SYS_STATFS: u64 = 137;+const SYS_FSTATFS: u64 = 138;+const SYS_GETPRIORITY: u64 = 140;+const SYS_SETPRIORITY: u64 = 141;+const SYS_SETHOSTNAME: u64 = 170;+const SYS_TIMER_CREATE: u64 = 222;+const SYS_TIMER_SETTIME: u64 = 223;+const SYS_TIMER_GETTIME: u64 = 224;+const SYS_TIMER_GETOVERRUN: u64 = 225;+const SYS_TIMER_DELETE: u64 = 226;+const SYS_CLOCK_GETRES: u64 = 229;+const SYS_FCHOWNAT: u64 = 260;+const SYS_LINKAT: u64 = 265;+const SYS_SYMLINKAT: u64 = 266;+const SYS_PSELECT6: u64 = 270;+const SYS_PREADV: u64 = 295;+const SYS_PWRITEV: u64 = 296;+const SYS_STATX: u64 = 332;++const AT_FDCWD: i64 = -100;+const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+const AT_SYMLINK_FOLLOW: u64 = 0x400;+const AT_EMPTY_PATH: u64 = 0x1000;+const O_RDWR: u64 = 2;+const EROFS: i64 = 30;+const EMLINK_OR_EPERM: [i64; 2] = [EPERM, 31];+const EACCES: i64 = 13;+const ENOTCONN: i64 = 107;+const ECONNREFUSED: i64 = 111;+const ENOPROTOOPT: i64 = 92;+const EAFNOSUPPORT: i64 = 97;+const EPIPE: i64 = 32;+const SIGALRM: i32 = 14;++fn cpath(s: &str) -> Vec<u8> {+    let mut v = Vec::from(s.as_bytes());+    v.push(0);+    v+}++fn p2(nr: u64, a: &str, b: &str) -> i64 {+    let (a, b) = (cpath(a), cpath(b));+    syscall3(nr, a.as_ptr() as u64, b.as_ptr() as u64, 0)+}++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 read_file(path: &str) -> Vec<u8> {+    let fd = open(path, O_RDONLY);+    let mut out = Vec::new();+    if fd < 0 {+        return out;+    }+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n <= 0 {+            break;+        }+        out.extend_from_slice(&buf[..n as usize]);+    }+    close(fd);+    out+}++fn st(path: &str) -> Stat {+    let mut s = Stat::default();+    if stat(path, &mut s) < 0 {+        s.ino = 0;+        s.nlink = 0;+    }+    s+}++fn lst(path: &str) -> Stat {+    let mut s = Stat::default();+    lstat(path, &mut s);+    s+}++/// Burn CPU in user mode for `ms` (looking at the clock only now and then,+/// so it's nearly all user time).+fn spin_ms(ms: u64) {+    let end = now_us() + ms * 1000;+    let mut x = 0u64;+    while now_us() < end {+        for _ in 0..200_000 {+            x = core::hint::black_box(x.wrapping_mul(31).wrapping_add(7));+        }+    }+}++fn main() -> i64 {+    let group = arg(1).unwrap_or("");+    let mut c = Checks::new("coretest");+    match group {+        "links" => links(&mut c),+        "owners" => owners(&mut c),+        "statfs" => statfs(&mut c),+        "info" => info(&mut c),+        "sockets" => sockets(&mut c),+        "timers" => timers(&mut c),+        "ids" => ids(&mut c),+        "misc" => misc(&mut c),+        _ => c.check(false, "usage: coretest links|owners|statfs|info|sockets|timers|ids|misc"),+    }+    c.done()+}++// --- links ---------------------------------------------------------------------++fn links_on(c: &mut Checks, dir: &str) {+    let (a, b, l, d) = (format!("{}/a.txt", dir), format!("{}/b.txt", dir), format!("{}/l", dir), format!("{}/d", dir));+    unlink(&a);+    unlink(&b);+    unlink(&l);+    rmdir(&d);+    write_file(&a, b"hello");+    c.check(p2(SYS_LINK, &a, &b) == 0, &format!("{}: link a.txt b.txt", dir));+    let (sa, sb) = (st(&a), st(&b));+    c.check(sa.ino == sb.ino && sa.ino != 0, &format!("{}: both names, one inode number", dir));+    c.check(sa.nlink == 2 && sb.nlink == 2, &format!("{}: st_nlink is 2", dir));+    let fd = open(&b, O_WRONLY);+    lseek(fd, 0, 2);+    write(fd, b" world");+    close(fd);+    c.check(read_file(&a) == b"hello world", &format!("{}: a write through one name shows through the other", dir));+    c.check(p2(SYS_LINK, &a, &b) == -EEXIST, &format!("{}: link onto an existing name: EEXIST", dir));+    // rename onto another hard link of the same file: nothing happens (POSIX).+    c.check(rename(&a, &b) == 0 && st(&a).nlink == 2 && st(&b).nlink == 2, &format!("{}: rename a.txt over its own link b.txt: both stay", dir));+    c.check(unlink(&a) == 0 && st(&b).nlink == 1 && read_file(&b) == b"hello world", &format!("{}: unlink one name, the data stays", dir));+    // symlinks+    c.check(p2(SYS_SYMLINK, "b.txt", &l) == 0, &format!("{}: symlink b.txt l", dir));+    let mut buf = [0u8; 64];+    let n = readlink(&l, &mut buf);+    c.check(n == 5 && &buf[..5] == b"b.txt", &format!("{}: readlink gives the target as written", dir));+    c.check(lst(&l).is_symlink() && st(&l).is_file() && st(&l).ino == st(&b).ino, &format!("{}: lstat sees the link, stat the file", dir));+    c.check(read_file(&l) == b"hello world", &format!("{}: open follows the symlink", dir));+    c.check(p2(SYS_SYMLINK, "x", &l) == -EEXIST, &format!("{}: symlink onto an existing name: EEXIST", dir));+    // linkat: the symlink itself, or (AT_SYMLINK_FOLLOW) what it points to.+    let (l2, l3) = (format!("{}/l2", dir), format!("{}/l3", dir));+    let (cl, cl2, cl3) = (cpath(&l), cpath(&l2), cpath(&l3));+    let r = syscall5(SYS_LINKAT, AT_FDCWD as u64, cl.as_ptr() as u64, AT_FDCWD as u64, cl2.as_ptr() as u64, 0);+    c.check(r == 0 && lst(&l2).is_symlink(), &format!("{}: linkat without FOLLOW links the symlink itself", dir));+    let r = syscall5(SYS_LINKAT, AT_FDCWD as u64, cl.as_ptr() as u64, AT_FDCWD as u64, cl3.as_ptr() as u64, AT_SYMLINK_FOLLOW);+    c.check(r == 0 && lst(&l3).is_file() && st(&b).nlink == 2, &format!("{}: linkat AT_SYMLINK_FOLLOW links the file", dir));+    // symlinkat relative to a directory fd; a dangling one is fine.+    mkdir(&d);+    let dfd = open(&d, O_RDONLY | O_DIRECTORY);+    let (t, name) = (cpath("../nowhere"), cpath("dangling"));+    c.check(syscall3(SYS_SYMLINKAT, t.as_ptr() as u64, dfd as u64, name.as_ptr() as u64) == 0, &format!("{}: symlinkat(dirfd) makes a dangling link", dir));+    c.check(lst(&format!("{}/dangling", d)).is_symlink() && open(&format!("{}/dangling", d), O_RDONLY) == -ENOENT, &format!("{}: ...which lstat sees and open can't follow", dir));+    close(dfd);+    c.check(p2(SYS_LINK, &d, &format!("{}/dlink", dir)) == -EPERM, &format!("{}: hard link to a directory: EPERM", dir));+    // A directory moved into another one (native rename).+    let e = format!("{}/e", dir);+    rmdir(&e);+    mkdir(&e);+    c.check(rename(&d, &format!("{}/d", e)) == 0 && lst(&format!("{}/d/dangling", e)).is_symlink(), &format!("{}: rename moves a full directory", dir));+    // A long target (ext2 keeps short ones in the inode, long ones in a block).+    let long: alloc::string::String = core::iter::repeat('x').take(100).collect();+    let ll = format!("{}/long", dir);+    unlink(&ll);+    c.check(p2(SYS_SYMLINK, &long, &ll) == 0 && readlink(&ll, &mut [0u8; 200]) == 100, &format!("{}: a 100-byte symlink target", dir));+    // clean up+    for f in [&b, &l, &l2, &l3, &ll, &format!("{}/d/dangling", e)] {+        unlink(f);+    }+    rmdir(&format!("{}/d", e));+    rmdir(&e);+    c.check(access(&b) == -ENOENT && access(&e) == -ENOENT, &format!("{}: all cleaned up", dir));+}++fn links(c: &mut Checks) {+    mkdir("/tmp/lk");+    links_on(c, "/tmp/lk");+    rmdir("/tmp/lk");+    mkdir("/mnt/disk1/lk");+    links_on(c, "/mnt/disk1/lk");+    rmdir("/mnt/disk1/lk");+    // Across file systems: EXDEV.+    write_file("/tmp/x.txt", b"x");+    c.check(p2(SYS_LINK, "/tmp/x.txt", "/mnt/disk1/x.txt") == -EXDEV, "link across file systems: EXDEV");+    unlink("/tmp/x.txt");+    // FAT and exFAT have no links on disk; btrfs is mounted read-only.+    write_file("/mnt/disk0/lk.txt", b"x");+    c.check(EMLINK_OR_EPERM.contains(&-p2(SYS_LINK, "/mnt/disk0/lk.txt", "/mnt/disk0/lk2.txt")), "FAT: link is EPERM");+    c.check(p2(SYS_SYMLINK, "lk.txt", "/mnt/disk0/lk3") == -EPERM, "FAT: symlink is EPERM");+    unlink("/mnt/disk0/lk.txt");+    write_file("/mnt/disk2/lk.txt", b"x");+    c.check(p2(SYS_SYMLINK, "lk.txt", "/mnt/disk2/lk3") == -EPERM, "exFAT: symlink is EPERM");+    unlink("/mnt/disk2/lk.txt");+    c.check(p2(SYS_LINK, "/mnt/disk3/hello.txt", "/mnt/disk3/h2") == -EROFS, "btrfs: link is EROFS");+    c.check(p2(SYS_SYMLINK, "hello.txt", "/mnt/disk3/h3") == -EROFS, "btrfs: symlink is EROFS");+    let s = st("/mnt/disk3/hello.txt");+    c.check(s.nlink >= 1 && s.ino != 0, "btrfs: stat shows the real link count and inode");+}++// --- owners --------------------------------------------------------------------++fn chown(nr: u64, path: &str, uid: u32, gid: u32) -> i64 {+    let p = cpath(path);+    syscall3(nr, p.as_ptr() as u64, uid as u64, gid as u64)+}++fn owners(c: &mut Checks) {+    for dir in ["/tmp", "/mnt/disk1"] {+        let (f, l) = (format!("{}/own.txt", dir), format!("{}/own.lnk", dir));+        unlink(&f);+        unlink(&l);+        write_file(&f, b"x");+        p2(SYS_SYMLINK, "own.txt", &l);+        c.check(chown(SYS_CHOWN, &f, 1000, 100) == 0, &format!("{}: chown 1000:100", dir));+        let s = st(&f);+        c.check(s.uid == 1000 && s.gid == 100, &format!("{}: stat shows 1000:100", dir));+        c.check(chown(SYS_CHOWN, &f, u32::MAX, 50) == 0 && st(&f).uid == 1000 && st(&f).gid == 50, &format!("{}: -1 leaves the uid alone", dir));+        c.check(chown(SYS_LCHOWN, &l, 7, 7) == 0 && lst(&l).uid == 7 && st(&f).uid == 1000, &format!("{}: lchown changes the link, not the file", dir));+        c.check(chown(SYS_CHOWN, &l, 8, 8) == 0 && st(&f).uid == 8 && lst(&l).uid == 7, &format!("{}: chown through a link changes the file", dir));+        let fd = open(&f, O_RDONLY);+        c.check(syscall3(SYS_FCHOWN, fd as u64, 9, 9) == 0 && st(&f).uid == 9, &format!("{}: fchown", dir));+        let empty = cpath("");+        let r = syscall5(SYS_FCHOWNAT, fd as u64, empty.as_ptr() as u64, 0, 0, AT_EMPTY_PATH);+        c.check(r == 0 && st(&f).uid == 0, &format!("{}: fchownat(fd, \"\", AT_EMPTY_PATH)", dir));+        close(fd);+        let name = cpath("own.lnk");+        let dfd = open(dir, O_RDONLY | O_DIRECTORY);+        let r = syscall5(SYS_FCHOWNAT, dfd as u64, name.as_ptr() as u64, 3, 3, AT_SYMLINK_NOFOLLOW);+        c.check(r == 0 && lst(&l).uid == 3 && st(&f).uid == 0, &format!("{}: fchownat(dirfd, AT_SYMLINK_NOFOLLOW)", dir));+        close(dfd);+        unlink(&f);+        unlink(&l);+    }+    // Directories too.+    mkdir("/tmp/ownd");+    c.check(chown(SYS_CHOWN, "/tmp/ownd", 5, 6) == 0 && st("/tmp/ownd").uid == 5, "chown a directory");+    rmdir("/tmp/ownd");+    // FAT: no owners on disk. Root:root (what it shows) is fine, anything else EPERM.+    write_file("/mnt/disk0/own.txt", b"x");+    c.check(chown(SYS_CHOWN, "/mnt/disk0/own.txt", 0, 0) == 0, "FAT: chown to 0:0 (no change) succeeds");+    c.check(chown(SYS_CHOWN, "/mnt/disk0/own.txt", 1000, 1000) == -EPERM, "FAT: chown to 1000 is EPERM");+    unlink("/mnt/disk0/own.txt");+    write_file("/mnt/disk2/own.txt", b"x");+    c.check(chown(SYS_CHOWN, "/mnt/disk2/own.txt", 1000, u32::MAX) == -EPERM, "exFAT: chown to 1000 is EPERM");+    unlink("/mnt/disk2/own.txt");+    c.check(chown(SYS_CHOWN, "/mnt/disk3/hello.txt", 1, 1) == -EROFS, "btrfs: chown is EROFS");+    c.check(chown(SYS_CHOWN, "/tmp/missing", 1, 1) == -ENOENT, "chown of a missing file: ENOENT");+    // Pipes: nothing to own, accepted.+    let (r, w) = pipe().unwrap();+    c.check(syscall3(SYS_FCHOWN, r as u64, 1, 1) == 0, "fchown on a pipe: accepted");+    close(r);+    close(w);+}++// --- statfs / statx --------------------------------------------------------------++fn statfs_of(path: &str) -> (i64, [u64; 15]) {+    let mut buf = [0u64; 15];+    let p = cpath(path);+    (syscall3(SYS_STATFS, p.as_ptr() as u64, buf.as_mut_ptr() as u64, 0), buf)+}++fn statfs(c: &mut Checks) {+    let cases: [(&str, u64, &str); 6] = [+        ("/", 0x8584_58f6, "ramfs"),+        ("/tmp", 0x8584_58f6, "ramfs"),+        ("/mnt/disk0", 0x4d44, "FAT (MSDOS_SUPER_MAGIC)"),+        ("/mnt/disk1/", 0xEF53, "ext2"),+        ("/mnt/disk2", 0x2011_BAB0, "exFAT"),+        ("/mnt/disk3/hello.txt", 0x9123_683E, "btrfs"),+    ];+    for (path, magic, what) in cases {+        let (r, b) = statfs_of(path);+        c.check(r == 0 && b[0] == magic, &format!("statfs {}: f_type = {}", path, what));+        if path.starts_with("/mnt") {+            c.check(b[1] >= 512 && b[2] > 0 && b[3] <= b[2] && b[4] <= b[3], &format!("statfs {}: sizes make sense (bsize {}, {} blocks, {} free)", path, b[1], b[2], b[3]));+            c.check(b[8] >= 255, &format!("statfs {}: f_namelen {}", path, b[8]));+        }+    }+    let (_, b) = statfs_of("/mnt/disk3");+    c.check(b[10] & 1 == 1, "btrfs: ST_RDONLY in f_flags");+    let (_, b) = statfs_of("/mnt/disk1");+    c.check(b[10] & 1 == 0 && b[5] > 0 && b[6] <= b[5], "ext2: writable, counts inodes");+    // Writing a file uses up free blocks on ext2.+    let (_, before) = statfs_of("/mnt/disk1");+    write_file("/mnt/disk1/sf.bin", &[7u8; 64 * 1024]);+    let (_, after) = statfs_of("/mnt/disk1");+    c.check(after[3] < before[3], "ext2: a 64 KiB file lowers f_bfree");+    unlink("/mnt/disk1/sf.bin");+    c.check(statfs_of("/nope").0 == -ENOENT, "statfs of a missing path: ENOENT");+    let fd = open("/mnt/disk0", O_RDONLY | O_DIRECTORY);+    let mut b = [0u64; 15];+    c.check(syscall3(SYS_FSTATFS, fd as u64, b.as_mut_ptr() as u64, 0) == 0 && b[0] == 0x4d44, "fstatfs on a FAT directory fd");+    close(fd);+    let (r, w) = pipe().unwrap();+    c.check(syscall3(SYS_FSTATFS, r as u64, b.as_mut_ptr() as u64, 0) == 0 && b[0] == 0x5049_5045, "fstatfs on a pipe: PIPEFS_MAGIC");+    close(r);+    close(w);+    // st_dev tells file systems apart.+    c.check(st("/tmp").dev != st("/mnt/disk1").dev && st("/mnt/disk1/").dev == st("/mnt/disk1/lost+found").dev, "st_dev: one per file system");+    // statx+    write_file("/tmp/sx.txt", b"twelve bytes");+    let mut sx = [0u8; 256];+    let p = cpath("/tmp/sx.txt");+    let r = syscall5(SYS_STATX, AT_FDCWD as u64, p.as_ptr() as u64, 0, 0x7ff, sx.as_mut_ptr() as u64);+    let u32at = |b: &[u8], o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());+    let u64at = |b: &[u8], o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());+    let s = st("/tmp/sx.txt");+    c.check(r == 0 && u32at(&sx, 0) & 0x7ff == 0x7ff, "statx: basic fields in stx_mask");+    c.check(u64at(&sx, 40) == 12 && u64at(&sx, 32) == s.ino && u32at(&sx, 16) == 1, "statx: size, inode number, nlink match stat");+    c.check(u32::from(u16::from_le_bytes([sx[28], sx[29]])) == s.mode && u64at(&sx, 112) as i64 == s.mtime[0], "statx: mode and mtime match stat");+    c.check(u32at(&sx, 140) as u64 == s.dev, "statx: device minor = st_dev");+    let fd = open("/tmp/sx.txt", O_RDONLY);+    let empty = cpath("");+    let r = syscall5(SYS_STATX, fd as u64, empty.as_ptr() as u64, AT_EMPTY_PATH, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(r == 0 && u64at(&sx, 40) == 12, "statx(fd, \"\", AT_EMPTY_PATH)");+    close(fd);+    let r = syscall5(SYS_STATX, AT_FDCWD as u64, p.as_ptr() as u64, 0x4, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(r == -EINVAL, "statx with an unknown flag: EINVAL");+    p2(SYS_SYMLINK, "sx.txt", "/tmp/sx.lnk");+    let pl = cpath("/tmp/sx.lnk");+    syscall5(SYS_STATX, AT_FDCWD as u64, pl.as_ptr() as u64, AT_SYMLINK_NOFOLLOW, 0x7ff, sx.as_mut_ptr() as u64);+    c.check(u32::from(u16::from_le_bytes([sx[28], sx[29]])) & S_IFMT == S_IFLNK, "statx AT_SYMLINK_NOFOLLOW: the link itself");+    unlink("/tmp/sx.lnk");+    unlink("/tmp/sx.txt");+}++// --- info: sysinfo, times, getrusage, clocks ---------------------------------------++fn clock_ns(clock: u64) -> (i64, u64) {+    let mut ts = [0u64; 2];+    let r = syscall3(SYS_CLOCK_GETTIME, clock, ts.as_mut_ptr() as u64, 0);+    (r, ts[0] * 1_000_000_000 + ts[1])+}++fn info(c: &mut Checks) {+    let mut si = [0u64; 14];+    c.check(syscall3(SYS_SYSINFO, si.as_mut_ptr() as u64, 0, 0) == 0, "sysinfo (99): 0");+    let procs = si[10] & 0xffff;+    let mem_unit = si[13] & 0xffff_ffff;+    c.check(si[0] > 0 && si[4] > si[5] && si[5] > 0, "sysinfo: uptime, totalram > freeram > 0");+    c.check(procs >= 1 && mem_unit == 1, "sysinfo: procs >= 1, mem_unit 1");+    c.check(sysinfo().cpus == 4, "Rustbelt's own sysinfo (502) still works");+    // CPU time: spin 300 ms, it shows up everywhere.+    let mut tms = [0u64; 4];+    let t0 = syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    let (_, p0) = clock_ns(2);+    spin_ms(300);+    let t1 = syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    c.check(t1 - t0 >= 25 && t1 - t0 <= 60, &format!("times: returns ticks (100/s): {} after 300 ms", t1 - t0));+    c.check(tms[0] + tms[1] >= 15, &format!("times: utime+stime {} ticks after spinning 300 ms", tms[0] + tms[1]));+    c.check(tms[0] >= tms[1], "times: spinning is mostly user time");+    let (r, p1) = clock_ns(2);+    c.check(r == 0 && p1 - p0 >= 150_000_000, "CLOCK_PROCESS_CPUTIME_ID advanced >= 150 ms");+    let (r, t) = clock_ns(3);+    c.check(r == 0 && t >= 150_000_000, "CLOCK_THREAD_CPUTIME_ID too");+    let mut ru = [0u64; 18];+    c.check(syscall3(SYS_GETRUSAGE, 0, ru.as_mut_ptr() as u64, 0) == 0, "getrusage(RUSAGE_SELF)");+    c.check(ru[0] * 1_000_000 + ru[1] >= 150_000 && ru[4] > 0, "getrusage: ru_utime >= 150 ms, ru_maxrss > 0");+    // Children's times count once they're reaped.+    let pid = fork();+    if pid == 0 {+        spin_ms(200);+        exit(0);+    }+    wait4(pid, 0).ok();+    syscall3(SYS_TIMES, tms.as_mut_ptr() as u64, 0, 0);+    c.check(tms[2] + tms[3] >= 10, &format!("times: the reaped child's {} ticks are in cutime+cstime", tms[2] + tms[3]));+    syscall3(SYS_GETRUSAGE, (-1i64) as u64, ru.as_mut_ptr() as u64, 0);+    c.check(ru[0] * 1_000_000 + ru[1] + ru[2] * 1_000_000 + ru[3] >= 100_000, "getrusage(RUSAGE_CHILDREN) has it too");+    c.check(syscall3(SYS_GETRUSAGE, 5, ru.as_mut_ptr() as u64, 0) == -EINVAL, "getrusage(5): EINVAL");+    let mut res = [0u64; 2];+    c.check(syscall3(SYS_CLOCK_GETRES, 1, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 1000], "clock_getres(MONOTONIC): 1 µs");+    c.check(syscall3(SYS_CLOCK_GETRES, 2, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 10_000_000], "clock_getres(PROCESS_CPUTIME): 10 ms");+    c.check(syscall3(SYS_CLOCK_GETRES, 0, 0, 0) == 0, "clock_getres with a NULL pointer: 0");+    c.check(syscall3(SYS_CLOCK_GETRES, 42, res.as_mut_ptr() as u64, 0) == -EINVAL, "clock_getres(42): EINVAL");+}++// --- sockets -----------------------------------------------------------------------++fn getsockopt_int(fd: i64, level: u64, opt: u64) -> (i64, u32) {+    let mut v = 0u32;+    let mut len = 4u32;+    let r = syscall5(SYS_GETSOCKOPT, fd as u64, level, opt, &mut v as *mut u32 as u64, &mut len as *mut u32 as u64);+    (r, v)+}++fn setsockopt_int(fd: i64, level: u64, opt: u64, v: u32) -> i64 {+    syscall5(SYS_SETSOCKOPT, fd as u64, level, opt, &v as *const u32 as u64, 4)+}++fn sockname(nr: u64, fd: i64) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let mut len = 16u32;+    let r = syscall3(nr, fd as u64, &mut sa as *mut _ as u64, &mut len as *mut u32 as u64);+    (r, sa)+}++fn sockets(c: &mut Checks) {+    sigaction_raw(13, 1, 0, 0); // SIGPIPE: SIG_IGN, so a write after shutdown is just EPIPE+    let srv = socket(SOCK_STREAM, 0);+    c.check(setsockopt_int(srv, 1, 2, 1) == 0, "setsockopt SO_REUSEADDR: 0 (was ENOPROTOOPT)");+    c.check(getsockopt_int(srv, 1, 2) == (0, 1), "getsockopt SO_REUSEADDR reads it back");+    c.check(setsockopt_int(srv, 1, 15, 1) == 0 && setsockopt_int(srv, 1, 9, 1) == 0, "SO_REUSEPORT, SO_KEEPALIVE: 0");+    c.check(setsockopt_int(srv, 6, 1, 1) == 0, "TCP_NODELAY: 0");+    c.check(setsockopt_int(srv, 1, 99, 1) == -ENOPROTOOPT, "an unknown option: ENOPROTOOPT");+    c.check(bind(srv, 7091) == 0 && listen(srv, 4) == 0, "bind 7091 + listen");+    let (r, sa) = sockname(SYS_GETSOCKNAME, srv);+    c.check(r == 0 && sa.port() == 7091 && sa.family == 2, "getsockname on the listener: port 7091");+    c.check(getsockopt_int(srv, 1, 30) == (0, 1), "SO_ACCEPTCONN: listening");+    c.check(sockname(SYS_GETPEERNAME, srv).0 == -ENOTCONN, "getpeername on a listener: ENOTCONN");+    let cl = socket(SOCK_STREAM, 0);+    c.check(syscall3(SYS_SHUTDOWN, cl as u64, 1, 0) == -ENOTCONN, "shutdown before connecting: ENOTCONN");+    c.check(connect(cl, Ip([127, 0, 0, 1]), 7091) == 0, "connect to 127.0.0.1:7091");+    let (conn, _) = accept(srv);+    c.check(conn >= 0, "accept");+    let (r, me) = sockname(SYS_GETSOCKNAME, cl);+    let (r2, peer) = sockname(SYS_GETPEERNAME, conn);+    c.check(r == 0 && r2 == 0 && me.addr == [127, 0, 0, 1] && me.port() == peer.port() && me.port() != 0, "client's getsockname = server's getpeername");+    let (_, p) = sockname(SYS_GETPEERNAME, cl);+    c.check(p.port() == 7091 && p.addr == [127, 0, 0, 1], "client's getpeername: 127.0.0.1:7091");+    c.check(getsockopt_int(cl, 1, 3) == (0, 1), "SO_TYPE: SOCK_STREAM");+    c.check(getsockopt_int(cl, 1, 4) == (0, 0), "SO_ERROR: 0");+    let (r, rcv) = getsockopt_int(cl, 1, 8);+    c.check(r == 0 && rcv > 0, "SO_RCVBUF > 0");+    // shutdown(SHUT_WR): the other side reads end-of-file, but can still answer.+    write(cl, b"ping");+    c.check(syscall3(SYS_SHUTDOWN, cl as u64, 1, 0) == 0, "shutdown(SHUT_WR)");+    let mut buf = [0u8; 16];+    let mut got = Vec::new();+    loop {+        let n = read(conn, &mut buf);+        if n <= 0 {+            c.check(n == 0, "server reads end-of-file after the data");+            break;+        }+        got.extend_from_slice(&buf[..n as usize]);+    }+    c.check(got == b"ping", "server got \"ping\"");+    c.check(write(cl, b"x") == -EPIPE, "client write after SHUT_WR: EPIPE");+    write(conn, b"pong");+    let n = read(cl, &mut buf);+    c.check(n == 4 && &buf[..4] == b"pong", "client still reads the answer");+    close(conn);+    close(cl);+    close(srv);+    // UDP+    let u = socket(SOCK_DGRAM, 0);+    c.check(getsockopt_int(u, 1, 3) == (0, 2), "UDP: SO_TYPE is SOCK_DGRAM");+    bind(u, 7092);+    c.check(sockname(SYS_GETSOCKNAME, u).1.port() == 7092, "UDP getsockname: 7092");+    close(u);+    // Nonblocking connect to a closed port: SO_ERROR says why.+    let nb = socket(SOCK_STREAM, 0);+    set_nonblocking(nb, true);+    let r = connect(nb, Ip([127, 0, 0, 1]), 7093);+    c.check(r == -EINPROGRESS || r == -ECONNREFUSED, "nonblocking connect to a closed port: EINPROGRESS");+    if r == -EINPROGRESS {+        let mut fds = [PollFd { fd: nb as i32, events: POLLOUT, revents: 0 }];+        poll(&mut fds, 1000);+        c.check(getsockopt_int(nb, 1, 4) == (0, ECONNREFUSED as u32), "SO_ERROR: ECONNREFUSED");+        c.check(getsockopt_int(nb, 1, 4) == (0, 0), "SO_ERROR is cleared once read");+    }+    close(nb);+    let mut sv = [0i32; 2];+    c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, sv.as_mut_ptr() as u64, 0) == -EAFNOSUPPORT, "socketpair(AF_UNIX): EAFNOSUPPORT (no AF_UNIX yet)");+    c.check(syscall3(SYS_SHUTDOWN, 0, 1, 0) == -88, "shutdown on a non-socket: ENOTSOCK");+}++// --- timers --------------------------------------------------------------------------++static ALRM: AtomicI64 = AtomicI64::new(0);+static TCODE: AtomicI64 = AtomicI64::new(0);+static TVAL: AtomicU64 = AtomicU64::new(0);++extern "C" fn on_alarm(_sig: i32) {+    ALRM.fetch_add(1, Ordering::SeqCst);+}++extern "C" fn on_timer(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {+    unsafe {+        TCODE.store((*info).code as i64, Ordering::SeqCst);+        TVAL.store(*((info as *const u8).add(24) as *const u64), Ordering::SeqCst);+    }+    ALRM.fetch_add(1, Ordering::SeqCst);+}++fn wait_for(n: i64, ms: u64) -> bool {+    let end = now_us() + ms * 1000;+    while ALRM.load(Ordering::SeqCst) < n && now_us() < end {+        sleep_ms(5);+    }+    ALRM.load(Ordering::SeqCst) >= n+}++/// Sleep the whole `ms`: a signal cuts nanosleep short (EINTR), so go back+/// to sleep until the time is really up.+fn sleep_through(ms: u64) {+    let end = now_us() + ms * 1000;+    loop {+        let now = now_us();+        if now >= end {+            return;+        }+        nanosleep_us(end - now);+    }+}++fn itimer(value_us: u64, interval_us: u64) -> [u64; 4] {+    let new = [interval_us / 1_000_000, interval_us % 1_000_000, value_us / 1_000_000, value_us % 1_000_000];+    let mut old = [0u64; 4];+    syscall3(SYS_SETITIMER, 0, new.as_ptr() as u64, old.as_mut_ptr() as u64);+    old+}++fn timers(c: &mut Checks) {+    signal(SIGALRM, on_alarm);+    ALRM.store(0, Ordering::SeqCst);+    c.check(syscall3(SYS_ALARM, 5, 0, 0) == 0, "alarm(5): no alarm before");+    c.check(syscall3(SYS_ALARM, 0, 0, 0) == 5, "alarm(0) cancels it: 5 seconds were left");+    // One-shot setitimer.+    let t0 = now_us();+    itimer(100_000, 0);+    let mut cur = [0u64; 4];+    syscall3(SYS_GETITIMER, 0, cur.as_mut_ptr() as u64, 0);+    c.check(cur[2] == 0 && cur[3] > 0 && cur[3] <= 100_000, "getitimer: time left <= 100 ms");+    c.check(wait_for(1, 1000), "setitimer 100 ms: SIGALRM arrives");+    let dt = now_us() - t0;+    c.check(dt >= 100_000 && dt < 400_000, &format!("...after {} ms (never early)", dt / 1000));+    sleep_through(150);+    c.check(ALRM.load(Ordering::SeqCst) == 1, "one-shot: only once");+    // pause() wakes up for it.+    syscall3(SYS_ALARM, 1, 0, 0);+    let t0 = now_us();+    pause();+    c.check(ALRM.load(Ordering::SeqCst) == 2 && now_us() - t0 >= 900_000, "alarm(1) + pause: woken after a second");+    // Periodic.+    ALRM.store(0, Ordering::SeqCst);+    itimer(60_000, 60_000);+    sleep_through(500);+    let old = itimer(0, 0);+    let n = ALRM.load(Ordering::SeqCst);+    c.check((3..=9).contains(&n), &format!("periodic 60 ms timer: {} signals in 500 ms", n));+    c.check(old[0] == 0 && old[1] == 60_000, "setitimer returns the old interval");+    sleep_through(150);+    c.check(ALRM.load(Ordering::SeqCst) == n, "disarmed: no more signals");+    c.check(syscall3(SYS_SETITIMER, 1, cur.as_ptr() as u64, 0) == -EINVAL, "ITIMER_VIRTUAL: EINVAL (not supported)");+    // POSIX timers.+    sigaction_info(SIGUSR1, on_timer, 0);+    ALRM.store(0, Ordering::SeqCst);+    let mut sev = [0u64; 8];+    sev[0] = 42; // sigval+    sev[1] = SIGUSR1 as u64; // signo, notify = SIGEV_SIGNAL (0)+    let mut id = -1i32;+    let r = syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+    c.check(r == 0 && id >= 0, "timer_create(CLOCK_MONOTONIC, SIGUSR1)");+    let spec = [0u64, 0, 0, 80_000_000]; // interval 0, value 80 ms+    c.check(syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0) == 0, "timer_settime 80 ms");+    let mut now = [0u64; 4];+    syscall3(SYS_TIMER_GETTIME, id as u64, now.as_mut_ptr() as u64, 0);+    c.check(now[2] == 0 && now[3] > 0 && now[3] <= 80_000_000, "timer_gettime: time left <= 80 ms");+    c.check(wait_for(1, 1000), "the timer's signal arrives");+    c.check(TCODE.load(Ordering::SeqCst) == -2 && TVAL.load(Ordering::SeqCst) == 42, "si_code SI_TIMER, si_value 42");+    c.check(syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0) == 0, "timer_getoverrun: 0");+    // Absolute time on CLOCK_MONOTONIC, periodic.+    ALRM.store(0, Ordering::SeqCst);+    let (s, ns) = clock_gettime(1);+    let at = s * 1_000_000_000 + ns + 50_000_000;+    let spec = [0u64, 50_000_000, at / 1_000_000_000, at % 1_000_000_000];+    c.check(syscall6(SYS_TIMER_SETTIME, id as u64, 1, spec.as_ptr() as u64, 0, 0, 0) == 0, "timer_settime TIMER_ABSTIME, every 50 ms");+    c.check(wait_for(3, 1000), "...fires repeatedly");+    let zero = [0u64; 4];+    let mut old = [0u64; 4];+    syscall6(SYS_TIMER_SETTIME, id as u64, 0, zero.as_ptr() as u64, old.as_mut_ptr() as u64, 0, 0);+    c.check(old[1] == 50_000_000, "disarming returns the old interval");+    c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == 0, "timer_delete");+    c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == -EINVAL, "timer_delete again: EINVAL");+    // SIGEV_NONE: nothing is sent, the time just runs down.+    sev[1] = 1u64 << 32; // notify = SIGEV_NONE+    syscall3(SYS_TIMER_CREATE, 0, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+    let spec = [0u64, 0, 10, 0];+    syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0);+    sleep_through(50);+    syscall3(SYS_TIMER_GETTIME, id as u64, now.as_mut_ptr() as u64, 0);+    c.check(now[2] == 9 || (now[2] < 10 && now[2] >= 9), "SIGEV_NONE on CLOCK_REALTIME: the time runs down");+    syscall3(SYS_TIMER_DELETE, id as u64, 0, 0);+    c.check(syscall3(SYS_TIMER_CREATE, 2, 0, &mut id as *mut i32 as u64) == -EINVAL, "timer_create on a CPU clock: EINVAL");+    sev[1] = (99u64 << 32) | SIGUSR1 as u64;+    c.check(syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64) == -EINVAL, "an unknown sigev_notify: EINVAL");+    // A child's timers don't survive it.+    let pid = fork();+    if pid == 0 {+        syscall3(SYS_ALARM, 100, 0, 0);+        let mut id = 0i32;+        syscall3(SYS_TIMER_CREATE, 1, 0, &mut id as *mut i32 as u64);+        exit(0);+    }+    wait4(pid, 0).ok();+    c.check(syscall3(SYS_ALARM, 0, 0, 0) == 0, "the child's alarm isn't ours");+}++// --- ids, priority, hostname --------------------------------------------------------------++fn res(nr: u64) -> [u32; 3] {+    let mut v = [0u32; 3];+    syscall3(nr, &mut v[0] as *mut u32 as u64, &mut v[1] as *mut u32 as u64, &mut v[2] as *mut u32 as u64);+    v+}++fn in_child(f: impl FnOnce() -> i64) -> i64 {+    let pid = fork();+    if pid == 0 {+        exit(f());+    }+    match wait4(pid, 0) {+        Ok((_, s)) => wexitstatus(s) as i64,+        Err(e) => e,+    }+}++fn ids(c: &mut Checks) {+    c.check(syscall3(SYS_GETUID, 0, 0, 0) == 0 && syscall3(SYS_GETEUID, 0, 0, 0) == 0 && syscall3(SYS_GETGID, 0, 0, 0) == 0, "we start as root");+    c.check(res(SYS_GETRESUID) == [0, 0, 0], "getresuid: 0 0 0");+    // Root drops to 1000 for good.+    let r = in_child(|| {+        let mut fails = 0;+        let groups = [5u32, 6];+        fails += (syscall3(SYS_SETGROUPS, 2, groups.as_ptr() as u64, 0) != 0) as i64;+        let mut got = [0u32; 8];+        fails += (syscall3(SYS_GETGROUPS, 0, 0, 0) != 2) as i64;+        fails += (syscall3(SYS_GETGROUPS, 8, got.as_mut_ptr() as u64, 0) != 2 || got[..2] != groups) as i64;+        fails += (syscall3(SYS_GETGROUPS, 1, got.as_mut_ptr() as u64, 0) != -EINVAL) as i64;+        fails += (syscall3(SYS_SETGID, 100, 0, 0) != 0 || res(SYS_GETRESGID) != [100, 100, 100]) as i64;+        fails += (syscall3(SYS_SETUID, 1000, 0, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 1000]) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != -EPERM) as i64;+        fails += (syscall3(SYS_SETGROUPS, 0, 0, 0) != -EPERM) as i64;+        let name = b"evil";+        fails += (syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, 4, 0) != -EPERM) as i64;+        // A file it makes is its own.+        unlink("/tmp/u1000.txt");+        write_file("/tmp/u1000.txt", b"x");+        let s = st("/tmp/u1000.txt");+        fails += (s.uid != 1000 || s.gid != 100) as i64;+        fails += (syscall3(SYS_SETPRIORITY, 0, 0, 5) != 0) as i64;+        fails += (syscall3(SYS_SETPRIORITY, 0, 0, 0) != -EACCES) as i64; // only root raises priority+        fails+    });+    c.check(r == 0, "child: setgroups/getgroups, setgid, setuid(1000) for good, then EPERM; its files are 1000:100; nice only up");+    c.check(st("/tmp/u1000.txt").uid == 1000, "the file stays 1000's");+    unlink("/tmp/u1000.txt");+    // setresuid keeps root in the saved id: it can come back.+    let r = in_child(|| {+        let mut fails = 0;+        fails += (syscall3(SYS_SETRESUID, 1000, 1000, 0) != 0) as i64;+        fails += (syscall3(SYS_GETEUID, 0, 0, 0) != 1000) as i64;+        fails += (syscall3(SYS_SETRESUID, 2000, u64::MAX, u64::MAX) != -EPERM) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != 0 || res(SYS_GETRESUID) != [1000, 0, 0]) as i64;+        // setreuid(-1, 1000): the effective id goes back to the real one, so+        // (Linux's rule) the saved id stays 0...+        fails += (syscall3(SYS_SETREUID, u32::MAX as u64, 1000, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 0]) as i64;+        // ...while setting the real id copies the effective one into it.+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != 0 || syscall3(SYS_SETREUID, 1000, 1000, 0) != 0 || res(SYS_GETRESUID) != [1000, 1000, 1000]) as i64;+        fails += (syscall3(SYS_SETUID, 0, 0, 0) != -EPERM) as i64; // no way back now+        fails+    });+    c.check(r == 0, "child: setresuid(1000,1000,0), back to euid 0 via the saved id, setreuid");+    c.check(res(SYS_GETRESUID) == [0, 0, 0], "the parent is still root");+    // Priority.+    c.check(syscall3(SYS_GETPRIORITY, 0, 0, 0) == 20, "getpriority: 20 (nice 0; the raw call returns 20 - nice)");+    c.check(syscall3(SYS_SETPRIORITY, 0, 0, 7) == 0 && syscall3(SYS_GETPRIORITY, 0, 0, 0) == 13, "setpriority nice 7 -> 13");+    c.check(in_child(|| syscall3(SYS_GETPRIORITY, 0, 0, 0)) == 13, "a child inherits it");+    c.check(syscall3(SYS_SETPRIORITY, 0, 0, (-25i64) as u64) == 0 && syscall3(SYS_GETPRIORITY, 0, 0, 0) == 40, "nice -25 is clamped to -20 (root may)");+    syscall3(SYS_SETPRIORITY, 0, 0, 0);+    c.check(syscall3(SYS_GETPRIORITY, 0, 99999, 0) == -ESRCH, "getpriority of no such process: ESRCH");+    c.check(syscall3(SYS_GETPRIORITY, 7, 0, 0) == -EINVAL, "getpriority with a bad `which`: EINVAL");+    // Host name.+    let name = b"rustbelt-test";+    c.check(syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, name.len() as u64, 0) == 0, "sethostname rustbelt-test");+    let mut u = [0u8; 6 * 65];+    c.check(uname(&mut u)[1] == "rustbelt-test", "uname shows it");+    c.check(syscall3(SYS_SETHOSTNAME, name.as_ptr() as u64, 65, 0) == -EINVAL, "a 65-byte name: EINVAL");+    let q = b"qemu";+    syscall3(SYS_SETHOSTNAME, q.as_ptr() as u64, 4, 0);+    c.check(uname(&mut u)[1] == "qemu", "back to qemu");+}++// --- misc: flock, preadv/pwritev, sendfile, sigmasks, getdents ------------------------------++static USR1: AtomicI64 = AtomicI64::new(0);+extern "C" fn on_usr1(_sig: i32) {+    USR1.fetch_add(1, Ordering::SeqCst);+}++fn misc(c: &mut Checks) {+    // flock: locks belong to the open file (not the process, not the fd).+    write_file("/tmp/fl.txt", b"x");+    let a = open("/tmp/fl.txt", O_RDWR);+    let b = open("/tmp/fl.txt", O_RDWR);+    c.check(syscall3(SYS_FLOCK, a as u64, 2, 0) == 0, "flock LOCK_EX");+    c.check(syscall3(SYS_FLOCK, b as u64, 2 | 4, 0) == -EAGAIN, "another open file: LOCK_EX|LOCK_NB is EAGAIN");+    c.check(syscall3(SYS_FLOCK, b as u64, 1 | 4, 0) == -EAGAIN, "...and LOCK_SH|LOCK_NB too");+    let a2 = dup(a);+    c.check(syscall3(SYS_FLOCK, a2 as u64, 2 | 4, 0) == 0, "a dup of the lock holder may lock again");+    c.check(syscall3(SYS_FLOCK, a as u64, 1, 0) == 0 && syscall3(SYS_FLOCK, b as u64, 1 | 4, 0) == 0, "converted to shared: others may share it");+    c.check(syscall3(SYS_FLOCK, b as u64, 8, 0) == 0 && syscall3(SYS_FLOCK, a as u64, 2, 0) == 0, "LOCK_UN, then LOCK_EX again");+    // A child blocks until the holder lets go.+    let pid = fork();+    if pid == 0 {+        // Our copies of a, a2 and b share the parent's open files: close+        // them, or the lock could never go away (like on Linux).+        close(a);+        close(a2);+        close(b);+        let fd = open("/tmp/fl.txt", O_RDWR);+        let t0 = now_us();+        let r = syscall3(SYS_FLOCK, fd as u64, 2, 0);+        exit(if r == 0 && now_us() - t0 >= 80_000 { 0 } else { 1 });+    }+    sleep_ms(150);+    close(a); // a2 still holds it+    sleep_ms(30);+    c.check(try_wait(pid) == -EAGAIN, "closing one of two fds keeps the lock");+    close(a2); // last fd: released+    let r = wait4(pid, 0);+    c.check(matches!(r, Ok((_, s)) if wexitstatus(s) == 0), "a blocked flock waits, and gets it when the last fd closes");+    c.check(syscall3(SYS_FLOCK, b as u64, 2 | 4, 0) == 0, "the child's lock went with it");+    c.check(syscall3(SYS_FLOCK, b as u64, 3, 0) == -EINVAL, "flock with a bad op: EINVAL");+    close(b);+    unlink("/tmp/fl.txt");+    // preadv / pwritev+    write_file("/tmp/pv.txt", b"0123456789");+    let fd = open("/tmp/pv.txt", O_RDWR);+    let (mut x, mut y) = ([0u8; 3], [0u8; 4]);+    let iov = [x.as_mut_ptr() as u64, 3, y.as_mut_ptr() as u64, 4];+    c.check(syscall5(SYS_PREADV, fd as u64, iov.as_ptr() as u64, 2, 2, 0) == 7 && &x == b"234" && &y == b"5678", "preadv at offset 2 into two buffers");+    c.check(lseek(fd, 0, 1) == 0, "...without moving the file offset");+    let (p, q) = (b"ab", b"cd");+    let iov = [p.as_ptr() as u64, 2, q.as_ptr() as u64, 2];+    c.check(syscall5(SYS_PWRITEV, fd as u64, iov.as_ptr() as u64, 2, 8, 0) == 4, "pwritev at 8 (past the end)");+    close(fd);+    c.check(read_file("/tmp/pv.txt") == b"01234567abcd", "the file grew to 0123456789 -> 01234567abcd");+    // sendfile+    let src = open("/tmp/pv.txt", O_RDONLY);+    let dst = open("/tmp/sf.txt", O_WRONLY | O_CREAT | O_TRUNC);+    c.check(syscall5(SYS_SENDFILE, dst as u64, src as u64, 0, 100, 0) == 12, "sendfile file -> file: 12 bytes");+    c.check(lseek(src, 0, 1) == 12, "...moving the input offset");+    let mut off = 4u64;+    let (r, w) = pipe().unwrap();+    c.check(syscall5(SYS_SENDFILE, w as u64, src as u64, &mut off as *mut u64 as u64, 3, 0) == 3 && off == 7, "sendfile at an offset into a pipe updates *offset");+    let mut buf = [0u8; 8];+    c.check(read(r, &mut buf) == 3 && &buf[..3] == b"456", "the pipe got 456");+    c.check(syscall5(SYS_SENDFILE, dst as u64, r as u64, 0, 3, 0) == -EINVAL, "sendfile from a pipe: EINVAL");+    close(r);+    close(w);+    close(src);+    close(dst);+    c.check(read_file("/tmp/sf.txt") == b"01234567abcd", "the copy matches");+    unlink("/tmp/sf.txt");+    unlink("/tmp/pv.txt");+    // ppoll / pselect6: the mask size must be 8.+    let mask = 0u64;+    let ts = [0u64, 0];+    let mut fds = [PollFd { fd: 0, events: POLLIN, revents: 0 }];+    c.check(syscall5(SYS_PPOLL, fds.as_mut_ptr() as u64, 1, ts.as_ptr() as u64, &mask as *const u64 as u64, 4) == -EINVAL, "ppoll with sigsetsize 4: EINVAL");+    let arg = [&mask as *const u64 as u64, 16];+    c.check(syscall6(SYS_PSELECT6, 0, 0, 0, 0, ts.as_ptr() as u64, arg.as_ptr() as u64) == -EINVAL, "pselect6 with sigsetsize 16: EINVAL");+    let arg = [&mask as *const u64 as u64, 8];+    c.check(syscall6(SYS_PSELECT6, 0, 0, 0, 0, ts.as_ptr() as u64, arg.as_ptr() as u64) == 0, "pselect6 with sigsetsize 8: fine");+    // ppoll's mask: a blocked, pending signal is let in for the wait only.+    signal(SIGUSR1, on_usr1);+    USR1.store(0, Ordering::SeqCst);+    let old = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1)) as u64;+    kill(getpid(), SIGUSR1);+    let (r, w) = pipe().unwrap();+    let mut fds = [PollFd { fd: r as i32, events: POLLIN, revents: 0 }];+    let ts = [1u64, 0];+    let open_mask = old & !sigbit(SIGUSR1);+    let rr = syscall5(SYS_PPOLL, fds.as_mut_ptr() as u64, 1, ts.as_ptr() as u64, &open_mask as *const u64 as u64, 8);+    c.check(rr == -EINTR && USR1.load(Ordering::SeqCst) == 1, "ppoll with SIGUSR1 unblocked: EINTR, handler ran");+    c.check(sigprocmask(SIG_BLOCK, 0) as u64 & sigbit(SIGUSR1) != 0, "the old mask (SIGUSR1 blocked) is back");+    sigprocmask(SIG_SETMASK, old);+    close(r);+    close(w);+    // getdents64 while deleting (rm -r): nothing is skipped.+    for dir in ["/tmp/gd", "/mnt/disk1/gd"] {+        mkdir(dir);+        for i in 0..40 {+            write_file(&format!("{}/f{}", dir, i), b"x");+        }+        let fd = open(dir, O_RDONLY | O_DIRECTORY);+        let mut buf = [0u8; 256]; // small: many getdents calls+        let mut seen = 0;+        let mut ino_ok = true;+        loop {+            let n = getdents64(fd, &mut buf);+            if n <= 0 {+                break;+            }+            let mut names = Vec::new();+            let mut pos = 0usize;+            while pos < n as usize {+                let ino = u64::from_le_bytes(buf[pos..pos + 8].try_into().unwrap());+                let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+                let raw = &buf[pos + 19..pos + reclen];+                let name = core::str::from_utf8(&raw[..raw.iter().position(|&b| b == 0).unwrap_or(raw.len())]).unwrap_or("?");+                if name != "." && name != ".." {+                    names.push((alloc::string::String::from(name), ino));+                }+                pos += reclen;+            }+            for (name, ino) in names {+                let path = format!("{}/{}", dir, name);+                ino_ok &= st(&path).ino == ino;+                unlink(&path);+                seen += 1;+            }+        }+        close(fd);+        c.check(seen == 40 && rmdir(dir) == 0, &format!("{}: deleting while reading: all 40 seen, directory empty", dir));+        c.check(ino_ok, &format!("{}: d_ino matches st_ino", dir));+    }+}
@@ -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 16"), "uname: Rustbelt, stage 16");+    c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 17"), "uname: Rustbelt, stage 17");      let (secs, _) = clock_gettime(0);     c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");
@@ -83,7 +83,7 @@ pub const AT_EMPTY_PATH: u64 = 0x1000; /// Longest path the kernel takes, counting the NUL. pub const PATH_MAX: usize = 4096; pub const SYS_MOUNTS: u64 = 501;-pub const SYS_SYSINFO: u64 = 502;+pub const SYS_RB_SYSINFO: u64 = 502; // ours (Linux's sysinfo is 99) pub const SYS_SOCKET: u64 = 41; pub const SYS_CONNECT: u64 = 42; pub const SYS_ACCEPT: u64 = 43;@@ -686,7 +686,7 @@ pub struct SysInfo { /// Ask the kernel for CPU, thread, process and memory counts. pub fn sysinfo() -> SysInfo {     let mut info = SysInfo::default();-    syscall3(SYS_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);+    syscall3(SYS_RB_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);     info } /// Current directory into `buf`; returns its length (or -errno). The