PlayOS Security Model

Cross-references: architecture.md §13–15, runtime-ipc.md §3, sprints/Sprint-12.md


Table of Contents

  1. Trust Zones
  2. Component Privilege Levels
  3. Game Restrictions
  4. IPC Access Control
  5. Filesystem Access Control
  6. seccomp Filter Policy
  7. Landlock Filesystem Restrictions
  8. Input Security
  9. System Image Integrity
  10. Secure Boot Chain
  11. Development vs Production
  12. 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

ComponentUserCapabilitiesNotes
playos-initrootAll (required for process supervision, mounts, device setup)Drop unnecessary caps after init (future)
playos-compositorrootAllSprint 12 scopes privilege reduction to games; dropping system-component privileges is deferred
playos-shellrootAllTrusted; root accepted by control-socket check
playos-overlayrootAllTrusted; root accepted by control-socket check
Active gameplayos-game (uid 1001, gid 1001)NonePR_SET_NO_NEW_PRIVS = 1; supplementary groups audio, render, input; seccomp + Landlock before exec
playos-installerrootAllOnly present in installer image

3. Game Restrictions

A normal game must not be able to:

ActionEnforcement
Modify the system imageSystem 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 dataLandlock path restrictions + per-game directory
Mount or format filesystemsseccomp blocks mount, umount2
Load kernel modulesseccomp blocks init_module, finit_module
Change kernel parametersseccomp blocks sysctl, game user has no /proc/sys write access
Invoke unrestricted shutdown/rebootseccomp blocks reboot syscall
Connect to control IPCUNIX group restriction; game is not in playos-trusted
Synthesize reserved system inputInput routing is compositor-enforced at Wayland/evdev level
Create trusted overlaysTrusted roles require PLAYOS_TRUSTED_* env + group check
Ptrace other processesseccomp blocks ptrace
Escalate privilegesPR_SET_NO_NEW_PRIVS; seccomp blocks setuid, setcap

4. IPC Access Control

Control socket (/run/playos/control.sock)

  • Owner: root:playos-trusted, mode 0660
  • Who can connect: playos-shell, playos-overlay (both in playos-trusted)
  • Who cannot: playos-game — enforced by UNIX group check at connect(2) time

Compositor socket (/run/playos/compositor.sock)

  • Owner: root:playos-trusted, mode 0660
  • Who can connect: playos-runtime internal 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> and cache/<id>
  • Other directories (config/, games/, logs/) owned by playos-system, not writable by games
  • Network profiles: /data/config/network/<slug>.json plus a last pointer, 0600 root:root. The Wi-Fi passphrase is stored in plaintext — see §12.

Device nodes

DeviceOwnerModeGame access
/dev/dri/card*root:drm0660❌ Not in drm group
/dev/dri/renderD*root:render0660✅ In render group (needed for Wayland/EGL)
/dev/input/event* — gamepad (045e:028e)root:input0660✅ In input group (built-in controller via libplayos evdev)
/dev/input/event* — ASUS EC (0b05:1abe)root:root0600❌ Reserved (Command Center/M1-M2/brightness/volume/power); compositor/shell read as root
/run/playos/*.sockroot:playos-trusted0660❌ 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

PathAccess
/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
/tmpRead + write + create + remove (shared scratch)
/run/playos/Read + execute (Wayland socket path)
/lib, /usr/libRead + execute (musl dynamic loader + shared libraries)
/dev/sndRead + write (ALSA PCM/control nodes)
/dev/inputRead (built-in controller evdev nodes)
/dev/driRead + write (DRM render node for client-side rendering)
/dev/shmRead + write + create + remove (wl_shm fallback)
/etc/asound.confRead (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 via playos_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 the render node; 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 the input group.
  • 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.rules moves these nodes to root:root 0600, so a game (in input) 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; veritysetup is in the Buildroot tree, but no build step produces a hash tree and no boot step mounts a verity device.

At boot, playos-init:

  1. Sets up a dm-verity device over the system partition
  2. Mounts the dm-verity device read-only
  3. If hash verification fails for any block, the kernel returns I/O errors (enforced by dm-verity)
  4. playos-init monitors 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 by scripts/sign-efi.sh (sbsign) for development and CI builds
    • manifest-key.pub / manifest-key.sec — Ed25519 game-manifest signing key used by scripts/sign-manifest.sh; the public key is embedded in playos-init (src/security/game_key.h) for warn-only manifest verification
  • Production: HSM-backed keys, never leave the signing server (post-MVP)
  • sbsign is the release-pipeline signing tool; pesign is an acceptable alternative for RPM-oriented flows

Recovery

If Secure Boot verification fails:

  1. UEFI firmware refuses to boot the artifact
  2. User must boot into UEFI Secure Boot key management to enroll the PlayOS development key (dev builds)
  3. Production: chain-of-trust failure surfaces as boot failure → A/B rollback → recovery mode

11. Development vs Production

FeatureDevelopment imageProduction image
BusyBox shell✅ Present❌ Absent
SSH daemon❌ (planned post-network sprint)❌ Absent
gdbserver, strace✅ Present❌ Absent
Serial console✅ Enabled✅ Enabled (needed for recovery)
dm-verityOptional✅ Required
Secure BootOptional (disabled ok)✅ Required
Debug assertions✅ Enabled❌ Disabled
Open TCP socketsAllowed (SSH)❌ None
seccomp✅ Enforced✅ Enforced
Landlock✅ Enforced✅ Enforced

The post-build production lint CI step asserts:

  • No /bin/sh or /bin/busybox in 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:

  1. dm-verity for system partition integrity (Sprint 12 gap)
  2. Signed game manifests (Ed25519 — warn-only in Sprint 12, enforced post-MVP)
  3. User namespaces for additional game isolation if needed
  4. Hardware-backed keys for update signing
  5. IMA/EVM for individual file integrity in the initramfs
  6. Audit logging for privileged IPC commands
  7. Network namespace for games (when networking is introduced)
  8. Mandatory access control (SELinux or AppArmor) if seccomp + Landlock proves insufficient
  9. 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 /data is 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 in boot.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 in sprints/Sprint-16.md → Parked.