Decisions
0027: One app cannot take the desktop
Per-app limits keep the session responsive when one app runs away.
Status: accepted
Context
a fork bomb, a memory bomb or a full file table from a normal user must not freeze the desktop. Before this, app scopes had only systemd's default task limit, no memory limit, and the system-wide open file count was lower than one process's hard limit.
Decision
- Defaults, no kernel patch: measurement did not ask for one.
- Every app window's scope (Memory Saver's
adopt_args) gets TasksMax=8192, 80% of RAM and 25% of swap.user@.serviceshares one such set. A user's slice holds 16384 tasks. - The session scope gets CPUWeight=1000 and 256 MB of memory.min, passed down through its slices.
system.slicekeeps 128 MB, so login and Force Quit work under pressure. - Fork storms die on their own: wold reads
pids.eventseach tick and kills a scope refused at least 2000 forks a tick, two ticks running (memsaver/storm.rs). A program that backs off is never touched. - An app with 4 OOM kills within 5 ticks is killed too: one lost tab is not a loop. When the user's slice refuses forks like a storm at 95% of its tasks, with no app at its own limit, the app with the most tasks goes (
memsaver/storm.rs,engine.rs). Unit tested, not yet run in a guest. fs.file-maxgoes back to systemd's value. Open files are charged to the app's memory, so the limit is memory, not a count everyone shares./tmpand/dev/shmhold 25% of RAM each. Their pages outlive the app that wrote them.
Measured in a throwaway guest (8 vCPU, 8 GB, WolKernel, btrfs, zram; image/vm/test/limits). Stand-in compositor at 120 Hz in the session scope, the bomb in a window scope, 20 s then killed:
| Attack | Before | After |
|---|---|---|
| Fork bomb | runs until killed by hand | dead in 7.1 s, no failed spawns, 5 ms worst input latency |
| Thread bomb | runs until killed by hand | dead in 6.0 s |
| Memory bomb | 95 s outage, spawn median 3.7 s, root login up to 13 s | spawn median 3.5 ms, root login 0.9 s at worst |
| Pipe fill | guest wedged in 1 of 2 runs | 18 of 18 spawns, root 16 of 16 |
| File table | 19 of 20 spawns fail, root 1 of 3 | no failures, root 18 of 18 |
| PID, inotify, tmpfs, disk fill | no desktop harm | no desktop harm |
Recovery after the kill took 1.0 s or less in every run after the change. make -j16 beside a 4000-task browser-like tree: 4.91 s before, 5.00 s after, one run each, nothing killed.
Limits: fork() from unrelated scopes returned EAGAIN with 16,000 tasks in one scope on the 8 GB guest, cause not found; 8192 stays under it. The table was measured before the OOM loop and full-slice kills existed. A full disk or a full tmpfs stays full until something deletes the files. inotify limits stay per user.
Consequences
an app that needs more than 8192 tasks or 80% of RAM cannot get it on WolOS; the limits are systemd properties, so a future change is a drop-in and no code. fs.file-max and the tmpfs sizes differ from Arch's defaults (the drop-ins under files/ are the record). The kill rules in memsaver/storm.rs have unit tests only until they run in a guest (see Limits).
Reference synced from wonderlandos/wolos docs/adr/0027-one-app-cannot-take-the-desktop.md.