sigilOS closes its identity seam with the landing of identity.sg (sigil-os#13). A 16-slot user table, pid-gated session tokens, a sudo-equivalent elevation gate backed by a broker-held token that EL0 cannot forge, and full POSIX rwx access checks are all now in place. Multi-user sigilOS is real.
Memory layout
Two tables sit in a fixed region of the OS address space, above the FS tag state at 0xA20000 and below the IPC ring at 0xA60000:
Root (uid=0) and guest (uid=1) are pre-installed by id_user_init() at boot. The session table starts empty; slots are allocated as processes start.
Sessions and the pid gate
id_sess_alloc(uid, pid) finds a free slot in the session table and writes the uid, gid (from the user record), pid, and a freshly-allocated capability token into it. From that point on, every session operation requires the caller to present the same pid via id_sess_verify(slot, pid). A mismatch returns -1 immediately — no state is read, no capability is checked.
This pid-gate is the same structural pattern as capability gates everywhere else in sigilOS: the check is the first instruction in the dispatch path, not an afterthought added at the end. A process that doesn't hold the right pid cannot read another session's uid, gid, or cap_token — even if it somehow learns the slot number.
The capability token stored in each session slot is the handle the broker passes to the FS tag-query layer (syscalls 91–102) to scope file access. When the kernel injects the session slot on tag-query calls, it reads the cap_token here and presents it to the FS — EL0 never handles the raw token.
Elevation: a capability-backed sudo
id_elevate(slot, pid, elev_token) is the privilege escalation gate. To elevate a session to uid=0 (root), the caller must present:
- The correct pid for the session slot (pid gate, same as all other ops)
- The correct elevation token:
ID_ELEV_TOKEN = 0x494C4F55
The elevation token is held by the OS broker, not EL0. An EL0 process cannot read the broker's memory — the capability boundary prevents it. So the only way for a process to elevate is to ask the broker, which can apply its own policy (PIN prompt, biometric, time-of-day gating) before presenting the token on the caller's behalf.
This is materially different from a traditional setuid binary. With setuid, any process that can execute the binary gets root. With id_elevate, elevation requires the broker to actively vouch for the caller — the token is the proof. If the broker never hands it over, elevation never happens, regardless of what EL0 tries.
id_elevate(slot, pid, 0xDEADBEEF) → -1 # wrong token
id_elevate(slot, 9999, ID_ELEV_TOKEN) → -1 # wrong pid
id_elevate(slot, pid, ID_ELEV_TOKEN) → 0 # elevation granted, sess_uid=0
POSIX rwx access checks
id_access(mode, fuid, fgid, uid, gid) implements the standard UNIX permission check. The mode is a 9-bit octal value (owner/group/other × read/write/exec). The function decodes it without bitwise shifts — all arithmetic — to stay within the Sigil integer model:
owner = (mode / 64) % 8
group = (mode / 8) % 8
other = mode % 8
The decision tree:
- uid == 0 → return 1 (root bypasses all permission checks)
- uid == fuid → check owner bits
- gid == fgid → check group bits
- else → check other bits
The FS mode/owner/group metadata that feeds this check was already landed in sigil-fs#2 (every MFT record carries uid, gid, mode). The identity seam completes the circuit: the FS has the metadata, the OS now has the check, and the kernel injects the session's uid/gid on every syscall so EL0 cannot supply a forged identity.
Test coverage
| Assertion | What it checks | Result |
|---|---|---|
| init | root uid=0, guest uid=1 pre-installed | PASS |
| alloc/verify | id_sess_alloc → slot; id_sess_verify correct pid | PASS |
| uid/gid | session carries correct uid/gid from user record | PASS |
| dual-session | two sessions don't share slots; each verifies independently | PASS |
| wrong token | id_elevate with bad token → -1, uid unchanged | PASS |
| elevation | id_elevate with ID_ELEV_TOKEN → sess_uid=0 | PASS |
| access owner | owner rwx bits checked correctly | PASS |
| access other | non-owner/non-group falls through to other bits | PASS |
| root bypass | uid=0 → access=1 regardless of mode | PASS |
| close lifecycle | id_sess_close frees slot; verify after close → -1 | PASS |
| invalid uid | id_sess_alloc with uid≥16 → -1 | PASS |
17 assertions total. QEMU raspi3b PASS.