RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 18:09:36 -0700
commitb0a701d1cb6bbe800c2c067ccce8f3714f33f207
parentd8a39ce8e095ab024f7d388b114e5af62fab315a
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.
MREADME.md+7 -5
Msrc/dev.rs+57 -5
Msrc/fs/file.rs+18 -1
Msrc/fs/mod.rs+6 -0
Msrc/fs/procfs.rs+9 -3
Msrc/net/unix.rs+37 -16
Msrc/process.rs+1 -1
Msrc/syscall.rs+12 -0
Msrc/syscall_ext.rs+1 -1
Mtests/virtio.rs+1 -1
Muser/ptytest.rs+73 -0
Muser/ulib.rs+1 -0
Muser/unixtest.rs+27 -5

13 files changed, 250 insertions, 38 deletions

@@ -1000,11 +1000,11 @@ heap_allocation::simple_allocation...               [ok] | `tests/virtio.rs` | **the virtual drivers**: 2 x 4 KiB from virtio-rng differ, aren't constant, have 15384-17384 one bits out of 32768 and contain all 256 byte values; the `random` program (hex, dice, a usage error); hvc0 receives exactly the bytes QEMU feeds in from `tests/hvc0-input.txt`, and 8 KiB written to it go out with nothing dropped; virtio-gpu answers `GET_DISPLAY_INFO`, refuses a flush of a resource that doesn't exist with `ERR_INVALID_RESOURCE_ID`, full and clipped flushes work, a pattern written into the framebuffer reads back; the text console is on the framebuffer (128x48) and printing lights up pixels; `/dev` lists `fb0 hvc0 null random urandom`; `tile` draws the expected colours at known pixels, moves the console to 61x46 and `tile -r` back, with no frames leaked. `scripts/gpu-check.sh` checks what QEMU actually displays | | `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed(SIGSEGV, "stack overflow")` and leaves no frames behind | | `tests/process.rs` | **the process model** (stage 14), mostly by running the user test programs: `cowtest` (globals, heap and stack stay separate after fork in both directions, three generations, `MAP_SHARED` vs `MAP_PRIVATE`); `forktest` (fork/wait4/getppid, 10 children reaped with `wait4(-1)`, `ECHILD`, execve errors, vfork + exec, `pipe2(O_CLOEXEC)`, fork + dup2 + exec through a pipe, a child dying of SIGSEGV); **exec argv/envp/auxv** (`forktest args "two words" ""` with `FOO=bar EMPTY=`: argc, every argv and envp string, `AT_PAGESZ`, `AT_ENTRY` = `_start`, `AT_PHDR` covering the entry, `AT_RANDOM`, `AT_EXECFN`, stack position); `mmaptest` (zeroed anonymous memory, 64 MiB that costs nothing until touched, SIGSEGV handlers that see `si_addr` and `SEGV_ACCERR`/`SEGV_MAPERR` and fix the fault so the access is retried, `PROT_NONE`, file-backed private mappings, `brk`, `EINVAL`s, memory back to the start); an unhandled fault is `Killed(SIGSEGV)` with status 11 and exit code 139; `sigtest` (handler round trip through sigreturn with registers intact, masks and pending signals, SIGKILL can't be caught, default actions, SIGCHLD with `si_pid`/`si_status`, SIGPIPE, stop/continue seen by `wait4(WUNTRACED/WCONTINUED)`, `EINTR` vs `SA_RESTART`, `kill` errors, signal 0, `kill(-pgid)`); wait-status encoding; a **fork bomb** stops at `EAGAIN`, is killed and reaped, and leaves frames and the process count unchanged; **20 fork/exec/exit loops leak no frames** and leave no shared frames; **Ctrl+C** (`tty::input_char('\x03')`) kills `spin` in the foreground group with SIGINT (exit code 130), and does nothing when no group is in the foreground |-| `tests/libc.rs` | **libc plumbing** (stage 15), by running four user test programs twice each (a warm-up, then a run that must leave the thread, process, futex-waiter, pty, socket and frame counts exactly as they were): `threads` (4 threads sum 1..=40,000,000 on several CPUs; a futex mutex keeps a shared counter exact while an unlocked one loses updates; each thread sees its own TLS value through `fs:`; `gettid`; `FUTEX_WAIT` with a stale value is `EAGAIN`, times out, `FUTEX_WAKE` with no waiters; `CLONE_CHILD_CLEARTID` + futex wake for join; `exit` ends one thread, `exit_group` all; `tgkill` and `ESRCH`); `echoserver` (an epoll server and a poll server on 127.0.0.1, three forked clients each, plus UDP over loopback); `ptytest` (canonical mode holds a half line back, backspace editing, CR -> NL, echo, ONLCR; raw mode passes bytes through without echo; `TCGETS`/`TCSETS`; ^C and ^\ kill the child that made the pty its controlling terminal with SIGINT/SIGQUIT and echo "^C"; `/dev/tty` with and without a controlling terminal; closing the master gives the slave EOF); `nbtest` (`EAGAIN` from pipes, sockets and ptys; 64 KiB pipe capacity; poll bits; `fcntl(F_SETFL, O_NONBLOCK)` and `FIONBIO`; nonblocking `connect` is `EINPROGRESS` then `POLLOUT`; `select`, `ppoll`, `readv`/`writev`, `pread`/`pwrite`, `getcwd`, `uname`, `CLOCK_REALTIME`, `nanosleep`, `readlink /proc/self/exe`) |+| `tests/libc.rs` | **libc plumbing** (stage 15), by running four user test programs twice each (a warm-up, then a run that must leave the thread, process, futex-waiter, pty, socket and frame counts exactly as they were): `threads` (4 threads sum 1..=40,000,000 on several CPUs; a futex mutex keeps a shared counter exact while an unlocked one loses updates; each thread sees its own TLS value through `fs:`; `gettid`; `FUTEX_WAIT` with a stale value is `EAGAIN`, times out, `FUTEX_WAKE` with no waiters; `CLONE_CHILD_CLEARTID` + futex wake for join; `exit` ends one thread, `exit_group` all; `tgkill` and `ESRCH`); `echoserver` (an epoll server and a poll server on 127.0.0.1, three forked clients each, plus UDP over loopback); `ptytest` (canonical mode holds a half line back, backspace editing, CR -> NL, echo, ONLCR; raw mode passes bytes through without echo; `TCGETS`/`TCSETS`; ^C and ^\ kill the child that made the pty its controlling terminal with SIGINT/SIGQUIT and echo "^C"; `/dev/tty` with and without a controlling terminal; closing the master gives the slave EOF; stage 19: every `/dev` node's device number, mode and one inode for `stat` and `fstat`, `ttyname` of a pty slave and the console, `/proc/self/cwd`); `nbtest` (`EAGAIN` from pipes, sockets and ptys; 64 KiB pipe capacity; poll bits; `fcntl(F_SETFL, O_NONBLOCK)` and `FIONBIO`; nonblocking `connect` is `EINPROGRESS` then `POLLOUT`; `select`, `ppoll`, `readv`/`writev`, `pread`/`pwrite`, `getcwd`, `uname`, `CLOCK_REALTIME`, `nanosleep`, `readlink /proc/self/exe`) | | `tests/musl_prereq.rs` | **musl prerequisites** (stage 15.1), each program twice with no leaks: the FPU is set up (XSAVE with x87 + SSE (+ AVX) under `-cpu max`); `fputest` (16 xmm/ymm registers with a per-thread pattern in 4 threads x 3 processes at once, fork copies them, exec clears them, a signal handler gets clean registers and Linux's fpstate in its frame and may edit it, 40 async signals mid-check, SIGFPE when TCG raises it); `pathtest` (`EFAULT` for NULL/kernel/unmapped/straddling path pointers, `ENAMETOOLONG`, `struct stat` fields and types, `lstat`, `AT_EMPTY_PATH`, every `*at()` call, `O_EXCL`/`O_DIRECTORY`/`O_NOFOLLOW`, `fchdir`, `rename`); `rawabi` (a gcc-built no-libc C program, unmodified) | | `tests/libc_batch1.rs` | **libc batch 1** (stage 16): `libctest` runs one group per test, twice, and nothing (threads, processes, frames, file locks) may be left over: `wait` (dash's `wait`: SIGCHLD blocked, fork, `rt_sigsuspend` returns `EINTR` once the handler ran and puts the old mask back; a SIGCHLD already pending ends it at once); `signals` (`rt_sigpending`, `rt_sigtimedwait` with and without a timeout and from another process, real-time signals queued 3 times with their values, a standard signal only once, `EPERM`/`ESRCH` for `rt_sigqueueinfo`, `sigaltstack` + `SA_ONSTACK`, `EPERM` on the stack, unknown `sa_flags` dropped); `locks` (`F_GETFL` access modes, `F_SETFL`, `F_GETLK`/`F_SETLK`/`F_SETLKW` between two processes, `EDEADLK`, locks dropped on close and on exit, `RLIMIT_NOFILE` vs `dup` and `F_DUPFD`); `files` (`truncate`/`ftruncate`/`fsync` on ramfs, ext2 and FAT, `umask` on `open` and `mkdir` and across fork, `chmod`/`fchmod`/`fchmodat`, `utimensat` with `UTIME_NOW`/`UTIME_OMIT`, `EROFS` on btrfs); `misc` (rlimits, `prctl`, `sched_getaffinity`, `madvise(MADV_DONTNEED)` reads zeros, a robust futex gets `FUTEX_OWNER_DIED`); `orphans` (running and zombie orphans are freed, a `PR_SET_CHILD_SUBREAPER` process adopts and reaps a grandchild); `--trace` names unknown calls `syscall_N` | | `tests/coreutils_batch2.rs` | **coreutils batch 2** (stage 17): `coretest` runs one group per test, twice, and nothing (threads, processes, frames, file locks, timers) may be left over: `links` (`link`/`linkat`/`symlink`/`symlinkat` on ramfs and ext2 with `st_nlink` and `st_ino`, a dangling symlink, renaming a full directory and over a second name, `EPERM` on FAT/exFAT, `EROFS` on btrfs, `EXDEV`); `owners` (the `chown` family on ramfs and ext2, `-1`, `lchown` vs `chown` on a symlink, FAT's `EPERM` unless nothing changes); `statfs` (`statfs`/`fstatfs` magic numbers and sizes per mount, `ST_RDONLY`, pipes, `statx` vs `stat`, `st_dev` per mount); `info` (`sysinfo` 99 and 502, `times`, `getrusage` for self and children, the CPU-time clocks, `clock_getres`); `sockets` (`getsockname`/`getpeername`, `SO_REUSEADDR` & co., `SO_TYPE`, `SO_ERROR` after a refused connect, `SHUT_WR` = end of file for the peer, `EPIPE` after it, `socketpair` refused for `AF_INET` and unknown families; since stage 18 it works for `AF_UNIX`); `timers` (`alarm`, one-shot and periodic `setitimer`, `pause`, POSIX timers with `SI_TIMER` + `si_value`, `TIMER_ABSTIME`, `SIGEV_NONE`, not inherited by fork); `ids` (`setuid` & co. with the saved id, groups, new files owned by their creator, nice values, `sethostname` -> `uname`); `misc` (`flock` between open files and processes, `preadv`/`pwritev`, `sendfile`, mask sizes for `ppoll`/`pselect6`, a `ppoll` mask letting a signal in, `getdents64` while deleting on ramfs and ext2); `--trace` names (`link`, `statx`, `rb_sysinfo`) |-| `tests/unix_sockets.rs` | **Unix sockets and /proc/self** (stage 18): `unixtest` runs one group per test, twice, and threads, processes, frames (so no open file stuck "in flight") and bound socket names must be back where they were: `stream` (a server on a path in `/tmp` and on ext2 echoes for a forked client; `S_IFSOCK` with mode 0777 minus the umask, `EADDRINUSE`, `ENXIO` from `open`, `unlink`, `ENOENT`/`ECONNREFUSED`, `EPERM` on FAT; abstract names and autobind; `ECONNRESET` for unread data and for a connection the server closed before `accept`; `EPIPE`; `SO_PEERCRED`, `getsockname`/`getpeername`); `pair` (`socketpair` both ways and across fork, `SHUT_WR`, `MSG_PEEK`, seqpacket message borders, dgram pairs, `SOCK_CLOEXEC`/`SOCK_NONBLOCK`); `dgram` (sender names, `MSG_TRUNC`, `connect` and `EPERM` for strangers, `AF_UNSPEC`, `EPROTOTYPE`, `EMSGSIZE`, `ECONNREFUSED`); `rights` (a file and a pipe end read through received fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`, `EBADF` for a bad fd, fds still in flight closed with their socket, a stream read stopping before an fd message, `SCM_CREDENTIALS`); `nonblock` (`EAGAIN`, `SO_SNDBUF` 4096 reading back 8192, the buffer limit, poll bits, a blocked writer finishing in one `write`, `EPOLLRDHUP`/`EPOLLHUP`, nonblocking `accept`/`accept4`, a full backlog, `SO_RCVTIMEO`, `FIONREAD`); `proc` (`/proc/self/fd` listing and links, `pipe:[N]`/`socket:[N]` matching `st_ino`, opening an entry, `/proc/self/exe`); `--trace` names (`sendmsg`, `recvmsg`) |+| `tests/unix_sockets.rs` | **Unix sockets and /proc/self** (stage 18): `unixtest` runs one group per test, twice, and threads, processes, frames (so no open file stuck "in flight") and bound socket names must be back where they were: `stream` (a server on a path in `/tmp` and on ext2 echoes for a forked client; `S_IFSOCK` with mode 0777 minus the umask, `EADDRINUSE`, `ENXIO` from `open`, `unlink`, `ENOENT`/`ECONNREFUSED`, `EPERM` on FAT; abstract names and autobind (two sockets, two different names); `ECONNRESET` for unread data and for a connection the server closed before `accept`; `EPIPE`; `SO_PEERCRED`, `getsockname`/`getpeername`); `pair` (`socketpair` both ways and across fork, `SHUT_WR`, `MSG_PEEK`, seqpacket message borders, dgram pairs, `SOCK_CLOEXEC`/`SOCK_NONBLOCK`); `dgram` (sender names, `MSG_TRUNC`, `connect` and `EPERM` for strangers, `AF_UNSPEC`, `EPROTOTYPE`, `EMSGSIZE`, `ECONNREFUSED`); `rights` (a file and a pipe end read through received fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`, `EBADF` for a bad fd, fds still in flight closed with their socket, a stream read taking the bytes before an fd message together with it and stopping after it (stage 19), `SCM_CREDENTIALS`); `nonblock` (`EAGAIN`, `SO_SNDBUF` 4096 reading back 8192, the buffer limit, poll bits, a blocked writer finishing in one `write`, `EPOLLRDHUP`/`EPOLLHUP`, nonblocking `accept`/`accept4`, a full backlog, `SO_RCVTIMEO`, `FIONREAD`); `proc` (`/proc/self/fd` listing and links, `pipe:[N]`/`socket:[N]` matching `st_ino`, opening an entry, `/proc/self/exe`); `--trace` names (`sendmsg`, `recvmsg`) |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -4590,9 +4590,11 @@ Some details that matter, all Linux's behaviour:   rest are closed. - `MSG_CMSG_CLOEXEC` in `recvmsg`'s flags marks the new fds   close-on-exec, like `O_CLOEXEC`.-- On a stream socket a `read` never runs past a message that carries-  fds: the bytes before it come out first, and the fds come with the-  next read.+- On a stream socket a read takes the bytes *before* a message that+  carries fds together with that message, fds attached, and then stops:+  one read never gets two batches of fds. (Stage 18 stopped in front of+  the fd message instead; stage 19 does what Linux does: "AB", then+  "CD"+fd reads as "ABCD" plus the fd. A plain `read` closes the fd.)  The control buffer has a fixed layout: a `struct cmsghdr` (length, level, type: 16 bytes) and then the data, padded to 8 bytes. That's why
@@ -2,6 +2,7 @@ //! `>` and every program that reads or writes files can use them.) //! //!   /dev/null      reads as empty, swallows writes+//!   /dev/zero      reads as endless zero bytes, swallows writes //!   /dev/random    random bytes (virtio-rng; /dev/urandom is the same -- //!   /dev/urandom   on modern Linux they only differ before boot-time seeding) //!   /dev/console   the console terminal (keyboard + screen)@@ -17,6 +18,13 @@ //! gives a device-specific `File` (via `Inode::open_device`) instead of the //! usual read-the-inode file. Linux does the same with "character device" //! inodes whose `open` is routed to a driver.+//!+//! Stage 19: every node has a fixed identity, like on Linux. Its inode+//! number is the hash of its path (the same every time you look), and its+//! *device number* (`st_rdev`, "major:minor") says which driver it is:+//! 1:3 is null, 5:1 the console, 136:N the pty /dev/pts/N (`DEVICES`).+//! An open device file fstat()s as its node (syscall.rs `fd_stat`), which+//! is how `ttyname()` and the `tty` command find a terminal's name.  use crate::errno::Errno; use crate::fs::file::{File, KIND_DEVICE, SEEK_CUR, SEEK_END, SEEK_SET, Stat};@@ -37,9 +45,26 @@ impl FileSystem for DevFs {     } } +/// Every device node: (name, major, minor, permissions) -- the numbers and+/// modes Linux gives the same devices.+const DEVICES: &[(&str, u64, u64, u32)] = &[+    ("null", 1, 3, 0o666),+    ("zero", 1, 5, 0o666),+    ("random", 1, 8, 0o666),+    ("urandom", 1, 9, 0o666),+    ("tty", 5, 0, 0o666),+    ("console", 5, 1, 0o600),+    ("ptmx", 5, 2, 0o666),+    ("hvc0", 229, 0, 0o600),+    ("fb0", 29, 0, 0o660),+];++/// The major number of /dev/pts/N (whose minor is N).+const PTS_MAJOR: u64 = 136;+ /// The devices that exist on this machine. fn names() -> Vec<&'static str> {-    let mut names = alloc::vec!["console", "null", "ptmx", "pts", "random", "tty", "urandom"];+    let mut names = alloc::vec!["console", "null", "ptmx", "pts", "random", "tty", "urandom", "zero"];     if console::present() {         names.push("hvc0");     }@@ -106,6 +131,9 @@ impl Inode for PtsNode {     fn mode(&self) -> u32 {         0o620     }+    fn rdev(&self) -> u64 {+        crate::fs::file::makedev(PTS_MAJOR, self.0 as u64)+    }     fn open_device(&self) -> Result<Option<Arc<dyn File>>, Errno> {         tty::open_pts(self.0).map(Some)     }@@ -125,12 +153,16 @@ impl Inode for DevNode {         }     }     fn mode(&self) -> u32 {-        0o666+        DEVICES.iter().find(|d| d.0 == self.0).map_or(0o666, |d| d.3)+    }+    fn rdev(&self) -> u64 {+        DEVICES.iter().find(|d| d.0 == self.0).map_or(0, |d| crate::fs::file::makedev(d.1, d.2))     }     fn open_device(&self) -> Result<Option<Arc<dyn File>>, Errno> {         Ok(Some(match self.0 {             "null" => Arc::new(Null),-            "random" | "urandom" => Arc::new(Random),+            "zero" => Arc::new(Zero),+            "random" | "urandom" => Arc::new(Random(self.0)),             "console" => tty::console_file(),             "hvc0" => tty::open_hvc(),             "tty" => tty::open_ctty()?,@@ -162,7 +194,27 @@ impl File for Null {     } } -struct Random;+/// /dev/zero: as many zero bytes as you ask for.+struct Zero;++impl File for Zero {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        buf.fill(0);+        Ok(buf.len())+    }+    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        Ok(buf.len())+    }+    fn stat(&self) -> Result<Stat, Errno> {+        device_stat(0)+    }+    fn proc_link(&self) -> Option<String> {+        Some(String::from("/dev/zero"))+    }+}++/// /dev/random or /dev/urandom (the name: which one was opened).+struct Random(&'static str);  impl File for Random {     fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {@@ -179,7 +231,7 @@ impl File for Random {         device_stat(0)     }     fn proc_link(&self) -> Option<String> {-        Some(String::from("/dev/urandom")) // (or /dev/random: the same device)+        Some(alloc::format!("/dev/{}", self.0))     } } 
@@ -58,7 +58,7 @@ pub struct LinuxStat {     pub st_uid: u32,     // owner: everything belongs to root (0)     pub st_gid: u32,     _pad0: u32,-    pub st_rdev: u64,    // device number for device files (0 here)+    pub st_rdev: u64,    // device number for device files (`makedev`), 0 otherwise     pub st_size: i64,    // bytes     pub st_blksize: i64, // good I/O size: 4096     pub st_blocks: i64,  // 512-byte blocks used@@ -124,9 +124,26 @@ pub fn linux_stat(inode: &dyn Inode, path: &str) -> LinuxStat {     }     (st.st_uid, st.st_gid) = inode.owner();     st.st_dev = super::dev_of(path);+    st.st_rdev = inode.rdev();     st } +/// A device number from its two halves, packed the way Linux's (and+/// glibc's/musl's) `makedev` does: the low 8 bits of the minor, then 12+/// bits of major, then the rest of the minor, then the rest of the major.+/// For small numbers that's just `major << 8 | minor`.+pub const fn makedev(major: u64, minor: u64) -> u64 {+    ((major & 0xffff_f000) << 32) | ((major & 0xfff) << 8) | ((minor & 0xffff_ff00) << 12) | (minor & 0xff)+}++/// The two halves of a device number again (`major()` / `minor()`).+pub const fn dev_major(dev: u64) -> u32 {+    (((dev >> 32) & 0xffff_f000) | ((dev >> 8) & 0xfff)) as u32+}+pub const fn dev_minor(dev: u64) -> u32 {+    (((dev >> 12) & 0xffff_ff00) | (dev & 0xff)) as u32+}+ /// The inode number getdents reports for entry `name` of the directory at /// `dir`: the file system's own, or the same path hash `linux_stat` uses. fn entry_ino(dir: &str, entry: &super::DirEntry) -> u64 {
@@ -81,6 +81,12 @@ pub trait Inode: Send + Sync {     fn mode(&self) -> u32 {         0     }+    /// For a device file (stage 19): which device it is, as Linux numbers+    /// it (`makedev(major, minor)`, e.g. 5:1 for /dev/console). stat+    /// reports it as `st_rdev`. 0 for everything else.+    fn rdev(&self) -> u64 {+        0+    }     /// The target path of a symlink.     fn readlink(&self) -> Result<String, Errno> {         Err(Errno::EINVAL)
@@ -4,6 +4,7 @@ //! have just the corner that programs (and C libraries) lean on most: //! //!   /proc/self/exe     a symlink to the program that's running+//!   /proc/self/cwd     a symlink to the current directory (stage 19) //!   /proc/self/fd/     one symlink per open file descriptor: //!   /proc/self/fd/N      to the file's path ("/tmp/notes.txt"), or a //!                        name like "pipe:[4123]" or "socket:[1007]" for@@ -51,6 +52,7 @@ enum Node {     SelfDir,     FdDir,     Exe,+    Cwd,     /// /proc/self/fd/N     Fd(u64), }@@ -93,13 +95,13 @@ impl Inode for Node {     fn kind(&self) -> InodeKind {         match self {             Node::Root | Node::SelfDir | Node::FdDir => InodeKind::Directory,-            Node::Exe | Node::Fd(_) => InodeKind::Symlink,+            Node::Exe | Node::Cwd | Node::Fd(_) => InodeKind::Symlink,         }     }     fn size(&self) -> usize {         match self {             Node::Root => 1,-            Node::SelfDir => 2,+            Node::SelfDir => 3,             Node::FdDir => my_fds().len(),             _ => self.readlink().map(|t| t.len()).unwrap_or(0),         }@@ -108,7 +110,7 @@ impl Inode for Node {         match self {             Node::Root | Node::SelfDir => 0o555,             Node::FdDir => 0o500,-            Node::Exe => 0o777,+            Node::Exe | Node::Cwd => 0o777,             Node::Fd(_) => 0o700, // lrwx------, like Linux         }     }@@ -118,6 +120,7 @@ impl Inode for Node {             Node::SelfDir => 2,             Node::FdDir => 3,             Node::Exe => 4,+            Node::Cwd => 5,             Node::Fd(n) => 1000 + n,         })     }@@ -134,6 +137,7 @@ impl Inode for Node {     fn readlink(&self) -> Result<String, Errno> {         match *self {             Node::Exe => Ok(crate::syscall::current()?.exe.lock().clone()),+            Node::Cwd => Ok(crate::syscall::current()?.cwd.lock().clone()),             Node::Fd(n) => {                 let file = crate::syscall::current()?.file(n).map_err(|_| Errno::ENOENT)?;                 Ok(fd_link(&file))@@ -146,6 +150,7 @@ impl Inode for Node {             (Node::Root, "self") => Node::SelfDir,             (Node::SelfDir, "fd") => Node::FdDir,             (Node::SelfDir, "exe") => Node::Exe,+            (Node::SelfDir, "cwd") => Node::Cwd,             (Node::FdDir, n) => {                 let fd = fd_number(&format!("/proc/self/fd/{}", n)).ok_or(Errno::ENOENT)?;                 crate::syscall::current()?.file(fd).map_err(|_| Errno::ENOENT)?;@@ -161,6 +166,7 @@ impl Inode for Node {         Ok(match self {             Node::Root => alloc::vec![entry(String::from("self"), InodeKind::Directory, 2)],             Node::SelfDir => alloc::vec![+                entry(String::from("cwd"), InodeKind::Symlink, 5),                 entry(String::from("exe"), InodeKind::Symlink, 4),                 entry(String::from("fd"), InodeKind::Directory, 3),             ],
@@ -495,9 +495,15 @@ impl Endpoint {     }      /// bind with just the family: a free abstract name of 5 hex digits,-    /// like Linux.+    /// like Linux, which also starts counting at a random number (stage+    /// 19), so the names look like "@0001a" or "@b3c07", not "@00000".     fn autobind(self: &Arc<Self>) -> Result<(), Errno> {-        static NEXT: AtomicU64 = AtomicU64::new(0);+        static NEXT: AtomicU64 = AtomicU64::new(u64::MAX); // MAX: not picked yet+        if NEXT.load(Ordering::Relaxed) == u64::MAX {+            let mut r = [0u8; 4];+            crate::process::random_bytes(&mut r);+            let _ = NEXT.compare_exchange(u64::MAX, u32::from_le_bytes(r) as u64 & 0xf_ffff, Ordering::Relaxed, Ordering::Relaxed);+        }         for _ in 0..0x10_0000 {             let n = NEXT.fetch_add(1, Ordering::Relaxed) & 0xf_ffff;             let name = alloc::format!("{:05x}", n).into_bytes();@@ -802,7 +808,10 @@ impl Endpoint {     }      /// Receive up to `want` bytes.-    fn recv(&self, want: usize, flags: u64, nonblock: bool) -> Result<Received, Errno> {+    /// Take up to `want` bytes. `glue_to`: MSG_WAITALL's second and later+    /// helpings, which must come from the same sender as the first when+    /// credentials are passed (if not, we return nothing, like EOF).+    fn recv(&self, want: usize, flags: u64, nonblock: bool, glue_to: Option<Ucred>) -> Result<Received, Errno> {         let peek = flags & MSG_PEEK != 0;         let timeout = self.with(|s| s.rcvtimeo);         let r = wait_on(&self.readers, timeout, || {@@ -823,22 +832,27 @@ impl Endpoint {             }             let passcred = s.passcred;             let st = &mut *s;-            let first = st.queue.front_mut().unwrap();+            let first = st.queue.front().unwrap();             let cred = first.cred;             let from = first.from.clone();-            let fds = if peek { first.fds.clone() } else { core::mem::take(&mut first.fds) };+            let mut fds = Vec::new();             let mut data = Vec::new();             let full;             if self.ty == Type::Stream {-                // Gather from as many chunks as wanted, but not past one-                // carrying fds (they belong with its first byte) or from-                // another sender when credentials are passed.-                let had_fds = !fds.is_empty();+                if glue_to.is_some_and(|c| passcred && c != cred) {+                    return Some(Ok(Received { data: Vec::new(), full: 0, fds: Vec::new(), cred: None, from: Name::Unnamed }));+                }+                // Gather from as many chunks as wanted, like Linux: never+                // across a change of sender when credentials are passed,+                // and stop right after a chunk that carries fds (they come+                // with the bytes read so far, and one read takes at most+                // one batch of fds). So "AB", then "CD"+fd reads "ABCD"+                // with the fd attached.                 let mut used = 0;                 let mut i = 0;                 while data.len() < want && i < st.queue.len() {                     let m = &mut st.queue[i];-                    if i > 0 && (had_fds || !m.fds.is_empty() || (passcred && m.cred != cred)) {+                    if i > 0 && passcred && m.cred != cred {                         break;                     }                     let n = m.len().min(want - data.len());@@ -847,6 +861,10 @@ impl Endpoint {                         m.start += n;                         used += n;                     }+                    if !m.fds.is_empty() {+                        fds = if peek { m.fds.clone() } else { core::mem::take(&mut m.fds) };+                        break;+                    }                     i += 1;                 }                 full = data.len();@@ -858,6 +876,8 @@ impl Endpoint {                 }             } else {                 // A whole message; what doesn't fit is lost.+                let first = st.queue.front_mut().unwrap();+                fds = if peek { first.fds.clone() } else { core::mem::take(&mut first.fds) };                 full = first.len();                 let n = full.min(want);                 data.extend_from_slice(&first.data[first.start..first.start + n]);@@ -950,7 +970,7 @@ impl Drop for UnixSocket {  impl File for UnixSocket {     fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {-        let r = self.ep.recv(buf.len(), 0, self.nb(0))?;+        let r = self.ep.recv(buf.len(), 0, self.nb(0), None)?;         buf[..r.data.len()].copy_from_slice(&r.data);         Ok(r.data.len()) // (fds sent along are closed: read can't take them)     }@@ -1152,15 +1172,16 @@ fn recv(sock: &UnixSocket, want: usize, flags: u64) -> Result<(Received, usize),         return Err(Errno::EOPNOTSUPP);     }     let nb = sock.nb(flags);-    let mut r = sock.ep.recv(want, flags, nb)?;+    let mut r = sock.ep.recv(want, flags, nb, None)?;     if flags & MSG_WAITALL != 0 && flags & MSG_PEEK == 0 && sock.ep.ty == Type::Stream && !nb {+        let first_cred = r.cred; // (Some only when credentials are passed)         while r.data.len() < want && r.fds.is_empty() {-            match sock.ep.recv(want - r.data.len(), flags, nb) {-                Ok(more) if !more.data.is_empty() && more.fds.is_empty() && more.cred == r.cred => {+            match sock.ep.recv(want - r.data.len(), flags, nb, first_cred) {+                Ok(more) if more.data.is_empty() && more.fds.is_empty() => break, // EOF (or another sender)+                Ok(more) => {                     r.data.extend_from_slice(&more.data);+                    r.fds = more.fds; // (fds end the read)                 }-                Ok(more) if more.data.is_empty() => break, // EOF-                Ok(_) => break, // (can't happen: fds stop at a message border)                 Err(_) => break, // a signal: return what we have             }         }
@@ -553,7 +553,7 @@ fn user_thread_entry() { }  /// Random bytes for AT_RANDOM (virtio-rng; the TSC if there's none).-fn random_bytes(buf: &mut [u8]) {+pub(crate) fn random_bytes(buf: &mut [u8]) {     if !crate::virtio::rng::fill(buf) {         let mut x = unsafe { core::arch::x86_64::_rdtsc() } | 1;         for b in buf {
@@ -1121,6 +1121,18 @@ pub(crate) fn fd_stat(fd: u64) -> Result<LinuxStat, Errno> {     if let (Some(inode), Some(path)) = (file.vfs_inode(), file.path()) {         return Ok(fs::file::linux_stat(&*inode, &path));     }+    // Device files opened some other way than by path (the console fds+    // the kernel hands init, a pty from /dev/ptmx, /dev/tty): report the+    // /dev node they are, so fstat(fd) and stat("/dev/...") agree. That's+    // how ttyname() finds a terminal's name (stage 19).+    let kind = file.stat()?.kind;+    if kind == fs::file::KIND_DEVICE || kind == fs::file::KIND_CONSOLE {+        if let Some(link) = file.proc_link().filter(|l| l.starts_with("/dev/")) {+            if let Ok((inode, real)) = fs::lookup_resolved(&link, true) {+                return Ok(fs::file::linux_stat(&*inode, &real));+            }+        }+    }     let ino = match file.path() {         Some(path) => fs::file::ino_hash(path.as_bytes()),         None => fs::file::anon_ino(&file),
@@ -637,7 +637,7 @@ fn sys_statx(dirfd: u64, path_ptr: u64, flags: u64, mask: u64, buf: u64) -> Resu     for (off, t) in [(64, st.st_atime), (96, st.st_ctime), (112, st.st_mtime)] {         b.u64(off, t[0] as u64).u32(off + 8, t[1] as u32);     }-    b.u32(128, (st.st_rdev >> 8) as u32 & 0xfff).u32(132, st.st_rdev as u32 & 0xff);+    b.u32(128, fs::file::dev_major(st.st_rdev)).u32(132, fs::file::dev_minor(st.st_rdev));     b.u32(136, 0).u32(140, st.st_dev as u32); // device: major 0, minor = mount     put(buf, &b.0)?;     Ok(0)
@@ -150,7 +150,7 @@ fn dev_lists_the_devices() {     let dev = fs::lookup("/dev").expect("no /dev");     let mut names: Vec<String> = dev.list().expect("list /dev").into_iter().map(|e| e.name).collect();     names.sort();-    assert_eq!(names, ["console", "fb0", "hvc0", "null", "ptmx", "pts", "random", "tty", "urandom"]);+    assert_eq!(names, ["console", "fb0", "hvc0", "null", "ptmx", "pts", "random", "tty", "urandom", "zero"]);     assert_eq!(fs::lookup("/dev/fb0").unwrap().size(), 1024 * 768 * 4); } 
@@ -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();