PiROS — User Acceptance Tests
Manual acceptance steps for the programs and the admin list builder. Each case lists Do and Expect. Type at the serial console; under emulation, wait a few seconds for the login: prompt.
Setup
./build.sh # build + boot (or ./build.sh --build then boot manually)
Users: root/rootpw, bob/bobpw, mary/marypw. To start from a clean slate, rm dist/data.img before building (wipes all persistent /data and installed programs).
A. Core tools (as bob)
| # | Do | Expect |
|---|
| A1 | bob bobpw | welcome, bob. |
| A2 | echo hello world > files/greeting then cat files/greeting | hello world |
| A3 | ls files | list includes greeting |
| A4 | grep hello files/greeting | hello world |
| A5 | cp files/greeting files/copy then ls files | copy present |
| A6 | rm files/copy then ls files | copy gone |
| A7 | cat files/nope | cat: not found |
B. Editor (prano)
| # | Do | Expect |
|---|
| B1 | prano files/note | full-screen editor with status bar |
| B2 | type a line, press ^O | status shows [saved] |
| B3 | press ^X, then cat files/note | your text |
C. Scripting (psh)
| # | Do | Expect |
|---|
| C1 | prano files/demo.psh, enter the script below, ^O ^X | saved |
| C2 | psh files/demo.psh | 15 then hello psh |
$a = 5; $b = 10;
echo $a + $b; echo "\n";
$who = "psh";
echo "hello $who\n";
D. Publishing (cross-user)
| # | Do | Expect |
|---|
| D1 | mary marypw, echo lunch at 1 > files/memo, publish files/memo bob | published memo to bob |
| D2 | logout, bob bobpw, argo mary:memo | consumed -> files/memo (argo = inter-user consume-published fetch) |
| D3 | cat files/memo | lunch at 1 |
| D4 | argo --list mary (still bob) | lists memo — what mary has shared with bob |
| D5 | logout, root rootpw, argo mary:memo | argo: not found or access denied (root not in the audience) |
| D6 | argo --list mary (still root) | (nothing shared with you) (root not in the audience) |
E. Admin — managing permission lists (as root)
The admin toolkit manages lists (e.g. which programs a group may use, or which users are in a group). Today the lists are stored per-user data; wiring a saved list into actual kernel grants is the next step, so this validates the management workflow.
list_build keys: ↑/↓ or k/j move · Space toggle · Enter → Save? [Y/N] · Y save · X cancel.
| # | Do | Expect |
|---|
| E1 | root rootpw | welcome, root. |
| E2 | admin list_build editors echo,cat,grep,prano,psh | checkbox list, all [ ] |
| E3 | Space on echo, ↓ to prano, Space, ↓ to psh, Space | echo, prano, psh show [ * ] |
| E4 | Enter, then Y | saved tmp/sel.editors |
| E5 | admin list_save editors | saved files/list.editors |
| E6 | admin list_get editors | echo,prano,psh |
| E7 | admin list_get editors > files/editors.txt then cat files/editors.txt | echo,prano,psh (redirect composes) |
| E8 | admin list_build editors echo,cat,grep,prano,psh echo,prano,psh | reopens pre-checked with echo/prano/psh selected |
| E9 | In E8: deselect psh (Space on it), Enter, Y, then admin list_save editors, admin list_get editors | echo,prano |
| E10 | Build a user group: admin list_build wheel root,bob,mary root,bob, save, admin list_save wheel, admin list_get wheel | root,bob |
| E11 | Cancel path: admin list_build test a,b,c, toggle one, press X | list_build: cancelled (exit 1; nothing saved) |
F. Access control & isolation
| # | Do | Expect |
|---|
| F1 | su bob bobpw, then admin list_get editors | [DENIED] no grant for service 'admin' (admin is root-only) |
| F2 | as bob, ls files | bob's own files only — not root's list.editors (per-user isolation) |
| F3 | iambad (as bob) | [DENIED] no grant for tool 'iambad' |
| F4 | iamgood iambad (as bob) | Yes bob, you are good! (reachable via the service) |
G. Persistence
| # | Do | Expect |
|---|
| G1 | quit, then boot again (same dist/data.img) | — |
| G2 | root rootpw, admin list_get editors | echo,prano (survived reboot) |
| G3 | bob bobpw, ls files | files from earlier sessions still present |
H. Shell & scripting extras
| # | Do | Expect |
|---|
| H1 | bob bobpw, echo one > files/m, echo two >> files/m, cat files/m | one then two (append) |
| H2 | echo <h1>Hi</h1> > files/pg.html then cat files/pg.html | <h1>Hi</h1> (the > inside </h1> is not a redirect) |
| H3 | ls -v | one row per file: SIZE OWNER NAME |
| H4 | type ec then Tab | completes to echo |
| H5 | type cat fn then Tab (after any fn… file exists) | completes the filename |
| H6 | author files/f.psh (with prano): function dbl($n){ return $n*2; } echo dbl(21); then psh files/f.psh | 42 |
| H7 | author files/lib.psh: function sq($x){ return $x*$x; }; author files/use.psh: include "files/lib.psh"; echo sq(6);; then psh files/use.psh | 36 |
I. Directories & the shared /mnt volume
| # | Do | Expect |
|---|
| I1 | bob bobpw, mkdir files/proj, cd files/proj, pwd | files/proj |
| I2 | echo hi > note, ls, cd .. | note listed; back at files |
| I3 | mv files/proj/note files/moved, ls files | moved present |
| I4 | ls mnt, cat mnt/README.md | the shared read-only volume (built from host mnt/) |
| I5 | echo x > mnt/y | [DENIED] user-directed writes may only target files/ (mnt is read-only) |
J. Networking & CLI tools (as bob)
(Outbound needs QEMU's user-net, which ./build.sh sets up.)
| # | Do | Expect |
|---|
| J1 | ping 8.8.8.8 | replies with round-trip times |
| J2 | nslookup example.com | example.com -> <ip> |
| J3 | host example.com | resolves to an IP |
| J4 | http example.com (or curl example.com) | HTTP status + page bytes |
| J5 | echo hi > files/a, zip files/a.zip files/a, ls files | a.zip present (store-only zip) |
| J6 | unzip files/a.zip | extracts a |
| J7 | wget http://example.com/, then ls files | saved -> files/<name> (wget = HTTP downloader, saves into files/; curl prints instead) |
K. Runtime administration (as root)
Root mints users/grants live, signed by the box's machine key. New identities activate on the next boot (the record is written immediately).
| # | Do | Expect |
|---|
| K1 | root rootpw, admin user create dave davepw | created user 'dave'… |
| K2 | admin grant dave core cluster | granted 'core' to 'dave'… |
| K3 | admin user create dave x (again) | admin: user 'dave' already exists |
| K4 | bob bobpw, admin user create evil evilpw | [DENIED] admin: root only |
| K5 | quit, reboot, dave davepw, whoami, ls, echo hi > files/n, cat files/n | dave; his own encrypted /data works |
L. Services — background daemons (as root) ⭐
Services are root-managed background processes. sshd is a privileged kernel-hosted service (it runs each session's shell as the authenticated user); tintin (HTTP), nemo (FTP), queequeg (SQL) are sandboxed ring-3 processes running as their own no-login service user. Only root can start/stop them; the console stays usable while they run.
SSH service (reach it from another Mac terminal; ./build.sh forwards host :2222): | # | Do | Expect | |---|----|--------| | L1 | root rootpw, admin service start sshd | started service 'sshd' (task 1) + sshd: listening on :2222 | | L2 | ps | rows for console and a kernel task (sshd); sockets 1/8 | | L3 | echo still-here (at the console, while sshd runs) | prints — console is not blocked | | L4 | from a Mac terminal: ssh -p 2222 bob@localhost (password bobpw), then whoami, exit | bob; the remote shell runs as bob | | L4a | leave L4's bob session open; from a second Mac terminal ssh -p 2222 mary@localhost (marypw), then whoami | mary — the second session connects immediately (no head-of-line wait); both are live at once | | L4b | run a command in each session at once (bob ls files, mary whoami); at the console ps | both shells respond concurrently; ps shows one worker task per live SSH session (per-connection workers, capped at MAX_TASKS = 8) | | L5 | admin service list | sshd (running) [kernel-hosted] | | L6 | admin service stop sshd, then ps | sshd gone from ps |
HTTP service (tintin) — a sandboxed ring-3 service. First create + provision it: | # | Do | Expect | |---|----|--------| | L7 | admin service create tintin, then quit and reboot | created service 'tintin'… (activates after reboot) | | L8 | root rootpw, mkdir services/tintin/files/default, mkdir services/tintin/files/default/www | root manages the service's own store | | L9 | echo hello-web > files/idx, cp files/idx services/tintin/files/default/www/index.html | seeds the web root in tintin's store | | L10 | admin service start tintin, ps | a process row, user tintin | | L11 | from a Mac terminal (forward host :80 → guest :80, or run tintin on a forwarded port): curl http://localhost:8080/ | hello-web with Server: tintin | | L12 | su bob bobpw, cat services/tintin/files/default/www/index.html | denied (services/ is root-only) |
(nemo/queequeg follow the same pattern: admin service create <name> → reboot → admin service start <name>. FTP passive data + SQL need their ports forwarded; the control/query channels work over one forward.)
L. Real-hardware regression: ring-3 services bind + console doesn't drop SSH ⭐ RELEASE-ONLY
Run these on real hardware / the cloud VM (KVM), NOT under QEMU-TCG. This class of bug is invisible under TCG: it was a preemption-timer livelock that only triggers when the LAPIC uses TSC-deadline mode (real HW). Fixed in fix/tsc-deadline-preempt-livelock (LAPIC initial-count). These steps confirm the fix held on the actual silicon.
| # | Do | Expect (PASS) | Regression (the bug is back) |
|---|
| L13 | Boot; at the console after login: `syslog \ | grep apic` (or watch boot) | apic: preemption timer = LAPIC one-shot count | … = LAPIC TSC-deadline → the fix is not in this image |
| L14 | admin service start tintin, then watch the console for ~3 s | tintin prints starting → config loaded → web root ready → logs ready, binding port → listening | only started service 'tintin' … (task N) and then silence (no tintin: starting) |
| L15 | ps right after | a process row for tintin, and timer ticks are small/normal | tintin row stuck ready; timer ticks climbing into the thousands with no progress = livelock |
| L16 | netstat (or ports) | rows for both :2222 LISTEN (charon) and :80 LISTEN (tintin) | only :2222; no :80 |
| L17 | From your workstation (VM firewall port 80 open): curl http://<vm-ip>/ | HTTP 200 + the tintin site HTML | ERR_CONNECTION_TIMED_OUT / connection refused |
| L18 | Open an SSH session to :2222 and leave it live; at the console run a few commands / a longer tool; then in the SSH session run echo still-alive | SSH session stays up and responds; console stays usable | SSH session closes / hangs while the console is in use (the secondary symptom) |
| L19 | Over SSH, run netstat (and ports, syslog) | prints the socket/log table and returns; box stays responsive (echo ok after) | box freezes on the command (the diagnostic lock-across-send deadlock) |
If L18 regresses even when L14–L17 pass, it is the separate NIC-polling gap (the NIC is polled only from inside accept/recv/send — no idle poller), not the timer bug; capture it and hand back to Dell.
M. Diagnostics — watching resources & crashes (as root)
| # | Do | Expect |
|---|
| M1 | ps | tasks + resources: N RAM frames free, sockets X/8 |
| M2 | free | memory summary |
| M3 | syslog | event log: spawns, service starts, RES snapshots (oldest first) |
| M4 | start/stop a couple of services, then syslog | watch ram=… frames free change — evidence of resource use over time |
| M5 | if a service dies, syslog | a FAULT: line + an at-fault resource snapshot pinpoints what happened |
Notes
list_build items can't contain commas (CSV is the interchange format).
- Interactive tools redraw the screen with ANSI escapes — best viewed in a real terminal.
- Services activate after a reboot from
admin service create (like users). syslog is root-only.
- Known PoC limits: exited/stopped processes leak their frames + a socket (no reclaim yet); when fully idle the scheduler busy-yields (no
hlt). Watch these in syslog/ps.
- To reset everything:
rm dist/data.img and rebuild.