Files
punktfunk/crates
enricobuehlerandClaude Opus 5 80e57c70be fix(vdisplay): the env lock covered its writers and not its readers, and the gamescope ladder read back its own output
Two halves of the same problem — process env used as shared mutable state.

`apply_session_env` set XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS / WAYLAND_DISPLAY /
HYPRLAND_INSTANCE_SIGNATURE / SWAYSOCK under ENV_LOCK, while detection read those same
five keys with no lock at all, from five points spread across a `/proc` scan. That is
exactly the hazard ENV_LOCK's own doc names: glibc `setenv` can realloc `environ` and
free the old value string, so a concurrent `getenv` reads freed memory — UB documented
there as "could crash the host". It is reachable today: the host's session watcher
calls `detect_active_session` every second for a stream's lifetime while a second
client's connect runs `apply_session_env` on a spawn_blocking thread.

Detection now samples all five into an `EnvProbe` in ONE locked read and scans from
that snapshot. Taking the lock around the whole of `detect_active_session` would have
been simpler and wrong: it walks `/proc` every second, and holding a process-wide lock
across a directory walk trades this problem for the one Phase 7 is about. The readers
become pure functions of their inputs, which is also what let the tests stop mutating
process env to steer them — `without_inherited_swaysock` is gone, replaced by handing
in a probe. Empty values are now treated as absent, so `XDG_RUNTIME_DIR=""` falls back
to /run/user/<uid> instead of yielding a relative path.

The gamescope sub-mode ladder had a nastier version: `apply_input_env` WRITES
PUNKTFUNK_GAMESCOPE_NODE/_SESSION to publish the sub-mode it chose, and read the same
keys back as operator overrides. The Attach arm sets `_NODE=auto`, and `node_env` sits
at rung 2 of the ladder — above `dedicated_launch` at rung 3 — so one Attach decision
latched Attach for the rest of the host's life and silently overrode
`game_session=dedicated`. Only rung 1 could escape it, because the Spawn arm that
would have cleared the keys sits below the rung that by then always fired. The
overrides are now sampled once, before this module has written anything, which keeps
their actual meaning: "the operator set this before we ran". The live reads that
remain (`launch_is_nested`, `poolable_now`) are deliberate — those consume the
published decision.

And gamescope's `create` read both keys unguarded, racing the writer and skipping the
lock its two siblings take for the same keys; it now takes one guarded snapshot.

Verified on Linux (.25): 100 passed, 1 ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:45:36 +02:00
..