Skip to content

mohnkhan/MyOS2026

MyOS2026 — VM-First Operating System in Rust

CI

A modern, minimal, secure operating system designed specifically for virtual machines — fast boot, reproducible images, strong security defaults, and a full Unix utility layer. Written entirely in Rust.


At a Glance

Target Goal Result
Boot time < 2 s (BIOS, headless QEMU) 1.79–1.93 s
Idle RAM < 256 MB < 256 MB ✅
Base image < 1 GB compressed QCOW2 12.5 MB
SSH ready < 5 s after shell prompt < 1 s ✅
Integration tests 100% pass 10/10 smoke suite + 31-program syscall-diff corpus
Unit tests All pass 456/456 kernel + 17/17 dwarf-extractor + 134/134 mymc (cargo test --lib) ✅
File manager Dual-pane Rust TUI shipped on the image /bin/mymc 2.05 MiB stripped — copy/move/delete/mkdir + resumable transfers + previewer + fuzzy filter + history (Feature 070) ✅
/proc entries Linux-compatible 15 virtual files + /proc/audit/{data,stats} + per-PID /proc/<pid>/{trace,stack,wchan,status}status shows real Uid:/Gid:/Groups: lines (Feature 071) ✅
Credential audit log Every privilege transition recorded dmesg | grep AUDT (Stage 1) + /proc/audit/data binary stream + /proc/audit/stats KV counters (Stage 2) — every set{u,g,res,fs}{uid,gid} syscall (success + EPERM + EINVAL) is recorded; 112-byte fixed AuditRecord with seq + drop counter (Features 072 + 073) ✅
Memory-safety diagnostics KASAN + FASAN Both default-on for debug builds (heap redzones + frame poisoning/ownership) ✅
Panic backtrace Source-line annotated In-kernel DWARF lookup — every frame shows at <file>:<line> (Feature 066) ✅
Networking userland DNS, HTTP, nc, ping 5/5 tests pass
Reproducible builds Identical SHA-256
Verified boot BLAKE2b hash chain

All 11 success criteria (SC-001–SC-011) pass. See VALIDATION.md.


Demo

MyOS2026 shell demo

nsh$ prompt with mybox applets, pipe chains, and standard utilities — captured via make screenshot.

Animated terminal demo

Real nsh session over SSH — uname, /proc/meminfo, /proc/cpuinfo, ps, a base64 pipe, and the colored [1] prompt that appears after a failed command. Generated via make demo-gif (paramiko + compound nsh -c, cast trimmed to remove SSH-negotiation dead time). See Feature 062 below.


What's Inside

A complete, self-contained OS stack:

+-------------------------------------------------------+
|                    User Space                         |
|  init | nsh | myos-pkg | cloud-init | dropbear        |
|  mybox (97 applets) | sandbox | exploit-test          |
|  /proc/self/{maps,fd/,status,exe} | /proc/{cpuinfo,   |
|  uptime,net/dev,net/tcp} | /proc/[pid]/…             |
+-------------------------------------------------------+
|                  Security Layer                       |
|  Per-process syscall allowlist (SYS_SANDBOX_ENTER)    |
|  Capability tokens (CAP_FS_ADMIN, CAP_NET_BIND, …)    |
|  Verified boot (BLAKE2b → ed25519 attestation)        |
+-------------------------------------------------------+
|                  System Layer                         |
|  VFS (symlink-following) | Syscall dispatch | Pipes   |
|  IPC | MLFQ scheduler | Linux ELF binary compatibility  |
+-------------------------------------------------------+
|                    Kernel                             |
|  MM (demand paging) | Interrupts (APIC/HPET)          |
|  smoltcp 0.11 | DHCP | ext2 (read/write) | firewall   |
|  procfs (12 virtual files, Linux-compatible)           |
+-------------------------------------------------------+
|                VirtIO Drivers                         |
|  blk | net | console | rng | scsi (VirtualBox compat) |
+-------------------------------------------------------+
|               Virtual Hardware                        |
|  QEMU q35 (primary) | VirtualBox (secondary)          |
+-------------------------------------------------------+

Quick Start

# Prerequisites
apt install qemu-system-x86 ovmf sgdisk mtools e2fsprogs qemu-utils nasm python3
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
rustup target add x86_64-unknown-linux-musl

# Build and boot (< 2s to shell prompt)
RELEASE=1 bash build/scripts/assemble-image.sh myos.qcow2
make qemu

💾 Spare your SSD: make tmpfs-setup redirects target/ and dist/ (the only large gitignored output trees) into /tmp/MyOS/<hash>/ so the write-heavy build cycle hits RAM. Reversible (make tmpfs-teardown); idempotent; opt-in; no-op on CI. See docs/dev-tmpfs.md.

Interactive session (recommended)

Boot the VM in a graphical window showing the kernel framebuffer terminal:

make qemu-sdl                    # opens SDL window; QEMU monitor via Ctrl+Alt+2

SSH in simultaneously on port 2222:

ssh -p 2222 -i tests/keys/test_id_ed25519 \
  -o StrictHostKeyChecking=no root@127.0.0.1

Headless SSH session

qemu-system-x86_64 -machine q35 -cpu qemu64 -smp 2 -m 256M \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive file=myos.qcow2,format=qcow2,if=virtio \
  -netdev user,id=net0,hostfwd=tcp::2222-:22 \
  -device virtio-net-pci,netdev=net0 -device virtio-rng-pci \
  -serial stdio -display none &

ssh -p 2222 -i tests/keys/test_id_ed25519 \
  -o StrictHostKeyChecking=no root@127.0.0.1

Running in VirtualBox

MyOS2026 boots in VirtualBox using the default hardware profile — no custom settings required. The kernel includes a native LSI Logic MPT SCSI driver and an Intel E1000 NIC driver.

Import and boot:

# Convert QCOW2 → VDI (VirtualBox native format)
qemu-img convert -f qcow2 -O vdi myos.qcow2 myos.vdi

# Create VM via VBoxManage (or use the GUI)
VBoxManage createvm --name MyOS2026 --ostype Linux_64 --register
VBoxManage modifyvm MyOS2026 --memory 256 --cpus 1 --nic1 nat
VBoxManage modifyvm MyOS2026 --firmware efi --graphicscontroller vmsvga

# Add SCSI controller (LSI Logic — the VirtualBox default)
VBoxManage storagectl MyOS2026 --name SCSI --add scsi --controller LsiLogic
VBoxManage storageattach MyOS2026 --storagectl SCSI --port 0 --device 0 \
  --type hdd --medium myos.vdi

# Start (headless)
VBoxManage startvm MyOS2026 --type headless

# SSH in (once nsh$ appears in VBoxManage guestcontrol or serial log)
ssh -p 22 -i tests/keys/test_id_ed25519 -o StrictHostKeyChecking=no root@<guest-ip>

VirtualBox hardware mapping:

VirtualBox device Kernel driver Notes
LsiLogic SCSI drivers::lsi_scsi MPT 1.x protocol; auto-detected
Intel E1000 (NIC1) drivers::e1000 Default VirtualBox NIC; MAC from RA[0]
EFI firmware Limine UEFI loader Use --firmware efi

The kernel auto-detects which block device is present (root= cmdline override available for explicit selection: root=lsi-scsi, root=virtio-scsi, root=virtio-blk).

Generate documentation artifacts

# Styled PNG screenshot → docs/screenshots/demo.png
# Requires: cargo install silicon
make screenshot

# Animated GIF demo → docs/demo.gif
# Requires: pip install asciinema && (download agg from github.com/asciinema/agg/releases)
make demo-gif

Once in, the full Unix utility set is available via mybox:

nsh$ ls /bin | wc -l        # 91+ binaries (mybox applets + sh + test binaries)
nsh$ cat /etc/hostname | grep -c .
1
nsh$ ps | head -5
nsh$ echo hello | grep hello
hello

Feature Highlights

mybox — Busybox-in-Rust (91 applets)

A multi-call binary providing 91 Unix applets via symlinks in /bin. Dispatch is purely by argv[0] basename — no runtime overhead per applet.

Category Applets
File ops cat, cp, mv, rm, ln, mkdir, rmdir, touch, chmod, chown, chgrp, install, truncate
Text grep, sed, awk, cut, head, tail, sort, uniq, wc, tr, comm, diff, patch, tee
Filesystem ls, find, du, df, stat, file, readlink, realpath, basename, dirname, pathchk
Process ps, kill, killall, nice, nohup, timeout, watch, pgrep, pkill, wait
System uname, hostname, dmesg, uptime, free, sysctl, env, printenv, nproc
Archive tar, gzip, gunzip, zcat, bzip2, bzcat, xz, unxz
Shell utils echo, printf, test, true, false, yes, seq, sleep, date, expr, xargs
Misc od, xxd, base64, md5sum, sha256sum, cmp, strings, stty

Networking applets (nslookup, wget, nc, ping) are included — shipped in Feature 025. DNS resolution, HTTP fetch, TCP netcat, and ICMP ping all pass integration tests.

nsh$ /bin/grep -i root /etc/passwd
root:x:0:0:root:/root:/bin/sh
nsh$ /bin/ls -la /bin/ls
lrwxrwxrwx        10 ls -> /bin/mybox
nsh$ mybox --list | wc -l
91

Linux ELF Binary Compatibility

Statically-linked musl ELF binaries compiled on Linux run directly on MyOS2026 without modification or recompilation:

# On a Linux host:
musl-gcc -static -o hello hello.c
cargo build --target x86_64-unknown-linux-musl --release

# Copy to MyOS2026 (scp or baked into the disk image) and run:
nsh$ /bin/hello
Hello, World!
nsh$ /bin/mybox-linux echo "from linux musl"
from linux musl

What works: ELF64 static executables (ET_EXEC), full System V AMD64 ABI initial stack layout (argc/argv/envp/auxv), all musl startup syscalls (set_tid_address, arch_prctl(ARCH_SET_FS), prlimit64, getrandom, rt_sigprocmask), anonymous mmap, brk, /proc/self/exe, /proc/self/maps. Invalid accesses deliver SIGSEGV (no kernel panic); stack overflows are caught at the stack bottom guard and deliver SIGSEGV to the process.

Out of scope: dynamic linking (PT_INTERP), 32-bit ELF, kernel modules.

Kernel Symlink Resolution

The VFS resolve() function follows symlinks after every directory lookup (depth-capped at 8 to prevent loops). EXT2 fast symlinks (≤60 bytes stored directly in i_block[]) are supported without data block allocation. This enables execve("/bin/cat") to transparently dispatch through /bin/mybox.

Security: Per-Process Syscall Sandbox

nsh$ sandbox --allow=read,write,exit /usr/bin/exploit-test
BLOCKED (errno=1)      ← mount(2) blocked by kernel allowlist

The kernel enforces a deny-by-default syscall filter per process, installed via SYS_SANDBOX_ENTER (nr=999). Filters survive execve and are independent across processes.

Verified Boot

Every RELEASE build embeds a BLAKE2b hash chain:

UEFI → Limine (config hash enrolled) → kernel.elf (BLAKE2b verified)
     → kernel_main ([vboot] ACTIVE  pubkey: be5f7844108bcdd1)

Any binary tampering before a single kernel instruction executes causes an immediate boot abort (hash_mismatch_panic: yes).

cloud-init

Boots with a cidata ISO and applies provisioning automatically:

  • Sets hostname via sethostname(2)
  • Writes SSH authorized keys to /root/.ssh/authorized_keys
  • Runs runcmd entries (e.g. chmod 700 /root/.ssh)

Reproducible Builds

Two independent builds from identical source produce byte-identical QCOW2.

RELEASE=1 bash build/scripts/assemble-image.sh build-a.qcow2
RELEASE=1 bash build/scripts/assemble-image.sh build-b.qcow2
sha256sum build-a.qcow2 build-b.qcow2
# a3e64333...  build-a.qcow2
# a3e64333...  build-b.qcow2  ← identical ✓

Achieved via SOURCE_DATE_EPOCH, pinned GPT/FAT UUIDs, and build/scripts/fix-ext2-timestamps.py.


Integration Tests

Test Suite Description Status
test_boot.py integration 9-phase boot sequence → nsh$ ✅ PASS
test_ssh.py integration SSH login (Dropbear, key auth) ✅ PASS
test_shell.py integration nsh pipes, redirects, builtins ✅ PASS
test_cloud_init.py integration cidata: hostname + SSH key injection ✅ PASS
test_sandbox.py integration Sandbox blocks mount(2) ✅ PASS
test_rollback.sh integration QCOW2 snapshot / rollback ✅ PASS
test_lsi_scsi.py vbox mptsas1068 detection + CD-ROM coexistence ✅ PASS
test_e1000_ssh.py vbox E1000 NIC + SSH login ✅ PASS
test_vbox_combined.py vbox LSI SCSI + E1000 together ✅ PASS
test_dual_nic.py vbox virtio-net + E1000 dual NIC ✅ PASS
test_signal.py syscalls rt_sigaction delivery + sigreturn ✅ PASS
test_nanosleep.py syscalls nanosleep blocks correct duration ✅ PASS
test_futex.py syscalls futex WAIT/WAKE under concurrent load ✅ PASS
test_misc_posix.py syscalls uname, getcwd/chdir, TIOCGWINSZ ✅ PASS
test_debug_mode.py misc kdebug subsystem log tags ✅ PASS
test_scheduler.py scheduler MLFQ: CPU demotion, I/O boost, starvation, nice ✅ PASS
test_linux_elf.py elf-compat 16 scenarios: musl C/Rust ELF, SIGSEGV, stack overflow, /proc ✅ PASS
test_reproducible.sh misc Two builds produce identical SHA-256 ✅ PASS

Run all suites:

make test-all QCOW2=dist/myos2026.qcow2

# Or individually:
make test-unit                                    # 434 kernel unit tests, no QEMU
make test-integration QCOW2=dist/myos2026.qcow2  # boot, SSH, shell, cloud-init, sandbox
make test-vbox        QCOW2=dist/myos2026.qcow2  # LSI SCSI, E1000, dual-NIC
make test-syscalls    QCOW2=dist/myos2026.qcow2  # signal, nanosleep, futex, misc-posix
python3 tests/boot/test_linux_elf.py dist/myos2026.qcow2  # Linux ELF compat (16 scenarios)

CI before push (Feature 035)

Every PR is gated by GitHub Actions; the ci check runs clippy, unit tests, and the integration suite under smp ∈ {1, 2} matrix axes. To run the same pipeline locally before pushing:

make ci-local       # ~15 min; same step order, same per-step timeouts as remote CI

Failed local runs leave artifacts under dist/ci-artifacts/ (QEMU serial log, test stdout/stderr, optional kernel-panic excerpt). The CI gate is documented in detail in specs/035-ci-pr-gate/quickstart.md.


What Is Implemented

Kernel

  • Boot: Limine (BIOS + UEFI), x86-64 entry, GDT/IDT, APIC/HPET timer
  • Memory: bitmap physical allocator, 4-level page table, demand paging, kernel heap
  • Drivers: UART, virtio-blk, virtio-net, virtio-console (bidirectional: TX output + RX input via poll), virtio-rng, virtio-scsi, LSI Logic MPT SCSI (VirtualBox), Intel E1000 NIC (VirtualBox), framebuffer
  • Filesystem: ext2 (superblock, block groups, inodes, directories, read/write, symlinks), VFS with symlink-following resolve(), 64-slot LRU write-back block cache
  • Linux ELF compat: ELF64 static loader, full System V AMD64 ABI initial stack (auxv), /proc/self/exe|maps, mmap_min_addr guard, stack-bottom guard → SIGSEGV
  • Process: PCB (with nice: i8, stack_bottom), fork, exec, wait, exit, FD table, dup/dup2, pipes
  • Scheduler: 3-level MLFQ — quanta P0=1/P1=2/P2=4 ticks, priority decay on quantum exhaustion, I/O boost (wake at P0), starvation prevention (boost every 100 ticks), nice-based base priority, fork inherits nice
  • Syscalls: Full POSIX Linux ABI (SYSCALL/SYSRET). Process: fork (CoW lazy frame sharing), execve, waitpid, exit, getpid, getppid, clone (CLONE_THREAD), set_tid_address, exit_group. Files: open, close, read, write, lseek, dup, dup2, pipe, fcntl, stat, fstat, lstat, getdents64. Memory: brk, mmap (anon+file-backed, MAP_SHARED/PRIVATE), munmap, mprotect. Signals: kill, rt_sigaction, rt_sigprocmask, rt_sigreturn, alarm. Time: clock_gettime, nanosleep, clock_nanosleep, gettimeofday. Threading: futex (WAIT/WAKE). Sockets: socket (TCP/UDP/ICMP-raw), bind, listen, accept, connect, send, recv, sendto, recvfrom, sendmsg, recvmsg, setsockopt, getsockopt, shutdown. Priority: nice (nr=34), getpriority (nr=140), setpriority (nr=141). Misc: uname, getcwd, chdir, ioctl (TIOCGWINSZ), sysinfo, gettid. Custom: gethostname (nr=125), vboot-status (nr=998), sandbox-enter (nr=999).
  • Network: smoltcp 0.11, virtio-net device, DHCP, TCP/UDP/ICMP sockets, sendmsg/recvmsg, packet firewall (default-deny, allow TCP/22 + ICMP + UDP)
  • /proc filesystem (Feature 024): /proc/self/exe, /proc/self/maps (dynamic load+heap+stack ranges), /proc/self/fd/<N> (symlinks to open file paths), /proc/self/status (Name/Pid/VmRSS/Threads), /proc/<pid>/exe, /proc/<pid>/maps, /proc/cpuinfo (model name, cores), /proc/uptime, /proc/net/dev (rx/tx per NIC), /proc/net/tcp (TCP socket table); fully compatible with musl readlink("/proc/self/exe"), /proc/self/maps mmap parsing, and standard Unix tools
  • Fix: kernel sys_write to a pipe now blocks instead of dropping data past the 4 KiB ring (closes #106). Both sys_write call sites for PIPE_FS_ID (stdout/stderr fast path at line ~322; general-fd path at line ~456) now route through a new pipe_write_blocking helper that loops over the kernel pipe primitives, yielding when the ring is full and any reader is still attached, returning EPIPE if all readers close before any byte was written. Before this fix, a single write(fd, buf, N) for N > 4096 returned only what fit in the ring (pipe::write caps at BUF_SZ), and the rest was silently dropped — dmesg | tail -5 and ls /bin | head -12 both returned empty chunks because dmesg's first write filled the ring and musl's stdio gave up. O_NONBLOCK still returns EAGAIN immediately. 7 new unit tests in kernel/src/proc/pipe.rs document the kernel pipe contract (write caps at BUF_SZ, write_space tracks outstanding bytes, read_ready flips on writer-close, has_readers is the EPIPE signal, ring wraparound preserves bytes across (head + count) % BUF_SZ). New pipe::write_space(idx) accessor for callers that need to size their reads/writes. Live end-to-end verification (running dmesg | tail -5 over SSH) is currently blocked by #105 — the kernel [BAD-RET] scheduling panic fires during dropbear's SSH-handshake handling before any pipe activity from a test command can complete; will resolve automatically when #105 is fixed.
  • Dedicated kernel audit ring — Stage 2 (Feature 073 — Stage 2 of #144). Adds a 4096-slot, BSS-resident, SeqLock-protected ring of fixed 112-byte binary AuditRecords alongside Stage 1's kmsg-ring ASCII path. Each credential-changing syscall now pushes into BOTH transports inside the same PTABLE.lock acquisition (FR-011 atomicity): the Stage 1 dmesg | grep AUDT line for ad-hoc operator grep, AND a Stage 2 binary record for programmatic streaming consumers. Userland reads via /proc/audit/data (binary, 112-byte aligned; cursor model: oldest_seq + offset / 112; blocking-read via sched_yield per FR-022; root-only open(2) via explicit gate in sys_open) and /proc/audit/stats (4-line ASCII KV: seq_oldest/seq_newest/dropped_total/bytes_written; idempotent — reads don't consume records; root-only). Per-slot AtomicU32 SeqLock counters give lock-free reader correctness without taking a writer lock (PTABLE.lock already serialises the F072 hook). Drop counter is monotonically exact (SC-006): if a slow consumer falls behind, the seq returned jumps and dropped_total accounts for every overwritten record. Record discriminants pinned in contracts/audit-ring-format.md: op (0–8, 9 values), result (0–2), target_kind (0=Single, 1=Triple-for-setresuid/setresgid with u32::MAX sentinel, 2=Groups). 22 new kernel unit tests (5 layout/encoder + 8 ring SeqLock + 3 stats + 4 read_data + 2 other) bring the kernel suite to 456/456 passing. 5 new integration scenarios in tests/boot/test_credentials.py (S5 streaming + S5b 9-op coverage + S6 stats + S7 EACCES non-root + S8 SC-006 overflow + S9 bytes_written stability). make bench-setuid-stage2 enforces ≤100 ns Stage 2 delta vs the post-F072 baseline. Userland audit daemon (drain to ext2) deferred — needs ext2 write path (#73); follow-up issue + ROADMAP row filed. Spec: specs/073-credential-audit-ring/.
  • Kernel audit log of credential transitions — Stage 1 (Feature 072 — Stage 1 of #144; Stage 2 shipped as Feature 073 above). Every credential-changing syscall — setuid/seteuid/setresuid/setgid/setegid/setresgid/setgroups/setfsuid/setfsgid — now emits a structured single-line audit record into the existing kmsg ring (Feature 038) under a new Level::Audit = 6 (tag AUDT). Records carry the operation name, caller PID, pre-call credential tuple, target argument, post-call credential tuple, and the result (ok | EPERM | EINVAL). Both successful changes AND denials are recorded (FR-012) — an attacker probing for privilege escalation leaves a trace whether they succeed or not. The hook fires INSIDE proc::table::with_creds_mut's PTABLE.lock acquisition so pre/post snapshots are atomic with the mutation (FR-010 — same lock the mutation already holds). The setfsuid/setfsgid Linux quirk (return value is OLD fsuid even on EPERM) is preserved at the syscall ABI; the audit record reports the LOGICAL outcome (pre.fsuid != post.fsuid ? ok : EPERM). The (uid_t)-1 sentinel for setresuid/setresgid renders literally as -1 (not 4294967295). Default-on, no compile flag, no per-PID gate — by design, you cannot opt out of audit on your own PID. Read records with dmesg | grep AUDT or programmatically via SYS_KMSG_READ (440) filtering on level == 6. 15 audit format unit tests + 2 Level::Audit ABI tests in kernel/src/proc/audit.rs::tests; integration scenarios in tests/boot/test_credentials.py cover success (US1) + EPERM/setfsuid quirk (US2) + zero-bytes-for-non-cred (NFR-002 / SC-004); make bench-setuid enforces the ≤200 ns regression budget vs the pre-F072 baseline (NFR-001 / SC-003). Stage 2 (dedicated /proc/audit ring with drop counter + sequence number + userland daemon draining to ext2) is deferred per #147. Spec: specs/072-credential-audit-log/.
  • Real UID / credential model on Pcb (Feature 071 — closes #74). Replaces the 11 hardcoded 0 UID/GID syscall arms in kernel/src/proc/syscall.rs (lines 315-318 + 340-350) with reads/writes through a new Credentials sub-struct on Pcb: 8 × u32 scalars (uid/euid/suid/fsuid + GID equivalents) + a fixed [u32; 16] supplementary-group array + ngroups: u8 = exactly 100 bytes per Pcb (within NFR-001 ≤112 B budget). All 14 set/get UID/GID syscalls land in kernel/src/proc/syscalls/io.rs: getuid/geteuid/getgid/getegid/getresuid/getresgid/getgroups (reads) + setuid/seteuid/setresuid/setgid/setegid/setresgid/setgroups/setfsuid/setfsgid (writes) — each implementing the full Linux EPERM truth tables verbatim, with all-or-nothing atomicity on setresuid/setresgid (no partial field writes on EPERM) and the Linux setfsuid "returns OLD value even on permission denial" quirk preserved. /proc/<pid>/status grows three new tab-separated lines (Uid:\t<r>\t<e>\t<s>\t<f>\n, Gid:, Groups:\t<space-separated>\n) matching Linux byte-for-byte; the old hardcoded "Uid:\t0 0 0 0" space-separated form is replaced. Feature 045's /proc/<pid>/trace permission rule upgrades from structural (writer == target || writer == PID 1) to standard own-effective-UID-or-effective-root (writer.creds.is_root() || writer.creds.euid == target.creds.euid), closing the F045 spec TODO. Default boot state preserved (every process starts Credentials::root() = all zeros), so existing v0 binaries see identical values — the change is plumbing, not policy. 31 EPERM-matrix kernel unit tests in kernel/src/proc/credentials.rs::tests cover the truth tables (setuid_unprivileged_to_real/saved_ok, setresuid_atomicity, setfsuid_returns_old_value_on_perm_check, setgroups_size_over_16_einval, trace allow/deny 4-cell matrix, etc.). 3 single-process corpus programs (credentials_round_trip, getgroups_query, setresuid_root_nop) ride make syscall-diff for live VM coverage; the multi-PID setuid scenarios are covered by unit tests because live multi-user shells aren't shipped today. SC-005 audit grep returns zero matches post-merge. Deferrals (4 GitHub issues + ROADMAP rows): S_ISUID exec semantics (#141), capabilities(7) (#142), user namespaces (#143), audit log (#144). Spec: specs/071-pcb-credentials/.
  • mymc — dual-pane file manager in Rust userland (Feature 070). Trimmed from upstream Cargonaut to fit MyOS2026's image constraints: 2.05 MiB stripped musl release binary (NFR-001 budget: 2.5 MiB), no async runtime (std::thread + std::sync::mpsc), no wasmtime plugin host, no SFTP/S3 backends, no OS keychain dependency. Phase A ships dual-pane LocalFs navigation (j/k/Enter/Backspace + Tab focus + Alt-1/2 jump), resumable copy/move engine (8 MiB chunks fsync'd + CRC-chained .mymc-transfer-*.json checkpoints; SIGKILL → scan_resumable picks up at last fsync), confirm/input/resume/tasks/picker dialog system (F5 copy/F6 rename/F7 mkdir/F8 delete + F12 in-flight jobs + launch-time resume prompt), text previewer (F3; plain text, no syntect to fit NFR-001), hand-rolled fuzzy filter (<; nucleo-matcher would have added 300 KiB of Unicode tables), per-pane directory history (Alt-Shift-h / Alt-y / Alt-u), quick-cd popup (Alt-c), panel filter (Alt-!), sync-other-panel (Alt-i/Alt-o), toggle-hidden / split-orient / recursive-dir-size (Alt-./Alt-,/Ctrl-Space), and the mymc.sh cd-on-exit shell wrapper (FR-017; MYMC_EXIT_CWD_FILE). Configuration is layered TOML (/etc/mymc/config.toml~/.config/mymc/config.toml--config). Keymap is data-driven (/etc/mymc/keymap.toml) with 7 dialog kinds and Mode/Key/Mods abstraction. Three CI jobs feed ci rollup: mymc-size (NFR-001 + NFR-007 unsafe-grep), mymc-coverage (NFR-006 tarpaulin ≥ 80%), mymc-startup (SC-004 + FR-010 ≤ 150 ms), mymc-boot (T_A.07 image install + non-interactive CLI). 134 unit tests pass; clippy -D warnings clean; zero unsafe blocks in userland/mymc/src/**. Deferrals (5 GitHub issues + ROADMAP rows): editor handoff (#129; needs MyOS2026 editor), archive-as-VFS (#130; needs tar/zip workspace deps), HMAC audit (#131; needs OS keychain), seccomp hardening (#132), io_uring (#133). Spec: specs/070-mymc-userland-fm/. Upstream: github.com/mohnkhan/cargonaut.
  • DWARF CFI-based frame-pointer-free unwinding (Feature 068 — closes #112). Off-by-default kernel-side stack unwinder that walks past frames with broken or zeroed frame pointers via DWARF .debug_frame CFI rules. Build-time dwarf-extractor --cfi walks every FDE via gimli's UnwindContext, emits a sorted 16-byte CfiEntry rule table to kernel/src/debug/cfi_generated.rs (currently 55,893 rows; 100% of 13,379 FDEs fit the 4-tuple shape). Runtime kernel::debug::unwind::next_frame is pure binary search + 4-tuple arithmetic with kernel-VA bounds checking — panic-safe by construction, reuses Feature 066's recursion guard. Panic-handler integration is purely ADDITIVE: existing FP backtrace runs as today; when the FP walker truncates (typically RBP=0 from an asm trampoline), the handler re-walks the rbp chain to find the broken frame, then calls next_frame repeatedly to extend the backtrace past it. Each CFI-recovered frame is prefixed by a cfi> rip=<hex> marker so operators can distinguish FP-walked vs CFI-walked frames at a glance. SC-002 (zero CFI bytes when feature OFF) enforced by scripts/ci/check-cfi-zero-cost.sh; SC-003 (≤20% loaded-image growth when ON) enforced by scripts/ci/check-cfi-size-budget.sh. Verified end-to-end via image-cfi-test which boots a chain cfi_test_outer → cfi_test_middle → asm-trampoline-zeroes-rbp → panic; the CFI walker bridges past the broken frame as designed. Quickstart: specs/068-dwarf-cfi-unwinding/quickstart.md.
  • Idle-flush UART decoupling for kprint! (Feature 067 — closes #69). Boot with uart_async=1 on the kernel cmdline; from that point forward, kprint! pushes to the kmsg ring and returns immediately — the UART catches up via a 4-line-per-pass drain called from the idle loop. Measured speedup: 813× (7.2 ms → 8.9 µs per kprint! call under QEMU TCG; production should be ~3 µs). Default sync behavior preserved (byte-identical boot log under no opt-in). The framebuffer terminal write is gated on the same flag so async mode is genuinely fire-and-forget for the issuing CPU. set_uart_mirror(true) runtime flip synchronously flushes any backlog before returning (FR-007a — preserves UART monotonic-by-seq invariant). Ring overflow during async mode emits a single [uart: NN lines dropped (ring overflow)] marker per skip event so silent gaps are impossible (FR-008a). Panic-time emission unaffected — write_direct bypass means uart_async=1 panic_now=1 produces a complete panic block on UART (FR-009 + SC-004). Quickstart: specs/067-uart-async-flush/quickstart.md.
  • CI gate for named-root-cause discipline in PR bodies (closes #72). Feature 047 FR-011 / US4 require every feature PR body to name the concrete root cause (file:line + what was wrong + the fix + why it works); until now this was enforced only by self-policing + reviewer eyes. scripts/ci/check-pr-body.sh is a 100-line bash gate that runs alongside docs-gate on every PR: requires a ## Summary or ## Root cause heading; if the body uses ## Root cause, additionally requires at least one file.ext:line reference. Same [no-docs] bypass token as docs-gate (one knob for both). 9-case self-test (scripts/ci/test-check-pr-body.sh) covers summary-only, root-cause-with-fileref, root-cause-without-fileref, empty-body, no-required-heading, case-insensitive, both-headings, fileref-anywhere, and gh-fails-skip cases.
  • DWARF inlined-function chain expansion in panic backtrace (closes #111). Feature 066's panic-handler emission loop now displays the full inlined-function chain beneath each primary frame as indented inline> <name> at <file>:<line> lines. Critical for kernel debugging because rustc aggressively inlines generic methods (Option::unwrap, Mutex::lock, Result::expect) — before this, a panic that fired inside such a method would just show the OUTER function with a confusing source location (e.g., kernel_main+0x109a at <rust>/core/src/option.rs:1015); now it ALSO shows inline> <core::option::Option<u32>>::unwrap at kernel/src/lib.rs:136 naming the exact call site. The build-time dwarf-extractor tool (userland/tools/dwarf-extractor) walks every CU's DIE tree, captures DW_TAG_inlined_subroutine entries, resolves names through the DW_AT_abstract_origin → DW_AT_specification chain, and emits INLINED_CHAINS / INLINE_OVERFLOW static tables. Sweep-line bucketing produces non-overlapping per-RIP-range chains with outermost-to-innermost ordering; the 8-level cap (FR-003) records elided counts in INLINE_OVERFLOW. Measured: 6,169 chains captured for the current kernel ELF; total DWARF table grew from 9.29% → 10.34% of loaded image (still well under FR-011's 20% ceiling). Closes US2 of Feature 066, which deferred this work for time-budget reasons.
  • #105 BAD-RET scheduling panic — root cause fixed. Three months of intermittent kernel panics during dropbear SSH handshake under multi-execve load ([BAD-RET] about to switch to pid=N new_rsp=... ret_addr=0x0 — halting) traced to a 128-KiB struct-assignment overflowing the kernel stack. In kernel/src/net/unix.rs:alloc_pair, the line t.pairs[i] = PairEntry::new() was compiled as "construct on caller's kstack, then move to static slot"; PairEntry contains two [u8; 65536] ring buffers, so 128 KiB landed on a 256 KiB kstack — and overflowed into PHYSICALLY ADJACENT kstack frames, silently corrupting OTHER processes' saved-context area (PCB.kernel_rsp + 56). The all-zeros pattern in the BAD-RET dump was the implicit memset of the ring data arrays. Fix: in-place field reset (no construct), one struct field at a time. Regression test pins both invariants. Found via the sticky HW watchpoint instrumented for #120 — first reproduction after the false-positive filter caught the corruption with a 10-frame backtrace from compiler_builtins::memset through unix::alloc_pair to sys_socket. Verified across 572 execves + 250 zombie-reaps on a 5-minute reproducer with zero panics.
  • DWARF-based in-kernel stack unwinder (Feature 066 — closes #67). Every panic backtrace now ends each line with at <file>:<line> — sourced from a build-time KALLSYMS-style lookup table compiled from the kernel ELF's DWARF (userland/tools/dwarf-extractor is a new host-target Rust workspace member using gimli). The kernel itself has no DWARF parser; runtime is just binary search on &'static arrays — panic-safe by construction. Path normalization at build time keeps the embedded strings short: kernel/-relative for first-party (kernel/src/mm/phys.rs), <cargo>/-prefixed for crates (<cargo>/spin-0.9.8/src/mutex.rs), <rust>/-prefixed for stdlib (<rust>/core/src/option.rs) — no developer-host paths leak. SC-001 acceptance: 3/3 kernel frames in the panic backtrace now show file+line (4th is the _start asm trampoline). SC-002 measured: 14% loaded-image growth — the embedded table is ~1.5 MB for 125k line entries; well under FR-011's 20% hard ceiling but above the original 5% soft target (spec updated to reflect measured reality). The infrastructure for inlined-function chain expansion (US2) ships as empty stubs in v1 — kernel accessors + panic-handler iteration are wired and zero-cost when tables are empty; a future feature populates them. CFI walking (US3) similarly deferred. Quickstart: specs/066-dwarf-unwinder/quickstart.md.
  • FASAN — Frame Allocator SANitizer (Feature 065). Per-frame physical-memory poisoning + ownership tracking + diagnostic accessors layered on kernel/src/mm/phys.rs. Every allocated frame is filled with 0xAABBCCDDAABBCCDD (alloc-pattern sentinel); every freed frame is filled with 0xDEADBEEFDEADBEEF (free-poison sentinel). A 1-byte-per-frame shadow array (1 MiB BSS) records the current owner (one of FREE/KSTACK/HEAP/USERPAGE/PAGETABLE/DEVICE/BOOT/UNKNOWN). The BAD-RET handler in the scheduler now emits three [FASAN] frame=... owner=... sample=[...] lines per panic (failing frame + page±1 neighbours), so issue #105's "kstack contents are all zero, no idea why" mystery becomes a one-repro diagnosis — the developer sees whether bytes are the alloc pattern ("allocated but never written"), free poison ("freed, then handed back without re-init"), or true zeros (something actively wrote 0), plus the owners of the neighbouring frames so corruption from above/below is visible. The kernel panic handler and kassert! path additionally emit a --- FASAN per-PID kstack summary --- block, one line per live PID with the top-of-kstack frame's owner + 4-word sample. The allocator emits [FASAN-XSTATE] warnings on illegal owner transitions (double-free, alloc-over-non-FREE) — non-halting per FR-011. SC-002 budget: ≤10% binary growth — measured at 0.04% (text+data delta 4,512 bytes; the 1 MiB SHADOW is BSS-only). SC-004 budget: ≤10% boot-time hit — tests/boot/test_boot.py confirms all 9 phases still pass with FASAN on. Feature-flagged frame-poison in kernel/Cargo.toml (default-on for debug + KASAN; off via --no-default-features for release; OFF stubs preserve every consumer's signature per contract §1.4). 26 production alloc_frame() call sites migrated to alloc_frame_owned(<owner>) covering kstack/heap/userpage/pagetable/device buckets. Quickstart: specs/065-frame-poisoning/quickstart.md.
  • Wiki-ready demo GIF + nsh non-TTY fallback (Feature 062). make demo-gif now produces a real recording of nsh over SSH — docs/demo.gif, 416 KB, ~15 s playback — usable directly in a wiki article or README. Three pieces: (1) nsh's main loop falls back to a line-buffered prompt → read → run loop when Editor::with_config returns ENOTTY, so scripted demos no longer panic with "Not a tty" (userland/shell/src/main.rs:108); (2) build/scripts/capture-demo-gif.sh switched from the broken nsh$-on-serial wait + paramiko-with-tight-timeouts pattern to a sshd started-marker wait + raw-socket banner probe, and runs all demo commands as a single semicolon-chained nsh -c "cmd1; cmd2; ..." via paramiko (per-command exec_command was flaky against MyOS dropbear, ~7 s per cmd and the 3rd hung); (3) cast post-processing trims ~30 s of SSH-negotiation dead time so the GIF starts immediately. tests/demo/demo-commands.sh was rewritten in nsh-compatible syntax (one command per line, # comments stripped by the driver). Colored prompt is rendered host-side using the same ANSI escapes as userland/shell/src/prompt.rs so the GIF matches what a real TTY would show. Discoveries documented in Learnings.MD include: dropbear in MyOS does not allocate PTYs; a multi-line stdin pipe through nsh's new fallback path triggers a kernel [BAD-RET] scheduling panic (separate issue, not blocking demo); nsh pipes drop output for large left-hand sides (dmesg | tail -5 returns empty, echo small | base64 works fine).
  • TSC-resolution timestamps in kmsg ring (Feature 061 — closes #68). Replaces the kmsg ring's 32-bit scheduler-tick timestamp source (10 ms granularity) with a 64-bit nanosecond timestamp derived from the x86 TSC. /proc/dmesg's text format switches from [<integer-ticks>] to Linux dmesg-style [<seconds>.<microseconds>] — sub-microsecond precision visible in every dmesg line, panic-tail block, and per-PID syscall trace record (Feature 045 inherits the new resolution transparently). /proc/uptime's first field also switches to the new TSC-derived source. Per-entry layout grew from 256 to 264 bytes (the Entry header widened from 16 to 24 bytes; the 240-byte message payload is unchanged) — an 8 KiB BSS bump on the always-on ring. SYS_KMSG_READ(440)'s binary struct ABI changed (v1 → v2: see specs/061-tsc-kmsg-timestamps/contracts/kmsg-entry-format.md); zero current userspace consumers are affected because the text-format consumers (mybox dmesg, mybox strace) parse /proc/dmesg not the struct. New monotonic_ns() accessor in kernel/src/arch/x86_64/timer.rs is the canonical TSC-derived nanosecond clock for future kernel features. Quickstart: specs/061-tsc-kmsg-timestamps/quickstart.md.
  • In-kernel dmesg ring buffer + GDB workflow (Feature 038): 1024-entry × 256-byte ring in BSS captures every kprint!/debug! line via dual-write at the UART driver; readable from userland via /proc/dmesg or syscall 440 (SYS_KMSG_READ); cleared by writing to /proc/dmesg-clear; the panic handler dumps the most recent 256 entries to UART with --- dmesg tail --- markers so the QEMU serial log preserves pre-panic context; make debug and make debug-kasan launch the kernel under QEMU paused at entry with gdbserver on :1234 (see specs/038-dmesg-gdb-stub/quickstart.md). Note: per-entry size and format updated by Feature 061 — see that entry above.
  • Live-verified: kernel-side strace decoder works end-to-end (issue #91 — PR #92 follow-up). New strace_test=1 cmdline trigger calls crate::proc::strace::emit directly with 7 synthetic records (covering path-decode arms, integer-arg arms, and the catchall) then panics — the panic handler's --- dmesg tail --- block surfaces all 7 records on serial output. New make image-strace-test target builds the QCOW2 with the trigger baked in; new tests/boot/test_strace_kernel.py integration test scrapes the captured serial and asserts each expected record's exact format. Proves the kernel decoder + kmsg-write path + panic-dump surface are correct in isolation from any userland glue. The remaining userland-applet integration (strace ./prog round-trip via hvc0/nsh) can now be diagnosed cleanly — anything misbehaving from here is in the userland polling loop, not the kernel emission.
  • Strace-format syscall trace + userland strace binary (issues #70 + #78 — Feature 045 follow-ups). The per-PID trace gate in syscall_dispatch now emits decoded records like [S 7] open("/etc/passwd", 0x80002, 0o0), [S 7] write(1, 0x20001030, 51), [S 7] execve("/bin/echo", 0x..., 0x...) instead of the v0 [S pid/nr] format. Decoder lives in kernel/src/proc/strace.rs and covers the ~10 most-used syscalls (read/write/open/openat/close/mmap/brk/execve/exit_group/fork); any syscall not in the decode table falls through to the v0 catchall, so coverage is purely additive. Path arguments are read from userland via the same *const u8 + null-scan pattern as sys_open; flags are emitted as hex (symbolic decoding deferred to v2). New strace mybox applet at userland/mybox/src/applets/strace.rsstrace ./prog [args] forks, enables trace on the child via /proc/<pid>/trace in pre_exec, execs, polls /proc/dmesg filtering for the child's [S <pid>] records, and streams to stderr. v1 supports the fork-then-exec mode only; -p <pid> attach mode and return-value capture are tracked as follow-ups.
  • Per-CPU current-PCB cache (issue #66 — Feature 045 R3 Option B). sched::current_pid() was taking SCHED.lock() on every call — once per syscall via syscall_dispatch, adding ~50–100 ns of uncontended-mutex overhead per syscall. Replaced with a lock-free AtomicU32 cache (kernel/src/sched/percpu.rs) updated at every context-switch commit point (schedule_to_next + exit_current). current_pid() and current_pid_try() now read the atomic in single-digit ns. The panic path's current_pid_try no longer needs the try_lock fallback (which could return None on contention and force pid=unknown in the dump). Single-AtomicU32 today; the shape transparently becomes a per-CPU-array indexed by APIC ID when SMP arrives (#75).
  • Panic dump: register snapshot + page-table chain walk (issues #64 + #65 — Feature 046 follow-ups). The #[panic_handler] now emits two new blocks alongside the existing dmesg-tail / backtrace / pcb-dump. --- registers --- captures all 16 GPRs + RFLAGS + RIP + CR0/CR2/CR3/CR4 via a single inline-asm spill to a stack buffer (snapshots panic_handler entry state, not the panic site — the actual fault RIP is in the backtrace block). --- page table chain (VA=CR2, CR3=...) --- walks PML4 → PDPT → PD → PT for the value in CR2 (the faulting VA on page-fault panics, informational otherwise), showing the raw PTE + present/not-present at each level and halting at the first missing level. Lock-free, panic-safe, follows the existing DirectWriter pattern. New walk_page_table_chain(va) -> [PageTableLevel; 4] accessor in kernel/src/mm/virt.rs is reusable from fault handlers, future kassert!s, or any other diagnostic that needs to show the translation chain.
  • Fix: per-channel TTY input buffer — /dev/hvc0 no longer races /dev/tty0/1/2/3 for host-sent bytes (issue #82, closes #63). Before this change, every byte arriving on the virtio-console RX queue landed in the same PS/2-fed shared line buffer that all other VT readers were sleeping on; whichever nsh was first off sched::sleep_until consumed the bytes, so host input to /dev/hvc0 almost never reached the nsh actually listening there. Added a dedicated HVC0_* set of input statics (canonical line buffer + raw ring + waiter PID), a push_byte_hvc0() producer entry point called from virtio::console::poll_rx, and a read_hvc0() consumer entry point dispatched from both branches of sys_read (redirected fd 0/1/2 + general fd ≥ 3). Echo on hvc0 routes through virtio::console::write back to the host (not the framebuffer). The previously-failing tests/boot/test_hvc0_rx.py (HELLO_FROM_HOST round-trip) now passes in ~0.00s after socket connect.
  • Fix: nsh on /dev/hvc0 no longer SIGABRTs at startup (issue #63 partial): two surgical kernel fixes. (1) PTY_MASTER_FS_ID had silently shared 0xF5 with HVC0_FS_ID, so every write to /dev/hvc0 was being routed into pty::master_write(0) (slot 0 never in_use → returns 0 → Rust stdio panics with WriteZero → SIGABRT). Moved PTY_MASTER_FS_ID to 0xF2. (2) sys_read's redirected-fd branch (used when init dup2s /dev/hvc0 onto fd 0/1/2 before exec'ing nsh) was missing an HVC0_FS_ID handler and fell through to vfs::readEIO. Added the missing branch. Banner now flows host-bound through the virtio-console socket. Full host-input echo is gated on a follow-up architectural change (per-channel TTY input buffers).
  • Per-PID /proc/<pid>/{stack,wchan} + kernel symbol-table accessor + symbolized panic backtrace (Feature 049): two new procfs files answer the classic "where is this process blocked?" question without a debugger. /proc/<pid>/stack walks the saved RBP chain of a sleeping task and emits up to 16 RIPs as 0x<hex>\n per line; /proc/<pid>/wchan emits a one-line <name>+0x<offset>\n for the top frame, or [running] / [zombie] / [never scheduled] for non-walkable states. Backed by a new build-time symbol-table extractor (build/scripts/gen-symtab.{sh,awk}) that pipes nm --demangle target/kernel.elf through a two-pass kernel link (Linux KALLSYMS pattern) so the table is byte-deterministic and embedded inside the same kernel.elf it describes — measured 6.21% .text growth for 3,183 symbols, well under the SC-005 ≤10% budget. Same accessor symbolizes Feature 046's panic-backtrace block (raw RIPs → 0x<rip> <name>+0x<offset>), turning a hex dump into something readable at a glance. New make image-wchan-test target boots the kernel with wchan_test=1 for the end-to-end integration test. Quickstart: specs/049-stack-wchan-symbols/quickstart.md.
  • Structured kernel panic + kassert! with PCB context (Feature 046): every panic!() and every failed kassert!() emits a self-contained, line-oriented block on serial output for post-mortem diagnosis. Panic emission order: existing PANIC <file>:<line> <message> line → existing --- dmesg tail (N entries) --- block (Feature 038) → new --- backtrace (M frames) --- block (frame-pointer walker, max 16 frames) → new --- pcb dump (K live pids) --- block (one line per live PID with PID/PPID/state/last_syscall_nr/CR3). kassert!(cond, msg) is a drop-in replacement for assert!() that on failure emits --- kassert FAILED --- plus message, file:line, current PID, CR3, kernel RSP, last_syscall_nr, and the most-recent 16 kmsg ring entries — then halts. Zero-cost when the condition is true. New Pcb.last_syscall_nr field is written at every syscall entry and feeds both dump paths. Frame-pointer flag (-C force-frame-pointers=yes) promoted from KASAN-only to ALL kernel builds; binary cost measured at −1.54% (text section actually shrinks slightly at debug-profile, well under SC-005's ≤5% budget — see specs/046-structured-panic-kassert/research.md R9). New make image-kassert target for the integration-test trigger. Quickstart: specs/046-structured-panic-kassert/quickstart.md.
  • Per-PID syscall trace toggle (Feature 045): /proc/<pid>/trace exposes a runtime per-task flag. Writing 1 enables [S pid/nr] (and [S blocked pid/nr]) records into the kmsg ring (Feature 038) for syscalls issued by that PID; writing 0 disables. Output reaches the same surfaces as any other dmesg line — dmesg from userland, /proc/dmesg direct read, or SYS_KMSG_READ (440). Trace records bypass the UART mirror so a heavily traced process does not stall on serial output. Inherited at fork (FR-010), preserved across execve (FR-011). v1 permission model: writer must be the target task or PID 1 (research R1 — degrades from standard own-UID-or-root because MyOS does not yet model UID per task). Replaces the prior compile-time debug-proc Cargo feature for the syscall-entry trace point. Quickstart: specs/045-per-pid-syscall-trace/quickstart.md.
  • Differential syscall harness vs Linux (Feature 040 + transport rework in Feature 041): a corpus of 31 small static-musl C programs (tests/syscall_diff/corpus/) runs on both the host Linux kernel and inside MyOS2026 via QEMU; the harness diffs observable outputs (exit code, exit signal, stdout, stderr, normalized) and reports per-test PASS / FAIL / KNOWN / SKIP. A TOML allowlist (known-divergences.toml) suppresses by-design divergences with mandatory justifications. Five syscall families are covered (file-I/O, process, signal, memory, time). Invoke with make syscall-diff for the full corpus, make syscall-diff CASE=<name> to debug one program, make syscall-diff TRANSPORT=ssh for the legacy paramiko/dropbear path. Wired into the ci rollup as the syscall-diff job — PRs that introduce POSIX-deviation regressions are blocked from merge once Actions is re-enabled at the repo level. The default transport since Feature 041 is virtio-console over a UNIX socket (no dropbear) — code complete + unit-tested; live integration blocked on a kernel-side hvc0/socket bug (#54). The legacy SSH transport remains available via --transport ssh (still hit by #50 intermittently). Operations guide: docs/syscall-diff.md (architecture, CLI reference, troubleshooting, known limitations, Transports section).
  • Security: per-process syscall allowlist, capability bitmask, verified boot attestation

Userland (all statically linked, musl)

Binary Description
init Stage 1–3: mount root, spawn cloud-init, sshd, nsh
nsh Shell: rustyline REPL (↑/↓ history, ← → editing, Ctrl-R search, Tab completion), ANSI color prompt, persistent /root/.nsh_history, pipes, redirects, &&, banner, help
mybox 97 Unix applets via multi-call binary (stripped)
cloud-init cidata provisioning: hostname, SSH keys, runcmd
dropbear SSH daemon (cross-compiled C, key auth, port 22)
myos-pkg Package manager: install, remove, list, verify (signed tar.gz)
sandbox Installs per-process syscall allowlist then exec's target
exploit-test Calls mount(2) via raw syscall; used by T057 regression test
vboot-check Queries SYS_VBOOT_STATUS; prints key fingerprint + chain

Build System

make all                          # kernel + userland + disk image
make test-unit                    # 434 kernel unit tests (no QEMU)
make test-smoke QCOW2=dist/myos2026.qcow2  # curated 9-test smoke suite (no KVM required)
make test-slow QCOW2=dist/myos2026.qcow2   # timing-sensitive tests (requires /dev/kvm)
cargo clippy -p mybox --target x86_64-unknown-linux-musl -- -D warnings

Architecture Details

Design Principles

Principle Choice
Kernel type Minimal monolithic (Rust, no_std)
Bootloader Limine v8.x (BIOS + UEFI, single config)
I/O model virtio-only (blk / net / console / rng / scsi)
Network smoltcp 0.11 (pure Rust, no_std)
Filesystem ext2 (custom pure-Rust read/write driver)
SSH Dropbear (userspace, cross-compiled for musl)
Userland Rust + statically linked musl
Assembly ~170 LOC total (entry stub, ISR trampoline, context-switch)

Repository Layout

kernel/          Rust kernel (no_std)
  src/
    arch/x86_64/ Entry stub, GDT/IDT, APIC, timer, syscall setup
    mm/          Physical + virtual memory, heap, demand paging
    drivers/     UART, virtio (blk/net/console/rng/scsi), LSI Logic MPT SCSI,
                 Intel E1000 NIC, generic PCI scanner, framebuffer
    fs/          ext2 + VFS (symlink-following) + 64-slot LRU block cache
    net/         smoltcp, DHCP, firewall, ethernet dispatch
    proc/        Process table, ELF loader, syscall handlers, capabilities
    sched/       MLFQ scheduler (3-level, decay, I/O boost, starvation prevention, nice)
    ipc/         Pipes

userland/        Userspace crates (musl-static)
  init/          Stage 1–3 init
  shell/         nsh minimal shell
  mybox/         91-applet multi-call binary (Busybox-in-Rust)
    src/applets/ cat, chmod, chown, cp, cut, date, echo, env, false,
                 grep, head, hostname, kill, ls, mkdir, mv, ps, pwd,
                 rm, sleep, sort, sed, awk, find, tar, gzip,
                 nslookup, wget, nc, ping, …(97 total)
  pkg/           myos-pkg package manager
  cloud-init/    Provisioning agent (hostname, SSH keys, runcmd)
  sshd/          Dropbear SSH daemon + host keys
  tools/         sandbox, exploit-test, vboot-check, network utilities

bootloader/      Limine config + vendored binaries
build/           Makefile, image assembly scripts, CI helpers
  scripts/       assemble-image.sh, fix-ext2-timestamps.py,
                 setup-verified-boot.sh, sign-release.sh, convert-vdi.sh
tests/           Integration and benchmark test suites
  boot/          test_boot.py, test_ssh.py, test_shell.py, test_cloud_init.py,
                 test_sandbox.py, test_debug_mode.py, test_lsi_scsi.py,
                 test_e1000_ssh.py, test_vbox_combined.py, test_dual_nic.py,
                 test_signal.py, test_nanosleep.py, test_futex.py,
                 test_misc_posix.py, test_scheduler.py, test_linux_elf.py,
                 test_reproducible.sh
  snapshot/      test_rollback.sh
  bench/         boot_time.sh, bench_boot_time.py
  keys/          Test SSH keypair (test_id_ed25519)
specs/           Feature specs, implementation plans, contracts
  001-vm-optimized-os/   Core OS: kernel, userland, verified boot, security
  002-rust-busybox/      mybox 30-applet multi-call binary
  003-shell-screenshots-demo/  Shell banner + demo artifacts
  004-kdebug-copyright/  Compile-time debug feature flags + copyright
  005-scsi-e1000-drivers/  LSI Logic MPT SCSI + Intel E1000 NIC drivers
  006-syscall-coverage/  Full POSIX syscall layer (60 tasks)
  009-mlfq-scheduler/    MLFQ priority scheduler + nice syscalls
  010-mybox-core-utils/  mybox expanded to 91 applets (full POSIX core utility set)
  011-linux-elf-compat/  Linux ELF binary compatibility (static musl binaries)
  022-file-backed-mmap/  File-backed mmap (MAP_SHARED/MAP_PRIVATE, demand paging)
  023-cow-fork/          Copy-on-Write fork (lazy frame sharing, CoW fault handler)
  024-proc-fs-expansion/ /proc filesystem: self/{fd/,maps,status,exe}, cpuinfo, uptime, net/*
  025-networking-userland/ DNS resolver, wget (HTTP+HTTPS), nc, ping — 5/5 integration tests
  026-nsh-rustyline/     rustyline REPL for nsh: interactive editing, history, ANSI prompt, Tab completion
  027-interactive-console/ Console shell wired to TTY, password SSH, 3 virtual terminals, TUI demo shell
  028-cmos-rtc-wallclock/  CMOS RTC driver; clock_gettime(CLOCK_REALTIME) and gettimeofday() return correct UTC epoch
  032-virtio-console-rx/  virtio-console RX: receiveq allocated, pre-filled, polled every 10 ms — console now bidirectional
  033-console-demo-smoke-tests/ Interactive console wired to qemu-sdl; curated 9-test smoke suite; test_shell.py fixed; KVM tests quarantined
  034-terminal-multiplexer/  myscreen: screen-style terminal multiplexer; PTY kernel subsystem; AF_UNIX sockets; detach/reattach; 5 integration tests
  035-console-hvc0-fix/     Fix /dev/hvc0 routing: was assigned NULL_FS_ID; now HVC0_FS_ID routes reads→tty::read() and writes→virtio::console::write()
  036-socketpair-cloexec-fix/ sys_socketpair(AF_UNIX) backed by pipes; CLOEXEC honoured in sys_pipe2; recv/send pipe fallback; OVMF VARS fix for reliable UEFI boot

Boot Log

[ 0.20s] Booting from Hard Disk   ← Limine BIOS stage 2
[ 0.69s] MyOS2026 v0.1.0          ← kernel_main
[ 0.89s] [1] UART ok
         [2] mm ok
         [3] interrupts ok
         [4] drivers ok            (virtio-net, virtio-blk, virtio-rng)
         [4b] rtc ok               (epoch=<unix-timestamp> from CMOS RTC)
         [5] fs ok                 (ext2 mounted at /)
         [6a] firewall ok
         [6b] net stack ok         (DHCP → 10.0.2.15/24)
         [7] userspace: launching init
[ 4.46s] → dropbear listening :22
[ 4.83s] MyOS2026 v0.1.0 — type 'help' for built-in commands
         nsh$


Kernel Address Sanitizer (Feature 037)

The kernel ships with an opt-in KASAN-equivalent for catching memory-safety bugs at the corruption site. Build with make image-kasan to produce a sanitized disk image; run python3 tests/boot/test_kasan.py dist/myos2026-kasan.qcow2 for the 7-scenario integration test (5 violations + 2 negatives). The CI kasan-test job runs this on every PR; failures block merges. See specs/037-kernel-kasan/quickstart.md for the full developer guide.

The default build (no --features kasan) is unchanged: zero runtime cost, kernel.elf binary size within 1 % of pre-feature baseline.

What Is Not Yet Implemented

  • Dynamic linking — only statically-linked ELF binaries run; PT_INTERP (glibc/musl dynamic linker) is rejected with ENOEXEC
  • HTTPS wget — TLS (wget https://...) is compiled in (rustls + webpki-roots) but certificate verification against public CAs requires certificate pinning or a local CA bundle; HTTP (wget http://...) works fully
  • Package managementmyos-pkg installs from signed tar.gz; repo tooling and signing pipeline deferred
  • initrd/initramfs — no preloaded ramdisk support; all binaries live on the ext2 partition
  • Loadable kernel modules (monolithic; in-VM module builds deferred)
  • GPG-signed release artifacts (persistent ed25519 key used; GPG pipeline not wired up)
  • strace-equivalent userland tool
  • POSIX lstat() does not distinguish final symlink component (stat() and lstat() both follow symlinks)

Use Cases

  • OS learning platform — every subsystem fits in your head, written in safe Rust
  • Secure ephemeral VMs — sandbox + verified boot + fast teardown via snapshot/rollback
  • CI/CD throwaway environments — < 2s boot, 12.5 MB image, SSH ready in < 5s
  • Kernel and systems programming research — modify kernel, rebuild, boot in < 2 minutes

Contributing

  1. Fork the repository
  2. Read specs/001-vm-optimized-os/plan.md for the core OS architecture
  3. Read specs/006-syscall-coverage/plan.md for the syscall layer design
  4. Run make test-unit — kernel unit tests, no QEMU needed
  5. Boot: make qemu (< 2s to shell prompt)
  6. Open a PR

Good first issues:

  • POSIX lstat() that does not follow the final symlink component
  • strace-style syscall tracer using kernel instrumentation hooks
  • Dynamic ELF loader (PT_INTERP support) — enables glibc-linked binaries
  • GPG signing pipeline for release artifacts

License

Mozilla Public License 2.0

About

VM First Experimental Operating System written in Rust, A Rust OS operating System

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors