PlayOS Security Model
Cross-references: architecture.md §13–15, runtime-ipc.md §3, sprints/Sprint-12.md
Table of Contents
- Trust Zones
- Component Privilege Levels
- Game Restrictions
- IPC Access Control
- Filesystem Access Control
- seccomp Filter Policy
- Landlock Filesystem Restrictions
- Input Security
- System Image Integrity
- Secure Boot Chain
- Development vs Production
- Post-MVP Hardening Roadmap
1. Trust Zones
┌───────────────────────────────────────────────────────────────┐
│ Zone 1: Kernel │
│ Linux kernel + drivers │
│ Full hardware access │
└───────────────────┬───────────────────────────────────────────┘
│
┌───────────────────▼───────────────────────────────────────────┐
│ Zone 2: Trusted System Components │
│ playos-init (root) │
│ playos-compositor (display + input caps) │
│ playos-shell (playos-trusted group) │
│ playos-overlay (playos-trusted group) │
│ │
│ ← communicate via /run/playos/ UNIX sockets → │
└───────────────────┬───────────────────────────────────────────┘
│ public libplayos C ABI only
┌───────────────────▼───────────────────────────────────────────┐
│ Zone 3: Untrusted Game Process │
│ User: playos-game (unprivileged) │
│ No access to Zone 2 sockets │
│ No DRM primary nodes │
│ Raw evdev via libplayos (reserved buttons stripped) │
│ Restricted to own save/cache directories │
│ seccomp + Landlock enforced │
└───────────────────────────────────────────────────────────────┘
2. Component Privilege Levels
| Component | User | Capabilities | Notes |
|---|---|---|---|
playos-init | root | All (required for process supervision, mounts, device setup) | Drop unnecessary caps after init (future) |
playos-compositor | root | All | Sprint 12 scopes privilege reduction to games; dropping system-component privileges is deferred |
playos-shell | root | All | Trusted; root accepted by control-socket check |
playos-overlay | root | All | Trusted; root accepted by control-socket check |
| Active game | playos-game (uid 1001, gid 1001) | None | PR_SET_NO_NEW_PRIVS = 1; supplementary groups audio, render, input; seccomp + Landlock before exec |
playos-installer | root | All | Only present in installer image |
3. Game Restrictions
A normal game must not be able to:
| Action | Enforcement |
|---|---|
| Modify the system image | System partition mounted ro; game user has no write access |
Open DRM primary nodes (/dev/dri/card*) | drm group; game is not in it |
| Access another game's save data | Landlock path restrictions + per-game directory |
| Mount or format filesystems | seccomp blocks mount, umount2 |
| Load kernel modules | seccomp blocks init_module, finit_module |
| Change kernel parameters | seccomp blocks sysctl, game user has no /proc/sys write access |
| Invoke unrestricted shutdown/reboot | seccomp blocks reboot syscall |
| Connect to control IPC | UNIX group restriction; game is not in playos-trusted |
| Synthesize reserved system input | Input routing is compositor-enforced at Wayland/evdev level |
| Create trusted overlays | Trusted roles require PLAYOS_TRUSTED_* env + group check |
| Ptrace other processes | seccomp blocks ptrace |
| Escalate privileges | PR_SET_NO_NEW_PRIVS; seccomp blocks setuid, setcap |
4. IPC Access Control
Control socket (/run/playos/control.sock)
- Owner:
root:playos-trusted, mode0660 - Who can connect:
playos-shell,playos-overlay(both inplayos-trusted) - Who cannot:
playos-game— enforced by UNIX group check atconnect(2)time
Compositor socket (/run/playos/compositor.sock)
- Owner:
root:playos-trusted, mode0660 - Who can connect:
playos-runtimeinternal client only - Who cannot:
playos-game
Lifecycle fd (PLAYOS_LIFECYCLE_FD)
- Direction: Write end held by
playos-init; read end passed to game - Game can: Read lifecycle events (single-byte values)
- Game cannot: Write to the fd; the write end is
close()d in the game process before exec - Not a socket: Cannot be used to connect to any IPC endpoint
5. Filesystem Access Control
System partition (/)
- Mounted read-only via
MS_RDONLY(Sprint 11) - dm-verity hash tree appended to system image (Sprint 12+ — planned, not yet implemented)
- Any write attempt returns
EROFS
Data partition (/data)
- Mounted read-write, owned by
root - Per-game directories:
chown playos-game:playos-game /data/saves/<id>andcache/<id> - Other directories (
config/,games/,logs/) owned byplayos-system, not writable by games - Network profiles:
/data/config/network/<slug>.jsonplus alastpointer,0600 root:root. The Wi-Fi passphrase is stored in plaintext — see §12.
Device nodes
| Device | Owner | Mode | Game access |
|---|---|---|---|
/dev/dri/card* | root:drm | 0660 | ❌ Not in drm group |
/dev/dri/renderD* | root:render | 0660 | ✅ In render group (needed for Wayland/EGL) |
/dev/input/event* — gamepad (045e:028e) | root:input | 0660 | ✅ In input group (built-in controller via libplayos evdev) |
/dev/input/event* — ASUS EC (0b05:1abe) | root:root | 0600 | ❌ Reserved (Command Center/M1-M2/brightness/volume/power); compositor/shell read as root |
/run/playos/*.sock | root:playos-trusted | 0660 | ❌ Not in playos-trusted |
Note: Games render client-side by opening /dev/dri/renderD* directly (in the render group); the compositor scans out the resulting buffer. Primary nodes (/dev/dri/card*) remain denied via the drm group. The gamepad input node stays in input, while the ASUS embedded-controller input nodes are moved to root:root 0600 by 99-playos-input.rules so a game cannot read reserved system buttons directly.
6. seccomp Filter Policy
Applied to game processes as a hand-built classic-BPF filter (no runtime
libseccomp dependency — playos-init is a static PID 1). The filter
verifies AUDIT_ARCH_X86_64, kills any other ABI, and returns EPERM
for the privileged/credential syscall deny-list below. A full syscall
allowlist is deferred (games are dynamically linked; see Sprint-12.md
Decisions). Path-based open/openat restrictions are enforced by
Landlock (§7), not by pointer-dereferencing BPF.
Allowed syscalls
Everything not listed below is allowed. This is a deliberate MVP tradeoff: Landlock default-deny is the path boundary, the seccomp deny-list blocks the privileged syscalls Landlock cannot reach.
Blocked syscalls (return EPERM)
mount, umount2, pivot_root, chroot, acct, swapon, swapoff, quotactl,
_sysctl, uselib, init_module, finit_module, delete_module
setuid, setgid, setreuid, setregid, setresuid, setresgid, setfsuid,
setfsgid, capset
ptrace, process_vm_readv, process_vm_writev
reboot, kexec_load, kexec_file_load, settimeofday, clock_settime,
adjtimex, sethostname, setdomainname, iopl, ioperm, personality,
vhangup, mknod, unshare, setns
seccomp, bpf, perf_event_open, userfaultfd, add_key, request_key,
keyctl, name_to_handle_at, open_by_handle_at
prctl(PR_SET_SECCOMP) # games cannot stack/replace filters; other
# prctl calls are allowed
ioctl restrictions
Device-node access is enforced by Landlock + group membership, not by
ioctl filtering. /dev/dri (render node) and /dev/input are granted
via the Landlock allowlist and render/input group membership;
/dev/dri/card* (primary nodes) remain denied via the drm group.
7. Landlock Filesystem Restrictions
Requires Linux ≥ 5.13 (ROG Ally ships with kernels that support this). Falls back to logging-only if unavailable. Implemented in playos-init/src/security/landlock.c; rule construction is data-driven by launch identity.
Allowed paths for game processes
| Path | Access |
|---|---|
/data/games/<game-id>/ | Read + execute (read-only game content, dynamic traversal) |
/data/saves/<game-id>/ | Read + write + create + remove + truncate |
/data/cache/<game-id>/ | Read + write + create + remove + truncate |
/tmp | Read + write + create + remove (shared scratch) |
/run/playos/ | Read + execute (Wayland socket path) |
/lib, /usr/lib | Read + execute (musl dynamic loader + shared libraries) |
/dev/snd | Read + write (ALSA PCM/control nodes) |
/dev/input | Read (built-in controller evdev nodes) |
/dev/dri | Read + write (DRM render node for client-side rendering) |
/dev/shm | Read + write + create + remove (wl_shm fallback) |
/etc/asound.conf | Read (single-file rule, optional) |
Denied (implicitly — not in allowed set)
/data/games/<other-game-id>/— other games/data/saves/<other-game-id>/— other games' saves/data/config/— system configuration/data/log/— system logs (games write viaplayos_log(), not direct fs access)/run/playos/control.sock— control IPC/run/playos/compositor.sock— compositor control/sys/,/proc/— system and process snooping/dev/dri/card*— primary DRM nodes (games render via therendernode; primary scanout remains compositor-only)
8. Input Security
Implemented (Sprint 12). Reserved system actions are consumed by
playos-compositor at the seat layer (src/system_button.c) before any event
reaches a client, and are stripped from the game's view by the libplayos
snapshot mask (playos_input.c). On the ROG Ally the controls are split across
two evdev nodes:
- Gamepad (
Microsoft X-Box 360 pad,045e:028e,event5): face buttons, sticks, triggers, d-pad, and HOME (BTN_MODE). Games read this node via raw evdev; it stays in theinputgroup. - ASUS embedded controller (
Asus Keyboard,0b05:1abe,event6/7/8): Command Center (KEY_PROG1/KEY_PROG2), M1/M2, brightness, volume, and power/sleep.99-playos-input.rulesmoves these nodes toroot:root 0600, so a game (ininput) cannot open them; the compositor and shell run as root and still read them.
HOME (BTN_MODE) lives on the gamepad node, so it is kept out of the game's
snapshot by the mask + seat intercept (not by udev). The i8042 "AT Translated
Set 2 keyboard" (event3) is left in input — it carries only volume/power/
sleep/wakeup, with power/sleep intercepted at the seat and reboot
seccomp-blocked.
Input routing hierarchy:
libinput event
│
▼ playos-compositor intercepts
├── reserved action → PlayOS only (never to client)
├── overlay visible → overlay Wayland client
├── game foreground → game Wayland client (filtered: no reserved keys)
└── otherwise → shell Wayland client
Games read the built-in controller through raw evdev via libplayos. Reserved buttons (BTN_MODE, KEY_PROG1/2, and the EC's M1/M2) never reach the game: the EC node is root:root 0600 (udev), libplayos masks reserved buttons out of the controller snapshot, and the compositor seat intercept strips them before any event reaches a client.
9. System Image Integrity
Sprint 11 (initial): Read-only mount
mount(device, "/", "ext4", MS_RDONLY, NULL);
Any write to the system partition returns EROFS.
Post-Sprint 12 (production): dm-verity — planned, not implemented
# At build time (in playos-refdistro release pipeline):
veritysetup format system.img system.img.verity > system.verity.superblock
Status (2026-09-24): not implemented. What exists today is the read-only mount above: writes are refused with
EROFS, but nothing detects tampering with the image itself, so a modified partition is only caught if it fails to boot. dm-verity is item 1 of §12;veritysetupis in the Buildroot tree, but no build step produces a hash tree and no boot step mounts a verity device.
At boot, playos-init:
- Sets up a dm-verity device over the system partition
- Mounts the dm-verity device read-only
- If hash verification fails for any block, the kernel returns I/O errors (enforced by dm-verity)
playos-initmonitors for dm-verity errors; repeated errors trigger A/B rollback
10. Secure Boot Chain
Target signing chain (post-MVP, Sprint 12 foundations)
UEFI Secure Boot (platform key)
└── signs BOOTX64.EFI
BOOTX64.EFI (Linux EFI stub)
└── kernel + embedded initramfs (verified by EFI stub signature)
Kernel (IMA or dm-verity)
└── system partition hash tree (dm-verity root hash embedded in initramfs)
A/B update bundles
└── signed with PlayOS update key (RAUC bundle signature)
Development key setup (Sprint 12)
- Dev keys live in
playos-refdistro/keys/dev/:efi-signing-key.pem+efi-signing-cert.pem— self-signed EFI signing key used byscripts/sign-efi.sh(sbsign) for development and CI buildsmanifest-key.pub/manifest-key.sec— Ed25519 game-manifest signing key used byscripts/sign-manifest.sh; the public key is embedded inplayos-init(src/security/game_key.h) for warn-only manifest verification
- Production: HSM-backed keys, never leave the signing server (post-MVP)
sbsignis the release-pipeline signing tool;pesignis an acceptable alternative for RPM-oriented flows
Recovery
If Secure Boot verification fails:
- UEFI firmware refuses to boot the artifact
- User must boot into UEFI Secure Boot key management to enroll the PlayOS development key (dev builds)
- Production: chain-of-trust failure surfaces as boot failure → A/B rollback → recovery mode
11. Development vs Production
| Feature | Development image | Production image |
|---|---|---|
| BusyBox shell | ✅ Present | ❌ Absent |
| SSH daemon | ❌ (planned post-network sprint) | ❌ Absent |
gdbserver, strace | ✅ Present | ❌ Absent |
| Serial console | ✅ Enabled | ✅ Enabled (needed for recovery) |
| dm-verity | Optional | ✅ Required |
| Secure Boot | Optional (disabled ok) | ✅ Required |
| Debug assertions | ✅ Enabled | ❌ Disabled |
| Open TCP sockets | Allowed (SSH) | ❌ None |
| seccomp | ✅ Enforced | ✅ Enforced |
| Landlock | ✅ Enforced | ✅ Enforced |
The post-build production lint CI step asserts:
- No
/bin/shor/bin/busyboxin the image - No open listening TCP sockets
- No
gdbserver,strace, or debug tools - System partition is read-only
- All EFI artifacts are signed
12. Post-MVP Hardening Roadmap
In priority order after v0.1.0:
- dm-verity for system partition integrity (Sprint 12 gap)
- Signed game manifests (Ed25519 — warn-only in Sprint 12, enforced post-MVP)
- User namespaces for additional game isolation if needed
- Hardware-backed keys for update signing
- IMA/EVM for individual file integrity in the initramfs
- Audit logging for privileged IPC commands
- Network namespace for games (when networking is introduced)
- Mandatory access control (SELinux or AppArmor) if seccomp + Landlock proves insufficient
- Secrets at rest on
/data(parked from Sprint 16) — Wi-Fi passphrases sit in plaintext in/data/config/network/<slug>.json(0600 root:root). They are invisible to games and never written to a log, but/datais an unencrypted ext4 partition, so anyone holding the disk (or mounting it elsewhere) can read them. Options, cheapest first: encrypt the profile with a per-device key already present inboot.json, seal it with the fTPM via/dev/tpmrm0, or prompt per boot and store nothing (which would break the auto-connect that T7 requires for a console). Parked as a cross-cutting secrets-at-rest design decision, not a Wi-Fi feature — recorded insprints/Sprint-16.md→ Parked.