foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Grok Bot <grokbot@localhost> | 2026-10-31 18:52:10 -0700 |
|---|---|---|
| commit | f0db8ff6a57092f8450ec7c8c9e97d2f7de18356 | |
| parent | 649975870b61ca788c3636c7ddc0d55084dcea6f | |
| parent | c075970bdb6422c0ba75b72634da2c7710504c53 | |
| tree | browse files at this commit | |
Import rust-kernel user/ with its history (git subtree split at a55b41d, Rustbelt 0.19.0) git-subtree-dir: import/kernel-user git-subtree-mainline: 649975870b61ca788c3636c7ddc0d55084dcea6f git-subtree-split: c075970bdb6422c0ba75b72634da2c7710504c53
47 files changed, 9777 insertions, 0 deletions
@@ -0,0 +1,41 @@+//! cat [file...] -- print files (or stdin, also for "-"). Shell alias: type.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// Copy everything from `fd` to stdout.+fn copy(fd: i64) -> i64 {+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n <= 0 {+ return n; // 0 = end of file, < 0 = error+ }+ write_all(1, &buf[..n as usize]);+ }+}++fn main() -> i64 {+ if argc() < 2 {+ return if copy(0) < 0 { 1 } else { 0 };+ }+ let mut status = 0;+ for i in 1..argc() {+ let name = arg(i).unwrap_or("");+ let fd = if name == "-" { 0 } else { open(name, 0) };+ let r = if fd < 0 { fd } else { copy(fd) };+ if r < 0 {+ eprintln!("cat: {}: {}", name, strerror(r));+ status = 1;+ }+ if fd > 0 {+ close(fd);+ }+ }+ status+}
@@ -0,0 +1,41 @@+//! Helper program that `demo` (and the tests) spawn:+//! child -> print my arguments, exit 0+//! child <number> -> exit with that number (tests wait() exit codes)+//! child pipe -> write a line to stdout (demo reads it via a pipe)+//! child cat -> copy stdin to stdout until EOF+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ match arg(1) {+ Some("pipe") => {+ println!("hello through a pipe from pid {}", getpid());+ 0+ }+ Some("cat") => {+ let mut buf = [0u8; 64];+ loop {+ let n = read(0, &mut buf);+ if n <= 0 {+ return n;+ }+ write(1, &buf[..n as usize]);+ }+ }+ Some(number) if number.parse::<i64>().is_ok() => number.parse().unwrap(),+ _ => {+ print!("[child] pid {} got {} args:", getpid(), argc());+ for i in 0..argc() {+ print!(" {:?}", arg(i).unwrap_or("?"));+ }+ println!();+ 0+ }+ }+}
@@ -0,0 +1,926 @@+//! 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");+ // Stage 18 fix check (Clarke): a mount point itself reports the+ // *mounted* file system's st_dev and root inode, not the directory it+ // covers. find -xdev, du -x and cp -x compare st_dev with this.+ write_file("/tmp/devchk", b"x");+ c.check(st("/tmp").dev == st("/tmp/devchk").dev && st("/tmp").dev != st("/").dev, "stat(\"/tmp\"): the mounted ramfs's st_dev, not /'s");+ c.check(st("/dev").dev == st("/dev/null").dev && st("/dev").dev != st("/").dev, "stat(\"/dev\"): devfs's st_dev");+ c.check(st("/mnt/disk1").dev == st("/mnt/disk1/lost+found").dev && st("/mnt/disk1").ino == 2, "stat(\"/mnt/disk1\"): ext2's st_dev and its root inode (2)");+ let mut ls = Stat::default();+ c.check(lstat("/tmp", &mut ls) == 0 && ls.dev == st("/tmp/devchk").dev, "lstat of a mount point: the mounted fs's st_dev too");+ let fd = open("/mnt/disk1", O_RDONLY | O_DIRECTORY);+ let mut fs_ = Stat::default();+ c.check(fstat(fd, &mut fs_) == 0 && fs_.dev == st("/mnt/disk1/lost+found").dev && fs_.ino == 2, "fstat of an fd opened on a mount point: same");+ close(fd);+ unlink("/tmp/devchk");+ // 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, 1], "clock_getres(MONOTONIC): 1 ns (stage 19; was 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) == 0, "socketpair(AF_UNIX) works (stage 18)");+ close(sv[0] as i64);+ close(sv[1] as i64);+ c.check(syscall5(SYS_SOCKETPAIR, 3, 1, 0, sv.as_mut_ptr() as u64, 0) == -EAFNOSUPPORT, "socketpair(family 3): EAFNOSUPPORT");+ 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);+static TOVR: AtomicI64 = AtomicI64::new(-1);++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);+ TOVR.store(*((info as *const u8).add(20) as *const i32) as i64, Ordering::SeqCst); // si_overrun+ }+ 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");+ // Overruns (stage 18 fix, found by Clarke): a 20 ms timer whose signal+ // is blocked for 0.5 s expires ~25 times but sends one signal; the+ // other ~24 add up as overruns. timer_getoverrun reports the count of+ // the last *delivered* signal: 0 while it is still pending.+ ALRM.store(0, Ordering::SeqCst);+ TOVR.store(-1, Ordering::SeqCst);+ let r = syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+ let old_mask = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1)) as u64;+ let spec = [0u64, 20_000_000, 0, 20_000_000]; // every 20 ms+ syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0);+ sleep_through(500);+ c.check(r == 0 && syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0) == 0, "overrun: 0 before the first signal is delivered");+ sigprocmask(SIG_SETMASK, old_mask); // delivered now+ let first = TOVR.load(Ordering::SeqCst);+ let got = syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0);+ c.check(ALRM.load(Ordering::SeqCst) == 1 && (18..=30).contains(&first), &format!("blocked 0.5 s: one signal, si_overrun {} (Linux: ~24)", first));+ c.check(got == first, &format!("timer_getoverrun = the delivered signal's si_overrun ({})", got));+ c.check(wait_for(2, 1000), "unblocked: the next signal comes");+ let next = TOVR.load(Ordering::SeqCst);+ // (Not 0: timerd and signal delivery both work in ~55 ms ticks.)+ c.check((0..=10).contains(&next), &format!("...with a fresh count ({}), not the old one added", next));+ syscall3(SYS_TIMER_DELETE, id as u64, 0, 0);+ // 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));+ }+}
@@ -0,0 +1,107 @@+//! cowtest -- checks that fork really gives the child its *own* memory.+//!+//! fork doesn't copy anything up front: parent and child share every page,+//! read-only, and the first write to a page makes the copy (copy-on-write).+//! This checks that the copies are made at the right moments: writes in the+//! child don't show in the parent and vice versa -- for global data, the+//! heap and the stack -- while MAP_SHARED memory *is* shared.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec;+use ulib::*;++/// 64 KiB of global data (16 pages, in .bss).+static mut GLOBAL: [u8; 65536] = [0; 65536];++fn global() -> &'static mut [u8; 65536] {+ unsafe { &mut *core::ptr::addr_of_mut!(GLOBAL) }+}++fn all(buf: &[u8], value: u8) -> bool {+ buf.iter().all(|&b| b == value)+}++fn main() -> i64 {+ let mut c = Checks::new("cowtest");+ global().fill(1);+ let mut heap = vec![2u8; 100_000];+ let mut stack = [3u8; 2000];++ // 1. The child writes; the parent must not see it.+ let pid = fork();+ if pid == 0 {+ let before = all(global(), 1) && all(&heap, 2) && all(&stack, 3);+ global().fill(0xc1);+ heap.fill(0xc2);+ stack.fill(0xc3);+ let after = all(global(), 0xc1) && all(&heap, 0xc2) && all(core::hint::black_box(&stack), 0xc3);+ exit(if before && after { 0 } else { 1 });+ }+ c.check(wait(pid) == 0, "the child starts with the parent's data and can change its copy");+ c.check(all(global(), 1), "the child's writes to global data stay in the child");+ c.check(all(&heap, 2), "... and to the heap");+ c.check(all(core::hint::black_box(&stack), 3), "... and to the stack");++ // 2. The parent writes after fork; the child must keep the old data.+ // A pipe orders things: the child only checks after we wrote.+ let (r, w) = pipe().unwrap_or((-1, -1));+ let pid = fork();+ if pid == 0 {+ close(w);+ let mut b = [0u8];+ read(r, &mut b); // wait for the parent's go+ exit(if all(global(), 1) && all(&heap, 2) { 0 } else { 1 });+ }+ close(r);+ global().fill(0x77);+ heap.fill(0x77);+ write(w, b"!");+ close(w);+ c.check(wait(pid) == 0, "the parent's writes after fork stay in the parent");+ c.check(all(global(), 0x77), "and the parent sees its own writes");++ // 3. A grandchild: three generations share pages, each writes.+ let pid = fork();+ if pid == 0 {+ let pid2 = fork();+ if pid2 == 0 {+ global()[0] = 3;+ exit(if global()[1] == 0x77 { 0 } else { 1 });+ }+ global()[0] = 2;+ let code = wait(pid2);+ exit(if code == 0 && global()[0] == 2 { 0 } else { 1 });+ }+ c.check(wait(pid) == 0 && global()[0] == 0x77, "three generations: each keeps its own copy");++ // 4. MAP_SHARED memory is *not* copied: both see the same pages.+ let shared = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);+ let private = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ c.check(shared > 0 && private > 0, "mmap shared + private anonymous memory");+ let (sp, pp) = (shared as *mut u8, private as *mut u8);+ unsafe {+ sp.write_volatile(10);+ pp.write_volatile(10);+ }+ let pid = fork();+ if pid == 0 {+ unsafe {+ sp.write_volatile(42);+ sp.add(4096).write_volatile(43); // a page nobody touched before fork+ pp.write_volatile(42);+ }+ exit(0);+ }+ wait(pid);+ let (s0, s1, p0) = unsafe { (sp.read_volatile(), sp.add(4096).read_volatile(), pp.read_volatile()) };+ c.check(s0 == 42 && s1 == 43, "MAP_SHARED: the child's writes show up in the parent");+ c.check(p0 == 10, "MAP_PRIVATE: they don't");+ c.done()+}
@@ -0,0 +1,52 @@+//! cp source dest -- copy a file. If `dest` is a directory the copy goes+//! inside it with the same name (shell alias: copy).+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String};+use ulib::*;++fn main() -> i64 {+ let (Some(src), Some(dst), 3) = (arg(1), arg(2), argc()) else {+ eprintln!("usage: cp source dest");+ return 2;+ };+ let mut st = Stat::default();+ let mut dst = String::from(dst);+ if stat(&dst, &mut st) == 0 && st.is_dir() {+ let name = src.rsplit('/').next().unwrap_or(src);+ dst = format!("{}/{}", dst.trim_end_matches('/'), name);+ }+ let from = open(src, 0);+ if from < 0 {+ eprintln!("cp: {}: {}", src, strerror(from));+ return 1;+ }+ let to = open(&dst, O_WRONLY | O_CREAT | O_TRUNC);+ if to < 0 {+ eprintln!("cp: {}: {}", dst, strerror(to));+ close(from);+ return 1;+ }+ let mut buf = [0u8; 512];+ let mut status = 0;+ loop {+ let n = read(from, &mut buf);+ if n <= 0 || write_all(to, &buf[..n.max(0) as usize]) < 0 {+ if n < 0 {+ eprintln!("cp: {}: {}", src, strerror(n));+ status = 1;+ }+ break;+ }+ }+ close(from);+ close(to);+ status+}
@@ -0,0 +1,157 @@+//! Stage 6 demo: this program was itself loaded from the FAT32 disk.+//! It exercises the syscalls the shell (next stage) will need: cwd, reading+//! a file on the FAT disk, stat, lseek, getdents64, mkdir/unlink/rmdir,+//! pipes, and spawn + wait. Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++static mut FAILURES: i64 = 0;++fn check(ok: bool, what: &str) {+ if !ok {+ println!("[demo] CHECK FAILED: {}", what);+ unsafe { FAILURES += 1 };+ }+}++fn cwd_is(expected: &str) -> bool {+ let mut buf = [0u8; 64];+ let n = getcwd(&mut buf);+ n >= 0 && &buf[..n as usize] == expected.as_bytes()+}++fn main() -> i64 {+ println!("[demo] pid {} started from /mnt/disk0/bin/demo", getpid());++ // --- current directory --------------------------------------------------+ check(cwd_is("/"), "cwd starts at /");+ check(chdir("/mnt/disk0") == 0, "chdir /mnt/disk0");+ check(cwd_is("/mnt/disk0"), "getcwd after chdir");++ // --- read a file from the FAT disk (relative path!) ---------------------+ let fd = open("readme.txt", 0);+ check(fd >= 3, "open readme.txt");+ let mut buf = [0u8; 256];+ let n = read(fd, &mut buf).max(0) as usize;+ let text = core::str::from_utf8(&buf[..n]).unwrap_or("?");+ print!("[demo] readme.txt from FAT32: {}", text.lines().next().unwrap_or(""));+ println!();+ check(text.starts_with("Hello from the FAT32 disk!"), "readme.txt contents");++ // --- stat / fstat / lseek ------------------------------------------------+ let mut st = Stat::default();+ check(stat("readme.txt", &mut st) == 0 && st.is_file() && st.size == n as i64, "stat readme.txt");+ check(fstat(1, &mut st) == 0 && st.is_char_device() && isatty(1), "fstat stdout = a terminal");+ check(stat("bin", &mut st) == 0 && st.is_dir(), "stat bin is a directory");+ check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");+ let mut word = [0u8; 4];+ check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");+ check(lseek(fd, 0, SEEK_END) == n as i64, "lseek SEEK_END = size");+ check(lseek(1, 0, SEEK_SET) == -ESPIPE, "can't seek the console");+ close(fd);++ // --- list a directory with getdents64 -----------------------------------+ let dir = open("bin", 0);+ let mut dents = [0u8; 1024];+ let mut len = getdents64(dir, &mut dents);+ check(len > 0, "getdents64 bin");+ let (mut saw_demo, mut saw_child) = (false, false);+ print!("[demo] /mnt/disk0/bin:");+ // One call returns as many whole records as fit in the buffer; keep+ // calling until it returns 0 (the end of the directory).+ while len > 0 {+ for_each_dirent(&dents, len as usize, |name, d_type| {+ print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+ saw_demo |= name == "demo";+ saw_child |= name == "child";+ });+ len = getdents64(dir, &mut dents);+ }+ println!();+ check(saw_demo && saw_child, "bin contains demo and child");+ check(len == 0 && getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+ close(dir);++ // --- relative paths, "..", errors ----------------------------------------+ check(chdir("notes") == 0 && cwd_is("/mnt/disk0/notes"), "chdir notes");+ check(chdir("../..") == 0 && cwd_is("/mnt"), "chdir ../..");+ check(chdir("disk0/readme.txt") == -ENOTDIR, "chdir to a file -> ENOTDIR");+ check(chdir("/nope") == -ENOENT, "chdir to nothing -> ENOENT");++ // --- create and delete (ramfs /tmp and the FAT disk) ---------------------+ for base in ["/tmp", "/mnt/disk0/notes"] {+ let mut path_buf = [0u8; 64];+ let dir = join(&mut path_buf, base, "/newdir");+ check(mkdir(dir) == 0, "mkdir");+ check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");+ let mut file_buf = [0u8; 64];+ let file = join(&mut file_buf, dir, "/f.txt");+ let fd = open(file, O_WRONLY | O_CREAT);+ check(write(fd, b"bytes") == 5, "write new file");+ close(fd);+ check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");+ check(unlink(dir) == -EISDIR, "unlink a directory -> EISDIR");+ check(unlink(file) == 0, "unlink file");+ check(rmdir(dir) == 0, "rmdir empty dir");+ check(stat(dir, &mut st) == -ENOENT, "gone after rmdir");+ }+ check(rmdir("/tmp") == -EBUSY, "rmdir a mount point -> EBUSY");+ println!("[demo] mkdir/unlink/rmdir work on ramfs and FAT32");++ // --- spawn a child with its stdout connected to a pipe -------------------+ chdir("/mnt/disk0");+ let (r, w) = pipe().unwrap_or((-1, -1));+ let opts = SpawnOptions { stdout: w, ..SpawnOptions::INHERIT };+ let pid = spawn("bin/child", "child\0pipe", &opts);+ check(pid > 0, "spawn child (pipe)");+ close(w); // only the child holds the write end now: EOF when it exits+ let mut got = [0u8; 128];+ let mut total = 0;+ loop {+ let n = read(r, &mut got[total..]); // blocks until data or EOF+ if n <= 0 {+ break;+ }+ total += n as usize;+ }+ close(r);+ let line = core::str::from_utf8(&got[..total]).unwrap_or("?").trim_end();+ println!("[demo] read from pipe: {:?}", line);+ check(line.starts_with("hello through a pipe"), "pipe contents");+ check(wait(pid) == 0, "child (pipe) exit code 0");++ // --- spawn + wait: exit codes ---------------------------------------------+ let pid = spawn("bin/child", "child\0with\0three args", &SpawnOptions::INHERIT);+ check(wait(pid) == 0, "child with args exits 0");+ let pid = spawn("bin/child", "child\042", &SpawnOptions::INHERIT);+ let code = wait(pid);+ println!("[demo] spawned child 42 (pid {}), wait -> exit code {}", pid, code);+ check(code == 42, "wait returns the exit code");+ check(wait(pid) == -ECHILD, "second wait -> ECHILD");+ let pid = spawn("/mnt/disk0/bin/evil", "", &SpawnOptions::INHERIT);+ check(wait(pid) == 139, "killed child -> 139");+ check(spawn("bin/nope", "", &SpawnOptions::INHERIT) == -ENOENT, "spawn missing -> ENOENT");+ check(spawn("readme.txt", "", &SpawnOptions::INHERIT) == -8, "spawn non-ELF -> ENOEXEC");+ check(wait(1) == -ECHILD, "wait for a non-child -> ECHILD");++ sleep_ms(100); // blocks in the kernel (no busy loop)++ let failures = unsafe { FAILURES };+ if failures == 0 {+ println!("[demo] all stage 6 checks passed, exiting with 0");+ }+ failures+}++/// Concatenate two strings into `buf` (no heap in user programs).+fn join<'a>(buf: &'a mut [u8], a: &str, b: &str) -> &'a str {+ buf[..a.len()].copy_from_slice(a.as_bytes());+ buf[a.len()..a.len() + b.len()].copy_from_slice(b.as_bytes());+ core::str::from_utf8(&buf[..a.len() + b.len()]).unwrap()+}
@@ -0,0 +1,13 @@+/*+ * dynhello.c -- stage 19: a *dynamically linked* program (plain+ * `musl-gcc`, no -static). Its ELF file has a PT_INTERP header naming+ * the dynamic loader /lib/ld-musl-x86_64.so.1, which Rustbelt doesn't+ * have, so execve must fail with ENOEXEC ("Exec format error").+ */+#include <stdio.h>++int main(void)+{+ puts("hello from a dynamic program (you shouldn't see this on Rustbelt)");+ return 0;+}
@@ -0,0 +1,206 @@+//! echoserver -- an event-loop echo server over loopback (127.0.0.1).+//!+//! One process serves several clients at once without threads: it never+//! blocks on any single socket. Every socket is nonblocking, and the loop+//! asks the kernel "which of my sockets are ready?" (epoll_wait, or plain+//! poll), then only accepts/reads/writes those:+//!+//! loop {+//! ready = epoll_wait(...) // sleep until something happens+//! for each ready fd:+//! listener -> accept new clients, watch them too+//! client -> read; echo it back; 0 bytes = client left+//! }+//!+//! The self-test forks 3 client processes that talk to it over loopback,+//! once with epoll and once with poll, then checks UDP over loopback.+//! With an argument (`echoserver PORT`) it just serves forever.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::vec::Vec;+use ulib::*;++const LOOPBACK: Ip = Ip([127, 0, 0, 1]);+const CLIENTS: usize = 3;+const ROUNDS: usize = 3;++/// A client process: connect, send ROUNDS messages, check each echo.+fn client(port: u16, id: usize) -> i64 {+ let fd = socket(SOCK_STREAM, 0);+ if connect(fd, LOOPBACK, port) < 0 {+ return 10;+ }+ for round in 0..ROUNDS {+ let msg = format!("hello {} from client {}", round, id);+ write_all(fd, msg.as_bytes());+ let mut buf = [0u8; 64];+ let mut got = 0;+ while got < msg.len() {+ let n = read(fd, &mut buf[got..]);+ if n <= 0 {+ return 11;+ }+ got += n as usize;+ }+ if &buf[..got] != msg.as_bytes() {+ return 12;+ }+ }+ close(fd); // the server sees end-of-file+ 0+}++fn listener(port: u16) -> i64 {+ let fd = socket(SOCK_STREAM | O_NONBLOCK, 0); // SOCK_NONBLOCK+ if bind(fd, port) < 0 || listen(fd, 4) < 0 {+ return -1;+ }+ fd+}++/// Accept everyone waiting (nonblocking: stop at EAGAIN).+fn accept_all(lfd: i64, mut add: impl FnMut(i64)) {+ loop {+ let (fd, _) = accept(lfd);+ if fd < 0 {+ break; // -EAGAIN: nobody else waiting+ }+ set_nonblocking(fd, true);+ add(fd);+ }+}++/// Read what's there and echo it back. Returns false when the client left.+fn echo(fd: i64, served: &mut usize) -> bool {+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n == -EAGAIN {+ return true; // drained for now+ }+ if n <= 0 {+ return false; // 0 = end of file (or an error)+ }+ write_all(fd, &buf[..n as usize]);+ *served += 1;+ }+}++/// The epoll flavour: register fds once, then wait for just the ready ones.+/// Returns the number of clients served to the end (or -1 on a stall).+fn serve_epoll(lfd: i64, want: usize) -> i64 {+ let ep = epoll_create1(0);+ epoll_ctl(ep, EPOLL_CTL_ADD, lfd, EPOLLIN, lfd as u64);+ let (mut finished, mut served) = (0, 0);+ let mut events = [EpollEvent::default(); 8];+ while finished < want {+ let n = epoll_wait(ep, &mut events, 5000);+ if n <= 0 {+ close(ep);+ return -1; // 5 s with nothing happening: something's wrong+ }+ for ev in &events[..n as usize] {+ let fd = ev.data as i64;+ if fd == lfd {+ accept_all(lfd, |c| {+ epoll_ctl(ep, EPOLL_CTL_ADD, c, EPOLLIN, c as u64);+ });+ } else if !echo(fd, &mut served) {+ epoll_ctl(ep, EPOLL_CTL_DEL, fd, 0, 0);+ close(fd);+ finished += 1;+ }+ }+ }+ close(ep);+ println!("echoserver: epoll: {} clients, {} echoes", finished, served);+ finished as i64+}++/// The poll flavour: pass the whole fd list on every call.+fn serve_poll(lfd: i64, want: usize) -> i64 {+ let mut fds = alloc::vec![PollFd { fd: lfd as i32, events: POLLIN, revents: 0 }];+ let (mut finished, mut served) = (0, 0);+ while finished < want {+ if poll(&mut fds, 5000) <= 0 {+ return -1;+ }+ let mut new = Vec::new();+ let mut gone = Vec::new();+ for p in fds.iter() {+ if p.revents == 0 {+ continue;+ }+ if p.fd as i64 == lfd {+ accept_all(lfd, |c| new.push(c));+ } else if !echo(p.fd as i64, &mut served) {+ close(p.fd as i64);+ gone.push(p.fd);+ finished += 1;+ }+ }+ fds.retain(|p| !gone.contains(&p.fd));+ fds.extend(new.into_iter().map(|c| PollFd { fd: c as i32, events: POLLIN, revents: 0 }));+ }+ println!("echoserver: poll: {} clients, {} echoes", finished, served);+ finished as i64+}++/// Start the clients, run the server, collect the clients' results.+fn round(c: &mut Checks, port: u16, use_epoll: bool) {+ let lfd = listener(port);+ c.check(lfd >= 0, "listen on a loopback port");+ let mut pids = Vec::new();+ for id in 0..CLIENTS {+ let pid = fork();+ if pid == 0 {+ close(lfd);+ exit(client(port, id));+ }+ pids.push(pid);+ }+ let finished = if use_epoll { serve_epoll(lfd, CLIENTS) } else { serve_poll(lfd, CLIENTS) };+ close(lfd);+ let ok = pids.iter().all(|&pid| wait(pid) == 0);+ let what = if use_epoll { "epoll" } else { "poll" };+ c.check(finished == CLIENTS as i64, &format!("{}: served {} clients to the end", what, CLIENTS));+ c.check(ok, &format!("{}: every client got its echoes back", what));+}++fn main() -> i64 {+ if let Some(port) = arg(1).and_then(|a| a.parse().ok()) {+ let lfd = listener(port);+ if lfd < 0 {+ eprintln!("echoserver: can't listen on port {}", port);+ return 1;+ }+ println!("echoserver: listening on port {} (epoll)", port);+ loop {+ serve_epoll(lfd, usize::MAX);+ }+ }+ let mut c = Checks::new("echoserver");+ round(&mut c, 7070, true);+ round(&mut c, 7071, false);++ // UDP over loopback: a datagram to ourselves.+ let a = socket(SOCK_DGRAM, 0);+ let b = socket(SOCK_DGRAM, 0);+ bind(b, 7072);+ c.check(sendto(a, b"ping", LOOPBACK, 7072) == 4, "UDP sendto 127.0.0.1");+ set_recv_timeout(b, 2000);+ let mut buf = [0u8; 16];+ let (n, from) = recvfrom(b, &mut buf);+ c.check(n == 4 && &buf[..4] == b"ping" && from.ip() == LOOPBACK, "UDP datagram arrives, from 127.0.0.1");+ close(a);+ close(b);+ c.done()+}
@@ -0,0 +1,17 @@+//! A misbehaving program: tries to read kernel memory directly. The kernel's+//! pages aren't USER_ACCESSIBLE, so the CPU raises a page fault, and the+//! kernel kills this process (and carries on).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ let kernel_code = 0x20_0000 as *const u64; // where the kernel is loaded+ println!("[evil] pid {}: reading kernel memory at {:p}...", ulib::getpid(), kernel_code);+ let value = unsafe { core::ptr::read_volatile(kernel_code) };+ println!("[evil] got {:#x} -- this must never be printed!", value);+ 1+}
@@ -0,0 +1,136 @@+//! `fetch [-o file] http://host[:port]/path` (alias `wget`): download a+//! web page or file with HTTP/1.0 and print it, or save it with `-o`.+//!+//! HTTP is just text over a TCP connection. We send+//! GET /path HTTP/1.0\r\n+//! Host: example.com\r\n+//! \r\n+//! and the server answers with a status line ("HTTP/1.0 200 OK"), header+//! lines, an empty line, then the body. With HTTP/1.0 the server closes+//! the connection when the body is done, so "read until EOF" is the whole+//! protocol on our side. (Plain HTTP only: HTTPS would need TLS.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::fmt::Write;++/// "http://host:port/path" -> (host, port, path)+fn parse_url(url: &str) -> Option<(&str, u16, &str)> {+ let rest = url.strip_prefix("http://")?;+ let (hostport, path) = match rest.find('/') {+ Some(i) => (&rest[..i], &rest[i..]),+ None => (rest, "/"),+ };+ let (host, port) = match hostport.split_once(':') {+ Some((h, p)) => (h, p.parse().ok()?),+ None => (hostport, 80),+ };+ if host.is_empty() { None } else { Some((host, port, path)) }+}++fn main() -> i64 {+ let (mut out_path, mut url) = (None, None);+ let mut i = 1;+ while let Some(a) = ulib::arg(i) {+ if a == "-o" || a == "-O" {+ i += 1;+ out_path = ulib::arg(i);+ } else {+ url = Some(a);+ }+ i += 1;+ }+ let Some((host, port, path)) = url.and_then(parse_url) else {+ eprintln!("usage: fetch [-o file] http://host[:port]/path");+ return 2;+ };++ // 1. Name -> address (DNS), 2. connect (TCP handshake).+ let ip = match ulib::resolve(host) {+ Ok(ip) => ip,+ Err(e) => {+ eprintln!("fetch: {}: {}", host, ulib::strerror(e));+ return 1;+ }+ };+ let start = ulib::now_us();+ let fd = ulib::socket(ulib::SOCK_STREAM, 0);+ let r = if fd < 0 { fd } else { ulib::connect(fd, ip, port) };+ if r < 0 {+ eprintln!("fetch: connect to {}:{}: {}", ip, port, ulib::strerror(r));+ return 1;+ }++ // 3. Send the request.+ let mut request = alloc::string::String::new();+ let _ = write!(request, "GET {} HTTP/1.0\r\nHost: {}\r\nUser-Agent: rustbelt-fetch/0.12\r\nConnection: close\r\n\r\n", path, host);+ if ulib::write_all(fd, request.as_bytes()) < 0 {+ eprintln!("fetch: send failed");+ return 1;+ }++ // 4. Read the response: headers first (up to the empty line)...+ let mut head = alloc::vec::Vec::new();+ let mut buf = [0u8; 4096];+ let body_start = loop {+ let n = ulib::read(fd, &mut buf);+ if n <= 0 {+ eprintln!("fetch: connection closed before the headers ended");+ return 1;+ }+ head.extend_from_slice(&buf[..n as usize]);+ if let Some(i) = head.windows(4).position(|w| w == b"\r\n\r\n") {+ break i + 4;+ }+ if head.len() > 16 * 1024 {+ eprintln!("fetch: headers too long");+ return 1;+ }+ };+ let status_line = head.split(|&b| b == b'\r').next().unwrap_or(b"");+ let status_line = core::str::from_utf8(status_line).unwrap_or("?");+ let code: u32 = status_line.split(' ').nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);++ // ...then the body, to stdout or the file, until the server closes.+ let out = match out_path {+ Some(p) => ulib::open(p, ulib::O_WRONLY | ulib::O_CREAT | ulib::O_TRUNC),+ None => 1,+ };+ if out < 0 {+ eprintln!("fetch: {}: {}", out_path.unwrap(), ulib::strerror(out));+ return 1;+ }+ let mut total = head.len() - body_start;+ ulib::write_all(out, &head[body_start..]);+ loop {+ let n = ulib::read(fd, &mut buf);+ if n < 0 {+ eprintln!("fetch: read: {}", ulib::strerror(n));+ break;+ }+ if n == 0 {+ break; // the server closed: that's the end of the body+ }+ if ulib::write_all(out, &buf[..n as usize]) < 0 {+ eprintln!("fetch: write failed");+ return 1;+ }+ total += n as usize;+ }+ ulib::close(fd);+ let ms = ((ulib::now_us() - start) / 1000).max(1);+ if let Some(p) = out_path {+ ulib::close(out);+ println!(+ "fetch: {}, saved {} bytes to {} in {} ms ({} KiB/s)",+ status_line, total, p, ms, total as u64 * 1000 / 1024 / ms+ );+ }+ if (200..300).contains(&code) { 0 } else { 1 }+}++extern crate alloc;
@@ -0,0 +1,186 @@+//! forktest -- checks fork, execve, wait4, vfork and fd inheritance.+//!+//! forktest run all the checks (exit code = failed checks)+//! forktest bomb fork until the kernel says no (EAGAIN), then clean+//! up: the process table is bounded, nothing leaks+//! forktest args ... (used by the exec check) verify argv, envp and the+//! auxiliary vector the kernel built for us+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::vec::Vec;+use ulib::*;++const SELF: &str = "/mnt/disk0/bin/forktest";++fn main() -> i64 {+ match arg(1) {+ Some("bomb") => bomb(),+ Some("args") => check_args(),+ _ => all(),+ }+}++fn all() -> i64 {+ let mut c = Checks::new("forktest");+ let me = getpid();++ // 1. The basic dance: fork returns twice.+ let pid = fork();+ if pid == 0 {+ // The child: a new pid, and its parent is the process that forked.+ exit(if getppid() == me && getpid() != me { 7 } else { 1 });+ }+ c.check(pid > 0, "fork returns the child's pid in the parent");+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((p, s)) if p == pid && wifexited(s) && wexitstatus(s) == 7), "child saw getppid() == parent; wait4 gives its exit code 7");++ // 2. Many children, reaped in any order with wait4(-1).+ let mut sum = 0;+ for i in 1..=10 {+ if fork() == 0 {+ exit(i);+ }+ }+ let mut reaped = 0;+ while let Ok((_, status)) = wait4(-1, 0) {+ sum += wexitstatus(status);+ reaped += 1;+ }+ c.check(reaped == 10 && sum == 55, "10 children, wait4(-1) reaps them all (exit codes add up to 55)");+ c.check(wait4(-1, 0) == Err(-ECHILD), "no children left: ECHILD");++ // 3. execve: the child becomes another program (here: forktest itself+ // in "args" mode, which checks what it was given).+ let pid = fork();+ if pid == 0 {+ let err = execve(SELF, &["forktest", "args", "two words", ""], &["FOO=bar", "EMPTY="]);+ exit(100 + (-err).min(50)); // execve only returns on failure+ }+ let code = wait(pid);+ c.check(code == 0, "execve: argv, envp and auxv arrive the Linux way (see the [forktest args] lines)");+ let pid = fork();+ if pid == 0 {+ let err = execve("/mnt/disk0/bin/no-such-program", &["x"], &[]);+ exit(if err == -ENOENT { 0 } else { 1 });+ }+ c.check(wait(pid) == 0, "execve of a missing file fails with ENOENT (and the caller keeps running)");++ // 4. vfork: the parent waits until the child has exec'd.+ let pid = vfork();+ if pid == 0 {+ execve("/mnt/disk0/bin/child", &["child", "5"], &[]);+ exit(99);+ }+ c.check(wait(pid) == 5, "vfork + execve child 5 -> exit code 5");++ // 5. fds are inherited: the child writes into our pipe via dup2.+ let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1));+ c.check(fcntl(r, F_GETFD, 0) == FD_CLOEXEC as i64, "pipe2(O_CLOEXEC) sets FD_CLOEXEC");+ let pid = fork();+ if pid == 0 {+ dup2(w, 1); // stdout = the pipe (dup2 clears close-on-exec on fd 1)+ execve("/mnt/disk0/bin/child", &["child", "pipe"], &[]);+ exit(99);+ }+ close(w); // else the read below would never see end-of-file+ let mut out = Vec::new();+ let mut buf = [0u8; 128];+ loop {+ let n = read(r, &mut buf);+ if n <= 0 {+ break;+ }+ out.extend_from_slice(&buf[..n as usize]);+ }+ close(r);+ c.check(wait(pid) == 0 && out.starts_with(b"hello through a pipe"), "fork + dup2 + execve: the child's stdout comes through our pipe");++ // 6. A killed child: the status says which signal.+ let pid = fork();+ if pid == 0 {+ unsafe { core::ptr::read_volatile(0x10 as *const u8) }; // NULL-ish pointer+ exit(0);+ }+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGSEGV), "a child that touches address 0x10 dies of SIGSEGV");+ c.done()+}++/// The exec'd side of check 3.+fn check_args() -> i64 {+ let mut c = Checks::new("forktest args");+ c.check(argc() == 4, "argc == 4");+ c.check(arg(0) == Some("forktest") && arg(2) == Some("two words") && arg(3) == Some(""), "argv[0..4] as passed (incl. an empty one)");+ c.check(env(0) == Some("FOO=bar") && env(1) == Some("EMPTY=") && env(2).is_none(), "envp exactly as passed");+ c.check(getenv("FOO") == Some("bar"), "getenv(\"FOO\") == \"bar\"");+ c.check(getauxval(AT_PAGESZ) == Some(4096), "AT_PAGESZ = 4096");+ let entry = getauxval(AT_ENTRY).unwrap_or(0);+ c.check(entry != 0 && entry == ulib_start(), "AT_ENTRY = the address of _start");+ // AT_PHDR points at our own ELF program headers: the first loadable+ // one (type 1 = PT_LOAD) should contain the entry point.+ let (phdr, phent, phnum) = (getauxval(AT_PHDR).unwrap_or(0), getauxval(AT_PHENT).unwrap_or(0), getauxval(AT_PHNUM).unwrap_or(0));+ let mut found = false;+ if phdr != 0 && phent == 56 {+ for i in 0..phnum {+ let h = (phdr + i * phent) as *const u64;+ let (ty, vaddr, memsz) = unsafe { (*(h as *const u32), *h.add(2), *h.add(5)) };+ found |= ty == 1 && (vaddr..vaddr + memsz).contains(&entry);+ }+ }+ c.check(found, "AT_PHDR/AT_PHENT/AT_PHNUM: our program headers, a PT_LOAD contains the entry");+ let random = getauxval(AT_RANDOM).map(|p| unsafe { *(p as *const [u8; 16]) });+ c.check(random.is_some_and(|r| r != [0; 16]), "AT_RANDOM: 16 random bytes");+ let execfn = getauxval(AT_EXECFN).map(|p| unsafe { cstr(p as *const u8) });+ c.check(execfn == Some(SELF), "AT_EXECFN = the path we were run as");+ let sp = &c as *const _ as u64;+ c.check(sp > 0xff_0000_0000 && sp < 0x100_0000_0000, "the stack is at the top of user space");+ c.done()+}++fn ulib_start() -> u64 {+ unsafe extern "C" {+ fn _start();+ }+ _start as usize as u64+}++/// Fork until fork fails. Every child just waits (pause) for a signal.+/// Then kill them all and reap them; the kernel tests check that no memory+/// leaked and that the system still works afterwards.+fn bomb() -> i64 {+ let mut pids = Vec::new();+ let error = loop {+ let pid = fork();+ if pid == 0 {+ loop {+ pause(); // until SIGKILL+ }+ }+ if pid < 0 {+ break pid;+ }+ pids.push(pid);+ if pids.len() > 1000 {+ break 0; // unbounded? stop anyway+ }+ };+ println!("[forktest bomb] {} children, then fork failed with {} ({})", pids.len(), error, strerror(error));+ for &pid in &pids {+ kill(pid, SIGKILL);+ }+ let mut reaped = 0;+ while let Ok((_, status)) = wait4(-1, 0) {+ if wifsignaled(status) && wtermsig(status) == SIGKILL {+ reaped += 1;+ }+ }+ println!("[forktest bomb] killed and reaped {}", reaped);+ if error == -EAGAIN && reaped == pids.len() && !pids.is_empty() { 0 } else { 1 }+}
@@ -0,0 +1,457 @@+//! fputest -- every thread keeps its own FPU/SSE/AVX registers (stage 15.1).+//!+//! The kernel never uses the FPU registers itself, but user code does (any+//! C program's `double` maths, and libc's memcpy/strlen use SSE). So on a+//! context switch the kernel saves one thread's registers and loads the+//! next one's (XSAVE, or FXSAVE on older CPUs), a signal handler gets+//! clean registers and sigreturn puts the old ones back, fork copies them+//! and exec clears them. This program checks all of that:+//!+//! - threads + processes (spread over all CPUs) each load *distinct*+//! values into xmm0-15 (all 256 bits of ymm0-15 if the CPU has AVX) and+//! MXCSR, spin and yield for a while, then check nothing changed;+//! - a signal handler that scribbles over every register, synchronously+//! (kill to ourselves) and asynchronously (another thread's tgkill while+//! we spin): the interrupted code must see its own values afterwards;+//! - the handler starts with clean registers, finds the saved ones via+//! uc_mcontext.fpstate (Linux's layout), and a value it changes there is+//! what sigreturn loads;+//! - an unmasked SSE divide-by-zero raises SIGFPE (FPE_FLTDIV);+//! - fork copies the registers; exec starts with zeros and MXCSR 0x1f80.+//!+//! Our programs are built soft-float, so Rust code here never touches the+//! vector registers: only the `asm!` blocks below do. (That's also why the+//! asm can't name xmm registers as operands: it uses memory instead.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::sync::Arc;+use alloc::vec::Vec;+use core::arch::asm;+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};+use ulib::*;++/// The 16 vector registers, 32 bytes each (only the low 16 used without AVX).+#[repr(C, align(32))]+#[derive(Clone, Copy, PartialEq)]+struct Regs([u8; 512]);++static AVX: AtomicBool = AtomicBool::new(false);++/// Can we use AVX? The CPU must have it (CPUID.1:ECX bit 28) *and* the+/// kernel must have turned on XSAVE (OSXSAVE, bit 27) with the AVX state+/// in XCR0 (bits 1 and 2): otherwise AVX instructions fault (#UD).+fn detect_avx() -> bool {+ let ecx = core::arch::x86_64::__cpuid(1).ecx;+ if ecx & (1 << 27) == 0 || ecx & (1 << 28) == 0 {+ return false;+ }+ let (lo, _hi): (u32, u32);+ unsafe { asm!("xgetbv", in("ecx") 0, out("eax") lo, out("edx") _hi, options(nomem, nostack)) };+ lo & 6 == 6+}+fn avx() -> bool {+ AVX.load(Ordering::Relaxed)+}++/// Load all vector registers from `r`.+fn load(r: &Regs) {+ unsafe {+ if avx() {+ asm!(+ "vmovdqu ymm0, [{p} + 0]",+ "vmovdqu ymm1, [{p} + 32]",+ "vmovdqu ymm2, [{p} + 64]",+ "vmovdqu ymm3, [{p} + 96]",+ "vmovdqu ymm4, [{p} + 128]",+ "vmovdqu ymm5, [{p} + 160]",+ "vmovdqu ymm6, [{p} + 192]",+ "vmovdqu ymm7, [{p} + 224]",+ "vmovdqu ymm8, [{p} + 256]",+ "vmovdqu ymm9, [{p} + 288]",+ "vmovdqu ymm10, [{p} + 320]",+ "vmovdqu ymm11, [{p} + 352]",+ "vmovdqu ymm12, [{p} + 384]",+ "vmovdqu ymm13, [{p} + 416]",+ "vmovdqu ymm14, [{p} + 448]",+ "vmovdqu ymm15, [{p} + 480]",+ p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+ } else {+ asm!(+ "movdqu xmm0, [{p} + 0]",+ "movdqu xmm1, [{p} + 32]",+ "movdqu xmm2, [{p} + 64]",+ "movdqu xmm3, [{p} + 96]",+ "movdqu xmm4, [{p} + 128]",+ "movdqu xmm5, [{p} + 160]",+ "movdqu xmm6, [{p} + 192]",+ "movdqu xmm7, [{p} + 224]",+ "movdqu xmm8, [{p} + 256]",+ "movdqu xmm9, [{p} + 288]",+ "movdqu xmm10, [{p} + 320]",+ "movdqu xmm11, [{p} + 352]",+ "movdqu xmm12, [{p} + 384]",+ "movdqu xmm13, [{p} + 416]",+ "movdqu xmm14, [{p} + 448]",+ "movdqu xmm15, [{p} + 480]",+ p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+ }+ }+}++/// Store all vector registers (the bytes AVX doesn't have stay 0).+fn store() -> Regs {+ let mut r = Regs([0; 512]);+ unsafe {+ if avx() {+ asm!(+ "vmovdqu [{p} + 0], ymm0",+ "vmovdqu [{p} + 32], ymm1",+ "vmovdqu [{p} + 64], ymm2",+ "vmovdqu [{p} + 96], ymm3",+ "vmovdqu [{p} + 128], ymm4",+ "vmovdqu [{p} + 160], ymm5",+ "vmovdqu [{p} + 192], ymm6",+ "vmovdqu [{p} + 224], ymm7",+ "vmovdqu [{p} + 256], ymm8",+ "vmovdqu [{p} + 288], ymm9",+ "vmovdqu [{p} + 320], ymm10",+ "vmovdqu [{p} + 352], ymm11",+ "vmovdqu [{p} + 384], ymm12",+ "vmovdqu [{p} + 416], ymm13",+ "vmovdqu [{p} + 448], ymm14",+ "vmovdqu [{p} + 480], ymm15",+ p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+ } else {+ asm!(+ "movdqu [{p} + 0], xmm0",+ "movdqu [{p} + 32], xmm1",+ "movdqu [{p} + 64], xmm2",+ "movdqu [{p} + 96], xmm3",+ "movdqu [{p} + 128], xmm4",+ "movdqu [{p} + 160], xmm5",+ "movdqu [{p} + 192], xmm6",+ "movdqu [{p} + 224], xmm7",+ "movdqu [{p} + 256], xmm8",+ "movdqu [{p} + 288], xmm9",+ "movdqu [{p} + 320], xmm10",+ "movdqu [{p} + 352], xmm11",+ "movdqu [{p} + 384], xmm12",+ "movdqu [{p} + 416], xmm13",+ "movdqu [{p} + 448], xmm14",+ "movdqu [{p} + 480], xmm15",+ p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+ }+ }+ r+}++fn get_mxcsr() -> u32 {+ let mut m = 0u32;+ unsafe { asm!("stmxcsr [{}]", in(reg) &mut m, options(nostack, preserves_flags)) };+ m+}+fn set_mxcsr(m: u32) {+ unsafe { asm!("ldmxcsr [{}]", in(reg) &m, options(nostack, readonly, preserves_flags)) };+}++/// A register image no other `seed` produces (only the parts this CPU has).+fn pattern(seed: u64) -> Regs {+ let mut r = Regs([0; 512]);+ let mut x = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;+ for (i, chunk) in r.0.chunks_mut(8).enumerate() {+ x ^= x << 13;+ x ^= x >> 7;+ x ^= x << 17;+ if avx() || i % 4 < 2 {+ chunk.copy_from_slice(&x.to_le_bytes());+ }+ }+ r+}+/// A distinct MXCSR per seed: all exceptions masked, rounding mode (bits+/// 13-14) and flush-to-zero / denormals-are-zero (15, 6) from the seed.+fn mxcsr_for(seed: u64) -> u32 {+ 0x1f80 | ((seed as u32 & 3) << 13) | ((seed as u32 >> 2 & 1) << 15) | ((seed as u32 >> 3 & 1) << 6)+}++/// Load a pattern, spin and yield across many timer ticks (so the thread+/// is switched out and probably migrated), checking it now and then.+/// Returns how many checks found a wrong register.+fn hammer(seed: u64, rounds: u32) -> u32 {+ let want = pattern(seed);+ let mx = mxcsr_for(seed);+ load(&want);+ set_mxcsr(mx);+ let mut bad = 0;+ for round in 0..rounds {+ let mut x = round as u64;+ for _ in 0..20_000 {+ x = core::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1));+ }+ if round % 3 == 0 {+ sched_yield();+ }+ if store() != want || get_mxcsr() != mx {+ bad += 1;+ load(&want); // keep going with good values+ set_mxcsr(mx);+ }+ }+ set_mxcsr(0x1f80);+ bad+}++// --- signal handlers -----------------------------------------------------------++/// Offsets in the ucontext the kernel passes (same as Linux's): the+/// `fpstate` pointer in uc_mcontext, and inside the FXSAVE area: MXCSR,+/// xmm0, the software-reserved bytes (sw_bytes) and, in the XSAVE area,+/// the upper halves of ymm0-15.+const UC_FLAGS: usize = 0;+const UC_FPSTATE: usize = 40 + 23 * 8;+const FX_MXCSR: usize = 24;+const FX_XMM: usize = 160;+const FX_SW: usize = 464;+const XS_YMM_HI: usize = 576;+const MAGIC1: u32 = 0x4650_5853;+const MAGIC2: u32 = 0x4650_5845;++static SYNC_SEED: AtomicU64 = AtomicU64::new(0);+/// Bit flags the handler sets for main to check.+static H_CLEAN: AtomicU32 = AtomicU32::new(0); // registers zero + MXCSR default on entry+static H_SAVED: AtomicU32 = AtomicU32::new(0); // the frame held the interrupted values+static H_LAYOUT: AtomicU32 = AtomicU32::new(0); // magic numbers / flags right+static ASYNC_HITS: AtomicU32 = AtomicU32::new(0);+const NEW_XMM15: u64 = 0x1515_1515_cafe_f00d;++fn rd32(p: *const u8) -> u32 {+ unsafe { core::ptr::read_unaligned(p as *const u32) }+}+fn rd64(p: *const u8) -> u64 {+ unsafe { core::ptr::read_unaligned(p as *const u64) }+}++/// Scribble over every vector register and MXCSR (a handler that does+/// float maths, or calls memcpy, would).+fn clobber() {+ load(&pattern(0xdead));+ set_mxcsr(0x1f80 | (3 << 13));+}++extern "C" fn on_usr1(_sig: i32, _info: *const SigInfo, uc: *mut u8) {+ let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+ H_CLEAN.store(clean as u32, Ordering::SeqCst);+ let want = pattern(SYNC_SEED.load(Ordering::SeqCst));+ let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+ if fp.is_null() || fp as u64 % 64 != 0 {+ H_SAVED.store(0, Ordering::SeqCst);+ } else {+ // xmm i is at FX_XMM + 16*i; our image has register i at 32*i.+ let mut ok = rd32(unsafe { fp.add(FX_MXCSR) }) == mxcsr_for(SYNC_SEED.load(Ordering::SeqCst));+ for i in 0..16 {+ for b in 0..16 {+ ok &= unsafe { *fp.add(FX_XMM + 16 * i + b) } == want.0[32 * i + b];+ }+ }+ let flags = rd64(unsafe { uc.add(UC_FLAGS) });+ let magic1 = rd32(unsafe { fp.add(FX_SW) });+ let layout = if magic1 == MAGIC1 {+ // XSAVE frame: sw_bytes say how big it is; magic2 at the end.+ let size = rd32(unsafe { fp.add(FX_SW + 16) }) as usize;+ let xfeatures = rd64(unsafe { fp.add(FX_SW + 8) });+ if avx() {+ for i in 0..16 {+ for b in 0..16 {+ ok &= unsafe { *fp.add(XS_YMM_HI + 16 * i + b) } == want.0[32 * i + 16 + b];+ }+ }+ }+ flags & 1 != 0 && rd32(unsafe { fp.add(size) }) == MAGIC2 && xfeatures & 3 == 3 && (!avx() || xfeatures & 4 != 0)+ } else {+ // FXSAVE-only frame (no XSAVE): fine as long as we're not using AVX.+ magic1 == 0 && !avx()+ };+ H_SAVED.store(ok as u32, Ordering::SeqCst);+ H_LAYOUT.store(layout as u32, Ordering::SeqCst);+ // Change the saved xmm15: sigreturn must load what's in the frame.+ unsafe { core::ptr::write_unaligned(fp.add(FX_XMM + 16 * 15) as *mut u64, NEW_XMM15) };+ }+ clobber();+}++extern "C" fn on_usr2(_sig: i32) {+ clobber();+ ASYNC_HITS.fetch_add(1, Ordering::SeqCst);+}++static FPE_CODE: AtomicU32 = AtomicU32::new(0);+/// SIGFPE from an unmasked SSE exception. Returning would just run the+/// faulting instruction again, so mask all exceptions (and clear the+/// flags) in the *saved* MXCSR first: the retry then gives inf quietly.+extern "C" fn on_fpe(_sig: i32, info: *const SigInfo, uc: *mut u8) {+ FPE_CODE.store(unsafe { (*info).code } as u32, Ordering::SeqCst);+ let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+ if !fp.is_null() {+ unsafe { core::ptr::write_unaligned(fp.add(FX_MXCSR) as *mut u32, 0x1f80) };+ }+}++// --- the tests ---------------------------------------------------------------------++/// N threads in this process, each with its own seed; returns bad checks.+fn thread_round(base: u64, n: u64, rounds: u32) -> u32 {+ let bad = Arc::new(AtomicU32::new(0));+ let mut ts = Vec::new();+ for i in 0..n {+ let bad = bad.clone();+ ts.push(spawn_thread(0, move || {+ bad.fetch_add(hammer(base + i, rounds), Ordering::SeqCst);+ }));+ }+ let main_bad = hammer(base + 99, rounds);+ for t in ts {+ match t {+ Ok(t) => t.join(),+ Err(_) => return 1000,+ }+ }+ bad.load(Ordering::SeqCst) + main_bad+}++fn exec_check() -> i64 {+ // Run straight after exec: nothing has touched the vector registers.+ let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+ if clean { 0 } else { 1 }+}++fn main() -> i64 {+ AVX.store(detect_avx(), Ordering::Relaxed);+ if arg(1) == Some("exec-check") {+ return exec_check();+ }+ let mut c = Checks::new("fputest");+ println!(+ "fputest: {} registers, {} CPU(s)",+ if avx() { "ymm0-15 (AVX)" } else { "xmm0-15 (SSE)" },+ sysinfo().cpus+ );++ // 1. The registers start clean (main ran just after exec).+ c.check(exec_check() == 0, "a new program starts with zeroed registers and MXCSR 0x1f80");++ // 2. Threads and processes, all with distinct values, on all CPUs.+ let mut children = Vec::new();+ for p in 1..=2u64 {+ let pid = fork();+ if pid == 0 {+ exit(thread_round(p * 1000, 3, 60).min(100) as i64);+ }+ children.push(pid);+ }+ let bad = thread_round(0, 3, 60);+ let mut child_bad = 0;+ for pid in children {+ child_bad += wait(pid);+ }+ c.check(bad == 0, "4 threads kept their own vector registers + MXCSR");+ c.check(child_bad == 0, "so did 4 threads in each of 2 forked processes (12 at once)");++ // 3. fork copies the registers; exec clears them.+ let want = pattern(7);+ load(&want);+ set_mxcsr(mxcsr_for(7));+ let pid = fork();+ if pid == 0 {+ let same = store() == want && get_mxcsr() == mxcsr_for(7);+ if !same {+ exit(2);+ }+ execve("/mnt/disk0/bin/fputest", &["fputest", "exec-check"], &[]);+ exit(3);+ }+ let parent_same = store() == want;+ set_mxcsr(0x1f80);+ let code = wait(pid);+ c.check(parent_same && code != 2, "fork: the child starts with the parent's registers");+ c.check(code == 0, "exec: the new program starts with clean registers");++ // 4. A synchronous signal whose handler clobbers everything.+ sigaction_info(SIGUSR1, on_usr1, 0);+ SYNC_SEED.store(42, Ordering::SeqCst);+ let mut want = pattern(42);+ load(&want);+ set_mxcsr(mxcsr_for(42));+ kill(getpid(), SIGUSR1); // the handler runs on the way back from this+ let got = store();+ let mx = get_mxcsr();+ set_mxcsr(0x1f80);+ c.check(H_CLEAN.load(Ordering::SeqCst) == 1, "the handler starts with clean registers");+ c.check(H_SAVED.load(Ordering::SeqCst) == 1, "uc_mcontext.fpstate holds the interrupted registers");+ c.check(H_LAYOUT.load(Ordering::SeqCst) == 1, "the fpstate has Linux's layout (sw_bytes, magics, uc_flags)");+ want.0[32 * 15..32 * 15 + 8].copy_from_slice(&NEW_XMM15.to_le_bytes());+ c.check(got == want && mx == mxcsr_for(42), "after sigreturn: our registers back, with the handler's xmm15 edit");++ // 5. Asynchronous signals: another thread interrupts us while we spin.+ signal(SIGUSR2, on_usr2);+ let me = gettid();+ let sender = spawn_thread(0, move || {+ for _ in 0..40 {+ tgkill(getpid(), me, SIGUSR2);+ sleep_ms(2);+ }+ });+ let want = pattern(4242);+ load(&want);+ set_mxcsr(mxcsr_for(4242));+ let t0 = now_us();+ let mut bad = 0;+ while ASYNC_HITS.load(Ordering::SeqCst) < 40 && now_us() - t0 < 5_000_000 {+ if store() != want || get_mxcsr() != mxcsr_for(4242) {+ bad += 1;+ load(&want);+ set_mxcsr(mxcsr_for(4242));+ }+ }+ set_mxcsr(0x1f80);+ if let Ok(t) = sender {+ t.join();+ }+ let hits = ASYNC_HITS.load(Ordering::SeqCst);+ println!("fputest: {} async signals, each handler clobbered every register", hits);+ c.check(hits == 40 && bad == 0, "40 async signals with clobbering handlers changed nothing we saw");++ // 6. An unmasked SSE exception (divide by zero) -> SIGFPE, FPE_FLTDIV.+ sigaction_info(SIGFPE, on_fpe, 0);+ let one = 1.0f64.to_bits();+ let zero = 0u64;+ let mut out = 0u64;+ set_mxcsr(0x1f80 & !(1 << 9)); // unmask ZE (bit 9)+ unsafe {+ asm!(+ "movq xmm0, [{a}]",+ "divsd xmm0, [{b}]", // 1.0 / 0.0+ "movq [{o}], xmm0",+ a = in(reg) &one, b = in(reg) &zero, o = in(reg) &mut out,+ options(nostack)+ );+ }+ let mx = get_mxcsr();+ set_mxcsr(0x1f80);+ let code = FPE_CODE.load(Ordering::SeqCst);+ if code == 0 && out == f64::INFINITY.to_bits() && mx & 4 != 0 {+ // QEMU's software CPU (TCG) sets the flag but never raises #XM.+ println!("fputest: skipped SIGFPE check: this CPU doesn't raise SIMD exceptions (QEMU TCG)");+ } else {+ c.check(code == 3, "unmasked 1.0/0.0 -> SIGFPE with si_code FPE_FLTDIV");+ c.check(out == f64::INFINITY.to_bits() && mx & (1 << 9) != 0, "the handler's MXCSR edit took effect (retry gave inf)");+ }++ c.done()+}
@@ -0,0 +1,76 @@+//! getty [-1] DEVICE -- run a shell on another terminal, e.g. /dev/hvc0.+//!+//! Real Unix systems run one "getty" per terminal line: it opens the line+//! and starts a login shell on it. Ours skips the login. Since stage 15 it+//! does it the classic Unix way, with fork + exec:+//!+//! 1. fork: the child will become the shell.+//! 2. setsid (in the child): a new *session* with no terminal yet.+//! 3. open DEVICE: a session leader without a terminal that opens one+//! gets it as its *controlling terminal*. That's what lets ^C on+//! that terminal send SIGINT to the right programs.+//! 4. dup2 it onto fds 0, 1 and 2 (stdin, stdout, stderr).+//! 5. execve the shell: it keeps the fds and the session.+//!+//! getty waits for the shell; when it exits (`exit`) a new one starts,+//! unless `-1` was given.+//!+//! Try it: `getty /dev/hvc0 &` here, then on the host+//! `socat - UNIX-CONNECT:target/hvc0.sock` and type commands.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++const SHELL: &str = "/mnt/disk0/bin/sh";++/// The child's side (steps 2-5). Only returns on failure.+fn become_shell(dev: &str) -> i64 {+ setsid();+ let fd = open(dev, O_RDWR); // becomes our controlling terminal+ if fd < 0 {+ eprintln!("getty: {}: {}", dev, strerror(fd));+ return 126;+ }+ for target in 0..3 {+ dup2(fd, target);+ }+ if fd > 2 {+ close(fd);+ }+ let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";+ write(1, greeting.as_bytes());+ let err = execve(SHELL, &["sh"], &environ());+ eprintln!("getty: {}: {}", SHELL, strerror(err));+ 127+}++fn main() -> i64 {+ let (once, dev) = match (arg(1), arg(2)) {+ (Some("-1"), Some(dev)) => (true, dev),+ (Some(dev), None) if dev != "-1" => (false, dev),+ _ => {+ eprintln!("usage: getty [-1] DEVICE (e.g. getty /dev/hvc0 &)");+ return 2;+ }+ };+ loop {+ let pid = fork();+ if pid == 0 {+ exit(become_shell(dev));+ }+ if pid < 0 {+ eprintln!("getty: fork: {}", strerror(pid));+ return 1;+ }+ let status = wait(pid);+ if once || status == 126 || status == 127 {+ return status; // done, or the terminal/shell can't be opened+ }+ sleep_ms(500); // don't spin if the shell keeps dying at once+ }+}
@@ -0,0 +1,85 @@+//! grep [-i] [-v] [-n] pattern [file...] -- print lines containing+//! `pattern` (plain text, no regular expressions). -i: ignore case,+//! -v: lines that do *not* match, -n: show line numbers.+//! Exit code 0 if something matched, 1 if not, 2 on errors (like grep).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn contains(line: &[u8], pattern: &[u8], ignore_case: bool) -> bool {+ pattern.is_empty()+ || line.windows(pattern.len()).any(|w| match ignore_case {+ true => w.eq_ignore_ascii_case(pattern),+ false => w == pattern,+ })+}++fn main() -> i64 {+ let (mut ignore_case, mut invert, mut numbers) = (false, false, false);+ let mut i = 1;+ while let Some(a) = arg(i).filter(|a| a.starts_with('-') && a.len() > 1) {+ for flag in a[1..].chars() {+ match flag {+ 'i' => ignore_case = true,+ 'v' => invert = true,+ 'n' => numbers = true,+ _ => {+ eprintln!("grep: unknown option -{}", flag);+ return 2;+ }+ }+ }+ i += 1;+ }+ let Some(pattern) = arg(i) else {+ eprintln!("usage: grep [-i] [-v] [-n] pattern [file...]");+ return 2;+ };+ let files = i + 1..argc();+ let many = files.len() > 1;+ let (mut matched, mut status) = (false, 0);+ let mut search = |fd: i64, name: &str| {+ let mut number = 0;+ for_each_line(fd, |line| {+ number += 1;+ if contains(line, pattern.as_bytes(), ignore_case) != invert {+ matched = true;+ if many {+ print!("{}:", name);+ }+ if numbers {+ print!("{}:", number);+ }+ write_all(1, line);+ write(1, b"\n");+ }+ })+ };+ if files.is_empty() {+ search(0, "");+ }+ for f in files {+ let name = arg(f).unwrap_or("");+ let fd = open(name, 0);+ let r = if fd < 0 { fd } else { search(fd, name) };+ if fd >= 0 {+ close(fd);+ }+ if r < 0 {+ eprintln!("grep: {}: {}", name, strerror(r));+ status = 2;+ }+ }+ if status != 0 {+ status+ } else if matched {+ 0+ } else {+ 1+ }+}
@@ -0,0 +1,57 @@+//! First user program: says hello, uses the VFS through syscalls, and checks+//! that bad syscall arguments get the right errors. Exit code = failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn check(failures: &mut i64, ok: bool, what: &str) {+ if !ok {+ println!("[hello] CHECK FAILED: {}", what);+ *failures += 1;+ }+}++fn main() -> i64 {+ let mut failures = 0;+ println!("Hello from user mode! I'm pid {}.", getpid());++ // Read a file from the ramfs.+ let mut buf = [0u8; 128];+ let fd = open("/hello.txt", 0);+ check(&mut failures, fd >= 3, "open /hello.txt");+ let n = read(fd, &mut buf);+ let text = core::str::from_utf8(&buf[..n.max(0) as usize]).unwrap_or("<not utf-8>");+ print!("[hello] /hello.txt says: {}", text);+ check(&mut failures, text.starts_with("Hello from the ramfs!"), "contents of /hello.txt");+ check(&mut failures, read(fd, &mut buf) == 0, "read at EOF returns 0");+ check(&mut failures, close(fd) == 0, "close");++ // Create a file on the second mount (/tmp), then read it back.+ let fd = open("/tmp/note.txt", O_WRONLY | O_CREAT);+ check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt");+ close(fd);+ let fd = open("/tmp/note.txt", 0);+ let n = read(fd, &mut buf);+ check(&mut failures, &buf[..n.max(0) as usize] == b"written from ring 3", "read back /tmp/note.txt");+ close(fd);+ println!("[hello] wrote and read back /tmp/note.txt");++ // Error cases.+ check(&mut failures, open("/does/not/exist", 0) == -ENOENT, "ENOENT");+ check(&mut failures, read(42, &mut buf) == -EBADF, "EBADF");+ check(&mut failures, close(fd) == -EBADF, "double close -> EBADF");+ // Ask the kernel to print *kernel* memory for us (0x200000 = kernel code).+ let r = syscall3(SYS_WRITE, 1, 0x20_0000, 16);+ check(&mut failures, r == -EFAULT, "kernel pointer -> EFAULT");+ check(&mut failures, syscall3(999, 0, 0, 0) == -38, "unknown syscall -> ENOSYS");++ if failures == 0 {+ println!("[hello] all syscall checks passed, exiting with 0");+ }+ failures+}
@@ -0,0 +1,35 @@+//! `host name` (alias `nslookup`): look up a name's IPv4 addresses.+//!+//! DNS turns names into addresses. We send a UDP query to the DNS server+//! DHCP told us about (QEMU's is 10.0.2.3, which forwards to the host's+//! resolver), asking for the name's "A" (IPv4 address) records. smoltcp+//! builds and parses the DNS packets; the kernel's `resolve` syscall wraps+//! it (on Linux, DNS lives in the C library instead).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ let Some(name) = ulib::arg(1) else {+ eprintln!("usage: host name");+ return 2;+ };+ let mut addrs = [[0u8; 4]; 8];+ let n = ulib::resolve_all(name, &mut addrs);+ if n < 0 {+ let why = if n == -ulib::ETIMEDOUT { "no answer from the DNS server" } else { ulib::strerror(n) };+ eprintln!("host: {}: {}", name, why);+ return 1;+ }+ if n == 0 {+ println!("{} has no IPv4 address", name);+ return 1;+ }+ for a in &addrs[..n as usize] {+ println!("{} has address {}", name, ulib::Ip(*a));+ }+ 0+}
@@ -0,0 +1,132 @@+//! `httpd [-p port] [-n count] [-t seconds]`: a tiny web server.+//!+//! The server side of sockets: `bind` to a port, `listen`, then `accept`+//! connections one by one. Each request gets one answer and the connection+//! is closed (HTTP/1.0 style). `/` is a greeting page; any other path is a+//! file from the current directory (`cd B:` then `httpd` serves the FAT32+//! disk).+//!+//! QEMU's user-mode network hides the guest behind NAT, so to reach this+//! from the host QEMU must forward a port, e.g.+//! -netdev user,id=net0,hostfwd=tcp:127.0.0.1:5580-:80+//! and then `curl http://127.0.0.1:5580/` on the host.+//!+//! `-n` stops after that many requests; `-t` gives up if no request comes+//! within that many seconds (both used by the self-test).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::format;++fn main() -> i64 {+ let (mut port, mut max_requests, mut timeout_s) = (80u16, u64::MAX, 0u64);+ let mut i = 1;+ while let Some(a) = ulib::arg(i) {+ let value = ulib::arg(i + 1).and_then(|v| v.parse::<u64>().ok());+ match (a, value) {+ ("-p", Some(v)) => port = v as u16,+ ("-n", Some(v)) => max_requests = v,+ ("-t", Some(v)) => timeout_s = v,+ _ => {+ eprintln!("usage: httpd [-p port] [-n count] [-t seconds]");+ return 2;+ }+ }+ i += 2;+ }++ let fd = ulib::socket(ulib::SOCK_STREAM, 0);+ for r in [fd, ulib::bind(fd, port), ulib::listen(fd, 4)] {+ if r < 0 {+ eprintln!("httpd: {}", ulib::strerror(r));+ return 1;+ }+ }+ ulib::set_recv_timeout(fd, timeout_s * 1000); // 0 = wait forever+ println!("[httpd] listening on port {}", port);++ let mut served = 0;+ while served < max_requests {+ let (conn, peer) = ulib::accept(fd);+ if conn < 0 {+ println!("[httpd] no request within {} s, giving up", timeout_s);+ return 1;+ }+ served += 1;+ handle(conn, peer, served);+ ulib::close(conn);+ }+ println!("[httpd] served {} request{}, bye", served, if served == 1 { "" } else { "s" });+ 0+}++/// Read one request, send one response.+fn handle(conn: i64, peer: ulib::SockAddrIn, visitor: u64) {+ ulib::set_recv_timeout(conn, 5000);+ let mut req = [0u8; 2048];+ let mut len = 0;+ while len < req.len() && !req[..len].windows(4).any(|w| w == b"\r\n\r\n") {+ let n = ulib::read(conn, &mut req[len..]);+ if n <= 0 {+ return; // gave up or hung up+ }+ len += n as usize;+ }+ let line = req[..len].split(|&b| b == b'\r').next().unwrap_or(b"");+ let line = core::str::from_utf8(line).unwrap_or("");+ let mut words = line.split(' ');+ let (method, path) = (words.next().unwrap_or(""), words.next().unwrap_or("/"));++ let (status, bytes) = if method != "GET" {+ respond(conn, "405 Method Not Allowed", b"only GET, sorry\n")+ } else if path == "/" {+ let mut uts = [0u8; 6 * 65];+ let u = ulib::uname(&mut uts);+ let body = format!(+ "Hello from Rustbelt! {} {} ({})\nYou are visitor {}. Ask for /path to get a file from my current directory.\n",+ u[0], u[2], u[3], visitor+ );+ respond(conn, "200 OK", body.as_bytes())+ } else if path.contains("..") {+ respond(conn, "403 Forbidden", b"no ..\n")+ } else {+ send_file(conn, &path[1..])+ };+ println!("[httpd] {}:{} {} {} -> {} ({} bytes)", peer.ip(), peer.port(), method, path, status, bytes);+}++fn respond(conn: i64, status: &'static str, body: &[u8]) -> (&'static str, usize) {+ let head = format!("HTTP/1.0 {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nServer: rustbelt-httpd\r\n\r\n", status, body.len());+ ulib::write_all(conn, head.as_bytes());+ ulib::write_all(conn, body);+ (&status[..3], body.len())+}++fn send_file(conn: i64, path: &str) -> (&'static str, usize) {+ let mut st = ulib::Stat::default();+ if ulib::stat(path, &mut st) < 0 || !st.is_file() {+ return respond(conn, "404 Not Found", b"not found\n");+ }+ let fd = ulib::open(path, 0);+ if fd < 0 {+ return respond(conn, "404 Not Found", b"not found\n");+ }+ let head = format!("HTTP/1.0 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n", st.size);+ ulib::write_all(conn, head.as_bytes());+ let mut buf = [0u8; 4096];+ let mut sent = 0;+ loop {+ let n = ulib::read(fd, &mut buf);+ if n <= 0 || ulib::write_all(conn, &buf[..n as usize]) < 0 {+ break;+ }+ sent += n as usize;+ }+ ulib::close(fd);+ ("200", sent)+}
@@ -0,0 +1,47 @@+//! `ifconfig` (alias `ip`): show the network card and our IP address.+//!+//! eth0: virtio-net MAC 52:54:00:12:34:56 IRQ 11+//! inet 10.0.2.15/24 gateway 10.0.2.2 DNS 10.0.2.3 (DHCP)+//! RX 12 packets (2 KiB) TX 9 packets (1 KiB)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::Ip;++fn main() -> i64 {+ let mut info = ulib::netinfo();+ if info.present == 0 {+ eprintln!("ifconfig: no network card (QEMU needs -device virtio-net-pci)");+ return 1;+ }+ // Right after boot DHCP may still be running: give it up to 3 s.+ for _ in 0..30 {+ if info.configured != 0 {+ break;+ }+ ulib::sleep_ms(100);+ info = ulib::netinfo();+ }+ let m = info.mac;+ println!(+ "eth0: virtio-net MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} IRQ {}",+ m[0], m[1], m[2], m[3], m[4], m[5], info.irq+ );+ if info.configured != 0 {+ println!(+ " inet {}/{} gateway {} DNS {} (DHCP)",+ Ip(info.address), info.prefix_len, Ip(info.gateway), Ip(info.dns)+ );+ } else {+ println!(" no address yet (waiting for DHCP)");+ }+ println!(+ " RX {} packets ({} KiB) TX {} packets ({} KiB)",+ info.rx_packets, info.rx_bytes / 1024, info.tx_packets, info.tx_bytes / 1024+ );+ 0+}
@@ -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+}
@@ -0,0 +1,28 @@+/* Linker script for user programs: a static executable loaded at 4 MiB into+ user space (USER_START = 0x80_0000_0000, see src/memory.rs). Each part+ starts on its own page so it can get its own page permissions:+ code = read+execute, rodata = read-only, data/bss = read+write.+ The `large` code model (see build.rs) names its sections .ltext, .lrodata,+ .ldata and .lbss, so those must be listed too -- otherwise the linker+ places them wherever it likes (e.g. code in a non-executable segment). */+ENTRY(_start)++PHDRS {+ text PT_LOAD FLAGS(5); /* R-X */+ rodata PT_LOAD FLAGS(4); /* R-- */+ data PT_LOAD FLAGS(6); /* RW- */+}++SECTIONS {+ . = 0x8000400000;+ .text : ALIGN(4K) { *(.text .text.* .ltext .ltext.*) } :text+ .rodata : ALIGN(4K) { *(.rodata .rodata.* .lrodata .lrodata.*) } :rodata+ /* Align the location counter itself, not just .data: a program with no+ .data at all (e.g. sleep) would otherwise get its .bss on the+ read-only .rodata page (the linker drops empty sections, alignment+ included) and crash on its first write to a static. */+ . = ALIGN(4K);+ .data : { *(.data .data.* .ldata .ldata.* .got .got.*) } :data+ .bss : ALIGN(8) { *(.bss .bss.* .lbss .lbss.* COMMON) } :data+ /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) }+}
@@ -0,0 +1,167 @@+//! ls [-l] [path...] -- list directories (shell alias: dir).+//!+//! On the console the names are printed in columns; into a pipe or file+//! one per line (like the real `ls`, so `ls | wc -l` counts files).+//! -l adds sizes, DOS style: `<DIR>` or the size in bytes. On file systems+//! with Unix permissions (ext2) it also shows them, like `-rw-r--r--`, and+//! where symlinks point: `hello-link -> hello.txt`.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String, vec::Vec};+use ulib::*;++struct Entry {+ name: String,+ dir: bool,+ link: bool,+ dev: bool, // a device like /dev/fb0 (shown as <DEV>)+ size: u64,+ mode: u64, // 0 = no permissions on this file system+ target: String, // symlinks (with -l): where they point+}++fn main() -> i64 {+ let mut long = false;+ let mut paths = Vec::new();+ for i in 1..argc() {+ match arg(i) {+ Some("-l") => long = true,+ Some(a) => paths.push(a),+ None => {}+ }+ }+ if paths.is_empty() {+ paths.push(".");+ }+ let mut st = Stat::default();+ let columns = !long && isatty(1);++ let mut status = 0;+ for (i, path) in paths.iter().enumerate() {+ let r = stat(path, &mut st);+ if r < 0 {+ eprintln!("ls: {}: {}", path, strerror(r));+ status = 1;+ continue;+ }+ if !st.is_dir() {+ let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.is_char_device(), size: st.size as u64, mode: st.perm() as u64, target: String::new() };+ print_entries(&[entry], long, columns);+ continue;+ }+ if paths.len() > 1 {+ println!("{}{}:", if i > 0 { "\n" } else { "" }, path);+ }+ match read_dir(path, long) {+ Ok(entries) => print_entries(&entries, long, columns),+ Err(e) => {+ eprintln!("ls: {}: {}", path, strerror(e));+ status = 1;+ }+ }+ }+ status+}++/// All entries of a directory, sorted by name.+fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {+ let fd = open(path, 0);+ if fd < 0 {+ return Err(fd);+ }+ let mut entries = Vec::new();+ let mut buf = [0u8; 1024];+ loop {+ // Each call returns as many records as fit; 0 = no more.+ let n = getdents64(fd, &mut buf);+ if n <= 0 {+ break;+ }+ for_each_dirent(&buf, n as usize, |name, d_type| {+ if name != "." && name != ".." {+ let (dir, link, dev) = (d_type == 4, d_type == 10, d_type == 2); // DT_DIR, DT_LNK, DT_CHR+ entries.push(Entry { name: String::from(name), dir, link, dev, size: 0, mode: 0, target: String::new() });+ }+ });+ }+ close(fd);+ entries.sort_by(|a, b| a.name.cmp(&b.name));+ if long {+ let mut st = Stat::default();+ for e in entries.iter_mut() {+ let full = format!("{}/{}", path, e.name);+ if e.link {+ let mut buf = [0u8; 256];+ let n = readlink(&full, &mut buf);+ if n >= 0 {+ e.target = String::from(core::str::from_utf8(&buf[..n as usize]).unwrap_or("?"));+ }+ } else if stat(&full, &mut st) == 0 {+ e.size = st.size as u64;+ e.mode = st.perm() as u64;+ }+ }+ }+ Ok(entries)+}++fn print_entries(entries: &[Entry], long: bool, columns: bool) {+ if long {+ for e in entries {+ // `drwxr-xr-x ` in front if the file system has permissions.+ let perms = if e.mode != 0 || e.link { format!("{} ", mode_string(e)) } else { String::new() };+ if e.dir {+ println!("{} <DIR> {}/", perms, e.name);+ } else if e.link {+ println!("{} {} -> {}", perms, e.name, e.target);+ } else if e.dev {+ println!("{} <DEV> {}", perms, e.name);+ } else {+ println!("{}{:>10} {}", perms, e.size, e.name);+ }+ }+ return;+ }+ let shown = |e: &Entry| match (e.dir, e.link) {+ (true, _) => format!("{}/", e.name),+ (_, true) => format!("{}@", e.name), // like `ls -F`+ _ => e.name.clone(),+ };+ if !columns {+ entries.iter().for_each(|e| println!("{}", shown(e)));+ return;+ }+ // Columns across an 80-character screen.+ let width = entries.iter().map(|e| shown(e).len() + 2).max().unwrap_or(1);+ let per_line = (79 / width).max(1);+ for (i, e) in entries.iter().enumerate() {+ let last = i + 1 == entries.len() || (i + 1) % per_line == 0;+ if last {+ println!("{}", shown(e));+ } else {+ print!("{:<w$}", shown(e), w = width);+ }+ }+}++/// `-rw-r--r--` style: type, then rwx for owner, group and others.+fn mode_string(e: &Entry) -> String {+ if e.link {+ return String::from("lrwxrwxrwx"); // symlink permissions are never used+ }+ let mut s = String::from(if e.dir { "d" } else if e.dev { "c" } else { "-" });+ for shift in [6, 3, 0] {+ let bits = (e.mode >> shift) & 7;+ s.push(if bits & 4 != 0 { 'r' } else { '-' });+ s.push(if bits & 2 != 0 { 'w' } else { '-' });+ s.push(if bits & 1 != 0 { 'x' } else { '-' });+ }+ s+}
@@ -0,0 +1,26 @@+//! mkdir dir... -- create directories (shell alias: md).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ if argc() < 2 {+ eprintln!("usage: mkdir dir...");+ return 2;+ }+ let mut status = 0;+ for i in 1..argc() {+ let dir = arg(i).unwrap_or("");+ let r = mkdir(dir);+ if r < 0 {+ eprintln!("mkdir: {}: {}", dir, strerror(r));+ status = 1;+ }+ }+ status+}
@@ -0,0 +1,130 @@+//! mmaptest -- checks mmap / munmap / mprotect / brk and demand paging.+//!+//! mmaptest run all the checks (exit code = failed checks)+//! mmaptest crash write to read-only memory without a handler: the+//! default action of SIGSEGV ends the program (139)+//!+//! Faults are caught with a SIGSEGV handler that records what happened+//! (address, SEGV_MAPERR = nothing mapped / SEGV_ACCERR = not allowed) and+//! then *fixes* it (mprotect / mmap the page), so the faulting instruction+//! can run again when the handler returns. Garbage collectors and+//! user-space pagers use the same trick.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicU64, Ordering::SeqCst};+use ulib::*;++const PAGE: u64 = 4096;+const SEGV_MAPERR: i32 = 1;+const SEGV_ACCERR: i32 = 2;++static FAULTS: AtomicU64 = AtomicU64::new(0);+static ADDR: AtomicU64 = AtomicU64::new(0);+static CODE: AtomicU64 = AtomicU64::new(0);++extern "C" fn on_segv(sig: i32, info: *const SigInfo, _context: *mut u8) {+ let info = unsafe { &*info };+ FAULTS.fetch_add(1, SeqCst);+ ADDR.store(info.addr_or_pid, SeqCst);+ CODE.store(info.code as u64, SeqCst);+ let page = info.addr_or_pid & !(PAGE - 1);+ // Fix it: make the page readable and writable (mapping it if needed).+ if sig != SIGSEGV || mprotect(page, PAGE, PROT_READ | PROT_WRITE) < 0 {+ mmap(page, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);+ }+}++fn peek(addr: u64) -> u8 {+ unsafe { (addr as *const u8).read_volatile() }+}+fn poke(addr: u64, value: u8) {+ unsafe { (addr as *mut u8).write_volatile(value) }+}++fn main() -> i64 {+ if arg(1) == Some("crash") {+ let p = mmap(0, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ println!("[mmaptest] writing to read-only memory at {:#x}...", p);+ poke(p, 1);+ println!("[mmaptest] not reached");+ return 1;+ }+ let mut c = Checks::new("mmaptest");+ let before = sysinfo().frames_in_use;++ // 1. Anonymous memory: zeroed, readable, writable.+ let a = mmap(0, 3 * PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ c.check(a as i64 > 0 && a % PAGE == 0, "mmap 3 pages of anonymous memory");+ c.check((0..3 * PAGE).all(|i| peek(a + i) == 0), "it starts zeroed");+ for i in 0..3 * PAGE {+ poke(a + i, i as u8);+ }+ c.check((0..3 * PAGE).all(|i| peek(a + i) == i as u8), "it keeps what we write");++ // 2. Demand paging: a big mapping costs (almost) nothing until touched.+ let big = mmap(0, 64 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ let after_map = sysinfo().frames_in_use;+ poke(big, 1);+ poke(big + (32 << 20), 2);+ poke(big + (64 << 20) - 1, 3);+ let after_touch = sysinfo().frames_in_use;+ c.check(big as i64 > 0 && after_map < before + 8, "mmap 64 MiB: no memory used yet (pages come on first touch)");+ c.check(after_touch >= after_map + 3 && after_touch < after_map + 16, "touching 3 pages of it uses ~3 frames");+ c.check(munmap(big, 64 << 20) == 0, "munmap the 64 MiB");++ // 3. Faults become signals our handler sees.+ c.check(sigaction_info(SIGSEGV, on_segv, 0) == 0, "install a SIGSEGV handler (SA_SIGINFO)");+ c.check(mprotect(a + PAGE, PAGE, PROT_READ) == 0, "mprotect the middle page read-only");+ c.check(peek(a + PAGE + 5) == 5 && FAULTS.load(SeqCst) == 0, "reading it still works");+ poke(a + PAGE + 7, 99);+ c.check(FAULTS.load(SeqCst) == 1 && ADDR.load(SeqCst) == a + PAGE + 7 && CODE.load(SeqCst) == SEGV_ACCERR as u64, "writing it: SIGSEGV, si_addr = that byte, SEGV_ACCERR");+ c.check(peek(a + PAGE + 7) == 99, "the handler's mprotect fixed it and the write was retried");++ c.check(munmap(a + PAGE, PAGE) == 0, "munmap the middle page");+ c.check(peek(a) == 0 && peek(a + 2 * PAGE + 1) == 1, "the pages around the hole are untouched");+ let v = peek(a + PAGE + 7);+ c.check(FAULTS.load(SeqCst) == 2 && CODE.load(SeqCst) == SEGV_MAPERR as u64 && v == 0, "reading the hole: SEGV_MAPERR; the handler maps a fresh (zero) page");+ c.check(mprotect(a, PAGE, PROT_NONE) == 0 && { poke(a, 4); FAULTS.load(SeqCst) == 3 }, "PROT_NONE: even reading faults");+ c.check(munmap(a, 3 * PAGE) == 0, "munmap all of it");++ // 4. A file mapped read-only (private): the same bytes read() gives.+ let fd = open("/mnt/disk0/readme.txt", 0);+ let mut buf = [0u8; 211];+ let n = read(fd, &mut buf);+ let f = mmap(0, 2 * PAGE, PROT_READ, MAP_PRIVATE, fd, 0) as u64;+ c.check(fd >= 0 && n == 211 && f as i64 > 0, "mmap /mnt/disk0/readme.txt");+ c.check((0..211).all(|i| peek(f + i) == buf[i as usize]), "the mapping has the file's bytes");+ c.check(peek(f + 211) == 0 && peek(f + PAGE - 1) == 0, "after the end of the file: zeros");+ let faults = FAULTS.load(SeqCst);+ poke(f, b'J');+ c.check(FAULTS.load(SeqCst) == faults + 1 && peek(f) == b'J', "writing it faults; after mprotect we write our private copy");+ let mut again = [0u8; 1];+ lseek(fd, 0, SEEK_SET);+ read(fd, &mut again);+ c.check(again[0] == b'H', "the file itself is unchanged");+ c.check(mmap(0, PAGE, PROT_READ, MAP_SHARED, fd, 0) < 0, "MAP_SHARED of a file isn't supported (an error, not silent loss)");+ munmap(f, 2 * PAGE);+ close(fd);++ // 5. brk: grow the heap by 1 MiB, use the end, shrink back.+ let start = brk(0);+ let end = brk(start + (1 << 20));+ c.check(end == start + (1 << 20), "brk grows the heap by 1 MiB");+ poke(end - 1, 7);+ c.check(peek(end - 1) == 7, "the new end is usable");+ c.check(brk(start) == start, "brk shrinks it back");++ // 6. Bad requests.+ c.check(mmap(0, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == -22, "mmap of 0 bytes: EINVAL");+ c.check(mmap(0x1000, PAGE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == -22, "MAP_FIXED outside user space: EINVAL");++ sigaction_raw(SIGSEGV, SIG_DFL, 0, 0);+ let leaked = sysinfo().frames_in_use as i64 - before as i64;+ c.check(leaked < 16, "memory use is back to (about) where it started");+ c.done()+}
@@ -0,0 +1,151 @@+//! nbtest -- checks O_NONBLOCK: "would block" becomes EAGAIN instead.+//!+//! A blocking read on an empty pipe sleeps until data comes. With+//! O_NONBLOCK (set with fcntl(F_SETFL), ioctl(FIONBIO), pipe2 or+//! SOCK_NONBLOCK) it returns -EAGAIN at once ("try again later"), which+//! is what an event loop wants: it only reads when poll said "ready", and+//! must never get stuck on one fd.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use ulib::*;++const FIONBIO: u64 = 0x5421;++fn main() -> i64 {+ let mut c = Checks::new("nbtest");+ let mut buf = [0u8; 4096];++ // Pipes.+ let (r, w) = pipe2(O_NONBLOCK).unwrap();+ c.check(read(r, &mut buf) == -EAGAIN, "pipe: read with nothing there: EAGAIN");+ let mut p = [PollFd { fd: r as i32, events: POLLIN, revents: 0 }];+ c.check(poll(&mut p, 0) == 0, "pipe: poll says not readable");+ let mut total = 0;+ loop {+ let n = write(w, &buf);+ if n < 0 {+ c.check(n == -EAGAIN, "pipe: write to a full pipe: EAGAIN");+ break;+ }+ total += n;+ }+ println!("nbtest: the pipe took {} bytes before it was full", total);+ c.check(total == 65536, "pipe: holds 64 KiB, like Linux");+ let mut p = [PollFd { fd: w as i32, events: POLLOUT, revents: 0 }];+ c.check(poll(&mut p, 0) == 0, "pipe: poll says a full pipe isn't writable");+ c.check(read(r, &mut buf) == 4096, "pipe: then reading works");+ c.check(poll(&mut p, 0) == 1 && p[0].revents & POLLOUT != 0, "pipe: room again -> POLLOUT");+ close(w);+ while read(r, &mut buf) > 0 {}+ c.check(read(r, &mut buf) == 0, "pipe: writer gone -> end of file, not EAGAIN");+ close(r);++ // fcntl and FIONBIO switch it on and off.+ let (r, w) = pipe().unwrap();+ c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK == 0, "fcntl F_GETFL: blocking by default");+ set_nonblocking(r, true);+ c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK != 0, "fcntl F_SETFL O_NONBLOCK sticks");+ c.check(read(r, &mut buf) == -EAGAIN, "fcntl-made nonblocking pipe: EAGAIN");+ let off = 0i32;+ ioctl(r, FIONBIO, &off as *const i32 as u64);+ c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK == 0, "ioctl FIONBIO 0 turns it off");+ close(r);+ close(w);++ // Sockets (over loopback).+ let l = socket(SOCK_STREAM, 0);+ bind(l, 7080);+ listen(l, 2);+ set_nonblocking(l, true);+ c.check(accept(l).0 == -EAGAIN, "socket: accept with nobody calling: EAGAIN");+ let s = socket(SOCK_STREAM | O_NONBLOCK, 0);+ let r = connect(s, Ip([127, 0, 0, 1]), 7080);+ c.check(r == 0 || r == -EINPROGRESS, "socket: nonblocking connect: EINPROGRESS (or done at once)");+ let mut p = [PollFd { fd: s as i32, events: POLLOUT, revents: 0 }];+ c.check(poll(&mut p, 2000) == 1 && p[0].revents & POLLOUT != 0, "socket: poll POLLOUT = connected");+ let mut p = [PollFd { fd: l as i32, events: POLLIN, revents: 0 }];+ c.check(poll(&mut p, 2000) == 1, "socket: listener POLLIN = a connection to accept");+ let (a, _) = accept(l);+ c.check(a >= 0, "socket: accept it");+ c.check(read(s, &mut buf) == -EAGAIN, "socket: recv with no data: EAGAIN");+ write(a, b"hi");+ let mut p = [PollFd { fd: s as i32, events: POLLIN, revents: 0 }];+ poll(&mut p, 2000);+ c.check(read(s, &mut buf) == 2, "socket: data after POLLIN");+ close(a);+ close(s);+ close(l);++ // A terminal: the slave side of a pty.+ if let Ok((m, n)) = openpty() {+ let slave = open(&alloc::format!("/dev/pts/{}", n), O_RDWR | O_NOCTTY | O_NONBLOCK);+ c.check(read(slave, &mut buf) == -EAGAIN, "tty: read with no line typed: EAGAIN");+ close(slave);+ close(m);+ } else {+ c.check(false, "openpty");+ }++ // Small extras libc needs: pread/pwrite, writev, clocks, nanosleep.+ let fd = open("/tmp/nbtest.txt", O_CREAT | O_TRUNC | O_RDWR);+ writev(fd, &[b"hello ", b"vectored ", b"world"]);+ pwrite(fd, b"HELLO", 0);+ let n = pread(fd, &mut buf, 6);+ c.check(&buf[..n.max(0) as usize] == b"vectored world", "writev + pread at an offset");+ c.check(lseek(fd, 0, SEEK_CUR) == 20, "pread/pwrite leave the file position alone");+ lseek(fd, 0, SEEK_SET);+ let n = read(fd, &mut buf);+ c.check(&buf[..n.max(0) as usize] == b"HELLO vectored world", "pwrite overwrote the start");+ close(fd);+ unlink("/tmp/nbtest.txt");+ // readv: one read filling several buffers in turn.+ let fd = open("/tmp/nbtest.txt", O_CREAT | O_TRUNC | O_RDWR);+ write(fd, b"abcdefgh");+ lseek(fd, 0, SEEK_SET);+ let (mut a, mut b) = ([0u8; 3], [0u8; 5]);+ let n = readv(fd, &mut [&mut a, &mut b]);+ c.check(n == 8 && &a == b"abc" && &b == b"defgh", "readv fills the buffers in order");+ close(fd);+ unlink("/tmp/nbtest.txt");++ // select and ppoll on a pipe: not readable, then readable.+ let (r, w) = pipe2(0).unwrap();+ let (mut rs, mut ws) = ([0u64; 16], [0u64; 16]);+ fd_set(&mut rs, r);+ let t0 = now_us();+ c.check(select(r + 1, &mut rs, &mut ws, 20_000) == 0 && now_us() - t0 >= 20_000, "select: times out after 20 ms on an empty pipe");+ write(w, b"!");+ let (mut rs, mut ws) = ([0u64; 16], [0u64; 16]);+ fd_set(&mut rs, r);+ fd_set(&mut ws, w);+ c.check(select(w.max(r) + 1, &mut rs, &mut ws, 0) == 2 && fd_isset(&rs, r) && fd_isset(&ws, w), "select: pipe readable + writable");+ let mut p = [PollFd { fd: r as i32, events: POLLIN, revents: 0 }];+ c.check(ppoll_us(&mut p, 0) == 1 && p[0].revents & POLLIN != 0, "ppoll: POLLIN");+ close(r);+ close(w);++ // getcwd and uname, which libc start-up and `uname -a` use.+ let mut cwd = [0u8; 64];+ let n = getcwd(&mut cwd);+ 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 19"), "uname: Rustbelt, stage 19");++ let (secs, _) = clock_gettime(0);+ c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");+ let t0 = now_us();+ nanosleep_us(30_000);+ c.check(now_us() - t0 >= 25_000, "nanosleep 30 ms");+ let mut link = [0u8; 64];+ let n = readlink("/proc/self/exe", &mut link);+ c.check(n > 0 && link[..n as usize].ends_with(b"nbtest"), "readlink /proc/self/exe = this program");+ c.done()+}
@@ -0,0 +1,29 @@+//! A buggy program: a function that calls itself forever. Every call needs+//! a little more stack, so the stack grows down until it runs into the+//! *guard zone* below it, which is never mapped. The CPU raises a page+//! fault there and the kernel kills just this program, saying+//! "killed: stack overflow", instead of letting the stack quietly grow over+//! other memory.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++/// Uses 1 KiB of stack per call, then calls itself again.+fn recurse(depth: u64) -> u64 {+ let mut buf = [0u8; 1024];+ // `black_box` stops the compiler from removing the array or turning+ // the recursion into a loop.+ buf[0] = depth as u8;+ let buf = core::hint::black_box(buf);+ recurse(depth + 1) + buf[0] as u64+}++fn main() -> i64 {+ println!("[overflow] pid {}: recursing until the stack runs out...", ulib::getpid());+ let n = recurse(0);+ println!("[overflow] returned {} -- this must never be printed!", n);+ 1+}
@@ -0,0 +1,177 @@+//! pathtest -- Linux's path syscalls: C strings, `struct stat`, *at() (stage 15.1).+//!+//! Since 15.1 every path argument is what C passes: a pointer to a+//! NUL-terminated string (before, our own programs passed pointer ++//! length, which no libc can). The kernel copies it in carefully, so:+//! - a NULL, kernel-space, or unmapped pointer is EFAULT (not a crash),+//! also when the string runs off the end of a mapped page;+//! - more than PATH_MAX (4096) bytes without a NUL is ENAMETOOLONG;+//! - "" is ENOENT.+//! Then: stat/fstat/lstat/newfstatat fill in Linux's 144-byte `struct+//! stat`, and the *at() calls take paths relative to a directory fd.+//! Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::string::String;+use alloc::vec;+use ulib::*;++/// A kernel address: user code may never pass one of these.+const KERNEL_PTR: u64 = 0xffff_8000_0000_0000;++fn sys(nr: u64, a: u64, b: u64, c: u64) -> i64 {+ syscall3(nr, a, b, c)+}++fn main() -> i64 {+ let mut c = Checks::new("pathtest");+ let mut st = Stat::default();+ let stp = &mut st as *mut Stat as u64;++ // --- bad pointers: EFAULT, from every path call ---------------------------+ for &(bad, what) in &[(0u64, "NULL"), (KERNEL_PTR, "a kernel address"), (0xf0_0000_0000, "an unmapped user address")] {+ let calls = [+ ("open", sys(SYS_OPEN, bad, 0, 0)),+ ("openat", syscall5(SYS_OPENAT, AT_FDCWD as u64, bad, 0, 0, 0)),+ ("stat", sys(SYS_STAT, bad, stp, 0)),+ ("lstat", sys(SYS_LSTAT, bad, stp, 0)),+ ("newfstatat", syscall5(SYS_NEWFSTATAT, AT_FDCWD as u64, bad, stp, 0, 0)),+ ("access", sys(SYS_ACCESS, bad, 0, 0)),+ ("mkdir", sys(SYS_MKDIR, bad, 0o755, 0)),+ ("rmdir", sys(SYS_RMDIR, bad, 0, 0)),+ ("unlink", sys(SYS_UNLINK, bad, 0, 0)),+ ("rename", sys(SYS_RENAME, bad, bad, 0)),+ ("chdir", sys(SYS_CHDIR, bad, 0, 0)),+ ("readlink", sys(SYS_READLINK, bad, stp, 64)),+ ("execve", sys(SYS_EXECVE, bad, 0, 0)),+ ];+ let failed: alloc::vec::Vec<&str> = calls.iter().filter(|(_, r)| *r != -EFAULT).map(|(n, _)| *n).collect();+ if !failed.is_empty() {+ println!("pathtest: not EFAULT for {}: {:?}", what, failed);+ }+ c.check(failed.is_empty(), &alloc::format!("13 path calls with {} as the path: EFAULT", what));+ }+ // A path that runs off the end of a mapped page into an unmapped one:+ // the kernel must notice at the page boundary.+ let two = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ munmap(two + 4096, 4096);+ let tail = two + 4096 - 8;+ unsafe { core::ptr::copy_nonoverlapping(b"/tmp/abc".as_ptr(), tail as *mut u8, 8) }; // no NUL+ c.check(sys(SYS_STAT, tail, stp, 0) == -EFAULT, "a path running into an unmapped page: EFAULT");+ unsafe { *((two + 4095) as *mut u8) = 0 }; // now it ends just in time+ c.check(sys(SYS_STAT, tail, stp, 0) == -ENOENT, "the same path ending right at the page end: ENOENT");+ munmap(two, 4096);+ // Good path, bad `struct stat` pointer.+ c.check(sys(SYS_STAT, c"/".as_ptr() as u64, KERNEL_PTR, 0) == -EFAULT, "stat into a kernel address: EFAULT");+ c.check(sys(SYS_FSTAT, 0, 0, 0) == -EFAULT, "fstat into NULL: EFAULT");++ // --- lengths ------------------------------------------------------------------+ let mut long = vec![b'a'; 5000]; // no NUL in the first 4096 bytes+ long.push(0);+ c.check(sys(SYS_STAT, long.as_ptr() as u64, stp, 0) == -ENAMETOOLONG, "no NUL within PATH_MAX: ENAMETOOLONG");+ // 4095 bytes + NUL is allowed (short components, so it's just missing).+ let mut max = String::new();+ while max.len() < 4095 {+ max.push_str("/d");+ }+ max.truncate(4095);+ c.check(stat(&max, &mut st) == -ENOENT, "a 4095-byte path is accepted (ENOENT)");+ c.check(sys(SYS_STAT, c"".as_ptr() as u64, stp, 0) == -ENOENT, "an empty path: ENOENT");++ // --- Linux's struct stat ----------------------------------------------------+ c.check(core::mem::size_of::<Stat>() == 144, "struct stat is 144 bytes");+ c.check(stat("/mnt/disk0/bin", &mut st) == 0 && st.is_dir() && st.nlink == 2, "a directory: S_IFDIR, 2 links");+ let r = stat("/mnt/disk0/bin/pathtest", &mut st);+ let ino = st.ino;+ c.check(+ r == 0 && st.is_file() && st.size > 0 && st.blksize == 4096 && st.blocks == (st.size + 511) / 512 && st.ino != 0,+ "a file: S_IFREG, size, blksize 4096, 512-byte blocks, an inode number",+ );+ c.check(st.uid == 0 && st.gid == 0 && st.perm() != 0, "owned by root, with permission bits");+ let fd = open("/mnt/disk0/bin/pathtest", O_RDONLY);+ let mut st2 = Stat::default();+ c.check(fstat(fd, &mut st2) == 0 && st2.ino == ino && st2.size == st.size, "fstat on it: same inode and size");+ let mut st3 = Stat::default();+ c.check(fstatat(fd, "", &mut st3, AT_EMPTY_PATH) == 0 && st3.ino == ino, "newfstatat(fd, \"\", AT_EMPTY_PATH) = fstat");+ c.check(fstatat(fd, "x", &mut st3, 0) == -ENOTDIR, "a file as dirfd: ENOTDIR");+ close(fd);+ c.check(fstat(0, &mut st) == 0 && st.is_char_device() && isatty(0), "stdin: a character device and a tty");+ let (r, w) = pipe().unwrap();+ c.check(fstat(r, &mut st) == 0 && st.file_type() == S_IFIFO, "a pipe: S_IFIFO");+ close(r);+ close(w);++ // Symlinks: stat follows, lstat doesn't (on the ext2 disk, if it's there).+ let link = (0..4).map(|i| alloc::format!("/mnt/disk{}/hello-link", i)).find(|p| lstat(p, &mut st) == 0);+ if let Some(link) = link {+ c.check(lstat(&link, &mut st) == 0 && st.is_symlink() && st.size == 9, "lstat on a symlink: S_IFLNK, size = target length");+ c.check(stat(&link, &mut st) == 0 && st.is_file(), "stat on it: the file it points to");+ let mut buf = [0u8; 64];+ let n = readlink(&link, &mut buf);+ c.check(n == 9 && &buf[..9] == b"hello.txt", "readlink: the target, no NUL");+ let dir = &link[..link.rfind('/').unwrap()];+ let dfd = open(dir, O_RDONLY | O_DIRECTORY);+ let n = syscall5(SYS_READLINKAT, dfd as u64, c"hello-link".as_ptr() as u64, buf.as_mut_ptr() as u64, 64, 0);+ c.check(n == 9, "readlinkat(dirfd, \"hello-link\")");+ c.check(fstatat(dfd, "hello-link", &mut st, AT_SYMLINK_NOFOLLOW) == 0 && st.is_symlink(), "newfstatat AT_SYMLINK_NOFOLLOW = lstat");+ close(dfd);+ } else {+ println!("pathtest: no ext2 disk with hello-link: skipped symlink checks");+ }+ c.check(readlink("/mnt/disk0/bin", &mut [0u8; 8]) == -EINVAL, "readlink on a non-link: EINVAL");++ // --- open flags ----------------------------------------------------------------+ c.check(open("/mnt/disk0/bin/pathtest", O_RDONLY | O_DIRECTORY) == -ENOTDIR, "O_DIRECTORY on a file: ENOTDIR");+ c.check(open("/tmp", O_WRONLY) == -EISDIR, "a directory opened for writing: EISDIR");+ let fd = open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL);+ c.check(fd >= 0 && open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL) == -EEXIST, "O_CREAT|O_EXCL: creates, then EEXIST");+ close(fd);+ c.check(access("/tmp/pt-excl") == 0 && access("/tmp/nope") == -ENOENT, "access: 0 / ENOENT");++ // --- *at() relative to a directory fd ------------------------------------------+ c.check(mkdir("/tmp/ptdir") == 0, "mkdir /tmp/ptdir");+ let dfd = open("/tmp/ptdir", O_RDONLY | O_DIRECTORY);+ c.check(dfd >= 0, "open a directory with O_DIRECTORY");+ let fd = openat(dfd, "inner.txt", O_WRONLY | O_CREAT);+ c.check(fd >= 0 && write(fd, b"12345") == 5, "openat(dirfd, \"inner.txt\", O_CREAT) + write");+ close(fd);+ c.check(stat("/tmp/ptdir/inner.txt", &mut st) == 0 && st.size == 5, "...it's in that directory");+ c.check(openat(AT_FDCWD, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "openat(AT_FDCWD, absolute)");+ c.check(openat(99, "inner.txt", O_RDONLY) == -EBADF, "openat with a bad dirfd: EBADF");+ c.check(openat(99, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "...unless the path is absolute");+ c.check(syscall5(SYS_MKDIRAT, dfd as u64, c"sub".as_ptr() as u64, 0o755, 0, 0) == 0, "mkdirat(dirfd, \"sub\")");+ c.check(sys(SYS_FACCESSAT, dfd as u64, c"sub".as_ptr() as u64, 0) == 0, "faccessat(dirfd, \"sub\")");+ c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, 0, 0, 0) == -EISDIR, "unlinkat on a directory without AT_REMOVEDIR: EISDIR");+ c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, AT_REMOVEDIR, 0, 0) == 0, "unlinkat(AT_REMOVEDIR)");+ let r = syscall5(SYS_RENAMEAT, dfd as u64, c"inner.txt".as_ptr() as u64, AT_FDCWD as u64, c"/tmp/renamed.txt".as_ptr() as u64, 0);+ c.check(r == 0 && access("/tmp/ptdir/inner.txt") == -ENOENT, "renameat(dirfd, \"inner.txt\", AT_FDCWD, \"/tmp/renamed.txt\")");+ // fchdir: cd to the directory we have open.+ c.check(sys(SYS_FCHDIR, dfd as u64, 0, 0) == 0 && stat("../renamed.txt", &mut st) == 0 && st.size == 5, "fchdir(dirfd), then a relative path");+ chdir("/");+ close(dfd);++ // --- rename -----------------------------------------------------------------------+ let fd = open("/tmp/pt-new", O_WRONLY | O_CREAT);+ write(fd, b"new contents");+ close(fd);+ c.check(rename("/tmp/pt-new", "/tmp/renamed.txt") == 0, "rename a file over another");+ c.check(stat("/tmp/renamed.txt", &mut st) == 0 && st.size == 12 && access("/tmp/pt-new") == -ENOENT, "...it replaced it");+ c.check(rename("/tmp/renamed.txt", "/mnt/disk0/x.txt") == -EXDEV, "rename across file systems: EXDEV");+ c.check(rename("/tmp/ptdir", "/tmp/ptdir2") == 0 && stat("/tmp/ptdir2", &mut st) == 0 && st.is_dir(), "rename an empty directory");+ c.check(rename("/tmp/ptdir2", "/tmp/ptdir2/in") == -EINVAL, "rename a directory into itself: EINVAL");+ c.check(rename("/tmp/nope", "/tmp/x") == -ENOENT, "rename a missing file: ENOENT");+ unlink("/tmp/renamed.txt");+ unlink("/tmp/pt-excl");+ rmdir("/tmp/ptdir2");++ // --- small things musl asks at startup ------------------------------------------+ c.check(sys(SYS_GETUID, 0, 0, 0) == 0, "getuid: 0 (root)");+ c.done()+}
@@ -0,0 +1,65 @@+/*+ * piehello.c -- stage 19: "hello" as a static PIE, built with the stock+ * `musl-gcc -static-pie` (no special flags). The file's addresses start+ * at 0; the kernel loads it at a random base and tells us where through+ * the auxiliary vector. musl's start-up code (rcrt1.o) then fixes up the+ * program's own pointers before main runs.+ *+ * Exit code = number of failed checks. `piehello where` prints the load+ * address of main (tests/static_pie.rs runs it twice: different bases).+ */+#define _GNU_SOURCE+#include <elf.h>+#include <link.h>+#include <stdio.h>+#include <string.h>+#include <sys/auxv.h>++static int fails;+#define CHECK(cond, what) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("piehello: FAIL %s\n", what); \+ } \+ } while (0)++/* Pointers in data: these need fixing up (relocating) at start-up. */+static const char *greeting = "hello from a static PIE";+static int (*say)(const char *) = puts;++extern char _start[];+extern const Elf64_Ehdr __ehdr_start; /* the linker's name for our ELF header */++int main(int argc, char **argv)+{+ if (argc > 1 && strcmp(argv[1], "where") == 0) {+ printf("%p\n", (void *)main);+ return 0;+ }+ say(greeting);+ unsigned long phdr = getauxval(AT_PHDR), entry = getauxval(AT_ENTRY);+ CHECK(getauxval(AT_BASE) == 0, "AT_BASE is 0 (no dynamic loader)");+ CHECK(entry == (unsigned long)_start, "AT_ENTRY is where _start is");+ CHECK(getauxval(AT_PHENT) == sizeof(Elf64_Phdr), "AT_PHENT");+ /* Our ELF header is loaded too, at the load bias (the file's lowest+ * address is 0), and the program headers follow it, where AT_PHDR says. */+ unsigned long bias = (unsigned long)&__ehdr_start;+ CHECK(phdr == bias + __ehdr_start.e_phoff, "AT_PHDR points at the program headers");+ const Elf64_Phdr *ph = (const Elf64_Phdr *)phdr;+ int has_tls = 0, has_relro = 0, has_interp = 0;+ for (unsigned long i = 0; i < getauxval(AT_PHNUM); i++) {+ has_tls |= ph[i].p_type == PT_TLS;+ has_relro |= ph[i].p_type == PT_GNU_RELRO;+ has_interp |= ph[i].p_type == PT_INTERP;+ }+ CHECK(bias != 0 && bias % 4096 == 0, "a page-aligned, non-zero load bias");+ CHECK(bias >= 0x8000000000UL, "loaded in Rustbelt's user space");+ CHECK(has_relro && !has_interp, "PT_GNU_RELRO, no PT_INTERP");+ (void)has_tls;+ /* The relocated pointers point into the loaded image. */+ CHECK((unsigned long)greeting > bias, "relocated string pointer");+ /* RELRO: musl made it read-only after relocating; mprotect worked. */+ printf("argv[0] = %s, %d argument(s), bias %#lx\n", argv[0], argc, bias);+ return fails;+}
@@ -0,0 +1,122 @@+/*+ * piethreads.c -- stage 19: a static PIE (stock `musl-gcc -static-pie`)+ * using pthreads, thread-local storage (PT_TLS), mutexes and stdio, plus+ * musl's ttyname() on a pseudo-terminal. Exit code = failed checks.+ */+#define _GNU_SOURCE+#include <errno.h>+#include <fcntl.h>+#include <pthread.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/stat.h>+#include <sys/sysmacros.h>+#include <time.h>+#include <unistd.h>++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("piethreads: FAIL "); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++#define THREADS 4+#define ROUNDS 10000++static __thread int mine = 7; /* PT_TLS: initialized ... */+static __thread char scratch[64]; /* ... and zeroed (.tbss) */+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;+static long shared;+static FILE *out;++static void *worker(void *arg)+{+ long id = (long)arg;+ if (mine != 7 || scratch[0] != 0)+ return (void *)1; /* every thread starts with a fresh TLS copy */+ mine = id;+ snprintf(scratch, sizeof scratch, "thread %ld", id);+ for (int i = 0; i < ROUNDS; i++) {+ pthread_mutex_lock(&lock);+ shared++;+ pthread_mutex_unlock(&lock);+ }+ pthread_mutex_lock(&lock);+ fprintf(out, "%s done\n", scratch);+ pthread_mutex_unlock(&lock);+ /* Nobody else changed our copy meanwhile. */+ return (void *)(long)(mine != id);+}++static void threads(void)+{+ const char *path = "/tmp/piethreads.txt";+ out = fopen(path, "w");+ CHECK(out != NULL, "fopen: errno %d", errno);+ if (!out)+ return;+ pthread_t t[THREADS];+ for (long i = 0; i < THREADS; i++)+ CHECK(pthread_create(&t[i], NULL, worker, (void *)i) == 0, "pthread_create %ld", i);+ for (int i = 0; i < THREADS; i++) {+ void *r;+ pthread_join(t[i], &r);+ CHECK(r == NULL, "thread %d saw wrong TLS", i);+ }+ CHECK(shared == (long)THREADS * ROUNDS, "counter %ld", shared);+ CHECK(mine == 7, "main thread's TLS changed");+ fclose(out);+ /* Read the file back through stdio: one line per thread. */+ FILE *in = fopen(path, "r");+ char line[64];+ int lines = 0;+ while (in && fgets(line, sizeof line, in))+ lines += strstr(line, " done\n") != NULL;+ if (in)+ fclose(in);+ CHECK(lines == THREADS, "%d lines in the file", lines);+ unlink(path);+}++/* musl's ttyname: readlink /proc/self/fd/N, then stat() that name and+ * fstat() the fd -- both must give the same device and inode (stage 19). */+static void tty_names(void)+{+ int master = posix_openpt(O_RDWR | O_NOCTTY);+ CHECK(master >= 0, "posix_openpt: errno %d", errno);+ if (master < 0)+ return;+ CHECK(grantpt(master) == 0 && unlockpt(master) == 0, "grantpt/unlockpt");+ char *name = ptsname(master);+ CHECK(name && strncmp(name, "/dev/pts/", 9) == 0, "ptsname %s", name ? name : "(null)");+ int slave = name ? open(name, O_RDWR | O_NOCTTY) : -1;+ CHECK(slave >= 0, "open %s: errno %d", name, errno);+ if (slave >= 0) {+ char *tn = ttyname(slave);+ CHECK(tn && strcmp(tn, name) == 0, "ttyname(slave) = %s, errno %d", tn ? tn : "(null)", errno);+ struct stat st;+ CHECK(fstat(slave, &st) == 0 && S_ISCHR(st.st_mode) && major(st.st_rdev) == 136, "pts rdev");+ close(slave);+ }+ char *tm = ttyname(master);+ CHECK(tm && strcmp(tm, "/dev/ptmx") == 0, "ttyname(master) = %s", tm ? tm : "(null)");+ close(master);+ if (isatty(0)) {+ char *t0 = ttyname(0);+ CHECK(t0 != NULL, "ttyname(0): errno %d", errno);+ }+}++int main(void)+{+ threads();+ tty_names();+ printf("piethreads: %d failed\n", fails);+ return fails;+}
@@ -0,0 +1,103 @@+//! `ping [-c count] [-i ms] host`: send ICMP echo requests, time the replies.+//!+//! ICMP is IP's "control message" protocol. An *echo request* (type 8) asks+//! a host to send the same bytes straight back as an *echo reply* (type 0).+//! The packet: type, code, checksum (2 bytes), identifier (2), sequence+//! number (2), then any payload. We use a Linux-style "ping socket"+//! (SOCK_DGRAM + IPPROTO_ICMP): the kernel fills in the identifier and+//! checksum and only gives us replies to *our* requests.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++const PAYLOAD: usize = 56; // like Linux's ping: 56 + 8 header = 64 bytes++fn main() -> i64 {+ let mut count = 4u16;+ let mut interval_ms = 1000;+ let mut host = None;+ let mut i = 1;+ while let Some(a) = ulib::arg(i) {+ match a {+ "-c" => { i += 1; count = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(4); }+ "-i" => { i += 1; interval_ms = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(1000); }+ _ => host = Some(a),+ }+ i += 1;+ }+ let Some(host) = host else {+ eprintln!("usage: ping [-c count] [-i interval_ms] host");+ return 2;+ };+ let ip = match ulib::resolve(host) {+ Ok(ip) => ip,+ Err(e) => {+ eprintln!("ping: {}: {}", host, ulib::strerror(e));+ return 2;+ }+ };+ let fd = ulib::socket(ulib::SOCK_DGRAM, ulib::IPPROTO_ICMP);+ if fd < 0 {+ eprintln!("ping: socket: {}", ulib::strerror(fd));+ return 2;+ }+ ulib::set_recv_timeout(fd, 1000); // a reply later than 1 s counts as lost+ println!("PING {} ({}): {} data bytes", host, ip, PAYLOAD);++ let (mut received, mut min, mut max, mut total) = (0u64, u64::MAX, 0u64, 0u64);+ for seq in 1..=count {+ // Echo request: type 8, code 0, checksum and id (kernel fills in), seq.+ let mut packet = [0u8; 8 + PAYLOAD];+ packet[0] = 8;+ packet[6..8].copy_from_slice(&seq.to_be_bytes());+ for (j, b) in packet[8..].iter_mut().enumerate() {+ *b = j as u8; // a recognizable payload+ }+ let sent_at = ulib::now_us();+ let n = ulib::sendto(fd, &packet, ip, 0);+ if n < 0 {+ eprintln!("ping: sendto: {}", ulib::strerror(n));+ return 2;+ }+ // Wait for *this* reply (a late one for an older seq is skipped).+ let mut reply = [0u8; 8 + PAYLOAD + 64];+ loop {+ let (n, from) = ulib::recvfrom(fd, &mut reply);+ if n < 0 {+ println!("Request timeout for icmp_seq={}", seq);+ break;+ }+ let n = n as usize;+ if n >= 8 && reply[0] == 0 && reply[6..8] == seq.to_be_bytes() {+ let us = ulib::now_us() - sent_at;+ let ok = reply[8..n] == packet[8..];+ println!(+ "{} bytes from {}: icmp_seq={} time={}.{:02} ms{}",+ n, from.ip(), seq, us / 1000, us % 1000 / 10,+ if ok { "" } else { " (payload differs!)" }+ );+ received += 1;+ (min, max, total) = (min.min(us), max.max(us), total + us);+ break;+ }+ }+ if seq < count {+ ulib::sleep_ms(interval_ms);+ }+ }+ ulib::close(fd);+ let loss = (count as u64 - received) * 100 / count.max(1) as u64;+ println!("--- {} ping statistics ---", host);+ println!("{} packets transmitted, {} received, {}% packet loss", count, received, loss);+ if received > 0 {+ let avg = total / received;+ println!(+ "rtt min/avg/max = {}.{:02}/{}.{:02}/{}.{:02} ms",+ min / 1000, min % 1000 / 10, avg / 1000, avg % 1000 / 10, max / 1000, max % 1000 / 10+ );+ }+ if received > 0 { 0 } else { 1 }+}
@@ -0,0 +1,213 @@+//! ptytest -- checks pseudo-terminals (ptys) and the line discipline.+//!+//! A pty is a pair: the *master* side is held by a terminal emulator (or+//! ssh, tmux, a compositor's terminal window), the *slave* side+//! (/dev/pts/N) looks exactly like a real terminal to the program running+//! on it. Between them sits the line discipline: in *canonical* mode it+//! collects a whole line (with backspace editing) before the program can+//! read it, echoes what's typed, turns ^C into SIGINT; in *raw* mode bytes+//! pass straight through (what editors and games use).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::string::String;+use ulib::*;++/// Read whatever the other side has (waits up to 1 s for the first byte).+fn read_some(fd: i64, buf: &mut [u8]) -> usize {+ let mut p = [PollFd { fd: fd as i32, events: POLLIN, revents: 0 }];+ if poll(&mut p, 1000) <= 0 {+ return 0;+ }+ read(fd, buf).max(0) as usize+}++/// musl's ttyname(fd), step by step (stage 19): the name is what+/// /proc/self/fd/N points to, but only if stat() of that name is the same+/// file (st_dev and st_ino) as fstat(fd). Otherwise ENODEV.+fn ttyname(fd: i64) -> Result<String, i64> {+ let mut buf = [0u8; 64];+ let n = readlink(&format!("/proc/self/fd/{}", fd), &mut buf);+ if n < 0 {+ return Err(n);+ }+ let name = String::from(core::str::from_utf8(&buf[..n as usize]).map_err(|_| -EINVAL)?);+ let (mut a, mut b) = (Stat::default(), Stat::default());+ if stat(&name, &mut a) != 0 || fstat(fd, &mut b) != 0 {+ return Err(-ENOENT);+ }+ if a.dev != b.dev || a.ino != b.ino {+ return Err(-ENODEV);+ }+ Ok(name)+}++/// Linux's device numbers (`makedev`).+fn makedev(major: u64, minor: u64) -> u64 {+ ((major & 0xffff_f000) << 32) | ((major & 0xfff) << 8) | ((minor & 0xffff_ff00) << 12) | (minor & 0xff)+}++/// Every /dev node has one identity: stat twice, or stat and fstat of an+/// open fd, agree; st_rdev and the mode are Linux's.+fn device_identities(c: &mut Checks, master: i64, slave: i64, n: u32) {+ for (path, major, minor, perm) in [+ ("/dev/null", 1, 3, 0o666),+ ("/dev/zero", 1, 5, 0o666),+ ("/dev/random", 1, 8, 0o666),+ ("/dev/urandom", 1, 9, 0o666),+ ("/dev/tty", 5, 0, 0o666),+ ("/dev/console", 5, 1, 0o600),+ ("/dev/ptmx", 5, 2, 0o666),+ ] {+ let (mut a, mut b, mut f) = (Stat::default(), Stat::default(), Stat::default());+ stat(path, &mut a);+ stat(path, &mut b);+ let fd = if path == "/dev/tty" { -1 } else { open(path, O_RDWR | O_NOCTTY) };+ let fd_same = fd < 0 || (fstat(fd, &mut f) == 0 && f.dev == a.dev && f.ino == a.ino && f.rdev == a.rdev && f.mode == a.mode);+ if fd >= 0 {+ close(fd); // (for /dev/ptmx: a new pty, gone again)+ }+ c.check(+ a.ino == b.ino && a.dev == b.dev && a.rdev == makedev(major, minor) && a.mode == S_IFCHR | perm && fd_same,+ &format!("{}: char device {}:{}, mode {:o}, one inode for stat and fstat", path, major, minor, perm),+ );+ }+ let (mut a, mut m) = (Stat::default(), Stat::default());+ stat(&format!("/dev/pts/{}", n), &mut a);+ c.check(a.rdev == makedev(136, n as u64) && a.mode == S_IFCHR | 0o620, "/dev/pts/N: char device 136:N, mode 620");+ stat("/dev/ptmx", &mut a);+ fstat(master, &mut m);+ c.check(m.ino == a.ino && m.rdev == makedev(5, 2), "a pty master fstats as /dev/ptmx");+ c.check(ttyname(slave) == Ok(format!("/dev/pts/{}", n)), "ttyname(pty slave) = /dev/pts/N");+ let console = open("/dev/console", O_RDWR | O_NOCTTY);+ c.check(ttyname(console).as_deref() == Ok("/dev/console"), "ttyname(console fd) = /dev/console");+ close(console);+ // The console fds the kernel gave us (if stdin is the console).+ if isatty(0) {+ c.check(ttyname(0).is_ok(), "ttyname(0) works for the console the kernel handed us");+ }+ let mut buf = [0u8; 64];+ let r = readlink("/proc/self/cwd", &mut buf);+ let mut cwd = [0u8; 64];+ let k = getcwd(&mut cwd);+ c.check(r > 0 && r == k && buf[..r as usize] == cwd[..k as usize], "/proc/self/cwd links to the current directory");+}++fn main() -> i64 {+ let mut c = Checks::new("ptytest");+ let (master, n) = match openpty() {+ Ok(x) => x,+ Err(e) => {+ println!("ptytest: openpty: {}", strerror(e));+ return 1;+ }+ };+ let path = format!("/dev/pts/{}", n);+ let slave = open(&path, O_RDWR | O_NOCTTY);+ c.check(slave >= 0, &format!("open {} (the slave side)", path));+ device_identities(&mut c, master, slave, n);+ let mut buf = [0u8; 64];++ // Canonical mode: a line is readable only once it's complete.+ write(master, b"hel");+ set_nonblocking(slave, true);+ c.check(read(slave, &mut buf) == -EAGAIN, "canonical: half a line can't be read yet (EAGAIN)");+ write(master, b"lo\x7f\x7fp me\r"); // two backspaces, then Enter ('\r')+ let got = read_some(slave, &mut buf);+ c.check(&buf[..got] == b"help me\n", "canonical: backspace edits the line, Enter ('\\r') ends it as '\\n'");+ let got = read_some(master, &mut buf);+ c.check(got > 0 && buf[..got].ends_with(b"\r\n"), "echo: what's typed comes back to the master");++ // Output processing: the program's '\n' reaches the terminal as "\r\n".+ write(slave, b"out\n");+ let got = read_some(master, &mut buf);+ c.check(&buf[..got] == b"out\r\n", "output: '\\n' becomes \"\\r\\n\" (ONLCR)");++ // Raw mode: every byte at once, no echo, no conversions.+ let saved = tcgetattr(slave).unwrap_or_default();+ let mut raw = saved;+ cfmakeraw(&mut raw);+ c.check(tcsetattr(slave, &raw) == 0, "TCSETS: switch to raw mode");+ write(master, b"x\r\x03");+ let got = read_some(slave, &mut buf);+ c.check(&buf[..got] == b"x\r\x03", "raw: bytes arrive at once, unchanged (even ^C)");+ set_nonblocking(master, true);+ c.check(read(master, &mut buf) == -EAGAIN, "raw: no echo");+ set_nonblocking(master, false);+ tcsetattr(slave, &saved);+ c.check(tcgetattr(slave).map(|t| t.lflag & ICANON != 0).unwrap_or(false), "TCGETS: canonical mode back on");+ // (We keep `slave` open: with no slave open at all, the master reports+ // a hang-up (POLLHUP) instead of waiting for the child to open it.)++ // ^C: a child makes the pty its controlling terminal (setsid + open)+ // and waits; typing ^C on the master must kill it with SIGINT.+ let pid = fork();+ if pid == 0 {+ setsid();+ let fd = open(&path, O_RDWR); // session leader: becomes our ctty+ if fd < 0 {+ exit(3);+ }+ // /dev/tty is "my controlling terminal", whichever that is.+ let me = open("/dev/tty", O_RDWR);+ if me < 0 {+ exit(4);+ }+ write(me, b"ready\n");+ loop {+ pause();+ }+ }+ let got = read_some(master, &mut buf); // "ready\r\n"+ c.check(&buf[..got] == b"ready\r\n", "child opened the pty as its controlling terminal");+ write(master, b"\x03");+ let status = match wait4(pid, 0) {+ Ok((_, s)) => s,+ Err(_) => 0,+ };+ c.check(wifsignaled(status) && wtermsig(status) == SIGINT, "^C on the master: SIGINT for the foreground job");+ let got = read_some(master, &mut buf);+ c.check(got >= 2 && &buf[..2] == b"^C", "^C is echoed as \"^C\"");++ // ^\ is SIGQUIT (it would dump core on Unix).+ let pid = fork();+ if pid == 0 {+ setsid();+ if open(&path, O_RDWR) < 0 {+ exit(3);+ }+ loop {+ pause();+ }+ }+ sleep_ms(100); // let the child open the pty first+ write(master, b"\x1c");+ let status = match wait4(pid, 0) {+ Ok((_, s)) => s,+ Err(_) => 0,+ };+ c.check(wifsignaled(status) && wtermsig(status) == SIGQUIT, "^\\ on the master: SIGQUIT");+ set_nonblocking(master, true);+ while read(master, &mut buf) > 0 {} // drop the "^\\" echo+ set_nonblocking(master, false);+ // Without a controlling terminal, /dev/tty doesn't exist for us.+ let pid = fork();+ if pid == 0 {+ setsid();+ exit(if open("/dev/tty", O_RDWR) < 0 { 0 } else { 1 });+ }+ c.check(matches!(wait4(pid, 0), Ok((_, 0))), "no controlling terminal: /dev/tty fails");++ // Hang-up: closing the master gives the slave end-of-file.+ close(master);+ c.check(read(slave, &mut buf) == 0, "master closed: the slave reads end-of-file");+ close(slave);+ c.done()+}
@@ -0,0 +1,74 @@+//! random -- print random bytes from the kernel's entropy source.+//!+//! random 16 random bytes as hex (a fresh 128-bit id)+//! random -n 32 32 bytes as hex+//! random -d 6 [N] roll N dice with 6 sides (default 1 die)+//!+//! The bytes come from the `getrandom` system call (Linux number 318),+//! which the kernel fills from the virtio-rng device, i.e. from the host's+//! /dev/urandom. /dev/random and /dev/urandom read the same source.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ match (arg(1), arg(2), arg(3)) {+ (None, ..) => hex(16),+ (Some("-n"), Some(n), None) => match n.parse::<usize>() {+ Ok(n) if n <= 4096 => hex(n),+ _ => usage(),+ },+ (Some("-d"), Some(sides), count) => {+ let sides = sides.parse::<u64>().unwrap_or(0);+ let count = count.map_or(Some(1), |c| c.parse::<u64>().ok()).unwrap_or(0);+ if sides < 2 || count == 0 || count > 100 {+ return usage();+ }+ for i in 0..count {+ print!("{}{}", if i > 0 { " " } else { "" }, roll(sides));+ }+ println!();+ 0+ }+ _ => usage(),+ }+}++/// Print `n` random bytes as hex digits.+fn hex(n: usize) -> i64 {+ let mut buf = [0u8; 4096];+ let r = getrandom(&mut buf[..n]);+ if r < 0 {+ eprintln!("random: getrandom: {}", strerror(r));+ return 1;+ }+ for b in &buf[..n] {+ print!("{:02x}", b);+ }+ println!();+ 0+}++/// A fair number in 1..=sides. Plain `x % sides` would favour small+/// numbers slightly, so throw away the values from the uneven top part.+fn roll(sides: u64) -> u64 {+ let limit = u64::MAX - u64::MAX % sides;+ loop {+ let mut b = [0u8; 8];+ getrandom(&mut b);+ let x = u64::from_le_bytes(b);+ if x < limit {+ return x % sides + 1;+ }+ }+}++fn usage() -> i64 {+ eprintln!("usage: random | random -n BYTES (max 4096) | random -d SIDES [COUNT]");+ 2+}
@@ -0,0 +1,274 @@+/*+ * rawabi.c - a tiny static Linux program with no libc at all (stage 15.1).+ *+ * This file knows nothing about Rustbelt: it is written against the plain+ * Linux x86_64 system call ABI (numbers, argument registers, C strings,+ * struct layouts) the way musl's own syscall layer is. If it passes here+ * and on a real Linux machine, musl's startup code and its file calls+ * have a good chance of working too.+ *+ * Build (build.rs does this with the host's gcc):+ * cc -O2 -static -nostdlib -ffreestanding -fno-stack-protector -fpie -no-pie+ * -Wl,-Ttext-segment=0x8000400000 -o rawabi rawabi.c+ * -fpie makes the code RIP-relative, so it works at Rustbelt's user base+ * (0x80_0000_0000, above 4 GiB) while staying an ordinary ET_EXEC file.+ *+ * Run: rawabi [scratch-dir] (default /tmp). Exit status = failed checks.+ */++typedef unsigned long u64;+typedef long i64;++/* ---- raw system calls: number in rax, args in rdi rsi rdx r10 r8 r9 ---- */++static i64 sys6(i64 n, i64 a, i64 b, i64 c, i64 d, i64 e, i64 f) {+ register i64 r10 __asm__("r10") = d;+ register i64 r8 __asm__("r8") = e;+ register i64 r9 __asm__("r9") = f;+ i64 ret;+ __asm__ volatile("syscall"+ : "=a"(ret)+ : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9)+ : "rcx", "r11", "memory");+ return ret;+}+#define sys0(n) sys6(n, 0, 0, 0, 0, 0, 0)+#define sys1(n, a) sys6(n, (i64)(a), 0, 0, 0, 0, 0)+#define sys2(n, a, b) sys6(n, (i64)(a), (i64)(b), 0, 0, 0, 0)+#define sys3(n, a, b, c) sys6(n, (i64)(a), (i64)(b), (i64)(c), 0, 0, 0)+#define sys4(n, a, b, c, d) sys6(n, (i64)(a), (i64)(b), (i64)(c), (i64)(d), 0, 0)++/* Linux x86_64 syscall numbers (arch/x86/entry/syscalls/syscall_64.tbl). */+enum {+ NR_read = 0, NR_write = 1, NR_close = 3, NR_stat = 4, NR_fstat = 5,+ NR_lseek = 8, NR_mmap = 9, NR_munmap = 11, NR_brk = 12,+ NR_rt_sigaction = 13, NR_rt_sigprocmask = 14, NR_ioctl = 16,+ NR_writev = 20, NR_access = 21, NR_getpid = 39, NR_fork = 57,+ NR_exit = 60, NR_wait4 = 61, NR_kill = 62, NR_uname = 63,+ NR_getcwd = 79, NR_chdir = 80, NR_rename = 82, NR_mkdir = 83,+ NR_rmdir = 84, NR_unlink = 87, NR_getuid = 102, NR_arch_prctl = 158,+ NR_getdents64 = 217, NR_set_tid_address = 218, NR_clock_gettime = 228,+ NR_clock_nanosleep = 230, NR_exit_group = 231, NR_openat = 257,+ NR_newfstatat = 262, NR_unlinkat = 263,+};++#define AT_FDCWD (-100)+#define AT_REMOVEDIR 0x200+#define O_RDONLY 0+#define O_WRONLY 1+#define O_CREAT 0100+#define O_TRUNC 01000+#define O_DIRECTORY 0200000+#define ENOENT 2+#define EFAULT 14+#define SIGUSR1 10+#define SA_RESTORER 0x04000000+#define ARCH_SET_FS 0x1002+#define S_IFMT 0170000+#define S_IFREG 0100000+#define S_IFDIR 0040000++/* Linux's x86_64 `struct stat` (144 bytes). */+struct kstat {+ u64 dev, ino, nlink;+ unsigned mode, uid, gid, pad0;+ u64 rdev;+ i64 size, blksize, blocks;+ i64 atime, atime_ns, mtime, mtime_ns, ctime, ctime_ns;+ i64 unused[3];+};+struct timespec { i64 sec, nsec; };+struct iovec { const void *base; u64 len; };+struct utsname { char f[6][65]; }; /* sysname, nodename, release, ... */+struct kaction { void (*handler)(int); u64 flags; void (*restorer)(void); u64 mask; };++/* gcc may emit calls to these even with -ffreestanding. */+void *memset(void *d, int c, u64 n) { char *p = d; while (n--) *p++ = (char)c; return d; }+void *memcpy(void *d, const void *s, u64 n) { char *p = d; const char *q = s; while (n--) *p++ = *q++; return d; }++/* ---- tiny output helpers ---- */++static u64 slen(const char *s) { u64 n = 0; while (s[n]) n++; return n; }+static void puts_(const char *s) { sys3(NR_write, 1, s, slen(s)); }+static void putu(u64 v) {+ char b[24]; int i = 23; b[i] = 0;+ do { b[--i] = (char)('0' + v % 10); v /= 10; } while (v);+ puts_(b + i);+}+static int seq(const char *a, const char *b) { while (*a && *a == *b) a++, b++; return *a == *b; }+/* Join dir and name into out (no bounds games: test names are short). */+static const char *join(char *out, const char *dir, const char *name) {+ char *p = out;+ while (*dir) *p++ = *dir++;+ *p++ = '/';+ while (*name) *p++ = *name++;+ *p = 0;+ return out;+}++static int failures;+static void check(int ok, const char *what) {+ puts_(ok ? "rawabi: ok " : "rawabi: FAIL ");+ puts_(what);+ puts_("\n");+ if (!ok) failures++;+}++/* ---- SSE math: plain C doubles compile to SSE2 instructions on x86_64 ---- */++/* pi from the Leibniz series; `volatile` keeps the loop in xmm registers+ * doing real work instead of being folded at compile time. */+static double leibniz_pi(int terms) {+ volatile double sum = 0.0;+ for (int k = 0; k < terms; k++)+ sum += (k & 1 ? -4.0 : 4.0) / (2.0 * k + 1.0);+ return sum;+}+static double sqrt_sd(double x) { double r; __asm__("sqrtsd %1, %0" : "=x"(r) : "x"(x)); return r; }+/* Print a double with 6 decimals using integer math only. */+static void putf(double v) {+ if (v < 0) { puts_("-"); v = -v; }+ u64 whole = (u64)v, frac = (u64)((v - (double)whole) * 1e6 + 0.5);+ if (frac >= 1000000) { whole++; frac -= 1000000; }+ putu(whole); puts_(".");+ for (u64 d = 100000; d > frac && d > 1; d /= 10) puts_("0");+ putu(frac);+}++/* ---- a signal handler that does its own SSE math ---- */++static volatile int got_signal;+static volatile double handler_result;+static void on_usr1(int sig) {+ /* Clobbers xmm registers the interrupted code may be using; the kernel+ * must save them in the signal frame and put them back at sigreturn. */+ handler_result = sqrt_sd(16.0) * sqrt_sd(0.25); /* = 2, exactly */+ got_signal = sig;+}+/* The restorer: what the handler "returns" into. musl uses the same. */+__asm__(".text\n.global rawabi_restorer\nrawabi_restorer:\n"+ " mov $15, %eax\n syscall\n hlt\n");+__attribute__((visibility("hidden"))) void rawabi_restorer(void);++static u64 tls_block[4]; /* a stand-in thread pointer, like musl's */++static int run(int argc, char **argv) {+ const char *dir = argc > 1 ? argv[1] : "/tmp";+ char work[256], f1[256], f2[256], cwd[256];++ puts_("rawabi: a static Linux binary with no libc, pid ");+ putu((u64)sys0(NR_getpid));+ puts_("\n");++ /* What musl's __init_tls and __init_libc do first. */+ tls_block[0] = (u64)tls_block; /* x86_64 TLS: fs:0 points to itself */+ i64 r = sys2(NR_arch_prctl, ARCH_SET_FS, tls_block);+ u64 self;+ __asm__ volatile("mov %%fs:0, %0" : "=r"(self));+ check(r == 0 && self == (u64)tls_block, "arch_prctl(ARCH_SET_FS) + %fs:0");+ check(sys1(NR_set_tid_address, &tls_block[1]) == sys0(NR_getpid), "set_tid_address returns the tid");+ u64 oldmask = ~0ul;+ check(sys4(NR_rt_sigprocmask, 0, 0, &oldmask, 8) == 0 && oldmask == 0, "rt_sigprocmask(.., oldset, 8)");+ i64 uid = sys0(NR_getuid); /* 0 on Rustbelt (no users yet) */+ check(uid >= 0, "getuid");++ struct utsname u;+ check(sys1(NR_uname, &u) == 0 && seq(u.f[0], "Rustbelt") | seq(u.f[0], "Linux"), "uname");+ puts_("rawabi: uname: "); puts_(u.f[0]); puts_(" "); puts_(u.f[2]); puts_("\n");++ /* Memory: brk and an anonymous mmap, both written to. */+ u64 cur = (u64)sys1(NR_brk, 0);+ u64 grown = (u64)sys1(NR_brk, cur + 65536);+ if (grown == cur + 65536) ((volatile char *)cur)[65535] = 1;+ check(grown == cur + 65536, "brk grows the heap");+ char *m = (char *)sys6(NR_mmap, 0, 8192, 3, 0x22, -1, 0); /* RW, PRIVATE|ANON */+ if ((u64)m < (u64)-4096) m[8191] = 7;+ check((u64)m < (u64)-4096 && sys2(NR_munmap, m, 8192) == 0, "mmap/munmap anonymous");++ /* Files, with C-string paths and the *at() calls musl uses. */+ join(work, dir, "rawabi.d");+ join(f1, work, "a.txt");+ join(f2, work, "b.txt");+ sys1(NR_unlink, f1); sys1(NR_unlink, f2); sys1(NR_rmdir, work); /* leftovers */+ check(sys2(NR_mkdir, work, 0755) == 0, "mkdir");+ int fd = (int)sys4(NR_openat, AT_FDCWD, f1, O_WRONLY | O_CREAT | O_TRUNC, 0644);+ struct iovec iov[2] = { { "hello ", 6 }, { "from rawabi\n", 12 } };+ check(fd >= 0 && sys3(NR_writev, fd, iov, 2) == 18, "openat(AT_FDCWD, O_CREAT) + writev");+ sys1(NR_close, fd);++ struct kstat st;+ check(sys2(NR_stat, f1, &st) == 0 && (st.mode & S_IFMT) == S_IFREG && st.size == 18, "stat: regular file, 18 bytes");+ check(sys4(NR_newfstatat, AT_FDCWD, work, &st, 0) == 0 && (st.mode & S_IFMT) == S_IFDIR, "newfstatat: directory");+ check(sys2(NR_access, f1, 4) == 0 && sys2(NR_access, f2, 0) == -ENOENT, "access: R_OK / ENOENT");+ check(sys2(NR_stat, (char *)0, &st) == -EFAULT, "stat(NULL) = -EFAULT");++ check(sys2(NR_rename, f1, f2) == 0 && sys2(NR_stat, f1, &st) == -ENOENT, "rename");+ char buf[64];+ fd = (int)sys4(NR_openat, AT_FDCWD, f2, O_RDONLY, 0);+ i64 n = sys3(NR_read, fd, buf, sizeof buf);+ struct kstat fst;+ check(n == 18 && buf[0] == 'h' && buf[17] == '\n', "read back the renamed file");+ check(sys2(NR_fstat, fd, &fst) == 0 && fst.size == 18 && sys3(NR_lseek, fd, 6, 0) == 6, "fstat + lseek");+ sys1(NR_close, fd);++ /* getdents64 on the directory finds b.txt. */+ fd = (int)sys4(NR_openat, AT_FDCWD, work, O_RDONLY | O_DIRECTORY, 0);+ char dents[512];+ int found = 0;+ n = sys3(NR_getdents64, fd, dents, sizeof dents);+ for (i64 off = 0; off < n;) {+ unsigned short reclen = *(unsigned short *)(dents + off + 16);+ if (seq(dents + off + 19, "b.txt")) found = 1;+ off += reclen;+ }+ sys1(NR_close, fd);+ check(found, "getdents64 lists b.txt");++ check(sys1(NR_chdir, work) == 0 && sys2(NR_getcwd, cwd, sizeof cwd) > 0 && seq(cwd, work), "chdir + getcwd");+ check(sys3(NR_unlinkat, AT_FDCWD, "b.txt", 0) == 0, "unlinkat(AT_FDCWD, relative path)");+ sys1(NR_chdir, dir);+ check(sys3(NR_unlinkat, AT_FDCWD, work, AT_REMOVEDIR) == 0, "unlinkat(AT_REMOVEDIR)");++ /* Time. */+ struct timespec t0, t1, nap = { 0, 20 * 1000 * 1000 };+ sys2(NR_clock_gettime, 1, &t0);+ i64 slept = sys4(NR_clock_nanosleep, 1, 0, &nap, 0);+ sys2(NR_clock_gettime, 1, &t1);+ i64 ms = (t1.sec - t0.sec) * 1000 + (t1.nsec - t0.nsec) / 1000000;+ check(slept == 0 && ms >= 19, "clock_nanosleep(CLOCK_MONOTONIC, 20 ms)");++ /* SSE math, then a signal whose handler clobbers xmm registers. */+ double pi = leibniz_pi(200000);+ double root = sqrt_sd(pi * pi);+ puts_("rawabi: pi ~ "); putf(pi); puts_(", sqrt(pi^2) = "); putf(root); puts_("\n");+ check(pi > 3.14158 && pi < 3.14160 && root == pi, "SSE double math");+ struct kaction sa = { on_usr1, SA_RESTORER, rawabi_restorer, 0 };+ check(sys4(NR_rt_sigaction, SIGUSR1, &sa, 0, 8) == 0, "rt_sigaction with SA_RESTORER");+ double before = pi * 2.0; /* lives in an xmm register across... */+ sys2(NR_kill, sys0(NR_getpid), SIGUSR1); /* ...the signal */+ double after = before / 2.0;+ check(got_signal == SIGUSR1 && handler_result == 2.0 && after == pi, "xmm state survives a signal handler");++ /* fork + wait4 with a rusage buffer, the way musl's waitpid calls it. */+ i64 pid = sys0(NR_fork);+ if (pid == 0) {+ double c = leibniz_pi(1000); /* the child has its own FPU state */+ sys1(NR_exit, c > 3.0 ? 7 : 1);+ }+ int status = 0;+ char rusage[144];+ check(pid > 0 && sys4(NR_wait4, pid, &status, 0, rusage) == pid && ((status >> 8) & 0xff) == 7,+ "fork + wait4(pid, &status, 0, &rusage)");++ puts_(failures ? "rawabi: some checks FAILED\n" : "rawabi: all checks passed\n");+ return failures;+}++/* The ELF entry point. The kernel leaves argc at (%rsp), then argv[];+ * the stack is 16-byte aligned here, so align it the C way and call run(). */+__attribute__((used)) static void start_c(u64 *sp) {+ int argc = (int)sp[0];+ sys1(NR_exit_group, run(argc, (char **)(sp + 1)));+}+__asm__(".text\n.global _start\n_start:\n"+ " xor %ebp, %ebp\n mov %rsp, %rdi\n and $-16, %rsp\n call start_c\n hlt\n");
@@ -0,0 +1,221 @@+/*+ * remaptest.c -- stage 19: mremap, checked from musl.+ *+ * Built with `musl-gcc -static-pie` (see build.rs). mremap makes an mmap+ * region bigger or smaller, moving it if needed; realloc() uses it for big+ * blocks so the bytes don't have to be copied.+ *+ * remaptest all checks (exit code = number of failed checks)+ */+#define _GNU_SOURCE+#include <errno.h>+#include <fcntl.h>+#include <signal.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/mman.h>+#include <sys/wait.h>+#include <unistd.h>++#define PG 4096UL++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("remaptest: FAIL line %d: ", __LINE__); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++static char *anon(size_t pages)+{+ char *p = mmap(0, pages * PG, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ return p == MAP_FAILED ? 0 : p;+}++static void fill(char *p, size_t pages, int seed)+{+ for (size_t i = 0; i < pages; i++)+ memset(p + i * PG, seed + (int)i, PG);+}++static int same(const char *p, size_t pages, int seed)+{+ for (size_t i = 0; i < pages; i++)+ for (size_t j = 0; j < PG; j += 512)+ if (p[i * PG + j] != (char)(seed + (int)i))+ return 0;+ return 1;+}++static int zero(const char *p, size_t len)+{+ for (size_t i = 0; i < len; i += 256)+ if (p[i])+ return 0;+ return 1;+}++/* Does touching `p` kill a child with SIGSEGV? (= nothing mapped there) */+static int faults(volatile char *p)+{+ pid_t pid = fork();+ if (pid == 0) {+ (void)*p;+ _exit(0);+ }+ int st = 0;+ waitpid(pid, &st, 0);+ return WIFSIGNALED(st) && WTERMSIG(st) == SIGSEGV;+}++static void grow_in_place(void)+{+ char *p = anon(8);+ CHECK(p, "mmap 8 pages");+ munmap(p + 3 * PG, 5 * PG); /* keep 3, leave room behind them */+ fill(p, 3, 'a');+ char *q = mremap(p, 3 * PG, 6 * PG, 0);+ CHECK(q == p, "grow in place: %p, wanted %p", q, p);+ if (q != p)+ return;+ CHECK(same(p, 3, 'a'), "grow in place kept the data");+ CHECK(zero(p + 3 * PG, 3 * PG), "the new part is zero");+ p[6 * PG - 1] = 1; /* and writable */+ q = mremap(p, 6 * PG, 2 * PG, 0);+ CHECK(q == p, "shrink: %p", q);+ CHECK(same(p, 2, 'a'), "shrink kept the data");+ CHECK(faults(p + 2 * PG), "the cut-off part is gone");+ CHECK(mremap(p, 2 * PG, 2 * PG, 0) == p, "same size: nothing to do");+ munmap(p, 2 * PG);+}++static void move(void)+{+ char *a = anon(2);+ char *b = anon(1); /* right below a: can't grow in place */+ CHECK(a && b && b + PG == a, "b (%p) sits right below a (%p)", b, a);+ fill(b, 1, 'm');+ errno = 0;+ CHECK(mremap(b, PG, 4 * PG, 0) == MAP_FAILED && errno == ENOMEM, "no room and no MAYMOVE: ENOMEM (%d)", errno);+ char *c = mremap(b, PG, 4 * PG, MREMAP_MAYMOVE);+ CHECK(c != MAP_FAILED && c != b, "MAYMOVE moved it: %p", c);+ if (c == MAP_FAILED)+ return;+ CHECK(same(c, 1, 'm'), "the moved page kept its bytes");+ CHECK(zero(c + PG, 3 * PG), "the rest is zero");+ CHECK(faults(b), "nothing left at the old address");++ /* MREMAP_FIXED: to an address we pick. */+ char *spot = anon(4);+ munmap(spot, 4 * PG);+ char *d = mremap(c, 4 * PG, 2 * PG, MREMAP_MAYMOVE | MREMAP_FIXED, spot);+ CHECK(d == spot, "MREMAP_FIXED: %p, wanted %p", d, spot);+ if (d == spot)+ CHECK(same(d, 1, 'm'), "MREMAP_FIXED kept the bytes");+ CHECK(faults(c), "and the old place is empty");+ munmap(spot, 2 * PG);+ munmap(a, 2 * PG);+}++static void errors(void)+{+ char *p = anon(2);+ errno = 0;+ CHECK(mremap(p + 1, PG, 2 * PG, MREMAP_MAYMOVE) == MAP_FAILED && errno == EINVAL, "unaligned: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 0, MREMAP_MAYMOVE) == MAP_FAILED && errno == EINVAL, "new_len 0: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, 64) == MAP_FAILED && errno == EINVAL, "unknown flag: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, MREMAP_FIXED, p + 8 * PG) == MAP_FAILED && errno == EINVAL,+ "FIXED without MAYMOVE: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, 2 * PG, 4 * PG, MREMAP_MAYMOVE | MREMAP_FIXED, p + PG) == MAP_FAILED && errno == EINVAL,+ "FIXED onto itself: EINVAL (%d)", errno);+ munmap(p, 2 * PG);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, MREMAP_MAYMOVE) == MAP_FAILED && errno == EFAULT, "nothing mapped: EFAULT (%d)", errno);+}++static void file_backed(void)+{+ int fd = open("/tmp/remap.dat", O_RDWR | O_CREAT | O_TRUNC, 0644);+ CHECK(fd >= 0, "open /tmp/remap.dat");+ char page[PG];+ for (int i = 0; i < 3; i++) {+ memset(page, 'F' + i, PG);+ write(fd, page, PG);+ }+ char *keep = anon(1); /* so the mapping below can't grow in place */+ char *f = mmap(0, PG, PROT_READ, MAP_PRIVATE, fd, 0);+ (void)keep;+ CHECK(f != MAP_FAILED && f[0] == 'F', "file page 0");+ char *g = mremap(f, PG, 3 * PG, MREMAP_MAYMOVE);+ CHECK(g != MAP_FAILED, "mremap a file mapping");+ if (g != MAP_FAILED) {+ CHECK(g[0] == 'F' && g[PG] == 'G' && g[2 * PG + 7] == 'H', "moved file pages read the file: %c %c %c", g[0],+ g[PG], g[2 * PG + 7]);+ munmap(g, 3 * PG);+ }+ munmap(keep, PG);+ close(fd);+ unlink("/tmp/remap.dat");+}++static void after_fork(void)+{+ char *p = anon(1);+ fill(p, 1, 'p');+ pid_t pid = fork();+ if (pid == 0) {+ char *q = mremap(p, PG, 16 * PG, MREMAP_MAYMOVE);+ if (q == MAP_FAILED || q[0] != 'p')+ _exit(1);+ q[0] = 'c'; /* copy-on-write must still work after the move */+ _exit(q[0] == 'c' ? 0 : 2);+ }+ int st = 0;+ waitpid(pid, &st, 0);+ CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "child moved and wrote: status %#x", st);+ CHECK(p[0] == 'p', "parent's copy unchanged: %c", p[0]);+ munmap(p, PG);+}++static void with_realloc(void)+{+ size_t n = 1 << 20;+ unsigned *v = malloc(n);+ CHECK(v, "malloc 1 MiB");+ for (size_t i = 0; i < n / 4; i++)+ v[i] = (unsigned)i * 2654435761u;+ for (size_t big = 4 << 20; big <= 32 << 20; big *= 2) {+ v = realloc(v, big);+ CHECK(v, "realloc %zu", big);+ if (!v)+ return;+ size_t bad = 0;+ for (size_t i = 0; i < n / 4; i++)+ bad += v[i] != (unsigned)i * 2654435761u;+ CHECK(bad == 0, "realloc to %zu MiB kept the data (%zu wrong)", big >> 20, bad);+ }+ v = realloc(v, 2 << 20);+ CHECK(v && v[12345] == 12345u * 2654435761u, "realloc down");+ free(v);+}++int main(void)+{+ grow_in_place();+ move();+ errors();+ file_backed();+ after_fork();+ with_realloc();+ printf("remaptest: %s (%d failed)\n", fails ? "FAILED" : "all passed", fails);+ return fails;+}
@@ -0,0 +1,30 @@+//! rm path... -- delete files, and empty directories (shell alias: del).+//! (Real rm needs -r for directories; this one just tries rmdir.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ if argc() < 2 {+ eprintln!("usage: rm path...");+ return 2;+ }+ let mut status = 0;+ for i in 1..argc() {+ let path = arg(i).unwrap_or("");+ let mut r = unlink(path);+ if r == -EISDIR {+ r = rmdir(path); // fails with ENOTEMPTY if there's something in it+ }+ if r < 0 {+ eprintln!("rm: {}: {}", path, strerror(r));+ status = 1;+ }+ }+ status+}
@@ -0,0 +1,786 @@+//! sh -- the Rustbelt shell, a normal user program (/bin/sh on the disk).+//!+//! sh interactive: prompt, line editing, history+//! sh script.sh run the commands in a file (echoing each one)+//!+//! It reads a line, parses it (shparse.rs: words, `|`, `<`, `>`, `>>`, `&`,+//! drive letters), then either runs a *builtin* (cd, pwd, echo, ... -- these+//! must change the shell itself, or are just tiny) or starts a program from+//! a `bin` directory the Unix way: `fork` a copy of the shell, and in the+//! copy connect pipes and files to stdin/stdout with `dup2`, then `execve`+//! the program. Everything goes through the same syscalls any program+//! could use.+//!+//! Job control: every pipeline gets its own *process group*, and while it+//! runs in the foreground the console's foreground group is set to it, so+//! Ctrl+C (SIGINT) and Ctrl+Z (SIGTSTP) reach the program, not the shell+//! (which ignores both). `fg` / `bg` continue a stopped job; `kill` sends+//! any signal.++#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;+#[path = "shparse.rs"]+mod shparse;++use alloc::{+ format,+ string::{String, ToString},+ vec::Vec,+};+use core::fmt::Write;+use shparse::Drive;+use ulib::*;++/// Built-in aliases (DOS names for the Unix-y programs).+/// Plus Linux names for the network tools.+const DEFAULT_ALIASES: &[(&str, &str)] = &[+ ("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp"),+ ("ip", "ifconfig"), ("nslookup", "host"), ("wget", "fetch"),+];++const HELP: &str = "\+Rustbelt shell commands+ builtins cd [dir] pwd echo [text] ver drives alias [name=value]+ history jobs wait [pid] cls / clear help exit [code]+ kill [-SIG] pid|%job ... kill -l fg [%job] bg [%job]+ programs ls [-l] cat wc grep [-i] [-v] mkdir rm touch cp sleep sysinfo+ network ifconfig (ip) ping [-c n] host host name (nslookup)+ fetch [-o file] http://... (wget) httpd [-p port] [-n count]+ devices random [-n bytes | -d sides [n]] getty [-1] /dev/hvc0 tile [n] | tile -r+ (bin dirs) hello demo evil spin [rounds] overflow child sh+ tests forktest cowtest mmaptest sigtest+ run [--trace] prog args run a program (--trace prints its syscalls)+ start prog args run it in the background (or: prog args &)+ a | b | c pipes; > file >> file < file redirection+ Ctrl+C interrupt the running program (SIGINT); Ctrl+Z stops it+ A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt+";++/// A background (or stopped) job: one pipeline, one process group.+struct Job {+ pid: i64, // the last program of the pipeline (its exit code counts)+ pgid: i64, // the group: the first program's pid+ pids: Vec<i64>, // programs not reaped yet+ command: String,+ stopped: bool,+}++/// One command of a pipeline, after alias, drive letter and `run`/`start`+/// processing.+struct Stage {+ argv: Vec<String>,+ stdin: Option<String>,+ stdout: Option<(String, bool)>, // (file, append)+ trace: bool,+ program_only: bool, // `run x` always runs a program, never a builtin+}++struct Shell {+ drives: Vec<Drive>,+ aliases: Vec<(String, String)>,+ history: Vec<String>,+ jobs: Vec<Job>,+ status: i64, // exit code of the last command+ exit: Option<i64>, // set by the `exit` builtin+ job_control: bool, // interactive: process groups + the console's foreground+}++fn main() -> i64 {+ let mut sh = Shell {+ drives: Vec::new(),+ aliases: DEFAULT_ALIASES.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect(),+ history: Vec::new(),+ jobs: Vec::new(),+ status: 0,+ exit: None,+ job_control: false,+ };++ // Where do commands come from? A script file, or stdin. If stdin is the+ // console we're interactive; otherwise (a pipe, `sh < file`) we read+ // lines just like from a script.+ let fd = match arg(1) {+ Some(path) => {+ // O_CLOEXEC: the programs we fork + exec mustn't inherit it.+ let fd = open(path, O_CLOEXEC);+ if fd < 0 {+ eprintln!("sh: {}: {}", path, strerror(fd));+ return 127;+ }+ fd+ }+ None => 0,+ };+ let interactive = fd == 0 && isatty(0);+ let mut input = LineReader { fd, buf: Vec::new(), eof: false };++ sh.refresh_drives();+ if interactive {+ // Job control: be the leader of our own process group, own the+ // console, and ignore the keyboard signals -- they're for the+ // programs we run, not for us.+ sh.job_control = true;+ setpgid(0, 0);+ tcsetpgrp(0, getpgrp());+ for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+ sigaction_raw(sig, SIG_IGN, 0, 0);+ }+ let disks: Vec<String> = sh.drives.iter().map(|d| format!("{}: = {}", d.letter, d.path)).collect();+ println!("Rustbelt shell. Type `help` for commands. Drives: {}", disks.join(", "));+ }++ while sh.exit.is_none() {+ sh.refresh_drives();+ sh.reap_jobs();+ let prompt = sh.prompt();+ let line = if interactive {+ sh.edit_line(&prompt)+ } else {+ match input.read_line() {+ Some(line) => {+ println!("{}{}", prompt, line); // show what's being run+ line+ }+ None => break, // end of the script+ }+ };+ sh.run_line(&line, interactive);+ }+ sh.exit.unwrap_or(sh.status)+}++impl Shell {+ /// Ask the kernel for the mount table and (re)assign drive letters.+ fn refresh_drives(&mut self) {+ let mut buf = [0u8; 1024];+ let n = mounts(&mut buf).max(0) as usize;+ self.drives = shparse::assign_drives(core::str::from_utf8(&buf[..n]).unwrap_or(""));+ }++ fn cwd(&self) -> String {+ let mut buf = [0u8; 256];+ let n = getcwd(&mut buf).max(0) as usize;+ String::from_utf8_lossy(&buf[..n]).into_owned()+ }++ /// `A:/ $ `, `B:/notes $ `+ fn prompt(&self) -> String {+ format!("{} $ ", shparse::display(&self.cwd(), &self.drives))+ }++ /// Print "[done]" for background jobs that have finished (without+ /// blocking: WNOHANG says 0 while a program is still running).+ fn reap_jobs(&mut self) {+ self.jobs.retain_mut(|job| {+ let mut code = 0;+ job.pids.retain(|&pid| match wait4(pid, WNOHANG | WUNTRACED) {+ Ok((0, _)) => true, // still running+ Ok((_, status)) if wifstopped(status) => {+ job.stopped = true;+ true+ }+ Ok((_, status)) => {+ if pid == job.pid {+ code = exit_code(status);+ }+ false+ }+ Err(_) => false, // gone already+ });+ if !job.pids.is_empty() {+ return true;+ }+ println!("[done] pid {} ({}), exit code {}", job.pid, job.command, code);+ false+ });+ }++ /// Read a line from the keyboard with editing. The console is put in+ /// raw mode so we get every key (and arrow keys as ESC [ A..D) and do+ /// the echo ourselves; back to normal ("cooked") before running+ /// anything, so programs see the usual line-at-a-time input.+ fn edit_line(&mut self, prompt: &str) -> String {+ print!("{}", prompt);+ set_raw(0, true);+ let mut line: Vec<u8> = Vec::new();+ let mut hist = self.history.len(); // position while browsing history+ let mut esc = 0; // 0: normal, 1: after ESC, 2: after ESC [+ // One byte per read: if several lines were typed (or pasted) ahead,+ // the rest stays in the tty for the next prompt instead of being+ // lost when we stop at the first Enter.+ let mut buf = [0u8; 1];+ 'read: loop {+ let n = read(0, &mut buf);+ if n <= 0 {+ break;+ }+ for &b in &buf[..n as usize] {+ match (esc, b) {+ (0, 0x1b) => esc = 1,+ (1, b'[') => esc = 2,+ (1, _) => esc = 0,+ (2, b'0'..=b'9' | b';') => {} // parameters, e.g. ESC [ 3 ~+ (2, key) => {+ esc = 0;+ // Up / Down: replace the line with an older/newer one.+ let new = match key {+ b'A' if hist > 0 => hist - 1,+ b'B' if hist < self.history.len() => hist + 1,+ _ => continue, // Left/Right/Home/... not supported+ };+ hist = new;+ let text = self.history.get(hist).map(|s| s.as_bytes()).unwrap_or(b"");+ erase(line.len());+ line = text.to_vec();+ write(1, &line);+ }+ (_, b'\n' | b'\r') => {+ println!();+ break 'read;+ }+ (_, 0x08 | 0x7f) => {+ if line.pop().is_some() {+ erase(1);+ }+ }+ (_, 0x03) => {+ // Ctrl+C while typing: throw the line away (in raw+ // mode the console doesn't make it a signal).+ println!("^C");+ line.clear();+ break 'read;+ }+ (_, 0x20..=0x7e) => {+ line.push(b);+ write(1, &[b]); // echo+ }+ _ => {} // Tab, other control keys: ignored+ }+ }+ }+ set_raw(0, false);+ String::from_utf8_lossy(&line).into_owned()+ }++ fn run_line(&mut self, line: &str, interactive: bool) {+ let line = line.trim();+ if line.is_empty() {+ return;+ }+ if interactive && self.history.last().map(|h| h.as_str()) != Some(line) {+ self.history.push(line.to_string());+ }+ match shparse::parse(line) {+ Ok(Some(pipeline)) => self.status = self.run_pipeline(pipeline, line),+ Ok(None) => {} // just a comment+ Err(e) => {+ eprintln!("sh: {}", e);+ self.status = 2;+ }+ }+ }++ /// Words -> argv strings: drive letters translated (unless quoted),+ /// aliases expanded, `run [--trace]` / `start` prefixes removed.+ fn prepare(&self, cmd: &shparse::Command, background: &mut bool) -> Result<Stage, String> {+ let path = |w: &shparse::Word| match w.quoted {+ false => shparse::translate(&w.text, &self.drives).unwrap_or_else(|| w.text.clone()),+ true => w.text.clone(),+ };+ let mut argv: Vec<String> = cmd.words.iter().map(path).collect();+ let mut stage = Stage {+ argv: Vec::new(),+ stdin: cmd.stdin.as_ref().map(path),+ stdout: cmd.stdout.as_ref().map(|(w, append)| (path(w), *append)),+ trace: false,+ program_only: false,+ };+ // `run` / `start` prefixes (`start run --trace x` works too).+ loop {+ match argv.first().map(|s| s.as_str()) {+ Some("run") => {+ argv.remove(0);+ stage.program_only = true;+ if argv.first().map(|s| s.as_str()) == Some("--trace") {+ argv.remove(0);+ stage.trace = true;+ }+ }+ Some("start") => {+ argv.remove(0);+ stage.program_only = true;+ *background = true;+ }+ _ => break,+ }+ }+ if argv.is_empty() {+ return Err(String::from("run/start: which program?"));+ }+ // Aliases: replace the command name (a few rounds, for aliases of+ // aliases; the limit stops `alias a=a` from looping forever).+ for _ in 0..5 {+ let Some((_, value)) = self.aliases.iter().find(|(name, _)| *name == argv[0]) else { break };+ let mut words: Vec<String> = value.split_whitespace().map(|s| s.to_string()).collect();+ if words.is_empty() || words[0] == argv[0] {+ break;+ }+ words.extend(argv.drain(1..));+ argv = words;+ }+ stage.argv = argv;+ Ok(stage)+ }++ /// Run `a < in | b | c > out`: create the pipes, spawn every program+ /// with the right stdin/stdout, then wait for them (unless background).+ /// Returns the exit code of the last command.+ fn run_pipeline(&mut self, pipeline: shparse::Pipeline, line: &str) -> i64 {+ let mut background = pipeline.background;+ let mut stages = Vec::new();+ for cmd in &pipeline.commands {+ match self.prepare(cmd, &mut background) {+ Ok(stage) => stages.push(stage),+ Err(e) => {+ eprintln!("sh: {}", e);+ return 2;+ }+ }+ }++ let mut opened: Vec<i64> = Vec::new(); // fds to close when done+ let mut children: Vec<(i64, String)> = Vec::new();+ let mut pgid = 0; // the job's process group = its first program's pid+ let mut status = 0;+ let mut prev_pipe = -1; // read end of the pipe from the previous stage+ let mut last_pid = None; // the last stage, if it's a program+ for (i, stage) in stages.iter().enumerate() {+ // stdin: `< file`, else the previous pipe, else ours (-1).+ let mut stdin = prev_pipe;+ if let Some(file) = &stage.stdin {+ stdin = open(file, O_CLOEXEC);+ if stdin < 0 {+ eprintln!("sh: {}: {}", file, strerror(stdin));+ status = 1;+ break;+ }+ opened.push(stdin);+ }+ // stdout: `> file` / `>> file`, else a new pipe to the next+ // stage, else ours (-1).+ let mut stdout = -1;+ prev_pipe = -1;+ if i + 1 < stages.len() {+ // O_CLOEXEC: the programs get these only as their stdin+ // / stdout (dup2), not as extra open fds.+ let (r, w) = pipe2(O_CLOEXEC).unwrap_or((-1, -1));+ opened.extend([r, w]);+ stdout = w;+ prev_pipe = r;+ }+ if let Some((file, append)) = &stage.stdout {+ let flags = O_WRONLY | O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC };+ stdout = open(file, flags);+ if stdout < 0 {+ eprintln!("sh: {}: {}", file, strerror(stdout));+ status = 1;+ break;+ }+ opened.push(stdout);+ }++ if !stage.program_only {+ if let Some(code) = self.builtin(&stage.argv, if stdout < 0 { 1 } else { stdout }) {+ status = code;+ continue;+ }+ }+ match self.spawn(stage, stdin, stdout, pgid, !background) {+ Ok(pid) => {+ if pgid == 0 {+ pgid = pid;+ }+ children.push((pid, stage.argv[0].clone()));+ if i + 1 == stages.len() {+ last_pid = Some(pid);+ }+ }+ Err(code) => {+ status = code;+ break;+ }+ }+ }+ // Close our copies of the pipe ends and files: a reader only sees+ // end-of-file once *every* write end is closed, including ours.+ for fd in opened {+ close(fd);+ }++ if children.is_empty() {+ return status;+ }+ let mut job = Job {+ pid: last_pid.unwrap_or(children[children.len() - 1].0),+ pgid,+ pids: children.iter().map(|(pid, _)| *pid).collect(),+ command: line.to_string(),+ stopped: false,+ };+ if background {+ for (pid, name) in &children {+ println!("[pid {}] {} started in the background", pid, name);+ }+ self.jobs.push(job);+ return 0;+ }+ match self.wait_job(&mut job) {+ Some(code) if last_pid.is_some() => code,+ Some(_) => status, // the last stage was a builtin+ None => 148, // stopped (128 + SIGTSTP)+ }+ }++ /// Wait for a foreground job: it owns the console until all of its+ /// programs end (-> the exit code of the last one) or it's stopped by+ /// Ctrl+Z (-> None, and it goes on the job list).+ fn wait_job(&mut self, job: &mut Job) -> Option<i64> {+ if self.job_control {+ tcsetpgrp(0, job.pgid);+ }+ let mut code = 0;+ let mut signal = 0;+ while let Some(&next) = job.pids.first() {+ match wait4(next, WUNTRACED) {+ Ok((_, status)) if wifstopped(status) => {+ job.stopped = true;+ break;+ }+ Ok((pid, status)) => {+ job.pids.retain(|&p| p != pid);+ if pid == job.pid {+ code = exit_code(status);+ signal = if wifsignaled(status) { wtermsig(status) } else { 0 };+ }+ }+ Err(e) if e == -EINTR => {}+ Err(_) => break, // no children left (someone else reaped them)+ }+ }+ if self.job_control {+ tcsetpgrp(0, getpgrp()); // the console is ours again+ }+ if job.stopped {+ println!("\n[stopped] pid {} ({}) -- `fg` to continue", job.pid, job.command);+ self.jobs.push(Job { pids: core::mem::take(&mut job.pids), command: core::mem::take(&mut job.command), ..*job });+ return None;+ }+ match signal {+ 0 if code != 0 => println!("[exit code {}]", code),+ 0 => {}+ SIGINT => println!("[exit code {}] (interrupted: SIGINT)", code),+ sig => println!("[exit code {}] (killed by SIG{})", code, SIGNAL_NAMES.get(sig as usize).unwrap_or(&"?")),+ }+ Some(code)+ }++ /// `%2` (job number), `%` (the latest job) or a pid -> index in jobs.+ fn find_job(&self, spec: Option<&str>) -> Option<usize> {+ match spec {+ None | Some("%") => self.jobs.len().checked_sub(1),+ Some(s) => match s.strip_prefix('%') {+ Some(n) => n.parse::<usize>().ok().filter(|&n| n >= 1 && n <= self.jobs.len()).map(|n| n - 1),+ None => {+ let pid: i64 = s.parse().ok()?;+ self.jobs.iter().position(|j| j.pid == pid || j.pgid == pid)+ }+ },+ }+ }++ /// Find a program: names with a `/` are paths; others are looked up in+ /// the `bin` directory of every drive (our PATH): A:/bin, B:/bin, ...+ fn find_program(&self, name: &str) -> Option<String> {+ if name.contains('/') {+ return Some(name.to_string());+ }+ let mut st = Stat::default();+ self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.is_file())+ }++ /// Start one program: fork, then in the child set up its process+ /// group, stdin/stdout and signals, and execve it.+ fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64, pgid: i64, foreground: bool) -> Result<i64, i64> {+ let name = &stage.argv[0];+ let Some(path) = self.find_program(name) else {+ eprintln!("sh: {}: command not found (try `help`)", name);+ return Err(127);+ };+ let pid = fork();+ if pid < 0 {+ eprintln!("sh: {}: fork: {}", name, strerror(pid));+ return Err(126);+ }+ if pid == 0 {+ // --- the child: still the shell's code, in a copy of its memory.+ if self.job_control {+ setpgid(0, pgid); // pgid 0: a new group, named after me+ if foreground {+ tcsetpgrp(0, getpgrp());+ }+ // Ignored signals stay ignored across execve: undo ours.+ for sig in [SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU] {+ sigaction_raw(sig, SIG_DFL, 0, 0);+ }+ }+ if stdin >= 0 {+ dup2(stdin, 0); // the new fd 0 doesn't close on exec+ }+ if stdout >= 0 {+ dup2(stdout, 1);+ }+ if stage.trace {+ trace_me(true);+ }+ let argv: Vec<&str> = stage.argv.iter().map(|s| s.as_str()).collect();+ let err = execve(&path, &argv, &environ());+ // Only reached if execve failed.+ eprintln!("sh: {}: {}", name, strerror(err));+ exit(126);+ }+ // --- the parent. Set the group here too: whichever of us runs+ // first, the child is in its group before we wait or tcsetpgrp.+ if self.job_control {+ setpgid(pid, if pgid == 0 { pid } else { pgid });+ }+ Ok(pid)+ }++ /// Run a builtin, writing its output to `out`. `None` if `argv[0]`+ /// isn't one.+ fn builtin(&mut self, argv: &[String], out: i64) -> Option<i64> {+ let mut out = Fd(out);+ let arg1 = argv.get(1).map(|s| s.as_str());+ let code = match argv[0].as_str() {+ "cd" => {+ let dir = arg1.unwrap_or("/");+ let r = chdir(dir);+ if r < 0 {+ match shparse::drive_letter(dir) {+ // Still has its letter: translate() didn't know it.+ Some(letter) => eprintln!("cd: there is no drive {}: (see `drives`)", letter),+ None => eprintln!("cd: {}: {}", dir, strerror(r)),+ }+ 1+ } else {+ 0+ }+ }+ "pwd" => {+ let _ = writeln!(out, "{}", self.cwd());+ 0+ }+ "echo" => {+ let _ = writeln!(out, "{}", argv[1..].join(" "));+ 0+ }+ "help" => {+ let _ = write!(out, "{}", HELP);+ 0+ }+ "exit" => {+ self.exit = Some(arg1.and_then(|s| s.parse().ok()).unwrap_or(self.status));+ 0+ }+ "cls" | "clear" => {+ let _ = write!(out, "\x0c"); // form feed: the console clears the screen+ 0+ }+ "ver" => {+ let mut buf = [0u8; 6 * 65];+ let [sysname, _node, release, version, machine, _] = uname(&mut buf);+ let _ = writeln!(out, "{} {} ({}) {}", sysname, release, version, machine);+ 0+ }+ "drives" => {+ for d in &self.drives {+ let _ = writeln!(out, "{}: {:<12} {}", d.letter, d.path, d.fs);+ }+ 0+ }+ "alias" => match arg1 {+ None => {+ for (name, value) in &self.aliases {+ let _ = writeln!(out, "{} = {}", name, value);+ }+ 0+ }+ Some(def) => match def.split_once('=') {+ Some((name, value)) => {+ self.aliases.retain(|(n, _)| n != name);+ self.aliases.push((name.to_string(), value.to_string()));+ 0+ }+ None => {+ eprintln!("alias: use alias name=value");+ 2+ }+ },+ },+ "history" => {+ for (i, line) in self.history.iter().enumerate() {+ let _ = writeln!(out, "{:4} {}", i + 1, line);+ }+ 0+ }+ "jobs" => {+ self.reap_jobs();+ for (i, job) in self.jobs.iter().enumerate() {+ let state = if job.stopped { "stopped" } else { "running" };+ let _ = writeln!(out, "[{}] [pid {}] {} {}", i + 1, job.pid, state, job.command);+ }+ 0+ }+ "wait" => {+ // `wait` waits for all background jobs, `wait 7` for pid 7.+ let only: Option<i64> = arg1.and_then(|s| s.parse().ok());+ let mut code = 0;+ self.jobs.retain(|job| {+ if only.is_some_and(|pid| pid != job.pid) {+ return true;+ }+ for &pid in &job.pids {+ let c = wait(pid);+ if pid == job.pid {+ code = c;+ }+ }+ let _ = writeln!(out, "[done] pid {} ({}), exit code {}", job.pid, job.command, code);+ false+ });+ code+ }+ "kill" => self.kill(&argv[1..], &mut out),+ "fg" | "bg" => {+ let Some(i) = self.find_job(arg1) else {+ eprintln!("{}: no such job", argv[0]);+ return Some(1);+ };+ let mut job = self.jobs.remove(i);+ kill(-job.pgid, SIGCONT); // wake the whole pipeline up+ job.stopped = false;+ if argv[0] == "bg" {+ let _ = writeln!(out, "[pid {}] {} continued in the background", job.pid, job.command);+ self.jobs.push(job);+ 0+ } else {+ let _ = writeln!(out, "{}", job.command);+ self.wait_job(&mut job).unwrap_or(148)+ }+ }+ _ => return None,+ };+ Some(code)+ }+}++impl Shell {+ /// `kill [-SIG] pid|%job ...` and `kill -l`. Default signal: SIGTERM.+ fn kill(&self, args: &[String], out: &mut Fd) -> i64 {+ let mut sig = SIGTERM;+ let mut args = args;+ match args.first().map(|s| s.as_str()) {+ Some("-l") => {+ for (i, name) in SIGNAL_NAMES.iter().enumerate().skip(1) {+ let _ = write!(out, "{:2}) SIG{:<7}{}", i, name, if i % 5 == 0 { "\n" } else { " " });+ }+ let _ = writeln!(out);+ return 0;+ }+ Some(opt) if opt.starts_with('-') && opt.len() > 1 && !opt[1..].starts_with(|c: char| c == '-') => {+ let name = opt[1..].strip_prefix("s ").unwrap_or(&opt[1..]);+ match signal_number(name) {+ Some(n) => sig = n,+ None => {+ eprintln!("kill: {}: unknown signal (see `kill -l`)", name);+ return 2;+ }+ }+ args = &args[1..];+ }+ _ => {}+ }+ if args.is_empty() {+ eprintln!("usage: kill [-SIGNAL] pid|%job ... (kill -l lists signals)");+ return 2;+ }+ let mut code = 0;+ for target in args {+ // %n = a job's whole process group (negative pid).+ let pid = if target.starts_with('%') {+ // (Without job control the job has no group of its own.)+ let job = |i: usize| if self.job_control { -self.jobs[i].pgid } else { self.jobs[i].pid };+ self.find_job(Some(target)).map(job)+ } else {+ target.parse::<i64>().ok()+ };+ let r = match pid {+ Some(pid) => kill(pid, sig),+ None => -ESRCH,+ };+ if r < 0 {+ eprintln!("kill: {}: {}", target, if r == -ESRCH { "No such process" } else { strerror(r) });+ code = 1;+ }+ }+ code+ }+}++/// Erase `n` characters before the cursor (the console treats \x08 as+/// "back one column and blank it").+fn erase(n: usize) {+ for _ in 0..n {+ write(1, b"\x08");+ }+}++/// Reads a file (or pipe) line by line.+struct LineReader {+ fd: i64,+ buf: Vec<u8>,+ eof: bool,+}++impl LineReader {+ fn read_line(&mut self) -> Option<String> {+ loop {+ if let Some(i) = self.buf.iter().position(|&b| b == b'\n') {+ let line: Vec<u8> = self.buf.drain(..=i).collect();+ return Some(String::from_utf8_lossy(&line[..i]).trim_end_matches('\r').to_string());+ }+ if self.eof {+ // The last line may lack a final newline.+ if self.buf.is_empty() {+ return None;+ }+ let line = String::from_utf8_lossy(&self.buf).into_owned();+ self.buf.clear();+ return Some(line);+ }+ let mut chunk = [0u8; 512];+ let n = read(self.fd, &mut chunk);+ if n <= 0 {+ self.eof = true;+ } else {+ self.buf.extend_from_slice(&chunk[..n as usize]);+ }+ }+ }+}
@@ -0,0 +1,236 @@+//! The shell's parser: command line -> tokens -> pipeline, and the drive+//! letter mapping (`B:/notes` <-> `/mnt/disk0/notes`).+//!+//! Pure functions with no syscalls, so the same file is compiled into the+//! shell (user/sh.rs) *and* into the kernel's test suite (tests/shell.rs),+//! where it's tested without starting any processes.+//!+//! Grammar (a small subset of what a Unix shell accepts):+//!+//! line = pipeline [ "&" ] [ "#" comment ]+//! pipeline = command { "|" command }+//! command = { word | "<" word | ">" word | ">>" word }+//! word = { plain-char | 'single quoted' | "double quoted" }+//!+//! `|`, `<`, `>`, `>>` and `&` are operators even without spaces around+//! them (`ls|wc` works). Quotes just group: no `$variables` or escapes.++#![allow(dead_code)]++use alloc::{format, string::String, vec::Vec};++/// One word of a command. `quoted` words are never drive-translated, so+/// `grep "A:" file` searches for the text A:.+#[derive(Debug, Clone, PartialEq)]+pub struct Word {+ pub text: String,+ pub quoted: bool,+}++#[derive(Debug, Clone, PartialEq)]+pub enum Token {+ Word(Word),+ Pipe, // |+ In, // <+ Out, // >+ Append, // >>+ Background, // &+}++/// Split a line into tokens. Errors on an unterminated quote.+pub fn tokenize(line: &str) -> Result<Vec<Token>, String> {+ let mut tokens = Vec::new();+ let mut chars = line.chars().peekable();+ // The word being built, if any (a word can mix plain and quoted parts:+ // a"b c"d is the single word `ab cd`).+ let mut word: Option<Word> = None;++ while let Some(c) = chars.next() {+ let op = match c {+ '|' => Some(Token::Pipe),+ '<' => Some(Token::In),+ '&' => Some(Token::Background),+ '>' if chars.peek() == Some(&'>') => {+ chars.next();+ Some(Token::Append)+ }+ '>' => Some(Token::Out),+ _ => None,+ };+ if op.is_some() || c.is_whitespace() || (c == '#' && word.is_none()) {+ // Anything here ends the current word.+ if let Some(w) = word.take() {+ tokens.push(Token::Word(w));+ }+ if let Some(op) = op {+ tokens.push(op);+ } else if c == '#' {+ break; // comment: ignore the rest of the line+ }+ continue;+ }+ let w = word.get_or_insert(Word { text: String::new(), quoted: false });+ if c == '\'' || c == '"' {+ w.quoted = true;+ loop {+ match chars.next() {+ Some(q) if q == c => break,+ Some(other) => w.text.push(other),+ None => return Err(format!("unterminated {} quote", c)),+ }+ }+ } else {+ w.text.push(c);+ }+ }+ if let Some(w) = word {+ tokens.push(Token::Word(w));+ }+ Ok(tokens)+}++/// One program (or builtin) with its arguments and redirections.+#[derive(Debug, Clone, Default, PartialEq)]+pub struct Command {+ pub words: Vec<Word>, // words[0] is the command name+ pub stdin: Option<Word>, // < file+ pub stdout: Option<(Word, bool)>, // > file (false) or >> file (true)+}++/// `a | b | c`, maybe with a trailing `&` (run in the background).+#[derive(Debug, Clone, PartialEq)]+pub struct Pipeline {+ pub commands: Vec<Command>,+ pub background: bool,+}++/// Parse a line. `Ok(None)` for an empty line or a comment.+pub fn parse(line: &str) -> Result<Option<Pipeline>, String> {+ let tokens = tokenize(line)?;+ if tokens.is_empty() {+ return Ok(None);+ }+ let mut commands = Vec::new();+ let mut current = Command::default();+ let mut background = false;+ let mut iter = tokens.into_iter().peekable();++ while let Some(token) = iter.next() {+ if background {+ return Err(String::from("`&` must be at the end of the line"));+ }+ match token {+ Token::Word(w) => current.words.push(w),+ Token::Pipe => {+ if current.words.is_empty() {+ return Err(String::from("missing command before `|`"));+ }+ commands.push(core::mem::take(&mut current));+ }+ Token::Background => background = true,+ Token::In | Token::Out | Token::Append => {+ let target = match iter.next() {+ Some(Token::Word(w)) => w,+ _ => return Err(String::from("missing file name after redirection")),+ };+ match token {+ Token::In => current.stdin = Some(target),+ Token::Out => current.stdout = Some((target, false)),+ _ => current.stdout = Some((target, true)),+ }+ }+ }+ }+ if current.words.is_empty() {+ // e.g. `> file`, `&` or `ls |`: nothing to run.+ return Err(String::from(if commands.is_empty() { "missing command" } else { "missing command after `|`" }));+ }+ commands.push(current);+ Ok(Some(Pipeline { commands, background }))+}++// --- drive letters -----------------------------------------------------------++/// A drive letter and the directory it stands for.+#[derive(Debug, Clone, PartialEq)]+pub struct Drive {+ pub letter: char, // always upper case+ pub path: String, // absolute, no trailing slash except "/"+ pub fs: String, // file system type, e.g. "fat32"+}++/// Assign letters from the kernel's mount table (the text from the+/// `mounts` syscall: one "path fstype" per line, in mount order).+/// A: is always the root; B:, C:, ... go to the mounts under /mnt (the+/// disks), in the order they were mounted. Other mounts such as /tmp are+/// reachable through A: (A:/tmp).+pub fn assign_drives(mount_table: &str) -> Vec<Drive> {+ let mut drives = Vec::new();+ let root_fs = mount_table+ .lines()+ .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs))+ .unwrap_or("ramfs");+ drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs.trim()) });+ let mut letter = b'B';+ for line in mount_table.lines() {+ if let Some((path, fs)) = line.split_once(' ') {+ if path.starts_with("/mnt/") && letter <= b'Z' {+ drives.push(Drive { letter: letter as char, path: String::from(path), fs: String::from(fs.trim()) });+ letter += 1;+ }+ }+ }+ drives+}++/// Does `word` start with a drive letter ("B:", "b:/x", "C:notes")?+pub fn drive_letter(word: &str) -> Option<char> {+ let b = word.as_bytes();+ if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {+ Some((b[0] as char).to_ascii_uppercase())+ } else {+ None+ }+}++/// `B:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case+/// insensitive; `B:` and `B:/` are the drive's root, and `B:notes` means+/// `B:/notes` (drives have no current directory of their own). `None` if+/// `word` doesn't start with a known drive letter.+pub fn translate(word: &str, drives: &[Drive]) -> Option<String> {+ let letter = drive_letter(word)?;+ let drive = drives.iter().find(|d| d.letter == letter)?;+ let rest = word[2..].trim_start_matches('/');+ Some(if rest.is_empty() {+ drive.path.clone()+ } else if drive.path == "/" {+ format!("/{}", rest)+ } else {+ format!("{}/{}", drive.path, rest)+ })+}++/// The other direction, for the prompt: `/mnt/disk0/bin` -> `B:/bin`,+/// `/tmp` -> `A:/tmp`. Picks the drive with the longest matching path.+pub fn display(path: &str, drives: &[Drive]) -> String {+ let mut best: Option<(&Drive, &str)> = None;+ for d in drives {+ let rest = if d.path == "/" {+ path.strip_prefix('/')+ } else {+ // "/mnt/disk0" matches "/mnt/disk0" and "/mnt/disk0/x", not "/mnt/disk01".+ path.strip_prefix(d.path.as_str())+ .filter(|r| r.is_empty() || r.starts_with('/'))+ .map(|r| r.trim_start_matches('/'))+ };+ if let Some(rest) = rest {+ if best.map_or(true, |(b, _)| d.path.len() > b.path.len()) {+ best = Some((d, rest));+ }+ }+ }+ match best {+ Some((d, rest)) => format!("{}:/{}", d.letter, rest),+ None => String::from(path),+ }+}
@@ -0,0 +1,189 @@+//! sigtest -- checks signals: handlers and sigreturn, masks, default+//! actions, SIGCHLD, stop/continue, SIGPIPE and interrupted system calls.+//!+//! A signal handler is a normal function that the kernel makes the program+//! "call" at a moment it didn't choose: it saves the interrupted registers+//! in a frame on the user stack, runs the handler, and when the handler+//! returns (into ulib's `__restore_rt`), rt_sigreturn puts everything back.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::sync::atomic::{AtomicI64, Ordering::SeqCst};+use ulib::*;++static HITS: AtomicI64 = AtomicI64::new(0);+static LAST_SIG: AtomicI64 = AtomicI64::new(0);+static MASK_IN_HANDLER: AtomicI64 = AtomicI64::new(0);+static CHLD_PID: AtomicI64 = AtomicI64::new(0);+static CHLD_STATUS: AtomicI64 = AtomicI64::new(0);+static CHLD_CODE: AtomicI64 = AtomicI64::new(0);++extern "C" fn on_signal(sig: i32) {+ HITS.fetch_add(1, SeqCst);+ LAST_SIG.store(sig as i64, SeqCst);+ // While a handler runs, its own signal is blocked.+ MASK_IN_HANDLER.store(sigprocmask(SIG_BLOCK, 0), SeqCst);+}++extern "C" fn on_child(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {+ let info = unsafe { &*info };+ CHLD_PID.store(info.addr_or_pid as u32 as i64, SeqCst);+ CHLD_STATUS.store(info.status as i64, SeqCst);+ CHLD_CODE.store(info.code as i64, SeqCst);+}++/// Busy work that keeps values in registers across a signal: if sigreturn+/// restored anything wrong, the sum comes out different.+#[inline(never)]+fn compute_with_signal(sig: i32) -> u64 {+ let mut x = core::hint::black_box(0x1234_5678_u64);+ let mut y = core::hint::black_box(0x0bad_cafe_u64);+ for i in 0..1000u64 {+ if i == 500 {+ kill(getpid(), sig); // the handler runs before kill returns+ }+ x = x.wrapping_mul(31).wrapping_add(i);+ y ^= x.rotate_left(7);+ }+ x ^ y+}++/// Fork a child that runs `f` and return its wait4 status.+fn in_child(f: fn()) -> i32 {+ let pid = fork();+ if pid == 0 {+ f();+ exit(0);+ }+ wait4(pid, 0).map(|(_, s)| s).unwrap_or(-1)+}++fn main() -> i64 {+ let mut c = Checks::new("sigtest");+ // 1. A handler round trip, with every register restored.+ sigaction_raw(SIGUSR2, SIG_IGN, 0, 0);+ let without = compute_with_signal(SIGUSR2); // ignored: nothing happens+ c.check(HITS.load(SeqCst) == 0, "SIG_IGN: an ignored signal does nothing");+ c.check(signal(SIGUSR1, on_signal) == 0, "rt_sigaction installs a handler");+ let with = compute_with_signal(SIGUSR1);+ c.check(HITS.load(SeqCst) == 1 && LAST_SIG.load(SeqCst) == SIGUSR1 as i64, "kill(getpid(), SIGUSR1) runs the handler once, with sig = 10");+ c.check(with == without, "after rt_sigreturn the interrupted code computes the same result");+ c.check(MASK_IN_HANDLER.load(SeqCst) as u64 & sigbit(SIGUSR1) != 0, "the signal is blocked while its handler runs");+ c.check(sigprocmask(SIG_BLOCK, 0) as u64 & sigbit(SIGUSR1) == 0, "... and unblocked again after sigreturn");++ // 2. Blocking: a blocked signal waits (pending) until unblocked.+ sigprocmask(SIG_BLOCK, sigbit(SIGUSR1));+ kill(getpid(), SIGUSR1);+ c.check(HITS.load(SeqCst) == 1, "blocked: kill doesn't run the handler yet");+ sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));+ c.check(HITS.load(SeqCst) == 2, "unblocking delivers the pending signal");+ c.check(sigaction_raw(SIGKILL, SIG_IGN, 0, 0) == -22, "SIGKILL can't be caught or ignored (EINVAL)");++ // 3. Default actions, seen through wait4's status.+ let s = in_child(|| {+ kill(getpid(), SIGTERM);+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGTERM, "SIGTERM's default action terminates");+ let s = in_child(|| {+ kill(getpid(), SIGQUIT);+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGQUIT, "SIGQUIT (\"core\") terminates too (no core files here)");+ let s = in_child(|| {+ kill(getpid(), SIGCHLD); // default: ignore+ kill(getpid(), SIGURG_NUM);+ });+ c.check(wifexited(s) && wexitstatus(s) == 0, "SIGCHLD and SIGURG are ignored by default");+ let s = in_child(|| {+ // Write to a pipe nobody reads: SIGPIPE.+ let (r, w) = pipe().unwrap();+ close(r);+ write(w, b"x");+ });+ c.check(wifsignaled(s) && wtermsig(s) == SIGPIPE, "writing to a pipe without readers: SIGPIPE");+ let s = in_child(|| {+ sigaction_raw(SIGPIPE, SIG_IGN, 0, 0);+ let (r, w) = pipe().unwrap();+ close(r);+ exit(if write(w, b"x") == -32 { 0 } else { 1 });+ });+ c.check(wifexited(s) && wexitstatus(s) == 0, "... with SIGPIPE ignored, write fails with EPIPE");++ // 4. SIGCHLD: the parent hears when a child ends.+ sigaction_info(SIGCHLD, on_child, SA_RESTART);+ let pid = fork();+ if pid == 0 {+ exit(3);+ }+ while CHLD_PID.load(SeqCst) == 0 {+ pause();+ }+ c.check(CHLD_PID.load(SeqCst) == pid && CHLD_STATUS.load(SeqCst) == 3 && CHLD_CODE.load(SeqCst) == 1, "SIGCHLD arrives with si_pid, status 3, CLD_EXITED");+ c.check(wait(pid) == 3, "and the child can still be reaped");+ sigaction_raw(SIGCHLD, SIG_DFL, 0, 0);++ // 5. Stop and continue.+ let pid = fork();+ if pid == 0 {+ loop {+ sleep_ms(10);+ }+ }+ kill(pid, SIGSTOP);+ let r = wait4(pid, WUNTRACED);+ c.check(matches!(r, Ok((p, s)) if p == pid && wifstopped(s) && wstopsig(s) == SIGSTOP), "SIGSTOP stops a child; wait4(WUNTRACED) reports it");+ kill(pid, SIGCONT);+ let r = wait4(pid, WCONTINUED);+ c.check(matches!(r, Ok((_, s)) if wifcontinued(s)), "SIGCONT continues it; wait4(WCONTINUED) reports that");+ kill(pid, SIGKILL);+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wifsignaled(s) && wtermsig(s) == SIGKILL), "SIGKILL ends it");++ // 6. Interrupted system calls: a signal during a blocking read.+ for restart in [false, true] {+ sigaction_raw(SIGUSR1, on_signal as usize, if restart { SA_RESTART } else { 0 }, 0);+ let (r, w) = pipe().unwrap();+ let me = getpid();+ let pid = fork();+ if pid == 0 {+ sleep_ms(120);+ kill(me, SIGUSR1); // interrupts the parent's read+ sleep_ms(120);+ write(w, b"y");+ exit(0);+ }+ let hits = HITS.load(SeqCst);+ let mut b = [0u8; 1];+ let n = read(r, &mut b);+ if restart {+ c.check(n == 1 && HITS.load(SeqCst) == hits + 1, "SA_RESTART: the handler runs, then read continues and gets the data");+ } else {+ c.check(n == -EINTR && HITS.load(SeqCst) == hits + 1, "no SA_RESTART: read returns EINTR after the handler");+ }+ wait(pid);+ close(r);+ close(w);+ }++ // 7. kill errors and process groups.+ c.check(kill(99999, SIGTERM) == -ESRCH, "kill of a pid that doesn't exist: ESRCH");+ c.check(kill(getpid(), 99) == -22, "kill with signal 99: EINVAL");+ c.check(kill(getpid(), 0) == 0, "signal 0 only checks that the process exists");+ let pid = fork();+ if pid == 0 {+ setpgid(0, 0);+ loop {+ pause();+ }+ }+ setpgid(pid, pid);+ c.check(kill(-pid, SIGTERM) == 0, "kill(-pgid) signals a whole process group");+ let r = wait4(pid, 0);+ c.check(matches!(r, Ok((_, s)) if wtermsig(s) == SIGTERM), "the group's member got it");+ c.done()+}++const SIGURG_NUM: i32 = 23;
@@ -0,0 +1,28 @@+//! sleep N -- wait N seconds (or `sleep 250ms`). The kernel blocks the+//! thread until the timer says it's time; no CPU is used meanwhile.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ let a = arg(1).unwrap_or("");+ let ms = match a.strip_suffix("ms") {+ Some(n) => n.parse::<u64>().ok(),+ None => a.parse::<u64>().ok().map(|s| s * 1000),+ };+ match ms {+ Some(ms) => {+ sleep_ms(ms);+ 0+ }+ None => {+ eprintln!("usage: sleep seconds | sleep 250ms");+ 2+ }+ }+}
@@ -0,0 +1,22 @@+//! A CPU hog: busy-loops without ever making a syscall in between. The timer+//! interrupt still preempts it, so everything else keeps running.+//! `spin` does 3 rounds of 30 million additions; `spin <n>` does n rounds.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ println!("[spin] pid {}: spinning without syscalls...", ulib::getpid());+ let rounds: u64 = ulib::arg(1).and_then(|a| a.parse().ok()).unwrap_or(3);+ let mut count: u64 = 0;+ for round in 1..=rounds {+ for _ in 0..30_000_000 {+ count = core::hint::black_box(count + 1);+ }+ println!("[spin] round {} done (count = {})", round, count);+ }+ 0+}
@@ -0,0 +1,48 @@+//! `sysinfo`: how many CPUs there are, how busy each one is, and how many+//! threads, processes and memory frames are in use.+//!+//! The load is measured, not guessed: each CPU counts its ~10 ms timer+//! ticks, and how many of them interrupted real work rather than its idle+//! thread. We read those counters twice, a quarter of a second apart; busy ticks+//! divided by all ticks in between is that CPU's load.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ let before = ulib::sysinfo();+ ulib::sleep_ms(250); // sleeping, not spinning: we don't count as load+ let after = ulib::sysinfo();++ let seconds = after.uptime_ticks * 10 / 182; // the PIT ticks ~18.2 times a second+ println!(+ "{} CPU{} online (this program ran on CPU {}), up {} s",+ after.cpus,+ if after.cpus == 1 { "" } else { "s" },+ after.this_cpu,+ seconds+ );+ println!(+ "{} threads, {} processes, {} KiB of memory frames in use",+ after.threads,+ after.processes,+ after.frames_in_use * 4+ );+ println!("CPU load over the last quarter second:");+ for cpu in 0..(after.cpus as usize).min(16) {+ let ticks = after.ticks[cpu] - before.ticks[cpu];+ let busy = after.busy_ticks[cpu] - before.busy_ticks[cpu];+ let percent = if ticks == 0 { 0 } else { busy * 100 / ticks };+ // A 20-character bar: one '#' per 5%.+ let mut bar = [b'.'; 20];+ for c in bar.iter_mut().take((percent / 5) as usize) {+ *c = b'#';+ }+ let bar = core::str::from_utf8(&bar).unwrap_or("");+ println!(" CPU{} [{}] {:3}%", cpu, bar, percent);+ }+ 0+}
@@ -0,0 +1,135 @@+//! threads [N] -- several threads in one process, pthread-style.+//!+//! Threads share *everything* a process owns: memory, open files, signal+//! handlers. Each has only its own registers, stack and TLS block (and a+//! thread id). ulib's `spawn_thread` = pthread_create: clone with+//! CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | ..., a fresh+//! stack, CLONE_SETTLS for the TLS block, and CLONE_CHILD_CLEARTID so+//! `join` can sleep on a futex until the kernel says "thread gone".+//!+//! The demo: a parallel sum. 1..=COUNT is split into N slices (default 4,+//! one per CPU); each thread adds up its slice, then adds its result to a+//! shared total under a futex mutex. Many small increments of a shared+//! counter (also under the mutex) check the mutex really excludes; a+//! counter bumped *without* it shows why that matters.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::sync::Arc;+use alloc::vec::Vec;+use core::cell::UnsafeCell;+use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};+use ulib::*;++const COUNT: u64 = 40_000_000;+const BUMPS: u64 = 20_000;++/// Data guarded by a FutexMutex (a tiny `Mutex<T>`).+struct Shared {+ lock: FutexMutex,+ total: UnsafeCell<u64>,+ bumps: UnsafeCell<u64>,+ /// Bumped with a plain read + write, no lock: races lose updates.+ racy: UnsafeCell<u64>,+}+unsafe impl Sync for Shared {}+unsafe impl Send for Shared {}++fn main() -> i64 {+ let n: u64 = arg(1).and_then(|a| a.parse().ok()).unwrap_or(4).clamp(1, 16);+ let mut c = Checks::new("threads");+ let shared = Arc::new(Shared {+ lock: FutexMutex::new(),+ total: UnsafeCell::new(0),+ bumps: UnsafeCell::new(0),+ racy: UnsafeCell::new(0),+ });+ let cpus_seen = Arc::new(AtomicU64::new(0)); // bit i = some thread ran on CPU i+ let tids = Arc::new(AtomicU64::new(0)); // sum of the threads' tids+ let tls_ok = Arc::new(AtomicU32::new(0));+ let start = now_us();+ let mut threads = Vec::new();+ for i in 0..n {+ let (shared, cpus_seen, tids, tls_ok) = (shared.clone(), cpus_seen.clone(), tids.clone(), tls_ok.clone());+ let from = COUNT * i / n + 1;+ let to = COUNT * (i + 1) / n;+ // Each thread gets `i` as its TLS value.+ let t = spawn_thread(i, move || {+ let mut sum = 0u64;+ for x in from..=to {+ sum = sum.wrapping_add(core::hint::black_box(x));+ }+ cpus_seen.fetch_or(1 << sysinfo().this_cpu, Ordering::Relaxed);+ tids.fetch_add(gettid() as u64, Ordering::Relaxed);+ if tls_value() == i {+ tls_ok.fetch_add(1, Ordering::Relaxed);+ }+ for _ in 0..BUMPS {+ shared.lock.lock();+ unsafe { *shared.bumps.get() += 1 };+ shared.lock.unlock();+ unsafe {+ let v = core::ptr::read_volatile(shared.racy.get());+ core::ptr::write_volatile(shared.racy.get(), v + 1);+ }+ }+ shared.lock.lock();+ unsafe { *shared.total.get() += sum };+ shared.lock.unlock();+ });+ match t {+ Ok(t) => threads.push(t),+ Err(e) => {+ println!("threads: spawn_thread: {}", strerror(e));+ return 1;+ }+ }+ }+ let spawned_tids: u64 = threads.iter().map(|t| t.tid as u64).sum();+ for t in threads {+ t.join();+ }+ let ms = (now_us() - start) / 1000;+ let total = unsafe { *shared.total.get() };+ let bumps = unsafe { *shared.bumps.get() };+ let racy = unsafe { *shared.racy.get() };+ let cpus = cpus_seen.load(Ordering::Relaxed).count_ones();+ println!("threads: {} threads summed 1..={} = {} in {} ms on {} CPU(s)", n, COUNT, total, ms, cpus);+ println!("threads: locked counter {} (want {}), unlocked counter {} (races lose updates)", bumps, n * BUMPS, racy);+ c.check(total == COUNT * (COUNT + 1) / 2, "parallel sum is right");+ c.check(bumps == n * BUMPS, "the futex mutex never let two threads in at once");+ c.check(tls_ok.load(Ordering::Relaxed) == n as u32, "every thread saw its own TLS value (fs:[8])");+ c.check(tids.load(Ordering::Relaxed) == spawned_tids, "gettid in each thread = the tid clone returned");+ c.check(gettid() == getpid(), "the main thread's tid is the pid");++ // A futex wait whose value doesn't match returns at once (EAGAIN); one+ // that matches sleeps until its timeout.+ let word = AtomicU32::new(7);+ c.check(futex_wait(&word, 8, None) == -EAGAIN, "futex_wait with a stale value: EAGAIN");+ let t0 = now_us();+ let r = futex_wait(&word, 7, Some(50_000));+ c.check(r == -ETIMEDOUT && now_us() - t0 >= 40_000, "futex_wait times out after 50 ms");+ c.check(futex_wake(&word, 1) == 0, "futex_wake with nobody waiting wakes 0");++ // exit vs exit_group: a thread that calls exit (60) ends only itself.+ let alive = Arc::new(AtomicU32::new(0));+ let a2 = alive.clone();+ let t = spawn_thread(0, move || {+ a2.store(1, Ordering::SeqCst);+ // falls off the end: ulib calls exit(0) = just this thread+ })+ .unwrap();+ t.join();+ c.check(alive.load(Ordering::SeqCst) == 1, "a thread's exit ends only that thread");++ // tgkill: a signal for one particular thread (here: ourselves).+ c.check(tgkill(getpid(), gettid(), 0) == 0, "tgkill(pid, tid, 0) finds the thread");+ c.check(tgkill(getpid(), 99999, 0) == -ESRCH, "tgkill to a missing thread: ESRCH");+ c.done()+}
@@ -0,0 +1,173 @@+//! tile [N] -- draw a tiling-window-manager mock-up on /dev/fb0.+//!+//! tile 4 "windows" in a dwindle layout, the text console in the first+//! tile 6 1 to 6 windows+//! tile -r give the text console the whole screen back+//!+//! A hint of the compositor planned for later (think Hyprland): a gradient+//! wallpaper, windows with gaps between them and a border, the focused one+//! cyan. The "windows" are only coloured rectangles, except the first one,+//! which is where the kernel's text console moves to (FB_SET_CONSOLE), so+//! the shell keeps working inside it.+//!+//! How it talks to the framebuffer (no mmap yet, so no direct pointer):+//! ioctl(FB_GET_INFO) width, height, bytes per row, bits per pixel+//! lseek + write raw 0x00RRGGBB pixels (the wallpaper rows)+//! ioctl(FB_FILL) fill a rectangle with one colour (fast)+//! ioctl(FB_FLUSH) send the changed part to the screen+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::vec;+use alloc::vec::Vec;+use ulib::*;++const GAP: u32 = 10; // space between windows and around the screen edge+const BORDER: u32 = 3;+const ACTIVE: u32 = 0x33ccff; // focused window border (cyan)+const INACTIVE: u32 = 0x595959; // the other borders (gray)+/// Window contents: soft pastels (the first window is the console).+const COLORS: [u32; 5] = [0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf38ba8, 0xcba6f7];++#[derive(Clone, Copy)]+struct Rect {+ x: u32,+ y: u32,+ w: u32,+ h: u32,+}++fn main() -> i64 {+ let fd = open("/dev/fb0", 0);+ if fd < 0 {+ eprintln!("tile: /dev/fb0: {} (no virtio-gpu?)", strerror(fd));+ return 1;+ }+ let status = match arg(1) {+ Some("-r") => reset(fd),+ Some(n) => match n.parse::<usize>() {+ Ok(n @ 1..=6) => draw(fd, n),+ _ => {+ eprintln!("usage: tile [1-6] | tile -r");+ 2+ }+ },+ None => draw(fd, 4),+ };+ close(fd);+ status+}++fn reset(fd: i64) -> i64 {+ let r = ioctl(fd, FB_SET_CONSOLE, 0);+ if r < 0 {+ eprintln!("tile: {}", strerror(r));+ return 1;+ }+ println!("console: full screen, {}x{} characters", r >> 16, r & 0xffff);+ 0+}++fn draw(fd: i64, n: usize) -> i64 {+ let mut info = [0u32; 4];+ if ioctl(fd, FB_GET_INFO, info.as_mut_ptr() as u64) < 0 {+ eprintln!("tile: FB_GET_INFO failed");+ return 1;+ }+ let [width, height, stride, _bpp] = info;+ wallpaper(fd, width, height, stride);++ let screen = Rect { x: GAP, y: GAP, w: width - 2 * GAP, h: height - 2 * GAP };+ let windows = dwindle(screen, n);+ for (i, &win) in windows.iter().enumerate() {+ fill(fd, win, if i == 0 { ACTIVE } else { INACTIVE });+ let inside = shrink(win, BORDER);+ if i > 0 {+ fake_content(fd, inside, COLORS[(i - 1) % COLORS.len()]);+ }+ }+ let flush = [0, 0, width, height];+ ioctl(fd, FB_FLUSH, flush.as_ptr() as u64);++ // Move the console into window 0 (inside its border).+ let c = shrink(windows[0], BORDER);+ let r = ioctl(fd, FB_SET_CONSOLE, [c.x, c.y, c.w, c.h].as_ptr() as u64);+ if r < 0 {+ eprintln!("tile: FB_SET_CONSOLE: {}", strerror(r));+ return 1;+ }+ println!("tile: {} windows on {}x{}, console {}x{}", n, width, height, r >> 16, r & 0xffff);+ 0+}++/// Split the screen like a "dwindle" layout: each window takes half of+/// what is left, alternating left|right and top/bottom splits.+fn dwindle(mut area: Rect, n: usize) -> Vec<Rect> {+ let mut out = Vec::new();+ for i in 0..n {+ if i + 1 == n {+ out.push(area); // the last window gets the rest+ break;+ }+ if i % 2 == 0 {+ let w = (area.w - GAP) / 2;+ out.push(Rect { w, ..area });+ area = Rect { x: area.x + w + GAP, w: area.w - w - GAP, ..area };+ } else {+ let h = (area.h - GAP) / 2;+ out.push(Rect { h, ..area });+ area = Rect { y: area.y + h + GAP, h: area.h - h - GAP, ..area };+ }+ }+ out+}++/// A vertical gradient, written 8 rows (32 KiB) at a time with lseek ++/// write. (Programs only have a 64 KiB heap until mmap exists.)+fn wallpaper(fd: i64, width: u32, height: u32, stride: u32) {+ const ROWS: u32 = 8;+ let mut buf = vec![0u8; (stride * ROWS) as usize];+ for y0 in (0..height).step_by(ROWS as usize) {+ let rows = ROWS.min(height - y0);+ for dy in 0..rows {+ let color = gradient(y0 + dy, height).to_le_bytes();+ let row = &mut buf[(dy * stride) as usize..][..(width * 4) as usize];+ row.chunks_exact_mut(4).for_each(|px| px.copy_from_slice(&color));+ }+ lseek(fd, (y0 * stride) as i64, SEEK_SET);+ write(fd, &buf[..(rows * stride) as usize]);+ }+}++/// Dark blue at the top to purple at the bottom.+fn gradient(y: u32, height: u32) -> u32 {+ let mix = |a: u32, b: u32| a + (b - a) * y / height;+ mix(0x1e, 0x50) << 16 | mix(0x1e, 0x2a) << 8 | mix(0x2e, 0x78)+}++/// Fill a window with `color` and draw some darker bars, like lines of text.+fn fake_content(fd: i64, r: Rect, color: u32) {+ fill(fd, r, color);+ let dark = (color >> 1) & 0x7f7f7f; // every channel halved+ let mut y = r.y + 16;+ let mut line = 0;+ while y + 6 < r.y + r.h - 8 {+ let w = (r.w - 32) * [9, 6, 8, 4, 7][line % 5] / 10;+ fill(fd, Rect { x: r.x + 16, y, w, h: 6 }, dark);+ y += 16;+ line += 1;+ }+}++fn shrink(r: Rect, by: u32) -> Rect {+ Rect { x: r.x + by, y: r.y + by, w: r.w - 2 * by, h: r.h - 2 * by }+}++fn fill(fd: i64, r: Rect, color: u32) {+ ioctl(fd, FB_FILL, [r.x, r.y, r.w, r.h, color].as_ptr() as u64);+}
@@ -0,0 +1,25 @@+//! touch file... -- create empty files (existing files are left alone;+//! there are no timestamps to update yet).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ let mut status = 0;+ for i in 1..argc() {+ let path = arg(i).unwrap_or("");+ let fd = open(path, O_WRONLY | O_CREAT); // no O_TRUNC: keep the contents+ if fd < 0 {+ eprintln!("touch: {}: {}", path, strerror(fd));+ status = 1;+ } else {+ close(fd);+ }+ }+ status+}
@@ -0,0 +1,1676 @@+//! A tiny "libc" for user programs: syscall wrappers, print!/println!,+//! eprintln!, a small heap (so `Vec` and `String` work), the `_start` entry+//! point and a panic handler.+//!+//! Each user program includes it with+//! #[macro_use] #[path = "ulib.rs"] mod ulib;+//! and defines `fn main() -> i64` (the exit code).+//!+//! User programs are built for the `x86_64-unknown-none` target, which has+//! SSE/floating point disabled. That matters: the kernel doesn't save SSE+//! registers on a context switch.++#![allow(dead_code, unused_macros)]++extern crate alloc;++use core::alloc::{GlobalAlloc, Layout};+use core::arch::asm;+use core::cell::UnsafeCell;+use core::fmt;++pub const SYS_READ: u64 = 0;+pub const SYS_WRITE: u64 = 1;+pub const SYS_OPEN: u64 = 2;+pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_IOCTL: u64 = 16;+pub const SYS_PIPE: u64 = 22;+pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_NANOSLEEP: u64 = 35;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;+pub const SYS_WAIT4: u64 = 61;+pub const SYS_MMAP: u64 = 9;+pub const SYS_MPROTECT: u64 = 10;+pub const SYS_MUNMAP: u64 = 11;+pub const SYS_BRK: u64 = 12;+pub const SYS_RT_SIGACTION: u64 = 13;+pub const SYS_RT_SIGPROCMASK: u64 = 14;+pub const SYS_RT_SIGRETURN: u64 = 15;+pub const SYS_DUP: u64 = 32;+pub const SYS_DUP2: u64 = 33;+pub const SYS_PAUSE: u64 = 34;+pub const SYS_FORK: u64 = 57;+pub const SYS_VFORK: u64 = 58;+pub const SYS_EXECVE: u64 = 59;+pub const SYS_KILL: u64 = 62;+pub const SYS_FCNTL: u64 = 72;+pub const SYS_SETPGID: u64 = 109;+pub const SYS_GETPPID: u64 = 110;+pub const SYS_GETPGRP: u64 = 111;+pub const SYS_EXIT_GROUP: u64 = 231;+pub const SYS_PIPE2: u64 = 293;+pub const SYS_TRACE_ME: u64 = 505;+pub const SYS_UNAME: u64 = 63;+pub const SYS_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_READLINK: u64 = 89;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500;+// Stage 15.1: more of Linux's path calls (all take C strings).+pub const SYS_LSTAT: u64 = 6;+pub const SYS_ACCESS: u64 = 21;+pub const SYS_RENAME: u64 = 82;+pub const SYS_FCHDIR: u64 = 81;+pub const SYS_GETUID: u64 = 102;+pub const SYS_OPENAT: u64 = 257;+pub const SYS_MKDIRAT: u64 = 258;+pub const SYS_NEWFSTATAT: u64 = 262;+pub const SYS_UNLINKAT: u64 = 263;+pub const SYS_RENAMEAT: u64 = 264;+pub const SYS_READLINKAT: u64 = 267;+pub const SYS_FACCESSAT: u64 = 269;+pub const AT_FDCWD: i64 = -100;+pub const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+pub const AT_REMOVEDIR: u64 = 0x200;+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_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;+pub const SYS_SENDTO: u64 = 44;+pub const SYS_RECVFROM: u64 = 45;+pub const SYS_BIND: u64 = 49;+pub const SYS_LISTEN: u64 = 50;+pub const SYS_SETSOCKOPT: u64 = 54;+pub const SYS_CLOCK_GETTIME: u64 = 228;+pub const SYS_RESOLVE: u64 = 503;+pub const SYS_NETINFO: u64 = 504;+pub const SYS_GETRANDOM: u64 = 318;++pub const O_RDONLY: u64 = 0;+pub const O_WRONLY: u64 = 1;+pub const O_CREAT: u64 = 0o100;+pub const O_EXCL: u64 = 0o200;+pub const O_DIRECTORY: u64 = 0o200000;+pub const O_TRUNC: u64 = 0o1000;+pub const O_APPEND: u64 = 0o2000;+pub const TTY_SET_RAW: u64 = 1;+// /dev/fb0 ioctls (see the table in the kernel's syscall.rs).+pub const FB_GET_INFO: u64 = 0x4600;+pub const FB_FLUSH: u64 = 0x46f0;+pub const FB_FILL: u64 = 0x46f1;+pub const FB_SET_CONSOLE: u64 = 0x46f2;+pub const WNOHANG: u64 = 1;+pub const WUNTRACED: u64 = 2;+pub const WCONTINUED: u64 = 8;+pub const O_CLOEXEC: u64 = 0o2000000;+pub const F_GETFD: u64 = 1;+pub const F_SETFD: u64 = 2;+pub const FD_CLOEXEC: u64 = 1;+pub const TIOCGPGRP: u64 = 0x540F;+pub const TIOCSPGRP: u64 = 0x5410;+pub const SEEK_SET: u64 = 0;+pub const SEEK_CUR: u64 = 1;+pub const SEEK_END: u64 = 2;+pub const SPAWN_TRACE: u64 = 1;++pub const EPERM: i64 = 1;+pub const ENOENT: i64 = 2;+pub const ESRCH: i64 = 3;+pub const EINTR: i64 = 4;+pub const EAGAIN: i64 = 11;+pub const EBADF: i64 = 9;+pub const ECHILD: i64 = 10;+pub const EFAULT: i64 = 14;+pub const EBUSY: i64 = 16;+pub const EEXIST: i64 = 17;+pub const ENOTDIR: i64 = 20;+pub const EISDIR: i64 = 21;+pub const ESPIPE: i64 = 29;+pub const EXDEV: i64 = 18;+pub const ENODEV: i64 = 19;+pub const EINVAL: i64 = 22;+pub const ENAMETOOLONG: i64 = 36;+pub const ENOTEMPTY: i64 = 39;+pub const ELOOP: i64 = 40;+pub const ETIMEDOUT: i64 = 110;++/// Linux's `struct stat` on x86_64 (what stat/fstat/lstat fill in since+/// stage 15.1; the same 144 bytes a C program's <sys/stat.h> describes).+#[repr(C)]+#[derive(Default, Clone, Copy)]+pub struct Stat {+ pub dev: u64,+ pub ino: u64,+ pub nlink: u64,+ pub mode: u32, // file type (S_IF*) | permission bits like 0o644+ pub uid: u32,+ pub gid: u32,+ _pad0: u32,+ pub rdev: u64,+ pub size: i64,+ pub blksize: i64,+ pub blocks: i64,+ pub atime: [i64; 2], // seconds, nanoseconds+ pub mtime: [i64; 2],+ pub ctime: [i64; 2],+ _unused: [i64; 3],+}+pub const S_IFMT: u32 = 0o170000; // the type bits of `mode`+pub const S_IFIFO: u32 = 0o010000;+pub const S_IFCHR: u32 = 0o020000; // terminals, /dev/null, /dev/fb0, ...+pub const S_IFDIR: u32 = 0o040000;+pub const S_IFREG: u32 = 0o100000;+pub const S_IFLNK: u32 = 0o120000;+pub const S_IFSOCK: u32 = 0o140000;++impl Stat {+ pub fn file_type(&self) -> u32 {+ self.mode & S_IFMT+ }+ pub fn is_dir(&self) -> bool {+ self.file_type() == S_IFDIR+ }+ pub fn is_file(&self) -> bool {+ self.file_type() == S_IFREG+ }+ pub fn is_symlink(&self) -> bool {+ self.file_type() == S_IFLNK+ }+ pub fn is_char_device(&self) -> bool {+ self.file_type() == S_IFCHR+ }+ /// Permission bits (0o755 & co.).+ pub fn perm(&self) -> u32 {+ self.mode & 0o7777+ }+}++/// Same layout as the kernel's `syscall::SpawnOptions`.+#[repr(C)]+pub struct SpawnOptions {+ pub stdin: i64, // my fd to give the child as its fd 0 (-1 = my fd 0)+ pub stdout: i64,+ pub stderr: i64,+ pub flags: u64, // SPAWN_TRACE+}++impl SpawnOptions {+ pub const INHERIT: SpawnOptions = SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 };+}++/// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax.+/// The `syscall` instruction overwrites rcx and r11.+pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {+ let ret: i64;+ unsafe {+ asm!(+ "syscall",+ inlateout("rax") nr as i64 => ret,+ in("rdi") a1,+ in("rsi") a2,+ in("rdx") a3,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack),+ );+ }+ ret+}++/// 5-argument version (4th argument goes in r10, 5th in r8).+pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+ syscall6(nr, a1, a2, a3, a4, a5, 0)+}++/// 6-argument version (6th in r9). mmap is the only one that needs it.+pub fn syscall6(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64, a6: u64) -> i64 {+ let ret: i64;+ unsafe {+ asm!(+ "syscall",+ inlateout("rax") nr as i64 => ret,+ in("rdi") a1,+ in("rsi") a2,+ in("rdx") a3,+ in("r10") a4,+ in("r8") a5,+ in("r9") a6,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack),+ );+ }+ ret+}++pub fn read(fd: i64, buf: &mut [u8]) -> i64 {+ syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+pub fn write(fd: i64, buf: &[u8]) -> i64 {+ syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64)+}+/// Call `f` with `path` as a C string (NUL-terminated), the way the+/// kernel's path calls want it (Linux's ABI). A Rust &str isn't one: it has+/// no NUL at the end, so we copy it into a buffer and add one. Too long+/// for the kernel: -ENAMETOOLONG; a NUL inside: -EINVAL (C can't say that).+pub fn with_cpath(path: &str, f: impl FnOnce(u64) -> i64) -> i64 {+ if path.len() >= PATH_MAX {+ return -ENAMETOOLONG;+ }+ if path.as_bytes().contains(&0) {+ return -EINVAL;+ }+ let mut c = alloc::vec::Vec::with_capacity(path.len() + 1);+ c.extend_from_slice(path.as_bytes());+ c.push(0);+ f(c.as_ptr() as u64)+}+/// open(path, flags, mode): new files get mode 0o644 (the kernel ignores it).+pub fn open(path: &str, flags: u64) -> i64 {+ with_cpath(path, |p| syscall3(SYS_OPEN, p, flags, 0o644))+}+/// openat(dirfd, path, flags): a relative path starts at directory `dirfd`.+pub fn openat(dirfd: i64, path: &str, flags: u64) -> i64 {+ with_cpath(path, |p| syscall5(SYS_OPENAT, dirfd as u64, p, flags, 0o644, 0))+}+pub fn close(fd: i64) -> i64 {+ syscall3(SYS_CLOSE, fd as u64, 0, 0)+}+/// stat follows symlinks; lstat describes the link itself.+pub fn stat(path: &str, st: &mut Stat) -> i64 {+ with_cpath(path, |p| syscall3(SYS_STAT, p, st as *mut Stat as u64, 0))+}+pub fn lstat(path: &str, st: &mut Stat) -> i64 {+ with_cpath(path, |p| syscall3(SYS_LSTAT, p, st as *mut Stat as u64, 0))+}+pub fn fstat(fd: i64, st: &mut Stat) -> i64 {+ syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0)+}+/// newfstatat(dirfd, path, st, flags): what musl's stat() really calls.+pub fn fstatat(dirfd: i64, path: &str, st: &mut Stat, flags: u64) -> i64 {+ with_cpath(path, |p| syscall5(SYS_NEWFSTATAT, dirfd as u64, p, st as *mut Stat as u64, flags, 0))+}+/// Is `fd` a terminal? (Like libc: does it answer the TCGETS ioctl?)+pub fn isatty(fd: i64) -> bool {+ tcgetattr(fd).is_ok()+}+pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {+ syscall3(SYS_LSEEK, fd as u64, offset as u64, whence)+}+/// Returns (read end, write end), or the negative errno.+pub fn pipe() -> Result<(i64, i64), i64> {+ pipe2(0)+}+/// pipe with flags (O_CLOEXEC: both ends close on exec).+pub fn pipe2(flags: u64) -> Result<(i64, i64), i64> {+ let mut fds = [0u32; 2];+ match syscall3(SYS_PIPE2, fds.as_mut_ptr() as u64, flags, 0) {+ 0 => Ok((fds[0] as i64, fds[1] as i64)),+ e => Err(e),+ }+}+pub fn sched_yield() {+ syscall3(SYS_SCHED_YIELD, 0, 0, 0);+}+pub fn sleep_ms(ms: u64) {+ nanosleep_us(ms * 1000);+}+/// nanosleep(*timespec, *remaining): 0, or -EINTR if a signal woke us.+pub fn nanosleep_us(us: u64) -> i64 {+ let ts = [us / 1_000_000, (us % 1_000_000) * 1000];+ syscall3(SYS_NANOSLEEP, ts.as_ptr() as u64, 0, 0)+}+pub fn getpid() -> i64 {+ syscall3(SYS_GETPID, 0, 0, 0)+}+pub fn exit(code: i64) -> ! {+ syscall3(SYS_EXIT_GROUP, code as u64, 0, 0);+ loop {} // not reached+}++// --- Processes (fork, exec, wait), stage 14 --------------------------------++/// fork: 0 in the child, the child's pid in the parent (or -errno).+pub fn fork() -> i64 {+ syscall3(SYS_FORK, 0, 0, 0)+}+/// vfork: like fork, but the parent sleeps until the child calls execve or+/// exits. (Here the child gets a copy-on-write copy anyway, so it's safe to+/// do more than exec in it; on Linux it borrows the parent's memory.)+pub fn vfork() -> i64 {+ syscall3(SYS_VFORK, 0, 0, 0)+}+pub fn getppid() -> i64 {+ syscall3(SYS_GETPPID, 0, 0, 0)+}+pub fn getpgrp() -> i64 {+ syscall3(SYS_GETPGRP, 0, 0, 0)+}+/// Put process `pid` (0 = me) into group `pgid` (0 = a new group named+/// after the process).+pub fn setpgid(pid: i64, pgid: i64) -> i64 {+ syscall3(SYS_SETPGID, pid as u64, pgid as u64, 0)+}+/// Which process group gets the console's Ctrl+C.+pub fn tcgetpgrp(fd: i64) -> i64 {+ let mut pgid = 0u32;+ match ioctl(fd, TIOCGPGRP, &mut pgid as *mut u32 as u64) {+ 0 => pgid as i64,+ e => e,+ }+}+pub fn tcsetpgrp(fd: i64, pgid: i64) -> i64 {+ let pgid = pgid as u32;+ ioctl(fd, TIOCSPGRP, &pgid as *const u32 as u64)+}+/// Print my system calls from now on (inherited across fork and exec).+pub fn trace_me(on: bool) -> i64 {+ syscall3(SYS_TRACE_ME, on as u64, 0, 0)+}+pub fn dup(fd: i64) -> i64 {+ syscall3(SYS_DUP, fd as u64, 0, 0)+}+pub fn dup2(old: i64, new: i64) -> i64 {+ syscall3(SYS_DUP2, old as u64, new as u64, 0)+}+pub fn fcntl(fd: i64, cmd: u64, arg: u64) -> i64 {+ syscall3(SYS_FCNTL, fd as u64, cmd, arg)+}++/// A list of C strings (NUL-terminated) plus the NULL-terminated array of+/// pointers to them that execve wants.+pub struct CStrings {+ data: alloc::vec::Vec<u8>,+ ends: alloc::vec::Vec<usize>,+ ptrs: alloc::vec::Vec<u64>,+}++impl CStrings {+ pub fn new<'a>(items: impl IntoIterator<Item = &'a str>) -> CStrings {+ let mut data = alloc::vec::Vec::new();+ let mut ends = alloc::vec::Vec::new();+ for item in items {+ data.extend_from_slice(item.as_bytes());+ data.push(0);+ ends.push(data.len());+ }+ CStrings { data, ends, ptrs: alloc::vec::Vec::new() }+ }+ /// The `char **`: pointers into `data`, then NULL.+ pub fn as_ptr(&mut self) -> *const u64 {+ let base = self.data.as_ptr() as u64;+ let mut start = 0;+ self.ptrs.clear();+ for &end in &self.ends {+ self.ptrs.push(base + start as u64);+ start = end;+ }+ self.ptrs.push(0);+ self.ptrs.as_ptr()+ }+}++/// execve(path, argv, envp): only returns on failure (-errno).+pub fn execve(path: &str, argv: &[&str], envp: &[&str]) -> i64 {+ let mut args = CStrings::new(argv.iter().copied());+ let mut env = CStrings::new(envp.iter().copied());+ with_cpath(path, |p| syscall3(SYS_EXECVE, p, args.as_ptr() as u64, env.as_ptr() as u64))+}++/// The same environment I got: for passing on to execve.+pub fn environ() -> alloc::vec::Vec<&'static str> {+ (0..).map_while(env).collect()+}++/// wait4(pid, &status, options): returns (pid, status), or -errno. pid -1 =+/// any child; WNOHANG and nothing finished: pid 0.+pub fn wait4(pid: i64, options: u64) -> Result<(i64, i32), i64> {+ let mut status = 0i32;+ match syscall5(SYS_WAIT4, pid as u64, &mut status as *mut i32 as u64, options, 0, 0) {+ n if n < 0 => Err(n),+ n => Ok((n, status)),+ }+}++// The status macros (C's WIFEXITED etc.): wait4's `status` packs how the+// child ended into one int -- exit code << 8, or the signal number, or+// 0x7f for "stopped".+pub fn wifexited(status: i32) -> bool {+ status & 0x7f == 0+}+pub fn wexitstatus(status: i32) -> i32 {+ (status >> 8) & 0xff+}+pub fn wifsignaled(status: i32) -> bool {+ (status & 0x7f) + 1 >= 2 && status & 0x7f != 0x7f+}+pub fn wtermsig(status: i32) -> i32 {+ status & 0x7f+}+pub fn wcoredump(status: i32) -> bool {+ status & 0x80 != 0+}+pub fn wifstopped(status: i32) -> bool {+ status & 0xff == 0x7f+}+pub fn wstopsig(status: i32) -> i32 {+ wexitstatus(status)+}+pub fn wifcontinued(status: i32) -> bool {+ status == 0xffff+}+/// A shell-style exit code: the code itself, or 128 + the signal number.+pub fn exit_code(status: i32) -> i64 {+ if wifsignaled(status) { 128 + wtermsig(status) as i64 } else { wexitstatus(status) as i64 }+}++/// Wait for child `pid` to end; returns its exit code (139 = killed by+/// SIGSEGV: 128 + 11), or -errno.+pub fn wait(pid: i64) -> i64 {+ loop {+ match wait4(pid, 0) {+ Ok((_, status)) => return exit_code(status),+ Err(e) if e == -EINTR => continue,+ Err(e) => return e,+ }+ }+}+/// Like `wait`, but don't block: -EAGAIN while the child is still running.+pub fn try_wait(pid: i64) -> i64 {+ match wait4(pid, WNOHANG) {+ Ok((0, _)) => -EAGAIN,+ Ok((_, status)) => exit_code(status),+ Err(e) => e,+ }+}++// --- Signals -----------------------------------------------------------------++pub const SIGHUP: i32 = 1;+pub const SIGINT: i32 = 2;+pub const SIGQUIT: i32 = 3;+pub const SIGILL: i32 = 4;+pub const SIGABRT: i32 = 6;+pub const SIGBUS: i32 = 7;+pub const SIGFPE: i32 = 8;+pub const SIGKILL: i32 = 9;+pub const SIGUSR1: i32 = 10;+pub const SIGSEGV: i32 = 11;+pub const SIGUSR2: i32 = 12;+pub const SIGPIPE: i32 = 13;+pub const SIGALRM: i32 = 14;+pub const SIGTERM: i32 = 15;+pub const SIGCHLD: i32 = 17;+pub const SIGCONT: i32 = 18;+pub const SIGSTOP: i32 = 19;+pub const SIGTSTP: i32 = 20;+pub const SIGTTIN: i32 = 21;+pub const SIGTTOU: i32 = 22;++/// Signal names, index = number ("" = unused).+pub const SIGNAL_NAMES: [&str; 32] = [+ "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM",+ "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR",+ "SYS",+];+/// "INT" or "SIGINT" or "2" -> 2.+pub fn signal_number(name: &str) -> Option<i32> {+ if let Ok(n) = name.parse::<i32>() {+ return (1..64).contains(&n).then_some(n);+ }+ let name = name.strip_prefix("SIG").unwrap_or(name);+ SIGNAL_NAMES.iter().position(|&n| !n.is_empty() && n.eq_ignore_ascii_case(name)).map(|i| i as i32)+}++pub const SIG_DFL: usize = 0;+pub const SIG_IGN: usize = 1;+pub const SA_SIGINFO: u64 = 4;+pub const SA_RESTORER: u64 = 0x0400_0000;+pub const SA_RESTART: u64 = 0x1000_0000;+pub const SA_NODEFER: u64 = 0x4000_0000;+pub const SIG_BLOCK: u64 = 0;+pub const SIG_UNBLOCK: u64 = 1;+pub const SIG_SETMASK: u64 = 2;++/// The kernel's `struct sigaction` (not quite libc's: no sa_sigaction+/// union, the mask is 64 bits).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SigAction {+ pub handler: usize, // fn(i32) or fn(i32, *SigInfo, *ucontext); or SIG_DFL / SIG_IGN+ pub flags: u64,+ pub restorer: usize,+ pub mask: u64,+}++/// The first fields of Linux's `siginfo_t` (it's 128 bytes in all).+#[repr(C)]+pub struct SigInfo {+ pub signo: i32,+ pub errno: i32,+ pub code: i32,+ _pad: i32,+ /// SIGSEGV/SIGBUS: the faulting address. SIGCHLD/kill: the sender's+ /// pid (low 32 bits) and uid (high 32 bits).+ pub addr_or_pid: u64,+ /// SIGCHLD: the exit code (or the signal that ended/stopped the child).+ pub status: i32,+}++// After a handler returns it "returns" into this: the restorer, which asks+// the kernel to put everything back as it was before the signal (from the+// frame the kernel saved on our stack). Linux's libc does exactly this.+core::arch::global_asm!(+ ".global __restore_rt",+ "__restore_rt:",+ "mov rax, 15", // rt_sigreturn+ "syscall",+);+unsafe extern "C" {+ fn __restore_rt();+}++/// Install `handler` for `sig` (plain `fn(i32)`). Returns the old action.+pub fn signal(sig: i32, handler: extern "C" fn(i32)) -> i64 {+ sigaction_raw(sig, handler as usize, 0, 0)+}+/// SA_SIGINFO handler: also gets the SigInfo (fault address, sender pid...).+pub fn sigaction_info(sig: i32, handler: extern "C" fn(i32, *const SigInfo, *mut u8), flags: u64) -> i64 {+ sigaction_raw(sig, handler as usize, flags | SA_SIGINFO, 0)+}+/// Set SIG_DFL / SIG_IGN / a handler address, with flags and a mask of+/// signals to block while it runs.+pub fn sigaction_raw(sig: i32, handler: usize, flags: u64, mask: u64) -> i64 {+ let act = SigAction { handler, flags: flags | SA_RESTORER, restorer: __restore_rt as usize, mask };+ syscall5(SYS_RT_SIGACTION, sig as u64, &act as *const _ as u64, 0, 8, 0)+}+/// Change the blocked set; returns the old one (or -errno).+pub fn sigprocmask(how: u64, set: u64) -> i64 {+ let mut old = 0u64;+ match syscall5(SYS_RT_SIGPROCMASK, how, &set as *const u64 as u64, &mut old as *mut u64 as u64, 8, 0) {+ 0 => old as i64,+ e => e,+ }+}+pub fn sigbit(sig: i32) -> u64 {+ 1 << (sig - 1)+}+/// kill(pid, sig): pid > 0 one process, 0 my group, -pgid a group, -1 all.+pub fn kill(pid: i64, sig: i32) -> i64 {+ syscall3(SYS_KILL, pid as u64, sig as u64, 0)+}+/// Sleep until a signal handler has run (always returns -EINTR).+pub fn pause() -> i64 {+ syscall3(SYS_PAUSE, 0, 0, 0)+}++// --- Memory: mmap and brk ----------------------------------------------------++pub const PROT_NONE: u64 = 0;+pub const PROT_READ: u64 = 1;+pub const PROT_WRITE: u64 = 2;+pub const PROT_EXEC: u64 = 4;+pub const MAP_SHARED: u64 = 0x01;+pub const MAP_PRIVATE: u64 = 0x02;+pub const MAP_FIXED: u64 = 0x10;+pub const MAP_ANONYMOUS: u64 = 0x20;++/// mmap: the address of the new mapping, or -errno. fd -1 = anonymous.+pub fn mmap(addr: u64, len: u64, prot: u64, flags: u64, fd: i64, offset: u64) -> i64 {+ syscall6(SYS_MMAP, addr, len, prot, flags, fd as u64, offset)+}+pub fn munmap(addr: u64, len: u64) -> i64 {+ syscall3(SYS_MUNMAP, addr, len, 0)+}+pub fn mprotect(addr: u64, len: u64, prot: u64) -> i64 {+ syscall3(SYS_MPROTECT, addr, len, prot)+}+/// brk: move the end of the heap; returns the new end (brk(0) = current).+pub fn brk(addr: u64) -> u64 {+ syscall3(SYS_BRK, addr, 0, 0) as u64+}++/// Console raw mode on/off (keys arrive one by one, no echo). Returns the+/// old mode, or -ENOTTY if `fd` isn't the console.+pub fn set_raw(fd: i64, raw: bool) -> i64 {+ syscall3(SYS_IOCTL, fd as u64, TTY_SET_RAW, raw as u64)+}+/// Linux's `struct utsname`: sysname, nodename, release, version, machine,+/// domainname. Returns them as strings.+pub fn uname(buf: &mut [u8; 6 * 65]) -> [&str; 6] {+ syscall3(SYS_UNAME, buf.as_mut_ptr() as u64, 0, 0);+ core::array::from_fn(|i| {+ let field = &buf[i * 65..(i + 1) * 65];+ let len = field.iter().position(|&b| b == 0).unwrap_or(65);+ core::str::from_utf8(&field[..len]).unwrap_or("?")+ })+}+/// Fill `buf` with random bytes from the kernel (virtio-rng). Linux 318.+pub fn getrandom(buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETRANDOM, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+/// A raw ioctl: what `request` means depends on the device.+pub fn ioctl(fd: i64, request: u64, arg: u64) -> i64 {+ syscall3(SYS_IOCTL, fd as u64, request, arg)+}+/// The mount table as text ("path fstype" lines); returns the length.+pub fn mounts(buf: &mut [u8]) -> i64 {+ syscall3(SYS_MOUNTS, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+/// What `sysinfo` reports (same layout as the kernel's syscall::SysInfo).+/// `ticks[cpu]` counts that CPU's ~10 ms timer ticks, `busy_ticks[cpu]` the+/// ones where it was running a real thread rather than idling: sample twice+/// and subtract to get a CPU's load.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SysInfo {+ pub cpus: u64,+ pub this_cpu: u64,+ pub threads: u64,+ pub processes: u64,+ pub frames_in_use: u64,+ pub uptime_ticks: u64,+ pub ticks: [u64; 16],+ pub busy_ticks: [u64; 16],+}+/// Ask the kernel for CPU, thread, process and memory counts.+pub fn sysinfo() -> SysInfo {+ let mut info = SysInfo::default();+ syscall3(SYS_RB_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);+ info+}+/// Current directory into `buf`; returns its length (or -errno). The+/// kernel, like Linux, also writes a NUL and counts it; we don't.+pub fn getcwd(buf: &mut [u8]) -> i64 {+ let n = syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0);+ if n > 0 { n - 1 } else { n }+}+pub fn chdir(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_CHDIR, p, 0, 0))+}+/// mkdir(path, mode): 0o755 (the kernel ignores the mode for now).+pub fn mkdir(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_MKDIR, p, 0o755, 0))+}+pub fn rmdir(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_RMDIR, p, 0, 0))+}+/// The target of a symlink, into `buf` (no NUL added); its length or -errno.+pub fn readlink(path: &str, buf: &mut [u8]) -> i64 {+ with_cpath(path, |p| syscall3(SYS_READLINK, p, buf.as_mut_ptr() as u64, buf.len() as u64))+}+pub fn unlink(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_UNLINK, p, 0, 0))+}+/// rename(old, new): replaces a file at `new`. -EXDEV: copy + delete instead.+pub fn rename(old: &str, new: &str) -> i64 {+ with_cpath(old, |o| with_cpath(new, |n| syscall3(SYS_RENAME, o, n, 0)))+}+/// access(path, mode): 0 if it exists (we don't check permissions yet).+pub fn access(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_ACCESS, p, 0, 0))+}+pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+/// Start program `path` with arguments `argv` (NUL-separated, e.g.+/// "child\0hi"). Returns the child's pid (or -errno).+pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {+ with_cpath(path, |p| {+ syscall5(SYS_SPAWN, p, argv.as_ptr() as u64, argv.len() as u64, opts as *const SpawnOptions as u64, 0)+ })+}++/// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,+/// calling `f(name, d_type)` for each (d_type: 4 = directory, 8 = file).+pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {+ let mut pos = 0;+ while pos + 19 < len {+ let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+ let d_type = buf[pos + 18];+ let name = &buf[pos + 19..pos + reclen];+ let name = &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())];+ f(core::str::from_utf8(name).unwrap_or("?"), d_type);+ pos += reclen;+ }+}++// --- program arguments, environment, auxiliary vector -------------------------+//+// The kernel starts a program the way Linux does: rsp points at+// argc, argv[0..argc], NULL, envp[0..], NULL, auxv pairs..., AT_NULL 0+// and the strings themselves sit higher up on the stack.++static mut ARGC: usize = 0;+static mut ARGV: *const *const u8 = core::ptr::null();+static mut ENVP: *const *const u8 = core::ptr::null();+static mut AUXV: *const [u64; 2] = core::ptr::null();++/// A C string as &str (up to its NUL).+pub unsafe fn cstr(ptr: *const u8) -> &'static str {+ unsafe {+ let mut len = 0;+ while *ptr.add(len) != 0 {+ len += 1;+ }+ core::str::from_utf8(core::slice::from_raw_parts(ptr, len)).unwrap_or("?")+ }+}++/// The program's arguments; `arg(0)` is the program name.+pub fn argc() -> usize {+ unsafe { ARGC }+}+pub fn arg(i: usize) -> Option<&'static str> {+ unsafe { if i < ARGC { Some(cstr(*ARGV.add(i))) } else { None } }+}+/// The i-th environment string ("NAME=value").+pub fn env(i: usize) -> Option<&'static str> {+ unsafe {+ let p = *ENVP.add(i);+ if p.is_null() { None } else { Some(cstr(p)) }+ }+}+/// getenv("PATH") -> the value after "PATH=".+pub fn getenv(name: &str) -> Option<&'static str> {+ (0..).map_while(env).find_map(|e| e.strip_prefix(name)?.strip_prefix('='))+}+pub const AT_PHDR: u64 = 3;+pub const AT_PHENT: u64 = 4;+pub const AT_PHNUM: u64 = 5;+pub const AT_PAGESZ: u64 = 6;+pub const AT_ENTRY: u64 = 9;+pub const AT_RANDOM: u64 = 25;+pub const AT_EXECFN: u64 = 31;+/// An auxiliary vector entry (AT_PAGESZ -> 4096 ...), if the kernel gave it.+pub fn getauxval(kind: u64) -> Option<u64> {+ unsafe {+ let mut p = AUXV;+ while (*p)[0] != 0 {+ if (*p)[0] == kind {+ return Some((*p)[1]);+ }+ p = p.add(1);+ }+ None+ }+}++/// Write all of `buf` (pipes and files may take less than all at once).+pub fn write_all(fd: i64, mut buf: &[u8]) -> i64 {+ while !buf.is_empty() {+ let n = write(fd, buf);+ if n <= 0 {+ return n;+ }+ buf = &buf[n as usize..];+ }+ 0+}++/// Call `f` for each line read from `fd` (without the '\n'). Lines longer+/// than 1 KiB come in pieces. Returns 0, or -errno if a read fails.+pub fn for_each_line(fd: i64, mut f: impl FnMut(&[u8])) -> i64 {+ let mut line = [0u8; 1024];+ let mut len = 0;+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n < 0 {+ return n;+ }+ if n == 0 {+ if len > 0 {+ f(&line[..len]); // last line without a newline+ }+ return 0;+ }+ for &b in &buf[..n as usize] {+ if b == b'\n' || len == line.len() {+ f(&line[..len]);+ len = 0;+ if b == b'\n' {+ continue;+ }+ }+ line[len] = b;+ len += 1;+ }+ }+}++/// `ls`, `cat` etc. print errors like "cat: x: No such file or directory".+pub fn strerror(errno: i64) -> &'static str {+ match errno.abs() {+ 1 => "Operation not permitted",+ 2 => "No such file or directory",+ 3 => "No such process",+ 4 => "Interrupted system call",+ 5 => "I/O error",+ 7 => "Argument list too long",+ 8 => "Exec format error",+ 9 => "Bad file descriptor",+ 10 => "No child processes",+ 11 => "Try again",+ 12 => "Out of memory",+ 14 => "Bad address",+ 16 => "Device or resource busy",+ 19 => "No such device",+ 17 => "File exists",+ 18 => "Invalid cross-device link",+ 20 => "Not a directory",+ 21 => "Is a directory",+ 22 => "Invalid argument",+ 24 => "Too many open files",+ 25 => "Not a terminal",+ 27 => "File too large",+ 28 => "No space left on device",+ 29 => "Illegal seek",+ 30 => "Read-only file system",+ 32 => "Broken pipe",+ 34 => "Result too large",+ 36 => "File name too long",+ 38 => "Function not implemented",+ 39 => "Directory not empty",+ 40 => "Too many levels of symbolic links",+ 95 => "Operation not supported",+ 88 => "Socket operation on non-socket",+ 89 => "Destination address required",+ 90 => "Message too long",+ 92 => "Protocol not available",+ 93 => "Protocol not supported",+ 97 => "Address family not supported",+ 98 => "Address already in use",+ 100 => "Network is down",+ 101 => "Network is unreachable",+ 104 => "Connection reset by peer",+ 106 => "Already connected",+ 107 => "Not connected",+ 110 => "Connection timed out",+ 111 => "Connection refused",+ _ => "Unknown error",+ }+}++// --- Networking ------------------------------------------------------------------+//+// BSD sockets, the API every OS copied from 1983's 4.2BSD Unix: `socket()`+// makes an fd, `connect()` dials, read/write move data, `close()` hangs up.+// Servers `bind()` a port, `listen()`, and `accept()` each caller.++pub const AF_INET: u64 = 2;+pub const SOCK_STREAM: u64 = 1; // TCP+pub const SOCK_DGRAM: u64 = 2; // UDP (or ICMP with IPPROTO_ICMP)+pub const IPPROTO_ICMP: u64 = 1;++/// An IPv4 address that prints as "10.0.2.15".+#[derive(Clone, Copy, PartialEq, Eq, Default)]+pub struct Ip(pub [u8; 4]);++impl fmt::Display for Ip {+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {+ let [a, b, c, d] = self.0;+ write!(f, "{}.{}.{}.{}", a, b, c, d)+ }+}++/// "10.0.2.2" -> Ip, or None if it isn't a dotted-quad address.+pub fn parse_ip(s: &str) -> Option<Ip> {+ let mut ip = [0u8; 4];+ let mut parts = s.split('.');+ for b in ip.iter_mut() {+ *b = parts.next()?.parse().ok()?;+ }+ if parts.next().is_some() { None } else { Some(Ip(ip)) }+}++/// Linux's `struct sockaddr_in` (port and address big-endian).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SockAddrIn {+ pub family: u16,+ pub port_be: u16,+ pub addr: [u8; 4],+ pub zero: [u8; 8],+}++impl SockAddrIn {+ pub fn new(ip: Ip, port: u16) -> SockAddrIn {+ SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr: ip.0, zero: [0; 8] }+ }+ pub fn ip(&self) -> Ip {+ Ip(self.addr)+ }+ pub fn port(&self) -> u16 {+ u16::from_be(self.port_be)+ }+}++pub fn socket(ty: u64, protocol: u64) -> i64 {+ syscall3(SYS_SOCKET, AF_INET, ty, protocol)+}+pub fn connect(fd: i64, ip: Ip, port: u16) -> i64 {+ let sa = SockAddrIn::new(ip, port);+ syscall3(SYS_CONNECT, fd as u64, &sa as *const _ as u64, 16)+}+pub fn bind(fd: i64, port: u16) -> i64 {+ let sa = SockAddrIn::new(Ip([0; 4]), port);+ syscall3(SYS_BIND, fd as u64, &sa as *const _ as u64, 16)+}+pub fn listen(fd: i64, backlog: u64) -> i64 {+ syscall3(SYS_LISTEN, fd as u64, backlog, 0)+}+/// Wait for a connection: (new fd or -errno, caller's address).+pub fn accept(fd: i64) -> (i64, SockAddrIn) {+ let mut sa = SockAddrIn::default();+ let fd = syscall3(SYS_ACCEPT, fd as u64, &mut sa as *mut _ as u64, 0);+ (fd, sa)+}+pub fn sendto(fd: i64, buf: &[u8], ip: Ip, port: u16) -> i64 {+ let sa = SockAddrIn::new(ip, port);+ syscall5(SYS_SENDTO, fd as u64, buf.as_ptr() as u64, buf.len() as u64, 0, &sa as *const _ as u64)+}+/// Receive one datagram (or some stream bytes): (length or -errno, sender).+pub fn recvfrom(fd: i64, buf: &mut [u8]) -> (i64, SockAddrIn) {+ let mut sa = SockAddrIn::default();+ let n = syscall5(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0, &mut sa as *mut _ as u64);+ (n, sa)+}+/// SO_RCVTIMEO: recv/accept give up with -EAGAIN after `ms` (0 = never).+pub fn set_recv_timeout(fd: i64, ms: u64) -> i64 {+ let tv = [ms / 1000, (ms % 1000) * 1000]; // struct timeval+ syscall5(SYS_SETSOCKOPT, fd as u64, 1, 20, tv.as_ptr() as u64, 16)+}+/// Microseconds since boot (clock_gettime(CLOCK_MONOTONIC)).+pub fn now_us() -> u64 {+ let mut ts = [0u64; 2];+ syscall3(SYS_CLOCK_GETTIME, 1, ts.as_mut_ptr() as u64, 0);+ ts[0] * 1_000_000 + ts[1] / 1000+}+/// Look up a host name with DNS (or parse it if it's already an address).+pub fn resolve(name: &str) -> Result<Ip, i64> {+ if let Some(ip) = parse_ip(name) {+ return Ok(ip);+ }+ let mut out = [[0u8; 4]; 1];+ match syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, 1, 0) {+ n if n < 0 => Err(n),+ 0 => Err(-2), // no A record: ENOENT+ _ => Ok(Ip(out[0])),+ }+}+/// All IPv4 addresses for `name` (up to `out.len()`); count or -errno.+pub fn resolve_all(name: &str, out: &mut [[u8; 4]]) -> i64 {+ syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, out.len() as u64, 0)+}++/// What `ifconfig` shows; same layout as the kernel's `net::NetInfo`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct NetInfo {+ pub present: u8,+ pub configured: u8,+ pub mac: [u8; 6],+ pub address: [u8; 4],+ pub prefix_len: u8,+ pub irq: u8,+ pub _pad: [u8; 2],+ pub gateway: [u8; 4],+ pub dns: [u8; 4],+ pub rx_packets: u64,+ pub rx_bytes: u64,+ pub tx_packets: u64,+ pub tx_bytes: u64,+}+pub fn netinfo() -> NetInfo {+ let mut info = NetInfo::default();+ syscall3(SYS_NETINFO, &mut info as *mut NetInfo as u64, 0, 0);+ info+}++pub struct Stdout;++impl fmt::Write for Stdout {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write(1, s.as_bytes());+ Ok(())+ }+}++/// Collects the pieces of one `print!`/`println!` so the whole line goes to+/// the kernel in a single `write`. With several CPUs, two programs printing+/// at the same moment would otherwise have their pieces mixed up mid-line.+/// Flushes when full and when dropped (at the end of the macro).+pub struct LineWriter {+ fd: i64,+ buf: [u8; 256],+ len: usize,+}++impl LineWriter {+ pub fn new(fd: i64) -> LineWriter {+ LineWriter { fd, buf: [0; 256], len: 0 }+ }+ pub fn flush(&mut self) {+ if self.len > 0 {+ write_all(self.fd, &self.buf[..self.len]);+ self.len = 0;+ }+ }+}++impl fmt::Write for LineWriter {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ if self.len + s.len() > self.buf.len() {+ self.flush();+ }+ if s.len() > self.buf.len() {+ write_all(self.fd, s.as_bytes()); // too big to buffer: send as is+ } else {+ self.buf[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());+ self.len += s.len();+ }+ Ok(())+ }+}++impl Drop for LineWriter {+ fn drop(&mut self) {+ self.flush();+ }+}++macro_rules! print {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let mut out = $crate::ulib::LineWriter::new(1);+ let _ = write!(out, $($arg)*);+ }};+}++pub struct Stderr;++impl fmt::Write for Stderr {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write(2, s.as_bytes());+ Ok(())+ }+}++/// Write formatted text to any fd: `fprint!(fd, "...")`.+pub struct Fd(pub i64);++impl fmt::Write for Fd {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write_all(self.0, s.as_bytes());+ Ok(())+ }+}++macro_rules! eprintln {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let mut out = $crate::ulib::LineWriter::new(2);+ let _ = writeln!(out, $($arg)*);+ }};+}++macro_rules! println {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let mut out = $crate::ulib::LineWriter::new(1);+ let _ = writeln!(out, $($arg)*);+ }};+}++// The entry point. The kernel jumps here with rsp at `argc` (see above);+// hand that pointer to Rust (rdi = first argument), keeping rsp 16-byte+// aligned the way a `call` expects.+core::arch::global_asm!(+ ".global _start",+ "_start:",+ "mov rdi, rsp",+ "and rsp, -16",+ "call start_rust",+ "ud2",+);++/// Find argv/envp/auxv on the initial stack, run main, exit with its result.+#[unsafe(no_mangle)]+extern "C" fn start_rust(sp: *const usize) -> ! {+ unsafe {+ ARGC = *sp;+ ARGV = sp.add(1) as *const *const u8;+ ENVP = ARGV.add(ARGC + 1);+ let mut p = ENVP;+ while !(*p).is_null() {+ p = p.add(1);+ }+ AUXV = p.add(1) as *const [u64; 2];+ }+ exit(crate::main())+}++#[panic_handler]+fn panic(info: &core::panic::PanicInfo) -> ! {+ println!("[user panic] {}", info);+ exit(101)+}++// --- heap ----------------------------------------------------------------+//+// A tiny allocator so programs can use `Vec`, `String`, `format!` (with+// `extern crate alloc;`). Memory comes from `brk`: the kernel's "program+// break" is the end of a heap area right after the program, and moving it+// up gives us more (zeroed, demand-paged) memory.+// Same idea as the kernel's fixed-size-block allocator: sizes are rounded up+// to a power of two (16 B .. 1 MiB) and freed blocks go on one free list+// per size, to be reused by the next allocation of that size. Programs may+// have several threads (stage 15), so a tiny spinlock guards the heap; a+// thread that finds it taken yields its CPU instead of spinning long.++const CLASSES: usize = 17; // 16 << 16 = 1 MiB++struct Heap {+ start: UnsafeCell<u64>, // where the heap began (0 = not set up yet)+ used: UnsafeCell<u64>, // bump pointer: end of what's handed out+ end: UnsafeCell<u64>, // the current break+ free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty)+ lock: AtomicBool,+}++unsafe impl Sync for Heap {} // the fields are only touched under `lock`++impl Heap {+ fn lock(&self) {+ while self.lock.swap(true, Ordering::Acquire) {+ sched_yield();+ }+ }+ fn unlock(&self) {+ self.lock.store(false, Ordering::Release);+ }+}++#[global_allocator]+static HEAP: Heap = Heap {+ start: UnsafeCell::new(0),+ used: UnsafeCell::new(0),+ end: UnsafeCell::new(0),+ free: UnsafeCell::new([0; CLASSES]),+ lock: AtomicBool::new(false),+};++/// Size class for a layout: 0 = 16 bytes, 1 = 32, ...+fn class_of(layout: Layout) -> usize {+ let size = layout.size().max(layout.align()).max(16).next_power_of_two();+ size.trailing_zeros() as usize - 4+}++unsafe impl GlobalAlloc for Heap {+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ self.lock();+ let p = unsafe { self.alloc_locked(layout) };+ self.unlock();+ p+ }++ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ let class = class_of(layout);+ self.lock();+ unsafe {+ let free = &mut *self.free.get();+ *(ptr as *mut usize) = free[class];+ free[class] = ptr as usize;+ }+ self.unlock();+ }+}++impl Heap {+ unsafe fn alloc_locked(&self, layout: Layout) -> *mut u8 {+ let class = class_of(layout);+ if class >= CLASSES {+ return core::ptr::null_mut(); // too big: "out of memory"+ }+ unsafe {+ let free = &mut *self.free.get();+ if free[class] != 0 {+ // Pop a freed block; its first 8 bytes hold the next one.+ let block = free[class] as *mut u8;+ free[class] = *(block as *const usize);+ return block;+ }+ let (used, end) = (&mut *self.used.get(), &mut *self.end.get());+ if *self.start.get() == 0 {+ *self.start.get() = brk(0);+ *used = *self.start.get();+ *end = *used;+ }+ // Carve a new block. Blocks are aligned to their size, which+ // covers any alignment up to that size.+ let size = 16u64 << class;+ let start = (*used).next_multiple_of(size);+ if start + size > *end {+ // Grow the heap (at least 64 KiB at a time).+ let want = (start + size).max(*end + 64 * 1024);+ let got = brk(want);+ if got < start + size {+ return core::ptr::null_mut(); // the kernel said no+ }+ *end = got;+ }+ *used = start + size;+ start as *mut u8+ }+ }+}++// --- Stage 15: what a libc's plumbing needs ----------------------------------++pub const SYS_POLL: u64 = 7;+pub const SYS_SELECT: u64 = 23;+pub const SYS_PPOLL: u64 = 271;+pub const SYS_PREAD64: u64 = 17;+pub const SYS_PWRITE64: u64 = 18;+pub const SYS_READV: u64 = 19;+pub const SYS_WRITEV: u64 = 20;+pub const SYS_CLONE: u64 = 56;+pub const SYS_EXIT_THREAD: u64 = 60; // Linux's "exit": just this thread+pub const SYS_SETSID: u64 = 112;+pub const SYS_ARCH_PRCTL: u64 = 158;+pub const SYS_GETTID: u64 = 186;+pub const SYS_FUTEX: u64 = 202;+pub const SYS_EPOLL_WAIT: u64 = 232;+pub const SYS_EPOLL_CTL: u64 = 233;+pub const SYS_TGKILL: u64 = 234;+pub const SYS_EPOLL_CREATE1: u64 = 291;++pub const O_NOCTTY: u64 = 0o400;+pub const O_NONBLOCK: u64 = 0o4000;+pub const O_RDWR: u64 = 2;+pub const F_GETFL: u64 = 3;+pub const F_SETFL: u64 = 4;+pub const EINPROGRESS: i64 = 115;++/// Turn O_NONBLOCK on or off for `fd` (fcntl F_GETFL + F_SETFL).+pub fn set_nonblocking(fd: i64, on: bool) -> i64 {+ let flags = fcntl(fd, F_GETFL, 0);+ if flags < 0 {+ return flags;+ }+ let flags = if on { flags as u64 | O_NONBLOCK } else { flags as u64 & !O_NONBLOCK };+ fcntl(fd, F_SETFL, flags)+}++pub fn pread(fd: i64, buf: &mut [u8], offset: u64) -> i64 {+ syscall6(SYS_PREAD64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, offset, 0, 0)+}+pub fn pwrite(fd: i64, buf: &[u8], offset: u64) -> i64 {+ syscall6(SYS_PWRITE64, fd as u64, buf.as_ptr() as u64, buf.len() as u64, offset, 0, 0)+}+/// writev: several buffers in one call (`struct iovec {base, len}`).+pub fn readv(fd: i64, bufs: &mut [&mut [u8]]) -> i64 {+ let iov: alloc::vec::Vec<[u64; 2]> = bufs.iter_mut().map(|b| [b.as_mut_ptr() as u64, b.len() as u64]).collect();+ syscall3(SYS_READV, fd as u64, iov.as_ptr() as u64, iov.len() as u64)+}+pub fn writev(fd: i64, bufs: &[&[u8]]) -> i64 {+ let iov: alloc::vec::Vec<[u64; 2]> = bufs.iter().map(|b| [b.as_ptr() as u64, b.len() as u64]).collect();+ syscall3(SYS_WRITEV, fd as u64, iov.as_ptr() as u64, iov.len() as u64)+}++/// Linux 228: 0 = CLOCK_REALTIME (wall clock), 1 = CLOCK_MONOTONIC.+pub fn clock_gettime(clock: u64) -> (u64, u64) {+ let mut ts = [0u64; 2];+ syscall3(SYS_CLOCK_GETTIME, clock, ts.as_mut_ptr() as u64, 0);+ (ts[0], ts[1])+}++// poll / epoll (see the kernel's poll.rs).+pub const POLLIN: u16 = 0x1;+pub const POLLOUT: u16 = 0x4;+pub const POLLERR: u16 = 0x8;+pub const POLLHUP: u16 = 0x10;+pub const POLLNVAL: u16 = 0x20;++/// `struct pollfd`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct PollFd {+ pub fd: i32,+ pub events: u16,+ pub revents: u16,+}+/// Wait until one of `fds` is ready (timeout_ms -1 = forever); returns+/// how many are, 0 on timeout.+pub fn poll(fds: &mut [PollFd], timeout_ms: i64) -> i64 {+ syscall3(SYS_POLL, fds.as_mut_ptr() as u64, fds.len() as u64, timeout_ms as u64)+}+/// ppoll: poll with a timespec timeout (and a signal mask, unused here).+pub fn ppoll_us(fds: &mut [PollFd], timeout_us: u64) -> i64 {+ let ts = [timeout_us / 1_000_000, timeout_us % 1_000_000 * 1000];+ syscall5(SYS_PPOLL, fds.as_mut_ptr() as u64, fds.len() as u64, ts.as_ptr() as u64, 0, 8)+}+/// select(nfds, read set, write set, NULL, timeval): the 1983 interface.+/// The sets are bitmaps of fds (64 per word); on return only the ready+/// ones are still set.+pub fn select(nfds: i64, read: &mut [u64; 16], write: &mut [u64; 16], timeout_us: u64) -> i64 {+ let tv = [timeout_us / 1_000_000, timeout_us % 1_000_000];+ syscall5(SYS_SELECT, nfds as u64, read.as_mut_ptr() as u64, write.as_mut_ptr() as u64, 0, tv.as_ptr() as u64)+}+pub fn fd_set(set: &mut [u64; 16], fd: i64) {+ set[fd as usize / 64] |= 1 << (fd % 64);+}+pub fn fd_isset(set: &[u64; 16], fd: i64) -> bool {+ set[fd as usize / 64] & (1 << (fd % 64)) != 0+}++pub const EPOLL_CTL_ADD: u64 = 1;+pub const EPOLL_CTL_DEL: u64 = 2;+pub const EPOLL_CTL_MOD: u64 = 3;+pub const EPOLLIN: u32 = 0x1;+pub const EPOLLOUT: u32 = 0x4;++/// `struct epoll_event` is *packed* on x86_64: 4 + 8 = 12 bytes.+#[repr(C, packed)]+#[derive(Clone, Copy, Default)]+pub struct EpollEvent {+ pub events: u32,+ pub data: u64,+}+pub fn epoll_create1(flags: u64) -> i64 {+ syscall3(SYS_EPOLL_CREATE1, flags, 0, 0)+}+pub fn epoll_ctl(epfd: i64, op: u64, fd: i64, events: u32, data: u64) -> i64 {+ let ev = EpollEvent { events, data };+ syscall5(SYS_EPOLL_CTL, epfd as u64, op, fd as u64, &ev as *const EpollEvent as u64, 0)+}+pub fn epoll_wait(epfd: i64, out: &mut [EpollEvent], timeout_ms: i64) -> i64 {+ syscall5(SYS_EPOLL_WAIT, epfd as u64, out.as_mut_ptr() as u64, out.len() as u64, timeout_ms as u64, 0)+}++// Sessions and terminals.+pub fn setsid() -> i64 {+ syscall3(SYS_SETSID, 0, 0, 0)+}+pub const TCGETS: u64 = 0x5401;+pub const TCSETS: u64 = 0x5402;+pub const TIOCSCTTY: u64 = 0x540E;+pub const TIOCGPTN: u64 = 0x8004_5430;+pub const TIOCSPTLCK: u64 = 0x4004_5431;+pub const ICANON: u32 = 0x2;+pub const ECHO: u32 = 0x8;+pub const ISIG: u32 = 0x1;+pub const VMIN: usize = 6;+pub const VTIME: usize = 5;++/// Linux's `struct termios` (36 bytes): the terminal's settings.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct Termios {+ pub iflag: u32,+ pub oflag: u32,+ pub cflag: u32,+ pub lflag: u32,+ pub line: u8,+ pub cc: [u8; 19],+}+pub fn tcgetattr(fd: i64) -> Result<Termios, i64> {+ let mut t = Termios::default();+ match ioctl(fd, TCGETS, &mut t as *mut Termios as u64) {+ 0 => Ok(t),+ e => Err(e),+ }+}+pub fn tcsetattr(fd: i64, t: &Termios) -> i64 {+ ioctl(fd, TCSETS, t as *const Termios as u64)+}+/// Raw mode, like libc's cfmakeraw: no line editing, echo or signals,+/// no character conversions; read returns as soon as 1 byte is there.+pub fn cfmakeraw(t: &mut Termios) {+ t.iflag &= !(0x1 | 0x2 | 0x8 | 0x20 | 0x40 | 0x80 | 0x100 | 0x400); // IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON+ t.oflag &= !0x1; // OPOST+ t.lflag &= !(ECHO | 0x40 | ICANON | ISIG | 0x8000); // ECHO ECHONL ICANON ISIG IEXTEN+ t.cflag = (t.cflag & !0x130) | 0x30; // CS8, no parity+ t.cc[VMIN] = 1;+ t.cc[VTIME] = 0;+}++/// Open a new pseudo-terminal: returns (master fd, slave number N); the+/// slave is /dev/pts/N. Like posix_openpt + grantpt + unlockpt.+pub fn openpty() -> Result<(i64, u32), i64> {+ let master = open("/dev/ptmx", O_RDWR | O_NOCTTY);+ if master < 0 {+ return Err(master);+ }+ let mut n = 0u32;+ let unlock = 0u32;+ let r = ioctl(master, TIOCGPTN, &mut n as *mut u32 as u64);+ let r = if r == 0 { ioctl(master, TIOCSPTLCK, &unlock as *const u32 as u64) } else { r };+ if r != 0 {+ close(master);+ return Err(r);+ }+ Ok((master, n))+}++// Threads: clone + futex + TLS.+pub const FUTEX_WAIT: u64 = 0;+pub const FUTEX_WAKE: u64 = 1;+pub const FUTEX_PRIVATE_FLAG: u64 = 128;+pub const CLONE_VM: u64 = 0x100;+pub const CLONE_FS: u64 = 0x200;+pub const CLONE_FILES: u64 = 0x400;+pub const CLONE_SIGHAND: u64 = 0x800;+pub const CLONE_THREAD: u64 = 0x10000;+pub const CLONE_SYSVSEM: u64 = 0x40000;+pub const CLONE_SETTLS: u64 = 0x80000;+pub const CLONE_PARENT_SETTID: u64 = 0x100000;+pub const CLONE_CHILD_CLEARTID: u64 = 0x200000;+pub const ARCH_SET_FS: u64 = 0x1002;+pub const ARCH_GET_FS: u64 = 0x1003;++use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};++pub fn gettid() -> i64 {+ syscall3(SYS_GETTID, 0, 0, 0)+}+pub fn tgkill(tgid: i64, tid: i64, sig: i32) -> i64 {+ syscall3(SYS_TGKILL, tgid as u64, tid as u64, sig as u64)+}+/// Sleep while `*word == expected` (until futex_wake, or `timeout_us`).+pub fn futex_wait(word: &AtomicU32, expected: u32, timeout_us: Option<u64>) -> i64 {+ let ts = timeout_us.map(|us| [us / 1_000_000, (us % 1_000_000) * 1000]);+ let ts_ptr = ts.as_ref().map_or(0, |t| t.as_ptr() as u64);+ syscall6(SYS_FUTEX, word.as_ptr() as u64, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, expected as u64, ts_ptr, 0, 0)+}+/// Wake up to `n` threads sleeping on `word`; returns how many woke.+pub fn futex_wake(word: &AtomicU32, n: u32) -> i64 {+ syscall6(SYS_FUTEX, word.as_ptr() as u64, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, n as u64, 0, 0, 0)+}++/// A mutex built on a futex (Ulrich Drepper's "Futexes Are Tricky",+/// mutex #2). The word is 0 = unlocked, 1 = locked, 2 = locked and+/// someone may be sleeping. Taking a free lock is one atomic instruction,+/// no syscall; only contention goes to the kernel.+pub struct FutexMutex {+ state: AtomicU32,+}+impl FutexMutex {+ pub const fn new() -> FutexMutex {+ FutexMutex { state: AtomicU32::new(0) }+ }+ pub fn lock(&self) {+ if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok() {+ return; // fast path: it was free+ }+ // Slow path: mark "contended" (2) and sleep until it's free. The+ // kernel only puts us to sleep if the word is *still* 2, so an+ // unlock between our swap and the futex call can't be missed.+ while self.state.swap(2, Ordering::Acquire) != 0 {+ futex_wait(&self.state, 2, None);+ }+ }+ pub fn unlock(&self) {+ // 1 -> 0: nobody waits. 2 -> 0: wake one sleeper.+ if self.state.swap(0, Ordering::Release) == 2 {+ futex_wake(&self.state, 1);+ }+ }+}++/// Thread-local storage, the x86_64 way: each thread's FS register base+/// points at its own block, whose first word points at itself (so code can+/// find the block with `mov rax, fs:[0]`). We keep one u64 per thread in+/// the second word.+#[repr(C)]+pub struct TlsBlock {+ pub self_ptr: u64,+ pub value: u64,+}+pub fn arch_prctl(code: u64, addr: u64) -> i64 {+ syscall3(SYS_ARCH_PRCTL, code, addr, 0)+}+/// This thread's TLS value (fs:[8]).+pub fn tls_value() -> u64 {+ let v: u64;+ unsafe { asm!("mov {}, fs:[8]", out(reg) v, options(nostack, readonly)) };+ v+}++/// A running thread (see `spawn_thread`).+pub struct Thread {+ pub tid: i64,+ /// The kernel writes our tid here (CLONE_PARENT_SETTID) and zeroes it ++ /// futex-wakes it when the thread exits (CLONE_CHILD_CLEARTID): that's+ /// how `join` knows it's done, exactly like pthread_join.+ tid_word: alloc::boxed::Box<AtomicU32>,+ stack: u64,+ tls: alloc::boxed::Box<TlsBlock>,+}++const THREAD_STACK: u64 = 64 * 1024;+type ThreadFn = alloc::boxed::Box<dyn FnOnce()>;++/// The new thread starts here, on its own stack, with the boxed closure.+extern "C" fn thread_entry(arg: u64) -> ! {+ let f = unsafe { alloc::boxed::Box::from_raw(arg as *mut ThreadFn) };+ f();+ // exit (60), not exit_group: only this thread ends.+ syscall3(SYS_EXIT_THREAD, 0, 0, 0);+ loop {}+}++/// Start `f` in a new thread of this process (same memory, same fds),+/// with `tls_value` in its TLS block. Like pthread_create.+pub fn spawn_thread(tls_value: u64, f: impl FnOnce() + Send + 'static) -> Result<Thread, i64> {+ let stack = mmap(0, THREAD_STACK, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ if stack < 0 {+ return Err(stack);+ }+ let stack = stack as u64;+ let boxed: alloc::boxed::Box<ThreadFn> = alloc::boxed::Box::new(alloc::boxed::Box::new(f));+ let arg = alloc::boxed::Box::into_raw(boxed) as u64;+ let mut tls = alloc::boxed::Box::new(TlsBlock { self_ptr: 0, value: tls_value });+ tls.self_ptr = &*tls as *const TlsBlock as u64;+ let tid_word = alloc::boxed::Box::new(AtomicU32::new(0));+ // The child's stack starts with [entry, arg] for the asm below.+ let sp = stack + THREAD_STACK - 16;+ unsafe {+ *(sp as *mut u64) = thread_entry as usize as u64;+ *((sp + 8) as *mut u64) = arg;+ }+ let flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM+ | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID;+ let tid_ptr = tid_word.as_ptr() as u64;+ let ret: i64;+ // clone returns twice: the parent gets the tid, the child 0 -- on its+ // new stack, so it must not touch the parent's stack frame. It pops+ // [entry, arg] and calls entry(arg) straight from assembly (after the+ // two pops rsp is 16-byte aligned, as `call` wants).+ unsafe {+ asm!(+ "syscall",+ "test rax, rax",+ "jnz 2f",+ "pop rax",+ "pop rdi",+ "call rax",+ "ud2",+ "2:",+ inlateout("rax") SYS_CLONE as i64 => ret,+ in("rdi") flags,+ in("rsi") sp,+ in("rdx") tid_ptr,+ in("r10") tid_ptr,+ in("r8") tls.self_ptr,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack)+ );+ }+ if ret < 0 {+ unsafe { drop(alloc::boxed::Box::from_raw(arg as *mut ThreadFn)) };+ munmap(stack, THREAD_STACK);+ return Err(ret);+ }+ Ok(Thread { tid: ret, tid_word, stack, tls })+}++impl Thread {+ /// Wait for the thread to finish, then free its stack.+ pub fn join(self) {+ loop {+ let tid = self.tid_word.load(Ordering::Acquire);+ if tid == 0 {+ break; // the kernel cleared it: the thread is gone+ }+ futex_wait(&self.tid_word, tid, None);+ }+ munmap(self.stack, THREAD_STACK);+ drop(self.tls);+ }+}++// --- Self-checking test programs ---------------------------------------------++/// Counts failed checks for the test programs (forktest, sigtest, ...):+/// each prints "ok" or "FAILED" per check and exits with the failures.+pub struct Checks {+ pub name: &'static str,+ pub failed: i64,+}++impl Checks {+ pub fn new(name: &'static str) -> Checks {+ Checks { name, failed: 0 }+ }+ pub fn check(&mut self, ok: bool, what: &str) {+ if ok {+ println!("[{}] ok: {}", self.name, what);+ } else {+ println!("[{}] FAILED: {}", self.name, what);+ self.failed += 1;+ }+ }+ /// Print the summary; the result is the exit code.+ pub fn done(self) -> i64 {+ if self.failed == 0 {+ println!("[{}] all checks passed", self.name);+ } else {+ println!("[{}] {} check(s) failed", self.name, self.failed);+ }+ self.failed+ }+}
@@ -0,0 +1,889 @@+//! unixtest -- stage 18: AF_UNIX sockets and /proc/self. Run with one+//! group name; exit code = number of failed checks:+//! stream a path socket (ramfs, ext2), echo across fork, abstract+//! names, autobind, EOF/ECONNRESET/EPIPE, SO_PEERCRED+//! pair socketpair: stream, seqpacket (message borders), dgram+//! dgram sendto/recvfrom between named sockets, MSG_TRUNC, connect+//! rights SCM_RIGHTS (a file and a pipe end), MSG_CTRUNC,+//! MSG_CMSG_CLOEXEC, in-flight fds closed, SCM_CREDENTIALS+//! nonblock EAGAIN, buffer limits (backpressure), poll and epoll+//! (EPOLLRDHUP, EPOLLHUP), nonblocking accept+//! proc /proc/self/fd and /proc/self/exe+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::string::String;+use alloc::vec::Vec;+use ulib::*;++const SYS_SENDMSG: u64 = 46;+const SYS_RECVMSG: u64 = 47;+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_ACCEPT4: u64 = 288;++const AF_UNIX: u64 = 1;+const SOCK_STREAM: u64 = 1;+const SOCK_DGRAM: u64 = 2;+const SOCK_SEQPACKET: u64 = 5;+const SOCK_NONBLOCK: u64 = 0o4000;+const SOCK_CLOEXEC: u64 = 0o2000000;+const MSG_PEEK: u64 = 2;+const MSG_CTRUNC: i32 = 8;+const MSG_TRUNC: u64 = 0x20;+const MSG_DONTWAIT: u64 = 0x40;+const MSG_WAITALL: u64 = 0x100;+const MSG_NOSIGNAL: u64 = 0x4000;+const MSG_CMSG_CLOEXEC: u64 = 0x4000_0000;+const SOL_SOCKET: u64 = 1;+const SO_TYPE: u64 = 3;+const SO_ERROR: u64 = 4;+const SO_SNDBUF: u64 = 7;+const SO_RCVBUF: u64 = 8;+const SO_PASSCRED: u64 = 16;+const SO_PEERCRED: u64 = 17;+const SO_ACCEPTCONN: u64 = 30;+const SO_DOMAIN: u64 = 39;+const POLLIN: u16 = 1;+const POLLOUT: u16 = 4;+const POLLHUP: u16 = 0x10;+const EPOLLRDHUP: u32 = 0x2000;+const EPOLLHUP: u32 = 0x10;+const ENXIO: i64 = 6;+const EPIPE: i64 = 32;+const EMSGSIZE: i64 = 90;+const EPROTOTYPE: i64 = 91;+const ENOTSOCK: i64 = 88;+const EADDRINUSE: i64 = 98;+const ECONNRESET: i64 = 104;+const EISCONN: i64 = 106;+const ENOTCONN: i64 = 107;+const ECONNREFUSED: i64 = 111;+const EOPNOTSUPP: i64 = 95;++// --- helpers ---------------------------------------------------------------------++/// struct sockaddr_un: (bytes, length to pass). A name starting with+/// '@' is abstract (a 0 byte in front, no trailing 0), as `ss` shows them.+fn sun(name: &str) -> ([u8; 110], u64) {+ let mut a = [0u8; 110];+ a[0] = AF_UNIX as u8;+ let b = name.as_bytes();+ if let Some(rest) = name.strip_prefix('@') {+ a[3..3 + rest.len()].copy_from_slice(rest.as_bytes());+ (a, 3 + rest.len() as u64)+ } else {+ a[2..2 + b.len()].copy_from_slice(b);+ (a, 2 + b.len() as u64 + 1)+ }+}++fn usock(ty: u64) -> i64 {+ syscall3(SYS_SOCKET, AF_UNIX, ty, 0)+}++fn ubind(fd: i64, name: &str) -> i64 {+ let (a, len) = sun(name);+ syscall3(SYS_BIND, fd as u64, a.as_ptr() as u64, len)+}++fn uconnect(fd: i64, name: &str) -> i64 {+ let (a, len) = sun(name);+ syscall3(SYS_CONNECT, fd as u64, a.as_ptr() as u64, len)+}++fn uaccept(fd: i64) -> i64 {+ syscall3(SYS_ACCEPT, fd as u64, 0, 0)+}++/// getsockname/getpeername: the name as text ('@' for abstract, "" for+/// unnamed), or the error.+fn name_of(nr: u64, fd: i64) -> Result<String, i64> {+ let mut a = [0u8; 110];+ let mut len = 110u32;+ let r = syscall3(nr, fd as u64, a.as_mut_ptr() as u64, &mut len as *mut u32 as u64);+ if r < 0 {+ return Err(r);+ }+ let path = &a[2..len as usize];+ Ok(match path.first() {+ None => String::new(),+ Some(0) => format!("@{}", core::str::from_utf8(&path[1..]).unwrap_or("?")),+ Some(_) => String::from(core::str::from_utf8(&path[..path.len() - 1]).unwrap_or("?")),+ })+}++fn pair(ty: u64) -> (i64, i64) {+ let mut sv = [-1i32; 2];+ let r = syscall5(SYS_SOCKETPAIR, AF_UNIX, ty, 0, sv.as_mut_ptr() as u64, 0);+ if r < 0 {+ return (r, r);+ }+ (sv[0] as i64, sv[1] as i64)+}++fn getopt(fd: i64, opt: u64) -> (i64, u32) {+ let mut v = 0u32;+ let mut len = 4u32;+ let r = syscall5(SYS_GETSOCKOPT, fd as u64, SOL_SOCKET, opt, &mut v as *mut u32 as u64, &mut len as *mut u32 as u64);+ (r, v)+}++fn setopt(fd: i64, opt: u64, v: u32) -> i64 {+ syscall5(SYS_SETSOCKOPT, fd as u64, SOL_SOCKET, opt, &v as *const u32 as u64, 4)+}++/// SO_PEERCRED: (pid, uid, gid).+fn peercred(fd: i64) -> (i64, [u32; 3]) {+ let mut v = [0u32; 3];+ let mut len = 12u32;+ let r = syscall5(SYS_GETSOCKOPT, fd as u64, SOL_SOCKET, SO_PEERCRED, v.as_mut_ptr() as u64, &mut len as *mut u32 as u64);+ (r, v)+}++fn send(fd: i64, data: &[u8], flags: u64) -> i64 {+ syscall6(SYS_SENDTO, fd as u64, data.as_ptr() as u64, data.len() as u64, flags, 0, 0)+}++fn recv(fd: i64, buf: &mut [u8], flags: u64) -> i64 {+ syscall6(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, flags, 0, 0)+}++fn send_to(fd: i64, data: &[u8], name: &str) -> i64 {+ let (a, len) = sun(name);+ syscall6(SYS_SENDTO, fd as u64, data.as_ptr() as u64, data.len() as u64, 0, a.as_ptr() as u64, len)+}++/// recvfrom with the sender's name.+fn recv_from(fd: i64, buf: &mut [u8], flags: u64) -> (i64, String) {+ let mut a = [0u8; 110];+ let mut len = 110u32;+ let n = syscall6(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, flags, a.as_mut_ptr() as u64, &mut len as *mut u32 as u64);+ let name = if len <= 2 {+ String::new()+ } else if a[2] == 0 {+ format!("@{}", core::str::from_utf8(&a[3..len as usize]).unwrap_or("?"))+ } else {+ String::from(core::str::from_utf8(&a[2..len as usize - 1]).unwrap_or("?"))+ };+ (n, name)+}++#[repr(C)]+struct MsgHdr {+ name: u64,+ namelen: u32,+ iov: u64,+ iovlen: u64,+ control: u64,+ controllen: u64,+ flags: i32,+}++#[repr(C)]+struct IoVec {+ base: u64,+ len: u64,+}++/// sendmsg: `parts` gathered, plus fds (SCM_RIGHTS) if any.+fn sendmsg(fd: i64, parts: &[&[u8]], fds: &[i32], flags: u64) -> i64 {+ let iov: Vec<IoVec> = parts.iter().map(|p| IoVec { base: p.as_ptr() as u64, len: p.len() as u64 }).collect();+ let mut ctl = Vec::new();+ if !fds.is_empty() {+ ctl.extend_from_slice(&(16 + 4 * fds.len() as u64).to_ne_bytes());+ ctl.extend_from_slice(&1i32.to_ne_bytes());+ ctl.extend_from_slice(&1i32.to_ne_bytes());+ for f in fds {+ ctl.extend_from_slice(&f.to_ne_bytes());+ }+ while ctl.len() % 8 != 0 {+ ctl.push(0);+ }+ }+ let h = MsgHdr { name: 0, namelen: 0, iov: iov.as_ptr() as u64, iovlen: iov.len() as u64, control: ctl.as_ptr() as u64, controllen: ctl.len() as u64, flags: 0 };+ syscall3(SYS_SENDMSG, fd as u64, &h as *const MsgHdr as u64, flags)+}++struct Got {+ n: i64,+ data: Vec<u8>,+ fds: Vec<i32>,+ cred: Option<[u32; 3]>,+ flags: i32,+}++/// recvmsg into two buffers of `len/2`, with `ctl_room` bytes of control.+fn recvmsg(fd: i64, len: usize, ctl_room: usize, flags: u64) -> Got {+ let mut a = alloc::vec![0u8; len / 2];+ let mut b = alloc::vec![0u8; len - len / 2];+ let iov = [IoVec { base: a.as_mut_ptr() as u64, len: a.len() as u64 }, IoVec { base: b.as_mut_ptr() as u64, len: b.len() as u64 }];+ let mut ctl = alloc::vec![0u8; ctl_room];+ let mut h = MsgHdr { name: 0, namelen: 0, iov: iov.as_ptr() as u64, iovlen: 2, control: ctl.as_mut_ptr() as u64, controllen: ctl_room as u64, flags: 0 };+ let n = syscall3(SYS_RECVMSG, fd as u64, &mut h as *mut MsgHdr as u64, flags);+ let mut got = Got { n, data: Vec::new(), fds: Vec::new(), cred: None, flags: h.flags };+ if n < 0 {+ return got;+ }+ a.extend_from_slice(&b);+ got.data = a[..(n as usize).min(len)].to_vec();+ let ctl = &ctl[..h.controllen as usize];+ let mut off = 0;+ while off + 16 <= ctl.len() {+ let clen = u64::from_ne_bytes(ctl[off..off + 8].try_into().unwrap()) as usize;+ let ty = i32::from_ne_bytes(ctl[off + 12..off + 16].try_into().unwrap());+ let body = &ctl[off + 16..(off + clen).min(ctl.len())];+ match ty {+ 1 => got.fds.extend(body.chunks(4).map(|c| i32::from_ne_bytes(c.try_into().unwrap()))),+ 2 => {+ let w: Vec<u32> = body.chunks(4).map(|c| u32::from_ne_bytes(c.try_into().unwrap())).collect();+ got.cred = Some([w[0], w[1], w[2]]);+ }+ _ => {}+ }+ off += (clen + 7) & !7;+ }+ got+}++fn read_all(fd: i64) -> Vec<u8> {+ let mut out = Vec::new();+ let mut buf = [0u8; 256];+ loop {+ let n = read(fd, &mut buf);+ if n <= 0 {+ break;+ }+ out.extend_from_slice(&buf[..n as usize]);+ }+ out+}++fn link(path: &str) -> String {+ let mut buf = [0u8; 256];+ let n = readlink(path, &mut buf);+ if n < 0 {+ return format!("error {}", n);+ }+ String::from(core::str::from_utf8(&buf[..n as usize]).unwrap_or("?"))+}++fn is_socket(st: &Stat) -> bool {+ st.file_type() == S_IFSOCK+}++fn main() -> i64 {+ let group = arg(1).unwrap_or("");+ let mut c = Checks::new("unixtest");+ sigaction_raw(13, 1, 0, 0); // SIGPIPE: SIG_IGN (EPIPE checks)+ match group {+ "stream" => stream(&mut c),+ "pair" => pairs(&mut c),+ "dgram" => dgram(&mut c),+ "rights" => rights(&mut c),+ "nonblock" => nonblock(&mut c),+ "proc" => proc(&mut c),+ _ => c.check(false, "usage: unixtest stream|pair|dgram|rights|nonblock|proc"),+ }+ c.done()+}++// --- stream --------------------------------------------------------------------------++/// The classic: a server binds a path, listens, a forked child connects+/// and talks to it.+fn echo_on(c: &mut Checks, path: &str) {+ unlink(path);+ let srv = usock(SOCK_STREAM);+ c.check(srv >= 0, &format!("{}: socket(AF_UNIX, SOCK_STREAM)", path));+ c.check(ubind(srv, path) == 0, &format!("{}: bind", path));+ let mut s = Stat::default();+ c.check(lstat(path, &mut s) == 0 && is_socket(&s), &format!("{}: bind made a socket file (S_IFSOCK)", path));+ c.check(s.perm() == 0o755, &format!("{}: ...mode 0777 minus the umask (0o{:o})", path, s.perm()));+ let other = usock(SOCK_STREAM);+ c.check(ubind(other, path) == -EADDRINUSE, &format!("{}: binding the name again: EADDRINUSE", path));+ c.check(ubind(srv, "/tmp/second.sock") == -EINVAL, &format!("{}: binding a bound socket again: EINVAL", path));+ c.check(uconnect(other, path) == -ECONNREFUSED, &format!("{}: connect before listen: ECONNREFUSED", path));+ close(other);+ c.check(listen(srv, 4) == 0, &format!("{}: listen", path));+ c.check(getopt(srv, SO_ACCEPTCONN) == (0, 1), &format!("{}: SO_ACCEPTCONN 1", path));+ c.check(name_of(SYS_GETSOCKNAME, srv).as_deref() == Ok(path), &format!("{}: getsockname gives the path", path));+ c.check(open(path, O_RDONLY) == -ENXIO, &format!("{}: open() on a socket file: ENXIO", path));+ let child = fork();+ if child == 0 {+ // The client.+ let cl = usock(SOCK_STREAM);+ let mut bad = 0;+ if uconnect(cl, path) != 0 {+ bad += 1;+ }+ if write(cl, b"hello, ") != 7 || write(cl, b"server") != 6 {+ bad += 1;+ }+ let mut buf = [0u8; 64];+ let n = read(cl, &mut buf);+ if n != 13 || &buf[..13] != b"HELLO, SERVER" {+ bad += 1;+ }+ if read(cl, &mut buf) != 0 {+ bad += 1; // the server closed: end-of-file+ }+ exit(bad);+ }+ let conn = uaccept(srv);+ c.check(conn >= 0, &format!("{}: accept", path));+ let (r, cred) = peercred(conn);+ c.check(r == 0 && cred[0] as i64 == child, &format!("{}: SO_PEERCRED on the server side: the client's pid", path));+ c.check(name_of(SYS_GETPEERNAME, conn).as_deref() == Ok(""), &format!("{}: getpeername: the client is unnamed", path));+ c.check(name_of(SYS_GETSOCKNAME, conn).as_deref() == Ok(path), &format!("{}: the accepted socket's name is the server's", path));+ let mut buf = [0u8; 13];+ let n = recv(conn, &mut buf, MSG_WAITALL);+ c.check(n == 13 && &buf == b"hello, server", &format!("{}: two writes read back as one stream (MSG_WAITALL)", path));+ let upper: Vec<u8> = buf.iter().map(|b| b.to_ascii_uppercase()).collect();+ c.check(write(conn, &upper) == 13, &format!("{}: server answers", path));+ close(conn);+ c.check(wait(child) == 0, &format!("{}: the client saw the echo and EOF", path));+ close(srv);+ let late = usock(SOCK_STREAM);+ c.check(uconnect(late, path) == -ECONNREFUSED, &format!("{}: connect after the server closed: ECONNREFUSED", path));+ close(late);+ c.check(unlink(path) == 0 && lstat(path, &mut s) == -ENOENT, &format!("{}: unlink removes the socket file", path));+ let late = usock(SOCK_STREAM);+ c.check(uconnect(late, path) == -ENOENT, &format!("{}: connect to a missing path: ENOENT", path));+ close(late);+}++fn stream(c: &mut Checks) {+ echo_on(c, "/tmp/echo.sock");+ mkdir("/mnt/disk1/us");+ echo_on(c, "/mnt/disk1/us/echo.sock");+ rmdir("/mnt/disk1/us");+ // FAT can't store a socket file.+ let s = usock(SOCK_STREAM);+ c.check(ubind(s, "/mnt/disk0/x.sock") == -EPERM, "bind on FAT: EPERM");+ close(s);+ // A relative path, from the cwd.+ chdir("/tmp");+ unlink("/tmp/rel.sock");+ let srv = usock(SOCK_STREAM);+ c.check(ubind(srv, "rel.sock") == 0 && listen(srv, 1) == 0, "bind a relative path");+ let cl = usock(SOCK_STREAM);+ c.check(uconnect(cl, "/tmp/rel.sock") == 0, "...and connect to it by its full path");+ c.check(name_of(SYS_GETSOCKNAME, srv).as_deref() == Ok("rel.sock"), "getsockname: the path as bound");+ // Connect to a regular file: ECONNREFUSED.+ let fd = open("/tmp/plain.txt", O_WRONLY | O_CREAT);+ close(fd);+ let x = usock(SOCK_STREAM);+ c.check(uconnect(x, "/tmp/plain.txt") == -ECONNREFUSED, "connect to a plain file: ECONNREFUSED");+ close(x);+ unlink("/tmp/plain.txt");+ // Close the server with the connection never accepted: the client+ // gets ECONNRESET, then EOF.+ close(srv);+ let mut buf = [0u8; 8];+ c.check(read(cl, &mut buf) == -ECONNRESET, "server closed before accept: ECONNRESET");+ c.check(read(cl, &mut buf) == 0, "...then end-of-file");+ c.check(send(cl, b"x", MSG_NOSIGNAL) == -EPIPE, "...and writing: EPIPE");+ close(cl);+ unlink("/tmp/rel.sock");++ // Abstract names: no file anywhere.+ let a = usock(SOCK_STREAM);+ c.check(ubind(a, "@rustbelt-test") == 0 && listen(a, 2) == 0, "bind an abstract name");+ c.check(name_of(SYS_GETSOCKNAME, a).as_deref() == Ok("@rustbelt-test"), "getsockname: @rustbelt-test");+ let b = usock(SOCK_STREAM);+ c.check(ubind(b, "@rustbelt-test") == -EADDRINUSE, "the same abstract name again: EADDRINUSE");+ c.check(uconnect(b, "@rustbelt-test") == 0, "connect to the abstract name");+ let conn = uaccept(a);+ c.check(name_of(SYS_GETPEERNAME, b).as_deref() == Ok("@rustbelt-test"), "getpeername: @rustbelt-test");+ // ECONNRESET: close with data the peer sent still unread.+ write(b, b"unread");+ sched_yield();+ close(conn);+ c.check(read(b, &mut buf) == -ECONNRESET, "peer closed with our data unread: ECONNRESET");+ c.check(getopt(b, SO_ERROR) == (0, 0), "SO_ERROR cleared after that");+ close(b);+ close(a);+ let again = usock(SOCK_STREAM);+ c.check(ubind(again, "@rustbelt-test") == 0, "the abstract name is free once its socket closed");+ close(again);+ // Autobind: bind with just the family.+ let au = usock(SOCK_STREAM);+ let fam = [AF_UNIX as u8, 0];+ c.check(syscall3(SYS_BIND, au as u64, fam.as_ptr() as u64, 2) == 0, "bind with only the family (autobind)");+ let n = name_of(SYS_GETSOCKNAME, au).unwrap_or_default();+ c.check(n.len() == 6 && n.starts_with('@') && n[1..].bytes().all(|b| b.is_ascii_hexdigit()), &format!("...gets an abstract name of 5 hex digits ({})", n));+ let au2 = usock(SOCK_STREAM);+ syscall3(SYS_BIND, au2 as u64, fam.as_ptr() as u64, 2);+ let n2 = name_of(SYS_GETSOCKNAME, au2).unwrap_or_default();+ c.check(n2.len() == 6 && n2 != n, &format!("...and a second socket gets another one ({})", n2));+ close(au2);+ close(au);+ // Odd calls.+ let s = usock(SOCK_STREAM);+ c.check(read(s, &mut buf) == -EINVAL, "read on an unconnected stream socket: EINVAL");+ c.check(send(s, b"x", MSG_NOSIGNAL) == -ENOTCONN, "send on it: ENOTCONN");+ c.check(name_of(SYS_GETPEERNAME, s) == Err(-ENOTCONN), "getpeername: ENOTCONN");+ c.check(name_of(SYS_GETSOCKNAME, s).as_deref() == Ok(""), "getsockname of an unbound socket: just the family");+ c.check(getopt(s, SO_DOMAIN) == (0, 1) && getopt(s, SO_TYPE) == (0, 1), "SO_DOMAIN AF_UNIX, SO_TYPE SOCK_STREAM");+ let (r, cred) = peercred(s);+ c.check(r == 0 && cred == [0, u32::MAX, u32::MAX], "SO_PEERCRED with no peer: pid 0, ids -1");+ c.check(listen(s, 1) == -EINVAL, "listen while unbound: EINVAL");+ close(s);+ c.check(syscall3(SYS_SOCKET, AF_UNIX, 9, 0) == -94, "socket type 9: ESOCKTNOSUPPORT");+ c.check(syscall3(SYS_SOCKET, AF_UNIX, SOCK_STREAM, 7) == -93, "protocol 7: EPROTONOSUPPORT");+ let d = usock(SOCK_DGRAM);+ c.check(listen(d, 1) == -EOPNOTSUPP, "listen on a datagram socket: EOPNOTSUPP");+ close(d);+ let fd = open("/dev/null", O_RDONLY);+ c.check(syscall3(SYS_SHUTDOWN, fd as u64, 1, 0) == -ENOTSOCK, "shutdown on a non-socket: ENOTSOCK");+ close(fd);+}++// --- socketpair ------------------------------------------------------------------------++fn pairs(c: &mut Checks) {+ let (a, b) = pair(SOCK_STREAM);+ c.check(a >= 0 && b >= 0, "socketpair(AF_UNIX, SOCK_STREAM)");+ c.check(write(a, b"ab") == 2 && write(b, b"cd") == 2, "both ends write");+ let mut buf = [0u8; 8];+ c.check(read(b, &mut buf) == 2 && &buf[..2] == b"ab", "b reads a's bytes");+ c.check(read(a, &mut buf) == 2 && &buf[..2] == b"cd", "a reads b's bytes");+ let (r, cred) = peercred(a);+ c.check(r == 0 && cred[0] as i64 == getpid(), "SO_PEERCRED: our own pid");+ c.check(name_of(SYS_GETPEERNAME, a).as_deref() == Ok(""), "getpeername: unnamed (but no error)");+ // Across fork: the child talks through its copy.+ let child = fork();+ if child == 0 {+ close(a);+ let mut buf = [0u8; 16];+ let n = read(b, &mut buf);+ let ok = n == 4 && &buf[..4] == b"ping" && write(b, b"pong") == 4;+ exit(if ok { 0 } else { 1 });+ }+ write(a, b"ping");+ c.check(read(a, &mut buf) == 4 && &buf[..4] == b"pong", "a forked child answers through the pair");+ c.check(wait(child) == 0, "child happy");+ // Shutdown one way.+ c.check(syscall3(SYS_SHUTDOWN, a as u64, 1, 0) == 0, "shutdown(SHUT_WR)");+ c.check(read(b, &mut buf) == 0, "the other end reads EOF");+ c.check(send(a, b"x", MSG_NOSIGNAL) == -EPIPE, "writing after SHUT_WR: EPIPE");+ c.check(write(b, b"still") == 5 && read(a, &mut buf) == 5, "the other direction still works");+ c.check(syscall3(SYS_SHUTDOWN, a as u64, 7, 0) == -EINVAL, "shutdown(7): EINVAL");+ close(b);+ c.check(read(a, &mut buf) == 0, "peer closed: EOF");+ c.check(send(a, b"x", MSG_NOSIGNAL) == -EPIPE, "peer closed: EPIPE");+ close(a);+ // MSG_PEEK+ let (a, b) = pair(SOCK_STREAM);+ write(a, b"peekaboo");+ c.check(recv(b, &mut buf[..4], MSG_PEEK) == 4 && &buf[..4] == b"peek", "MSG_PEEK looks");+ c.check(read(b, &mut buf) == 8 && &buf == b"peekaboo", "...without taking");+ close(a);+ close(b);+ // SEQPACKET: connected, but message borders stay.+ let (a, b) = pair(SOCK_SEQPACKET);+ c.check(a >= 0, "socketpair(SOCK_SEQPACKET)");+ write(a, b"one");+ write(a, b"three");+ c.check(read(b, &mut buf) == 3 && &buf[..3] == b"one", "seqpacket: first message alone");+ c.check(read(b, &mut buf[..2]) == 2 && &buf[..2] == b"th", "seqpacket: a short read...");+ c.check(recv(b, &mut buf, MSG_DONTWAIT) == -EAGAIN, "...throws the rest away");+ c.check(getopt(a, SO_TYPE) == (0, 5), "SO_TYPE: SOCK_SEQPACKET");+ close(a);+ c.check(read(b, &mut buf) == 0, "seqpacket: peer closed, EOF");+ close(b);+ // DGRAM pair.+ let (a, b) = pair(SOCK_DGRAM);+ send(a, b"x", 0);+ send(a, b"yz", 0);+ c.check(read(b, &mut buf) == 1 && read(b, &mut buf) == 2, "dgram pair: two messages, two reads");+ c.check(send(a, b"", 0) == 0 && read(b, &mut buf) == 0, "an empty datagram is a message too");+ close(a);+ close(b);+ // AF_INET can't do pairs; other families don't exist.+ let mut sv = [0i32; 2];+ c.check(syscall5(SYS_SOCKETPAIR, 2, 1, 0, sv.as_mut_ptr() as u64, 0) == -EOPNOTSUPP, "socketpair(AF_INET): EOPNOTSUPP");+ c.check(syscall5(SYS_SOCKETPAIR, 1, 1, 0, 0, 0) == -EFAULT, "socketpair to a bad pointer: EFAULT");+ // CLOEXEC / NONBLOCK flags.+ let (a, b) = pair(SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK);+ c.check(fcntl(a, F_GETFD, 0) == FD_CLOEXEC as i64 && fcntl(b, F_GETFD, 0) == FD_CLOEXEC as i64, "SOCK_CLOEXEC sets FD_CLOEXEC");+ c.check(read(a, &mut buf) == -EAGAIN, "SOCK_NONBLOCK: EAGAIN");+ close(a);+ close(b);+}++// --- datagrams --------------------------------------------------------------------------++fn dgram(c: &mut Checks) {+ unlink("/tmp/d1.sock");+ let a = usock(SOCK_DGRAM);+ let b = usock(SOCK_DGRAM);+ c.check(ubind(a, "/tmp/d1.sock") == 0 && ubind(b, "@dgram-b") == 0, "bind two datagram sockets");+ c.check(send_to(b, b"to a", "/tmp/d1.sock") == 4, "sendto a path");+ c.check(send_to(a, b"to b!", "@dgram-b") == 5, "sendto an abstract name");+ let mut buf = [0u8; 16];+ let (n, from) = recv_from(a, &mut buf, 0);+ c.check(n == 4 && from == "@dgram-b", &format!("recvfrom says who sent it ({})", from));+ let (n, from) = recv_from(b, &mut buf, 0);+ c.check(n == 5 && from == "/tmp/d1.sock", &format!("...a path, too ({})", from));+ // Truncation.+ send_to(b, b"0123456789", "/tmp/d1.sock");+ c.check(recv(a, &mut buf[..4], MSG_TRUNC) == 10, "MSG_TRUNC: the real length");+ send_to(b, b"0123456789", "/tmp/d1.sock");+ let g = recvmsg(a, 4, 0, 0);+ c.check(g.n == 4 && g.flags & 0x20 != 0, "recvmsg: MSG_TRUNC in msg_flags");+ // From an unnamed socket.+ let u = usock(SOCK_DGRAM);+ send_to(u, b"anon", "@dgram-b");+ let (n, from) = recv_from(b, &mut buf, 0);+ c.check(n == 4 && from.is_empty(), "from an unbound socket: no name");+ c.check(send(u, b"x", 0) == -ENOTCONN, "send with no address, not connected: ENOTCONN");+ c.check(send_to(u, b"x", "@nobody-here") == -ECONNREFUSED, "sendto a name nobody has: ECONNREFUSED");+ // connect sets the default destination; then only that peer may send to us.+ c.check(uconnect(u, "@dgram-b") == 0 && send(u, b"conn", 0) == 4, "connect + send");+ c.check(recv(b, &mut buf, 0) == 4, "...arrives");+ c.check(uconnect(b, "/tmp/d1.sock") == 0, "b connects to a");+ c.check(send_to(u, b"x", "@dgram-b") == -EPERM, "b is connected to a: others get EPERM");+ c.check(name_of(SYS_GETPEERNAME, b).as_deref() == Ok("/tmp/d1.sock"), "getpeername on a connected datagram socket");+ // Disconnect with AF_UNSPEC.+ let unspec = [0u8; 2];+ c.check(syscall3(SYS_CONNECT, b as u64, unspec.as_ptr() as u64, 2) == 0 && send_to(u, b"ok", "@dgram-b") == 2, "connect(AF_UNSPEC) undoes it");+ recv(b, &mut buf, 0);+ // Wrong kinds.+ let s = usock(SOCK_STREAM);+ c.check(uconnect(s, "@dgram-b") == -EPROTOTYPE, "a stream socket connecting to a datagram one: EPROTOTYPE");+ close(s);+ c.check(send_to(b, &alloc::vec![0u8; 300_000], "/tmp/d1.sock") == -EMSGSIZE, "a datagram bigger than the buffer: EMSGSIZE");+ // The peer goes away.+ close(a);+ c.check(send_to(b, b"x", "/tmp/d1.sock") == -ECONNREFUSED, "sendto a closed socket's name: ECONNREFUSED");+ c.check(send(u, b"x", 0) >= 0, "(u is still connected to b)");+ close(b);+ c.check(send(u, b"x", 0) == -ECONNREFUSED, "connected peer closed: ECONNREFUSED");+ close(u);+ unlink("/tmp/d1.sock");+}++// --- SCM_RIGHTS ---------------------------------------------------------------------------++fn rights(c: &mut Checks) {+ let fd = open("/tmp/rights.txt", O_RDWR | O_CREAT | O_TRUNC);+ write(fd, b"file data");+ lseek(fd, 0, SEEK_SET);+ let (pr, pw) = pipe().unwrap();+ let (a, b) = pair(SOCK_STREAM);+ c.check(sendmsg(a, &[b"fd", b"s!"], &[fd as i32, pr as i32], 0) == 4, "sendmsg with two fds (a file and a pipe end)");+ close(fd);+ close(pr);+ let g = recvmsg(b, 16, 64, 0);+ c.check(g.n == 4 && g.data == b"fds!", "recvmsg: the data, gathered and scattered");+ c.check(g.fds.len() == 2 && g.flags & MSG_CTRUNC == 0, &format!("recvmsg: two new fds ({:?})", g.fds));+ if g.fds.len() == 2 {+ let (f, p) = (g.fds[0] as i64, g.fds[1] as i64);+ c.check(read_all(f) == b"file data", "read the file through the received fd");+ write(pw, b"pipe data");+ close(pw);+ c.check(read_all(p) == b"pipe data", "read the pipe through the received end");+ c.check(fcntl(f, F_GETFD, 0) == 0, "no FD_CLOEXEC without MSG_CMSG_CLOEXEC");+ close(f);+ close(p);+ } else {+ close(pw);+ }+ // More fds than room: MSG_CTRUNC, the rest closed.+ let (pr, pw) = pipe().unwrap();+ let three = [pr as i32, pr as i32, pr as i32];+ c.check(sendmsg(a, &[b"3"], &three, 0) == 1, "send the same fd three times");+ close(pr);+ let g = recvmsg(b, 8, 16 + 4, MSG_CMSG_CLOEXEC); // CMSG_LEN(4)+ c.check(g.fds.len() == 1 && g.flags & MSG_CTRUNC != 0, &format!("room for one: one fd and MSG_CTRUNC ({:?})", g.fds));+ if let Some(&f) = g.fds.first() {+ c.check(fcntl(f as i64, F_GETFD, 0) == FD_CLOEXEC as i64, "MSG_CMSG_CLOEXEC sets FD_CLOEXEC");+ close(f as i64);+ }+ c.check(write(pw, b"x") == -EPIPE, "the copies that didn't fit were closed (the pipe has no reader)");+ close(pw);+ // In-flight fds die with the socket: send a pipe's read end, never+ // receive it, close the pair. The pipe's writer sees no reader left.+ let (pr, pw) = pipe().unwrap();+ sendmsg(a, &[b"x"], &[pr as i32], 0);+ close(pr);+ c.check(write(pw, b"y") == 1, "the read end is alive while in flight");+ close(a);+ close(b);+ c.check(write(pw, b"y") == -EPIPE, "closing the sockets closed the fd in flight");+ close(pw);+ // Data before an fd message merges with it, and the read stops right+ // after it (stage 19, like Linux): "aa", then "bb"+fd, then "cc".+ let (a, b) = pair(SOCK_STREAM);+ let (pr, pw) = pipe().unwrap();+ write(a, b"aa");+ sendmsg(a, &[b"bb"], &[pr as i32], 0);+ write(a, b"cc");+ let g = recvmsg(b, 16, 64, 0);+ c.check(g.n == 4 && g.data == b"aabb" && g.fds.len() == 1, "stream: a read takes the bytes before an fd message and the message, with its fd");+ for f in g.fds {+ close(f as i64);+ }+ let g = recvmsg(b, 16, 64, 0);+ c.check(g.n == 2 && g.data == b"cc" && g.fds.is_empty(), "...and stops after it");+ // Two fd messages never merge: one batch of fds per read.+ sendmsg(a, &[b"d"], &[pr as i32], 0);+ sendmsg(a, &[b"e"], &[pw as i32], 0);+ let g1 = recvmsg(b, 16, 64, 0);+ let g2 = recvmsg(b, 16, 64, 0);+ c.check(g1.n == 1 && g1.fds.len() == 1 && g2.n == 1 && g2.fds.len() == 1, "stream: two fd messages take two reads");+ for f in g1.fds.into_iter().chain(g2.fds) {+ close(f as i64);+ }+ // A plain read() merges too, and closes the fds it can't hand over.+ write(a, b"AB");+ sendmsg(a, &[b"CD"], &[pr as i32], 0);+ close(pr);+ let mut buf4 = [0u8; 8];+ c.check(read(b, &mut buf4) == 4 && &buf4[..4] == b"ABCD", "stream: read() returns 'ABCD' across an fd message");+ c.check(write(pw, b"y") == -EPIPE, "...and the fd it dropped was closed");+ close(pw);+ // read() can't take fds: they're closed.+ let (pr, pw) = pipe().unwrap();+ sendmsg(a, &[b"z"], &[pr as i32], 0);+ close(pr);+ let mut buf = [0u8; 4];+ c.check(read(b, &mut buf) == 1, "plain read of a message with an fd");+ c.check(write(pw, b"x") == -EPIPE, "...closed the fd");+ close(pw);+ // Bad fds, bad control messages.+ c.check(sendmsg(a, &[b"z"], &[999], 0) == -EBADF, "sending fd 999: EBADF");+ // SCM_CREDENTIALS with SO_PASSCRED.+ c.check(setopt(b, SO_PASSCRED, 1) == 0 && getopt(b, SO_PASSCRED) == (0, 1), "SO_PASSCRED on");+ write(a, b"who");+ let g = recvmsg(b, 8, 64, 0);+ c.check(g.cred.is_some_and(|cr| cr[0] as i64 == getpid() && cr[1] == 0), &format!("SCM_CREDENTIALS: our pid, uid 0 ({:?})", g.cred));+ close(a);+ close(b);+ // A datagram socket can carry fds too.+ let (a, b) = pair(SOCK_DGRAM);+ let (pr, pw) = pipe().unwrap();+ sendmsg(a, &[b"d"], &[pw as i32], 0);+ close(pw);+ let g = recvmsg(b, 8, 64, 0);+ c.check(g.fds.len() == 1 && write(g.fds[0] as i64, b"via dgram") == 9, "a pipe's write end through a datagram");+ for f in g.fds {+ close(f as i64);+ }+ c.check(read_all(pr) == b"via dgram", "...and the data arrives");+ close(pr);+ close(a);+ close(b);+ unlink("/tmp/rights.txt");+}++// --- nonblocking, buffers, poll ---------------------------------------------------------++fn nonblock(c: &mut Checks) {+ let (a, b) = pair(SOCK_STREAM | SOCK_NONBLOCK);+ let mut buf = [0u8; 4096];+ c.check(read(a, &mut buf) == -EAGAIN, "nothing to read: EAGAIN");+ let mut fds = [PollFd { fd: a as i32, events: POLLIN | POLLOUT, revents: 0 }];+ c.check(poll(&mut fds, 0) == 1 && fds[0].revents == POLLOUT, "poll: writable, not readable");+ // Small buffers, so they fill fast. setsockopt doubles (like Linux).+ c.check(setopt(a, SO_SNDBUF, 4096) == 0 && getopt(a, SO_SNDBUF) == (0, 8192), "SO_SNDBUF 4096 reads back 8192");+ c.check(getopt(b, SO_RCVBUF).1 == 212_992, "SO_RCVBUF default 212992");+ let mut total = 0;+ loop {+ let n = write(a, &buf);+ if n < 0 {+ c.check(n == -EAGAIN, "full: EAGAIN");+ break;+ }+ total += n;+ if total > 1 << 20 {+ break;+ }+ }+ c.check(total == 8192, &format!("the buffer limit: {} bytes went in", total));+ fds[0].events = POLLOUT;+ c.check(poll(&mut fds, 0) == 0, "poll: not writable when full");+ c.check(read(b, &mut buf[..1000]) == 1000, "read some");+ c.check(poll(&mut fds, 0) == 1, "writable again");+ while read(b, &mut buf) > 0 {}+ // Blocking backpressure: a child writes 100 KB through a small+ // buffer; it has to wait for us to read.+ let (x, y) = pair(SOCK_STREAM);+ setopt(x, SO_SNDBUF, 2048);+ let child = fork();+ if child == 0 {+ let data = alloc::vec![7u8; 100_000];+ let n = write(x, &data);+ exit(if n == 100_000 { 0 } else { 1 });+ }+ close(x);+ sleep_ms(100);+ let mut got = 0;+ loop {+ let n = read(y, &mut buf);+ if n <= 0 {+ break;+ }+ got += n;+ }+ c.check(got == 100_000, &format!("a blocked writer got everything through ({} bytes)", got));+ c.check(wait(child) == 0, "...in one write() call");+ close(y);+ // epoll: EPOLLRDHUP when the peer shuts down writing, EPOLLHUP when it's gone.+ let ep = epoll_create1(0);+ epoll_ctl(ep, EPOLL_CTL_ADD, a, EPOLLIN | EPOLLRDHUP, 1);+ let mut ev = [EpollEvent::default(); 2];+ c.check(epoll_wait(ep, &mut ev, 0) == 0, "epoll: nothing yet");+ syscall3(SYS_SHUTDOWN, b as u64, 1, 0);+ let n = epoll_wait(ep, &mut ev, 100);+ let e = ev[0].events;+ c.check(n == 1 && e & EPOLLRDHUP != 0 && e & EPOLLIN != 0 && e & EPOLLHUP == 0, &format!("peer SHUT_WR: EPOLLIN|EPOLLRDHUP (0x{:x})", e));+ close(b);+ let n = epoll_wait(ep, &mut ev, 100);+ let e = ev[0].events;+ c.check(n == 1 && e & EPOLLHUP != 0, &format!("peer closed: EPOLLHUP (0x{:x})", e));+ close(ep);+ close(a);+ // A nonblocking listener; poll says when a connection waits.+ let l = usock(SOCK_STREAM | SOCK_NONBLOCK);+ ubind(l, "@nb-listen");+ listen(l, 1);+ c.check(uaccept(l) == -EAGAIN, "accept with nobody waiting: EAGAIN");+ let mut pf = [PollFd { fd: l as i32, events: POLLIN, revents: 0 }];+ c.check(poll(&mut pf, 0) == 0, "poll: listener not readable");+ let cl = usock(SOCK_STREAM);+ c.check(uconnect(cl, "@nb-listen") == 0, "connect (goes into the backlog)");+ c.check(poll(&mut pf, 0) == 1 && pf[0].revents == POLLIN, "poll: listener readable");+ let conn = syscall5(SYS_ACCEPT4, l as u64, 0, 0, SOCK_NONBLOCK | SOCK_CLOEXEC, 0);+ c.check(conn >= 0 && fcntl(conn, F_GETFD, 0) == FD_CLOEXEC as i64, "accept4(SOCK_CLOEXEC|SOCK_NONBLOCK)");+ c.check(read(conn, &mut buf) == -EAGAIN, "...the new socket is nonblocking");+ c.check(syscall5(SYS_ACCEPT4, l as u64, 0, 0, 1, 0) == -EINVAL, "accept4 with a bad flag: EINVAL");+ // A full backlog: a nonblocking connect says EAGAIN.+ let mut extra = Vec::new();+ let mut r = 0;+ for _ in 0..5 {+ let s = usock(SOCK_STREAM | SOCK_NONBLOCK);+ r = uconnect(s, "@nb-listen");+ extra.push(s);+ if r < 0 {+ break;+ }+ }+ c.check(r == -EAGAIN, "backlog full: nonblocking connect gets EAGAIN");+ for s in extra {+ close(s);+ }+ close(conn);+ close(cl);+ // Unconnected stream socket: POLLOUT|POLLHUP (like Linux).+ let s = usock(SOCK_STREAM);+ let mut pf = [PollFd { fd: s as i32, events: POLLIN | POLLOUT, revents: 0 }];+ poll(&mut pf, 0);+ c.check(pf[0].revents == POLLOUT | POLLHUP, "an unconnected stream socket polls OUT|HUP");+ close(s);+ close(l);+ // recv(MSG_DONTWAIT) on a blocking socket.+ let (a, b) = pair(SOCK_STREAM);+ c.check(recv(a, &mut buf, MSG_DONTWAIT) == -EAGAIN, "MSG_DONTWAIT: EAGAIN on a blocking socket");+ // SO_RCVTIMEO+ set_recv_timeout(a, 100);+ let t = now_us();+ c.check(read(a, &mut buf) == -EAGAIN && now_us() - t >= 90_000, "SO_RCVTIMEO: EAGAIN after the timeout");+ c.check(send_to(a, b"x", "@somewhere") == -EISCONN, "sendto with an address on a connected stream: EISCONN");+ let mut v = 0u32;+ c.check(ioctl(b, 0x541B, &mut v as *mut u32 as u64) == 0 && v == 0, "FIONREAD 0");+ write(a, b"12345");+ c.check(ioctl(b, 0x541B, &mut v as *mut u32 as u64) == 0 && v == 5, "FIONREAD 5");+ close(a);+ close(b);+}++// --- /proc/self -----------------------------------------------------------------------++fn proc(c: &mut Checks) {+ let f = open("/tmp/proc.txt", O_RDWR | O_CREAT | O_TRUNC);+ write(f, b"proc data");+ let (pr, pw) = pipe().unwrap();+ let (a, b) = pair(SOCK_STREAM);+ // The listing.+ let dir = open("/proc/self/fd", O_RDONLY | O_DIRECTORY);+ c.check(dir >= 0, "open /proc/self/fd");+ let mut names = Vec::new();+ let mut buf = [0u8; 2048];+ loop {+ let n = getdents64(dir, &mut buf);+ if n <= 0 {+ break;+ }+ for_each_dirent(&buf, n as usize, |name, _| names.push(String::from(name)));+ }+ close(dir);+ let want = [0, 1, 2, f, pr, pw, a, b, dir];+ c.check(want.iter().all(|fd| names.iter().any(|n| *n == format!("{}", fd))), &format!("the listing has every fd ({:?})", names));+ c.check(!names.iter().any(|n| *n == "99"), "...and no others");+ // readlink+ c.check(link(&format!("/proc/self/fd/{}", f)) == "/tmp/proc.txt", "readlink: a file's path");+ let pl = link(&format!("/proc/self/fd/{}", pr));+ let mut st = Stat::default();+ fstat(pr, &mut st);+ c.check(pl == format!("pipe:[{}]", st.ino) && link(&format!("/proc/self/fd/{}", pw)) == pl, &format!("readlink: {} for both pipe ends (= st_ino)", pl));+ let sl = link(&format!("/proc/self/fd/{}", a));+ fstat(a, &mut st);+ c.check(sl == format!("socket:[{}]", st.ino) && is_socket(&st), &format!("readlink: {} (= st_ino, S_IFSOCK)", sl));+ c.check(link(&format!("/proc/self/fd/{}", b)) != sl, "the other end is another socket");+ let mut ls = Stat::default();+ c.check(lstat(&format!("/proc/self/fd/{}", f), &mut ls) == 0 && ls.is_symlink(), "lstat: a symlink");+ c.check(stat(&format!("/proc/self/fd/{}", a), &mut ls) == 0 && is_socket(&ls) && ls.ino == st.ino, "stat follows it, even to a socket");+ c.check(link("/proc/self/fd/99") == "error -2", "readlink of a closed fd: ENOENT");+ let exe = link("/proc/self/exe");+ c.check(exe.ends_with("/unixtest"), &format!("/proc/self/exe: {}", exe));+ // Opening an entry.+ let again = open(&format!("/proc/self/fd/{}", f), O_RDONLY);+ c.check(read_all(again) == b"proc data", "open /proc/self/fd/N of a file: a fresh offset");+ close(again);+ let rd = open(&format!("/proc/self/fd/{}", pr), O_RDONLY);+ write(pw, b"via proc");+ let mut b8 = [0u8; 8];+ c.check(rd >= 0 && read(rd, &mut b8) == 8 && &b8 == b"via proc", "open /proc/self/fd/N of a pipe: the same pipe");+ close(rd);+ c.check(open(&format!("/proc/self/fd/{}", a), O_RDWR) == -ENXIO, "open /proc/self/fd/N of a socket: ENXIO");+ c.check(open("/proc/self/fd/99", O_RDONLY) == -ENOENT, "open of a closed fd: ENOENT");+ let null = open("/dev/null", O_RDONLY);+ c.check(link(&format!("/proc/self/fd/{}", null)) == "/dev/null", "readlink: /dev/null");+ close(null);+ let ep = epoll_create1(0);+ c.check(link(&format!("/proc/self/fd/{}", ep)) == "anon_inode:[eventpoll]", "readlink: anon_inode:[eventpoll]");+ close(ep);+ let mut root = Vec::new();+ let d = open("/proc", O_RDONLY | O_DIRECTORY);+ let n = getdents64(d, &mut buf);+ if n > 0 {+ for_each_dirent(&buf, n as usize, |name, _| root.push(String::from(name)));+ }+ close(d);+ c.check(root.iter().any(|n| n == "self"), "/proc lists self");+ c.check(mkdir("/proc/self/x") < 0, "nothing can be made in /proc");+ // A child sees its own fds.+ let child = fork();+ if child == 0 {+ close(f);+ exit(if link(&format!("/proc/self/fd/{}", f)) == "error -2" { 0 } else { 1 });+ }+ c.check(wait(child) == 0 && link(&format!("/proc/self/fd/{}", f)) == "/tmp/proc.txt", "self is whoever asks");+ for fd in [f, pr, pw, a, b] {+ close(fd);+ }+ unlink("/tmp/proc.txt");+}
@@ -0,0 +1,251 @@+/*+ * vdsotest.c -- stage 19: the vDSO, checked from a real C library (musl).+ *+ * Built with `musl-gcc -static` as a classic ET_EXEC program (linked at+ * Rustbelt's user base, see build.rs). musl finds the vDSO through+ * AT_SYSINFO_EHDR and then calls __vdso_clock_gettime instead of making a+ * system call. We compare what the vDSO says with what the real system+ * call says, check the clocks never go backwards, and that it all still+ * works in a forked child and after execve.+ *+ * vdsotest all checks (exit code = number of failed checks)+ * vdsotest calls just call the clock functions a lot, print nothing+ * (tests/vdso.rs looks at the --trace output)+ * vdsotest exec (used internally: the program after execve)+ */+#define _GNU_SOURCE+#include <elf.h>+#include <errno.h>+#include <sched.h>+#include <signal.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/auxv.h>+#include <sys/mman.h>+#include <sys/syscall.h>+#include <sys/time.h>+#include <sys/wait.h>+#include <time.h>+#include <unistd.h>++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("vdsotest: FAIL line %d: ", __LINE__); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++extern char _start[]; /* musl's entry point (crt1.o) */++static long long ns(struct timespec t) { return t.tv_sec * 1000000000LL + t.tv_nsec; }+static long long us(struct timeval t) { return t.tv_sec * 1000000LL + t.tv_usec; }++/* The real system call, bypassing the C library (and so the vDSO). */+static long long sys_ns(int clock)+{+ struct timespec t;+ if (syscall(SYS_clock_gettime, clock, &t) != 0)+ return -1;+ return ns(t);+}++static long long vdso_ns(int clock)+{+ struct timespec t;+ if (clock_gettime(clock, &t) != 0)+ return -1;+ return ns(t);+}++/* The image behind AT_SYSINFO_EHDR is an ELF shared object, and the page+ * in front of it (the vvar page) is readable but can't be made writable. */+static void image(void)+{+ unsigned long base = getauxval(AT_SYSINFO_EHDR);+ CHECK(base != 0, "no AT_SYSINFO_EHDR");+ if (!base)+ return;+ const Elf64_Ehdr *eh = (const Elf64_Ehdr *)base;+ CHECK(memcmp(eh->e_ident, ELFMAG, SELFMAG) == 0, "vDSO has no ELF magic");+ CHECK(eh->e_type == ET_DYN, "vDSO e_type %d", eh->e_type);+ void *vvar = (void *)(base - 4096);+ unsigned seq = *(volatile unsigned *)vvar;+ CHECK(seq % 2 == 0, "vvar seq %u is odd", seq);+ errno = 0;+ CHECK(mprotect(vvar, 4096, PROT_READ | PROT_WRITE) == -1 && errno == EACCES, "mprotect(vvar, RW): errno %d", errno);+ /* Other auxv entries a C library looks at. */+ CHECK(getauxval(AT_PAGESZ) == 4096, "AT_PAGESZ");+ CHECK(getauxval(AT_CLKTCK) == 100, "AT_CLKTCK");+ CHECK(getauxval(AT_HWCAP) & (1 << 4), "AT_HWCAP has no TSC bit");+ CHECK(getauxval(AT_RANDOM) != 0, "AT_RANDOM");+ CHECK(getauxval(AT_SECURE) == 0, "AT_SECURE");+ CHECK(getauxval(AT_ENTRY) == (unsigned long)_start, "AT_ENTRY %lx", getauxval(AT_ENTRY));+ CHECK(getauxval(AT_PHNUM) > 0 && getauxval(AT_PHENT) == sizeof(Elf64_Phdr), "AT_PHNUM/AT_PHENT");+ CHECK(getauxval(AT_BASE) == 0, "AT_BASE (no dynamic loader)");+ const char *platform = (const char *)getauxval(AT_PLATFORM);+ CHECK(platform && strcmp(platform, "x86_64") == 0, "AT_PLATFORM");+ const char *execfn = (const char *)getauxval(AT_EXECFN);+ CHECK(execfn && strstr(execfn, "vdsotest"), "AT_EXECFN");+}++/* Each fast clock: vDSO, then syscall, then vDSO again must be in order+ * (they compute the same formula from the same numbers); and the clocks+ * never go backwards. */+static void clocks(void)+{+ static const int fast[] = { CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW, CLOCK_REALTIME_COARSE,+ CLOCK_MONOTONIC_COARSE };+ for (unsigned i = 0; i < sizeof fast / sizeof fast[0]; i++) {+ int c = fast[i];+ for (int round = 0; round < 100; round++) {+ long long a = vdso_ns(c), b = sys_ns(c), d = vdso_ns(c);+ if (!(a > 0 && a <= b && b <= d)) {+ CHECK(0, "clock %d out of order: vdso %lld, syscall %lld, vdso %lld", c, a, b, d);+ break;+ }+ }+ }+ long long prev = vdso_ns(CLOCK_MONOTONIC);+ for (int i = 0; i < 100000; i++) {+ long long now = vdso_ns(CLOCK_MONOTONIC);+ if (now < prev) {+ CHECK(0, "CLOCK_MONOTONIC went back from %lld to %lld", prev, now);+ break;+ }+ prev = now;+ }+ /* Nanoseconds really: two readings in a row usually differ by less+ * than a microsecond, but they must not be stuck on whole µs. */+ int sub_us = 0;+ for (int i = 0; i < 100 && !sub_us; i++)+ sub_us = vdso_ns(CLOCK_MONOTONIC) % 1000 != 0;+ CHECK(sub_us, "CLOCK_MONOTONIC only has microseconds");+ /* The wall clock is about now (after 2020), and REALTIME = MONOTONIC + boot. */+ CHECK(vdso_ns(CLOCK_REALTIME) / 1000000000 > 1577836800LL, "CLOCK_REALTIME is before 2020");+ /* Other clocks take the syscall path inside the vDSO, and still work. */+ struct timespec t;+ CHECK(clock_gettime(CLOCK_BOOTTIME, &t) == 0 && ns(t) > 0, "CLOCK_BOOTTIME");+ CHECK(clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t) == 0, "CLOCK_PROCESS_CPUTIME_ID");+ CHECK(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t) == 0, "CLOCK_THREAD_CPUTIME_ID");+ errno = 0;+ CHECK(clock_gettime(12345, &t) == -1 && errno == EINVAL, "bad clock: errno %d", errno);++ /* gettimeofday and time: also from the vDSO, also agreeing. */+ struct timeval tv1, tv2, tvs;+ struct timezone tz = { 99, 99 };+ gettimeofday(&tv1, NULL);+ CHECK(syscall(SYS_gettimeofday, &tvs, &tz) == 0, "gettimeofday syscall");+ gettimeofday(&tv2, NULL);+ CHECK(us(tv1) <= us(tvs) && us(tvs) <= us(tv2), "gettimeofday out of order: %lld %lld %lld", us(tv1), us(tvs),+ us(tv2));+ CHECK(tz.tz_minuteswest == 0 && tz.tz_dsttime == 0, "timezone not UTC");+ CHECK(tv1.tv_usec >= 0 && tv1.tv_usec < 1000000, "tv_usec %ld", (long)tv1.tv_usec);+ time_t t1 = time(NULL), ts = syscall(SYS_time, NULL), t2;+ time(&t2);+ CHECK(t1 <= ts && ts <= t2 && t2 - t1 < 2, "time out of order: %ld %ld %ld", (long)t1, (long)ts, (long)t2);+ CHECK(t1 == tv1.tv_sec || t1 == tv1.tv_sec + 1, "time %ld vs gettimeofday %ld", (long)t1, (long)tv1.tv_sec);++ /* sched_getcpu (getcpu in the vDSO): one of our 4 CPUs. */+ int cpu = sched_getcpu();+ CHECK(cpu >= 0 && cpu < 64, "sched_getcpu %d", cpu);+ unsigned scpu = 999, node = 999;+ CHECK(syscall(SYS_getcpu, &scpu, &node, NULL) == 0 && scpu < 64 && node == 0, "getcpu syscall %u %u", scpu, node);+ /* Pin ourselves? No sched_setaffinity here: just check both agree+ * at least once in a few tries (the scheduler may move us between). */+ int same = 0;+ for (int i = 0; i < 50 && !same; i++) {+ cpu = sched_getcpu();+ syscall(SYS_getcpu, &scpu, NULL, NULL);+ same = (unsigned)cpu == scpu;+ }+ CHECK(same, "sched_getcpu %d never matched getcpu %u", cpu, scpu);+}++/* Writing to the vvar page is a SIGSEGV (it's mapped read-only). */+static void vvar_is_read_only(void)+{+ unsigned long base = getauxval(AT_SYSINFO_EHDR);+ if (!base)+ return;+ pid_t pid = fork();+ if (pid == 0) {+ *(volatile unsigned *)(base - 4096) = 1;+ _exit(0);+ }+ int status = 0;+ waitpid(pid, &status, 0);+ CHECK(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV, "write to vvar: status %#x", status);+}++/* A forked child has the vDSO too (same pages, same answers). */+static void after_fork(void)+{+ long long before = vdso_ns(CLOCK_MONOTONIC);+ pid_t pid = fork();+ if (pid == 0) {+ long long a = vdso_ns(CLOCK_MONOTONIC), b = sys_ns(CLOCK_MONOTONIC), c = vdso_ns(CLOCK_MONOTONIC);+ _exit(getauxval(AT_SYSINFO_EHDR) && a >= before && a <= b && b <= c ? 0 : 1);+ }+ int status = 0;+ waitpid(pid, &status, 0);+ CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, "vDSO after fork: status %#x", status);+}++/* And a program started by execve gets a fresh one. */+static void after_exec(const char *self)+{+ pid_t pid = fork();+ if (pid == 0) {+ char *argv[] = { (char *)self, "exec", NULL };+ execv(self, argv);+ _exit(99);+ }+ int status = 0;+ waitpid(pid, &status, 0);+ CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, "vdsotest after execve: status %#x", status);+}++/* A dynamically linked program (PT_INTERP) can't run here: ENOEXEC. */+static void dynamic_is_enoexec(void)+{+ char *argv[] = { "dynhello", NULL };+ errno = 0;+ execv("/mnt/disk0/bin/dynhello", argv);+ CHECK(errno == ENOEXEC, "execve of a dynamic program: errno %d", errno);+}++int main(int argc, char **argv)+{+ if (argc > 1 && strcmp(argv[1], "calls") == 0) {+ /* Lots of clock reads; tests/vdso.rs checks none is a syscall. */+ struct timespec t;+ struct timeval tv;+ long long sum = 0;+ for (int i = 0; i < 1000; i++) {+ clock_gettime(CLOCK_MONOTONIC, &t);+ sum += t.tv_nsec;+ clock_gettime(CLOCK_REALTIME, &t);+ gettimeofday(&tv, NULL);+ sum += time(NULL) + sched_getcpu();+ }+ return sum == 42; /* use the result; 0 */+ }+ if (argc > 1 && strcmp(argv[1], "exec") == 0) {+ image();+ clocks();+ return fails;+ }+ image();+ clocks();+ vvar_is_read_only();+ after_fork();+ after_exec((const char *)getauxval(AT_EXECFN));+ dynamic_is_enoexec();+ printf("vdsotest: %d failed\n", fails);+ return fails;+}
@@ -0,0 +1,83 @@+//! wc [-l] [file...] -- count lines, words and bytes (stdin if no files).+//! -l: only lines.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// (lines, words, bytes) of everything readable from `fd`.+fn count(fd: i64) -> Result<[u64; 3], i64> {+ let mut c = [0u64; 3];+ let mut in_word = false;+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n < 0 {+ return Err(n);+ }+ if n == 0 {+ return Ok(c);+ }+ for &b in &buf[..n as usize] {+ c[2] += 1;+ if b == b'\n' {+ c[0] += 1;+ }+ let space = b == b' ' || b == b'\n' || b == b'\t' || b == b'\r';+ if !space && !in_word {+ c[1] += 1; // a word starts here+ }+ in_word = !space;+ }+ }+}++fn show(c: [u64; 3], lines_only: bool, name: &str) {+ let sep = if name.is_empty() { "" } else { " " };+ if lines_only {+ println!("{}{}{}", c[0], sep, name);+ } else {+ println!("{:>7} {:>7} {:>7}{}{}", c[0], c[1], c[2], sep, name);+ }+}++fn main() -> i64 {+ let lines_only = arg(1) == Some("-l");+ let first = if lines_only { 2 } else { 1 };+ if argc() <= first {+ return match count(0) {+ Ok(c) => {+ show(c, lines_only, "");+ 0+ }+ Err(_) => 1,+ };+ }+ let (mut total, mut status) = ([0u64; 3], 0);+ for i in first..argc() {+ let name = arg(i).unwrap_or("");+ let fd = open(name, 0);+ let r = if fd < 0 { Err(fd) } else { count(fd) };+ if fd >= 0 {+ close(fd);+ }+ match r {+ Ok(c) => {+ show(c, lines_only, name);+ (0..3).for_each(|k| total[k] += c[k]);+ }+ Err(e) => {+ eprintln!("wc: {}: {}", name, strerror(e));+ status = 1;+ }+ }+ }+ if argc() - first > 1 {+ show(total, lines_only, "total");+ }+ status+}