Audio is a privacy surface. On most operating systems, any process that can load an audio library can record from the microphone — the only gate is a one-time permission dialog that persists forever. sigilOS applies the same capability model to audio that it applies to files, network sockets, and framebuffers: a slot handle, a pid binding, and for microphone access, an explicit consent token that an EL0 process cannot forge.
The session model
audiosess.sg manages 8 concurrent audio sessions. Each session is a 64-byte header in a fixed table at 0xA30000, followed by a per-session ring buffer of 4096 PCM frames (16 KB at 16-bit stereo 44100 Hz = 4 bytes/frame):
| Address | Contents |
|---|---|
0xA30000 | Session slot table (8 × 64B headers): owner pid, state, volume L/R, ring head/tail |
0xA30200 | Per-slot PCM rings (8 × 16KB = 128KB) |
0xA50200 | Output ring (2048 frames = 8KB) |
0xA52200 | Accumulator scratch (int32, 2048 frames × 2 channels) |
A process calls au_alloc(pid) to claim a slot. It gets back a slot index (0–7) — an opaque handle, not a pointer. The slot records the caller's pid and sets default volume to 256 (unity gain on a 0–256 scale). From that point on, every operation on the slot is verified against the stored pid: a different pid attempting to write into an active slot returns -1 immediately.
Writing and mixing
au_write(slot, pid, buf, frames) fills the session's ring with PCM frames. The ring is a simple power-of-two circular buffer: head advances on write, tail advances on read, wrap is a modulo. If the ring is full, the write is truncated to available space — no blocking, no allocation.
au_mix_tick() is the mixer heartbeat, called by the OS timer or the audio HAL at the hardware period boundary. It:
- Drains up to 512 frames from every ACTIVE session into the int32 accumulator
- Applies per-session volume: left channel ×
vol_l / 256, right ×vol_r / 256(integer multiply, then shift — no floating point) - Clamps the int32 sum to [−32768, 32767] and writes 16-bit samples to the output ring
- Calls
au_hal_write()— currently a loopback no-op on QEMU; the real I2S/PCM HAL driver replaces this at Kernel integration
The clamp is the safety property: no matter how many sessions are active or what volume they request, the output never wraps around. A mix of 8 sessions at unity gain that all produce full-scale sine waves clamps cleanly rather than aliasing.
The microphone consent gate
Microphone access is the audio surface that matters most from a privacy perspective. au_mic_grant(slot, pid, token) enables microphone input for a session. The token must match AU_MIC_TOKEN() — a sentinel value held by the OS broker. An EL0 process cannot call this directly with the right token because it doesn't know the token value; the token is never exposed through any EL0 API.
The consent flow in sigilOS is:
- EL0 app requests microphone access via a capability syscall
- The OS broker presents a consent dialog (the WM owns the UI; EL0 cannot suppress or spoof it)
- If the user approves, the OS calls
au_mic_grantwith the correct token on behalf of the app - The app's session gains mic access; subsequent ticks drain input from the capture ring
Two independent checks gate the grant: the pid must match the slot owner, and the token must match the sentinel. Both failing returns -1. Neither check alone is sufficient — the pid check alone could be satisfied by any process that knows the slot number, so the token adds a second factor that only the OS broker holds.
Test coverage
The test file audiosesstest.sg runs 8 assertions:
| # | Assertion |
|---|---|
| 1 | au_alloc returns a valid slot; owner pid and default vol=256 stored |
| 2 | Two allocs return distinct slot IDs |
| 3 | au_write fills ring; au_read-back confirms frame content |
| 4 | au_write with wrong pid returns -1 (auth rejection) |
| 5 | au_mic_grant with wrong token returns -1 |
| 6 | au_mic_grant with correct token + correct pid returns 0 |
| 7 | au_mic_grant with correct token + wrong pid returns -1 |
| 8 | au_mix_tick drains both sessions; out_l=200, out_r=400 in accumulator after scaling; close guard — subsequent write returns -1 |
QEMU raspi3b: PASS (74,200,160).
How it fits the model
The pattern is identical to Cap<DBTxn>, Cap<NetSession>, and the VFS session token: EL0 holds a slot index, the broker holds the state. The slot index is not a capability in the strict sense — it's an opaque integer. The capability enforcement happens in the broker (pid check on every operation) and at the consent gate (token check for mic). An EL0 process that somehow learns another process's slot number still can't write to it, because the pid stored in the slot won't match. An EL0 process that captures the slot number and the pid still can't get mic access, because it doesn't have the mic token.
The HAL loopback means the full audio path — allocate, write, mix, output — is testable on QEMU without any real hardware. When the I2S driver lands, au_hal_write becomes a real write to the PCM peripheral. Nothing else changes.