79 Commits

Author SHA1 Message Date
1401af3d82 feat(pointer): virtio tablet as 2nd input dev (driver per-instance VAs, abs/btn decode), input server serves 2 devices on one notif; display software cursor, bar click switches window, presses/releases to front window in its px; k-ui hit-tests (press focuses, release on same presses, field only focuses) 2026-10-28 16:50:20 +01:00
0de13f124d fix: pm running apps grow (death-bit bound 60); spawntest stress: 30 files open, apps until BUSY then room comes back; docs: limits and why 2026-10-28 16:22:19 +01:00
1f2134fcec fix: storage tables grow (handles, open files, encrypted opens, grants: Slots/Vec, no 8/16 caps; release follows all narrowed grants); storage says FULL, init spawn STORAGE_BUSY (was NOT_FOUND), pm BUSY; cap slots per process 64 -> 255 2026-10-28 16:18:15 +01:00
3b71f29f4c fix: capacities single-sourced in types (display MAX_WINDOWS/BUFFER_PAGES, storage MAX_CLIENTS), display + storage budgets derived from them, const asserts on VA layouts; kernel regions 64 -> 256 2026-10-28 16:06:55 +01:00
495bf03484 fix: stacks at top of user VA in own 1 MiB band (guard, heap kept below), 8/16 pages default; kernel fault msg names pid, kind, flags stack overflow; flaky OVERFLOW + spawntest/test-qemu check it 2026-10-28 16:03:06 +01:00
18e8b3ecb8 feat(desktop): home app (system-only launch cap, LAUNCH_BADGE: apps+launch only), pm starts it with session (relaunch <=5); display keys F1/Alt+Tab next, F2 home, F4 close app (ROLE from launcher, terminal exempt), close returns to previous window; closed window -> app exits; shell 'name &'; launch by id; pm stack 8, storage 16 clients 2026-10-28 15:50:05 +01:00
c3eb70191b feat: room for many apps: qemu 512M, pool sized from RAM (448M, 114688 frames), user VA 32M -> 1G, display 8 windows (buffers at 0x1000_0000, stack 14 pages), pm app budget 24 MiB, init 16 children (notif bits moved) 2026-10-28 15:31:30 +01:00
8c518b84aa refactor(k-ui): retire glass look (wallpaper, blur, shadow, gloss): flat surfaces; theme drops [wallpaper]/[glass] tokens + orb contrast checks; light accent back to #0b6bcb; settings/tally 3 MiB 2026-10-28 15:09:38 +01:00
7804934008 perf(k-ui): own pixel loops (pixels.rs): float only in corners, SWAR blends, banded shadow; screen fade only over content toward wallpaper (was full-window tiny-skia fill, ~600ms); blur at quarter res, row/column-sum passes, SWAR 2x upscale; ring frame ~75 -> ~20ms 2026-10-28 15:03:19 +01:00
815a2f9fe2 perf(display): present copies window rows as-is (scanout R8G8B8X8 = window byte order, Host hands out row bytes, Framebuffer::write_rgba_row); Window copy_from/copy_to one fenced memcpy; full present ~65ms -> ~3ms 2026-10-28 14:47:06 +01:00
d26b265d38 feat(k-ui): glass look: orb wallpaper + blurred, tinted glass panels (half-res backdrop per theme+size, integer code, frames only copy), soft analytic shadow, top gloss; contrast checked over wallpaper/glass per orb (light accent -> #0a5cb0); partial frames drawn in place; Rect geometry, tree lookups split out; settings/tally 6 MiB 2026-10-28 14:19:54 +01:00
3b71243dea feat(k-ui): motion: ring springs to focus, toggle knob slides, screens slide+fade in (theme motion tokens, 0 = off); 16ms frame timer only while moving; motion-only frames drawn+presented in part (whole elements, pixel-snapped rects, == full draw); virtio fb flushes only damage; TICKS_PER_MS in types 2026-10-28 13:58:38 +01:00
e952a149ec feat(k-ui): text_input (label above, app owns text via on_change, caret, erase, submit, secret, placeholder); keys Left/Right/Space/Char/Erase; settings app filter; apps 8-page stack (k-ui overflowed 16K), args page -> 0x1000 2026-10-28 13:48:08 +01:00
2b82f052ae feat(theme): per-app accent: pm gives window manifest accent before app runs (display ACCENT, refused after BUFFER); theme::with_accent keeps hue, shifts lightness until contrast passes, else theme accent; tally pink 2026-10-28 13:40:38 +01:00
6827e1455d feat(theme): themes as files (theme crate: toml subset + ints, contrast check, wire); whimsy dark + light; display reads themes/, serves THEME/THEME_NAME, init SET_THEME -> THEME_CHANGED to all windows, bar themed; k-ui follows; settings appearance; init keeps choice in sealed users/<id>/settings/, reapplies at sign-in + display restart 2026-10-28 13:36:57 +01:00
29becf297c feat(settings): settings app: apps list + per-capability allow/revoke, about (who/role), sign out; system-only caps permissions/session (install refused); pm MANAGE_BADGE copy (list/apps/allow/revoke only), APPS op; init WHO + session pass-on; factory decisions seeded for new system apps; k-ui list_item/toggle, screens/back/scroll 2026-10-28 13:14:35 +01:00
a2d4a7d085 perf(kernel): kernel static all-zero -> .bss (image 812K -> 173K): domain::Slot (repr(u8) Empty=0) for tables, zero generation counters, core ids by position, pool/platform installed at boot; Option-backed Slot under kani (cbmc speed); build fails if .data > 16K 2026-10-28 13:01:52 +01:00
419b715a1f fix(libkos): heap leaked padding/remainders + never coalesced (UI churn OOM'd tally); address-ordered free list, merge on free, split align padding; Heap<Pages> host-tested 2026-10-28 13:01:52 +01:00
fe73b89525 feat(k-ui): first slice: App trait + run loop, builders (column/row/text/button), flex layout, theme tokens (Whimsy Dark), tiny-skia + fontdue render (Noto Sans Latin subset), keyboard focus ring; tally demo; manifest memory key (pm spawns with it) 2026-10-28 03:20:41 +01:00
c256f4ecf3 feat(kernel): lazy user FPU: off until first FP insn (illegal-insn trap -> zeroed regs, retry), save on dirty trap, restore on switch, off for threads without + kernel; spawntest per-thread FP check 2026-10-28 03:20:41 +01:00
34a44b5928 fix(libkos): as_chunks for canvas pixels (clippy) 2026-10-28 02:57:21 +01:00
07a0575908 feat(init,display): loader streams ELF segments (any size, kills half-built proc); window pixels RGBA8 premul (tiny-skia native), canvas mapped plain memory + pixels_mut/touch 2026-10-28 02:57:05 +01:00
864f039bc7 feat(display): windows: full-screen, F1 cycles, OS bar (names, front, user); canvas-only (display-owned buffers, 4 max, reused); launchers OPEN (pm per app, terminal own), window caps minted by display; FORGET on launcher death via init courier; raster crate (font, chunked text); F-keys in input; terminal+snake on canvas; REDRAW gone 2026-10-28 02:48:39 +01:00
d04dcfffa1 feat(ipc): saved replies: SaveReply/ReplySaved (8 per thread, released on thread death, unforgeable); terminal READ saved until Enter (line.rs editor, reader queue), now pinged 2026-10-28 02:09:34 +01:00
3d69c10079 feat(init): watchdog: pinger thread PINGs servers (framework answers, no server code), main kills after 5s unanswered -> crash path; not pinged: terminal, pm (handlers wait for user); give-up drains queued ping; spawntest hang check 2026-10-28 01:51:39 +01:00
28f1ace00a feat(init): supervise servers+session progs: restart on death, same endpoint, backoff 0/1/5/30s then give up, restarts_clients cascade; release grants/spawn at stop (stale-slot safe); storage death fatal; init output via printer thread (never blocks on log); terminal Ctrl+C via CONTROL-badged copy, latest wins; flaky test server + spawntest restart/backoff check 2026-10-28 01:44:24 +01:00
c4871320a0 fix(clippy): make clippy = host libs + riscv programs (shared PROGRAMS); libkos alloc spinlock (was unlocked across threads); keystore Account; gpu Rect; factory Entry; Default impls, let-chains, div_ceil, iter loops 2026-10-28 01:17:21 +01:00
91f7d28834 feat(users): per-user sealed folders users/<id>/{home,apps,data,permissions}; storage SET_USER/LOCK/PURGE; init sign-in/sign-out loop (purge fresh/removed/guest, seed from factory, session start/stop); shell logout; pm user+system app shelves; terminal SIGNED_OUT; per-program stack (gatekeeper 12p); 128K load scratch 2026-10-28 01:08:50 +01:00
29a25bd8c2 feat(users): keystore v2 (8 slots, per-user salt+name tag, owner admin key opens sealed user dir, per-user duress); gatekeeper login by name+passcode, owner menu (add/remove/guests), passcode change, guest sessions 2026-10-28 00:29:07 +01:00
9dc751c466 feat(storage): per-app/owner/system encryption live: gatekeeper hands seed + fresh flag, storage unlocks layer, init seeds areas from factory/ (empties on new key); rmdir op; test-qemu checks sealed home + reboot persistence 2026-10-28 00:07:41 +01:00
e8191be160 feat(storage): encrypted areas layer (home, system, data/<id>) over any FS; locked until key; plaintext refused; tamper/truncation detected; tests on memory + real ext4 2026-10-27 23:55:35 +01:00
eb9e3efaac feat(ext4): seek, rmdir (e2fsck-clean) 2026-10-27 23:53:13 +01:00
24b0734c73 feat(filecrypt): per-area keys (BLAKE2s), per-file salt, XChaCha20-Poly1305 chunks bound to index+last, seed DRBG w/ fast key erasure 2026-10-27 23:51:29 +01:00
3b7d004285 feat(boot): gatekeeper gates boot (init→UI→gatekeeper→data); keystore disk + virtio-rng discovery; terminal READ_SECRET; storage SET_KEY; PageCount ≤8192, alloc cap gone; device window 0x01D0_0000; test-qemu types passcode over 2 boots 2026-10-27 23:44:05 +01:00
b7ef67ffd8 feat(gatekeeper): auth flow (setup, count-before-check, threshold/duress wipe, torn-write-safe resume) over Keystore/Random/Prompt ports; host tests 2026-10-27 23:17:18 +01:00
600140d518 feat(keystore): sealed real/duress slots (argon2id 16MiB/3, ChaCha20-Poly1305), torn-write-safe 2-copy record, wipe state; virtio-rng driver + Entropy port; RustCrypto deps 2026-10-27 23:06:25 +01:00
6b13bf8bbf feat(kernel): 64 MiB user pool (frame table 16384) 2026-10-27 23:03:19 +01:00
90e11eb2e8 feat(apps): permission manager equips apps (manifest+consent+decisions in /system), launches via init w/ file grant; shell unprivileged (/home); install/apps/allow/revoke; revoke stops running app; app-image tool; counter app 2026-10-27 22:56:44 +01:00
9bc31a9bbe feat(manifest): app manifest + decision crate (strict TOML subset, checked newtypes, capability catalog); docs/apps.md spec 2026-10-27 22:32:40 +01:00
ec3bc0d2a1 feat(snake): 8 steps/s on fixed-period clock (draw time no longer adds to step) 2026-10-27 22:14:35 +01:00
c8056ddac6 perf: fast display (row runs, bulk fills, pixel scroll), terminal draws changed cells only + scrolls pixels, print! batched (was IPC per fmt fragment), snake sleeps on timer 2026-10-27 22:09:33 +01:00
7b8258b231 refactor: ByteOffset newtype for window/register/ring offsets; named virtio-gpu cmd fields 2026-10-27 21:52:33 +01:00
842d14987c refactor: unsafe only in adapter + libkos; forbid(unsafe_code) everywhere else; libkos Window (volatile, bounds-checked MMIO/DMA/shared mem), entry!, thread::spawn; MapRegion returns pages; kernel const-built (with_cores/install_platform); newtype addrs 2026-10-27 21:43:24 +01:00
83809bbe94 feat(userspace): programs in /bin w/o .elf; bin manifests (needs = display) so shell runs snake; Ctrl+C kills foreground via terminal interrupt notification; ctrl keys; sleep 2026-10-27 21:16:00 +01:00
1fc5a54f06 feat(storage): scoped grants (subtree, ro/rw), narrow-only, release revokes all holders; IPC KEEP_DELEGATE opt-in; shell cd/pwd, programs get ro cwd 2026-10-27 20:20:34 +01:00
c603a578c0 feat(userspace): shell runs programs (ls, cat) via spawn w/ terminal+storage+args page; ext4 holes read as zeros; storage re-attach same buffer 2026-10-27 17:22:08 +01:00
0fd568cdad feat(storage): ext4-only disk via mkfs.ext4 -d; drop FAT16; feature-gated mount (ro on unknown ro_compat); ext4 read spans blocks, delete clears inode, mkdir counts dir; virtio-blk writes; test-qemu e2fsck gate 2026-10-27 17:09:35 +01:00
4030dd4315 feat(adapter): init+storage packed into kernel image; drop adapter FAT16 reader + virtio-blk 2026-10-27 16:58:11 +01:00
b80303f553 feat(userspace): terminal server + shell desktop; US keymap w/ shift; libkos allocator multi-page fix; panics print 2026-10-27 16:50:35 +01:00
2845d190f0 feat(display): input routed via display server to focused app; input Subscribe; uses caps badged + Delegate 2026-10-27 16:38:16 +01:00
64ec5df379 feat(display): DrawText with built-in Spleen 8x16 font (BSD-2) 2026-10-27 16:33:26 +01:00
8587661684 feat(init): spawn service over IPC with per-client allowance refunded on child death; up to 4 death watchers; spawntest on test image 2026-10-27 16:21:08 +01:00
817a38993b feat(kernel): WatchDeath + SetTimer events as notification signals; adapter wakes at earliest deadline 2026-10-27 16:06:04 +01:00
a4f467f45d feat(userspace): log server sole console holder; init boots from manifest; adapter loads only init+storage 2026-10-27 15:40:19 +01:00
4049ba5007 feat(kernel): per-process budgets (pages, endpoints, notifs, regions) carved from creator, returned on death; init owns pool 2026-10-27 15:08:58 +01:00
7415bab7f5 feat(kernel): console cap gates DebugPutchar, no uart fast path; spawn start arg in a0; delegate returns slot 2026-10-27 14:54:36 +01:00
33e1c849d1 feat(kernel): ipc cap transfer drops Delegate; one owner per irq line, freed when owner dies 2026-10-27 14:42:28 +01:00
37c482c36c chore: fix build warnings (unused vars/imports, dead ext4 param/field/fn) 2026-10-27 14:12:50 +01:00
b4390b7b6d refactor(userspace): split syscall/fat16/ext4 by concern; trim comments 2026-10-27 13:21:53 +01:00
99273db6d3 refactor(adapter): split trap/page_table/smp/entry by concern; trim comments 2026-10-27 13:21:53 +01:00
95dbe7b9d5 refactor(kernel): split ipc/memory/lib/thread by concern; trim comments 2026-10-27 13:21:53 +01:00
b814bd9570 test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu 2026-10-27 12:54:42 +01:00
909b3f0503 feat(storage): full file protocol over per-client shared region (badged clients, owned handles, clamped lengths, ReadOnly on unwritable disk); init loads via libkos::fs; drop boot shared buffer 2026-10-27 12:42:37 +01:00
694c675a40 feat: devices via caps (BootInfo, init assigns devices, drivers map MMIO/DMA themselves, IRQ-driven input); drop boot device wiring and info pages; headless display fallback 2026-10-27 12:28:39 +01:00
8bc5402a29 fix(userspace): clip display rects, bounds-check session app ids and fat16 handles, reject bad fat16 geometry, session kills via process cap 2026-10-27 12:11:13 +01:00
e8a2963b24 feat(userspace): Call/ReplyRecv IPC, malformed-request replies, servers exit on dead endpoint; drop send/receive compat, select!, async_syscall 2026-10-27 12:06:10 +01:00
2a8a5b0c27 perf(kernel): linked cap tree, translate-based page lookup, next-fit frames/heap, targeted dequeue; host benches 2026-10-27 01:33:26 +01:00
19aeabd86d feat: SMP stage 2, lock-free fast paths (GetTime, GetProcessId, DebugPutchar, no-op Yield), storm checks yield equivalence 2026-10-27 01:07:57 +01:00
2e665fd66e feat: SMP stage 1 (4 harts, per-hart guarded stacks, home-core affinity, IPIs, TLB shootdown, one temporary kernel lock), 4-core storm 2026-10-27 00:56:15 +01:00
b87a62475f feat(adapter): higher-half kernel (linked at PHYS_OFFSET, boot trampoline, no low mappings), fix SBI ecall clobbers 2026-10-27 00:25:20 +01:00
55758ae5b2 feat: kernel W^X (per-section perms, firmware unmapped, stack guard, boot check), halt on kernel faults, region W^X across processes 2026-10-27 00:13:00 +01:00
bafc18a5f8 feat(kernel): SystemControl cap for halt, board device table + boot caps to init, RegionAddress syscall, idle loop services irqs 2026-10-26 23:59:57 +01:00
130a82f50d feat(kernel): shared ABI decoder+proofs, no-panic gate, syscall storm, kani on shrunk kernel 2026-10-26 23:44:09 +01:00
154599e3af feat(kernel): derivation-tree revocation, region caps
- caps record parent (mint/delegate/ipc), CapRef w/ pid+slot gens
- Revoke = delete subtree, keep self; drop/death re-parents children
- CreateRegion/MapRegion; mapping lives only as long as its cap
- drop Permission::Revoke + object-wide revoke
- mint any cap (badge only ep/ntfn), process caps delegable
- kani: descendants == subtree; model-based fuzz vs spec
2026-10-26 20:41:03 +01:00
76f10ff1f1 feat(kernel): seL4-style IPC, notifications, status ABI
- badges + Mint, Call/Reply/ReplyRecv w/ gen-checked reply slot
- cap transfer (1/msg, copy, needs Delegate, rechecked at delivery)
- notifications: signal/wait/poll/bind, irq -> notification routing
- WaitAny/WaitForInterrupt removed; a0=status, ErrorCode
- libkos: single ecall primitive, compat wrappers + typed new api
- fix: trap return clobbered user t0 on every trap
- fix: wait queues FIFO, delegate keeps badge, fault cap checked
- docs/ipc.md spec, ipc tests, ipc fuzz, kani ntfn + waitqueue
2026-10-26 20:27:38 +01:00
d43421e33e feat(kernel): object lifetimes via slot generations
- SlotGenerations per table, never wraps, retires exhausted slots
- caps to dead/reused process, endpoint, region rejected
- endpoints owned, die w/ owner, waiters woken w/ error
- DropCap syscall + libkos wrapper
- irq table bounds-checked, routes dropped w/ thread
- self-kill terminates caller, exit_thread full cleanup
- sched run_now: switch to named thread, not queue head
- lifetime tests, fuzz kills/recreates, kani gen proofs
2026-10-26 19:54:23 +01:00
8d2c40cb1e feat(kernel): kernel-owned memory model
- FrameTable: per-frame owner/use, replaces PagePool + ProcessMemory
- Quota per process, page tables charged to owner
- Regions for shared mem + device MMIO, torn down w/ owner
- Platform: create/destroy AS, map/unmap, zero_frame; drop translate_va
- RequestPages/ReleasePages/MapPage(move) checked vs kernel records
- user window 0x1000..32MiB, private pts by construction
- boot loader + device setup go through kernel api
- kernel built in place, boot allocator bounded
- isolation tests, randomized invariant test, FrameTable kani proofs
- fixes: inflated release, unmapped release, dst va unchecked,
  heap overflow, unbounded pt alloc, unzeroed frames, double free
2026-10-26 19:40:50 +01:00
e931485ca7 fix: sched round-robin, cap slot OOB panic, clippy, make ci
- RunList -> intrusive FIFO, no starvation, no silent drop
- cap table sized to CapSlot range (kani caught OOB panic)
- snake food color test uses const
- kani cfg lint, RUST_MIN_STACK for big kernel struct in tests
2026-10-26 19:08:30 +01:00
1c04ddf909 style: cargo fmt kernel crates 2026-10-26 19:07:54 +01:00
458 changed files with 37723 additions and 6926 deletions

View File

@@ -1,3 +1,7 @@
[target.riscv64gc-unknown-none-elf] [target.riscv64gc-unknown-none-elf]
runner = "qemu-system-riscv64 -machine virt -nographic -bios default -kernel" runner = "qemu-system-riscv64 -machine virt -nographic -bios default -kernel"
rustflags = ["-C", "panic=abort"] rustflags = ["-C", "panic=abort"]
[env]
# Kernel tables are large fixed arrays; tests build `Kernel` by value.
RUST_MIN_STACK = "33554432"

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
/target /target
.DS_Store .DS_Store
*.img *.img

525
Cargo.lock generated
View File

@@ -12,16 +12,61 @@ version = "0.1.0"
dependencies = [ dependencies = [
"domain", "domain",
"elf", "elf",
"hw-virtio",
"kernel", "kernel",
"platform", "platform",
"types", "types",
] ]
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]] [[package]]
name = "alarm-server" name = "alarm-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "app-image"
version = "0.1.0"
dependencies = [
"manifest",
]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]] [[package]]
name = "audio-capture-server" name = "audio-capture-server"
version = "0.1.0" version = "0.1.0"
@@ -30,10 +75,40 @@ version = "0.1.0"
name = "audio-playback-server" name = "audio-playback-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bluetooth-server" name = "bluetooth-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "bytemuck"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]] [[package]]
name = "calendar-server" name = "calendar-server"
version = "0.1.0" version = "0.1.0"
@@ -42,6 +117,55 @@ version = "0.1.0"
name = "camera-server" name = "camera-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "cat"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]] [[package]]
name = "clipboard-server" name = "clipboard-server"
version = "0.1.0" version = "0.1.0"
@@ -50,12 +174,58 @@ version = "0.1.0"
name = "contacts-server" name = "contacts-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "counter"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "display-qemu" name = "display-qemu"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"display-server", "display-server",
"driver-framebuffer-mock",
"driver-framebuffer-virtio", "driver-framebuffer-virtio",
"libkos", "libkos",
"port-framebuffer", "port-framebuffer",
@@ -68,8 +238,11 @@ name = "display-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"driver-framebuffer-mock", "driver-framebuffer-mock",
"libkos",
"port-framebuffer", "port-framebuffer",
"raster",
"server-framework", "server-framework",
"theme",
"types", "types",
] ]
@@ -85,6 +258,7 @@ name = "driver-block-virtio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hw-virtio", "hw-virtio",
"libkos",
"port-block-device", "port-block-device",
"types", "types",
] ]
@@ -102,6 +276,7 @@ name = "driver-framebuffer-virtio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hw-virtio", "hw-virtio",
"libkos",
"port-framebuffer", "port-framebuffer",
"types", "types",
] ]
@@ -110,7 +285,9 @@ dependencies = [
name = "driver-input-uart" name = "driver-input-uart"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"libkos",
"port-input-device", "port-input-device",
"types",
] ]
[[package]] [[package]]
@@ -118,10 +295,21 @@ name = "driver-input-virtio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hw-virtio", "hw-virtio",
"libkos",
"port-input-device", "port-input-device",
"types", "types",
] ]
[[package]]
name = "driver-rng-virtio"
version = "0.1.0"
dependencies = [
"hw-virtio",
"libkos",
"port-entropy",
"types",
]
[[package]] [[package]]
name = "echo" name = "echo"
version = "0.1.0" version = "0.1.0"
@@ -134,14 +322,86 @@ dependencies = [
name = "elf" name = "elf"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "filecrypt"
version = "0.1.0"
dependencies = [
"blake2",
"chacha20",
"chacha20poly1305",
"zeroize",
]
[[package]]
name = "flaky"
version = "0.1.0"
dependencies = [
"libkos",
"server-framework",
"types",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "fontdue"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e796cb6894e6418daacea84b28dd4f98d3b32c74d5bb452ba1ecfbf5ad8e84ca"
dependencies = [
"hashbrown",
"ttf-parser",
]
[[package]] [[package]]
name = "gatekeeper" name = "gatekeeper"
version = "0.1.0" version = "0.1.0"
dependencies = [
"driver-block-virtio",
"driver-rng-virtio",
"keystore",
"libkos",
"port-block-device",
"port-entropy",
"types",
"zeroize",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "gps-server" name = "gps-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "hello" name = "hello"
version = "0.1.0" version = "0.1.0"
@@ -149,10 +409,21 @@ dependencies = [
"libkos", "libkos",
] ]
[[package]]
name = "home"
version = "0.1.0"
dependencies = [
"k-ui",
"libkos",
"manifest",
"types",
]
[[package]] [[package]]
name = "hw-virtio" name = "hw-virtio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"libkos",
"types", "types",
] ]
@@ -161,10 +432,21 @@ name = "init"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"elf", "elf",
"hw-virtio",
"libkos", "libkos",
"server-framework",
"types", "types",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "input-qemu" name = "input-qemu"
version = "0.1.0" version = "0.1.0"
@@ -173,6 +455,7 @@ dependencies = [
"driver-input-virtio", "driver-input-virtio",
"input-server", "input-server",
"libkos", "libkos",
"port-input-device",
"server-framework", "server-framework",
"types", "types",
] ]
@@ -187,6 +470,19 @@ dependencies = [
"types", "types",
] ]
[[package]]
name = "k-ui"
version = "0.1.0"
dependencies = [
"fontdue",
"libkos",
"libm",
"port-framebuffer",
"theme",
"tiny-skia",
"types",
]
[[package]] [[package]]
name = "kernel" name = "kernel"
version = "0.1.0" version = "0.1.0"
@@ -200,13 +496,42 @@ dependencies = [
name = "keychain-server" name = "keychain-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "keystore"
version = "0.1.0"
dependencies = [
"argon2",
"blake2",
"chacha20poly1305",
"zeroize",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]] [[package]]
name = "libkos" name = "libkos"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"port-framebuffer",
"types", "types",
] ]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]] [[package]]
name = "log-server" name = "log-server"
version = "0.1.0" version = "0.1.0"
@@ -216,6 +541,18 @@ dependencies = [
"types", "types",
] ]
[[package]]
name = "ls"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "manifest"
version = "0.1.0"
[[package]] [[package]]
name = "media-index-server" name = "media-index-server"
version = "0.1.0" version = "0.1.0"
@@ -228,6 +565,12 @@ version = "0.1.0"
name = "notification-server" name = "notification-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "package-manager" name = "package-manager"
version = "0.1.0" version = "0.1.0"
@@ -237,6 +580,7 @@ name = "permission-manager"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"libkos", "libkos",
"manifest",
"server-framework", "server-framework",
"types", "types",
] ]
@@ -245,14 +589,28 @@ dependencies = [
name = "platform" name = "platform"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"domain",
"types", "types",
] ]
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "port-block-device" name = "port-block-device"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "port-entropy"
version = "0.1.0"
[[package]] [[package]]
name = "port-framebuffer" name = "port-framebuffer"
version = "0.1.0" version = "0.1.0"
@@ -272,6 +630,15 @@ version = "0.1.0"
name = "print-server" name = "print-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "raster"
version = "0.1.0"
dependencies = [
"driver-framebuffer-mock",
"port-framebuffer",
"types",
]
[[package]] [[package]]
name = "server-framework" name = "server-framework"
version = "0.1.0" version = "0.1.0"
@@ -289,13 +656,57 @@ dependencies = [
"types", "types",
] ]
[[package]]
name = "settings"
version = "0.1.0"
dependencies = [
"k-ui",
"libkos",
"manifest",
"types",
]
[[package]]
name = "shell"
version = "0.1.0"
dependencies = [
"libkos",
"manifest",
"types",
]
[[package]]
name = "sleep"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "smptest"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]] [[package]]
name = "snake" name = "snake"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"display-server",
"libkos", "libkos",
"server-framework", "port-framebuffer",
"types",
]
[[package]]
name = "spawntest"
version = "0.1.0"
dependencies = [
"flaky",
"libkos",
"manifest",
"types", "types",
] ]
@@ -312,24 +723,116 @@ name = "storage-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"driver-block-virtio", "driver-block-virtio",
"filecrypt",
"libkos", "libkos",
"port-block-device", "port-block-device",
"server-framework", "server-framework",
"types", "types",
] ]
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sync-server" name = "sync-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "tally"
version = "0.1.0"
dependencies = [
"k-ui",
"libkos",
]
[[package]] [[package]]
name = "telephony-server" name = "telephony-server"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "terminal-server"
version = "0.1.0"
dependencies = [
"libkos",
"port-framebuffer",
"raster",
"server-framework",
"types",
]
[[package]]
name = "theme"
version = "0.1.0"
dependencies = [
"libm",
"manifest",
"types",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"bytemuck",
"cfg-if",
"log",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
dependencies = [
"arrayref",
"bytemuck",
"libm",
"strict-num",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
"core_maths",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]] [[package]]
name = "types" name = "types"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "usb-server" name = "usb-server"
version = "0.1.0" version = "0.1.0"
@@ -337,3 +840,15 @@ version = "0.1.0"
[[package]] [[package]]
name = "user-manager" name = "user-manager"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"

View File

@@ -1,11 +1,31 @@
[workspace] [workspace]
members = [ members = [
"crates/adapter-riscv", "crates/adapter-riscv",
"crates/raster",
"crates/k-ui",
"crates/apps/flaky",
"crates/apps/tally",
"crates/apps/settings",
"crates/apps/home",
"crates/theme",
"crates/apps/hello", "crates/apps/hello",
"crates/apps/shell",
"crates/apps/cat",
"crates/apps/counter",
"crates/apps/ls",
"crates/apps/echo", "crates/apps/echo",
"crates/apps/sleep",
"crates/apps/smptest",
"crates/apps/spawntest",
"crates/apps/snake", "crates/apps/snake",
"crates/domain", "crates/domain",
"crates/elf", "crates/elf",
"crates/manifest",
"crates/keystore",
"crates/filecrypt",
"crates/ports/entropy",
"crates/drivers/rng/virtio",
"crates/tools/app-image",
"crates/init", "crates/init",
"crates/kernel", "crates/kernel",
"crates/platform", "crates/platform",
@@ -33,6 +53,7 @@ members = [
"crates/servers/power-server", "crates/servers/power-server",
"crates/servers/print-server", "crates/servers/print-server",
"crates/servers/session-server", "crates/servers/session-server",
"crates/servers/terminal-server",
"crates/servers/storage-server", "crates/servers/storage-server",
"crates/servers/sync-server", "crates/servers/sync-server",
"crates/servers/telephony-server", "crates/servers/telephony-server",
@@ -61,6 +82,18 @@ resolver = "2"
types = { path = "crates/types" } types = { path = "crates/types" }
domain = { path = "crates/domain" } domain = { path = "crates/domain" }
elf = { path = "crates/elf" } elf = { path = "crates/elf" }
manifest = { path = "crates/manifest" }
theme = { path = "crates/theme" }
keystore = { path = "crates/keystore" }
filecrypt = { path = "crates/filecrypt" }
# Audited RustCrypto implementations; never our own.
argon2 = { version = "0.5", default-features = false }
chacha20poly1305 = { version = "0.10", default-features = false }
blake2 = { version = "0.10", default-features = false }
zeroize = { version = "1", default-features = false }
chacha20 = { version = "0.9", default-features = false }
port-entropy = { path = "crates/ports/entropy" }
driver-rng-virtio = { path = "crates/drivers/rng/virtio" }
platform = { path = "crates/platform" } platform = { path = "crates/platform" }
kernel = { path = "crates/kernel" } kernel = { path = "crates/kernel" }
server-framework = { path = "crates/server-framework" } server-framework = { path = "crates/server-framework" }
@@ -101,6 +134,8 @@ libkos = { path = "crates/libkos" }
# ports # ports
port-framebuffer = { path = "crates/ports/framebuffer" } port-framebuffer = { path = "crates/ports/framebuffer" }
raster = { path = "crates/raster" }
k-ui = { path = "crates/k-ui" }
port-block-device = { path = "crates/ports/block-device" } port-block-device = { path = "crates/ports/block-device" }
port-input-device = { path = "crates/ports/input-device" } port-input-device = { path = "crates/ports/input-device" }
@@ -123,3 +158,10 @@ panic = "abort"
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
# The randomized kernel invariant tests are too slow unoptimized.
[profile.test.package.kernel]
opt-level = 2
[profile.test.package.domain]
opt-level = 2

View File

@@ -1,57 +1,103 @@
.PHONY: check fmt clippy test kani build all image run .PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test test-qemu
RISCV_TARGET = riscv64gc-unknown-none-elf RISCV_TARGET = riscv64gc-unknown-none-elf
KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv
# Harts QEMU boots; `make run-gui SMP=1` for a single core.
SMP ?= 4
KERNEL_CRATES = -p types -p domain -p kernel -p platform
all: check fmt clippy test all: check fmt clippy test
# Gate for kernel work: formatting, lints, unit tests, Kani proofs, RISC-V build.
ci: kernel-fmt kernel-clippy kernel-test kani build
kernel-fmt:
cargo fmt $(KERNEL_CRATES) -p adapter-riscv -- --check
kernel-clippy:
cargo clippy $(KERNEL_CRATES) --all-targets -- -D warnings
cargo clippy -p adapter-riscv --target $(RISCV_TARGET) --no-deps -- -D warnings
kernel-test:
cargo test $(KERNEL_CRATES)
check: check:
cargo check --workspace cargo check --workspace
fmt: fmt:
cargo fmt --all -- --check cargo fmt --all -- --check
# Libraries on the host; programs (no_std, they link only for RISC-V) for
# their target, as `build` builds them. The adapter is `kernel-clippy`'s.
clippy: clippy:
cargo clippy --workspace -- -D warnings cargo clippy --workspace --exclude adapter-riscv --lib -- -D warnings
cargo clippy --target $(RISCV_TARGET) --release $(PROGRAMS) -- -D warnings
test: test:
cargo test --workspace cargo test --workspace
# CBMC can eat all RAM and take the desktop down with it; cap each run.
KANI = systemd-run --user --scope -q -p MemoryMax=12G -p MemorySwapMax=0 cargo kani
kani-types: kani-types:
cargo kani -p types $(KANI) -p types
kani-domain: kani-domain:
cargo kani -p domain $(KANI) -p domain
kani-kernel: kani-kernel:
cargo kani -p kernel $(KANI) -p kernel
kani: kani-types kani-domain kani-kernel kani: kani-types kani-domain kani-kernel
PROGRAMS = -p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls -p counter -p gatekeeper -p flaky -p tally -p settings -p home
# Programs first: the kernel image packs init and storage (see the
# adapter's build.rs).
build: build:
cargo build --target $(RISCV_TARGET) --release \ cargo build --target $(RISCV_TARGET) --release $(PROGRAMS)
-p adapter-riscv -p init -p echo -p snake \ cargo build --target $(RISCV_TARGET) --release -p adapter-riscv
-p display-qemu -p input-qemu -p storage-qemu \ @# The kernel's tables are one static: if every empty entry is zero it
-p log-server -p permission-manager -p session-server @# lives in .bss and costs nothing in the image (see domain::slot).
@data=$$(llvm-size -A $(KERNEL_BIN) | awk '$$1 == ".data" { print $$2 }'); \
if [ "$${data:-0}" -gt 16384 ]; then \
echo "kernel .data is $$data bytes: some table's empty value is not all zero bytes (use domain::slot::Slot)"; exit 1; \
fi
# The keystore holds the owner's sealed keys: made once, never rebuilt.
KEYS_IMG ?= keys.img
QEMU_KEYS = -drive file=$(KEYS_IMG),if=none,format=raw,id=ks -device virtio-blk-device,drive=ks \
-device virtio-rng-device
image: build image: build
scripts/make-disk-image.sh scripts/make-disk-image.sh
[ -f $(KEYS_IMG) ] || truncate -s 64K $(KEYS_IMG)
# Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs
# the SMP self-test and halts. Passes only on "SMPTEST PASS".
test-qemu: build
IMG=test.img SMPTEST=1 scripts/make-disk-image.sh > /dev/null
SMP=$(SMP) scripts/test-qemu.sh test.img
run: build run: build
qemu-system-riscv64 -machine virt -nographic -bios default \ qemu-system-riscv64 -machine virt -m 512M -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) -kernel $(KERNEL_BIN)
run-disk: image run-disk: image
qemu-system-riscv64 -machine virt -nographic -bios default \ qemu-system-riscv64 -machine virt -m 512M -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 -device virtio-blk-device,drive=hd0 $(QEMU_KEYS)
run-gui: image run-gui: image
qemu-system-riscv64 -machine virt -bios default \ qemu-system-riscv64 -machine virt -m 512M -smp $(SMP) -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \ -device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \
-device virtio-gpu-device \ -device virtio-gpu-device \
-device virtio-keyboard-device \ -device virtio-keyboard-device -device virtio-tablet-device \
-serial stdio -serial stdio

View File

@@ -14,7 +14,6 @@ domain = { workspace = true }
elf = { workspace = true } elf = { workspace = true }
platform = { workspace = true } platform = { workspace = true }
kernel = { workspace = true } kernel = { workspace = true }
hw-virtio = { workspace = true }
[profile.release] [profile.release]
panic = "abort" panic = "abort"

View File

@@ -1,5 +1,29 @@
use std::path::PathBuf;
fn main() { fn main() {
let ld = std::path::Path::new("linker.ld").canonicalize().unwrap(); let ld = std::path::Path::new("linker.ld").canonicalize().unwrap();
println!("cargo:rustc-link-arg=-T{}", ld.display()); println!("cargo:rustc-link-arg=-T{}", ld.display());
println!("cargo:rerun-if-changed=linker.ld"); println!("cargo:rerun-if-changed=linker.ld");
// Init and storage are packed into the kernel image: they are what
// reads the disk, so they cannot come from it. `make build` builds them
// first; without them the image boots to "init missing".
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let bins = manifest.join("../../target/riscv64gc-unknown-none-elf/release");
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());
for (var, bin) in [
("KOS_INIT_ELF", "init"),
("KOS_STORAGE_ELF", "storage-qemu"),
] {
let path = bins.join(bin);
println!("cargo:rerun-if-changed={}", path.display());
let path = if path.is_file() {
path
} else {
let empty = out.join(bin);
std::fs::write(&empty, []).unwrap();
empty
};
println!("cargo:rustc-env={}={}", var, path.display());
}
} }

View File

@@ -1,36 +1,60 @@
OUTPUT_ARCH(riscv) OUTPUT_ARCH(riscv)
ENTRY(_start)
/* The kernel runs in the upper half: linked at its physical load address
plus PHYS_OFFSET (mmu::PHYS_OFFSET), loaded at the physical address.
Firmware jumps to the physical entry with the MMU off; _start switches
to the high mapping before any Rust code runs. */
PHYS_OFFSET = 0xFFFFFFC000000000;
LOAD_ADDRESS = 0x80200000;
ENTRY(_start_phys)
/* Every boundary below is page aligned: the kernel maps each range with
its own permissions (W^X), see mmu::init_kernel_page_table. */
SECTIONS { SECTIONS {
. = 0x80200000; . = PHYS_OFFSET + LOAD_ADDRESS;
_text_start = .;
.text : { .text : AT(ADDR(.text) - PHYS_OFFSET) {
*(.text.entry) *(.text.entry)
*(.text .text.*) *(.text .text.*)
} }
. = ALIGN(4096); . = ALIGN(4096);
.rodata : { _text_end = .;
.rodata : AT(ADDR(.rodata) - PHYS_OFFSET) {
*(.rodata .rodata.*) *(.rodata .rodata.*)
*(.srodata .srodata.*)
}
.eh_frame : AT(ADDR(.eh_frame) - PHYS_OFFSET) {
*(.eh_frame)
} }
. = ALIGN(4096); . = ALIGN(4096);
.data : { _rodata_end = .;
.data : AT(ADDR(.data) - PHYS_OFFSET) {
*(.data .data.*) *(.data .data.*)
*(.sdata .sdata.*)
} }
. = ALIGN(4096); . = ALIGN(4096);
.bss : { .bss : AT(ADDR(.bss) - PHYS_OFFSET) {
_bss_start = .; _bss_start = .;
*(.bss .bss.*) *(.bss .bss.*)
*(.sbss .sbss.*) *(.sbss .sbss.*)
_bss_end = .; _bss_end = .;
} }
/* One kernel stack per hart (smp::MAX_HARTS, smp::STACK_SLOT): each
slot is an unmapped guard page, so overflowing a stack faults
instead of silently overwriting what lies below, then the stack. */
. = ALIGN(4096); . = ALIGN(4096);
_stack_bottom = .; _stacks_start = .;
. += 0x80000; . += 8 * (4096 + 0x80000);
_stack_top = .; _stacks_end = .;
_heap_start = .; _heap_start = .;
} }
_start_phys = _start - PHYS_OFFSET;

View File

@@ -1,10 +1,47 @@
pub const RAM_BASE: usize = 0x80000000; pub const RAM_BASE: usize = 0x80000000;
pub const RAM_SIZE: usize = 128 * 1024 * 1024; /// What the Makefile and scripts give QEMU (`-m 512M`).
pub const RAM_SIZE: usize = 512 * 1024 * 1024;
pub const UART_BASE: usize = 0x10000000; pub const UART_BASE: usize = 0x10000000;
pub const VIRTIO_BASE: usize = 0x10001000; pub const VIRTIO_BASE: usize = 0x10001000;
pub const VIRTIO_SIZE: usize = 0x8000; pub const VIRTIO_SIZE: usize = 0x8000;
pub const PLIC_BASE: usize = 0x0C000000; pub const PLIC_BASE: usize = 0x0C000000;
pub const CLINT_BASE: usize = 0x02000000; pub const CLINT_BASE: usize = 0x02000000;
pub const USER_POOL_BASE: usize = 0x84000000; pub const USER_POOL_BASE: usize = 0x84000000;
pub const USER_POOL_PAGES: usize = 4096; /// The rest of RAM, from `USER_POOL_BASE` (448 MiB).
pub const USER_POOL_PAGES: usize = (RAM_BASE + RAM_SIZE - USER_POOL_BASE) / 4096;
pub const TICK_INTERVAL: u64 = 10_000_000; pub const TICK_INTERVAL: u64 = 10_000_000;
/// QEMU virt's PLIC lines.
const UART_IRQ: u16 = 10;
const VIRTIO_IRQ_BASE: u16 = 1;
const VIRTIO_SLOT_SIZE: usize = 0x1000;
const VIRTIO_SLOTS: usize = VIRTIO_SIZE / VIRTIO_SLOT_SIZE;
pub const BOARD_DEVICES: usize = 1 + VIRTIO_SLOTS;
/// What each entry of `devices()` is, for init's BootInfo.
pub fn kinds() -> [u64; BOARD_DEVICES] {
let mut kinds = [types::bootinfo::KIND_VIRTIO; BOARD_DEVICES];
kinds[0] = types::bootinfo::KIND_UART_16550;
kinds
}
/// Every device userspace may drive: the UART and each VirtIO MMIO slot.
/// The PLIC, CLINT and RAM stay the kernel's.
pub fn devices() -> Option<[kernel::BoardDevice; BOARD_DEVICES]> {
use types::{address::PhysicalAddress, bounded::PageCount, id::IrqId};
let one = PageCount::new(1)?;
let device = |base: usize, irq: u16| kernel::BoardDevice {
base: PhysicalAddress::from_raw(base),
pages: one,
irq: Some(IrqId::from_raw(irq)),
};
let mut all = [device(UART_BASE, UART_IRQ); BOARD_DEVICES];
for (i, d) in all.iter_mut().skip(1).enumerate() {
*d = device(
VIRTIO_BASE + i * VIRTIO_SLOT_SIZE,
VIRTIO_IRQ_BASE + i as u16,
);
}
Some(all)
}

View File

@@ -5,54 +5,46 @@ fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() } unsafe { &mut *crate::kernel_ptr() }
} }
pub fn grant_all( /// Init gets storage's process and endpoint (slots 1 and 2), storage its
uart: &Uart, /// endpoint (slot 0); everything else init starts itself.
init_pid: ProcessId, pub fn grant_all(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId) {
storage_pid: ProcessId,
display_pid: ProcessId,
input_pid: ProcessId,
) {
let all_perms = types::permission::PermissionSet::empty() let all_perms = types::permission::PermissionSet::empty()
.with(types::permission::Permission::Read) .with(types::permission::Permission::Read)
.with(types::permission::Permission::Write) .with(types::permission::Permission::Write)
.with(types::permission::Permission::Execute) .with(types::permission::Permission::Execute)
.with(types::permission::Permission::Delegate); .with(types::permission::Permission::Delegate);
// Delegate so storage can mint the badged copies it grants.
let ipc_perms = types::permission::PermissionSet::empty() let ipc_perms = types::permission::PermissionSet::empty()
.with(types::permission::Permission::Read) .with(types::permission::Permission::Read)
.with(types::permission::Permission::Execute); .with(types::permission::Permission::Execute)
.with(types::permission::Permission::Delegate);
let storage_ep = kern().create_endpoint() let storage_ep = kern()
.create_endpoint(storage_pid)
.unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(storage_pid), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Process(storage_pid),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(storage_ep), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Endpoint(storage_ep),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(storage_pid, domain::resource::Resource::Endpoint(storage_ep), ipc_perms) kern()
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .grant_capability(
storage_pid,
let display_ep = kern().create_endpoint() domain::resource::Resource::Endpoint(storage_ep),
.unwrap_or_else(|_| crate::halt(uart, "display endpoint failed")); ipc_perms,
)
kern().grant_capability(display_pid, domain::resource::Resource::Endpoint(display_ep), ipc_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(display_ep), all_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(display_pid), all_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
let input_ep = kern().create_endpoint()
.unwrap_or_else(|_| crate::halt(uart, "input endpoint failed"));
kern().grant_capability(input_pid, domain::resource::Resource::Endpoint(input_ep), ipc_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(input_ep), all_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(input_pid), all_perms)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
uart.write_str("k-os: capabilities granted\n"); uart.write_str("k-os: capabilities granted\n");

View File

@@ -1,6 +1,5 @@
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk; use crate::{board, mmu, platform, trap};
use crate::{board, kernel_cell::KernelCell, mmu, platform, trap, KERNEL};
unsafe extern "C" { unsafe extern "C" {
static _bss_start: u8; static _bss_start: u8;
@@ -8,40 +7,64 @@ unsafe extern "C" {
static _heap_start: u8; static _heap_start: u8;
} }
pub fn init(uart: &Uart) -> &'static mut mmu::PageTable { pub fn init(uart: &Uart, boot_hart: usize) -> &'static mut mmu::PageTable {
zero_bss(); zero_bss();
let heap_start = unsafe { &_heap_start as *const u8 as usize }; let heap_start = unsafe { &_heap_start as *const u8 as usize };
mmu::init_allocator(heap_start); mmu::init_allocator(heap_start);
let kernel_pt = mmu::init_kernel_page_table(); let kernel_pt = mmu::init_kernel_page_table();
let harts = crate::smp::probe_harts();
if boot_hart >= harts {
crate::halt(uart, "boot hart outside the hart range");
}
crate::smp::init(
boot_hart,
harts,
mmu::virt_to_phys(kernel_pt as *const mmu::PageTable as usize),
);
mmu::activate(kernel_pt); mmu::activate(kernel_pt);
uart.write_str("k-os: MMU enabled (Sv39)\n"); uart.write_str("k-os: MMU enabled (Sv39)\n");
match mmu::check_wx(kernel_pt) {
Ok(code_pages) => {
uart.write_str("k-os: W^X verified, ");
uart.write_dec(code_pages);
uart.write_str(" code pages\n");
}
Err(v) => {
let (what, va) = match v {
mmu::page_table::WxViolation::WritableCode(va) => ("writable code", va),
mmu::page_table::WxViolation::ExecutableData(va) => ("executable data", va),
mmu::page_table::WxViolation::MappedHole(va) => ("mapped hole", va),
mmu::page_table::WxViolation::LowerHalf(va) => ("lower-half mapping", va),
};
uart.write_str("k-os: W^X violation: ");
uart.write_str(what);
uart.write_str(" at ");
uart.write_hex(va);
uart.put_char(b'\n');
crate::halt(uart, "kernel page table violates W^X");
}
}
trap::init(); trap::init();
crate::hw::timer::init(); crate::hw::timer::init();
crate::hw::plic::Plic::init(); crate::hw::plic::Plic::init();
uart.write_str("k-os: traps and timer initialized\n"); uart.write_str("k-os: traps and timer initialized\n");
let riscv_platform = platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize); if board::USER_POOL_PAGES > domain::memory::frame::MAX_FRAMES {
let pool = domain::memory::page_pool::PagePool::new( crate::halt(uart, "user pool larger than the frame table");
}
let riscv_platform =
platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize, harts as u8);
crate::kern_mut().install_platform(riscv_platform);
crate::kern_mut().install_pool(
types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE), types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE),
board::USER_POOL_PAGES, board::USER_POOL_PAGES,
); );
KERNEL.init(kernel::Kernel::new(riscv_platform, pool));
kernel_pt kernel_pt
} }
pub fn probe_block_device(uart: &Uart) -> Option<VirtioBlk> {
let blk = VirtioBlk::probe();
if blk.is_some() {
uart.write_str("k-os: VirtIO block device found\n");
} else {
uart.write_str("k-os: no block device\n");
}
blk
}
fn zero_bss() { fn zero_bss() {
unsafe { unsafe {
let start = &_bss_start as *const u8 as *mut u8; let start = &_bss_start as *const u8 as *mut u8;

View File

@@ -1,84 +1,69 @@
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk; use crate::mmu;
use crate::{boot_fs, mmu}; use types::address::VirtualAddress;
use types::budget::Limits;
use types::id::{ProcessId, ThreadId}; use types::id::{ProcessId, ThreadId};
use types::mapping::PagePerms;
const PAGE_SIZE: usize = mmu::PAGE_SIZE;
const USER_VA_BASE: usize = 0x10000; const USER_VA_BASE: usize = 0x10000;
const MAX_FILE_SIZE: usize = 64 * 1024; /// The stack sits just below the image and stops short of the null page.
const USER_STACK_PAGES: usize = USER_VA_BASE / PAGE_SIZE - 1;
pub struct LoadedProcess { pub struct LoadedProcess {
pub pid: ProcessId, pub pid: ProcessId,
pub entry: usize,
} }
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> { fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() } unsafe { &mut *crate::kernel_ptr() }
} }
pub fn load_init( /// The two programs boot starts, packed into the kernel image.
uart: &Uart, pub static INIT_ELF: &[u8] = include_bytes!(env!("KOS_INIT_ELF"));
blk: &mut VirtioBlk, pub static STORAGE_ELF: &[u8] = include_bytes!(env!("KOS_STORAGE_ELF"));
kernel_pt: &mmu::PageTable,
) -> (LoadedProcess, ThreadId) { pub fn load_init(uart: &Uart) -> LoadedProcess {
let (pid, tid) = kern() let (pid, tid) = kern()
.boot(types::bounded::Priority::DEFAULT, types::time::TimeSlice::DEFAULT) .boot(
types::bounded::Priority::DEFAULT,
types::time::TimeSlice::DEFAULT,
)
.unwrap_or_else(|_| crate::halt(uart, "boot failed")); .unwrap_or_else(|_| crate::halt(uart, "boot failed"));
let entry = load_elf(blk, kernel_pt, pid, b"init.elf"); let entry = load_elf(uart, pid, INIT_ELF);
if entry == 0 { crate::halt(uart, "init.elf not found"); }
setup_thread(tid, entry); setup_thread(tid, entry);
uart.write_str("k-os: init loaded\n"); uart.write_str("k-os: init loaded\n");
(LoadedProcess { pid, entry }, tid) LoadedProcess { pid }
} }
pub fn load_server( /// Storage's budget, carved from init's.
uart: &Uart, const STORAGE: Limits = {
blk: &mut VirtioBlk, use types::storage::{BUFFER_PAGES, MAX_CLIENTS};
kernel_pt: &mmu::PageTable, Limits {
filename: &[u8], // A buffer per client, and its image, heap and cache.
label: &str, pages: MAX_CLIENTS * BUFFER_PAGES + 256,
) -> LoadedProcess { endpoints: 2,
let pid = kern().create_process_internal() notifications: 2,
// A buffer per client, and the disk's DMA.
regions: MAX_CLIENTS + 4,
}
};
/// Storage, which init needs to read anything else, its budget carved
/// from init's.
pub fn load_storage(uart: &Uart, init: ProcessId) -> LoadedProcess {
let pid = kern()
.create_process_internal(init, STORAGE)
.unwrap_or_else(|| crate::halt(uart, "process create failed")); .unwrap_or_else(|| crate::halt(uart, "process create failed"));
let entry = load_elf(blk, kernel_pt, pid, filename); let entry = load_elf(uart, pid, STORAGE_ELF);
if entry == 0 { crate::halt(uart, "elf not found"); } let tid = kern()
let tid = kern().spawn_thread_internal(pid) .spawn_thread_internal(pid)
.unwrap_or_else(|| crate::halt(uart, "thread spawn failed")); .unwrap_or_else(|| crate::halt(uart, "thread spawn failed"));
setup_thread(tid, entry); setup_thread(tid, entry);
uart.write_str("k-os: "); uart.write_str("k-os: storage loaded\n");
uart.write_str(label);
uart.write_str(" loaded\n");
LoadedProcess { pid, entry } LoadedProcess { pid }
}
pub fn enter_userspace(pid: ProcessId, entry: usize, shared_buf_va: usize) -> ! {
let pt_addr = kern().platform_mut().page_table_for(pid);
if pt_addr != 0 {
mmu::activate(unsafe { &*(pt_addr as *const mmu::PageTable) });
}
let kernel_sp: usize;
unsafe { core::arch::asm!("mv {0}, sp", out(reg) kernel_sp) };
crate::trap::set_kernel_stack(kernel_sp);
unsafe {
core::arch::asm!(
"csrc sstatus, {spp}",
"csrs sstatus, {spie}",
"csrw sepc, {entry}",
"mv sp, {stack}",
"mv a0, {arg}",
"sret",
spp = in(reg) 1usize << 8,
spie = in(reg) 1usize << 5,
entry = in(reg) entry,
stack = in(reg) USER_VA_BASE,
arg = in(reg) shared_buf_va,
options(noreturn),
);
}
} }
fn setup_thread(tid: ThreadId, entry: usize) { fn setup_thread(tid: ThreadId, entry: usize) {
@@ -89,88 +74,86 @@ fn setup_thread(tid: ThreadId, entry: usize) {
} }
} }
fn load_elf( /// Copies `data` into `pid`'s already mapped pages starting at `va`.
blk: &mut VirtioBlk, fn copy_to_user(pid: ProcessId, va: usize, data: &[u8]) -> bool {
kernel_pt: &mmu::PageTable, let mut done = 0;
pid: ProcessId, while done < data.len() {
filename: &[u8], let addr = va + done;
) -> usize { let page = addr & !(PAGE_SIZE - 1);
let buf_base = mmu::allocator::alloc_zeroed_page(); let offset = addr - page;
let file_pages = MAX_FILE_SIZE / 4096; let Some(pa) = kern().user_page_frame(pid, VirtualAddress::from_raw(page)) else {
for _ in 1..file_pages { return false;
mmu::allocator::alloc_zeroed_page(); };
let len = (PAGE_SIZE - offset).min(data.len() - done);
// SAFETY: `pa` is a pool frame the kernel just mapped for `pid`;
// pool frames are RAM, mapped at `phys_to_virt`.
unsafe {
core::ptr::copy_nonoverlapping(
data.as_ptr().add(done),
(mmu::phys_to_virt(pa.raw()) as *mut u8).add(offset),
len,
);
}
done += len;
} }
let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) }; true
}
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0); fn segment_perms(flags: elf::SegmentFlags) -> Option<PagePerms> {
if file_size == 0 { return 0; } match (flags.write, flags.execute) {
(true, true) => None,
(false, true) => Some(PagePerms::ReadExecute),
(true, false) => Some(PagePerms::ReadWrite),
(false, false) => Some(PagePerms::ReadOnly),
}
}
let elf_info = match elf::parse(&buf[..file_size]) { /// Loads `filename` into `pid` through the kernel's own mapping API, so
Ok(info) => info, /// every page is tracked, charged and checked like any other. Halts on any
Err(_) => return 0, /// malformed or overlapping image: boot servers are trusted to be sane.
}; fn load_elf(uart: &Uart, pid: ProcessId, file: &[u8]) -> usize {
if file.is_empty() {
let user_pt = mmu::create_user_page_table( crate::halt(uart, "program missing from the kernel image (make build)");
kernel_pt, }
types::address::PhysicalAddress::from_raw(0), let elf_info = elf::parse(file).unwrap_or_else(|_| crate::halt(uart, "bad ELF"));
0,
types::address::VirtualAddress::from_raw(USER_VA_BASE),
);
for segment in elf_info.segments() { for segment in elf_info.segments() {
let page_start = segment.vaddr & !0xFFF; let perms =
let offset_in_page = segment.vaddr & 0xFFF; segment_perms(segment.flags).unwrap_or_else(|| crate::halt(uart, "W+X segment"));
let total_size = offset_in_page + segment.mem_size; let page_start = segment.vaddr & !(PAGE_SIZE - 1);
let seg_pages = (total_size + 4095) / 4096; let offset_in_page = segment.vaddr - page_start;
let pages = offset_in_page
.checked_add(segment.mem_size)
.map(|total| total.div_ceil(PAGE_SIZE))
.unwrap_or_else(|| crate::halt(uart, "segment too large"));
if pages == 0 {
continue;
}
for p in 0..seg_pages { kern()
let page = mmu::allocator::alloc_zeroed_page(); .map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms)
let page_va = page_start + p * 4096; .unwrap_or_else(|_| crate::halt(uart, "cannot map segment"));
let seg_file_start = if p == 0 { 0 } else { p * 4096 - offset_in_page };
let dest_off = if p == 0 { offset_in_page } else { 0 };
let copy_len = if seg_file_start < segment.file_size {
(segment.file_size - seg_file_start).min(4096 - dest_off)
} else { 0 };
if copy_len > 0 { let copy_len = segment.file_size.min(segment.mem_size);
let src = segment.file_offset + seg_file_start; let data = segment
if src + copy_len <= file_size { .file_offset
unsafe { .checked_add(copy_len)
core::ptr::copy_nonoverlapping( .and_then(|end| file.get(segment.file_offset..end))
buf.as_ptr().add(src), .unwrap_or_else(|| crate::halt(uart, "segment outside file"));
(page as *mut u8).add(dest_off), if !copy_to_user(pid, segment.vaddr, data) {
copy_len, crate::halt(uart, "segment copy failed");
);
}
}
}
let flags = if segment.flags.execute {
mmu::Sv39Flags::user_rx()
} else if segment.flags.write {
mmu::Sv39Flags::user_rw()
} else {
mmu::Sv39Flags::user_rx()
};
mmu::map(user_pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(page_va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags,
});
} }
} }
for i in 0..16usize { let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE;
let sp = mmu::allocator::alloc_zeroed_page(); kern()
mmu::map(user_pt, mmu::Mapping { .map_fresh_range(
va: types::address::VirtualAddress::from_raw(USER_VA_BASE - (16 - i) * 4096), pid,
pa: types::address::PhysicalAddress::from_raw(sp), VirtualAddress::from_raw(stack_bottom),
pages: 1, USER_STACK_PAGES,
flags: mmu::Sv39Flags::user_rw(), PagePerms::ReadWrite,
}); )
} .unwrap_or_else(|_| crate::halt(uart, "cannot map stack"));
kern().platform_mut().set_page_table(pid, user_pt as *const mmu::PageTable as usize);
elf_info.entry_point elf_info.entry_point
} }

View File

@@ -1,125 +0,0 @@
use crate::hw::virtio_blk::VirtioBlk;
const SECTOR_SIZE: usize = 512;
const MAX_FILE_SIZE: usize = 64 * 1024; // 64K max for init binary
struct BootFat16 {
reserved_sectors: u16,
sectors_per_fat: u16,
root_dir_start: u32,
root_dir_sectors: u32,
data_start: u32,
sectors_per_cluster: u8,
}
impl BootFat16 {
fn parse(boot_sector: &[u8; SECTOR_SIZE]) -> Option<Self> {
if boot_sector[510] != 0x55 || boot_sector[511] != 0xAA {
return None;
}
let bytes_per_sector = u16::from_le_bytes([boot_sector[11], boot_sector[12]]);
if bytes_per_sector != SECTOR_SIZE as u16 { return None; }
let sectors_per_cluster = boot_sector[13];
let reserved_sectors = u16::from_le_bytes([boot_sector[14], boot_sector[15]]);
let num_fats = boot_sector[16];
let root_entry_count = u16::from_le_bytes([boot_sector[17], boot_sector[18]]);
let sectors_per_fat = u16::from_le_bytes([boot_sector[22], boot_sector[23]]);
let fat_start = reserved_sectors as u32;
let root_dir_start = fat_start + (num_fats as u32) * (sectors_per_fat as u32);
let root_dir_sectors = ((root_entry_count as u32) * 32 + 511) / 512;
let data_start = root_dir_start + root_dir_sectors;
Some(Self {
reserved_sectors,
sectors_per_fat,
root_dir_start,
root_dir_sectors,
data_start,
sectors_per_cluster,
})
}
fn cluster_to_sector(&self, cluster: u16) -> u32 {
self.data_start + ((cluster as u32) - 2) * (self.sectors_per_cluster as u32)
}
}
fn to_fat_name(name: &[u8]) -> [u8; 11] {
let mut fat = [b' '; 11];
let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len());
for i in 0..dot_pos.min(8) { fat[i] = name[i].to_ascii_uppercase(); }
if dot_pos < name.len() {
for i in 0..(name.len() - dot_pos - 1).min(3) {
fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase();
}
}
fat
}
pub fn read_file_from_disk(
blk: &mut VirtioBlk,
filename: &[u8],
out_buf: &mut [u8],
) -> Option<usize> {
let mut sector_buf = [0u8; SECTOR_SIZE];
// Read boot sector
if !blk.read_sector(0, &mut sector_buf) { return None; }
let fs = BootFat16::parse(&sector_buf)?;
let fat_name = to_fat_name(filename);
// Search root directory
let mut first_cluster: u16 = 0;
let mut file_size: u32 = 0;
let mut found = false;
for s in 0..fs.root_dir_sectors {
if !blk.read_sector((fs.root_dir_start + s) as u32, &mut sector_buf) { continue; }
for i in 0..(SECTOR_SIZE / 32) {
let off = i * 32;
if sector_buf[off] == 0x00 { break; }
if sector_buf[off] == 0xE5 { continue; }
if sector_buf[off..off + 11] == fat_name {
first_cluster = u16::from_le_bytes([sector_buf[off + 26], sector_buf[off + 27]]);
file_size = u32::from_le_bytes([
sector_buf[off + 28], sector_buf[off + 29],
sector_buf[off + 30], sector_buf[off + 31],
]);
found = true;
break;
}
}
if found { break; }
}
if !found { return None; }
let size = (file_size as usize).min(out_buf.len());
let mut read = 0;
let mut cluster = first_cluster;
let sectors_per_cluster = fs.sectors_per_cluster as u32;
while read < size && cluster >= 2 && cluster < 0xFFF8 {
for s in 0..sectors_per_cluster {
if read >= size { break; }
let sector = fs.cluster_to_sector(cluster) + s;
if !blk.read_sector(sector, &mut sector_buf) { return Some(read); }
let chunk = (size - read).min(SECTOR_SIZE);
out_buf[read..read + chunk].copy_from_slice(&sector_buf[..chunk]);
read += chunk;
}
// Read next cluster from FAT
let fat_offset = (cluster as u32) * 2;
let fat_sector = fs.reserved_sectors as u32 + fat_offset / SECTOR_SIZE as u32;
let fat_byte = (fat_offset % SECTOR_SIZE as u32) as usize;
if !blk.read_sector(fat_sector, &mut sector_buf) { break; }
cluster = u16::from_le_bytes([sector_buf[fat_byte], sector_buf[fat_byte + 1]]);
}
Some(read)
}

View File

@@ -1,109 +0,0 @@
use crate::hw::uart::Uart;
use crate::hw::virtio_gpu::VirtioGpu;
use crate::mmu;
use types::display::Dimension;
use types::id::ProcessId;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
const GPU_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000;
const CMDBUF_VA: usize = 0x23000;
const PHYSINFO_VA: usize = 0x24000;
const FB_VA: usize = 0x100000;
const DEFAULT_WIDTH: Dimension = Dimension::new(640);
const DEFAULT_HEIGHT: Dimension = Dimension::new(480);
pub fn configure(uart: &Uart, display_pid: ProcessId) {
let gpu = match VirtioGpu::probe() {
Some(g) => g,
None => {
uart.write_str("k-os: no GPU (headless)\n");
return;
}
};
let pt_addr = kern().platform_mut().page_table_for(display_pid);
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
map_page(pt, GPU_VA, gpu.base());
let vq_phys = alloc_contiguous(2);
map_range(pt, VQUEUE_VA, vq_phys, 2);
let cmd_phys = mmu::allocator::alloc_zeroed_page();
map_page(pt, CMDBUF_VA, cmd_phys);
gpu.init_device(vq_phys);
let queue = hw_virtio::queue::VirtioQueue::new(
hw_virtio::queue::QueueAddrs::from_base(vq_phys),
);
let dma = hw_virtio::gpu::GpuDmaAddrs {
cmd_pa: types::address::PhysicalAddress::from_raw(cmd_phys),
cmd_va: types::address::VirtualAddress::from_raw(cmd_phys),
resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512),
};
use hw_virtio::mmio::MmioAccess;
let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) {
Some((w, h)) => {
uart.write_str("k-os: GPU display: ");
uart.write_dec(w as usize);
uart.put_char(b'x');
uart.write_dec(h as usize);
uart.put_char(b'\n');
(Dimension::new(w as u16), Dimension::new(h as u16))
}
None => {
uart.write_str("k-os: GPU display: using default 640x480\n");
(DEFAULT_WIDTH, DEFAULT_HEIGHT)
}
};
let fb_pages = (width.raw() as usize * height.raw() as usize * 4 + 4095) / 4096;
let fb_phys = alloc_contiguous(fb_pages);
map_range(pt, FB_VA, fb_phys, fb_pages);
let info_phys = mmu::allocator::alloc_zeroed_page();
map_page(pt, PHYSINFO_VA, info_phys);
unsafe {
let info = info_phys as *mut u64;
core::ptr::write_volatile(info, cmd_phys as u64);
core::ptr::write_volatile(info.add(1), (cmd_phys + 512) as u64);
core::ptr::write_volatile(info.add(2), fb_phys as u64);
core::ptr::write_volatile(info.add(3), width.raw() as u64);
core::ptr::write_volatile(info.add(4), height.raw() as u64);
}
if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) {
uart.write_str("k-os: VirtIO GPU configured\n");
} else {
uart.write_str("k-os: VirtIO GPU setup failed (display will be headless)\n");
}
}
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(pa),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
for i in 0..count {
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
}
}
fn alloc_contiguous(pages: usize) -> usize {
let first = mmu::allocator::alloc_zeroed_page();
for _ in 1..pages {
mmu::allocator::alloc_zeroed_page();
}
first
}

View File

@@ -1,68 +0,0 @@
use crate::hw::uart::Uart;
use crate::{board, mmu};
use types::id::ProcessId;
const UART_VA: usize = 0x20000;
const INPUT_MMIO_VA: usize = 0x25000;
const INPUT_VQUEUE_VA: usize = 0x26000;
const INPUT_EVBUF_VA: usize = 0x28000;
const INPUT_PHYSINFO_VA: usize = 0x29000;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
pub fn configure(uart: &Uart, input_pid: ProcessId) {
let pt_addr = kern().platform_mut().page_table_for(input_pid);
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
// Always map UART for fallback
map_page(pt, UART_VA, board::UART_BASE);
// Try to find VirtIO input device
if let Some(mmio) = hw_virtio::mmio::scan_for_device(hw_virtio::mmio::DEVICE_ID_INPUT) {
let base = mmio.base();
map_page(pt, INPUT_MMIO_VA, base);
let vq_phys = alloc_contiguous(2);
map_range(pt, INPUT_VQUEUE_VA, vq_phys, 2);
let evbuf_phys = mmu::allocator::alloc_zeroed_page();
map_page(pt, INPUT_EVBUF_VA, evbuf_phys);
let info_phys = mmu::allocator::alloc_zeroed_page();
map_page(pt, INPUT_PHYSINFO_VA, info_phys);
unsafe {
core::ptr::write_volatile(info_phys as *mut u64, evbuf_phys as u64);
core::ptr::write_volatile((info_phys as *mut u64).add(1), vq_phys as u64);
}
hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32);
uart.write_str("k-os: VirtIO keyboard configured\n");
} else {
uart.write_str("k-os: UART mapped for input\n");
}
}
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(pa),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
for i in 0..count {
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
}
}
fn alloc_contiguous(pages: usize) -> usize {
let first = mmu::allocator::alloc_zeroed_page();
for _ in 1..pages {
mmu::allocator::alloc_zeroed_page();
}
first
}

View File

@@ -1,3 +1,80 @@
pub mod gpu_setup; // Boot-time userspace setup: init's BootInfo page.
pub mod input_setup;
pub mod storage_setup; use crate::hw::uart::Uart;
use types::address::VirtualAddress;
use types::bootinfo::{BOOT_INFO_VA, BootDevice, BootInfo, MAGIC, MAX_BOOT_DEVICES, NO_SLOT};
use types::id::ProcessId;
use types::mapping::PagePerms;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
/// Tells init, in a read-only page at BOOT_INFO_VA, which of its slots hold
/// each board device's capabilities.
pub(crate) fn boot_info(
uart: &Uart,
init: ProcessId,
board: &[kernel::BoardDevice],
kinds: &[u64],
placed: &[Option<kernel::BoardCaps>],
boot_caps: kernel::BootCaps,
) {
let empty = BootDevice {
kind: 0,
phys_base: 0,
pages: 0,
irq: 0,
region_slot: NO_SLOT,
irq_slot: NO_SLOT,
};
let mut info = BootInfo {
magic: MAGIC,
device_count: 0,
system_control_slot: boot_caps.system_control.value() as u64,
console_slot: boot_caps.console.value() as u64,
devices: [empty; MAX_BOOT_DEVICES],
};
for (((d, kind), caps), out) in board
.iter()
.zip(kinds)
.zip(placed)
.zip(info.devices.iter_mut())
{
let Some(caps) = caps else { continue };
*out = BootDevice {
kind: *kind,
phys_base: d.base.raw() as u64,
pages: d.pages.value() as u64,
irq: d.irq.map_or(0, |i| i.raw() as u64),
region_slot: caps.region.value() as u64,
irq_slot: caps.irq.map_or(NO_SLOT, |s| s.value() as u64),
};
info.device_count += 1;
}
// SAFETY: BootInfo is repr(C) and made only of u64s.
let words = unsafe {
core::slice::from_raw_parts(
&info as *const BootInfo as *const u64,
core::mem::size_of::<BootInfo>() / 8,
)
};
write_page(uart, init, BOOT_INFO_VA, words);
}
/// Writes `words` into a fresh read-only page mapped at `va` in `pid`.
fn write_page(uart: &Uart, pid: ProcessId, va: usize, words: &[u64]) {
let va = VirtualAddress::from_raw(va);
kern()
.map_fresh_range(pid, va, 1, PagePerms::ReadOnly)
.unwrap_or_else(|_| crate::halt(uart, "boot info page failed"));
let pa = kern()
.user_page_frame(pid, va)
.unwrap_or_else(|| crate::halt(uart, "boot info page vanished"));
let page = crate::mmu::phys_to_virt(pa.raw()) as *mut u64;
for (i, &w) in words.iter().take(crate::mmu::PAGE_SIZE / 8).enumerate() {
// SAFETY: `pa` is a pool frame just mapped for `pid`, reachable at
// `phys_to_virt`, and at most one page is written.
unsafe { core::ptr::write_volatile(page.add(i), w) };
}
}

View File

@@ -1,114 +0,0 @@
use crate::hw::uart::Uart;
use crate::mmu;
use types::id::ProcessId;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
const VIRTIO_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000;
const REQHDR_VA: usize = 0x23000;
const PHYSINFO_VA: usize = 0x24000;
const SHARED_BUF_VA: usize = 0x30000;
const SHARED_BUF_PAGES: usize = 16;
pub fn configure(
uart: &Uart,
init_pid: ProcessId,
storage_pid: ProcessId,
virtio_base: usize,
) {
let storage_pt = get_pt(storage_pid);
let init_pt = get_pt(init_pid);
map_virtio_mmio(storage_pt, virtio_base);
let vqueue_phys = alloc_and_map_virtqueue(storage_pt);
let reqhdr_phys = alloc_and_map_reqhdr(storage_pt);
write_phys_info(storage_pt, reqhdr_phys);
reinit_device(virtio_base, vqueue_phys);
uart.write_str("k-os: VirtIO configured for storage\n");
map_shared_buffer(init_pt, storage_pt);
uart.write_str("k-os: shared buffer mapped\n");
}
fn get_pt(pid: ProcessId) -> &'static mut mmu::PageTable {
let addr = kern().platform_mut().page_table_for(pid);
unsafe { &mut *(addr as *mut mmu::PageTable) }
}
fn map_virtio_mmio(pt: &mut mmu::PageTable, virtio_base: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(VIRTIO_VA),
pa: types::address::PhysicalAddress::from_raw(virtio_base),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn alloc_and_map_virtqueue(pt: &mut mmu::PageTable) -> usize {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::allocator::alloc_zeroed_page();
for i in 0..2usize {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(VQUEUE_VA + i * 4096),
pa: types::address::PhysicalAddress::from_raw(phys + i * 4096),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
phys
}
fn alloc_and_map_reqhdr(pt: &mut mmu::PageTable) -> usize {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(REQHDR_VA),
pa: types::address::PhysicalAddress::from_raw(phys),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
phys
}
fn write_phys_info(pt: &mut mmu::PageTable, reqhdr_phys: usize) {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(PHYSINFO_VA),
pa: types::address::PhysicalAddress::from_raw(phys),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
unsafe {
let info = phys as *mut u64;
core::ptr::write_volatile(info, reqhdr_phys as u64);
core::ptr::write_volatile(info.add(1), (reqhdr_phys + 16) as u64);
core::ptr::write_volatile(info.add(2), (reqhdr_phys + 32) as u64);
}
}
fn reinit_device(base: usize, vqueue_phys: usize) {
let mmio = hw_virtio::mmio::DirectMmio::new(base);
hw_virtio::mmio::init_legacy_device(&mmio, vqueue_phys, hw_virtio::queue::QUEUE_SIZE as u32);
}
fn map_shared_buffer(init_pt: &mut mmu::PageTable, storage_pt: &mut mmu::PageTable) {
for i in 0..SHARED_BUF_PAGES {
let page = mmu::allocator::alloc_zeroed_page();
let va = SHARED_BUF_VA + i * 4096;
let mapping = mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
};
mmu::map(init_pt, mapping);
mmu::map(storage_pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
}

View File

@@ -0,0 +1,73 @@
use core::arch::global_asm;
// Entered with the MMU off at the physical address; the kernel is linked high.
// PC-relative code only: enable `boot_page_table` (RAM mapped low for the next
// instruction and high, MMIO high), then jump high and take the hart's stack.
global_asm!(
r#"
.section .text.entry
.globl _start
_start:
lla t2, start_high_address
j 1f
.globl _secondary_start
_secondary_start:
lla t2, secondary_high_address
1:
li t0, 8 # smp::MAX_HARTS
bgeu a0, t0, spin
mv s0, a0
lla t0, boot_page_table
srli t0, t0, 12
li t1, 8 << 60
or t0, t0, t1
csrw satp, t0
sfence.vma
ld t0, 0(t2)
jr t0
# sp = _stacks_start + (hart + 1) * smp::STACK_SLOT
start_high:
call hart_stack
mv a0, s0
call kmain
j spin
secondary_high:
call hart_stack
mv a0, s0
call kmain_secondary
j spin
hart_stack:
lla sp, _stacks_start
addi t0, s0, 1
li t1, 0x81000
mul t0, t0, t1
add sp, sp, t0
ret
spin:
wfi
j spin
.pushsection .rodata
.balign 8
start_high_address:
.quad start_high
secondary_high_address:
.quad secondary_high
.popsection
# Sv39 root of 1 GiB leaves; flags V|R|W|A|D = 0xc7, plus X = 0xcf.
.pushsection .data
.balign 4096
boot_page_table:
.quad 0
.quad 0
.quad 0x200000cf # [2] 0x8000_0000 -> RAM, for the jump only
.fill 253, 8, 0
.quad 0x000000c7 # [256] PHYS_OFFSET + 0 -> MMIO
.quad 0
.quad 0x200000cf # [258] PHYS_OFFSET + 0x8000_0000 -> RAM
.fill 253, 8, 0
.popsection
"#
);

View File

@@ -1,5 +1,3 @@
pub mod plic; pub mod plic;
pub mod timer; pub mod timer;
pub mod uart; pub mod uart;
pub mod virtio_blk;
pub mod virtio_gpu;

View File

@@ -1,21 +1,37 @@
use crate::board; use crate::board;
const PLIC_PRIORITY: usize = board::PLIC_BASE; const PLIC: usize = crate::mmu::phys_to_virt(board::PLIC_BASE);
const PLIC_S_ENABLE: usize = board::PLIC_BASE + 0x2080; const PLIC_PRIORITY: usize = PLIC;
const PLIC_S_THRESHOLD: usize = board::PLIC_BASE + 0x201000;
const PLIC_S_CLAIM: usize = board::PLIC_BASE + 0x201004; // External interrupts go to the boot hart only: its supervisor context is
// 2 * hart + 1 on QEMU virt (even contexts are machine mode).
fn context() -> usize {
2 * crate::smp::boot_hart() + 1
}
fn enable_base() -> usize {
PLIC + 0x2000 + context() * 0x80
}
fn threshold() -> usize {
PLIC + 0x20_0000 + context() * 0x1000
}
fn claim_reg() -> usize {
threshold() + 4
}
pub struct Plic; pub struct Plic;
impl Plic { impl Plic {
pub fn init() { pub fn init() {
unsafe { core::ptr::write_volatile(PLIC_S_THRESHOLD as *mut u32, 0) }; unsafe { core::ptr::write_volatile(threshold() as *mut u32, 0) };
} }
pub fn enable(irq: u32) { pub fn enable(irq: u32) {
let word = (irq / 32) as usize; let word = (irq / 32) as usize;
let bit = irq % 32; let bit = irq % 32;
let addr = PLIC_S_ENABLE + word * 4; let addr = enable_base() + word * 4;
unsafe { unsafe {
let current = core::ptr::read_volatile(addr as *const u32); let current = core::ptr::read_volatile(addr as *const u32);
core::ptr::write_volatile(addr as *mut u32, current | (1 << bit)); core::ptr::write_volatile(addr as *mut u32, current | (1 << bit));
@@ -27,7 +43,7 @@ impl Plic {
pub fn disable(irq: u32) { pub fn disable(irq: u32) {
let word = (irq / 32) as usize; let word = (irq / 32) as usize;
let bit = irq % 32; let bit = irq % 32;
let addr = PLIC_S_ENABLE + word * 4; let addr = enable_base() + word * 4;
unsafe { unsafe {
let current = core::ptr::read_volatile(addr as *const u32); let current = core::ptr::read_volatile(addr as *const u32);
core::ptr::write_volatile(addr as *mut u32, current & !(1 << bit)); core::ptr::write_volatile(addr as *mut u32, current & !(1 << bit));
@@ -35,10 +51,10 @@ impl Plic {
} }
pub fn claim() -> u32 { pub fn claim() -> u32 {
unsafe { core::ptr::read_volatile(PLIC_S_CLAIM as *const u32) } unsafe { core::ptr::read_volatile(claim_reg() as *const u32) }
} }
pub fn complete(irq: u32) { pub fn complete(irq: u32) {
unsafe { core::ptr::write_volatile(PLIC_S_CLAIM as *mut u32, irq) }; unsafe { core::ptr::write_volatile(claim_reg() as *mut u32, irq) };
} }
} }

View File

@@ -1,4 +1,10 @@
use core::sync::atomic::{AtomicU64, Ordering};
use crate::board; use crate::board;
use crate::smp::{MAX_HARTS, hart_id};
/// When each hart's timer is set to go off next.
static NEXT: [AtomicU64; MAX_HARTS] = [const { AtomicU64::new(u64::MAX) }; MAX_HARTS];
pub fn read_time() -> u64 { pub fn read_time() -> u64 {
let time: u64; let time: u64;
@@ -7,10 +13,13 @@ pub fn read_time() -> u64 {
} }
fn sbi_set_timer(stime: u64) { fn sbi_set_timer(stime: u64) {
// SBI returns an error code in a0 and a value in a1; both are
// clobbered even though we ignore them.
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"ecall", "ecall",
in("a0") stime, inlateout("a0") stime => _,
lateout("a1") _,
in("a6") 0usize, in("a6") 0usize,
in("a7") 0x54494D45usize, in("a7") 0x54494D45usize,
); );
@@ -18,15 +27,27 @@ fn sbi_set_timer(stime: u64) {
} }
pub fn init() { pub fn init() {
let now = read_time(); set_next(read_time() + board::TICK_INTERVAL);
sbi_set_timer(now + board::TICK_INTERVAL);
} }
pub fn set_next(deadline: u64) { pub fn set_next(deadline: u64) {
if let Some(next) = NEXT.get(hart_id()) {
next.store(deadline, Ordering::Relaxed);
}
sbi_set_timer(deadline); sbi_set_timer(deadline);
} }
pub fn handle_interrupt() { /// Goes off at `deadline` too, if that is before the next tick.
let now = read_time(); pub fn wake_at(deadline: u64) {
sbi_set_timer(now + board::TICK_INTERVAL); if NEXT
.get(hart_id())
.is_some_and(|n| deadline < n.load(Ordering::Relaxed))
{
set_next(deadline);
}
}
/// Re-arms the tick; the kernel then re-arms for its earliest timer.
pub fn handle_interrupt() {
set_next(read_time() + board::TICK_INTERVAL);
} }

View File

@@ -6,7 +6,9 @@ pub struct Uart {
impl Uart { impl Uart {
pub fn new() -> Self { pub fn new() -> Self {
Self { base: board::UART_BASE } Self {
base: crate::mmu::phys_to_virt(board::UART_BASE),
}
} }
pub fn put_char(&self, byte: u8) { pub fn put_char(&self, byte: u8) {
@@ -30,7 +32,11 @@ impl Uart {
let mut i = 0; let mut i = 0;
while value > 0 { while value > 0 {
let digit = (value & 0xF) as u8; let digit = (value & 0xF) as u8;
buf[i] = if digit < 10 { b'0' + digit } else { b'a' + digit - 10 }; buf[i] = if digit < 10 {
b'0' + digit
} else {
b'a' + digit - 10
};
value >>= 4; value >>= 4;
i += 1; i += 1;
} }

View File

@@ -1,54 +0,0 @@
use hw_virtio::mmio::{self, DirectMmio};
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
use hw_virtio::block::{self, DmaAddrs};
use types::address::{PhysicalAddress, VirtualAddress};
const SECTOR_SIZE: usize = 512;
#[repr(C, align(4096))]
struct VirtqMem {
_data: [u8; 8192],
}
static mut VQUEUE: VirtqMem = VirtqMem { _data: [0; 8192] };
static mut REQ_HEADER: block::BlockReqHeader = block::BlockReqHeader { req_type: 0, reserved: 0, sector: 0 };
static mut REQ_STATUS: u8 = 0xFF;
static mut SECTOR_BUF: [u8; SECTOR_SIZE] = [0; SECTOR_SIZE];
pub struct VirtioBlk {
mmio: DirectMmio,
queue: VirtioQueue,
dma: DmaAddrs,
}
impl VirtioBlk {
pub fn probe() -> Option<Self> {
let mmio = mmio::scan_for_device(mmio::DEVICE_ID_BLOCK)?;
let vq_phys = unsafe { core::ptr::addr_of!(VQUEUE) as usize };
mmio::init_legacy_device(&mmio, vq_phys, QUEUE_SIZE as u32);
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys));
let dma = unsafe {
DmaAddrs {
header_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_HEADER) as usize),
header_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_HEADER) as usize),
status_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_STATUS) as usize),
status_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_STATUS) as usize),
data_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(SECTOR_BUF) as usize),
data_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(SECTOR_BUF) as usize),
}
};
Some(Self { mmio, queue, dma })
}
pub fn base(&self) -> usize {
self.mmio.base()
}
pub fn read_sector(&mut self, sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> bool {
block::read_sector(&self.mmio, &self.queue, &self.dma, sector, buf)
}
}

View File

@@ -1,41 +0,0 @@
use hw_virtio::mmio::{self, DirectMmio};
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
use hw_virtio::gpu::{self, GpuDmaAddrs};
use types::address::{PhysicalAddress, VirtualAddress};
use types::display::Dimension;
pub struct VirtioGpu {
mmio: DirectMmio,
}
impl VirtioGpu {
pub fn probe() -> Option<Self> {
let mmio = mmio::scan_for_device(mmio::DEVICE_ID_GPU)?;
Some(Self { mmio })
}
pub fn base(&self) -> usize {
self.mmio.base()
}
pub fn mmio(&self) -> &DirectMmio {
&self.mmio
}
pub fn init_device(&self, vqueue_phys: usize) {
mmio::init_legacy_device(&self.mmio, vqueue_phys, QUEUE_SIZE as u32);
}
pub fn setup_framebuffer(&self, cmd_phys: usize, vq_phys: usize, fb_phys: usize, width: Dimension, height: Dimension) -> bool {
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys));
let dma = GpuDmaAddrs {
cmd_pa: PhysicalAddress::from_raw(cmd_phys),
cmd_va: VirtualAddress::from_raw(cmd_phys),
resp_pa: PhysicalAddress::from_raw(cmd_phys + 512),
};
let ok1 = gpu::create_2d(&self.mmio, &queue, &dma, 1, width, height);
let ok2 = gpu::attach_backing(&self.mmio, &queue, &dma, 1, PhysicalAddress::from_raw(fb_phys), width.raw() as u32 * height.raw() as u32 * 4);
let ok3 = gpu::set_scanout(&self.mmio, &queue, &dma, 0, 1, width, height);
ok1 && ok2 && ok3
}
}

View File

@@ -1,33 +1,24 @@
use core::cell::UnsafeCell; use core::cell::UnsafeCell;
use core::mem::MaybeUninit; use core::mem::MaybeUninit;
use core::sync::atomic::{AtomicBool, Ordering};
pub struct KernelCell<T> { pub struct KernelCell<T> {
initialized: AtomicBool,
inner: UnsafeCell<MaybeUninit<T>>, inner: UnsafeCell<MaybeUninit<T>>,
} }
// Single-core, interrupts disabled during kernel access. // Access is serialized by the kernel lock.
unsafe impl<T> Sync for KernelCell<T> {} unsafe impl<T> Sync for KernelCell<T> {}
impl<T> KernelCell<T> { impl<T> KernelCell<T> {
pub const fn uninit() -> Self { /// Holds `value` from the start: built at compile time, so a large `T`
/// never lives on the stack.
pub const fn new(value: T) -> Self {
Self { Self {
initialized: AtomicBool::new(false), inner: UnsafeCell::new(MaybeUninit::new(value)),
inner: UnsafeCell::new(MaybeUninit::uninit()),
} }
} }
pub fn init(&self, value: T) { /// Raw pointer to the kernel; callers must avoid aliasing `&mut`.
unsafe { (*self.inner.get()).write(value) };
self.initialized.store(true, Ordering::Release);
}
/// Returns a raw pointer to the kernel. Callers must ensure
/// no aliasing `&mut` references exist. Safe on single-core
/// with interrupts disabled during kernel calls.
pub fn as_ptr(&self) -> *mut T { pub fn as_ptr(&self) -> *mut T {
debug_assert!(self.initialized.load(Ordering::Acquire));
unsafe { (*self.inner.get()).as_mut_ptr() } unsafe { (*self.inner.get()).as_mut_ptr() }
} }
} }

View File

@@ -3,69 +3,101 @@
mod board; mod board;
mod boot; mod boot;
mod boot_fs;
mod devices; mod devices;
mod entry;
mod hw; mod hw;
mod kernel_cell; mod kernel_cell;
mod mmu; mod mmu;
mod platform; mod platform;
mod smp;
mod trap; mod trap;
use core::arch::global_asm;
use hw::uart::Uart; use hw::uart::Uart;
use kernel_cell::KernelCell; use kernel_cell::KernelCell;
static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit(); /// Built at compile time with nothing in it (no platform, no cores, no
/// pool), so every byte is zero and it lives in `.bss`, not in the image;
/// boot installs the platform and the pool before anything runs.
static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> =
KernelCell::new(kernel::Kernel::with_cores(
platform::RiscVPlatform::new(0, 0),
0,
types::address::PhysicalAddress::from_raw(0),
0,
));
fn kern_mut() -> &'static mut kernel::Kernel<platform::RiscVPlatform> {
// SAFETY: boot runs with the kernel lock held and before any other
// hart has started.
unsafe { &mut *kernel_ptr() }
}
pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> { pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> {
KERNEL.as_ptr() KERNEL.as_ptr()
} }
global_asm!(
r#"
.section .text.entry
.globl _start
_start:
la sp, _stack_top
call kmain
spin:
wfi
j spin
"#
);
const SHARED_BUF_VA: usize = 0x30000;
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn kmain() -> ! { extern "C" fn kmain(hart: usize) -> ! {
let uart = Uart::new(); let uart = Uart::new();
uart.write_str("k-os: booted on RISC-V\n"); uart.write_str("k-os: booted on RISC-V\n");
let kernel_pt = boot::hardware::init(&uart); boot::hardware::init(&uart, hart);
let mut blk = boot::hardware::probe_block_device(&uart) // Not before init: zeroing .bss resets the lock.
.unwrap_or_else(|| halt(&uart, "no block device")); smp::lock();
// Both come from the kernel image; init starts everything else from disk.
let init = boot::loader::load_init(&uart);
let storage = boot::loader::load_storage(&uart, init.pid);
let (init, _) = boot::loader::load_init(&uart, &mut blk, kernel_pt); let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table"));
let storage = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"storage.elf", "storage"); kern_mut()
let display = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"display.elf", "display"); .register_board(init.pid, &board)
let input = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"input.elf", "input"); .unwrap_or_else(|_| halt(&uart, "board registration failed"));
let virtio_base = blk.base(); boot::caps::grant_all(&uart, init.pid, storage.pid);
drop(blk); // After grant_all: init already relies on those slots.
let mut placed = [None; board::BOARD_DEVICES];
devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base); let boot_caps = kern_mut()
devices::gpu_setup::configure(&uart, display.pid); .grant_boot_authority(init.pid, &board, &mut placed)
devices::input_setup::configure(&uart, input.pid); .unwrap_or_else(|_| halt(&uart, "boot authority failed"));
boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid); devices::boot_info(&uart, init.pid, &board, &board::kinds(), &placed, boot_caps);
start_other_harts(&uart, hart);
uart.write_str("k-os: entering userspace\n"); uart.write_str("k-os: entering userspace\n");
boot::loader::enter_userspace(init.pid, init.entry, SHARED_BUF_VA); trap::enter_first();
}
fn start_other_harts(uart: &Uart, boot: usize) {
unsafe extern "C" {
fn _secondary_start();
}
// Harts start with the MMU off, so at the entry's physical address.
let entry = mmu::virt_to_phys(_secondary_start as *const () as usize);
for hart in (0..smp::num_harts()).filter(|h| *h != boot) {
if !smp::sbi_hart_start(hart, entry) {
halt(uart, "could not start a hart");
}
}
uart.write_str("k-os: ");
uart.write_dec(smp::num_harts());
uart.write_str(" harts\n");
}
/// Where every hart but the boot hart starts, on its own stack.
#[unsafe(no_mangle)]
extern "C" fn kmain_secondary(_hart: usize) -> ! {
trap::init();
hw::timer::init();
mmu::activate(mmu::page_table::table_at(smp::kernel_root()));
smp::lock();
trap::enter_first();
} }
pub fn halt(uart: &Uart, msg: &str) -> ! { pub fn halt(uart: &Uart, msg: &str) -> ! {
uart.write_str("k-os: FATAL — "); uart.write_str("k-os: FATAL — ");
uart.write_str(msg); uart.write_str(msg);
uart.put_char(b'\n'); uart.put_char(b'\n');
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }
#[panic_handler] #[panic_handler]
@@ -78,5 +110,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
uart.write_dec(location.line() as usize); uart.write_dec(location.line() as usize);
} }
uart.put_char(b'\n'); uart.put_char(b'\n');
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }

View File

@@ -1,16 +1,21 @@
use super::page_table::{PageTable, PAGE_SIZE}; use super::page_table::{PAGE_SIZE, PageTable};
use crate::board;
// Boot-time bump allocator for page tables. // Boot bump allocator for kernel-only memory; must never reach the user pool.
// Only used during init before the scheduler starts.
static mut NEXT_FREE: usize = 0; static mut NEXT_FREE: usize = 0;
pub fn init_allocator(start: usize) { pub fn init_allocator(start: usize) {
unsafe { NEXT_FREE = start; } unsafe {
NEXT_FREE = start;
}
} }
pub fn alloc_zeroed_page() -> usize { pub fn alloc_zeroed_page() -> usize {
unsafe { unsafe {
let addr = NEXT_FREE; let addr = NEXT_FREE;
if addr + PAGE_SIZE > super::phys_to_virt(board::USER_POOL_BASE) {
crate::halt(&crate::hw::uart::Uart::new(), "boot allocator exhausted");
}
NEXT_FREE += PAGE_SIZE; NEXT_FREE += PAGE_SIZE;
core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE); core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE);
addr addr
@@ -18,9 +23,5 @@ pub fn alloc_zeroed_page() -> usize {
} }
pub fn alloc_page_table() -> &'static mut PageTable { pub fn alloc_page_table() -> &'static mut PageTable {
unsafe { unsafe { &mut *(alloc_zeroed_page() as *mut PageTable) }
let addr = alloc_zeroed_page();
let table = &mut *(addr as *mut PageTable);
table
}
} }

View File

@@ -2,4 +2,21 @@ pub mod allocator;
pub mod page_table; pub mod page_table;
pub use allocator::init_allocator; pub use allocator::init_allocator;
pub use page_table::{activate, init_kernel_page_table, create_user_page_table, map, translate, Sv39Flags, Mapping, PageTable, PAGE_SIZE}; pub use page_table::{
PAGE_SIZE, PageTable, Sv39Flags, USER_VA_END, USER_VA_START, activate, active_root, check_wx,
create_user_root, init_kernel_page_table, map_user_page, translate_user, unmap_user_page,
};
/// Direct-map offset: RAM, MMIO and the kernel image sit at physical + this
/// (start of Sv39's upper half). Must match linker.ld.
pub const PHYS_OFFSET: usize = 0xFFFF_FFC0_0000_0000;
/// Where the kernel sees physical address `pa`.
pub const fn phys_to_virt(pa: usize) -> usize {
pa + PHYS_OFFSET
}
/// The physical address behind a direct-map kernel address `va`.
pub const fn virt_to_phys(va: usize) -> usize {
va - PHYS_OFFSET
}

View File

@@ -1,209 +0,0 @@
use crate::board;
use super::allocator::alloc_page_table;
use types::address::{PhysicalAddress, VirtualAddress};
pub use types::mapping::Mapping;
pub const PAGE_SIZE: usize = 4096;
const PTE_V: u64 = 1 << 0;
const PTE_R: u64 = 1 << 1;
const PTE_W: u64 = 1 << 2;
const PTE_X: u64 = 1 << 3;
const PTE_U: u64 = 1 << 4;
const PTE_G: u64 = 1 << 5;
const PTE_A: u64 = 1 << 6;
const PTE_D: u64 = 1 << 7;
#[derive(Clone, Copy)]
pub struct Sv39Flags {
bits: u64,
}
impl Sv39Flags {
pub fn kernel_rwx() -> Self {
Self { bits: PTE_R | PTE_W | PTE_X | PTE_G }
}
pub fn kernel_rw() -> Self {
Self { bits: PTE_R | PTE_W | PTE_G }
}
pub fn user_rx() -> Self {
Self { bits: PTE_R | PTE_X | PTE_U }
}
pub fn user_rw() -> Self {
Self { bits: PTE_R | PTE_W | PTE_U }
}
pub fn user(read: bool, write: bool, exec: bool) -> Self {
let mut bits = PTE_U;
if read { bits |= PTE_R; }
if write { bits |= PTE_W; }
if exec { bits |= PTE_X; }
Self { bits }
}
fn to_pte_flags(self) -> u64 {
PTE_V | PTE_A | PTE_D | self.bits
}
}
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct PageTableEntry(u64);
impl PageTableEntry {
pub fn empty() -> Self {
Self(0)
}
fn new_leaf(ppn: u64, flags: Sv39Flags) -> Self {
Self(flags.to_pte_flags() | (ppn << 10))
}
fn new_branch(ppn: u64) -> Self {
Self(PTE_V | (ppn << 10))
}
pub fn is_valid(&self) -> bool {
self.0 & PTE_V != 0
}
pub fn ppn(&self) -> u64 {
(self.0 >> 10) & 0xFFF_FFFF_FFFF
}
}
#[repr(C, align(4096))]
pub struct PageTable {
pub entries: [PageTableEntry; 512],
}
impl PageTable {
pub fn zero(&mut self) {
for entry in &mut self.entries {
*entry = PageTableEntry::empty();
}
}
}
fn vpn(va: usize, level: usize) -> usize {
(va >> (12 + 9 * level)) & 0x1FF
}
pub fn translate(root: &PageTable, va: usize) -> Option<usize> {
let vpn2 = vpn(va, 2);
if !root.entries[vpn2].is_valid() { return None; }
let l1 = unsafe { &*((root.entries[vpn2].ppn() << 12) as *const PageTable) };
let vpn1 = vpn(va, 1);
if !l1.entries[vpn1].is_valid() { return None; }
let l0 = unsafe { &*((l1.entries[vpn1].ppn() << 12) as *const PageTable) };
let vpn0 = vpn(va, 0);
if !l0.entries[vpn0].is_valid() { return None; }
let pa = (l0.entries[vpn0].ppn() << 12) as usize | (va & 0xFFF);
Some(pa)
}
fn map_page(root: &mut PageTable, va: usize, pa: usize, flags: Sv39Flags) {
let vpn2 = vpn(va, 2);
let vpn1 = vpn(va, 1);
let vpn0 = vpn(va, 0);
if !root.entries[vpn2].is_valid() {
let table = alloc_page_table();
let ppn = (table as *const PageTable as u64) >> 12;
root.entries[vpn2] = PageTableEntry::new_branch(ppn);
}
let l1_addr = (root.entries[vpn2].ppn() << 12) as *mut PageTable;
let l1 = unsafe { &mut *l1_addr };
if !l1.entries[vpn1].is_valid() {
let table = alloc_page_table();
let ppn = (table as *const PageTable as u64) >> 12;
l1.entries[vpn1] = PageTableEntry::new_branch(ppn);
}
let l0_addr = (l1.entries[vpn1].ppn() << 12) as *mut PageTable;
let l0 = unsafe { &mut *l0_addr };
let ppn = (pa as u64) >> 12;
l0.entries[vpn0] = PageTableEntry::new_leaf(ppn, flags);
}
pub fn map(root: &mut PageTable, mapping: Mapping<Sv39Flags>) {
for i in 0..mapping.pages {
let offset = i * PAGE_SIZE;
map_page(root, mapping.va.raw() + offset, mapping.pa.raw() + offset, mapping.flags);
}
}
pub fn init_kernel_page_table() -> &'static mut PageTable {
let root = alloc_page_table();
let identity = |addr: usize| (VirtualAddress::from_raw(addr), PhysicalAddress::from_raw(addr));
let (va, pa) = identity(board::RAM_BASE);
map(root, Mapping { va, pa, pages: board::RAM_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rwx() });
let (va, pa) = identity(board::UART_BASE);
map(root, Mapping { va, pa, pages: 1, flags: Sv39Flags::kernel_rw() });
let (va, pa) = identity(board::PLIC_BASE);
map(root, Mapping { va, pa, pages: 0x400000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
let (va, pa) = identity(board::CLINT_BASE);
map(root, Mapping { va, pa, pages: 0x10000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
let (va, pa) = identity(board::VIRTIO_BASE);
map(root, Mapping { va, pa, pages: board::VIRTIO_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
root
}
pub fn activate(root: &PageTable) {
let ppn = (root as *const PageTable as u64) >> 12;
let satp = (8u64 << 60) | ppn;
unsafe {
core::arch::asm!(
"csrw satp, {0}",
"sfence.vma",
in(reg) satp,
);
}
}
pub fn create_user_page_table(
kernel_root: &PageTable,
_program_pa: PhysicalAddress,
_program_pages: usize,
_user_va: VirtualAddress,
) -> &'static mut PageTable {
let root = alloc_page_table();
for i in 0..512 {
if !kernel_root.entries[i].is_valid() {
continue;
}
if i == 0 {
// User mappings land in VPN[2]=0 — each process needs its own L1
let kernel_l1_addr = (kernel_root.entries[i].ppn() << 12) as *const PageTable;
let kernel_l1 = unsafe { &*kernel_l1_addr };
let new_l1 = alloc_page_table();
for j in 0..512 {
if kernel_l1.entries[j].is_valid() {
new_l1.entries[j] = kernel_l1.entries[j];
}
}
let ppn = (new_l1 as *const PageTable as u64) >> 12;
root.entries[i] = PageTableEntry::new_branch(ppn);
} else {
root.entries[i] = kernel_root.entries[i];
}
}
root
}

View File

@@ -0,0 +1,207 @@
use super::pte::{
Mapping, PAGE_SIZE, PTE_W, PTE_X, PageTable, PageTableEntry, Sv39Flags, map, table_at,
table_ppn,
};
use crate::board;
use crate::mmu::allocator::alloc_page_table;
use crate::mmu::{phys_to_virt, virt_to_phys};
use crate::smp;
use types::address::{PhysicalAddress, VirtualAddress};
pub fn init_kernel_page_table() -> &'static mut PageTable {
let root = alloc_page_table();
// Physical `pa` at its place in the upper-half direct map.
let direct = |pa: usize| {
(
VirtualAddress::from_raw(phys_to_virt(pa)),
PhysicalAddress::from_raw(pa),
)
};
// The kernel image, then the rest of RAM, each with only the access it
// needs: code is never writable, data never executable. RAM below the
// image belongs to the firmware and stays unmapped, as does the guard
// page under each hart's kernel stack. Nothing is mapped in the lower
// half, which is userspace's alone.
let layout = KernelLayout::from_linker();
let ram_end = phys_to_virt(board::RAM_BASE + board::RAM_SIZE);
let stacks = (0..smp::MAX_HARTS).map(|h| {
let slot = layout.stacks_start + h * smp::STACK_SLOT;
(
slot + PAGE_SIZE,
slot + smp::STACK_SLOT,
Sv39Flags::kernel_rw(),
)
});
for (start, end, flags) in [
(layout.text_start, layout.text_end, Sv39Flags::kernel_rx()),
(layout.text_end, layout.rodata_end, Sv39Flags::kernel_ro()),
(
layout.rodata_end,
layout.stacks_start,
Sv39Flags::kernel_rw(),
),
(layout.stacks_end, ram_end, Sv39Flags::kernel_rw()),
]
.into_iter()
.chain(stacks)
{
let (va, pa) = direct(virt_to_phys(start));
map(
root,
Mapping {
va,
pa,
pages: (end - start) / PAGE_SIZE,
flags,
},
);
}
for (base, size) in [
(board::UART_BASE, PAGE_SIZE),
(board::PLIC_BASE, 0x40_0000),
(board::CLINT_BASE, 0x1_0000),
(board::VIRTIO_BASE, board::VIRTIO_SIZE),
] {
let (va, pa) = direct(base);
map(
root,
Mapping {
va,
pa,
pages: size / PAGE_SIZE,
flags: Sv39Flags::kernel_rw(),
},
);
}
root
}
/// Page-aligned boundaries of the kernel image, from linker.ld.
pub struct KernelLayout {
pub text_start: usize,
pub text_end: usize,
pub rodata_end: usize,
pub stacks_start: usize,
pub stacks_end: usize,
}
impl KernelLayout {
pub fn from_linker() -> Self {
unsafe extern "C" {
static _text_start: u8;
static _text_end: u8;
static _rodata_end: u8;
static _stacks_start: u8;
static _stacks_end: u8;
}
// SAFETY: only the symbols' addresses are taken, never their values.
unsafe {
Self {
text_start: &_text_start as *const u8 as usize,
text_end: &_text_end as *const u8 as usize,
rodata_end: &_rodata_end as *const u8 as usize,
stacks_start: &_stacks_start as *const u8 as usize,
stacks_end: &_stacks_end as *const u8 as usize,
}
}
}
}
/// What `check_wx` found wrong with a kernel page table, if anything.
pub enum WxViolation {
/// A page that is both writable and executable.
WritableCode(usize),
/// An executable page outside the kernel's text.
ExecutableData(usize),
/// A mapped page under the firmware, or the stack guard.
MappedHole(usize),
/// A kernel mapping in the lower half, which belongs to userspace.
LowerHalf(usize),
}
/// Walks every leaf of `root` and checks the kernel layout: nothing is
/// both writable and executable, only kernel text is executable, the
/// firmware and every stack guard are unmapped, and nothing at all is mapped
/// in the lower half. Returns the number of executable pages.
pub fn check_wx(root: &PageTable) -> Result<usize, WxViolation> {
let layout = KernelLayout::from_linker();
let text = layout.text_start..layout.text_end;
let is_guard = |va: usize| {
(layout.stacks_start..layout.stacks_end).contains(&va)
&& (va - layout.stacks_start).is_multiple_of(smp::STACK_SLOT)
};
let firmware = phys_to_virt(board::RAM_BASE)..layout.text_start;
let mut executable = 0;
for (va, bits) in leaves(root) {
if va < phys_to_virt(0) {
return Err(WxViolation::LowerHalf(va));
}
if bits & PTE_W != 0 && bits & PTE_X != 0 {
return Err(WxViolation::WritableCode(va));
}
if bits & PTE_X != 0 {
if !text.contains(&va) {
return Err(WxViolation::ExecutableData(va));
}
executable += 1;
}
if is_guard(va) || firmware.contains(&va) {
return Err(WxViolation::MappedHole(va));
}
}
Ok(executable)
}
/// Every 4 KiB leaf in `root` as (virtual address, PTE flag bits).
/// The kernel maps nothing larger.
fn leaves(root: &PageTable) -> impl Iterator<Item = (usize, u64)> + '_ {
let next = |e: &PageTableEntry| table_at((e.ppn() << 12) as usize) as &PageTable;
root.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.flat_map(move |(i2, e2)| {
next(e2)
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.flat_map(move |(i1, e1)| {
next(e1)
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.map(move |(i0, e0)| {
let va = (i2 << 30) | (i1 << 21) | (i0 << 12);
// Sv39 addresses are sign extended from bit 38.
let va = if i2 >= 256 { va | !((1 << 39) - 1) } else { va };
(va, e0.0 & 0x3ff)
})
})
})
}
pub fn activate(root: &PageTable) {
smp::note_active_root(virt_to_phys(root as *const PageTable as usize));
let satp = (8u64 << 60) | table_ppn(root);
unsafe {
core::arch::asm!(
"csrw satp, {0}",
"sfence.vma",
in(reg) satp,
);
}
}
/// Physical address of the root table the hart is translating with.
pub fn active_root() -> usize {
let satp: usize;
// SAFETY: reading satp has no side effects.
unsafe { core::arch::asm!("csrr {0}, satp", out(reg) satp) };
(satp & ((1 << 44) - 1)) << 12
}

View File

@@ -0,0 +1,11 @@
//! Sv39 page tables: entries and mapping, the kernel table, user address spaces.
mod kernel;
mod pte;
mod user;
pub use kernel::{WxViolation, activate, active_root, check_wx, init_kernel_page_table};
pub use pte::{PAGE_SIZE, PageTable, Sv39Flags, table_at};
pub use user::{
USER_VA_END, USER_VA_START, create_user_root, map_user_page, translate_user, unmap_user_page,
};

View File

@@ -0,0 +1,139 @@
use crate::mmu::allocator::alloc_page_table;
use crate::mmu::{phys_to_virt, virt_to_phys};
use types::mapping::PagePerms;
pub use types::mapping::Mapping;
pub const PAGE_SIZE: usize = 4096;
const PTE_V: u64 = 1 << 0;
const PTE_R: u64 = 1 << 1;
pub(super) const PTE_W: u64 = 1 << 2;
pub(super) const PTE_X: u64 = 1 << 3;
const PTE_U: u64 = 1 << 4;
const PTE_G: u64 = 1 << 5;
const PTE_A: u64 = 1 << 6;
const PTE_D: u64 = 1 << 7;
#[derive(Clone, Copy)]
pub struct Sv39Flags {
bits: u64,
}
impl Sv39Flags {
pub fn kernel_rx() -> Self {
Self {
bits: PTE_R | PTE_X | PTE_G,
}
}
pub fn kernel_ro() -> Self {
Self {
bits: PTE_R | PTE_G,
}
}
pub fn kernel_rw() -> Self {
Self {
bits: PTE_R | PTE_W | PTE_G,
}
}
pub fn user(perms: PagePerms) -> Self {
let bits = match perms {
PagePerms::ReadOnly => PTE_R | PTE_U,
PagePerms::ReadWrite => PTE_R | PTE_W | PTE_U,
PagePerms::ReadExecute => PTE_R | PTE_X | PTE_U,
};
Self { bits }
}
fn to_pte_flags(self) -> u64 {
PTE_V | PTE_A | PTE_D | self.bits
}
}
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct PageTableEntry(pub(super) u64);
impl PageTableEntry {
pub fn empty() -> Self {
Self(0)
}
pub(super) fn new_leaf(ppn: u64, flags: Sv39Flags) -> Self {
Self(flags.to_pte_flags() | (ppn << 10))
}
pub(super) fn new_branch(ppn: u64) -> Self {
Self(PTE_V | (ppn << 10))
}
pub fn is_valid(&self) -> bool {
self.0 & PTE_V != 0
}
pub fn ppn(&self) -> u64 {
(self.0 >> 10) & 0xFFF_FFFF_FFFF
}
}
#[repr(C, align(4096))]
pub struct PageTable {
pub entries: [PageTableEntry; 512],
}
impl PageTable {
pub fn zero(&mut self) {
for entry in &mut self.entries {
*entry = PageTableEntry::empty();
}
}
}
pub(super) fn vpn(va: usize, level: usize) -> usize {
(va >> (12 + 9 * level)) & 0x1FF
}
fn map_page(root: &mut PageTable, va: usize, pa: usize, flags: Sv39Flags) {
let vpn2 = vpn(va, 2);
let vpn1 = vpn(va, 1);
let vpn0 = vpn(va, 0);
if !root.entries[vpn2].is_valid() {
root.entries[vpn2] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
}
let l1 = table_at((root.entries[vpn2].ppn() << 12) as usize);
if !l1.entries[vpn1].is_valid() {
l1.entries[vpn1] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
}
let l0 = table_at((l1.entries[vpn1].ppn() << 12) as usize);
let ppn = (pa as u64) >> 12;
l0.entries[vpn0] = PageTableEntry::new_leaf(ppn, flags);
}
/// The physical page number of a page table the kernel holds a reference to.
pub(super) fn table_ppn(table: &PageTable) -> u64 {
(virt_to_phys(table as *const PageTable as usize) as u64) >> 12
}
pub fn map(root: &mut PageTable, mapping: Mapping<Sv39Flags>) {
for i in 0..mapping.pages {
let offset = i * PAGE_SIZE;
map_page(
root,
mapping.va.raw() + offset,
mapping.pa.raw() + offset,
mapping.flags,
);
}
}
/// The page table at physical address `pa`, through the direct map.
pub fn table_at(pa: usize) -> &'static mut PageTable {
// SAFETY: every page table lives in RAM, which the kernel maps at
// `phys_to_virt`.
unsafe { &mut *(phys_to_virt(pa) as *mut PageTable) }
}

View File

@@ -0,0 +1,94 @@
use super::pte::{PAGE_SIZE, PageTable, PageTableEntry, Sv39Flags, table_at, vpn};
use platform::{FrameSource, MapError};
/// User mappings live in root slot 0, in a private L1 per address space:
/// all of its 1 GiB (leaf tables are made as pages are mapped).
pub const USER_VA_START: usize = 0x1000;
pub const USER_VA_END: usize = 0x4000_0000;
fn fresh_table(frames: &mut dyn FrameSource) -> Result<usize, MapError> {
let pa = frames
.alloc_table_frame()
.ok_or(MapError::OutOfFrames)?
.raw();
table_at(pa).zero();
Ok(pa)
}
fn in_user_window(va: usize) -> bool {
va.is_multiple_of(PAGE_SIZE) && (USER_VA_START..USER_VA_END).contains(&va)
}
/// Builds a root table sharing the kernel's mappings, with a private L1 for
/// the user window. Returns the root's physical address.
pub fn create_user_root(
kernel_root: &PageTable,
frames: &mut dyn FrameSource,
) -> Result<usize, MapError> {
let root_pa = fresh_table(frames)?;
let l1_pa = fresh_table(frames)?;
let root = table_at(root_pa);
for (mine, kernels) in root.entries.iter_mut().zip(&kernel_root.entries).skip(256) {
*mine = *kernels;
}
root.entries[0] = PageTableEntry::new_branch((l1_pa as u64) >> 12);
Ok(root_pa)
}
fn user_leaf_table(
root_pa: usize,
va: usize,
frames: Option<&mut dyn FrameSource>,
) -> Result<&'static mut PageTable, MapError> {
let root = table_at(root_pa);
let l1 = table_at((root.entries[0].ppn() << 12) as usize);
let slot = &mut l1.entries[vpn(va, 1)];
if !slot.is_valid() {
let frames = frames.ok_or(MapError::NoAddressSpace)?;
let l0_pa = fresh_table(frames)?;
*slot = PageTableEntry::new_branch((l0_pa as u64) >> 12);
}
Ok(table_at((slot.ppn() << 12) as usize))
}
pub fn map_user_page(
root_pa: usize,
va: usize,
pa: usize,
flags: Sv39Flags,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
if !in_user_window(va) || !pa.is_multiple_of(PAGE_SIZE) {
return Err(MapError::InvalidAddress);
}
let l0 = user_leaf_table(root_pa, va, Some(frames))?;
l0.entries[vpn(va, 0)] = PageTableEntry::new_leaf((pa as u64) >> 12, flags);
flush_tlb(va);
Ok(())
}
/// The physical page `va` maps to in the address space at `root_pa`.
pub fn translate_user(root_pa: usize, va: usize) -> Option<usize> {
if !in_user_window(va) {
return None;
}
let l0 = user_leaf_table(root_pa, va, None).ok()?;
let entry = l0.entries[vpn(va, 0)];
entry.is_valid().then(|| (entry.ppn() << 12) as usize)
}
pub fn unmap_user_page(root_pa: usize, va: usize) {
if !in_user_window(va) {
return;
}
if let Ok(l0) = user_leaf_table(root_pa, va, None) {
l0.entries[vpn(va, 0)] = PageTableEntry::empty();
flush_tlb(va);
}
}
fn flush_tlb(va: usize) {
// SAFETY: sfence.vma only invalidates translation caches.
unsafe { core::arch::asm!("sfence.vma {0}, zero", in(reg) va) };
}

View File

@@ -1,29 +1,28 @@
use domain::memory::MemoryRegion;
use domain::process::MAX_PROCESSES; use domain::process::MAX_PROCESSES;
use platform::Platform; use platform::{FrameSource, MapError, Platform};
use types::{ use types::{
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
bounded::PageCount,
id::{CoreId, IrqId, ProcessId}, id::{CoreId, IrqId, ProcessId},
permission::{Permission, PermissionSet}, mapping::PagePerms,
time::{Ticks, TimerDuration}, time::Ticks,
}; };
use crate::board;
use crate::hw::{plic::Plic, timer}; use crate::hw::{plic::Plic, timer};
use crate::mmu; use crate::mmu;
use crate::smp;
use crate::trap; use crate::trap;
pub struct RiscVPlatform { pub struct RiscVPlatform {
num_cores: u8, num_cores: u8,
kernel_pt: usize, kernel_pt: usize,
/// Root table of each process's address space, 0 if it has none.
page_tables: [usize; MAX_PROCESSES], page_tables: [usize; MAX_PROCESSES],
} }
impl RiscVPlatform { impl RiscVPlatform {
pub fn new(kernel_pt_addr: usize) -> Self { pub const fn new(kernel_pt_addr: usize, num_cores: u8) -> Self {
Self { Self {
num_cores: 1, num_cores,
kernel_pt: kernel_pt_addr, kernel_pt: kernel_pt_addr,
page_tables: [0usize; MAX_PROCESSES], page_tables: [0usize; MAX_PROCESSES],
} }
@@ -33,37 +32,86 @@ impl RiscVPlatform {
self.page_tables[pid.raw() as usize] self.page_tables[pid.raw() as usize]
} }
pub fn set_page_table(&mut self, pid: ProcessId, addr: usize) { fn root(&self, pid: ProcessId) -> Option<usize> {
self.page_tables[pid.raw() as usize] = addr; match self.page_tables.get(pid.raw() as usize) {
} Some(&0) | None => None,
Some(&root) => Some(root),
fn ensure_page_table(&mut self, pid: ProcessId) -> &mut mmu::PageTable {
let idx = pid.raw() as usize;
if self.page_tables[idx] == 0 {
let kernel_pt = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
let user_pt = mmu::create_user_page_table(
kernel_pt,
PhysicalAddress::from_raw(0),
0,
VirtualAddress::from_raw(0x10000),
);
self.page_tables[idx] = user_pt as *const mmu::PageTable as usize;
} }
unsafe { &mut *(self.page_tables[idx] as *mut mmu::PageTable) }
} }
} }
impl Platform for RiscVPlatform { impl Platform for RiscVPlatform {
fn configure_protection(&mut self, process: ProcessId, regions: &[MemoryRegion]) { const USER_VA_START: usize = mmu::USER_VA_START;
let pt = self.ensure_page_table(process); const USER_VA_END: usize = mmu::USER_VA_END;
for region in regions {
mmu::map(pt, mmu::Mapping { fn create_address_space(
va: VirtualAddress::from_raw(region.base().raw()), &mut self,
pa: region.base(), process: ProcessId,
pages: region.size().value() as usize, frames: &mut dyn FrameSource,
flags: mmu::Sv39Flags::user(true, true, true), ) -> Result<(), MapError> {
}); let slot = self
.page_tables
.get_mut(process.raw() as usize)
.ok_or(MapError::NoAddressSpace)?;
// SAFETY: the kernel page table lives in the boot heap forever.
let kernel_root = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
*slot = mmu::create_user_root(kernel_root, frames)?;
Ok(())
}
fn destroy_address_space(&mut self, process: ProcessId) {
let Some(root) = self.root(process) else {
return;
};
// No hart may translate through it once its frames are freed; that
// includes this one, which may be running on it.
smp::shoot_down(root);
if mmu::active_root() == root {
// SAFETY: the kernel page table lives in the boot heap forever.
mmu::activate(unsafe { &*(self.kernel_pt as *const mmu::PageTable) });
} }
self.page_tables[process.raw() as usize] = 0;
// SAFETY: flushing translations has no other effect.
unsafe { core::arch::asm!("sfence.vma zero, zero") };
}
fn map_page(
&mut self,
process: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
perms: PagePerms,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
let root = self.root(process).ok_or(MapError::NoAddressSpace)?;
mmu::map_user_page(
root,
va.raw(),
pa.raw(),
mmu::Sv39Flags::user(perms),
frames,
)
}
fn unmap_page(&mut self, process: ProcessId, va: VirtualAddress) {
if let Some(root) = self.root(process) {
mmu::unmap_user_page(root, va.raw());
// Other harts running this process may still cache it.
smp::shoot_down(root);
}
}
fn translate(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
let root = self.root(process)?;
mmu::translate_user(root, va.raw()).map(PhysicalAddress::from_raw)
}
fn zero_frame(&mut self, pa: PhysicalAddress) {
// SAFETY: pool frames are RAM, mapped at `phys_to_virt`, and the
// kernel only zeroes frames it has just allocated.
unsafe {
core::ptr::write_bytes(mmu::phys_to_virt(pa.raw()) as *mut u8, 0, mmu::PAGE_SIZE)
};
} }
fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> ! { fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> ! {
@@ -87,9 +135,8 @@ impl Platform for RiscVPlatform {
} }
} }
fn set_timer(&mut self, _duration: TimerDuration) { fn wake_at(&mut self, deadline: Ticks) {
let now = timer::read_time(); timer::wake_at(deadline.raw());
timer::set_next(now + board::TICK_INTERVAL);
} }
fn current_ticks(&self) -> Ticks { fn current_ticks(&self) -> Ticks {
@@ -104,68 +151,34 @@ impl Platform for RiscVPlatform {
Plic::enable(irq.raw() as u32); Plic::enable(irq.raw() as u32);
} }
fn send_ipi(&mut self, _target: CoreId) {} fn send_ipi(&mut self, target: CoreId) {
smp::kick(target.raw() as usize);
}
fn core_id(&self) -> CoreId { fn core_id(&self) -> CoreId {
CoreId::from_raw(0) CoreId::from_raw(smp::hart_id() as u8)
} }
fn num_cores(&self) -> u8 { fn num_cores(&self) -> u8 {
self.num_cores self.num_cores
} }
fn map_shared(
&mut self,
process: ProcessId,
base: PhysicalAddress,
size: PageCount,
permissions: PermissionSet,
) {
let pt = self.ensure_page_table(process);
mmu::map(pt, mmu::Mapping {
va: VirtualAddress::from_raw(base.raw()),
pa: base,
pages: size.value() as usize,
flags: mmu::Sv39Flags::user(
permissions.allows(Permission::Read),
permissions.allows(Permission::Write),
permissions.allows(Permission::Execute),
),
});
}
fn debug_putchar(&self, byte: u8) { fn debug_putchar(&self, byte: u8) {
crate::hw::uart::Uart::new().put_char(byte); crate::hw::uart::Uart::new().put_char(byte);
} }
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
let pt_addr = self.page_tables[process.raw() as usize];
if pt_addr == 0 { return None; }
let pt = unsafe { &*(pt_addr as *const mmu::PageTable) };
mmu::translate(pt, va.raw()).map(PhysicalAddress::from_raw)
}
fn map_user_page(&mut self, process: ProcessId, va: VirtualAddress, pa: PhysicalAddress, writable: bool, executable: bool) {
let pt = self.ensure_page_table(process);
let flags = mmu::Sv39Flags::user(true, writable, executable);
mmu::map(pt, mmu::Mapping {
va,
pa,
pages: 1,
flags,
});
}
fn halt(&self) -> ! { fn halt(&self) -> ! {
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"ecall", "ecall",
in("a0") 0usize, inlateout("a0") 0usize => _,
in("a1") 0usize, inlateout("a1") 0usize => _,
in("a7") 0x53525354usize, in("a7") 0x53525354usize,
in("a6") 0usize, in("a6") 0usize,
); );
} }
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }
} }

View File

@@ -0,0 +1,42 @@
//! State published under the kernel lock for lock-free fast paths to read.
use core::sync::atomic::{AtomicUsize, Ordering};
use super::{MAX_HARTS, hart_id};
/// Per core: highest waiting priority + 1, 0 if its queue is empty.
static WAITING: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
/// Per hart: the running thread's process id + 1 and priority + 1.
static RUNNING_PID: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
static RUNNING_PRIORITY: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
pub fn publish_waiting(core: usize, highest: Option<u8>) {
if let Some(w) = WAITING.get(core) {
w.store(highest.map_or(0, |p| p as usize + 1), Ordering::Release);
}
}
pub fn publish_running(pid: Option<u16>, priority: Option<u8>) {
let me = hart_id();
if let (Some(p), Some(q)) = (RUNNING_PID.get(me), RUNNING_PRIORITY.get(me)) {
p.store(pid.map_or(0, |x| x as usize + 1), Ordering::Release);
q.store(priority.map_or(0, |x| x as usize + 1), Ordering::Release);
}
}
/// The process the thread running on this hart belongs to.
pub fn running_pid() -> Option<usize> {
let v = RUNNING_PID.get(hart_id())?.load(Ordering::Acquire);
v.checked_sub(1)
}
/// Whether a Yield by the running thread would change nothing: no thread
/// waits on this core at its priority or above.
pub fn yield_is_noop() -> bool {
let me = hart_id();
let (Some(w), Some(p)) = (WAITING.get(me), RUNNING_PRIORITY.get(me)) else {
return false;
};
let mine = p.load(Ordering::Acquire);
mine != 0 && w.load(Ordering::Acquire) < mine
}

View File

@@ -0,0 +1,96 @@
//! Reschedule and leave-root IPIs, serviced without the kernel lock.
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use super::sbi::sbi_send_ipi;
use super::{MAX_HARTS, hart_id, kernel_root, num_harts};
use crate::mmu;
const SIP_SSIP: usize = 1 << 1;
/// The root table each hart translates with.
static ACTIVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
static RESCHEDULE: [AtomicBool; MAX_HARTS] = [const { AtomicBool::new(false) }; MAX_HARTS];
static LEAVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
static LEFT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
/// Called by `mmu::activate`.
pub fn note_active_root(root: usize) {
if let Some(r) = ACTIVE_ROOT.get(hart_id()) {
r.store(root, Ordering::SeqCst);
}
}
/// Whether this hart has an IPI or a reschedule request it has not acted
/// on; fast paths must then take the slow path.
pub fn has_pending_request() -> bool {
let sip: usize;
// SAFETY: reading sip has no side effects.
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
sip & SIP_SSIP != 0
|| RESCHEDULE
.get(hart_id())
.is_some_and(|r| r.load(Ordering::SeqCst))
}
// ---- IPIs ----
/// The lock-free half of an IPI: acknowledges it and leaves the root being
/// changed, if asked. Safe to call at any time.
pub fn service_ipi() {
let sip: usize;
// SAFETY: reading sip has no side effects.
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
if sip & SIP_SSIP == 0 {
return;
}
// Clear first: a request posted after this raises SSIP again.
// SAFETY: clearing our own pending software interrupt.
unsafe { core::arch::asm!("csrc sip, {0}", in(reg) SIP_SSIP) };
let me = hart_id();
let (Some(leave), Some(left)) = (LEAVE_ROOT.get(me), LEFT.get(me)) else {
return;
};
let root = leave.swap(0, Ordering::SeqCst);
if root != 0 {
if ACTIVE_ROOT.get(me).map(|r| r.load(Ordering::SeqCst)) == Some(root) {
mmu::activate(mmu::page_table::table_at(kernel_root()));
}
// SAFETY: flushing translations has no other effect.
unsafe { core::arch::asm!("sfence.vma zero, zero") };
left.fetch_add(1, Ordering::SeqCst);
}
}
/// Asks `hart` to look at its run queue again, on its way out of the
/// kernel.
pub fn kick(hart: usize) {
if let Some(r) = RESCHEDULE.get(hart) {
r.store(true, Ordering::SeqCst);
sbi_send_ipi(hart);
}
}
/// Consumes this hart's reschedule request.
pub fn take_reschedule() -> bool {
RESCHEDULE
.get(hart_id())
.is_some_and(|r| r.swap(false, Ordering::SeqCst))
}
/// Makes every other hart leave `root` and flush, waiting for each.
/// Kernel lock held, so no hart can start using `root` meanwhile.
pub fn shoot_down(root: usize) {
let me = hart_id();
for hart in 0..num_harts() {
if hart == me || ACTIVE_ROOT[hart].load(Ordering::SeqCst) != root {
continue;
}
let seen = LEFT[hart].load(Ordering::SeqCst);
LEAVE_ROOT[hart].store(root, Ordering::SeqCst);
sbi_send_ipi(hart);
while LEFT[hart].load(Ordering::SeqCst) == seen {
core::hint::spin_loop();
}
}
}

View File

@@ -0,0 +1,22 @@
//! The kernel lock: a ticket lock that services IPIs while waiting.
use core::sync::atomic::{AtomicU32, Ordering};
use super::service_ipi;
static NEXT_TICKET: AtomicU32 = AtomicU32::new(0);
static SERVING: AtomicU32 = AtomicU32::new(0);
/// Takes the kernel lock, first come first served. Leave-root requests
/// are serviced while waiting.
pub fn lock() {
let ticket = NEXT_TICKET.fetch_add(1, Ordering::Relaxed);
while SERVING.load(Ordering::Acquire) != ticket {
service_ipi();
core::hint::spin_loop();
}
}
pub fn unlock() {
SERVING.fetch_add(1, Ordering::Release);
}

View File

@@ -0,0 +1,61 @@
//! Harts: per-hart kernel stacks (the hart id doubles as the kernel `CoreId`),
//! the kernel lock, and IPIs. The kernel runs under one lock; userspace runs in parallel.
mod hints;
mod ipi;
mod lock;
mod sbi;
pub use hints::{publish_running, publish_waiting, running_pid, yield_is_noop};
pub use ipi::{
has_pending_request, kick, note_active_root, service_ipi, shoot_down, take_reschedule,
};
pub use lock::{lock, unlock};
pub use sbi::{probe_harts, sbi_hart_start};
use core::sync::atomic::{AtomicUsize, Ordering};
/// Must match linker.ld.
pub const MAX_HARTS: usize = 8;
/// A guard page and a 512 KiB stack. Must match linker.ld.
pub const STACK_SLOT: usize = 4096 + 0x8_0000;
/// The hart running this code, from which kernel stack it is on.
pub fn hart_id() -> usize {
let sp: usize;
// SAFETY: reading sp has no side effects.
unsafe { core::arch::asm!("mv {0}, sp", out(reg) sp) };
(sp - stacks_start()) / STACK_SLOT
}
pub fn stacks_start() -> usize {
unsafe extern "C" {
static _stacks_start: u8;
}
// SAFETY: only the symbol's address is taken.
unsafe { &_stacks_start as *const u8 as usize }
}
static BOOT_HART: AtomicUsize = AtomicUsize::new(0);
static NUM_HARTS: AtomicUsize = AtomicUsize::new(1);
/// Physical address of the kernel's own root table.
static KERNEL_ROOT: AtomicUsize = AtomicUsize::new(0);
pub fn boot_hart() -> usize {
BOOT_HART.load(Ordering::Relaxed)
}
pub fn num_harts() -> usize {
NUM_HARTS.load(Ordering::Relaxed)
}
pub fn kernel_root() -> usize {
KERNEL_ROOT.load(Ordering::Relaxed)
}
/// Records the boot hart, the hart count and the kernel root table.
pub fn init(boot: usize, harts: usize, kernel_root: usize) {
BOOT_HART.store(boot, Ordering::Relaxed);
NUM_HARTS.store(harts, Ordering::Relaxed);
KERNEL_ROOT.store(kernel_root, Ordering::Relaxed);
}

View File

@@ -0,0 +1,60 @@
//! SBI calls: IPIs and hart state management.
use super::MAX_HARTS;
const SBI_EXT_IPI: usize = 0x735049;
const SBI_EXT_HSM: usize = 0x48534D;
pub(super) fn sbi_send_ipi(hart: usize) {
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") 1usize => _,
inlateout("a1") hart => _,
in("a6") 0usize,
in("a7") SBI_EXT_IPI,
);
}
}
/// Whether `hart` exists, per SBI HSM `hart_get_status`.
fn sbi_hart_exists(hart: usize) -> bool {
let error: isize;
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") hart => error,
lateout("a1") _,
in("a6") 2usize,
in("a7") SBI_EXT_HSM,
);
}
error == 0
}
/// The number of harts, counting up from hart 0 until one is missing.
pub fn probe_harts() -> usize {
(0..MAX_HARTS)
.take_while(|h| sbi_hart_exists(*h))
.count()
.max(1)
}
/// Starts `hart` at the physical address `entry` with the MMU off.
pub fn sbi_hart_start(hart: usize, entry: usize) -> bool {
let error: isize;
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") hart => error,
inlateout("a1") entry => _,
in("a2") 0usize,
in("a6") 0usize,
in("a7") SBI_EXT_HSM,
);
}
error == 0
}

View File

@@ -0,0 +1,22 @@
use super::{TrapFrame, write_result};
/// Syscalls answered without the kernel lock because they touch nothing it
/// guards. A pending IPI or reschedule request forces the slow path.
pub(super) fn fast_syscall(frame: &mut TrapFrame) -> bool {
use types::syscall_abi::{SYS_GET_PROCESS_ID, SYS_GET_TIME, SYS_YIELD};
if crate::smp::has_pending_request() {
return false;
}
let result = match frame.regs[17] {
SYS_GET_TIME => kernel::SyscallResult::Value(crate::hw::timer::read_time() as usize),
SYS_GET_PROCESS_ID => match crate::smp::running_pid() {
Some(pid) => kernel::SyscallResult::Value(pid),
None => return false,
},
SYS_YIELD if crate::smp::yield_is_noop() => kernel::SyscallResult::Ok,
_ => return false,
};
frame.sepc += 4;
write_result(frame, &result);
true
}

View File

@@ -0,0 +1,165 @@
//! Floating point for user threads, lazily. A thread starts with the FPU
//! off (`sstatus.FS`); its first floating-point instruction traps as an
//! illegal instruction, and it is given zeroed registers and the FPU and
//! retries. From then on its registers are saved when a trap finds them
//! changed (FS dirty) and loaded whenever it is switched to; a thread
//! without them runs with the FPU off, so it never sees another's. The
//! kernel itself runs with the FPU off.
use core::arch::asm;
use domain::thread::{FpState, SavedContext};
use super::TrapFrame;
const FS: usize = 3 << 13;
const FS_OFF: usize = 0;
const FS_CLEAN: usize = 2 << 13;
const FS_DIRTY: usize = 3 << 13;
/// Turns the FPU off for the kernel; what the trap frame says the thread
/// returns with is unchanged.
pub(super) fn kernel_off() {
// SAFETY: clears sstatus.FS only.
unsafe { asm!("csrc sstatus, {0}", in(reg) FS) };
}
fn hardware_on() {
// SAFETY: sets sstatus.FS to (at least) clean, so the FP registers can
// be read and written; `kernel_off` turns it off again.
unsafe { asm!("csrs sstatus, {0}", in(reg) FS_CLEAN) };
}
/// On a trap from a thread: its registers into its context if it changed
/// them, and the frame then says they are saved.
pub(super) fn save_if_dirty(frame: &mut TrapFrame, ctx: &mut SavedContext) {
if frame.sstatus & FS != FS_DIRTY {
return;
}
if let Some(state) = ctx.fp_mut() {
hardware_on();
save(state);
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
}
}
/// Returning to a thread: its registers into the FPU, or the FPU off.
pub(super) fn load_for(frame: &mut TrapFrame, ctx: &SavedContext) {
frame.sstatus &= !FS;
match ctx.fp() {
Some(state) => {
hardware_on();
load(state);
frame.sstatus |= FS_CLEAN;
}
None => frame.sstatus |= FS_OFF,
}
}
/// An illegal instruction from a thread with the FPU off and no registers
/// yet: it gets them, zeroed, and retries. Whether that was it (otherwise
/// the instruction is a real fault).
pub(super) fn first_use(frame: &mut TrapFrame, ctx: &mut SavedContext) -> bool {
if frame.sstatus & FS != FS_OFF || ctx.fp().is_some() {
return false;
}
let state = ctx.enable_fp();
hardware_on();
load(state);
// sepc still names the instruction, so it runs again.
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
true
}
/// `$op` (fsd or fld) for every FP register, at `{base}` + 8 each.
macro_rules! fp_registers {
($op:literal) => {
concat!(
$op,
" f0, 0({base})\n",
$op,
" f1, 8({base})\n",
$op,
" f2, 16({base})\n",
$op,
" f3, 24({base})\n",
$op,
" f4, 32({base})\n",
$op,
" f5, 40({base})\n",
$op,
" f6, 48({base})\n",
$op,
" f7, 56({base})\n",
$op,
" f8, 64({base})\n",
$op,
" f9, 72({base})\n",
$op,
" f10, 80({base})\n",
$op,
" f11, 88({base})\n",
$op,
" f12, 96({base})\n",
$op,
" f13, 104({base})\n",
$op,
" f14, 112({base})\n",
$op,
" f15, 120({base})\n",
$op,
" f16, 128({base})\n",
$op,
" f17, 136({base})\n",
$op,
" f18, 144({base})\n",
$op,
" f19, 152({base})\n",
$op,
" f20, 160({base})\n",
$op,
" f21, 168({base})\n",
$op,
" f22, 176({base})\n",
$op,
" f23, 184({base})\n",
$op,
" f24, 192({base})\n",
$op,
" f25, 200({base})\n",
$op,
" f26, 208({base})\n",
$op,
" f27, 216({base})\n",
$op,
" f28, 224({base})\n",
$op,
" f29, 232({base})\n",
$op,
" f30, 240({base})\n",
$op,
" f31, 248({base})\n",
)
};
}
/// The FPU's registers into `state` (the FPU must be on).
fn save(state: &mut FpState) {
let base = state.registers.as_mut_ptr();
let control: u64;
// SAFETY: `base` points at 32 u64s, written in place; frcsr only reads.
unsafe {
asm!(fp_registers!("fsd"), base = in(reg) base, options(nostack));
asm!("frcsr {0}", out(reg) control, options(nostack));
}
state.control = control;
}
/// `state` into the FPU's registers (the FPU must be on).
fn load(state: &FpState) {
let base = state.registers.as_ptr();
// SAFETY: `base` points at 32 u64s, only read; fscsr writes fcsr.
unsafe {
asm!(fp_registers!("fld"), base = in(reg) base, options(nostack, readonly));
asm!("fscsr {0}", in(reg) state.control, options(nostack));
}
}

View File

@@ -1,4 +1,11 @@
use core::arch::global_asm; mod fast;
mod fp;
mod sched;
mod vector;
use fast::fast_syscall;
pub use sched::enter_first;
use sched::{finish, release, switch_to_next, switch_to_thread};
#[repr(C)] #[repr(C)]
pub struct TrapFrame { pub struct TrapFrame {
@@ -7,135 +14,26 @@ pub struct TrapFrame {
pub sstatus: usize, pub sstatus: usize,
} }
impl TrapFrame {
pub fn new() -> Self {
Self {
regs: [0; 32],
sepc: 0,
sstatus: 0,
}
}
}
global_asm!(r#"
.align 4
.globl trap_vector
trap_vector:
csrrw sp, sscratch, sp
bnez sp, _from_user
csrrw sp, sscratch, sp
_from_user:
addi sp, sp, -272
sd x1, 8(sp)
sd x3, 24(sp)
sd x4, 32(sp)
sd x5, 40(sp)
sd x6, 48(sp)
sd x7, 56(sp)
sd x8, 64(sp)
sd x9, 72(sp)
sd x10, 80(sp)
sd x11, 88(sp)
sd x12, 96(sp)
sd x13, 104(sp)
sd x14, 112(sp)
sd x15, 120(sp)
sd x16, 128(sp)
sd x17, 136(sp)
sd x18, 144(sp)
sd x19, 152(sp)
sd x20, 160(sp)
sd x21, 168(sp)
sd x22, 176(sp)
sd x23, 184(sp)
sd x24, 192(sp)
sd x25, 200(sp)
sd x26, 208(sp)
sd x27, 216(sp)
sd x28, 224(sp)
sd x29, 232(sp)
sd x30, 240(sp)
sd x31, 248(sp)
csrr t0, sscratch
sd t0, 16(sp)
csrr t0, sepc
sd t0, 256(sp)
csrr t0, sstatus
sd t0, 264(sp)
mv a0, sp
call trap_handler_rust
ld t0, 256(sp)
csrw sepc, t0
ld t0, 264(sp)
csrw sstatus, t0
andi t1, t0, 0x100
bnez t1, _restore_smode
ld t0, 16(sp)
csrw sscratch, t0
_restore_smode:
ld x1, 8(sp)
ld x3, 24(sp)
ld x4, 32(sp)
ld x5, 40(sp)
ld x6, 48(sp)
ld x7, 56(sp)
ld x8, 64(sp)
ld x9, 72(sp)
ld x10, 80(sp)
ld x11, 88(sp)
ld x12, 96(sp)
ld x13, 104(sp)
ld x14, 112(sp)
ld x15, 120(sp)
ld x16, 128(sp)
ld x17, 136(sp)
ld x18, 144(sp)
ld x19, 152(sp)
ld x20, 160(sp)
ld x21, 168(sp)
ld x22, 176(sp)
ld x23, 184(sp)
ld x24, 192(sp)
ld x25, 200(sp)
ld x26, 208(sp)
ld x27, 216(sp)
ld x28, 224(sp)
ld x29, 232(sp)
ld x30, 240(sp)
ld x31, 248(sp)
addi sp, sp, 272
csrr t0, sstatus
andi t0, t0, 0x100
bnez t0, _sret_smode
csrrw sp, sscratch, sp
_sret_smode:
sret
"#);
const CAUSE_INTERRUPT_BIT: usize = 1 << 63; const CAUSE_INTERRUPT_BIT: usize = 1 << 63;
const CAUSE_S_SOFTWARE: usize = 1;
const CAUSE_S_TIMER: usize = 5; const CAUSE_S_TIMER: usize = 5;
const CAUSE_S_EXTERNAL: usize = 9; const CAUSE_S_EXTERNAL: usize = 9;
const CAUSE_ILLEGAL_INSTRUCTION: usize = 2;
const CAUSE_ECALL_U: usize = 8; const CAUSE_ECALL_U: usize = 8;
/// Set in `sstatus` when the trap came from supervisor mode.
const SSTATUS_SPP: usize = 1 << 8;
pub fn init() { pub fn init() {
unsafe extern "C" { unsafe extern "C" {
fn trap_vector(); fn trap_vector();
} }
unsafe { unsafe {
let addr = trap_vector as usize; let addr = trap_vector as *const () as usize;
core::arch::asm!("csrw stvec, {0}", in(reg) addr); core::arch::asm!("csrw stvec, {0}", in(reg) addr);
core::arch::asm!("csrw sscratch, zero"); core::arch::asm!("csrw sscratch, zero");
// SIE stays clear — interrupts are enabled only when entering userspace via sret // The kernel never uses the FPU; user threads get it lazily.
core::arch::asm!("csrc sstatus, {0}", in(reg) 3usize << 13);
// SIE stays clear; interrupts are enabled only by sret into userspace.
let sie_bits = (1usize << 5) | (1usize << 9) | (1usize << 1); let sie_bits = (1usize << 5) | (1usize << 9) | (1usize << 1);
core::arch::asm!("csrs sie, {0}", in(reg) sie_bits); core::arch::asm!("csrs sie, {0}", in(reg) sie_bits);
} }
@@ -147,232 +45,38 @@ pub fn set_kernel_stack(kernel_sp: usize) {
} }
} }
fn decode_syscall(frame: &TrapFrame) -> Option<domain::syscall::Syscall> { fn args(frame: &TrapFrame) -> [usize; 6] {
use domain::message::Message; let mut a = [0usize; 6];
use domain::syscall::{Syscall, WaitSet}; a.copy_from_slice(&frame.regs[10..16]);
use types::syscall_abi::*; a
let nr = frame.regs[17]; // a7
let a0 = frame.regs[10];
let a1 = frame.regs[11];
let a2 = frame.regs[12];
let a3 = frame.regs[13];
let a4 = frame.regs[14];
match nr {
SYS_EXIT => Some(Syscall::Exit),
SYS_HALT => Some(Syscall::Halt),
SYS_EXIT_THREAD => Some(Syscall::ExitThread),
SYS_YIELD => Some(Syscall::Yield),
SYS_GET_TIME => Some(Syscall::GetTime),
SYS_GET_PROCESS_ID => Some(Syscall::GetProcessId),
SYS_SEND => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
let msg = Message::from_words([a1, a2, a3, a4]);
Some(Syscall::Send { endpoint: slot, message: msg })
}
SYS_RECEIVE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Receive { endpoint: slot })
}
SYS_DELEGATE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
let to = types::bounded::CapSlot::new(a1 as u8)?;
let perms = types::permission::PermissionSet::from_bits(a2 as u8);
Some(Syscall::Delegate { slot, to_endpoint: to, permissions: perms })
}
SYS_WAIT_FOR_INTERRUPT => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::WaitForInterrupt { irq: slot })
}
SYS_ACKNOWLEDGE_INTERRUPT => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::AcknowledgeInterrupt { irq: slot })
}
SYS_WAIT_ANY => {
let ep0 = if a0 != usize::MAX { types::bounded::CapSlot::new(a0 as u8) } else { None };
let ep1 = if a1 != usize::MAX { types::bounded::CapSlot::new(a1 as u8) } else { None };
let ep2 = if a2 != usize::MAX { types::bounded::CapSlot::new(a2 as u8) } else { None };
let ep3 = if a3 != usize::MAX { types::bounded::CapSlot::new(a3 as u8) } else { None };
let sources = WaitSet::new(
[ep0, ep1, ep2, ep3],
[None, None, None, None],
);
Some(Syscall::WaitAny { sources })
}
SYS_CREATE_PROCESS => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::CreateProcess {
create_cap: slot,
image: types::address::DataRef::empty(),
})
}
SYS_KILL => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Kill { process: slot })
}
SYS_REVOKE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Revoke { slot })
}
SYS_SPAWN_THREAD => Some(Syscall::SpawnThread {
entry: types::address::EntryPoint::from_raw(a0),
stack: types::address::StackPointer::from_raw(a1),
}),
SYS_REQUEST_PAGES => {
let count = types::bounded::PageCount::new(a0 as u16)?;
Some(Syscall::RequestPages { count })
}
SYS_RELEASE_PAGES => {
let count = types::bounded::PageCount::new(a1 as u16)?;
Some(Syscall::ReleasePages {
addr: types::address::VirtualAddress::from_raw(a0),
count,
})
}
SYS_SPAWN_THREAD_IN => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::SpawnThreadIn {
process: slot,
entry: types::address::EntryPoint::from_raw(a1),
stack: types::address::StackPointer::from_raw(a2),
})
}
SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint),
SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }),
SYS_MAP_PAGE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::MapPage {
process: slot,
src_va: types::address::VirtualAddress::from_raw(a1),
dst_va: types::address::VirtualAddress::from_raw(a2),
executable: a3 != 0,
})
}
_ => None,
}
} }
fn write_result(frame: &mut TrapFrame, outcome: &kernel::SyscallOutcome) { fn set_results(frame: &mut TrapFrame, regs: [usize; 8]) {
use kernel::{SyscallOutcome, SyscallResult}; frame.regs[10..18].copy_from_slice(&regs);
match outcome {
SyscallOutcome::Complete(result) => match result {
SyscallResult::Ok => frame.regs[10] = 0,
SyscallResult::Value(v) => frame.regs[10] = *v,
SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize,
SyscallResult::Message(m) => {
frame.regs[10] = m.word(0);
frame.regs[11] = m.word(1);
frame.regs[12] = m.word(2);
frame.regs[13] = m.word(3);
}
SyscallResult::Error(_) => frame.regs[10] = usize::MAX,
SyscallResult::WakeReason(_) => frame.regs[10] = 0,
},
SyscallOutcome::BlockCaller(_) => {}
SyscallOutcome::SwitchTo(_) => frame.regs[10] = 0,
SyscallOutcome::TerminateCaller => {}
}
} }
fn save_frame_to_context(frame: &TrapFrame, ctx: &mut domain::thread::SavedContext) { fn write_result(frame: &mut TrapFrame, result: &kernel::SyscallResult) {
ctx.registers_mut().copy_from_slice(&frame.regs); set_results(frame, kernel::abi::encode(result));
ctx.set_pc(frame.sepc);
ctx.set_status(frame.sstatus);
} }
fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFrame) { fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
frame.regs.copy_from_slice(ctx.registers()); // SAFETY: only called with the kernel lock held.
frame.sepc = ctx.program_counter(); unsafe { &mut *crate::kernel_ptr() }
frame.sstatus = ctx.status();
// ensure returning to U-mode
frame.sstatus &= !(1 << 8); // clear SPP
frame.sstatus |= 1 << 5; // set SPIE
} }
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) { fn this_core() -> types::id::CoreId {
let kern = unsafe { &mut *crate::kernel_ptr() }; types::id::CoreId::from_raw(crate::smp::hart_id() as u8)
if let Some(result) = kern.take_pending_result(tid) {
write_result_direct(frame, &result);
}
} }
fn write_result_direct(frame: &mut TrapFrame, result: &kernel::SyscallResult) { /// Claims every pending PLIC interrupt (boot hart only) and hands it to the kernel.
use kernel::SyscallResult; fn dispatch_external() {
match result { loop {
SyscallResult::Ok => frame.regs[10] = 0, let irq_num = crate::hw::plic::Plic::claim();
SyscallResult::Value(v) => frame.regs[10] = *v, if irq_num == 0 {
SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize, return;
SyscallResult::Message(m) => {
frame.regs[10] = m.word(0);
frame.regs[11] = m.word(1);
frame.regs[12] = m.word(2);
frame.regs[13] = m.word(3);
} }
SyscallResult::Error(_) => frame.regs[10] = usize::MAX, kern().handle_interrupt(types::id::IrqId::from_raw(irq_num as u16));
SyscallResult::WakeReason(_) => frame.regs[10] = 0, crate::hw::plic::Plic::complete(irq_num);
}
}
fn switch_to_thread(frame: &mut TrapFrame, current_tid: types::id::ThreadId, next_tid: types::id::ThreadId) {
let kern = unsafe { &mut *crate::kernel_ptr() };
if let Some(ctx) = kern.thread_context_mut(current_tid) {
save_frame_to_context(frame, ctx);
}
let next_pid = kern.thread_process(next_tid);
let current_pid = kern.thread_process(current_tid);
if let Some(ctx) = kern.thread_context(next_tid) {
load_context_to_frame(ctx, frame);
}
apply_pending_result(frame, next_tid);
if next_pid != current_pid {
if let Some(pid) = next_pid {
let pt_addr = kern.platform_mut().page_table_for(pid);
if pt_addr != 0 {
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) };
crate::mmu::activate(pt);
}
}
}
}
fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
let kern = unsafe { &mut *crate::kernel_ptr() };
if let Some(ctx) = kern.thread_context_mut(current_tid) {
save_frame_to_context(frame, ctx);
}
if let Some(next_tid) = kern.pick_next_thread() {
let next_pid = kern.thread_process(next_tid);
let current_pid = kern.thread_process(current_tid);
let core = types::id::CoreId::from_raw(0);
let _ = kern.set_thread_running(next_tid, core);
if let Some(ctx) = kern.thread_context(next_tid) {
load_context_to_frame(ctx, frame);
}
apply_pending_result(frame, next_tid);
if next_pid != current_pid {
if let Some(pid) = next_pid {
let pt_addr = kern.platform_mut().page_table_for(pid);
if pt_addr != 0 {
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) };
crate::mmu::activate(pt);
}
}
}
} else {
loop { unsafe { core::arch::asm!("wfi") }; }
} }
} }
@@ -381,85 +85,126 @@ extern "C" fn trap_handler_rust(frame: &mut TrapFrame) {
let scause: usize; let scause: usize;
unsafe { core::arch::asm!("csrr {0}, scause", out(reg) scause) }; unsafe { core::arch::asm!("csrr {0}, scause", out(reg) scause) };
if frame.sstatus & SSTATUS_SPP != 0 {
// Checked before taking the lock: this hart may already hold it.
kernel_fault(frame, scause);
}
if scause == CAUSE_ECALL_U && fast_syscall(frame) {
return;
}
crate::smp::service_ipi();
crate::smp::lock();
// The thread's FP registers are saved before anything can switch away.
if let Some(ctx) = kern()
.current_thread()
.and_then(|t| kern().thread_context_mut(t))
{
fp::save_if_dirty(frame, ctx);
}
fp::kernel_off();
if scause & CAUSE_INTERRUPT_BIT != 0 { if scause & CAUSE_INTERRUPT_BIT != 0 {
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT); handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
} else { } else {
handle_exception(frame, scause); handle_exception(frame, scause);
} }
finish(frame);
release();
} }
fn handle_interrupt(frame: &mut TrapFrame, cause: usize) { fn handle_interrupt(frame: &mut TrapFrame, cause: usize) {
match cause { match cause {
CAUSE_S_TIMER => { CAUSE_S_TIMER => {
crate::hw::timer::handle_interrupt(); crate::hw::timer::handle_interrupt();
let kern = unsafe { &mut *crate::kernel_ptr() }; let kern = kern();
let core = types::id::CoreId::from_raw(0); kern.fire_timers();
if let Some(current_tid) = kern.current_thread() { if let Some(current_tid) = kern.current_thread() {
let decision = kern.handle_tick(core); let decision = kern.handle_tick(this_core());
if let domain::scheduler::SchedulerDecision::Preempt(next) = decision { if let domain::scheduler::SchedulerDecision::Preempt(next) = decision {
switch_to_thread(frame, current_tid, next); switch_to_thread(frame, Some(current_tid), next);
} }
} }
} }
CAUSE_S_EXTERNAL => { CAUSE_S_EXTERNAL => dispatch_external(),
let irq_num = crate::hw::plic::Plic::claim(); CAUSE_S_SOFTWARE => {}
if irq_num != 0 {
let kern = unsafe { &mut *crate::kernel_ptr() };
let irq = types::id::IrqId::from_raw(irq_num as u16);
kern.handle_interrupt(irq);
crate::hw::plic::Plic::complete(irq_num);
}
}
_ => {} _ => {}
} }
} }
/// The kernel itself faulted; its state is untrusted, so halt.
fn kernel_fault(frame: &TrapFrame, scause: usize) -> ! {
let uart = crate::hw::uart::Uart::new();
uart.write_str("KERNEL FAULT: scause=");
uart.write_hex(scause);
let stval: usize;
unsafe { core::arch::asm!("csrr {0}, stval", out(reg) stval) };
uart.write_str(" stval=");
uart.write_hex(stval);
uart.write_str(" sepc=");
uart.write_hex(frame.sepc);
uart.write_str(" hart=");
uart.write_dec(crate::smp::hart_id());
uart.put_char(b'\n');
crate::halt(&uart, "fault in kernel mode");
}
fn handle_exception(frame: &mut TrapFrame, scause: usize) { fn handle_exception(frame: &mut TrapFrame, scause: usize) {
match scause { match scause {
CAUSE_ECALL_U => { CAUSE_ECALL_U => {
frame.sepc += 4; frame.sepc += 4;
if let Some(syscall) = decode_syscall(frame) { let Some(syscall) = kernel::abi::decode(frame.regs[17], &args(frame)) else {
let kern = unsafe { &mut *crate::kernel_ptr() }; set_results(frame, kernel::abi::unknown_syscall());
return;
};
{
let kern = kern();
let current_tid = kern.current_thread(); let current_tid = kern.current_thread();
let outcome = kern.handle_syscall(syscall); let outcome = kern.handle_syscall(syscall);
write_result(frame, &outcome);
match outcome { match outcome {
kernel::SyscallOutcome::Complete(_) => {} kernel::SyscallOutcome::Complete(result) => write_result(frame, &result),
kernel::SyscallOutcome::BlockCaller(_) => { kernel::SyscallOutcome::BlockCaller(_) => switch_to_next(frame, current_tid),
if let Some(tid) = current_tid {
switch_to_next(frame, tid);
}
}
kernel::SyscallOutcome::SwitchTo(next) => { kernel::SyscallOutcome::SwitchTo(next) => {
if let Some(tid) = current_tid { switch_to_thread(frame, current_tid, next)
switch_to_thread(frame, tid, next);
}
} }
kernel::SyscallOutcome::TerminateCaller => { kernel::SyscallOutcome::TerminateCaller => {
let uart = crate::hw::uart::Uart::new(); let uart = crate::hw::uart::Uart::new();
uart.write_str("k-os: process exited\n"); uart.write_str("k-os: process exited\n");
if let Some(tid) = current_tid { switch_to_next(frame, current_tid);
switch_to_next(frame, tid);
}
} }
} }
} }
} }
CAUSE_ILLEGAL_INSTRUCTION
if kern()
.current_thread()
.and_then(|t| kern().thread_context_mut(t))
.is_some_and(|ctx| fp::first_use(frame, ctx)) => {}
_ => { _ => {
let uart = crate::hw::uart::Uart::new(); let uart = crate::hw::uart::Uart::new();
uart.write_str("FAULT: scause=");
uart.write_hex(scause);
let stval: usize; let stval: usize;
unsafe { core::arch::asm!("csrr {0}, stval", out(reg) stval) }; unsafe { core::arch::asm!("csrr {0}, stval", out(reg) stval) };
uart.write_str(" stval="); let kern = kern();
uart.write_str("FAULT pid=");
let pid = kern.current_thread().and_then(|t| kern.thread_process(t));
uart.write_hex(pid.map_or(usize::MAX, |p| p.raw() as usize));
uart.write_str(": ");
uart.write_str(match scause {
2 => "illegal instruction",
5 | 13 => "load fault",
7 | 15 => "store fault",
12 => "instruction fetch fault",
_ => "fault",
});
uart.write_str(" at ");
uart.write_hex(stval); uart.write_hex(stval);
uart.write_str(" sepc="); if matches!(scause, 5 | 7 | 13 | 15) && types::layout::is_stack_overflow(stval) {
uart.write_str(" (stack overflow: give it more stack)");
}
uart.write_str(", pc ");
uart.write_hex(frame.sepc); uart.write_hex(frame.sepc);
uart.put_char(b'\n'); uart.put_char(b'\n');
let kern = unsafe { &mut *crate::kernel_ptr() };
if let Some(current_tid) = kern.current_thread() { if let Some(current_tid) = kern.current_thread() {
let fault = match scause { let fault = match scause {
2 => domain::fault::Fault::IllegalInstruction, 2 => domain::fault::Fault::IllegalInstruction,
@@ -469,10 +214,9 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
_ => domain::fault::Fault::InvalidSyscall, _ => domain::fault::Fault::InvalidSyscall,
}; };
kern.handle_fault(current_tid, fault, frame.sepc); kern.handle_fault(current_tid, fault, frame.sepc);
switch_to_next(frame, current_tid); switch_to_next(frame, Some(current_tid));
} else {
loop { unsafe { core::arch::asm!("wfi") }; }
} }
// No current thread (destroyed by another hart): `finish` picks one.
} }
} }
} }

View File

@@ -0,0 +1,141 @@
use super::{
CAUSE_S_EXTERNAL, CAUSE_S_TIMER, TrapFrame, dispatch_external, kern, this_core, write_result,
};
fn save_frame_to_context(frame: &TrapFrame, ctx: &mut domain::thread::SavedContext) {
ctx.registers_mut().copy_from_slice(&frame.regs);
ctx.set_pc(frame.sepc);
ctx.set_status(frame.sstatus);
}
fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFrame) {
frame.regs.copy_from_slice(ctx.registers());
frame.sepc = ctx.program_counter();
frame.sstatus = ctx.status();
frame.sstatus &= !(1 << 8); // SPP: return to U-mode
frame.sstatus |= 1 << 5; // SPIE
super::fp::load_for(frame, ctx);
}
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) {
if let Some(result) = kern().take_pending_result(tid) {
write_result(frame, &result);
}
}
unsafe extern "C" {
fn trap_return(frame: *mut TrapFrame) -> !;
}
/// Saves `frame` into `from` (if it still exists) and loads `to`.
pub(super) fn switch_to_thread(
frame: &mut TrapFrame,
from: Option<types::id::ThreadId>,
to: types::id::ThreadId,
) {
let kern = kern();
if let Some(ctx) = from.and_then(|t| kern.thread_context_mut(t)) {
save_frame_to_context(frame, ctx);
}
if let Some(ctx) = kern.thread_context(to) {
load_context_to_frame(ctx, frame);
}
apply_pending_result(frame, to);
}
/// Saves `from` and runs the next ready thread, idling until there is one.
pub(super) fn switch_to_next(frame: &mut TrapFrame, from: Option<types::id::ThreadId>) {
if let Some(ctx) = from.and_then(|t| kern().thread_context_mut(t)) {
save_frame_to_context(frame, ctx);
}
let next = next_runnable();
let _ = kern().set_thread_running(next, this_core());
switch_to_thread(frame, None, next);
}
/// The next thread to run on this hart, idling (lock released, kernel
/// address space active so no shootdown is needed) until one is ready.
fn next_runnable() -> types::id::ThreadId {
loop {
if let Some(tid) = kern().pick_next_thread() {
return tid;
}
crate::mmu::activate(crate::mmu::page_table::table_at(crate::smp::kernel_root()));
release();
unsafe { core::arch::asm!("wfi") };
crate::smp::service_ipi();
crate::smp::lock();
let _ = crate::smp::take_reschedule();
let sip: usize;
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
if sip & (1 << CAUSE_S_TIMER) != 0 {
// No thread is running, so there is no time slice to charge.
crate::hw::timer::handle_interrupt();
kern().fire_timers();
}
if sip & (1 << CAUSE_S_EXTERNAL) != 0 {
dispatch_external();
}
}
}
/// Releases the kernel lock after publishing state the lock-free fast paths read.
pub(super) fn release() {
let kern = kern();
for core in 0..crate::smp::num_harts() {
let id = types::id::CoreId::from_raw(core as u8);
crate::smp::publish_waiting(core, kern.highest_waiting(id).map(|p| p.value()));
}
let running = kern.current_thread();
crate::smp::publish_running(
running
.and_then(|t| kern.thread_process(t))
.map(|p| p.raw()),
running
.and_then(|t| kern.thread_priority(t))
.map(|p| p.value()),
);
crate::smp::unlock();
}
/// Last step before returning to userspace: apply a reschedule request,
/// ensure this hart has a thread, and activate its address space.
pub(super) fn finish(frame: &mut TrapFrame) {
let kern = kern();
let current = kern.current_thread();
if crate::smp::take_reschedule()
&& let Some(cur) = current
&& let domain::scheduler::SchedulerDecision::Preempt(next) = kern.reschedule(this_core())
{
switch_to_thread(frame, Some(cur), next);
}
if kern.current_thread().is_none() {
switch_to_next(frame, None);
}
let root = kern
.current_thread()
.and_then(|t| kern.thread_process(t))
.map(|pid| kern.platform_mut().page_table_for(pid))
.unwrap_or(0);
if root != 0 && crate::mmu::active_root() != root {
crate::mmu::activate(crate::mmu::page_table::table_at(root));
}
}
/// Enters userspace for the first time on this hart (kernel lock held).
pub fn enter_first() -> ! {
let mut frame = TrapFrame {
regs: [0; 32],
sepc: 0,
sstatus: 0,
};
match kern().current_thread() {
Some(tid) => switch_to_thread(&mut frame, None, tid),
None => switch_to_next(&mut frame, None),
}
finish(&mut frame);
release();
// SAFETY: `frame` holds a user context; sscratch is set from it on
// the way out, so later traps land at the top of this frame.
unsafe { trap_return(&mut frame) }
}

View File

@@ -0,0 +1,118 @@
use core::arch::global_asm;
global_asm!(
r#"
.section .text
.align 4
.globl trap_vector
trap_vector:
csrrw sp, sscratch, sp
bnez sp, _from_user
csrrw sp, sscratch, sp
_from_user:
addi sp, sp, -272
sd x1, 8(sp)
sd x3, 24(sp)
sd x4, 32(sp)
sd x5, 40(sp)
sd x6, 48(sp)
sd x7, 56(sp)
sd x8, 64(sp)
sd x9, 72(sp)
sd x10, 80(sp)
sd x11, 88(sp)
sd x12, 96(sp)
sd x13, 104(sp)
sd x14, 112(sp)
sd x15, 120(sp)
sd x16, 128(sp)
sd x17, 136(sp)
sd x18, 144(sp)
sd x19, 152(sp)
sd x20, 160(sp)
sd x21, 168(sp)
sd x22, 176(sp)
sd x23, 184(sp)
sd x24, 192(sp)
sd x25, 200(sp)
sd x26, 208(sp)
sd x27, 216(sp)
sd x28, 224(sp)
sd x29, 232(sp)
sd x30, 240(sp)
sd x31, 248(sp)
csrr t0, sscratch
sd t0, 16(sp)
csrr t0, sepc
sd t0, 256(sp)
csrr t0, sstatus
sd t0, 264(sp)
mv a0, sp
call trap_handler_rust
j _restore_frame
# trap_return(frame): resume `frame` as if returning from the trap that saved it.
.globl trap_return
trap_return:
mv sp, a0
_restore_frame:
ld t0, 256(sp)
csrw sepc, t0
ld t0, 264(sp)
csrw sstatus, t0
andi t1, t0, 0x100
bnez t1, _restore_smode
ld t0, 16(sp)
csrw sscratch, t0
_restore_smode:
ld x1, 8(sp)
ld x3, 24(sp)
ld x4, 32(sp)
ld x6, 48(sp)
ld x7, 56(sp)
ld x8, 64(sp)
ld x9, 72(sp)
ld x10, 80(sp)
ld x11, 88(sp)
ld x12, 96(sp)
ld x13, 104(sp)
ld x14, 112(sp)
ld x15, 120(sp)
ld x16, 128(sp)
ld x17, 136(sp)
ld x18, 144(sp)
ld x19, 152(sp)
ld x20, 160(sp)
ld x21, 168(sp)
ld x22, 176(sp)
ld x23, 184(sp)
ld x24, 192(sp)
ld x25, 200(sp)
ld x26, 208(sp)
ld x27, 216(sp)
ld x28, 224(sp)
ld x29, 232(sp)
ld x30, 240(sp)
ld x31, 248(sp)
# t0 (x5) is restored last on each path; it is the only scratch register.
csrr t0, sstatus
andi t0, t0, 0x100
bnez t0, _sret_smode
ld x5, 40(sp)
addi sp, sp, 272
csrrw sp, sscratch, sp
sret
_sret_smode:
ld x5, 40(sp)
addi sp, sp, 272
sret
"#
);

View File

@@ -0,0 +1,13 @@
[package]
name = "cat"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "cat"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,6 @@
[app]
id = "kos.cat"
name = "cat"
version = "1.0.0"
binary = "cat"
author = "K-OS"

View File

@@ -0,0 +1,48 @@
//! Prints files: `cat <file>...`.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::println;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
fn cat(fs: &mut Storage, path: &str) -> Result<(), FsError> {
let h = fs.open(path.as_bytes())?;
let mut buf = [0u8; 4096];
loop {
let n = fs.read(h, &mut buf)?;
libkos::io::write_bytes(&buf[..n]);
if n < buf.len() {
break;
}
}
fs.close(h)
}
libkos::entry!(main);
fn main() {
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
println!("cat: no storage");
return;
};
let mut any = false;
for path in libkos::start::args().skip(1) {
any = true;
match cat(&mut fs, path) {
Ok(()) => {}
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
Err(FsError::Denied) => println!("cat: {}: outside what it was given", path),
Err(_) => println!("cat: {}: cannot read", path),
}
}
if !any {
println!("usage: cat <file>...");
}
}

View File

@@ -0,0 +1,14 @@
[package]
name = "counter"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "counter"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,10 @@
# Remembers how often it ran, if the owner lets it keep a file.
[app]
id = "kos.counter"
name = "Counter"
version = "1.0.0"
binary = "counter"
author = "K-OS"
[capabilities]
optional = ["storage"]

View File

@@ -0,0 +1,59 @@
//! Counts its runs in its own data folder: the `storage` capability, which
//! it can do without (then it remembers nothing).
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::fs::Storage;
use libkos::println;
use types::address::VirtualAddress;
const DATA_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const COUNT: &[u8] = b"count";
/// The number in the file, 0 if there is none yet.
fn read_count(fs: &mut Storage) -> u64 {
let Ok(h) = fs.open(COUNT) else { return 0 };
let mut buf = [0u8; 20];
let n = fs.read(h, &mut buf).unwrap_or(0);
let _ = fs.close(h);
core::str::from_utf8(&buf[..n]).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0)
}
fn write_count(fs: &mut Storage, count: u64) -> bool {
let mut digits = [0u8; 20];
let mut n = 0;
let mut v = count;
loop {
digits[n] = b'0' + (v % 10) as u8;
n += 1;
v /= 10;
if v == 0 {
break;
}
}
digits[..n].reverse();
let _ = fs.delete(COUNT);
let Ok(h) = fs.create(COUNT) else { return false };
let ok = fs.write(h, &digits[..n]) == Ok(n);
let _ = fs.close(h);
ok
}
fn main() {
let Some(mut fs) = libkos::start::data().and_then(|d| Storage::attach(d, DATA_BUF_VA).ok()) else {
println!("counter: not allowed to keep anything, so this is run 1 as far as it knows");
return;
};
let count = read_count(&mut fs) + 1;
if write_count(&mut fs, count) {
println!("counter: run {} (kept in its own data folder)", count);
} else {
println!("counter: run {}, but it could not save that", count);
}
}
libkos::entry!(main);

View File

@@ -1,4 +1,5 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
@@ -6,12 +7,14 @@ extern crate libkos;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use libkos::syscall; use libkos::syscall;
#[unsafe(no_mangle)] libkos::entry!(main);
extern "C" fn main() {
fn main() {
let ep = CapSlot::const_new(0); let ep = CapSlot::const_new(0);
let (w0, w1, w2, w3) = syscall::receive(ep); if let Ok(syscall::Received::Call { words: [w0, w1, w2, w3], .. }) = syscall::receive_msg(ep) {
syscall::send(ep, w0.wrapping_add(1), w1, w2, w3); let _ = syscall::reply([w0.wrapping_add(1), w1, w2, w3], None);
}
syscall::exit(); syscall::exit();
} }

View File

@@ -0,0 +1,14 @@
[package]
name = "flaky"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "flaky"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }
server-framework = { workspace = true }

View File

@@ -0,0 +1,82 @@
//! A server that dies or hangs when asked: on the test image only, so the
//! self-test can watch init restart it (see init's supervise.rs).
#![no_std]
#![forbid(unsafe_code)]
use libkos::syscall;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::bounded::CapSlot;
/// Requests (word 0); a reply carries how many requests this instance has
/// served, this one included: a restarted server starts at 1.
pub mod op {
pub const WHO: usize = 1;
/// Panics: the process dies.
pub const CRASH: usize = 2;
/// Blocks for good inside the handler.
pub const HANG: usize = 3;
/// Recurses until its stack runs out: the kernel says it overflowed.
pub const OVERFLOW: usize = 4;
}
/// Ever deeper, a KiB of stack a call, until there is none.
#[allow(unconditional_recursion, reason = "running off the stack is what it is for")]
fn deeper(n: usize) -> usize {
let frame = core::hint::black_box([n as u8; 1024]);
// Used after the call, so the call is not a jump.
deeper(n + 1) + frame[n % 1024] as usize
}
/// Everything goes through `handle_call`.
struct Never;
impl Decode for Never {
fn decode(_: [usize; 4]) -> Option<Self> {
None
}
}
impl Encode for Never {
fn encode(&self) -> [usize; 4] {
[MALFORMED, 0, 0, 0]
}
}
struct Flaky {
served: usize,
}
impl Server for Flaky {
type Request = Never;
type Response = Never;
fn handle(&mut self, request: Never) -> Never {
request
}
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<syscall::Give>) {
match words[0] {
op::WHO => {}
op::CRASH => panic!("asked to crash"),
op::OVERFLOW => {
core::hint::black_box(deeper(0));
}
op::HANG => {
// A notification nobody signals.
if let Ok(never) = syscall::create_notification() {
loop {
let _ = syscall::wait(never);
}
}
}
_ => return ([MALFORMED, 0, 0, 0], None),
}
self.served += 1;
([self.served, 0, 0, 0], None)
}
}
pub fn run() -> ! {
server_framework::serve(&mut Flaky { served: 0 }, CapSlot::const_new(0))
}

View File

@@ -0,0 +1,11 @@
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
libkos::entry!(main);
fn main() {
flaky::run();
}

View File

@@ -1,9 +1,11 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
#[unsafe(no_mangle)] libkos::entry!(main);
extern "C" fn main() {
fn main() {
libkos::println!("Hello from k-os!"); libkos::println!("Hello from k-os!");
} }

View File

@@ -0,0 +1,15 @@
[package]
name = "home"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "home"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }
k-ui = { workspace = true }
manifest = { workspace = true }

View File

@@ -0,0 +1,10 @@
[app]
id = "kos.home"
name = "Home"
version = "1.0.0"
binary = "home"
author = "K-OS"
memory = "3 MiB"
[capabilities]
required = ["display", "launch"]

View File

@@ -0,0 +1,118 @@
//! Home: the session's desktop, which the permission manager starts with
//! it. It lists every app with a window and opens one when it is pressed,
//! as many as there is room for, each in its own window. A system app:
//! `launch` is only ever granted to apps on the system image.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate alloc;
extern crate libkos;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use k_ui::{App, Element, Spacing, TextColour, TextStyle, column, list_item, text};
use libkos::apps::Apps;
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
use types::apps::reply;
/// Where the request page for the permission manager is mapped.
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const LISTING: usize = 4096;
const ME: &str = "kos.home";
struct Entry {
id: String,
name: String,
}
struct Home {
apps: Option<Apps>,
entries: Vec<Entry>,
/// What went wrong last, shown until the next press.
trouble: Option<String>,
}
#[derive(Clone, Copy)]
enum Event {
/// Open the `n`th app listed.
Open(usize),
}
impl Home {
/// Every installed app that has a window, but Home.
fn reload(&mut self) {
let Some(apps) = &self.apps else { return };
// On the heap: an app's stack is a few pages.
let mut buf = alloc::vec![0u8; LISTING];
let Ok(n) = apps.apps(&mut buf) else { return };
let text = core::str::from_utf8(&buf[..n]).unwrap_or("");
self.entries = libkos::apps::entries(text)
.filter(|e| e.id != ME)
.filter(|e| Capabilities::from_bits(e.required).union(Capabilities::from_bits(e.optional)).contains(Capability::Display))
.map(|e| Entry { id: e.id.to_string(), name: e.name.to_string() })
.collect();
}
fn open(&mut self, index: usize) {
let (Some(apps), Some(e)) = (&self.apps, self.entries.get(index)) else { return };
self.trouble = match apps.launch(&[e.id.as_str()], None, None) {
// Its window comes to the front; it runs on its own.
Ok((process, _)) => {
libkos::syscall::drop_cap(process);
None
}
Err(r) if r.code == reply::BUSY => Some("There is no room for another app: close one first (F4).".to_string()),
Err(r) => Some(format!("{} did not open (code {}).", e.name, r.code)),
};
self.reload();
}
}
impl App for Home {
type Event = Event;
fn init() -> Self {
let apps = libkos::start::permissions().and_then(|ep| Apps::connect(ep, REQUEST_PAGE_VA));
let mut home = Home { apps, entries: Vec::new(), trouble: None };
home.reload();
home
}
fn update(&mut self, event: Event) {
match event {
Event::Open(i) => self.open(i),
}
}
fn view(&self) -> Element<Event> {
let hint = "Enter opens an app. Alt+Tab or F1: the next window. F2: back here. F4: close the app in front.";
let mut view = column()
.padding(Spacing::Xl)
.gap(Spacing::Md)
.child(text("Home").style(TextStyle::Heading))
.child(text(hint).style(TextStyle::Caption).colour(TextColour::Secondary))
.children(self.entries.iter().enumerate().map(|(i, e)| list_item(e.name.clone(), Event::Open(i)).subtitle(e.id.clone())));
if self.entries.is_empty() {
view = view.child(text("No apps with a window are installed."));
}
if let Some(t) = &self.trouble {
view = view.child(text(t.clone()).colour(TextColour::Accent));
}
view.into()
}
/// Home is not left: Escape stays.
fn back(&mut self) -> bool {
true
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Home>();
}

13
crates/apps/ls/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "ls"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "ls"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,6 @@
[app]
id = "kos.ls"
name = "ls"
version = "1.0.0"
binary = "ls"
author = "K-OS"

View File

@@ -0,0 +1,39 @@
//! Lists a directory: `ls [dir]`, the root by default.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::println;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
libkos::entry!(main);
fn main() {
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
println!("ls: no storage");
return;
};
let dir = libkos::start::args().nth(1).unwrap_or("/");
let listed = fs.list_dir(dir.as_bytes(), |e| {
let name = core::str::from_utf8(e.name()).unwrap_or("?");
if name == "." || name == ".." {
return;
}
if e.is_dir {
println!("{}/", name);
} else {
println!("{:<24} {:>8}", name, e.size);
}
});
match listed {
Ok(_) => {}
Err(FsError::Denied) => println!("ls: {}: outside what it was given", dir),
Err(_) => println!("ls: {}: no such directory", dir),
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "settings"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "settings"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }
k-ui = { workspace = true }
manifest = { workspace = true }

View File

@@ -0,0 +1,10 @@
[app]
id = "kos.settings"
name = "Settings"
version = "1.0.0"
binary = "settings"
author = "K-OS"
memory = "3 MiB"
[capabilities]
required = ["display", "permissions", "session"]

View File

@@ -0,0 +1,285 @@
//! Settings: what each app may use (allow or revoke, through the
//! permission manager), the theme (the display's list, kept per user by
//! init), who is signed in, and signing out. A system app:
//! `permissions` and `session` are only ever granted to apps on the system
//! image.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate alloc;
extern crate libkos;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use k_ui::{App, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, list_item, text, text_input, toggle};
use libkos::apps::Apps;
use libkos::session::Role;
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
/// Where the request page for the permission manager is mapped.
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const LISTING: usize = 4096;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Screen {
Home,
Apps,
/// One app, by its place in the list.
App(usize),
Appearance,
About,
}
#[derive(Clone)]
enum Event {
Show(Screen),
/// Allow or revoke this capability of the app on screen.
Flip(Capability),
/// What the app list is narrowed to.
Filter(String),
/// Use the display's `n`th theme.
Theme(usize),
SignOut,
}
struct AppRow {
id: String,
name: String,
required: Capabilities,
requested: Capabilities,
granted: Capabilities,
}
struct Settings {
screen: Screen,
apps: Option<Apps>,
rows: Vec<AppRow>,
/// Only apps whose name or id has this in it are listed.
filter: String,
/// Who is signed in, and how.
who: Option<(String, Role)>,
/// The display's themes, and which is in use.
themes: Vec<String>,
theme: usize,
/// What went wrong last, shown until the next change.
trouble: Option<String>,
}
/// What a capability lets an app do, as the user reads it.
fn describe(c: Capability) -> &'static str {
match c {
Capability::Display => "A window, and the keyboard while it is in front",
Capability::Storage => "A folder of its own",
Capability::Permissions => "Change what other apps may use",
Capability::Session => "See who is signed in, and sign out",
Capability::Network => "The network",
_ => c.name(),
}
}
impl Settings {
fn reload(&mut self) {
self.rows.clear();
let Some(apps) = &self.apps else { return };
// On the heap: an app's stack is a few pages.
let mut buf = alloc::vec![0u8; LISTING];
let Ok(n) = apps.apps(&mut buf) else {
self.trouble = Some("The permission manager did not answer.".to_string());
return;
};
let text = core::str::from_utf8(&buf[..n]).unwrap_or("");
for e in libkos::apps::entries(text) {
let required = Capabilities::from_bits(e.required);
self.rows.push(AppRow {
id: e.id.to_string(),
name: e.name.to_string(),
required,
requested: required.union(Capabilities::from_bits(e.optional)),
granted: Capabilities::from_bits(e.granted),
});
}
}
fn flip(&mut self, index: usize, cap: Capability) {
let (Some(apps), Some(row)) = (&self.apps, self.rows.get(index)) else { return };
let change = if row.granted.contains(cap) { apps.revoke(&row.id, cap.index()) } else { apps.allow(&row.id, cap.index()) };
self.trouble = change.err().map(|r| format!("That did not change (code {}).", r.code));
self.reload();
}
fn appearance(&self) -> Element<Event> {
let rows = self.themes.iter().enumerate().map(|(i, name)| {
let item = list_item(name.clone(), Event::Theme(i));
if i == self.theme { item.subtitle("In use") } else { item }
});
let view = Self::hint(Self::heading("Appearance"), "Esc goes back").children(rows);
self.with_trouble(view).into()
}
fn choose_theme(&mut self, index: usize) {
let (Some(name), Some(session)) = (self.themes.get(index), libkos::start::session()) else { return };
match libkos::session::set_theme(session, name.as_bytes()) {
true => self.theme = index,
false => self.trouble = Some("That theme could not be kept.".to_string()),
}
}
fn heading(title: &str) -> k_ui::element::FlexBuilder<Event> {
column().padding(Spacing::Xl).gap(Spacing::Md).child(text(title).style(TextStyle::Heading))
}
fn hint(view: k_ui::element::FlexBuilder<Event>, s: &str) -> k_ui::element::FlexBuilder<Event> {
view.child(text(s).style(TextStyle::Caption).colour(TextColour::Secondary))
}
fn home(&self) -> Element<Event> {
Self::heading("Settings")
.child(list_item("Apps", Event::Show(Screen::Apps)).subtitle("What each app may use"))
.child(list_item("Appearance", Event::Show(Screen::Appearance)).subtitle("The theme everything is drawn in"))
.child(list_item("About", Event::Show(Screen::About)).subtitle("Who is signed in, and this device"))
.child(button("Sign out", Event::SignOut).variant(ButtonVariant::Destructive))
.into()
}
fn app_list(&self) -> Element<Event> {
let wanted = self.filter.to_ascii_lowercase();
let shown = |r: &&AppRow| r.name.to_ascii_lowercase().contains(&wanted) || r.id.contains(&wanted);
let rows = self.rows.iter().enumerate().filter(|(_, r)| shown(r)).map(|(i, r)| {
let allowed = r.granted.intersect(r.requested).iter().count();
let asked = r.requested.iter().count();
list_item(r.name.clone(), Event::Show(Screen::App(i))).subtitle(format!("{} · {} of {} allowed", r.id, allowed, asked))
});
let find = text_input("Find an app", self.filter.clone(), Event::Filter).placeholder("Name or id");
let view = Self::hint(Self::heading("Apps"), "Esc goes back, Down goes to the list").child(find).children(rows);
self.with_trouble(view).into()
}
fn app(&self, index: usize) -> Element<Event> {
let Some(r) = self.rows.get(index) else { return self.app_list() };
let mut view = Self::hint(Self::heading(&r.name), &r.id);
if r.requested.is_empty() {
view = Self::hint(view, "It asks for nothing.");
}
for cap in r.requested.iter() {
let label = match r.required.contains(cap) {
true => format!("{} (needed to run)", describe(cap)),
false => describe(cap).to_string(),
};
view = view.child(toggle(label, r.granted.contains(cap), Event::Flip(cap)));
}
if !r.requested.is_empty() {
view = Self::hint(view, "Taking something away from a running app stops it.");
}
self.with_trouble(view).into()
}
fn about(&self) -> Element<Event> {
let (name, role) = match &self.who {
Some((n, Role::Owner)) => (n.as_str(), "an owner of this device"),
Some((n, Role::Standard)) => (n.as_str(), "a user of this device"),
Some((_, Role::Guest)) => ("Guest", "a guest: nothing is kept"),
None => ("?", "unknown"),
};
Self::hint(Self::heading("About"), "Esc goes back")
.child(text(format!("Signed in as {name}")).style(TextStyle::Subheading))
.child(text(format!("You are {role}.")))
.child(text(format!("K-OS {}", env!("CARGO_PKG_VERSION"))))
.child(text("RISC-V, on QEMU's virt board").colour(TextColour::Secondary))
.into()
}
fn with_trouble(&self, view: k_ui::element::FlexBuilder<Event>) -> k_ui::element::FlexBuilder<Event> {
match &self.trouble {
Some(t) => view.child(text(t.clone()).colour(TextColour::Accent)),
None => view,
}
}
}
impl App for Settings {
type Event = Event;
fn init() -> Self {
let apps = libkos::start::permissions().and_then(|ep| Apps::connect(ep, REQUEST_PAGE_VA));
let mut name = [0u8; types::name::BYTES];
let who = libkos::start::session()
.and_then(|s| libkos::session::who(s, &mut name))
.map(|(len, role)| (String::from_utf8_lossy(&name[..len]).into_owned(), role));
let window = libkos::start::display();
let themes = window.map_or(Vec::new(), |w| {
(0..)
.map_while(|i| libkos::display::theme_name(w, i))
.map(|(n, len)| String::from_utf8_lossy(&n[..len]).into_owned())
.collect()
});
let theme = window.and_then(libkos::display::theme).map_or(0, |(i, _)| i);
let mut s = Settings { screen: Screen::Home, apps, rows: Vec::new(), filter: String::new(), who, themes, theme, trouble: None };
s.reload();
s
}
fn update(&mut self, event: Event) {
match event {
Event::Show(screen) => {
self.trouble = None;
if screen == Screen::Apps {
self.filter.clear();
self.reload();
}
self.screen = screen;
}
Event::Flip(cap) => {
if let Screen::App(i) = self.screen {
self.flip(i, cap);
}
}
Event::Filter(f) => self.filter = f,
Event::Theme(i) => self.choose_theme(i),
Event::SignOut => {
if let Some(s) = libkos::start::session() {
libkos::session::sign_out(s);
}
}
}
}
fn view(&self) -> Element<Event> {
match self.screen {
Screen::Home => self.home(),
Screen::Apps => self.app_list(),
Screen::App(i) => self.app(i),
Screen::Appearance => self.appearance(),
Screen::About => self.about(),
}
}
fn back(&mut self) -> bool {
self.trouble = None;
self.screen = match self.screen {
Screen::Home => return false,
Screen::App(_) => Screen::Apps,
Screen::Apps | Screen::Appearance | Screen::About => Screen::Home,
};
true
}
fn screen(&self) -> usize {
match self.screen {
Screen::Home => 0,
Screen::Apps => 1,
Screen::About => 2,
Screen::Appearance => 3,
Screen::App(i) => 4 + i,
}
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Settings>();
}

View File

@@ -0,0 +1,14 @@
[package]
name = "shell"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "shell"
test = false
bench = false
[dependencies]
types = { workspace = true }
manifest = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,337 @@
//! The shell: a few built-ins, and anything else is an app. `name args...`
//! asks the permission manager to run it, passing along the terminal as
//! its output and a read-only grant to the current folder, which is all
//! the shell itself has to give; the app gets the rest of what it may have
//! from the permission manager. Then it waits (Ctrl+C kills it) and takes
//! the grant back. The owner manages apps from here too.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::apps::{Apps, Refused};
use libkos::fs::Storage;
use libkos::{print, println, syscall};
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
use types::apps::reply;
use types::bounded::CapSlot;
/// Where init's manifest put them: `uses`, then storage, then signing out.
const TERMINAL: CapSlot = CapSlot::const_new(0);
const PERMISSION: CapSlot = CapSlot::const_new(1);
/// The user's files, their `home`: the shell's `/`.
const HOME: CapSlot = CapSlot::const_new(2);
const SESSION: CapSlot = CapSlot::const_new(3);
/// The terminal again, the copy that takes Ctrl+C; apps get the plain one.
const CONTROL: CapSlot = CapSlot::const_new(4);
/// Bits on the shell's notification: the program died (its death watch is
/// unbadged), or Ctrl+C was typed (the terminal's copy).
const DIED: usize = 1;
const INTERRUPTED: usize = 1 << 1;
const MAX_ARGS: usize = 16;
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
/// The current directory, as a path on disk: no leading or trailing `/`.
struct Cwd {
path: [u8; types::storage::MAX_PATH],
len: usize,
}
impl Cwd {
fn as_bytes(&self) -> &[u8] {
&self.path[..self.len]
}
/// Where `cd to` from here leads: `/` starts at the root, `..` goes up
/// (the shell holds all of its root, so that is its to resolve).
fn join(&self, to: &str) -> Option<Cwd> {
let mut next = match to.starts_with('/') {
true => Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,
},
false => Cwd {
path: self.path,
len: self.len,
},
};
for part in to.split('/') {
match part {
"" | "." => {}
".." => {
next.len = next
.as_bytes()
.iter()
.rposition(|b| *b == b'/')
.unwrap_or(0)
}
name => {
let sep = usize::from(next.len > 0);
let end = next.len + sep + name.len();
let room = next.path.get_mut(next.len..end)?;
room[..sep].copy_from_slice(&b"/"[..sep]);
room[sep..].copy_from_slice(name.as_bytes());
next.len = end;
}
}
}
Some(next)
}
}
impl core::fmt::Display for Cwd {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(
f,
"/{}",
core::str::from_utf8(self.as_bytes()).unwrap_or("?")
)
}
}
struct Shell {
fs: Storage,
apps: Apps,
cwd: Cwd,
done: CapSlot,
}
/// `display, storage`.
struct Names(Capabilities);
impl core::fmt::Display for Names {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for (i, c) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(c.name())?;
}
Ok(())
}
}
fn names(bits: usize) -> Names {
Names(Capabilities::from_bits(bits as u32))
}
/// Says why the permission manager refused, about `what`.
fn refused(what: &str, r: Refused) {
match r.code {
reply::NOT_INSTALLED => println!("shell: {}: not found", what),
reply::MISSING => println!("shell: {}: cannot run without {}", what, names(r.detail)),
reply::ALREADY_INSTALLED => println!("shell: {}: already installed", what),
reply::UNAVAILABLE => println!("shell: {}: this device has no {}", what, names(r.detail)),
reply::DECLINED => println!("shell: {}: not installed", what),
reply::NO_MANIFEST => println!("shell: {}: no app here (no manifest.toml)", what),
reply::BAD_MANIFEST if r.detail > 0 => println!("shell: {}: bad manifest, line {}", what, r.detail),
reply::BAD_MANIFEST => println!("shell: {}: bad manifest", what),
reply::NOT_REQUESTED => println!("shell: {}: never asked for that", what),
reply::SYSTEM_ONLY => println!("shell: {}: asks for {}, which only system apps may have", what, names(r.detail)),
reply::BUSY => println!("shell: no room to run {}", what),
_ => println!("shell: {}: failed", what),
}
}
/// Runs app `args[0]` with `args` in the current folder, and waits for it
/// to finish or Ctrl+C; with a last argument `&`, does not wait.
fn run_program(sh: &mut Shell, args: &[&str]) {
if let Some((&"&", args)) = args.split_last() {
return start_program(sh, args);
}
let folder = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
match sh.apps.launch(args, Some(TERMINAL), folder) {
Ok((process, _granted)) => {
if syscall::watch_death(process, sh.done).is_ok() {
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
syscall::kill(process);
}
bits & DIED == 0
}) {}
} else {
let _ = syscall::kill(process);
}
syscall::drop_cap(process);
}
Err(r) => refused(args[0], r),
}
if let Some(g) = folder {
let _ = libkos::fs::release(g);
}
}
/// Starts app `args[0]` and lets it run on its own (its window, if it has
/// one, is closed with F4). It does not get the current folder: nobody
/// would be there to take the grant back when it is done.
fn start_program(sh: &mut Shell, args: &[&str]) {
let Some(name) = args.first() else { return println!("usage: <app> [args] &") };
match sh.apps.launch(args, Some(TERMINAL), None) {
Ok((process, _)) => {
// Not waited on: the handle is not needed.
syscall::drop_cap(process);
println!("{} started", name);
}
Err(r) => refused(name, r),
}
}
/// Installs the app in folder `from` (its manifest and binary); the owner
/// is asked on the terminal.
fn install(sh: &mut Shell, from: &str) {
let Some(path) = sh.cwd.join(from) else {
return println!("install: {}: no such folder", from);
};
let Ok(source) = sh.fs.grant(path.as_bytes(), false) else {
return println!("install: {}: no such folder", from);
};
match sh.apps.install(source) {
Ok(0) => println!("installed"),
Ok(bits) => println!("installed, may use: {}", names(bits as usize)),
Err(r) => refused(from, r),
}
let _ = libkos::fs::release(source);
}
fn list(sh: &mut Shell) {
let mut text = [0u8; types::apps::PAGE_SIZE];
match sh.apps.list(&mut text) {
Ok(n) => print!("{}", core::str::from_utf8(&text[..n]).unwrap_or("")),
Err(r) => refused("apps", r),
}
}
/// `allow <app id> <capability>` or `revoke ...`.
fn change(sh: &mut Shell, args: &[&str], allow: bool) {
let verb = if allow { "allow" } else { "revoke" };
let (Some(id), Some(name)) = (args.get(1), args.get(2)) else {
return println!("usage: {} <app id> <capability>", verb);
};
let Some(cap) = Capability::from_name(name) else {
return println!("{}: {}: no such capability", verb, name);
};
let done = match allow {
true => sh.apps.allow(id, cap.index()),
false => sh.apps.revoke(id, cap.index()),
};
if let Err(r) = done {
refused(id, r);
}
}
fn cd(sh: &mut Shell, to: &str) {
match sh.cwd.join(to) {
Some(next) if sh.fs.list_dir(next.as_bytes(), |_| {}).is_ok() => sh.cwd = next,
_ => println!("cd: {}: no such directory", to),
}
}
fn run(sh: &mut Shell, line: &str) {
let mut args = [""; MAX_ARGS];
let mut n = 0;
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
args[n] = w;
n += 1;
}
let args = &args[..n];
match args.first().copied() {
None => {}
Some("help") => {
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd, logout");
println!("apps: apps, install <folder>, allow|revoke <app id> <capability>");
println!("anything else runs an app: try ls, cat readme.txt, snake");
println!("apps see the current directory, read-only, and only what you allowed");
println!("Ctrl+C stops a running program; snake quits with q");
println!("end with & to leave it running (snake &); F4 closes the app in front");
}
Some("apps") => list(sh),
Some("install") => match args.get(1) {
Some(from) => install(sh, from),
None => println!("usage: install <folder>"),
},
Some("allow") => change(sh, args, true),
Some("revoke") => change(sh, args, false),
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL),
// Init stops this shell and everything it ran, and the lock
// screen comes back.
Some("logout") => {
libkos::terminal::clear(TERMINAL);
if syscall::send_msg(SESSION, [types::spawn::SIGN_OUT, 0, 0, 0], None).is_err() {
println!("logout: cannot sign out");
return;
}
// No prompt over the lock screen: wait to be stopped.
loop {
let _ = syscall::wait(sh.done);
}
}
Some(_) => run_program(sh, args),
}
}
/// Words with single spaces between them.
struct Joined<'a>(&'a [&'a str]);
impl core::fmt::Display for Joined<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for (i, w) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
f.write_str(w)?;
}
Ok(())
}
}
libkos::entry!(main);
fn main() {
// Everything this shell prints goes to the terminal.
libkos::start::set_log(Some(TERMINAL));
let Ok(done) = syscall::create_notification() else {
println!("shell: no notification to wait on");
return;
};
let Ok(fs) = Storage::attach(HOME, STORAGE_BUF_VA) else {
println!("shell: no storage");
return;
};
let Some(apps) = Apps::connect(PERMISSION, REQUEST_PAGE_VA) else {
println!("shell: no permission manager");
return;
};
if !libkos::terminal::on_interrupt(CONTROL, done, types::badge::Badge::new(INTERRUPTED)) {
println!("shell: Ctrl+C will not work");
}
let mut sh = Shell {
fs,
apps,
cwd: Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,
},
done,
};
println!("k-os shell. Type 'help'.");
let mut buf = [0u8; 256];
loop {
print!("$ ");
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
match core::str::from_utf8(&buf[..n]) {
Ok(line) => run(&mut sh, line.trim_end_matches('\n')),
Err(_) => println!("shell: not text"),
}
}
}

View File

@@ -0,0 +1,14 @@
[package]
name = "sleep"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "sleep"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,6 @@
[app]
id = "kos.sleep"
name = "sleep"
version = "1.0.0"
binary = "sleep"
author = "K-OS"

View File

@@ -0,0 +1,30 @@
//! Waits: `sleep <seconds>`. Mostly something to stop with Ctrl+C.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::{println, syscall};
use types::time::Ticks;
/// QEMU virt's timebase.
const TICKS_PER_SECOND: u64 = 10_000_000;
libkos::entry!(main);
fn main() {
let Some(seconds) = libkos::start::args().nth(1).and_then(|s| s.parse::<u64>().ok()) else {
println!("usage: sleep <seconds>");
return;
};
let Ok(alarm) = syscall::create_notification() else {
println!("sleep: no notification");
return;
};
let at = syscall::get_time().raw().saturating_add(seconds.saturating_mul(TICKS_PER_SECOND));
if syscall::set_timer(alarm, Ticks::from_raw(at)).is_ok() {
let _ = syscall::wait(alarm);
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "smptest"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "smptest"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,46 @@
//! Spinner half of init's SMP self-test: one thread per hart, each
//! incrementing its own counter in the page given in slot 0.
#![no_std]
#![no_main]
#![forbid(unsafe_code)]
extern crate libkos;
use core::sync::atomic::{AtomicUsize, Ordering};
use libkos::syscall;
use types::address::VirtualAddress;
use types::bounded::CapSlot;
use types::mapping::PagePerms;
/// Counter page layout (words): 0 next free counter index, 1..=THREADS counters.
const COUNTERS_VA: VirtualAddress = VirtualAddress::from_raw(0x0100_0000);
const THREADS: usize = 4;
fn main() {
let page = CapSlot::const_new(0);
let Ok(window) = libkos::window::map(page, COUNTERS_VA, PagePerms::ReadWrite) else {
libkos::println!("smptest: cannot map the counter page");
syscall::exit();
};
let words = window.atomics(types::address::ByteOffset::ZERO, 1 + THREADS);
for _ in 1..THREADS {
if !libkos::thread::spawn(words, spin) {
break;
}
}
spin(words);
}
libkos::entry!(main);
fn spin(word: &'static [AtomicUsize]) -> ! {
let mine = 1 + word[0].fetch_add(1, Ordering::Relaxed) % THREADS;
loop {
// Yield so a spinner sharing init's hart cannot starve it.
for _ in 0..1024 {
word[mine].fetch_add(1, Ordering::Relaxed);
}
syscall::yield_now();
}
}

View File

@@ -12,5 +12,4 @@ bench = false
types = { workspace = true } types = { workspace = true }
libkos = { workspace = true } libkos = { workspace = true }
display-server = { workspace = true } port-framebuffer = { workspace = true }
server-framework = { workspace = true }

View File

@@ -0,0 +1,9 @@
[app]
id = "kos.snake"
name = "Snake"
version = "1.0.0"
binary = "snake"
author = "K-OS"
[capabilities]
required = ["display"]

View File

@@ -44,6 +44,12 @@ pub struct Snake {
rng_state: u32, rng_state: u32,
} }
impl Default for Snake {
fn default() -> Self {
Self::new()
}
}
impl Snake { impl Snake {
pub fn new() -> Self { pub fn new() -> Self {
let start = Position { x: GRID_WIDTH / 2, y: GRID_HEIGHT / 2 }; let start = Position { x: GRID_WIDTH / 2, y: GRID_HEIGHT / 2 };

View File

@@ -1,4 +1,5 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
pub mod game; pub mod game;
pub mod renderer; pub mod renderer;

View File

@@ -1,91 +1,139 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
use libkos::display::Canvas;
use libkos::syscall;
use port_framebuffer::Framebuffer;
use snake::game::{Direction, GameState, Snake}; use snake::game::{Direction, GameState, Snake};
use snake::renderer::{self, RenderCommand}; use snake::renderer::{self, RenderCommand};
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::display::{Color, Dimension, ScreenCoord}; use types::display::{Color, Dimension, ScreenCoord};
use types::time::Ticks; use types::time::Ticks;
use libkos::syscall;
use server_framework::Encode;
use display_server::protocol::DisplayRequest;
const INPUT_EP: CapSlot = CapSlot::const_new(0); /// Steps a second; QEMU virt's timebase is 10 MHz.
const DISPLAY_EP: CapSlot = CapSlot::const_new(1); const STEPS_PER_SECOND: u64 = 8;
const TICK_DELAY: u64 = 5_000_000; const TICK_DELAY: u64 = 10_000_000 / STEPS_PER_SECOND;
const MAX_RENDER_CMDS: usize = 140; const MAX_RENDER_CMDS: usize = 140;
fn display_send(req: &DisplayRequest) { fn draw_rect(canvas: &mut Canvas, x: u16, y: u16, w: u16, h: u16, color: u32) {
let words = req.encode(); canvas.fill_rect(ScreenCoord::new(x), ScreenCoord::new(y), Dimension::new(w), Dimension::new(h), Color::from_raw(color));
syscall::send(DISPLAY_EP, words[0], words[1], words[2], words[3]);
syscall::receive(DISPLAY_EP);
} }
fn draw_rect(x: u16, y: u16, w: u16, h: u16, color: u32) { libkos::entry!(main);
display_send(&DisplayRequest::DrawRect {
x: ScreenCoord::new(x),
y: ScreenCoord::new(y),
w: Dimension::new(w),
h: Dimension::new(h),
color: Color::from_raw(color),
});
}
fn flush() { fn main() {
display_send(&DisplayRequest::Flush); // Its window, which the permission manager opened because snake's
} // manifest asks for the display. Input comes through it too.
let Some(mut canvas) = libkos::start::display().and_then(Canvas::attach) else {
#[unsafe(no_mangle)] libkos::println!("snake: needs the display (see its manifest)");
extern "C" fn main() { return;
};
let Ok(alarm) = syscall::create_notification() else {
libkos::println!("snake: no timer");
return;
};
// Takes focus; keys are polled every tick, the signal only wakes it.
let window = canvas.window();
let _ = libkos::display::subscribe(window, alarm, types::badge::Badge::new(1 << 1));
let mut clock = Clock::new(alarm);
let mut snake = Snake::new(); let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS]; let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
loop { loop {
let count = renderer::render(&snake, &mut commands); let count = renderer::render(&snake, &mut commands);
for i in 0..count { for cmd in &commands[..count] {
let cmd = &commands[i]; draw_rect(&mut canvas, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
draw_rect(cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
} }
flush(); canvas.flush();
if snake.state() == GameState::GameOver { if snake.state() == GameState::GameOver {
let gw = renderer::grid_pixel_width(); let gw = renderer::grid_pixel_width();
let gh = renderer::grid_pixel_height(); let gh = renderer::grid_pixel_height();
draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000); draw_rect(&mut canvas, gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
flush(); canvas.flush();
loop { syscall::yield_now(); } // Any key leaves.
while next_input(window) == Input::None {
clock.wait_tick();
}
return;
} }
wait_tick(); clock.wait_tick();
handle_input(&mut snake); if handle_input(window, &mut snake) == Input::Quit {
return;
}
snake.tick(); snake.tick();
} }
} }
fn handle_input(snake: &mut Snake) { #[derive(PartialEq)]
// Drain all pending events, apply the latest direction enum Input {
None,
Quit,
Turn(Direction),
Other,
}
/// The next key, if one is waiting: q or Escape quits, and so does the
/// window going.
fn next_input(window: CapSlot) -> Input {
use libkos::display::Event;
use libkos::input::{InputEvent, Key};
match libkos::display::poll(window) {
None => Input::None,
Some(Event::Closed) => Input::Quit,
Some(Event::Input(InputEvent::KeyDown(k))) => match k {
Key::Escape | Key::Char(b'q') => Input::Quit,
Key::Up | Key::Char(b'w') => Input::Turn(Direction::Up),
Key::Down | Key::Char(b's') => Input::Turn(Direction::Down),
Key::Left | Key::Char(b'a') => Input::Turn(Direction::Left),
Key::Right | Key::Char(b'd') => Input::Turn(Direction::Right),
_ => Input::Other,
},
Some(_) => Input::Other,
}
}
/// Drains the pending keys, applying each turn; reports a quit.
fn handle_input(window: CapSlot, snake: &mut Snake) -> Input {
loop { loop {
let _ = syscall::send(INPUT_EP, 0, 0, 0, 0); match next_input(window) {
let (tag, kind, _, _) = syscall::receive(INPUT_EP); Input::None => return Input::None,
if tag == 0 { break; } // NoEvent — queue drained Input::Quit => return Input::Quit,
if tag == 1 { Input::Turn(d) => snake.set_direction(d),
match kind { Input::Other => {}
0 => snake.set_direction(Direction::Up),
1 => snake.set_direction(Direction::Down),
2 => snake.set_direction(Direction::Left),
3 => snake.set_direction(Direction::Right),
_ => {}
}
} }
} }
} }
fn wait_tick() { /// The game's clock: a step every `TICK_DELAY`, counted from the last
let start = syscall::get_time(); /// step's deadline, so the time drawing takes does not slow it down.
let deadline = Ticks::from_raw(start.raw() + TICK_DELAY); struct Clock {
while syscall::get_time().raw() < deadline.raw() { alarm: CapSlot,
syscall::yield_now(); next: u64,
}
impl Clock {
fn new(alarm: CapSlot) -> Self {
Self { alarm, next: syscall::get_time().raw() }
}
/// Sleeps until the next step is due, instead of spinning. Keys
/// signal `alarm` too (snake has one notification), so it waits again
/// until the step really is due.
fn wait_tick(&mut self) {
// After a stall, carry on from now rather than rushing to catch up.
self.next = (self.next + TICK_DELAY).max(syscall::get_time().raw());
if syscall::set_timer(self.alarm, Ticks::from_raw(self.next)).is_err() {
return;
}
while syscall::get_time().raw() < self.next {
if syscall::wait(self.alarm).is_err() {
return;
}
}
} }
} }

View File

@@ -4,7 +4,7 @@ const CELL_SIZE: u16 = 24;
const BG_COLOR: u32 = 0xFF202040; const BG_COLOR: u32 = 0xFF202040;
const SNAKE_COLOR: u32 = 0xFF00FF00; const SNAKE_COLOR: u32 = 0xFF00FF00;
const HEAD_COLOR: u32 = 0xFF00CC00; const HEAD_COLOR: u32 = 0xFF00CC00;
const FOOD_COLOR: u32 = 0xFFFF0000; pub(crate) const FOOD_COLOR: u32 = 0xFFFF0000;
const BORDER_COLOR: u32 = 0xFF505080; const BORDER_COLOR: u32 = 0xFF505080;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]

View File

@@ -28,5 +28,5 @@ fn food_is_rendered() {
let count = render(&snake, &mut commands); let count = render(&snake, &mut commands);
// Food is command index 2 (after bg and border) // Food is command index 2 (after bg and border)
assert!(count > 2); assert!(count > 2);
assert_eq!(commands[2].color, 0xFF0040); assert_eq!(commands[2].color, crate::renderer::FOOD_COLOR);
} }

View File

@@ -0,0 +1,15 @@
[package]
name = "spawntest"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "spawntest"
test = false
bench = false
[dependencies]
types = { workspace = true }
manifest = { workspace = true }
flaky = { path = "../flaky" }
libkos = { workspace = true }

View File

@@ -0,0 +1,424 @@
//! The test image's last check, started by init with a spawn allowance:
//! programs run through init, the allowance holds, and it comes back when
//! a child dies. Reports the whole self-test and halts.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::spawn::{SpawnError, Spawned, spawn};
use libkos::syscall;
use types::bounded::CapSlot;
use types::budget::Limits;
/// Where init put them (see init's manifest: `uses`, storage (the disk,
/// the user's home), the spawn endpoint, then control).
const PERMISSION: CapSlot = CapSlot::const_new(0);
const FLAKY: CapSlot = CapSlot::const_new(1);
const STORAGE: CapSlot = CapSlot::const_new(2);
const HOME: CapSlot = CapSlot::const_new(3);
const INIT: CapSlot = CapSlot::const_new(4);
const CONTROL: CapSlot = CapSlot::const_new(5);
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0180_0000);
const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000);
const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000);
const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000);
const CROWD_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01E0_0000);
/// Apps the old tables held beside Home; the test wants more than this.
const OLD_APP_ROOM: usize = 7;
/// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n";
/// Over half the allowance (128 pages), so a second one fits only after
/// the first has died and its limits came back.
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
/// Spawns the program at `path`, read through a grant made just for it.
fn spawn_file(fs: &mut Storage, path: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
let file = fs.grant(path, false).map_err(|_| SpawnError::Failed)?;
let child = spawn(INIT, file, limits);
let _ = libkos::fs::release(file);
child
}
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one.
fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn_file(fs, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on");
}
syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?;
if !child.start() {
return Err("cannot start hello");
}
syscall::wait(died).map_err(|_| "wait failed")?;
Ok(())
}
/// A file written through storage, which the host then checks with e2fsck.
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
// Left by an earlier boot of the same disk.
let _ = fs.delete(b"selftest.txt");
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
if fs.write(h, NOTE) != Ok(NOTE.len()) {
return Err("short write");
}
fs.close(h).map_err(|_| "close failed")?;
let h = fs.open(b"selftest.txt").map_err(|_| "cannot reopen the file")?;
let mut back = [0u8; 64];
let n = fs.read(h, &mut back).map_err(|_| "read failed")?;
fs.close(h).map_err(|_| "close failed")?;
if &back[..n] != NOTE {
return Err("read back something else");
}
Ok(())
}
/// A read-only grant to one directory reaches nothing else and changes
/// nothing, can be passed on, and is gone everywhere once released.
fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
let grant = fs.grant(b"lost+found", false).map_err(|_| "storage refused a grant")?;
let mut inner = Storage::attach(grant, NARROW_BUF_VA).map_err(|_| "cannot attach through the grant")?;
inner.list_dir(b"", |_| {}).map_err(|_| "cannot list its own directory")?;
if inner.open(b"../selftest.txt").err() != Some(FsError::Denied) {
return Err("a grant reached outside its directory");
}
if inner.create(b"x").err() != Some(FsError::Denied) {
return Err("a read-only grant created a file");
}
let widened = inner.grant(b"", true).and_then(|g| Storage::attach(g, types::address::VirtualAddress::from_raw(NARROW_BUF_VA.raw() + 0x10_0000)));
if widened.and_then(|mut w| w.create(b"x")).is_ok() {
return Err("a grant was widened to write");
}
run_hello(fs, died, Some(grant))?;
libkos::fs::release(grant).map_err(|_| "release failed")?;
if inner.list_dir(b"", |_| {}).is_ok() {
return Err("a released grant still works");
}
Ok(())
}
/// An app runs through the permission manager with what it was granted;
/// revoking that stops it, and it cannot run again until allowed.
fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
use manifest::Capability;
let apps = libkos::apps::Apps::connect(PERMISSION, REQUEST_PAGE_VA).ok_or("no permission manager")?;
let display = Capability::Display.index();
let (snake, granted) = apps.launch(&["snake"], None, None).map_err(|_| "snake would not launch")?;
if granted & manifest::Capabilities::NONE.with(Capability::Display).bits() == 0 {
return Err("snake runs without the display it was granted");
}
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
apps.revoke("kos.snake", display).map_err(|_| "revoke failed")?;
syscall::wait(died).map_err(|_| "wait failed")?;
syscall::drop_cap(snake);
let refused = apps.launch(&["snake"], None, None);
if !matches!(refused, Err(r) if r.code == types::apps::reply::MISSING) {
return Err("snake ran without the display it requires");
}
apps.allow("kos.snake", display).map_err(|_| "allow failed")?;
let (snake, _) = apps.launch(&["snake"], None, None).map_err(|_| "snake did not run once allowed again")?;
syscall::watch_death(snake, died).map_err(|_| "cannot watch snake")?;
syscall::kill(snake);
syscall::wait(died).map_err(|_| "wait failed")?;
syscall::drop_cap(snake);
Ok(())
}
/// The user's home is sealed: seeded from the factory, and what is written
/// there one boot reads back the next. Theirs is the only folder that opens.
fn encrypted_home(fs: &mut Storage, home_fs: &mut Storage) -> Result<(), &'static str> {
let mut home = [0u8; 512];
let mut factory = [0u8; 512];
let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result<usize, &'static str> {
let h = fs.open(path).map_err(|_| "cannot open")?;
let n = fs.read(h, out).map_err(|_| "cannot read (a seal broken?)")?;
let _ = fs.close(h);
Ok(n)
};
let n = read(home_fs, b"readme.txt", &mut home)?;
let m = read(fs, b"factory/home/readme.txt", &mut factory)?;
if home[..n] != factory[..m] {
return Err("the encrypted home does not hold the factory's readme");
}
libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok");
const NOTE: &[u8] = b"sealed across a reboot";
let mut back = [0u8; 64];
match read(home_fs, b"persist.txt", &mut back) {
Ok(n) if &back[..n] == NOTE => libkos::println!("spawntest: an encrypted file from the last boot reads back: ok"),
Ok(_) => return Err("an encrypted file from the last boot reads back wrong"),
Err(_) => {
let h = home_fs.create(b"persist.txt").map_err(|_| "cannot create in home")?;
let wrote = home_fs.write(h, NOTE) == Ok(NOTE.len());
let _ = home_fs.close(h);
if !wrote {
return Err("cannot write in home");
}
libkos::println!("spawntest: wrote an encrypted file for the next boot");
}
}
// A made-up user's: nobody may reach it, the whole disk's holder neither.
if fs.create(b"users/0123456789abcdef/home/planted").is_ok() || fs.mkdir(b"users/0123456789abcdef").is_ok() {
return Err("someone else's folder could be written");
}
let mut folders = 0;
fs.list_dir(b"users", |e| folders += usize::from(e.name() != b"." && e.name() != b"..")).map_err(|_| "cannot list the users")?;
if folders != 1 {
return Err("not exactly one user's folder");
}
libkos::println!("spawntest: only the signed-in user's folder opens: ok");
Ok(())
}
/// Floating-point work that only comes out right if nobody else's
/// registers get in: a sum over `ROUNDS`, yielding (so switching threads)
/// all the way.
fn float_work(step: f64) -> u64 {
const ROUNDS: u32 = 2000;
let mut sum = 0.0f64;
let mut x = core::hint::black_box(step);
for _ in 0..ROUNDS {
sum += x * 0.5;
x += step;
syscall::yield_now();
}
sum.to_bits()
}
/// What `float_work(step)` gives when nothing interferes.
fn float_expected(step: f64) -> u64 {
let mut sum = 0.0f64;
let mut x = step;
for _ in 0..2000 {
sum += x * 0.5;
x += step;
}
sum.to_bits()
}
/// The other thread's answer, once it has one.
static FLOAT_RESULT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
fn float_thread(_: &'static core::sync::atomic::AtomicU64) -> ! {
FLOAT_RESULT.store(float_work(0.25), core::sync::atomic::Ordering::Release);
syscall::exit_thread()
}
/// Two threads computing with floats at once each get their own registers
/// (the kernel saves and restores them on every switch).
fn floating_point_is_kept_per_thread() -> Result<(), &'static str> {
if !libkos::thread::spawn(&FLOAT_RESULT, float_thread) {
return Err("cannot start a thread");
}
let mine = float_work(1.75);
while FLOAT_RESULT.load(core::sync::atomic::Ordering::Acquire) == 0 {
syscall::yield_now();
}
let theirs = FLOAT_RESULT.load(core::sync::atomic::Ordering::Acquire);
if mine != float_expected(1.75) || theirs != float_expected(0.25) {
return Err("floating-point registers leaked between threads");
}
Ok(())
}
/// How many requests the `flaky` answering now has served.
fn flaky() -> Result<usize, &'static str> {
syscall::request(FLAKY, [flaky::op::WHO, 0, 0, 0]).map(|r| r[0]).map_err(|_| "flaky does not answer")
}
/// A server that dies is started again on the same endpoint: the caller's
/// capability reaches the new one. Twice in a row, the second waits.
fn restarted_after_crashes() -> Result<(), &'static str> {
if flaky()? != 1 || flaky()? != 2 {
return Err("flaky counts wrong");
}
// Dies mid-call: the call comes back with an error.
let _ = syscall::request(FLAKY, [flaky::op::CRASH, 0, 0, 0]);
if flaky()? != 1 {
return Err("the crashed server still answers");
}
let crashed = syscall::get_time().raw();
let _ = syscall::request(FLAKY, [flaky::op::CRASH, 0, 0, 0]);
let fresh = flaky()? == 1;
// QEMU virt's timebase is 10 MHz.
let waited_ms = (syscall::get_time().raw() - crashed) / 10_000;
if !fresh || waited_ms < 900 {
return Err("a second crash in a row was not backed off");
}
Ok(())
}
/// A server stuck in a handler is noticed by the watchdog, killed (which
/// releases its caller) and restarted.
fn restarted_after_hanging() -> Result<(), &'static str> {
let asked = syscall::get_time().raw();
// Comes back, with an error, once the hung server is killed.
let _ = syscall::request(FLAKY, [flaky::op::HANG, 0, 0, 0]);
let hung_ms = (syscall::get_time().raw() - asked) / 10_000;
if hung_ms < 4_500 {
return Err("the hung server was stopped before the watchdog's deadline");
}
if flaky()? != 1 {
return Err("the hung server was not restarted");
}
Ok(())
}
/// Waits about `ms` on `alarm` (woken early by anything else it carries).
fn nap(alarm: CapSlot, ms: u64) {
let at = syscall::get_time().raw() + ms * types::time::TICKS_PER_MS;
if syscall::set_timer(alarm, types::time::Ticks::from_raw(at)).is_ok() {
let _ = syscall::wait(alarm);
}
}
/// Apps run until the permission manager says there is no room (busy, not
/// a failure), well past what the old tables held; once they are stopped,
/// the room comes back, all of it.
fn many_apps_and_their_room_comes_back(alarm: CapSlot) -> Result<usize, &'static str> {
use types::apps::reply;
let apps = libkos::apps::Apps::connect(PERMISSION, CROWD_PAGE_VA).ok_or("no permission manager")?;
let mut running = [None; 32];
let mut n = 0;
let refusal = loop {
match apps.launch(&["sleep", "300"], None, None) {
Ok((p, _)) if n < running.len() => {
running[n] = Some(p);
n += 1;
}
Ok(_) => return Err("more room than the test can hold"),
Err(r) => break r.code,
}
};
let stop = |running: &mut [Option<CapSlot>]| {
for p in running.iter_mut().filter_map(Option::take) {
let _ = syscall::kill(p);
syscall::drop_cap(p);
}
};
stop(&mut running);
if refusal != reply::BUSY {
return Err("a full permission manager did not say it was busy");
}
if n <= OLD_APP_ROOM {
return Err("no more apps ran than the old tables held");
}
// The deaths are its to hear; give it a moment, then all of it again.
let mut again = 0;
for _ in 0..100 {
match apps.launch(&["sleep", "300"], None, None) {
Ok((p, _)) => {
running[again] = Some(p);
again += 1;
if again == n {
break;
}
}
Err(r) if r.code == reply::BUSY => nap(alarm, 50),
Err(_) => {
stop(&mut running);
return Err("launching again failed");
}
}
}
stop(&mut running);
if again < n {
return Err("the room did not all come back");
}
Ok(n)
}
/// More files open at once than the old tables held (8 in ext4).
fn many_files_open_at_once(fs: &mut Storage) -> Result<(), &'static str> {
let mut open = [None; 30];
let mut result = Ok(());
for slot in open.iter_mut() {
match fs.open(b"selftest.txt") {
Ok(h) => *slot = Some(h),
Err(_) => {
result = Err("cannot hold 30 files open");
break;
}
}
}
for h in open.into_iter().flatten() {
let _ = fs.close(h);
}
result
}
/// A server that runs off its stack faults on the guard below it, and is
/// restarted like any other that died.
fn restarted_after_overflowing() -> Result<(), &'static str> {
let _ = syscall::request(FLAKY, [flaky::op::OVERFLOW, 0, 0, 0]);
if flaky()? != 1 {
return Err("the overflowed server was not restarted");
}
Ok(())
}
fn check() -> Result<(), &'static str> {
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?;
libkos::println!("spawntest: a file written to ext4 reads back: ok");
let mut home = Storage::attach(HOME, HOME_BUF_VA).map_err(|_| "cannot attach to home")?;
encrypted_home(&mut fs, &mut home)?;
let died = syscall::create_notification().map_err(|_| "no notification")?;
narrow_storage(&mut fs, died)?;
libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok");
run_hello(&mut fs, died, None)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
if spawn_file(&mut fs, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
return Err("init spawned past the allowance");
}
let tiny = Limits { pages: 1, ..HELLO };
if spawn_file(&mut fs, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
return Err("a missing program was not reported");
}
libkos::println!("spawntest: allowance and missing files refused: ok");
run_hello(&mut fs, died, None)?;
libkos::println!("spawntest: allowance comes back when a child dies: ok");
revoke_stops_a_running_app(died)?;
libkos::println!("spawntest: revoking stops a running app, allowing lets it run: ok");
floating_point_is_kept_per_thread()?;
libkos::println!("spawntest: floating point is kept per thread: ok");
restarted_after_crashes()?;
libkos::println!("spawntest: a crashed server is restarted, a crash loop backs off: ok");
restarted_after_hanging()?;
libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok");
many_files_open_at_once(&mut fs)?;
libkos::println!("spawntest: 30 files open at once: ok");
let crowd = many_apps_and_their_room_comes_back(died)?;
libkos::println!("spawntest: {} apps at once, then busy, and the room comes back: ok", crowd);
restarted_after_overflowing()?;
libkos::println!("spawntest: a server that overflows its stack is caught and restarted: ok");
Ok(())
}
libkos::entry!(main);
fn main() {
match check() {
Ok(()) => libkos::println!("SMPTEST PASS"),
Err(why) => libkos::println!("SMPTEST FAIL: {}", why),
}
syscall::halt(CONTROL);
}

View File

@@ -0,0 +1,13 @@
[package]
name = "tally"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tally"
test = false
bench = false
[dependencies]
libkos = { workspace = true }
k-ui = { workspace = true }

View File

@@ -0,0 +1,13 @@
[app]
id = "kos.tally"
name = "Tally"
version = "1.0.0"
binary = "tally"
author = "K-OS"
memory = "3 MiB"
[capabilities]
required = ["display"]
[appearance]
accent = "#e91e63"

View File

@@ -0,0 +1,61 @@
//! A count, and buttons to change it: the first K-UI app. Tab and the
//! arrows move between the buttons, Enter presses, Escape leaves.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate alloc;
extern crate libkos;
use alloc::format;
use k_ui::{App, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, row, text};
#[derive(Clone, Copy)]
enum Event {
Add,
Subtract,
Reset,
}
struct Tally(i64);
impl App for Tally {
type Event = Event;
fn init() -> Self {
Tally(0)
}
fn update(&mut self, event: Event) {
match event {
Event::Add => self.0 += 1,
Event::Subtract => self.0 -= 1,
Event::Reset => self.0 = 0,
}
}
fn view(&self) -> Element<Event> {
let mut buttons = row()
.gap(Spacing::Md)
.child(button("Subtract", Event::Subtract))
.child(button("Add", Event::Add).variant(ButtonVariant::Primary));
if self.0 != 0 {
buttons = buttons.child(button("Reset", Event::Reset).variant(ButtonVariant::Destructive));
}
column()
.padding(Spacing::Xl)
.gap(Spacing::Lg)
.child(text("Tally").style(TextStyle::Heading))
.child(text(format!("{}", self.0)).style(TextStyle::Subheading).colour(TextColour::Accent))
.child(buttons)
.child(text("Tab moves, Enter presses, Esc leaves").style(TextStyle::Caption).colour(TextColour::Secondary))
.into()
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Tally>();
}

View File

@@ -15,5 +15,4 @@ libkos = { workspace = true }
server-framework = { workspace = true } server-framework = { workspace = true }
display-server = { workspace = true } display-server = { workspace = true }
driver-framebuffer-virtio = { workspace = true } driver-framebuffer-virtio = { workspace = true }
driver-framebuffer-mock = { workspace = true }
port-framebuffer = { workspace = true } port-framebuffer = { workspace = true }

View File

@@ -1,38 +1,133 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
use types::bounded::CapSlot; use types::bounded::CapSlot;
#[unsafe(no_mangle)] /// Where init put them (see init's manifest: own endpoint, then `uses`).
extern "C" fn main() { const EP: CapSlot = CapSlot::const_new(0);
let ep = CapSlot::const_new(0); const INPUT: CapSlot = CapSlot::const_new(1);
/// `themes/`, read-only.
const THEMES: CapSlot = CapSlot::const_new(2);
/// Where storage's buffer is mapped while the themes are read.
const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x1800_0000);
/// The longest theme file read.
const THEME_BYTES: usize = 4096;
if let Some(fb) = driver_framebuffer_virtio::VirtioFramebuffer::try_new() { libkos::entry!(main);
run_virtio(fb, ep);
} else { fn main() {
run_mock(ep); // Init hands over the GPU, or nothing if the machine has none.
let dev = libkos::device::receive_device(EP);
let input = subscribe_to_input();
if let Some(fb) = dev.mmio.and_then(driver_framebuffer_virtio::VirtioFramebuffer::try_new) {
run_virtio(fb, input);
} }
libkos::println!("display: no GPU, running headless");
run_headless(input);
}
loop { libkos::syscall::yield_now(); } /// Has the input server signal this thread whenever events arrive; the
/// server then passes them on to the app in focus.
fn subscribe_to_input() -> Option<CapSlot> {
use types::badge::Badge;
use types::permission::{Permission, PermissionSet};
let notify = libkos::syscall::create_notification().ok()?;
libkos::syscall::bind_notification(notify).ok()?;
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let copy = libkos::syscall::mint(notify, Badge::NONE, signal).ok()?;
let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
libkos::syscall::drop_cap(copy);
sent.ok().map(|_| INPUT)
}
fn server<F: port_framebuffer::Framebuffer>(fb: F, input: Option<CapSlot>) -> display_server::server::DisplayServer<F, display_server::host::KernelHost> {
display_server::server::DisplayServer::new(fb, display_server::host::KernelHost::new(EP, input), themes())
}
/// Every theme in `themes/` that reads and passes contrast; the built-in
/// one if none does.
fn themes() -> display_server::themes::Themes {
let mut themes = display_server::themes::Themes::new();
let Ok(mut fs) = libkos::fs::Storage::attach(THEMES, STORAGE_BUF_VA) else {
libkos::println!("display: no themes folder; the built-in theme only");
return themes;
};
let mut names = [([0u8; types::storage::DIR_NAME_SIZE], 0usize); display_server::themes::MAX_THEMES];
let mut found = 0;
let _ = fs.list_dir(b"", |e| {
if !e.is_dir && e.name().ends_with(b".toml") && found < names.len() {
names[found] = (e.name, e.name_len);
found += 1;
}
});
let mut text = [0u8; THEME_BYTES];
for (name, len) in &names[..found] {
let name = &name[..*len];
let read = fs.open(name).and_then(|h| {
let n = fs.read(h, &mut text);
let _ = fs.close(h);
n
});
let shown = core::str::from_utf8(name).unwrap_or("?");
match read.ok().and_then(|n| core::str::from_utf8(&text[..n]).ok()) {
Some(t) => {
if let Err(e) = themes.add(t) {
libkos::println!("display: theme {} refused: {:?}", shown, e);
}
}
None => libkos::println!("display: cannot read theme {}", shown),
}
}
fs.detach();
// Storage has few connections to give; this one is done with.
let _ = libkos::fs::release(THEMES);
themes
} }
#[inline(never)] #[inline(never)]
fn run_virtio(mut fb: driver_framebuffer_virtio::VirtioFramebuffer, ep: CapSlot) { fn run_virtio(fb: driver_framebuffer_virtio::VirtioFramebuffer, input: Option<CapSlot>) -> ! {
use port_framebuffer::Framebuffer; let mut srv = server(fb, input);
fb.clear(types::display::Color::rgb(30, 30, 60)); server_framework::serve(&mut srv, EP)
fb.flush(); }
let mut srv = display_server::server::DisplayServer::new(fb);
server_framework::serve(&mut srv, ep); /// No GPU: clients still get answers (and the dimensions they draw for);
/// pixels go nowhere.
struct Headless;
impl port_framebuffer::Framebuffer for Headless {
fn width(&self) -> types::display::Dimension {
types::display::Dimension::new(640)
}
fn height(&self) -> types::display::Dimension {
types::display::Dimension::new(480)
}
fn write_pixel(&mut self, _: types::display::ScreenCoord, _: types::display::ScreenCoord, _: types::display::Color) {}
fn fill_rect(
&mut self,
_: types::display::ScreenCoord,
_: types::display::ScreenCoord,
_: types::display::Dimension,
_: types::display::Dimension,
_: types::display::Color,
) {
}
fn clear(&mut self, _: types::display::Color) {}
fn flush(&mut self) {}
fn scroll_up(
&mut self,
_: types::display::ScreenCoord,
_: types::display::Dimension,
_: types::display::Dimension,
_: types::display::Color,
) {
}
} }
#[inline(never)] #[inline(never)]
fn run_mock(ep: CapSlot) { fn run_headless(input: Option<CapSlot>) -> ! {
let fb = driver_framebuffer_mock::MockFramebuffer::new( let mut srv = server(Headless, input);
types::display::Dimension::new(640), server_framework::serve(&mut srv, EP)
types::display::Dimension::new(480),
);
let mut srv = display_server::server::DisplayServer::new(fb);
server_framework::serve(&mut srv, ep);
} }

View File

@@ -9,6 +9,7 @@ test = false
bench = false bench = false
[dependencies] [dependencies]
port-input-device = { workspace = true }
types = { workspace = true } types = { workspace = true }
libkos = { workspace = true } libkos = { workspace = true }

View File

@@ -1,32 +1,57 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
use driver_input_virtio::VirtioInputDevice;
use input_server::server::{InputServer, MAX_DEVICES, Pair};
use libkos::device::{self, KIND_UART_16550, KIND_VIRTIO};
use port_input_device::InputDevice;
use types::bounded::CapSlot; use types::bounded::CapSlot;
#[unsafe(no_mangle)] libkos::entry!(main);
extern "C" fn main() {
fn main() {
let ep = CapSlot::new(0).unwrap(); let ep = CapSlot::new(0).unwrap();
// Init hands over the input devices, two (a keyboard, a tablet), each
if let Some(dev) = driver_input_virtio::VirtioInputDevice::try_new() { // virtio or, failing a keyboard, the UART; either may be none.
run_input(dev, ep); let first = device::receive_device(ep);
} else { let second = device::receive_device(ep);
run_uart(ep); let irqs = [first.irq, second.irq];
let tablet = match (second.kind, second.mmio) {
(KIND_VIRTIO, Some(mmio)) => VirtioInputDevice::try_new(mmio, 1),
_ => None,
};
match (first.kind, first.mmio) {
(KIND_VIRTIO, Some(mmio)) => {
if let Some(d) = VirtioInputDevice::try_new(mmio, 0) {
run(Pair { first: d, second: tablet }, irqs, ep);
}
}
(KIND_UART_16550, Some(mmio)) => {
if let Some(d) = driver_input_uart::UartInputDevice::new(mmio) {
run(Pair { first: d, second: tablet }, irqs, ep);
}
}
_ => {}
} }
libkos::println!("input: no usable input device, exiting");
loop { libkos::syscall::yield_now(); } libkos::syscall::exit();
} }
/// Serves `device`, woken by its interrupts when it has them.
#[inline(never)] #[inline(never)]
fn run_input(device: driver_input_virtio::VirtioInputDevice, ep: CapSlot) { fn run<D: InputDevice>(device: D, irqs: [Option<CapSlot>; MAX_DEVICES], ep: CapSlot) -> ! {
let mut srv = input_server::server::InputServer::new(device); let mut lines = [CapSlot::const_new(0); MAX_DEVICES];
server_framework::serve(&mut srv, ep); let mut n = 0;
} for irq in irqs.into_iter().flatten() {
lines[n] = irq;
#[inline(never)] n += 1;
fn run_uart(ep: CapSlot) { }
let device = driver_input_uart::UartInputDevice::new(); let mut srv = match n > 0 && device::bind_irqs(&lines[..n]).is_ok() {
let mut srv = input_server::server::InputServer::new(device); true => InputServer::with_irqs(device, irqs),
server_framework::serve(&mut srv, ep); false => InputServer::new(device),
};
server_framework::serve(&mut srv, ep)
} }

View File

@@ -1,9 +1,11 @@
#![no_std] #![no_std]
#![forbid(unsafe_code)]
#![no_main] #![no_main]
extern crate libkos; extern crate libkos;
#[unsafe(no_mangle)] libkos::entry!(main);
extern "C" fn main() {
fn main() {
storage_server::run(); storage_server::run();
} }

View File

@@ -5,3 +5,6 @@ edition = "2024"
[dependencies] [dependencies]
types = { workspace = true } types = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }

View File

@@ -0,0 +1,80 @@
//! What a process may hold: frames and kernel objects. A child's limits
//! are carved from its creator's unused budget and go back when it dies,
//! so the limits of all live processes never add up to more than init got.
use crate::memory::quota::Quota;
pub use types::budget::Limits;
#[cfg(test)]
#[path = "budget_tests.rs"]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
pub pages: Quota,
pub endpoints: Quota,
pub notifications: Quota,
pub regions: Quota,
}
impl Budget {
pub const EMPTY: Self = Self::new(Limits {
pages: 0,
endpoints: 0,
notifications: 0,
regions: 0,
});
pub const fn new(limits: Limits) -> Self {
Self {
pages: Quota::new(limits.pages),
endpoints: Quota::new(limits.endpoints),
notifications: Quota::new(limits.notifications),
regions: Quota::new(limits.regions),
}
}
pub fn limits(&self) -> Limits {
Limits {
pages: self.pages.limit(),
endpoints: self.endpoints.limit(),
notifications: self.notifications.limit(),
regions: self.regions.limit(),
}
}
fn parts(&mut self) -> [&mut Quota; 4] {
[
&mut self.pages,
&mut self.endpoints,
&mut self.notifications,
&mut self.regions,
]
}
/// Carves `child` out of the unused budget: all of it or nothing.
pub fn carve(&mut self, child: Limits) -> bool {
let amounts = amounts(child);
let fits = self.parts().iter().zip(amounts).all(|(q, n)| {
q.limit()
.checked_sub(n)
.is_some_and(|rest| rest >= q.used())
});
if fits {
for (q, n) in self.parts().into_iter().zip(amounts) {
q.carve(n);
}
}
fits
}
pub fn refund(&mut self, child: Limits) {
for (q, n) in self.parts().into_iter().zip(amounts(child)) {
q.refund(n);
}
}
}
fn amounts(l: Limits) -> [usize; 4] {
[l.pages, l.endpoints, l.notifications, l.regions]
}

View File

@@ -0,0 +1,35 @@
use super::{Budget, Limits};
const L: Limits = Limits {
pages: 10,
endpoints: 2,
notifications: 2,
regions: 1,
};
#[test]
fn a_carve_takes_all_parts_or_none() {
let mut b = Budget::new(L);
b.endpoints.charge(2);
let child = Limits {
pages: 1,
endpoints: 1,
..Limits::default()
};
assert!(!b.carve(child));
assert_eq!(b.limits(), L);
assert!(b.carve(Limits {
endpoints: 0,
..child
}));
assert_eq!(b.limits().pages, 9);
}
#[test]
fn a_refund_restores_what_was_carved() {
let mut b = Budget::new(L);
assert!(b.carve(L));
assert_eq!(b.limits(), Limits::default());
b.refund(L);
assert_eq!(b.limits(), L);
}

View File

@@ -0,0 +1,83 @@
//! The capability derivation relation, reduced to its essence: every
//! capability is a node with at most one parent. Kept free of kernel
//! types so the revocation rule can be checked on its own.
/// `result[i]` is true iff `root` is `i` or an ancestor of `i`. `parents` must be acyclic.
pub fn descendants<const N: usize>(parents: &[Option<u16>; N], root: usize) -> [bool; N] {
let mut marked = [false; N];
let Some(r) = marked.get_mut(root) else {
return marked;
};
*r = true;
// Each pass extends the marked set by at least one generation of
// children; a chain in an N-node forest is at most N - 1 long.
for _ in 0..N {
let mut changed = false;
for i in 0..N {
let parent_marked = parents
.get(i)
.copied()
.flatten()
.and_then(|p| marked.get(usize::from(p)).copied())
.unwrap_or(false);
if let Some(m) = marked.get_mut(i)
&& !*m
&& parent_marked
{
*m = true;
changed = true;
}
}
if !changed {
break;
}
}
marked
}
#[cfg(kani)]
mod proofs {
use super::descendants;
const N: usize = 4;
fn is_ancestor(parents: &[Option<u16>; N], root: usize, mut i: usize) -> bool {
for _ in 0..=N {
if i == root {
return true;
}
match parents[i].map(usize::from) {
Some(p) if p < N => i = p,
_ => return false,
}
}
false
}
/// On every acyclic forest, `descendants` marks exactly the subtree under `root`.
#[kani::proof]
#[kani::unwind(6)]
fn marks_exactly_the_subtree() {
let mut parents = [None; N];
for slot in parents.iter_mut() {
if kani::any() {
*slot = Some(kani::any::<u16>() % N as u16);
}
}
// Only forests: following parents never revisits a node.
for start in 0..N {
let mut i = start;
let mut steps = 0;
while let Some(p) = parents[i] {
steps += 1;
kani::assume(steps <= N - 1);
i = p as usize;
}
}
let root = kani::any::<usize>() % N;
let marked = descendants(&parents, root);
for i in 0..N {
assert!(marked[i] == is_ancestor(&parents, root, i));
}
}
}

View File

@@ -0,0 +1,40 @@
use super::derivation::descendants;
#[test]
fn a_root_with_no_children_marks_only_itself() {
let parents = [None, None, None];
assert_eq!(descendants(&parents, 1), [false, true, false]);
}
#[test]
fn marks_children_and_grandchildren() {
// 0 -> 1 -> 2, 0 -> 3, 4 unrelated
let parents = [None, Some(0), Some(1), Some(0), None];
assert_eq!(descendants(&parents, 0), [true, true, true, true, false]);
}
#[test]
fn revoking_a_middle_node_spares_its_ancestors_and_siblings() {
// 0 -> 1 -> 2, 0 -> 3
let parents = [None, Some(0), Some(1), Some(0)];
assert_eq!(descendants(&parents, 1), [false, true, true, false]);
}
#[test]
fn deep_chains_are_followed_to_the_end() {
let parents = [None, Some(0), Some(1), Some(2), Some(3), Some(4)];
assert_eq!(descendants(&parents, 0), [true; 6]);
}
#[test]
fn chains_listed_out_of_order_are_still_found() {
// 3 -> 2 -> 1 -> 0 (children appear before their parents)
let parents = [Some(1), Some(2), Some(3), None];
assert_eq!(descendants(&parents, 3), [true, true, true, true]);
}
#[test]
fn an_out_of_range_root_marks_nothing() {
let parents = [None, Some(0)];
assert_eq!(descendants(&parents, 7), [false, false]);
}

View File

@@ -1,34 +1,83 @@
use types::{bounded::CapSlot, generation::Generation, permission::PermissionSet}; use types::{
badge::Badge, bounded::CapSlot, generation::Generation, id::ProcessId,
permission::PermissionSet,
};
pub mod authority; pub mod authority;
pub mod derivation;
pub mod tree;
use crate::{error::KernelError, resource::Resource}; #[cfg(test)]
#[path = "derivation_tests.rs"]
mod derivation_tests;
#[cfg(test)]
#[path = "tree_tests.rs"]
mod tree_tests;
use crate::{error::KernelError, generations::SlotGenerations, resource::Resource};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
mod tests; mod tests;
const MAX_CAPS_PER_PROCESS: usize = 16; // Sized so that every representable `CapSlot` indexes a real slot.
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
/// One capability slot at one point in time; both generations must match to resolve.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapRef {
pub process: ProcessId,
pub process_generation: Generation,
pub slot: CapSlot,
pub slot_generation: Generation,
}
#[derive(Clone)]
pub struct Capability { pub struct Capability {
resource: Resource, resource: Resource,
permissions: PermissionSet, permissions: PermissionSet,
generation: Generation, generation: Generation,
badge: Badge,
/// The capability this one was derived from (Mint, Delegate, IPC
/// transfer); `None` for capabilities the kernel created directly.
parent: Option<CapRef>,
} }
impl Capability { impl Capability {
pub fn new( pub fn new(resource: Resource, permissions: PermissionSet, generation: Generation) -> Self {
resource: Resource,
permissions: PermissionSet,
generation: Generation,
) -> Self {
Self { Self {
resource, resource,
permissions, permissions,
generation, generation,
badge: Badge::NONE,
parent: None,
} }
} }
pub fn with_parent(mut self, parent: Option<CapRef>) -> Self {
self.parent = parent;
self
}
pub fn parent(&self) -> Option<CapRef> {
self.parent
}
pub fn set_parent(&mut self, parent: Option<CapRef>) {
self.parent = parent;
}
/// The same capability with `badge` stamped on it.
pub fn with_badge(mut self, badge: Badge) -> Self {
self.badge = badge;
self
}
pub fn badge(&self) -> Badge {
self.badge
}
pub fn resource(&self) -> &Resource { pub fn resource(&self) -> &Resource {
&self.resource &self.resource
} }
@@ -44,39 +93,71 @@ impl Capability {
pub struct CapabilityTable { pub struct CapabilityTable {
slots: [Option<Capability>; MAX_CAPS_PER_PROCESS], slots: [Option<Capability>; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations<MAX_CAPS_PER_PROCESS>,
} }
impl CapabilityTable { impl CapabilityTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
slots: [const { None }; MAX_CAPS_PER_PROCESS], slots: [const { None }; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations::new(),
} }
} }
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> { fn slot(&self, slot: CapSlot) -> Option<&Capability> {
let idx = slot.value() as usize; self.slots.get(slot.value() as usize)?.as_ref()
self.slots[idx] }
.as_ref()
/// Generation of an occupied slot.
pub fn slot_generation(&self, slot: CapSlot) -> Option<Generation> {
self.slot(slot)?;
self.generations.current(slot.value() as usize)
}
pub fn get_mut(&mut self, slot: CapSlot) -> Result<&mut Capability, KernelError> {
self.slots
.get_mut(slot.value() as usize)
.and_then(Option::as_mut)
.ok_or(KernelError::InvalidSlot(slot)) .ok_or(KernelError::InvalidSlot(slot))
} }
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> { pub fn iter(&self) -> impl Iterator<Item = (CapSlot, &Capability)> {
let idx = self self.slots.iter().enumerate().filter_map(|(i, c)| {
.slots let cap = c.as_ref()?;
.iter() Some((CapSlot::new(i as u8)?, cap))
.position(|s| s.is_none()) })
.ok_or(KernelError::NoFreeSlot)?; }
self.slots[idx] = Some(cap); pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
CapSlot::new(idx as u8).ok_or(KernelError::NoFreeSlot) self.slot(slot).ok_or(KernelError::InvalidSlot(slot))
}
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> {
let generations = &self.generations;
let (idx, free) = self
.slots
.iter_mut()
.enumerate()
.find(|(i, s)| s.is_none() && generations.is_usable(*i))
.ok_or(KernelError::NoFreeSlot)?;
let slot = CapSlot::new(idx as u8).ok_or(KernelError::NoFreeSlot)?;
*free = Some(cap);
Ok(slot)
} }
pub fn remove(&mut self, slot: CapSlot) -> Result<(), KernelError> { pub fn remove(&mut self, slot: CapSlot) -> Result<(), KernelError> {
let idx = slot.value() as usize; let idx = slot.value() as usize;
if self.slots[idx].is_none() { let taken = self.slots.get_mut(idx).and_then(Option::take);
if taken.is_none() {
return Err(KernelError::InvalidSlot(slot)); return Err(KernelError::InvalidSlot(slot));
} }
self.slots[idx] = None; self.generations.advance(idx);
Ok(()) Ok(())
} }
} }
impl Default for CapabilityTable {
fn default() -> Self {
Self::new()
}
}

View File

@@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() {
#[test] #[test]
fn generation_mismatch_detected() { fn generation_mismatch_detected() {
let gen_v1 = Generation::initial(); let gen_v1 = Generation::initial();
let gen_v2 = gen_v1.next(); let gen_v2 = gen_v1.next().unwrap();
assert!(gen_v1.is_valid_against(&gen_v1)); assert!(gen_v1.is_valid_against(&gen_v1));
assert!(!gen_v1.is_valid_against(&gen_v2)); assert!(!gen_v1.is_valid_against(&gen_v2));
@@ -109,7 +109,6 @@ fn restrict_to_cannot_add_permissions() {
Permission::Write, Permission::Write,
Permission::Execute, Permission::Execute,
Permission::Delegate, Permission::Delegate,
Permission::Revoke,
]); ]);
let result = read_only.restrict_to(&wants_everything); let result = read_only.restrict_to(&wants_everything);
@@ -118,7 +117,6 @@ fn restrict_to_cannot_add_permissions() {
assert!(!result.allows(Permission::Write)); assert!(!result.allows(Permission::Write));
assert!(!result.allows(Permission::Execute)); assert!(!result.allows(Permission::Execute));
assert!(!result.allows(Permission::Delegate)); assert!(!result.allows(Permission::Delegate));
assert!(!result.allows(Permission::Revoke));
} }
#[test] #[test]
@@ -138,7 +136,6 @@ fn empty_permission_set_allows_nothing() {
assert!(!empty.allows(Permission::Write)); assert!(!empty.allows(Permission::Write));
assert!(!empty.allows(Permission::Execute)); assert!(!empty.allows(Permission::Execute));
assert!(!empty.allows(Permission::Delegate)); assert!(!empty.allows(Permission::Delegate));
assert!(!empty.allows(Permission::Revoke));
} }
#[test] #[test]
@@ -150,3 +147,45 @@ fn capability_preserves_resource_and_permissions() {
assert_eq!(cap.permissions(), &expected_permissions); assert_eq!(cap.permissions(), &expected_permissions);
assert_eq!(cap.generation(), &Generation::initial()); assert_eq!(cap.generation(), &Generation::initial());
} }
#[test]
fn removing_a_cap_changes_its_slot_generation() {
let mut table = CapabilityTable::new();
let slot = table.insert(endpoint_cap(1, &[Permission::Read])).unwrap();
let before = table.slot_generation(slot).unwrap();
table.remove(slot).unwrap();
assert_eq!(
table.slot_generation(slot),
None,
"empty slots have no generation"
);
let again = table.insert(endpoint_cap(2, &[Permission::Read])).unwrap();
assert_eq!(again, slot);
assert!(!before.is_valid_against(&table.slot_generation(again).unwrap()));
}
#[test]
fn iter_lists_occupied_slots() {
let mut table = CapabilityTable::new();
let a = table.insert(endpoint_cap(1, &[])).unwrap();
let b = table.insert(endpoint_cap(2, &[])).unwrap();
table.remove(a).unwrap();
let slots: [Option<types::bounded::CapSlot>; 2] = {
let mut it = table.iter().map(|(s, _)| s);
[it.next(), it.next()]
};
assert_eq!(slots, [Some(b), None]);
}
#[test]
fn caps_record_their_parent() {
let parent = CapRef {
process: types::id::ProcessId::from_raw(3),
process_generation: Generation::initial(),
slot: types::bounded::CapSlot::new(4).unwrap(),
slot_generation: Generation::initial(),
};
let cap = endpoint_cap(1, &[]).with_parent(Some(parent));
assert_eq!(cap.parent(), Some(parent));
assert_eq!(endpoint_cap(1, &[]).parent(), None);
}

View File

@@ -0,0 +1,216 @@
//! Derivation tree with explicit links: delete costs its children, revoke its subtree.
//! Nodes are `0..N`; `derivation::descendants` is the reference model it is checked against.
/// Longest walk any operation may take: every node visited once, plus the
/// climb back up. Corrupted links can therefore never hang the kernel.
const fn budget(n: usize) -> usize {
2 * n + 1
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct Node {
live: bool,
parent: Option<u16>,
first_child: Option<u16>,
prev: Option<u16>,
next: Option<u16>,
}
impl Node {
const EMPTY: Self = Self {
live: false,
parent: None,
first_child: None,
prev: None,
next: None,
};
}
pub struct CapTree<const N: usize> {
nodes: [Node; N],
}
impl<const N: usize> Default for CapTree<N> {
fn default() -> Self {
Self::new()
}
}
fn id(i: usize) -> Option<u16> {
u16::try_from(i).ok()
}
impl<const N: usize> CapTree<N> {
pub const fn new() -> Self {
Self {
nodes: [Node::EMPTY; N],
}
}
fn node(&self, i: usize) -> Option<&Node> {
self.nodes.get(i).filter(|n| n.live)
}
fn node_mut(&mut self, i: usize) -> Option<&mut Node> {
self.nodes.get_mut(i).filter(|n| n.live)
}
pub fn is_live(&self, i: usize) -> bool {
self.node(i).is_some()
}
pub fn parent(&self, i: usize) -> Option<usize> {
self.node(i)?.parent.map(usize::from)
}
/// Adds `i` under `parent` (or as a root); fails without change if `i` is live or out of range, or `parent` is not live.
pub fn attach(&mut self, i: usize, parent: Option<usize>) -> bool {
let (Some(me), Some(node)) = (id(i), self.nodes.get(i)) else {
return false;
};
if node.live || parent.is_some_and(|p| p == i || !self.is_live(p)) {
return false;
}
let next = parent
.and_then(|p| self.node(p))
.and_then(|p| p.first_child);
if let Some(n) = next.and_then(|n| self.node_mut(n.into())) {
n.prev = Some(me);
}
if let Some(p) = parent.and_then(|p| self.node_mut(p)) {
p.first_child = Some(me);
}
if let Some(node) = self.nodes.get_mut(i) {
*node = Node {
live: true,
parent: parent.and_then(id),
first_child: None,
prev: None,
next,
};
}
true
}
/// Unlinks `i` from its parent's child list, leaving its own children.
fn unlink(&mut self, i: usize) {
let Some(node) = self.node(i).copied() else {
return;
};
match node.prev {
Some(p) => {
if let Some(p) = self.node_mut(p.into()) {
p.next = node.next;
}
}
None => {
if let Some(p) = node.parent.and_then(|p| self.node_mut(p.into())) {
p.first_child = node.next;
}
}
}
if let Some(n) = node.next.and_then(|n| self.node_mut(n.into())) {
n.prev = node.prev;
}
}
/// Removes `i`; its children are adopted by its parent (or become
/// roots), so no descendant escapes a revocation further up.
pub fn detach(&mut self, i: usize) {
let Some(node) = self.node(i).copied() else {
return;
};
self.unlink(i);
let mut child = node.first_child;
for _ in 0..budget(N) {
let Some(c) = child else { break };
let next = self.node(c.into()).and_then(|n| n.next);
if let Some(n) = self.node_mut(c.into()) {
n.parent = None;
n.prev = None;
n.next = None;
}
// Re-attach under the grandparent; `attach` needs a dead node.
if let Some(n) = self.nodes.get_mut(usize::from(c)) {
n.live = false;
let kids = n.first_child;
if self.attach(c.into(), node.parent.map(usize::from))
&& let Some(n) = self.nodes.get_mut(usize::from(c))
{
n.first_child = kids;
}
}
child = next;
}
if let Some(n) = self.nodes.get_mut(i) {
*n = Node::default();
}
}
/// The direct children of `i`, written into `out`; returns how many.
pub fn children(&self, i: usize, out: &mut [u16; N]) -> usize {
let mut count = 0;
let mut cur = self.node(i).and_then(|n| n.first_child);
for _ in 0..budget(N) {
let Some(c) = cur else { break };
if let Some(slot) = out.get_mut(count) {
*slot = c;
count += 1;
}
cur = self.node(c.into()).and_then(|n| n.next);
}
count
}
/// Every node strictly below `root`, written into `out` in pre-order;
/// returns how many.
pub fn descendants(&self, root: usize, out: &mut [u16; N]) -> usize {
let mut count = 0;
let Some(start) = self.node(root) else {
return 0;
};
let mut cur = start.first_child;
for _ in 0..budget(N) {
let Some(c) = cur else { break };
if let Some(slot) = out.get_mut(count) {
*slot = c;
count += 1;
}
cur = self.next_in_subtree(root, c.into());
}
count
}
/// The node after `cur` in a pre-order walk of `root`'s subtree.
fn next_in_subtree(&self, root: usize, cur: usize) -> Option<u16> {
if let Some(child) = self.node(cur)?.first_child {
return Some(child);
}
let mut at = cur;
for _ in 0..budget(N) {
if at == root {
return None;
}
let node = self.node(at)?;
if let Some(next) = node.next {
return Some(next);
}
at = node.parent?.into();
}
None
}
/// Removes every node strictly below `root`, keeping `root`.
pub fn remove_descendants(&mut self, root: usize) {
let mut doomed = [0u16; N];
let n = self.descendants(root, &mut doomed);
for d in doomed.iter().take(n) {
if let Some(node) = self.nodes.get_mut(usize::from(*d)) {
*node = Node::default();
}
}
if let Some(r) = self.node_mut(root) {
r.first_child = None;
}
}
}

View File

@@ -0,0 +1,120 @@
use super::derivation::descendants;
use super::tree::CapTree;
const N: usize = 12;
/// The tree as a plain parent array, updated by the rules the tree must
/// follow; `descendants` from derivation.rs answers subtree questions.
struct Model {
live: [bool; N],
parents: [Option<u16>; N],
}
impl Model {
fn detach(&mut self, i: usize) {
let grandparent = self.parents[i];
for p in self.parents.iter_mut() {
if *p == Some(i as u16) {
*p = grandparent;
}
}
self.live[i] = false;
self.parents[i] = None;
}
fn below(&self, root: usize) -> [bool; N] {
let mut d = descendants(&self.parents, root);
d[root] = false;
d
}
}
struct Rng(u64);
impl Rng {
fn below(&mut self, n: usize) -> usize {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 % n as u64) as usize
}
}
fn check(t: &CapTree<N>, m: &Model) {
for i in 0..N {
assert_eq!(t.is_live(i), m.live[i], "liveness of {i}");
assert_eq!(t.parent(i), m.parents[i].map(usize::from), "parent of {i}");
if !m.live[i] {
continue;
}
let mut out = [0u16; N];
let n = t.descendants(i, &mut out);
let mut got = [false; N];
for d in &out[..n] {
assert!(!got[*d as usize], "{d} listed twice under {i}");
got[*d as usize] = true;
}
assert_eq!(got, m.below(i), "subtree of {i}");
}
}
#[test]
fn matches_the_parent_array_model_under_random_operations() {
for seed in 1..200u64 {
let mut rng = Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
let mut t = CapTree::<N>::new();
let mut m = Model {
live: [false; N],
parents: [None; N],
};
for _ in 0..300 {
let i = rng.below(N);
match rng.below(4) {
0 | 1 => {
let parent = match rng.below(3) {
0 => None,
_ => Some(rng.below(N)),
};
let ok = !m.live[i] && parent.is_none_or(|p| p != i && m.live[p]);
assert_eq!(t.attach(i, parent), ok, "attach {i} under {parent:?}");
if ok {
m.live[i] = true;
m.parents[i] = parent.map(|p| p as u16);
}
}
2 => {
t.detach(i);
if m.live[i] {
m.detach(i);
}
}
_ => {
t.remove_descendants(i);
if m.live[i] {
let below = m.below(i);
for (j, b) in below.iter().enumerate() {
if *b {
m.live[j] = false;
m.parents[j] = None;
}
}
}
}
}
check(&t, &m);
}
}
}
#[test]
fn detaching_a_middle_node_hands_its_children_up() {
let mut t = CapTree::<4>::new();
assert!(t.attach(0, None));
assert!(t.attach(1, Some(0)));
assert!(t.attach(2, Some(1)));
assert!(t.attach(3, Some(1)));
t.detach(1);
assert_eq!(t.parent(2), Some(0));
assert_eq!(t.parent(3), Some(0));
let mut out = [0u16; 4];
assert_eq!(t.descendants(0, &mut out), 2);
}

View File

@@ -4,7 +4,10 @@ use types::{
id::{EndpointId, ProcessId, ThreadId}, id::{EndpointId, ProcessId, ThreadId},
}; };
use crate::{endpoint::wait_queue::WaitQueue, error::KernelError, message::Message}; use crate::{
endpoint::wait_queue::WaitQueue, error::KernelError, generations::SlotGenerations,
message::Message,
};
pub mod wait_queue; pub mod wait_queue;
@@ -14,18 +17,19 @@ mod tests;
pub struct Endpoint { pub struct Endpoint {
id: EndpointId, id: EndpointId,
/// The process that created it; the endpoint dies with its owner.
owner: ProcessId,
waiting_senders: WaitQueue, waiting_senders: WaitQueue,
waiting_receivers: WaitQueue, waiting_receivers: WaitQueue,
generation: Generation,
} }
impl Endpoint { impl Endpoint {
pub fn new(id: EndpointId) -> Self { pub fn new(id: EndpointId, owner: ProcessId) -> Self {
Self { Self {
id, id,
owner,
waiting_senders: WaitQueue::new(), waiting_senders: WaitQueue::new(),
waiting_receivers: WaitQueue::new(), waiting_receivers: WaitQueue::new(),
generation: Generation::initial(),
} }
} }
@@ -33,8 +37,8 @@ impl Endpoint {
&self.id &self.id
} }
pub fn generation(&self) -> &Generation { pub fn owner(&self) -> ProcessId {
&self.generation self.owner
} }
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> { pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
@@ -53,6 +57,22 @@ impl Endpoint {
self.waiting_receivers.pop() self.waiting_receivers.pop()
} }
pub fn senders(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.waiting_senders.iter()
}
pub fn receivers(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.waiting_receivers.iter()
}
pub fn is_sender(&self, thread: ThreadId) -> bool {
self.waiting_senders.contains(thread)
}
pub fn is_receiver(&self, thread: ThreadId) -> bool {
self.waiting_receivers.contains(thread)
}
pub fn has_waiting_sender(&self) -> bool { pub fn has_waiting_sender(&self) -> bool {
!self.waiting_senders.is_empty() !self.waiting_senders.is_empty()
} }
@@ -68,51 +88,77 @@ impl Endpoint {
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool { pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
self.waiting_receivers.remove_thread(thread) self.waiting_receivers.remove_thread(thread)
} }
pub fn revoke(&mut self) {
self.generation = self.generation.next();
}
} }
const MAX_ENDPOINTS: usize = 256; pub const MAX_ENDPOINTS: usize = if cfg!(kani) { 2 } else { 256 };
pub struct EndpointTable { pub struct EndpointTable {
endpoints: [Option<Endpoint>; MAX_ENDPOINTS], endpoints: [crate::slot::Slot<Endpoint>; MAX_ENDPOINTS],
generations: SlotGenerations<MAX_ENDPOINTS>,
} }
impl EndpointTable { impl EndpointTable {
pub fn new() -> Self { pub const fn new() -> Self {
Self { Self {
endpoints: [const { None }; MAX_ENDPOINTS], endpoints: [const { crate::slot::Slot::Empty }; MAX_ENDPOINTS],
generations: SlotGenerations::new(),
} }
} }
pub fn create(&mut self) -> Option<EndpointId> { pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
let idx = self.endpoints.iter().position(|s| s.is_none())?; let generations = &self.generations;
let (idx, slot) = self
.endpoints
.iter_mut()
.enumerate()
.find(|(i, s)| !s.is_full() && generations.is_usable(*i))?;
let id = EndpointId::from_raw(idx as u16); let id = EndpointId::from_raw(idx as u16);
self.endpoints[idx] = Some(Endpoint::new(id)); slot.set(Endpoint::new(id, owner));
Some(id) Some(id)
} }
/// Generation capabilities to a live endpoint must carry.
pub fn generation(&self, id: EndpointId) -> Option<Generation> {
self.get(id)?;
self.generations.current(id.raw() as usize)
}
pub fn iter(&self) -> impl Iterator<Item = &Endpoint> {
self.endpoints.iter().filter_map(crate::slot::Slot::as_ref)
}
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = EndpointId> + '_ {
self.endpoints
.iter()
.filter_map(crate::slot::Slot::as_ref)
.filter(move |ep| ep.owner == owner)
.map(|ep| ep.id)
}
pub fn get(&self, id: EndpointId) -> Option<&Endpoint> { pub fn get(&self, id: EndpointId) -> Option<&Endpoint> {
self.endpoints.get(id.raw() as usize).and_then(|s| s.as_ref()) self.endpoints
.get(id.raw() as usize)
.and_then(|s| s.as_ref())
} }
pub fn get_mut(&mut self, id: EndpointId) -> Option<&mut Endpoint> { pub fn get_mut(&mut self, id: EndpointId) -> Option<&mut Endpoint> {
self.endpoints.get_mut(id.raw() as usize).and_then(|s| s.as_mut()) self.endpoints
.get_mut(id.raw() as usize)
.and_then(|s| s.as_mut())
} }
/// Removes the endpoint; capabilities to it become invalid even if the
/// slot is later reused.
pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> { pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> {
self.endpoints.get_mut(id.raw() as usize).and_then(|s| s.take()) let ep = self.endpoints.get_mut(id.raw() as usize)?.take()?;
self.generations.advance(id.raw() as usize);
Some(ep)
} }
}
pub fn remove_thread_from_all(&mut self, thread: ThreadId) { impl Default for EndpointTable {
for slot in &mut self.endpoints { fn default() -> Self {
if let Some(ep) = slot { Self::new()
ep.remove_sender(thread);
ep.remove_receiver(thread);
}
}
} }
} }

View File

@@ -1,35 +1,39 @@
use crate::endpoint::wait_queue::MAX_WAITERS; use crate::endpoint::wait_queue::MAX_WAITERS;
use super::*; use super::*;
use types::id::{EndpointId, ThreadId}; use types::id::{EndpointId, ProcessId, ThreadId};
fn thread(id: u16) -> ThreadId { fn thread(id: u16) -> ThreadId {
ThreadId::from_raw(id) ThreadId::from_raw(id)
} }
fn owner() -> ProcessId {
ProcessId::from_raw(0)
}
fn test_endpoint() -> Endpoint { fn test_endpoint() -> Endpoint {
Endpoint::new(EndpointId::from_raw(1)) Endpoint::new(EndpointId::from_raw(1), owner())
} }
// --- WaitQueue tests --- // --- WaitQueue tests ---
#[test] #[test]
fn empty_queue_pop_returns_none() { fn empty_queue_pop_returns_none() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
assert!(queue.pop().is_none()); assert!(queue.pop().is_none());
assert!(queue.is_empty()); assert!(queue.is_empty());
} }
#[test] #[test]
fn push_then_pop_returns_same_thread() { fn push_then_pop_returns_same_thread() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
assert_eq!(queue.pop(), Some(thread(1))); assert_eq!(queue.pop(), Some(thread(1)));
} }
#[test] #[test]
fn pop_order_is_fifo() { fn pop_order_is_fifo() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap(); queue.push(thread(2)).unwrap();
queue.push(thread(3)).unwrap(); queue.push(thread(3)).unwrap();
@@ -42,7 +46,7 @@ fn pop_order_is_fifo() {
#[test] #[test]
fn full_queue_rejects_push() { fn full_queue_rejects_push() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
for i in 0..MAX_WAITERS { for i in 0..MAX_WAITERS {
assert!(queue.push(thread(i as u16)).is_some()); assert!(queue.push(thread(i as u16)).is_some());
} }
@@ -51,7 +55,7 @@ fn full_queue_rejects_push() {
#[test] #[test]
fn is_empty_after_drain() { fn is_empty_after_drain() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap(); queue.push(thread(2)).unwrap();
queue.pop(); queue.pop();
@@ -99,13 +103,34 @@ fn senders_and_receivers_are_independent() {
} }
#[test] #[test]
fn revoke_bumps_generation() { fn reused_slot_gets_a_new_generation() {
let mut ep = test_endpoint(); let mut table = EndpointTable::new();
let gen_before = *ep.generation(); let id = table.create(owner()).unwrap();
ep.revoke(); let old = table.generation(id).unwrap();
let gen_after = *ep.generation(); table.destroy(id);
assert_eq!(
table.generation(id),
None,
"dead endpoints have no generation"
);
let again = table.create(owner()).unwrap();
assert_eq!(again, id);
assert!(!old.is_valid_against(&table.generation(again).unwrap()));
}
assert!(!gen_before.is_valid_against(&gen_after)); #[test]
fn ids_owned_by_lists_only_that_owners_endpoints() {
let mut table = EndpointTable::new();
let a = table.create(owner()).unwrap();
table.create(ProcessId::from_raw(1)).unwrap();
let c = table.create(owner()).unwrap();
let mut ids = [EndpointId::from_raw(u16::MAX); 4];
let mut n = 0;
for id in table.ids_owned_by(owner()) {
ids[n] = id;
n += 1;
}
assert_eq!(&ids[..n], &[a, c]);
} }
#[test] #[test]
@@ -141,22 +166,22 @@ fn rendezvous_receiver_first() {
#[test] #[test]
fn table_create_and_lookup() { fn table_create_and_lookup() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
assert!(table.get(id).is_some()); assert!(table.get(id).is_some());
} }
#[test] #[test]
fn table_create_returns_unique_ids() { fn table_create_returns_unique_ids() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let a = table.create().unwrap(); let a = table.create(owner()).unwrap();
let b = table.create().unwrap(); let b = table.create(owner()).unwrap();
assert_ne!(a, b); assert_ne!(a, b);
} }
#[test] #[test]
fn table_destroy_then_get_fails() { fn table_destroy_then_get_fails() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
let removed = table.destroy(id); let removed = table.destroy(id);
assert!(removed.is_some()); assert!(removed.is_some());
assert!(table.get(id).is_none()); assert!(table.get(id).is_none());
@@ -165,16 +190,57 @@ fn table_destroy_then_get_fails() {
#[test] #[test]
fn table_get_mut_allows_enqueue() { fn table_get_mut_allows_enqueue() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table.get_mut(id).unwrap().enqueue_sender(thread(1)).unwrap(); table
.get_mut(id)
.unwrap()
.enqueue_sender(thread(1))
.unwrap();
assert!(table.get(id).unwrap().has_waiting_sender()); assert!(table.get(id).unwrap().has_waiting_sender());
} }
#[test] #[test]
fn table_destroy_reuses_slot() { fn table_destroy_reuses_slot() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table.destroy(id); table.destroy(id);
let new_id = table.create().unwrap(); let new_id = table.create(owner()).unwrap();
assert_eq!(id, new_id); assert_eq!(id, new_id);
} }
#[test]
fn wait_queue_is_fifo_even_after_a_pop_leaves_a_hole() {
let mut q: WaitQueue = WaitQueue::new();
q.push(thread(1)).unwrap();
q.push(thread(2)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
q.push(thread(3)).unwrap();
assert_eq!(q.pop(), Some(thread(2)), "thread 3 jumped the queue");
assert_eq!(q.pop(), Some(thread(3)));
}
#[test]
fn wait_queue_keeps_order_around_a_removed_thread() {
let mut q: WaitQueue = WaitQueue::new();
for n in 1..=3 {
q.push(thread(n)).unwrap();
}
assert!(q.remove_thread(thread(2)));
q.push(thread(4)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
assert_eq!(q.pop(), Some(thread(3)));
assert_eq!(q.pop(), Some(thread(4)));
assert_eq!(q.pop(), None);
}
#[test]
fn wait_queue_kani_trace_push_push_remove() {
let mut q: WaitQueue<3> = WaitQueue::new();
q.push(thread(3)).unwrap();
q.push(thread(2)).unwrap();
assert!(q.remove_thread(thread(3)));
assert!(!q.remove_thread(thread(0)));
assert!(!q.remove_thread(thread(3)));
assert_eq!(q.pop(), Some(thread(2)));
assert_eq!(q.pop(), None);
}

View File

@@ -1,46 +1,139 @@
use types::id::ThreadId; use types::id::ThreadId;
pub const MAX_WAITERS: usize = 16; pub const MAX_WAITERS: usize = if cfg!(kani) { 2 } else { 16 };
pub struct WaitQueue { /// Bounded FIFO of blocked threads. Order is preserved across pops and
entries: [Option<ThreadId>; MAX_WAITERS], /// removals, so no waiter can be overtaken.
count: u8, pub struct WaitQueue<const N: usize = MAX_WAITERS> {
entries: [Option<ThreadId>; N],
len: usize,
} }
impl WaitQueue { impl<const N: usize> WaitQueue<N> {
pub fn new() -> Self { pub const fn new() -> Self {
Self { Self {
entries: [None; MAX_WAITERS], entries: [None; N],
count: 0, len: 0,
} }
} }
pub fn push(&mut self, thread: ThreadId) -> Option<()> { pub fn push(&mut self, thread: ThreadId) -> Option<()> {
let slot = self.entries.iter_mut().find(|s| s.is_none())?; if self.len == N || self.contains(thread) {
*slot = Some(thread); return None;
self.count += 1; }
*self.entries.get_mut(self.len)? = Some(thread);
self.len += 1;
Some(()) Some(())
} }
pub fn pop(&mut self) -> Option<ThreadId> { pub fn pop(&mut self) -> Option<ThreadId> {
let slot = self.entries.iter_mut().find(|s| s.is_some())?; let head = self.entries.first().copied().flatten()?;
let thread = slot.take(); self.close_gap(0);
self.count -= 1; Some(head)
thread
} }
pub fn remove_thread(&mut self, thread: ThreadId) -> bool { pub fn remove_thread(&mut self, thread: ThreadId) -> bool {
for slot in &mut self.entries { match self.entries[..self.len]
if *slot == Some(thread) { .iter()
*slot = None; .position(|e| *e == Some(thread))
self.count -= 1; {
return true; Some(i) => {
self.close_gap(i);
true
} }
None => false,
} }
false }
pub fn iter(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.entries.iter().take(self.len).flatten().copied()
}
pub fn contains(&self, thread: ThreadId) -> bool {
self.entries
.iter()
.take(self.len)
.any(|e| *e == Some(thread))
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.count == 0 self.len == 0
}
/// Removes entry `i`, shifting later entries forward to keep order.
fn close_gap(&mut self, i: usize) {
for j in i..self.len.saturating_sub(1) {
let next = self.entries.get(j + 1).copied().flatten();
if let Some(e) = self.entries.get_mut(j) {
*e = next;
}
}
self.len = self.len.saturating_sub(1);
if let Some(last) = self.entries.get_mut(self.len) {
*last = None;
}
}
}
impl<const N: usize> Default for WaitQueue<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(kani)]
mod proofs {
use super::*;
/// Matches a plain arrival-ordered array model: FIFO, no duplicates, nothing lost.
#[kani::proof]
#[kani::unwind(7)]
fn behaves_like_a_fifo_without_duplicates() {
const CAP: usize = 3;
let mut q: WaitQueue<CAP> = WaitQueue::new();
let mut model = [0u16; CAP];
let mut len = 0usize;
for _ in 0..5 {
let t: u16 = kani::any::<u16>() % 4;
match kani::any::<u8>() % 3 {
0 => {
let ok = q.push(ThreadId::from_raw(t)).is_some();
let dup = model[..len].contains(&t);
assert!(ok == (!dup && len < CAP));
if ok {
model[len] = t;
len += 1;
}
}
1 => {
let got = q.pop();
if len == 0 {
assert!(got.is_none());
} else {
assert!(got == Some(ThreadId::from_raw(model[0])));
for j in 0..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
}
_ => {
let removed = q.remove_thread(ThreadId::from_raw(t));
match model[..len].iter().position(|&m| m == t) {
Some(i) => {
assert!(removed);
for j in i..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
None => assert!(!removed),
}
}
}
assert!(q.len == len);
assert!(q.is_empty() == (len == 0));
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More