rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Edward <edward@localhost> | 2026-10-31 19:07:29 -0700 |
|---|---|---|
| commit | 52913323cee4a3db45fc331519752f5b25db6693 | |
| parent | a55b41da50171a068fabf0c50a3ed4acd17758c8 | |
| tree | browse files at this commit | |
Stage 20: plain static executables and timerfd (0.20.0) - Relink the kernel at 0x200_0000_0000 (one pre-link-arg in the target JSON) so user space starts at 0x40_0000; stock musl-gcc -static ET_EXEC binaries load unmodified. Each process gets a private slot-0 L3/L2 that shares the kernel's low 2 MiB (VGA, SMP trampoline) without the USER bit. - Drop the special ET_EXEC link flags from build.rs (and rawabi). - timerfd_create/settime/gettime (283/286/287) on the existing timer table: REALTIME/MONOTONIC/BOOTTIME, NONBLOCK/CLOEXEC/ABSTIME, one-shot + interval, poll/select/epoll readiness, ~55 ms resolution. - Tests: exechello, execthreads, timerfdtest; tests/timerfd.rs. README stage 20.
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +1 -1 |
| M | README.md | +157 -6 |
| M | build.rs | +28 -35 |
| M | scripts/run-headless.sh | +6 -6 |
| M | scripts/screenshot.sh | +1 -1 |
| M | src/elf.rs | +5 -4 |
| M | src/fs/file.rs | +4 -0 |
| M | src/memory.rs | +98 -29 |
| M | src/syscall.rs | +1 -1 |
| M | src/syscall_ext.rs | +48 -0 |
| M | src/timer.rs | +118 -0 |
| M | tests/shell.rs | +1 -1 |
| M | tests/static_pie.rs | +31 -2 |
| A | tests/timerfd.rs | +90 -0 |
| M | user/evil.rs | +1 -1 |
| A | user/exechello.c | +74 -0 |
| M | user/hello.rs | +2 -2 |
| M | user/nbtest.rs | +1 -1 |
| M | user/piethreads.c | +22 -1 |
| M | user/rawabi.c | +5 -4 |
| A | user/timerfdtest.c | +191 -0 |
| M | x86_64-rust_kernel.json | +1 -0 |
23 files changed, 887 insertions, 96 deletions
@@ -261,7 +261,7 @@ dependencies = [ [[package]] name = "rustbelt"-version = "0.19.0"+version = "0.20.0" dependencies = [ "bootloader", "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.19.0"+version = "0.20.0" edition = "2024" [dependencies]
@@ -1009,6 +1009,8 @@ heap_allocation::simple_allocation... [ok] | `tests/vdso.rs` | **the vDSO** (stage 19): the embedded image is a one-page `ET_DYN` with `__vdso_*` symbols; the vvar page is published (even `seq`, TSC flag set); the kernel clock counts in nanoseconds; `vdsotest` passes twice with no leaks (auxv entries, vDSO vs syscall ordering, 100,000 monotonic reads, sub-microsecond steps, `gettimeofday`/`time`/`sched_getcpu`, the vvar page can't be made writable or written, it all works after fork and after execve, a dynamic program is `ENOEXEC`); `--trace` shows **no** clock syscalls for 1000 rounds of clock calls, and with the fast path switched off (`vdso::force_syscall`) they're all there; `--trace` names (`gettimeofday`, `time`, `getcpu`) | | `tests/static_pie.rs` | **static PIE** (stage 19): `piehello` (a `musl-gcc -static-pie` program: relocations applied, `AT_PHDR`/`AT_ENTRY`/`AT_BASE`, RELRO, no INTERP), a different page-aligned base on every run, `piethreads` (pthreads, `__thread` variables, stdio to a file, `ttyname`) twice with no leaks, `dynhello` (PT_INTERP) refused with `ENOEXEC`, ET_EXEC programs still load | | `tests/mremap.rs` | **mremap** (stage 19): `remaptest` twice with no leaks: grow in place, shrink, same size, `ENOMEM` without `MREMAP_MAYMOVE`, a move keeps the bytes and empties the old place, `MREMAP_FIXED`, the `EINVAL`/`EFAULT` cases, a moved file mapping still reads the file, copy-on-write after a move in a forked child, musl's `realloc` of big blocks |+| `tests/static_pie.rs` (stage 20 part) | **plain static programs** (stage 20): `exechello` and `execthreads` (stock `musl-gcc -static`, ET_EXEC at `0x400000`) run, their heap and mmap memory stay out of the image, nothing leaks |+| `tests/timerfd.rs` | **timerfd** (stage 20): `timerfdtest` (one-shot, interval counts, `EAGAIN`, `EINVAL`, poll/select/epoll wake-ups, `TFD_TIMER_ABSTIME`) twice; closing leaves no timer and no memory behind | How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1142,6 +1144,8 @@ vdso/vdso.c, vdso/vdso.lds stage 19: the vDSO (clock_gettime, gettimeofday, tim src/vdso.rs stage 19: the vDSO's pages, the vvar page and its seqlock, the TSC trust rule user/{vdsotest,piehello,piethreads,dynhello,remaptest}.c stage 19: C programs built with musl-gcc (needs musl-tools; tests/vdso.rs, static_pie.rs and mremap.rs run them)+user/{exechello,timerfdtest}.c stage 20: plain `musl-gcc -static` programs (execthreads = piethreads.c+ built that way too); tests/static_pie.rs and tests/timerfd.rs run them src/fs/lock.rs stage 16: fcntl record locks (F_GETLK/F_SETLK/F_SETLKW), deadlock detection src/fpu.rs stage 15.1: FPU/SSE/AVX on per CPU (CR0/CR4/XCR0), XSAVE or FXSAVE save/restore scripts/hvc0-session.txt the commands run-headless.sh feeds into hvc0 (for `getty -1 /dev/hvc0`)@@ -1212,7 +1216,7 @@ physical RAM is also mapped at `physical_memory_offset` (`0x18000000000` here), | Virtual address | What | |---|---| | `0xb8000` | VGA text buffer (identity-mapped: virt = phys) |-| `0x200000`... | kernel code + data (the ELF; maps to phys around `0x400000`) |+| `0x200_0000_0000`... | kernel code + data (the ELF; maps to phys around `0x400000`). Until stage 20 it was at `0x200000` | | `0x0100_0000_0000`... | kernel stack (80 pages, guard page below it) | | `0x0180_0000_0000`... | all physical memory (`physical_memory_offset`) | | `0x4444_4444_0000` | kernel heap, 4 MiB (also holds thread stacks) (we map it in `allocator::init_heap`) |@@ -1486,7 +1490,7 @@ and demo programs from stages 5 and 6 (run them from the shell: `run hello`, | Program | What it does | |---|---| | `hello` | prints "Hello from user mode!", `getpid`, reads `/hello.txt` from the ramfs, creates and reads back `/tmp/note.txt`, checks error cases (`ENOENT`, `EBADF`, `EFAULT`, `ENOSYS`), exits with the number of failed checks (0) |-| `evil` | reads kernel memory at `0x200000` -> page fault in ring 3 -> the kernel kills it and keeps going |+| `evil` | reads kernel memory at `0x200_0000_0000` (`0x200000` before stage 20) -> page fault in ring 3 -> the kernel kills it and keeps going | | `spin` | busy-loops ~2 s with no syscalls; it still gets preempted (threads A/B/C keep printing) | | `demo` | stage 6 checks: cwd, reads `/mnt/disk0/readme.txt`, `stat`/`fstat`/`lseek`, lists `bin/` with `getdents64`, `chdir` errors, `mkdir`/`unlink`/`rmdir` on ramfs and FAT, a pipe from a child, arguments, `wait` exit codes (42, 139 for killed `evil`), `ECHILD`/`ENOENT`/`ENOEXEC`, `sleep_ms`; exits with the number of failed checks | | `child` | helper for `demo` and tests: `child pipe` writes a line to stdout, `child cat` copies stdin to stdout, `child 42` exits with 42, otherwise prints its arguments |@@ -1704,6 +1708,9 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 273 | `set_robust_list` | *head, 24 | **stage 16**: pthreads' robust mutexes: when the thread dies, each lock it still holds gets `FUTEX_OWNER_DIED` and a waiter is woken. `get_robust_list` 274 | | 280 | `utimensat` | dirfd, path, *[atime, mtime], flags | **stage 16**: set file times (`UTIME_NOW`, `UTIME_OMIT`; NULL = now); path NULL = the fd itself (`futimens`). ext2 keeps whole seconds; btrfs `EROFS` | | 281 | `epoll_pwait` | epfd, *events, max, timeout, *mask, 8 | **stage 15** |+| 283 | `timerfd_create` | clock, flags | **stage 20**: a timer you `read`; clocks REALTIME 0, MONOTONIC 1, BOOTTIME 7 (= MONOTONIC); flags TFD_NONBLOCK, TFD_CLOEXEC |+| 286 | `timerfd_settime` | fd, flags, *new, *old | **stage 20**: like `timer_settime`; TFD_TIMER_ABSTIME 1; TFD_TIMER_CANCEL_ON_SET 2 is `EINVAL` |+| 287 | `timerfd_gettime` | fd, *itimerspec | **stage 20**: time left and the interval | | 288 | `accept4` | fd, *addr, *len, flags | **stage 15**: flags: SOCK_NONBLOCK, SOCK_CLOEXEC (others `EINVAL` for `AF_UNIX`, stage 18) | | 291 | `epoll_create1` | flags | **stage 15**: EPOLL_CLOEXEC | | 292 | `dup3` | old, new, flags | flags: O_CLOEXEC |@@ -1769,14 +1776,15 @@ are mapped in every address space and the kernel keeps working after a CR3 switch), and L4 slot 1 is the process's own: ```-0x0000_0000_0020_0000 kernel code (bootloader 0.9 puts it here) shared, ring 0 only-0x0000_0080_0000_0000 USER_START (L4 slot 1) per process+0x0000_0000_0040_0000 stage 20: USER_START; plain `gcc -static` programs per process+0x0000_0080_0000_0000 L4 slot 1 (USER_START until stage 20) per process 0x0000_0080_0040_0000 program (.text R-X, .rodata R--, .data/.bss RW-), then the brk heap ... mmap regions, handed out downwards (stage 14, see below) 0x0000_00ff_ffef_f000 user stack (1 MiB, RW-, grows down; a 64 KiB guard zone under it) 0x0000_0100_0000_0000 USER_END 0x0000_0100_0020_0000 boot stack (L4 slot 2) shared 0x0000_0180_0000_0000 all physical memory (phys_mem_offset, slot 3) shared+0x0000_0200_0000_0000 kernel code (stage 20; was 0x200000) shared, ring 0 only 0x0000_4444_4444_0000 kernel heap (thread kernel stacks live here) shared ``` @@ -1785,6 +1793,8 @@ user space the whole lower half. With bootloader 0.9 the kernel sits low, so here user space is one 512 GiB slot that the kernel doesn't use. Only user pages have `USER_ACCESSIBLE`, which is why `evil` faults: the page at `0x200000` *is* mapped, just not for ring 3 (`PROTECTION_VIOLATION | USER_MODE`).+(Stage 20 moved the kernel to `0x200_0000_0000` and made user space start+at 4 MiB; see "Plain static programs and timerfd (stage 20)".) `.text` is not writable and everything else is `NO_EXECUTE` (when EFER.NXE is on). A **process** = a kernel thread (`thread.rs`, it already has a kernel stack and@@ -3316,6 +3326,7 @@ process's memory as a list of **regions** (Linux calls them VMAs) instead of a list of mapped pages: ```+0x0000_0000_0040_0000 (stage 20) a plain `gcc -static` program goes here instead 0x0000_0080_0040_0000 program: .text R-X, .rodata R--, .data/.bss RW- (loaded at exec) heap: from the end of the program up to brk (grows with brk) ... free ...@@ -4951,10 +4962,10 @@ Doesn't (yet): - **No dynamic loader**: programs with `PT_INTERP` (dynamically linked) get `ENOEXEC`. Use `-static-pie`. - **Stock `musl-gcc -static`** (ET_EXEC at `0x400000`) can't run; see- above for the flags that make an ET_EXEC that can.+ above for the flags that make an ET_EXEC that can. (Done in stage 20.) - **`timerfd`** isn't there yet: it needs a new kind of file that `poll` and `epoll` can wait on, and the timers (stage 17) only tick every- ~55 ms anyway. Next on the list.+ ~55 ms anyway. Next on the list. (Done in stage 20.) - **`mremap`**: no `MREMAP_DONTUNMAP`, no `old_len` 0 (a second view of a shared mapping), and only `mmap` regions. - **The clock isn't adjusted**: no NTP, no `settimeofday`; the vvar page@@ -4962,6 +4973,145 @@ Doesn't (yet): - **`/dev/tty`** opened as a file `fstat`s as the terminal it stands for (like `/dev/pts/0`), not as `/dev/tty` itself. +## Plain static programs and timerfd (stage 20)++Stage 20 is two small follow-ups to stage 19, kept deliberately small:+no new subsystems and no new dependencies.++1. A program built the most ordinary way, `musl-gcc -static hello.c`,+ now runs **unmodified**.+2. **timerfd**: a timer that is a file, so an event loop can wait for+ it with `poll`/`select`/`epoll` like any other fd.++### Why `gcc -static` programs couldn't run++A classic Linux program (ELF type **ET_EXEC**) is linked to run at one+fixed address, and every Linux linker picks **`0x400000`** (4 MiB) for+it. It can't be moved: its code has that address baked in.++Until now that address belonged to the kernel. bootloader 0.9 loads the+kernel wherever its ELF file says, and our kernel was linked at+`0x200000` (2 MiB), spilling up to about 5.5 MiB. So user space had to+live somewhere else: level-4 slot 1, starting at 512 GiB. Stage 19 got+around that with special link flags (see "Static PIE" above).++### Moving the kernel out of the way++Two small changes, the minimal way:++- **The kernel is linked at `0x200_0000_0000`** (2 TiB, level-4 slot 4)+ instead of `0x200000`. That's one line in `x86_64-rust_kernel.json`:+ `"pre-link-args": { "ld.lld": ["--image-base=0x20000000000"] }`. The+ bootloader reads the address from the ELF file, so nothing else+ changes; the slots it picks for its own things (boot stack in slot 2,+ physical memory in slot 3) stay the same.+- **User space now starts at 4 MiB**: `USER_START` is `0x40_0000`, and+ user space covers level-4 slots 0 and 1 (up to `USER_END`, 1 TiB, as+ before).++There's one catch: the first 2 MiB still belong to the kernel. The VGA+text buffer at `0xb8000`, the page where the other CPUs start (`0x8000`,+see `smp.rs`) and the bootloader's own page tables are all there,+identity-mapped (virtual address = physical address). So each process+gets its **own** level-3 and level-2 table for slot 0, with one entry+copied from the kernel: the one for 0..2 MiB. It isn't+`USER_ACCESSIBLE`, so ring 3 still can't touch it. 2..4 MiB is left+empty, so a NULL pointer plus a big offset still faults.++```+0x0000_0000_0000_0000 0..2 MiB: kernel only (VGA, SMP trampoline) shared by everyone+0x0000_0000_0020_0000 2..4 MiB: nothing (a gap)+0x0000_0000_0040_0000 USER_START: a plain `gcc -static` program, then its brk heap+0x0000_0080_0040_0000 our Rust programs and static PIEs (unchanged)+ ... mmap regions, handed out downwards+0x0000_00ff_ffef_f000 user stack (unchanged), vDSO and vvar just under it+0x0000_0100_0000_0000 USER_END+0x0000_0200_0000_0000 the kernel's code and data (was 0x200000)+```++The page-table code in `memory.rs` that walks a process's user pages+(`clone_user_space` for fork, `clear_user_space` for exec and exit)+now walks both slots and skips the kernel's 0..2 MiB entry. Each time it+makes a new address space it also checks that the kernel really maps nothing else+in slot 0, and panics with a clear message if it ever does.++Everything else stayed where it was: Rust programs (`user/link.ld`),+static PIEs (random base just above 512 GiB), the stack, the vDSO,+`mmap` from the top down. The heap of an ET_EXEC program starts right+after its image (`brk`), and mmap memory comes from near 1 TiB, so the+two can't run into the image or each other.++`build.rs` lost all its special ET_EXEC flags: `vdsotest` and the no-libc+`rawabi` are now built with the stock recipe (`musl-gcc -static`,+`cc -static -nostdlib -no-pie`). `evil` and `hello` now poke at the+kernel's new address (`0x200_0000_0000`) to check that ring 3 can't+read kernel memory.++### timerfd: a timer you can read++Stage 17's timers deliver a **signal** when they expire. Signals are+awkward in an event loop (they interrupt whatever you're doing). A+**timerfd** is the friendlier version: it's an fd, and++- every expiration adds 1 to a counter;+- `read(fd, &n, 8)` returns that counter as an 8-byte number and resets+ it to 0 (it waits if it's 0, or says `EAGAIN` with `TFD_NONBLOCK`; a+ buffer smaller than 8 bytes is `EINVAL`);+- `poll`/`select`/`epoll` report it **readable** while the counter isn't 0.++```c+int fd = timerfd_create(CLOCK_MONOTONIC, 0);+struct itimerspec its = { .it_value = {1, 0}, .it_interval = {1, 0} };+timerfd_settime(fd, 0, &its, NULL); /* in 1 s, then every second */+uint64_t n;+read(fd, &n, 8); /* waits; n = expirations since last read */+```++How it's built, reusing what we had:++- The timer is an entry in the **same table** as the stage 17 timers+ (`src/timer.rs`), and the same kernel thread, `timerd`, fires it.+ Instead of sending a signal, firing adds to the entry's counter, wakes+ readers, and calls `poll::notify()` so poll/select/epoll look again.+- The fd is a small `File` (`TimerFd`, also in `timer.rs`). Its `read`+ and `poll` look at the counter. When the last fd for it is closed, the+ file is dropped and **its timer is deleted** too. A timerfd isn't tied+ to a process: it survives `execve`, and fork shares it, as on Linux.+- Clocks: `CLOCK_REALTIME`, `CLOCK_MONOTONIC`, and `CLOCK_BOOTTIME` (the+ same as MONOTONIC: Rustbelt never sleeps). `TFD_TIMER_ABSTIME` means+ "at this time" instead of "in this long". `TFD_TIMER_CANCEL_ON_SET`+ (wake me if someone changes the clock) is `EINVAL`: nobody can set+ our clock yet anyway.++**Resolution**: like all our timers, a timerfd is checked on PIT ticks,+about every **55 ms**. It never fires early, but it can fire up to+~110 ms late. A 10 ms interval timer still counts correctly: when timerd+wakes up late, it adds all the periods that went by.++### Testing it++| Test file | Program it runs | What it shows |+|-----------|-----------------|---------------|+| `tests/static_pie.rs` | `exechello`, `execthreads` | stock `musl-gcc -static` programs (ET_EXEC at `0x400000`) run; `brk`, `mmap` and `malloc` memory stays out of the image; threads and stdio work; no leaks |+| `tests/timerfd.rs` | `timerfdtest` | one-shot, interval counts adding up, `EAGAIN`, `EINVAL`, poll/select/epoll wake-ups, `TFD_TIMER_ABSTIME`; closing leaves no timer and no memory behind |++`execthreads` is the same source as `piethreads` (`user/piethreads.c`),+just built as a plain ET_EXEC. From the shell: `exechello`,+`execthreads`, `timerfdtest`.++### What stage 20 does and doesn't do++Does: everything above.++Doesn't (yet):++- **The kernel isn't in the upper half** (`0xffff_8000_...`) like on+ Linux; it just moved to 2 TiB. User space is the lowest 1 TiB, minus+ the first 4 MiB.+- **timerfd**: no `TFD_TIMER_CANCEL_ON_SET`, no `TFD_IOC_SET_TICKS`+ ioctl, and the ~55 ms resolution above.+- Still **no dynamic loader** (`PT_INTERP` gets `ENOEXEC`).+ ## Roadmap Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -5018,6 +5168,7 @@ timers, user ids), and Unix sockets with `/proc/self` (stage 18). - [x] coreutils batch 2 (stage 17): `link`/`symlink` and their `*at` forms, native `rename` of full directories and `RENAME_NOREPLACE`, the `chown` family with owners on ramfs and ext2, `statfs`/`fstatfs`/`statx` with Linux's magic numbers and a real `st_dev`/`st_nlink`, Linux's `sysinfo` (99), `times`/`getrusage`/CPU-time clocks/`clock_getres`, `getsockname`/`getpeername`/`getsockopt`/`shutdown` and remembered socket options, `alarm`/`setitimer`/POSIX timers, real (unenforced) user and group ids, nice values, `sethostname`, `flock`, `preadv`/`pwritev`, `sendfile`, and a `getdents64` that `rm -r` can trust — [man 2 link](https://man7.org/linux/man-pages/man2/link.2.html), [man 2 statfs](https://man7.org/linux/man-pages/man2/statfs.2.html), [man 2 timer_create](https://man7.org/linux/man-pages/man2/timer_create.2.html), [man 7 credentials](https://man7.org/linux/man-pages/man7/credentials.7.html), [man 2 flock](https://man7.org/linux/man-pages/man2/flock.2.html) - [x] Unix sockets (stage 18): `AF_UNIX` stream/dgram/seqpacket with path, abstract and unnamed names, `socketpair`, `sendmsg`/`recvmsg` with `SCM_RIGHTS` (in-flight fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`) and `SCM_CREDENTIALS`, `SO_PEERCRED`/`SO_PASSCRED`, real buffer limits with backpressure, poll/epoll with `POLLRDHUP`/`POLLHUP`, and `/proc/self/fd` + `/proc/self/exe` — [man 7 unix](https://man7.org/linux/man-pages/man7/unix.7.html), [man 3 cmsg](https://man7.org/linux/man-pages/man3/cmsg.3.html), [man 2 sendmsg](https://man7.org/linux/man-pages/man2/sendmsg.2.html), [man 5 proc](https://man7.org/linux/man-pages/man5/proc.5.html), [Wayland protocol: wire format](https://wayland.freedesktop.org/docs/html/ch04.html) - [x] The vDSO, static PIE and mremap (stage 19): `__vdso_clock_gettime`/`gettimeofday`/`time`/`getcpu` with a seqlocked vvar page and `AT_SYSINFO_EHDR`, a nanosecond kernel clock, ET_DYN programs without PT_INTERP at a random base (`AT_PHDR`/`AT_ENTRY`/`AT_BASE`, `PT_GNU_STACK`/`PT_TLS`/`PT_GNU_RELRO`), musl-gcc C programs in the build, `mremap` with `MREMAP_MAYMOVE`/`MREMAP_FIXED`, the threads flake fixed, Linux device numbers and `ttyname` — [man 7 vdso](https://man7.org/linux/man-pages/man7/vdso.7.html), [man 2 mremap](https://man7.org/linux/man-pages/man2/mremap.2.html)+- [x] Plain static programs and timerfd (stage 20): the kernel moved to `0x200_0000_0000` so user space starts at 4 MiB and stock `musl-gcc -static` ET_EXEC binaries run unmodified; `timerfd_create`/`settime`/`gettime` on the existing timer table, readable with `read`/poll/select/epoll - [ ] The kernel/OS split and a musl port (next; waits for Tyler's go; see the missing-syscall list in "musl prerequisites") - [ ] A Hyprland-style tiling compositor on the framebuffer (later)
@@ -96,19 +96,22 @@ const C_PROGRAMS: &[&str] = &["rawabi"]; // stage 15.1 enum Musl { /// Stock `musl-gcc -static-pie`: a position-independent executable. StaticPie,- /// A classic static ET_EXEC. It must live at Rustbelt's user base, so- /// the plain `musl-gcc -static` recipe (linked at 0x400000) needs help:- /// see `build_musl_program`.+ /// Stock `musl-gcc -static`: a classic ET_EXEC linked at 0x400000+ /// (stage 20 made that address user space, so no extra flags needed). StaticExec, /// Plain dynamic linking (PT_INTERP): Rustbelt must refuse it. Dynamic, }-const MUSL_PROGRAMS: &[(&str, Musl)] = &[- ("vdsotest", Musl::StaticExec),- ("piehello", Musl::StaticPie),- ("piethreads", Musl::StaticPie),- ("dynhello", Musl::Dynamic),- ("remaptest", Musl::StaticPie),+/// (program name, source file in user/ without `.c`, how to link it)+const MUSL_PROGRAMS: &[(&str, &str, Musl)] = &[+ ("vdsotest", "vdsotest", Musl::StaticExec),+ ("piehello", "piehello", Musl::StaticPie),+ ("piethreads", "piethreads", Musl::StaticPie),+ ("dynhello", "dynhello", Musl::Dynamic),+ ("remaptest", "remaptest", Musl::StaticPie),+ ("exechello", "exechello", Musl::StaticExec), // stage 20+ ("execthreads", "piethreads", Musl::StaticExec), // stage 20: same source, ET_EXEC+ ("timerfdtest", "timerfdtest", Musl::StaticExec), // stage 20 ]; const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;@@ -136,8 +139,8 @@ fn main() { for program in C_PROGRAMS { build_c_program(&user_dir, &out_dir, program); }- for &(program, kind) in MUSL_PROGRAMS {- build_musl_program(&user_dir, &out_dir, program, kind);+ for &(program, source, kind) in MUSL_PROGRAMS {+ build_musl_program(&user_dir, &out_dir, program, source, kind); } build_disk(&manifest, &out_dir); build_ext2_disks(&manifest, &out_dir);@@ -162,15 +165,15 @@ fn build_program(user_dir: &Path, out_dir: &Path, program: &str) { } /// A freestanding C program: no libc, no startup files, just raw syscalls.-/// -fpie makes the code RIP-relative so it can live at Rustbelt's user base-/// (0x80_0000_0000, above 4 GiB); -no-pie still makes an ordinary static-/// ET_EXEC file, which also runs as-is on a real x86_64 Linux machine.+/// -no-pie makes an ordinary static ET_EXEC file at the usual 0x400000+/// (since stage 20 that's user space), which also runs as-is on a real+/// x86_64 Linux machine. fn build_c_program(user_dir: &Path, out_dir: &Path, program: &str) { let cc = env::var("CC").unwrap_or_else(|_| "cc".into()); let status = Command::new(&cc) .args(["-O2", "-static", "-nostdlib", "-ffreestanding", "-fno-stack-protector"])- .args(["-fpie", "-no-pie", "-Wall", "-Wextra"])- .args(["-Wl,-Ttext-segment=0x8000400000", "-Wl,--build-id=none", "-z", "noexecstack"])+ .args(["-no-pie", "-Wall", "-Wextra"])+ .args(["-Wl,--build-id=none", "-z", "noexecstack"]) .arg("-o") .arg(out_dir.join(program)) .arg(user_dir.join(format!("{program}.c")))@@ -202,21 +205,16 @@ fn build_vdso(manifest: &Path, out_dir: &Path) { assert!(status.success(), "building the vDSO failed"); } -/// A C program linked with musl (stage 19).+/// A C program linked with musl (stage 19), always with the stock recipes. ///-/// StaticExec: the ELF type the stock recipe makes (ET_EXEC), but moved to-/// Rustbelt's user base (0x80_0000_0000; the kernel owns the low addresses)-/// with `-Ttext-segment`. Debian's musl start files are compiled as-/// position-independent code, which then needs three more tricks: musl's-/// own crt1.o instead of gcc's crtbegin (which can't reach a far-away-/// `__cxa_finalize`), `--no-relax`, and a dummy `_DYNAMIC` (crt1's `_start`-/// takes its address, which is 0 -- too far away -- in a static program).-/// Static PIEs need none of this, which is why stage 19 added them.-fn build_musl_program(user_dir: &Path, out_dir: &Path, program: &str, kind: Musl) {- let musl = Path::new("/usr/lib/x86_64-linux-musl");+/// (Stage 19 needed extra flags for StaticExec -- `-Ttext-segment`, musl's+/// own crt1.o, `--no-relax`, a fake `_DYNAMIC` -- because the kernel sat+/// at 0x200000 and user space started at 512 GiB. Stage 20 moved the+/// kernel out of the way, so the plain `musl-gcc -static` works.)+fn build_musl_program(user_dir: &Path, out_dir: &Path, program: &str, source: &str, kind: Musl) { let mut cmd = Command::new("musl-gcc"); cmd.args(["-O2", "-Wall", "-Wextra", "-pthread", "-Wl,--build-id=none"]);- let source = user_dir.join(format!("{program}.c"));+ let source = user_dir.join(format!("{source}.c")); match kind { Musl::StaticPie => { cmd.arg("-static-pie").arg(&source);@@ -225,12 +223,7 @@ fn build_musl_program(user_dir: &Path, out_dir: &Path, program: &str, kind: Musl cmd.args(["-fPIE", "-pie"]).arg(&source); } Musl::StaticExec => {- cmd.args(["-static", "-no-pie", "-nostartfiles"])- .args(["-Wl,-Ttext-segment=0x8000400000", "-Wl,--defsym=_DYNAMIC=0x8000400000", "-Wl,--no-relax"])- .arg(musl.join("crt1.o"))- .arg(musl.join("crti.o"))- .arg(&source)- .arg(musl.join("crtn.o"));+ cmd.arg("-static").arg(&source); } } let status = cmd@@ -292,7 +285,7 @@ fn build_disk(manifest: &Path, out_dir: &Path) { } // ...and the user programs to /bin. mtools("mmd", &["::/bin"]);- let musl = MUSL_PROGRAMS.iter().map(|(name, _)| name);+ let musl = MUSL_PROGRAMS.iter().map(|(name, _, _)| name); for program in PROGRAMS.iter().chain(C_PROGRAMS).chain(musl) { mtools("mcopy", &[out_dir.join(program).to_str().unwrap(), &format!("::/bin/{program}")]); }
@@ -88,7 +88,7 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then expect=(- "Rustbelt 0.19.0 (stage 19: vDSO and static PIE) x86_64" # ver+ "Rustbelt 0.20.0 (stage 20: gcc -static and timerfd) x86_64" # ver "[smp] 4 CPUs online" # smp.rs started 3 more cores "[fpu] " # XSAVE (-cpu max) or FXSAVE (qemu64) "B: /mnt/disk0 fat32" # drives@@ -159,7 +159,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then "[nbtest] all checks passed" "[fputest] all checks passed" # stage 15.1: musl prerequisites "[pathtest] all checks passed"- "rawabi: uname: Rustbelt 0.19.0" # a no-libc gcc binary+ "rawabi: uname: Rustbelt 0.20.0" # a no-libc gcc binary "rawabi: all checks passed" "sh: nosuchprogram: command not found" "cd: there is no drive Q:"@@ -169,11 +169,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then for line in "${expect[@]}"; do grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; } done- # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 44 (the programs),+ # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 47 (the programs), # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3, # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.- for n in 1 44 3 4 20000; do+ for n in 1 47 3 4 20000; do grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; } done # No memory leaks: once the shell and all ~60 programs it ran have@@ -184,7 +184,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1 fi # The other direction: the host fetched a page from httpd in the guest.- if grep -qF "Hello from Rustbelt! Rustbelt 0.19.0" "$CURL_OUT" 2>/dev/null; then+ if grep -qF "Hello from Rustbelt! Rustbelt 0.20.0" "$CURL_OUT" 2>/dev/null; then echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")" else echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1@@ -196,7 +196,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then # drop the '\r's before comparing whole lines. tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT" # The shell on hvc0 ran the host's commands and answered over hvc0.- for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.19.0 (stage 19" \+ for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.20.0 (stage 20" \ "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; } done
@@ -45,7 +45,7 @@ fputest | grep passed pathtest | grep passed rawabi | grep -v "ok " TXT- set -- "Rustbelt 0.19.0 (stage 19: vDSO and static PIE) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+ set -- "Rustbelt 0.20.0 (stage 20: gcc -static and timerfd) x86_64" "tile: 4 windows on 1024x768, console 61x46" \ "^C" "(interrupted: SIGINT)" "threads: 4 threads summed 1..=40000000 = 800000020000000" \ "echoserver: epoll: 3 clients, 9 echoes" "[threads] all checks passed" \ "[fputest] all checks passed" "[pathtest] all checks passed" \
@@ -157,10 +157,11 @@ pub struct LoadInfo { pub exec_stack: bool, } -/// Where a PIE goes: 4 MiB above the start of user space plus a random-/// number of pages (up to 1 GiB), so the heap after it and mmap from the-/// top still have hundreds of GiB between them.-const PIE_BASE: u64 = USER_START + 0x40_0000;+/// Where a PIE goes: 512 GiB + 4 MiB (the start of level-4 slot 1, where it+/// always went) plus a random number of pages (up to 1 GiB), so the heap+/// after it and mmap from the top still have hundreds of GiB between them,+/// and plain `gcc -static` programs at 0x400000 are far below it.+const PIE_BASE: u64 = 0x80_0040_0000; const PIE_RANDOM_PAGES: u64 = 0x4_0000; /// A random load bias for a PIE whose lowest segment starts at `low`.
@@ -229,6 +229,10 @@ pub trait File: Send + Sync { fn as_epoll(&self) -> Option<&crate::poll::Epoll> { None }+ /// Stage 20: timerfd_settime / timerfd_gettime need the timerfd.+ fn as_timerfd(&self) -> Option<&crate::timer::TimerFd> {+ None+ } /// fcntl(F_GETFL): the access mode it was opened with (O_RDONLY 0, /// O_WRONLY 1, O_RDWR 2) | O_APPEND. (O_NONBLOCK comes from /// `nonblocking`.) Files that don't know say O_RDWR.
@@ -31,10 +31,19 @@ use x86_64::{ }, }; -/// User space: level-4 slot 1, i.e. 512 GiB starting at 512 GiB.-pub const USER_START: u64 = 0x0000_0080_0000_0000;+/// User space: from 4 MiB (where `gcc -static` puts a program, 0x400000)+/// to 1 TiB, i.e. level-4 slots 0 and 1. The first 2 MiB of slot 0 stay the+/// kernel's (VGA text buffer at 0xb8000, the SMP trampoline at 0x8000, the+/// bootloader's tables): every address space shares that one kernel-only+/// table. 2..4 MiB is a gap, so a NULL pointer plus a big offset still faults.+pub const USER_START: u64 = 0x0000_0000_0040_0000; pub const USER_END: u64 = 0x0000_0100_0000_0000; // exclusive+/// The level-4 slots holding user space (slot 0 only partly, see above).+const USER_L4_SLOTS: [usize; 2] = [0, 1];+/// Slot 1 is user-only: the kernel must never map anything there. const USER_L4_INDEX: usize = 1;+/// End of the kernel's low identity-mapped area in slot 0 (one L2 entry).+const LOW_KERNEL_END: u64 = 0x20_0000; /// Where all physical memory is mapped (set by `init`). static PHYS_OFFSET: AtomicU64 = AtomicU64::new(0);@@ -298,13 +307,46 @@ pub fn new_address_space() -> Option<PhysFrame> { // The kernel must not use our user slot, or user mappings would end up // in page tables shared with the kernel (and every other process). assert!(kernel[USER_L4_INDEX].is_unused(), "kernel uses the user L4 slot");- for i in 0..512 {+ for i in 1..512 { new[i] = kernel[i].clone(); } }+ // Slot 0 is shared between the kernel (first 2 MiB) and the user (the+ // rest), so it gets its own level-3 and level-2 tables; only the kernel's+ // first level-2 entry (0..2 MiB, not USER_ACCESSIBLE) is copied in.+ let Some(l3) = alloc_zeroed_frame() else {+ free_frame(frame);+ return None;+ };+ let Some(l2) = alloc_zeroed_frame() else {+ free_frame(l3);+ free_frame(frame);+ return None;+ };+ table_mut(l2)[0] = kernel_low_l2_entry();+ table_mut(l3)[0].set_frame(l2, USER_TABLE_FLAGS);+ table_mut(frame)[0].set_frame(l3, USER_TABLE_FLAGS); Some(frame) } +/// The kernel's level-2 entry for 0..2 MiB. Checks that the kernel maps+/// nothing else in slot 0 (that part belongs to user programs now: the+/// kernel image itself is linked at 0x200_0000_0000, see the target JSON).+fn kernel_low_l2_entry() -> x86_64::structures::paging::page_table::PageTableEntry {+ let kernel = table_mut(kernel_l4_frame());+ let l3 = table_mut(kernel[0].frame().expect("kernel slot 0 has no level-3 table"));+ assert!(l3.iter().skip(1).all(|e| e.is_unused()), "kernel maps memory in user space (slot 0)");+ let l2 = table_mut(l3[0].frame().expect("kernel slot 0 has no level-2 table"));+ let low = (LOW_KERNEL_END >> 21) as usize;+ assert!(l2.iter().skip(low).all(|e| e.is_unused()), "kernel maps memory in user space (below 1 GiB)");+ l2[0].clone()+}++/// Where the 2 MiB covered by level-2 entry `[i4][i3][i2]` starts.+fn l2_base(i4: usize, i3: usize, i2: usize) -> u64 {+ ((i4 as u64) << 39) | ((i3 as u64) << 30) | ((i2 as u64) << 21)+}+ /// Free everything a process's address space owns: every user page's frame /// (or just our share of it, see `release_frame`), the page tables of the /// user slot (levels 3, 2, 1) and the level-4 table. The kernel entries@@ -314,6 +356,15 @@ pub fn new_address_space() -> Option<PhysFrame> { /// thread that ran in it must have finished). pub unsafe fn free_address_space(l4: PhysFrame) { clear_user_space(l4);+ // What clear_user_space keeps: slot 0's own level-3 and level-2 tables.+ let entry = &mut table_mut(l4)[0];+ if let Ok(l3) = entry.frame() {+ if let Ok(l2) = table_mut(l3)[0].frame() {+ free_frame(l2);+ }+ free_frame(l3);+ }+ entry.set_unused(); free_frame(l4); } @@ -326,24 +377,37 @@ fn table_mut(frame: PhysFrame) -> &'static mut PageTable { /// the running process: the caller must flush the TLB afterwards). The /// level-4 table itself stays. pub fn clear_user_space(l4: PhysFrame) {- // The used entries' frames (user space only has 4 KiB pages, no huge ones).- fn used(t: &'static mut PageTable) -> impl Iterator<Item = PhysFrame> {- t.iter().filter(|e| !e.is_unused()).filter_map(|e| e.frame().ok())- }- let user_l4_entry = &mut table_mut(l4)[USER_L4_INDEX];- if let Ok(l3) = user_l4_entry.frame() {- for l2 in used(table_mut(l3)) {- for l1 in used(table_mut(l2)) {- for page in used(table_mut(l1)) {- release_frame(page);+ // User space only has 4 KiB pages (no huge ones). In slot 0 the kernel's+ // low entry (0..2 MiB) is skipped, and slot 0's own level-3 table and+ // first level-2 table stay (free_address_space frees them).+ for i4 in USER_L4_SLOTS {+ let Ok(l3) = table_mut(l4)[i4].frame() else { continue };+ for (i3, e3) in table_mut(l3).iter_mut().enumerate().filter(|(_, e)| !e.is_unused()) {+ let Ok(l2) = e3.frame() else { continue };+ for (i2, e2) in table_mut(l2).iter_mut().enumerate().filter(|(_, e)| !e.is_unused()) {+ if l2_base(i4, i3, i2) < USER_START {+ continue; // the kernel's+ }+ if let Ok(l1) = e2.frame() {+ for e1 in table_mut(l1).iter().filter(|e| !e.is_unused()) {+ if let Ok(page) = e1.frame() {+ release_frame(page);+ }+ }+ free_frame(l1); }- free_frame(l1);+ e2.set_unused();+ }+ if (i4, i3) != (0, 0) {+ free_frame(l2);+ e3.set_unused(); }- free_frame(l2); }- free_frame(l3);+ if i4 != 0 {+ free_frame(l3);+ table_mut(l4)[i4].set_unused();+ } }- user_l4_entry.set_unused(); } // ---------------------------------------------------------------------------@@ -474,18 +538,23 @@ pub fn flush_user_page(addr: u64) { /// so the first write to them faults and gets its own copy. /// On failure (out of memory) `dst` may be partly filled: free it. pub fn clone_user_space(src: PhysFrame, dst: PhysFrame, mut flags: impl FnMut(u64, PageTableFlags) -> PageTableFlags) -> Option<()> {- let Ok(l3) = table_mut(src)[USER_L4_INDEX].frame() else { return Some(()) };- for (i3, e3) in table_mut(l3).iter().enumerate().filter(|(_, e)| !e.is_unused()) {- let l2 = e3.frame().ok()?;- for (i2, e2) in table_mut(l2).iter().enumerate().filter(|(_, e)| !e.is_unused()) {- let l1 = e2.frame().ok()?;- for (i1, e1) in table_mut(l1).iter_mut().enumerate().filter(|(_, e)| !e.is_unused()) {- let addr = USER_START + ((i3 as u64) << 30) + ((i2 as u64) << 21) + ((i1 as u64) << 12);- let frame = e1.frame().ok()?;- let new_flags = flags(addr, e1.flags());- e1.set_flags(new_flags);- map_user_frame(dst, addr, frame, new_flags)?;- share_frame(frame);+ for i4 in USER_L4_SLOTS {+ let Ok(l3) = table_mut(src)[i4].frame() else { continue };+ for (i3, e3) in table_mut(l3).iter().enumerate().filter(|(_, e)| !e.is_unused()) {+ let l2 = e3.frame().ok()?;+ for (i2, e2) in table_mut(l2).iter().enumerate().filter(|(_, e)| !e.is_unused()) {+ if l2_base(i4, i3, i2) < USER_START {+ continue; // the kernel's low 2 MiB: dst has it already+ }+ let l1 = e2.frame().ok()?;+ for (i1, e1) in table_mut(l1).iter_mut().enumerate().filter(|(_, e)| !e.is_unused()) {+ let addr = l2_base(i4, i3, i2) + ((i1 as u64) << 12);+ let frame = e1.frame().ok()?;+ let new_flags = flags(addr, e1.flags());+ e1.set_flags(new_flags);+ map_user_frame(dst, addr, frame, new_flags)?;+ share_frame(frame);+ } } } }
@@ -1439,7 +1439,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> { "Rustbelt", // sysname (`uname -s`) host.as_str(), // nodename (hostname; sethostname changes it) env!("CARGO_PKG_VERSION"), // release (`uname -r`)- "stage 19: vDSO and static PIE", // version+ "stage 20: gcc -static and timerfd", // version "x86_64", // machine domain.as_str(), // domainname ];
@@ -36,6 +36,8 @@ //! | 38 | setitimer | same, set (periodic too) | //! | 222 | timer_create | POSIX timers (timer.rs) | //! | 223-226 | timer_settime/gettime/getoverrun/delete | |+//! | 283 | timerfd_create | stage 20: a timer you read() (timer.rs) |+//! | 286/287 | timerfd_settime/gettime | arm / read it, like timer_settime | //! | 102/104/107/108 | getuid/getgid/geteuid/getegid | the stored ids | //! | 105/106 | setuid/setgid | change ids (root: any; others: their own) | //! | 113/114 | setreuid/setregid | |@@ -121,6 +123,10 @@ pub const SYS_GETTIMEOFDAY: u64 = 96; pub const SYS_TIME: u64 = 201; pub const SYS_GETCPU: u64 = 309; pub const SYS_MREMAP: u64 = 25;+// Stage 20: timers as files.+pub const SYS_TIMERFD_CREATE: u64 = 283;+pub const SYS_TIMERFD_SETTIME: u64 = 286;+pub const SYS_TIMERFD_GETTIME: u64 = 287; /// linkat: link what a symlink points to, not the symlink. pub const AT_SYMLINK_FOLLOW: u64 = 0x400;@@ -171,6 +177,9 @@ pub fn dispatch(nr: u64, a: [u64; 6]) -> Option<Result<u64, Errno>> { SYS_TIMER_GETTIME => sys_timer_gettime(a[0], a[1]), SYS_TIMER_GETOVERRUN => current().and_then(|p| crate::timer::getoverrun(p.pid, a[0] as i32)), SYS_TIMER_DELETE => current().and_then(|p| crate::timer::delete(p.pid, a[0] as i32)).map(|_| 0),+ SYS_TIMERFD_CREATE => sys_timerfd_create(a[0], a[1]),+ SYS_TIMERFD_SETTIME => sys_timerfd_settime(a[0], a[1], a[2], a[3]),+ SYS_TIMERFD_GETTIME => with_timerfd(a[0], |t| put_itimerspec(a[1], t.gettime())).map(|_| 0), // 8. Ids, priority, host name. SYS_GETUID => creds().map(|c| c.ruid as u64), SYS_GETEUID => creds().map(|c| c.euid as u64),@@ -245,6 +254,9 @@ pub fn name(nr: u64) -> Option<&'static str> { SYS_TIMER_GETTIME => "timer_gettime", SYS_TIMER_GETOVERRUN => "timer_getoverrun", SYS_TIMER_DELETE => "timer_delete",+ SYS_TIMERFD_CREATE => "timerfd_create",+ SYS_TIMERFD_SETTIME => "timerfd_settime",+ SYS_TIMERFD_GETTIME => "timerfd_gettime", SYS_CLOCK_GETRES => "clock_getres", SYS_GETTIMEOFDAY => "gettimeofday", SYS_TIME => "time",@@ -860,6 +872,42 @@ fn sys_timer_gettime(id: u64, ptr: u64) -> Result<u64, Errno> { Ok(0) } +// Stage 20: timerfd. The timer lives in timer.rs's table, like the others.+const TFD_NONBLOCK: u64 = 0o4000; // = O_NONBLOCK+const TFD_CLOEXEC: u64 = 0o2000000; // = O_CLOEXEC+const TFD_TIMER_ABSTIME: u64 = 1;+const TFD_TIMER_CANCEL_ON_SET: u64 = 2; // "tell me when the clock is set": not supported++/// timerfd_create(clock, flags). Clocks: REALTIME, MONOTONIC, BOOTTIME+/// (= MONOTONIC: we never suspend).+fn sys_timerfd_create(clock: u64, flags: u64) -> Result<u64, Errno> {+ use crate::timer::{CLOCK_BOOTTIME, CLOCK_MONOTONIC, CLOCK_REALTIME};+ if ![CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME].contains(&clock) || flags & !(TFD_NONBLOCK | TFD_CLOEXEC) != 0 {+ return Err(Errno::EINVAL);+ }+ let file = crate::timer::timerfd_new(clock, flags & TFD_NONBLOCK != 0);+ current()?.add_file_with(Arc::new(file), 0, flags & TFD_CLOEXEC != 0)+}++/// Run `f` on the timerfd behind `fd` (EBADF: no such fd; EINVAL: not a timerfd).+fn with_timerfd<T>(fd: u64, f: impl FnOnce(&crate::timer::TimerFd) -> Result<T, Errno>) -> Result<T, Errno> {+ let file = current()?.file(fd)?;+ f(file.as_timerfd().ok_or(Errno::EINVAL)?)+}++/// timerfd_settime(fd, flags, *new, *old): like timer_settime.+fn sys_timerfd_settime(fd: u64, flags: u64, new: u64, old: u64) -> Result<u64, Errno> {+ if flags & !(TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET) != 0 || flags & TFD_TIMER_CANCEL_ON_SET != 0 {+ return Err(Errno::EINVAL);+ }+ let (interval, value) = (ts_us(new, 0)?, ts_us(new, 1)?);+ let prev = with_timerfd(fd, |t| Ok(t.settime(flags & TFD_TIMER_ABSTIME != 0, value, interval)))?;+ if old != 0 {+ put_itimerspec(old, prev)?;+ }+ Ok(0)+}+ // --- 8. Ids, priority, host name ----------------------------------------- fn creds() -> Result<Creds, Errno> {
@@ -20,6 +20,14 @@ //! //! Lifetime, as on Linux: fork doesn't copy timers; execve deletes the //! POSIX timers but keeps the alarm / ITIMER_REAL; exit deletes them all.+//!+//! Stage 20: timerfd. A timer that is a *file*: instead of a signal, each+//! expiration adds 1 to a counter that `read` returns (and resets), and+//! poll/select/epoll see the fd as readable while the counter isn't 0. It+//! lives in the same table (owner `TIMERFD_PID`, kind `NOTIFY_TIMERFD`), is+//! fired by the same timerd, and goes away when its last fd is closed --+//! not at exec or exit, since an fd can outlive (or be passed to) a process.+//! Same ~55 ms resolution as the rest. use crate::errno::Errno; use crate::process;@@ -43,6 +51,10 @@ pub const CLOCK_BOOTTIME: u64 = 7; pub const SIGEV_SIGNAL: i32 = 0; // send sigev_signo to the process pub const SIGEV_NONE: i32 = 1; // nothing (the program polls timer_gettime) pub const SIGEV_THREAD_ID: i32 = 4; // send sigev_signo to thread sigev_tid+/// Ours, not Linux's: "count it in a timerfd" (user space can't ask for it).+const NOTIFY_TIMERFD: i32 = -1;+/// The owner of every timerfd's table entry (no real process has this pid).+const TIMERFD_PID: u64 = u64::MAX; /// At most this many POSIX timers per process: EAGAIN after that. const MAX_TIMERS: usize = 32;@@ -66,6 +78,7 @@ struct Timer { sigval: u64, /// Expirations counted since the signal was sent, while it is still /// pending (stage 18 fix: this *adds up*; it used to be overwritten).+ /// For a timerfd: expirations since the last read. overrun: u64, /// What timer_getoverrun reports: `overrun` as it was when the last /// signal was delivered (0 before the first delivery), like Linux.@@ -129,6 +142,16 @@ fn timerd() { // (blocked, say) doesn't send another: like Linux, it counts // the expirations as overruns, which the delivery reports. // (Checked outside the table lock: signal locks come first.)+ if timer.notify == NOTIFY_TIMERFD {+ with_table(|t| {+ if let Some(x) = t.iter_mut().find(|x| x.pid == timer.pid && x.id == timer.id) {+ x.overrun = x.overrun.saturating_add(1 + missed);+ }+ });+ TIMERFD_WAIT.wake_all();+ crate::poll::notify();+ continue;+ } let posix = timer.id != ITIMER_ID && timer.notify != SIGEV_NONE; let still_pending = posix && process::get(timer.pid).is_some_and(|p| signal::timer_pending(&p, timer.signo, timer.id)); let send = with_table(|t| {@@ -324,3 +347,98 @@ pub fn process_exited(pid: u64) { pub fn count() -> usize { with_table(|t| t.len()) }++// --- timerfd (stage 20) ---------------------------------------------------++use crate::fs::file::{File, Stat};+use core::sync::atomic::AtomicI32;++/// Readers of every timerfd sleep here (one queue for all: timerfds are+/// few, and a reader that wakes for another fd's timer just sleeps again).+static TIMERFD_WAIT: WaitQueue = WaitQueue::new();++/// An open timerfd. Its timer is the table entry (TIMERFD_PID, `id`).+pub struct TimerFd {+ id: i32,+ nonblocking: AtomicBool,+}++/// timerfd_create: a new, disarmed timer on `clock` (already checked).+pub fn timerfd_new(clock: u64, nonblocking: bool) -> TimerFd {+ static NEXT_ID: AtomicI32 = AtomicI32::new(0);+ let id = NEXT_ID.fetch_add(1, Ordering::Relaxed) & i32::MAX; // never ITIMER_ID+ let notify = NOTIFY_TIMERFD;+ with_table(|t| t.push(Timer { pid: TIMERFD_PID, id, clock, next: None, interval: 0, signo: 0, notify, tid: 0, sigval: 0, overrun: 0, overrun_last: 0 }));+ TimerFd { id, nonblocking: AtomicBool::new(nonblocking) }+}++impl TimerFd {+ /// timerfd_settime: like timer_settime (this also zeroes the count).+ pub fn settime(&self, absolute: bool, value: u64, interval: u64) -> (u64, u64) {+ let old = settime(TIMERFD_PID, self.id, absolute, value, interval).unwrap_or((0, 0));+ crate::poll::notify(); // the count went back to 0: not readable any more+ old+ }+ /// timerfd_gettime: (time left, interval) in microseconds.+ pub fn gettime(&self) -> (u64, u64) {+ gettime(TIMERFD_PID, self.id).unwrap_or((0, 0))+ }+ /// Expirations since the last read (without resetting them).+ fn count(&self) -> u64 {+ with_table(|t| t.iter().find(|x| x.pid == TIMERFD_PID && x.id == self.id).map_or(0, |x| x.overrun))+ }+}++impl Drop for TimerFd {+ /// The last fd is closed: the timer goes too.+ fn drop(&mut self) {+ let _ = delete(TIMERFD_PID, self.id);+ }+}++impl File for TimerFd {+ /// An 8-byte count of expirations since the last read, then back to 0.+ /// None yet: wait for one (EAGAIN if non-blocking).+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ if buf.len() < 8 {+ return Err(Errno::EINVAL);+ }+ let nonblocking = self.nonblocking();+ let count = TIMERFD_WAIT.wait_until_intr(|| {+ let n = with_table(|t| {+ let x = t.iter_mut().find(|x| x.pid == TIMERFD_PID && x.id == self.id)?;+ Some(core::mem::take(&mut x.overrun))+ })+ .unwrap_or(0);+ match n {+ 0 if nonblocking => Some(Err(Errno::EAGAIN)),+ 0 => None,+ n => Some(Ok(n)),+ }+ })??;+ buf[..8].copy_from_slice(&count.to_le_bytes());+ crate::poll::notify();+ Ok(8)+ }+ fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {+ Err(Errno::EINVAL)+ }+ fn stat(&self) -> Result<Stat, Errno> {+ Ok(Stat { kind: crate::fs::file::KIND_DEVICE, size: 0, mode: 0o600 })+ }+ fn poll(&self) -> u16 {+ if self.count() > 0 { crate::poll::POLLIN } else { 0 }+ }+ fn set_nonblocking(&self, on: bool) {+ self.nonblocking.store(on, Ordering::Relaxed);+ }+ fn nonblocking(&self) -> bool {+ self.nonblocking.load(Ordering::Relaxed)+ }+ fn status_flags(&self) -> u64 {+ 2 // O_RDWR, like Linux's anon inode+ }+ fn as_timerfd(&self) -> Option<&TimerFd> {+ Some(self)+ }+}
@@ -176,7 +176,7 @@ fn scripted_shell_session() { "piped text\n", "wc -l\n2\n", "saved\nmore\n",- "Rustbelt 0.19.0",+ "Rustbelt 0.20.0", "B:/ $ exit 3\n", ] { assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -99,10 +99,10 @@ fn dynamic_program_is_enoexec() { } /// The loader still takes the classic ET_EXEC files (Rust programs, the-/// no-libc C program, a musl ET_EXEC), and checks PIEs at their base.+/// no-libc C program, musl ET_EXECs), and checks PIEs at their base. #[test_case] fn exec_files_still_load() {- for name in ["hello", "rawabi", "vdsotest"] {+ for name in ["hello", "rawabi", "vdsotest", "exechello", "execthreads"] { let image = fs::read_all(&alloc::format!("/mnt/disk0/bin/{}", name)).unwrap(); assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "{} is not ET_EXEC", name); assert!(elf::check(&image).is_ok(), "{} doesn't load", name);@@ -114,6 +114,35 @@ fn exec_files_still_load() { assert_eq!(run("rawabi", &["/tmp"]).0, ExitStatus::Exited(0)); } +/// Stage 20: stock `musl-gcc -static` (ET_EXEC at 0x400000, no special+/// flags) runs; its brk and mmap memory stay out of the image.+#[test_case]+fn stock_static_exec_hello() {+ let image = fs::read_all("/mnt/disk0/bin/exechello").unwrap();+ assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "not ET_EXEC");+ assert!(elf::check(&image).unwrap() < 0x50_0000, "not linked at 0x400000");+ let before = counts();+ let (status, out) = run("exechello", &[]);+ assert!(out.starts_with("hello from a plain static executable\n"), "output: {}", out);+ assert!(out.contains("exechello: 0 failed"), "output: {}", out);+ assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);+ assert_eq!(counts(), before, "exechello leaked something");+}++/// Stage 20: the pthreads + stdio program again, built as a stock ET_EXEC.+#[test_case]+fn stock_static_exec_threads_and_stdio() {+ let image = fs::read_all("/mnt/disk0/bin/execthreads").unwrap();+ assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "not ET_EXEC");+ let (status, out) = run("execthreads", &[]);+ assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);+ let before = counts();+ let (status, out) = run("execthreads", &[]);+ assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);+ assert!(out.contains("piethreads: 0 failed"));+ assert_eq!(counts(), before, "execthreads leaked something");+}+ #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info)
@@ -0,0 +1,90 @@+//! Stage 20: timerfd (timerfd_create / settime / gettime), checked by a+//! musl program (user/timerfdtest.c): one-shot, interval counts adding up,+//! non-blocking EAGAIN, poll/select/epoll wake-ups, and closing timerfds+//! leaves no timer and no memory behind.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use alloc::sync::Arc;+use alloc::vec::Vec;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::fs::file::File;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, thread, timer};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rustbelt::init();+ rustbelt::init_memory(boot_info);+ rustbelt::init_smp();+ fs::init();+ rustbelt::init_storage();+ test_main();+ rustbelt::hlt_loop();+}++/// Run /mnt/disk0/bin/`name` with `args`; its exit status and stdout.+fn run(name: &str, args: &[&str]) -> (ExitStatus, String) {+ let (reader, writer) = fs::pipe::new();+ let mut spawn = SpawnArgs::kernel(name);+ spawn.args.extend(args.iter().map(|a| String::from(*a)));+ spawn.stdio[1] = Arc::new(writer);+ let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+ let mut out = Vec::new();+ let mut buf = [0u8; 256];+ loop {+ match reader.read(&mut buf).unwrap() {+ 0 => break,+ n => out.extend_from_slice(&buf[..n]),+ }+ }+ (process::wait(&p), String::from_utf8_lossy(&out).into_owned())+}++fn counts() -> (usize, usize, usize) {+ thread::sleep_ticks(3);+ (thread::thread_count(), process::count(), memory::frames_in_use())+}++/// Everything in timerfdtest passes, twice; the second run leaves exactly+/// what was there before (the first one starts timerd, a kernel thread).+#[test_case]+fn timerfd_program_passes() {+ let (status, out) = run("timerfdtest", &[]);+ assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);+ assert!(out.contains("timerfdtest: 0 failed"), "output: {}", out);+ let (timers, before) = (timer::count(), counts());+ let (status, out) = run("timerfdtest", &[]);+ assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);+ assert_eq!(timer::count(), timers, "a closed timerfd left its timer behind");+ assert_eq!(counts(), before, "timerfdtest leaked something");+}++/// Kernel side: a timerfd's timer lives exactly as long as the file.+#[test_case]+fn timerfd_timer_goes_with_the_file() {+ let before = timer::count();+ let fd = timer::timerfd_new(timer::CLOCK_MONOTONIC, true);+ assert_eq!(timer::count(), before + 1);+ fd.settime(false, 60_000, 60_000);+ let mut buf = [0u8; 8];+ assert_eq!(fd.read(&mut buf[..4]), Err(rustbelt::errno::Errno::EINVAL));+ thread::sleep_ticks(6); // ~330 ms: several periods+ assert_eq!(fd.read(&mut buf), Ok(8));+ assert!(u64::from_le_bytes(buf) >= 2, "count {}", u64::from_le_bytes(buf));+ drop(fd);+ assert_eq!(timer::count(), before);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rustbelt::test_panic_handler(info)+}
@@ -9,7 +9,7 @@ mod ulib; fn main() -> i64 {- let kernel_code = 0x20_0000 as *const u64; // where the kernel is loaded+ let kernel_code = 0x200_0000_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);
@@ -0,0 +1,74 @@+/*+ * exechello.c -- stage 20: "hello" built with the stock recipe,+ * `musl-gcc -static` and no other flags. That makes a classic ET_EXEC+ * file linked at 0x400000 (4 MiB), the address every Linux linker uses.+ * Until stage 20 the kernel itself lived down there; now it doesn't.+ *+ * Checks: we really are at 0x400000, and the heap (brk) and mmap memory+ * land outside our image. Exit code = number of failed checks.+ */+#define _GNU_SOURCE+#include <elf.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/auxv.h>+#include <sys/mman.h>+#include <sys/syscall.h>+#include <unistd.h>++static int fails;+#define CHECK(cond, what) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("exechello: FAIL %s\n", what); \+ } \+ } while (0)++extern const Elf64_Ehdr __ehdr_start; /* first byte of the image */+extern char _end[]; /* one past the last byte (.bss end) */+extern char _start[];++/* Does [p, p + len) overlap our image? */+static int in_image(const void *p, unsigned long len)+{+ unsigned long a = (unsigned long)p, lo = (unsigned long)&__ehdr_start;+ return a < (unsigned long)_end && a + len > lo;+}++int main(void)+{+ puts("hello from a plain static executable");+ CHECK((unsigned long)&__ehdr_start == 0x400000, "image starts at 0x400000");+ CHECK(__ehdr_start.e_type == ET_EXEC, "ET_EXEC file");+ CHECK(getauxval(AT_ENTRY) == (unsigned long)_start, "AT_ENTRY is _start");+ CHECK(getauxval(AT_PHDR) == 0x400000 + __ehdr_start.e_phoff, "AT_PHDR");++ /* brk: starts at or after the image's end and grows from there.+ * (musl's sbrk() only takes 0, so grow it with the raw syscall.) */+ char *brk0 = sbrk(0);+ CHECK(brk0 != (char *)-1 && brk0 >= _end, "brk starts after the image");+ char *more = brk0;+ CHECK((char *)syscall(SYS_brk, brk0 + 64 * 1024) == brk0 + 64 * 1024, "brk grows by 64 KiB");+ {+ memset(more, 0x5a, 64 * 1024); /* writable, no fault */+ CHECK(!in_image(more, 64 * 1024), "brk memory outside the image");+ }++ /* mmap and malloc (small: musl's heap; big: its own mmap). */+ void *m = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ CHECK(m != MAP_FAILED && !in_image(m, 1 << 20), "mmap outside the image");+ CHECK(m == MAP_FAILED || (char *)m > brk0 + 64 * 1024, "mmap above the heap");+ for (int i = 0; i < 64; i++) {+ size_t n = i % 2 ? 40 : 300 * 1024;+ char *p = malloc(n);+ CHECK(p && !in_image(p, n), "malloc outside the image");+ if (p)+ memset(p, i, n);+ free(p);+ }+ printf("image %p..%p, brk %p, mmap %p\n", (void *)&__ehdr_start, (void *)_end, (void *)brk0, m);+ printf("exechello: %d failed\n", fails);+ return fails;+}
@@ -45,8 +45,8 @@ fn main() -> i64 { 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);+ // Ask the kernel to print *kernel* memory for us (0x200_0000_0000 = kernel code).+ let r = syscall3(SYS_WRITE, 1, 0x200_0000_0000, 16); check(&mut failures, r == -EFAULT, "kernel pointer -> EFAULT"); check(&mut failures, syscall3(999, 0, 0, 0) == -38, "unknown syscall -> ENOSYS");
@@ -137,7 +137,7 @@ fn main() -> i64 { c.check(n > 0 && cwd[0] == b'/', "getcwd gives an absolute path"); let mut ub = [0u8; 6 * 65]; let u = uname(&mut ub);- c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 19"), "uname: Rustbelt, stage 19");+ c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 20"), "uname: Rustbelt, stage 20"); let (secs, _) = clock_gettime(0); c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");
@@ -2,6 +2,10 @@ * 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.+ *+ * Stage 20 builds the same file a second time as `execthreads`, with the+ * stock `musl-gcc -static` (a plain ET_EXEC at 0x400000), and both check+ * that the heap and the thread stacks (mmap) stay out of the image. */ #define _GNU_SOURCE #include <errno.h>@@ -35,8 +39,21 @@ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static long shared; static FILE *out; +extern const char __ehdr_start[]; /* first byte of the image */+extern char _end[]; /* one past the last byte */++/* Does [p, p + len) overlap our image? */+static int in_image(const void *p, unsigned long len)+{+ unsigned long a = (unsigned long)p;+ return a < (unsigned long)_end && a + len > (unsigned long)__ehdr_start;+}+ static void *worker(void *arg) {+ int on_stack;+ if (in_image(&on_stack, sizeof on_stack))+ return (void *)1; /* the thread's stack (mmap) is inside the image */ long id = (long)arg; if (mine != 7 || scratch[0] != 0) return (void *)1; /* every thread starts with a fresh TLS copy */@@ -59,6 +76,10 @@ static void threads(void) const char *path = "/tmp/piethreads.txt"; out = fopen(path, "w"); CHECK(out != NULL, "fopen: errno %d", errno);+ CHECK(!in_image(out, 1), "FILE (malloc) inside the image");+ void *big = malloc(1 << 20); /* big: musl mmaps it */+ CHECK(big && !in_image(big, 1 << 20), "malloc(1 MiB) inside the image");+ free(big); if (!out) return; pthread_t t[THREADS];@@ -67,7 +88,7 @@ static void threads(void) for (int i = 0; i < THREADS; i++) { void *r; pthread_join(t[i], &r);- CHECK(r == NULL, "thread %d saw wrong TLS", i);+ CHECK(r == NULL, "thread %d saw wrong TLS or its stack in the image", i); } CHECK(shared == (long)THREADS * ROUNDS, "counter %ld", shared); CHECK(mine == 7, "main thread's TLS changed");
@@ -8,10 +8,11 @@ * 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.+ * cc -O2 -static -nostdlib -ffreestanding -fno-stack-protector -no-pie+ * -o rawabi rawabi.c+ * An ordinary ET_EXEC file at the usual 0x400000. (Until stage 20 it+ * needed -fpie and -Ttext-segment=0x8000400000: user space started at+ * 512 GiB back then.) * * Run: rawabi [scratch-dir] (default /tmp). Exit status = failed checks. */
@@ -0,0 +1,191 @@+/*+ * timerfdtest.c -- stage 20: timerfd, a timer you read() like a file.+ * Built with the stock `musl-gcc -static` (a plain ET_EXEC).+ *+ * Each expiration adds 1 to a counter; read() returns it as 8 bytes and+ * resets it. poll/select/epoll say "readable" while it isn't 0. Rustbelt's+ * timers tick every ~55 ms, so the checks leave room for that.+ *+ * Exit code = number of failed checks.+ */+#define _GNU_SOURCE+#include <errno.h>+#include <poll.h>+#include <stdint.h>+#include <stdio.h>+#include <string.h>+#include <sys/epoll.h>+#include <sys/select.h>+#include <sys/timerfd.h>+#include <sys/utsname.h>+#include <time.h>+#include <unistd.h>++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("timerfdtest: FAIL "); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++static long long now_ms(void)+{+ struct timespec ts;+ clock_gettime(CLOCK_MONOTONIC, &ts);+ return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000;+}++static void ms_sleep(long ms)+{+ struct timespec ts = { ms / 1000, ms % 1000 * 1000000L };+ nanosleep(&ts, NULL);+}++/* Arm `fd`: first expiry after `ms`, then every `every_ms` (0 = once). */+static int arm(int fd, long ms, long every_ms)+{+ struct itimerspec its = { { every_ms / 1000, every_ms % 1000 * 1000000L }, { ms / 1000, ms % 1000 * 1000000L } };+ return timerfd_settime(fd, 0, &its, NULL);+}++static void one_shot(void)+{+ int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);+ CHECK(fd >= 0, "timerfd_create: errno %d", errno);+ long long t0 = now_ms();+ CHECK(arm(fd, 150, 0) == 0, "settime: errno %d", errno);+ struct itimerspec cur;+ CHECK(timerfd_gettime(fd, &cur) == 0 && cur.it_value.tv_nsec + cur.it_value.tv_sec > 0, "gettime while armed");+ uint64_t n = 0;+ CHECK(read(fd, &n, sizeof n) == 8, "blocking read: errno %d", errno); /* waits for it */+ long long took = now_ms() - t0;+ CHECK(n == 1, "one-shot count %llu", (unsigned long long)n);+ CHECK(took >= 140 && took < 1000, "one-shot fired after %lld ms", took);+ CHECK(timerfd_gettime(fd, &cur) == 0 && cur.it_value.tv_sec == 0 && cur.it_value.tv_nsec == 0, "disarmed after firing");+ close(fd);+}++static void interval(void)+{+ int fd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);+ CHECK(fd >= 0, "timerfd_create(BOOTTIME): errno %d", errno);+ CHECK(arm(fd, 60, 60) == 0, "settime interval");+ struct itimerspec cur;+ timerfd_gettime(fd, &cur);+ CHECK(cur.it_interval.tv_sec == 0 && cur.it_interval.tv_nsec == 60000000, "gettime interval %ld", cur.it_interval.tv_nsec);+ ms_sleep(700); /* ~11 periods go by without a read: they add up */+ uint64_t n = 0;+ CHECK(read(fd, &n, sizeof n) == 8, "read interval: errno %d", errno);+ CHECK(n >= 5 && n <= 14, "interval count %llu after 700 ms of 60 ms periods", (unsigned long long)n);+ /* Read reset it; the next expirations count from 0 again. */+ ms_sleep(250);+ uint64_t m = 0;+ CHECK(read(fd, &m, sizeof m) == 8 && m >= 1 && m <= 7, "count after reset %llu", (unsigned long long)m);+ /* Disarm: no more expirations. */+ struct itimerspec off = { 0 };+ CHECK(timerfd_settime(fd, 0, &off, &cur) == 0 && cur.it_interval.tv_nsec == 60000000, "disarm returns old");+ ms_sleep(150);+ CHECK(read(fd, &m, sizeof m) == -1 && errno == EAGAIN, "disarmed: EAGAIN");+ close(fd);+}++static void nonblocking_and_errors(void)+{+ int fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC);+ CHECK(fd >= 0, "timerfd_create(REALTIME)");+ uint64_t n;+ CHECK(read(fd, &n, sizeof n) == -1 && errno == EAGAIN, "disarmed read: EAGAIN (errno %d)", errno);+ arm(fd, 5000, 0);+ CHECK(read(fd, &n, sizeof n) == -1 && errno == EAGAIN, "not yet expired: EAGAIN");+ char small[4];+ CHECK(read(fd, small, sizeof small) == -1 && errno == EINVAL, "4-byte buffer: EINVAL (errno %d)", errno);+ CHECK(write(fd, &n, sizeof n) == -1 && errno == EINVAL, "write: EINVAL");+ /* TFD_TIMER_ABSTIME on CLOCK_REALTIME: a wall-clock time 120 ms ahead. */+ struct timespec wall;+ clock_gettime(CLOCK_REALTIME, &wall);+ wall.tv_nsec += 120000000;+ if (wall.tv_nsec >= 1000000000) {+ wall.tv_sec++;+ wall.tv_nsec -= 1000000000;+ }+ struct itimerspec abs = { { 0, 0 }, wall };+ CHECK(timerfd_settime(fd, TFD_TIMER_ABSTIME, &abs, NULL) == 0, "ABSTIME settime");+ struct pollfd p = { fd, POLLIN, 0 };+ CHECK(poll(&p, 1, 2000) == 1 && (p.revents & POLLIN), "poll: ABSTIME timer fired");+ CHECK(read(fd, &n, sizeof n) == 8 && n == 1, "ABSTIME count");+ CHECK(poll(&p, 1, 0) == 0, "poll: not readable after read");+ /* Unsupported / bad arguments. */+ /* CANCEL_ON_SET ("wake me if someone sets the clock"): Linux has it,+ * Rustbelt says EINVAL. (uname's version is "stage N: ..." on Rustbelt.) */+ struct utsname u;+ int rustbelt = uname(&u) == 0 && strncmp(u.version, "stage", 5) == 0;+ int r = timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &abs, NULL);+ CHECK(rustbelt ? r == -1 && errno == EINVAL : r == 0, "CANCEL_ON_SET: r %d errno %d", r, errno);+ CHECK(timerfd_create(CLOCK_PROCESS_CPUTIME_ID, 0) == -1 && errno == EINVAL, "bad clock: EINVAL");+ CHECK(timerfd_create(CLOCK_MONOTONIC, 1) == -1 && errno == EINVAL, "bad flags: EINVAL");+ struct itimerspec bad = { { 0, 0 }, { 0, 1000000000 } };+ CHECK(timerfd_settime(fd, 0, &bad, NULL) == -1 && errno == EINVAL, "tv_nsec too big: EINVAL");+ struct itimerspec cur;+ CHECK(timerfd_gettime(1, &cur) == -1 && errno == EINVAL, "not a timerfd: EINVAL");+ CHECK(timerfd_gettime(999, &cur) == -1 && errno == EBADF, "bad fd: EBADF");+ close(fd);+}++static void readiness(void)+{+ int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);+ int ep = epoll_create1(EPOLL_CLOEXEC);+ struct epoll_event ev = { .events = EPOLLIN, .data.u64 = 0x1234 };+ CHECK(epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev) == 0, "epoll_ctl ADD: errno %d", errno);+ struct epoll_event out;+ CHECK(epoll_wait(ep, &out, 1, 0) == 0, "epoll: nothing yet");+ long long t0 = now_ms();+ arm(fd, 100, 0);+ int r = epoll_wait(ep, &out, 1, 3000); /* sleeps until the timer wakes it */+ long long took = now_ms() - t0;+ CHECK(r == 1 && out.data.u64 == 0x1234 && (out.events & EPOLLIN), "epoll_wait woke: r %d", r);+ CHECK(took >= 90 && took < 1500, "epoll woke after %lld ms", took);+ uint64_t n = 0;+ CHECK(read(fd, &n, sizeof n) == 8 && n == 1, "count after epoll");+ CHECK(epoll_wait(ep, &out, 1, 0) == 0, "epoll: drained");+ /* select too. */+ arm(fd, 80, 0);+ fd_set set;+ FD_ZERO(&set);+ FD_SET(fd, &set);+ struct timeval tv = { 2, 0 };+ CHECK(select(fd + 1, &set, NULL, NULL, &tv) == 1 && FD_ISSET(fd, &set), "select woke");+ read(fd, &n, sizeof n);+ close(ep);+ close(fd);+}++/* Lots of armed timerfds, closed (and some left open: exit closes them).+ * The kernel test checks no timer or memory is left behind. */+static void many(void)+{+ int fds[40];+ for (int i = 0; i < 40; i++) {+ fds[i] = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);+ CHECK(fds[i] >= 0, "timerfd %d", i);+ arm(fds[i], 10 + i, 20);+ }+ ms_sleep(120);+ for (int i = 0; i < 30; i++)+ close(fds[i]);+}++int main(void)+{+ one_shot();+ interval();+ nonblocking_and_errors();+ readiness();+ many();+ printf("timerfdtest: %d failed\n", fails);+ return fails;+}
@@ -9,6 +9,7 @@ "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld",+ "pre-link-args": { "ld.lld": ["--image-base=0x20000000000"] }, "panic-strategy": "abort", "disable-redzone": true, "stack-probes": { "kind": "inline" },