RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorEdward <edward@localhost>2026-10-31 18:09:36 -0700
commit0b513e9df34ecdda27531435d8b774fe5e56bb96
parent4b17d4fa030840eee1bab6fff28d3b5e14e22997
treebrowse files at this commit
Device identities, ttyname, fd-message merging, /proc/self/cwd (Clarke's probe)

- /dev nodes get Linux device numbers (st_rdev: null 1:3, zero 1:5,
  random 1:8, urandom 1:9, tty 5:0, console 5:1, ptmx 5:2, pts/N 136:N,
  hvc0 229:0, fb0 29:0) and Linux's modes. An open device file fstat()s
  as its /dev node (same dev, ino, rdev, mode as stat of the path), so
  musl's ttyname() and `tty` work, also for the console fds init gets.
  statx splits rdev with real major()/minor(). New /dev/zero.
- AF_UNIX stream reads merge the bytes before an fd-carrying message
  with it and stop after it, like Linux ("AB" + "CD"/fd reads "ABCD"
  with the fd); MSG_WAITALL keeps fds and doesn't lose data from another
  sender when credentials are passed.
- Autobind starts at a random 5-hex-digit name (Linux does too).
- /proc/self/cwd.
Mptytest.rs+73 -0
Mulib.rs+1 -0
Munixtest.rs+27 -5

3 files changed, 101 insertions, 5 deletions

@@ -17,6 +17,7 @@ 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).@@ -28,6 +29,77 @@ fn read_some(fd: i64, buf: &mut [u8]) -> usize {     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() {@@ -40,6 +112,7 @@ fn main() -> i64 {     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.
@@ -138,6 +138,7 @@ 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;
@@ -422,7 +422,12 @@ fn stream(c: &mut Checks) {     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('@'), &format!("...gets an abstract name of 5 hex digits ({})", n));+    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);@@ -619,19 +624,36 @@ fn rights(c: &mut Checks) {     close(b);     c.check(write(pw, b"y") == -EPIPE, "closing the sockets closed the fd in flight");     close(pw);-    // Data before an fd message doesn't merge with it.+    // 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 == 2 && g.data == b"aa" && g.fds.is_empty(), "stream: a read stops before a message carrying fds");-    let g = recvmsg(b, 16, 64, 0);-    c.check(g.n == 2 && g.fds.len() == 1, "...which comes with the next read");+    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();