rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Edward <edward@localhost> | 2026-10-31 18:09:36 -0700 |
|---|---|---|
| commit | b0a701d1cb6bbe800c2c067ccce8f3714f33f207 | |
| parent | d8a39ce8e095ab024f7d388b114e5af62fab315a | |
| tree | browse 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.| M | README.md | +7 -5 |
| M | src/dev.rs | +57 -5 |
| M | src/fs/file.rs | +18 -1 |
| M | src/fs/mod.rs | +6 -0 |
| M | src/fs/procfs.rs | +9 -3 |
| M | src/net/unix.rs | +37 -16 |
| M | src/process.rs | +1 -1 |
| M | src/syscall.rs | +12 -0 |
| M | src/syscall_ext.rs | +1 -1 |
| M | tests/virtio.rs | +1 -1 |
| M | user/ptytest.rs | +73 -0 |
| M | user/ulib.rs | +1 -0 |
| M | user/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();