`portal_config::ensure_key` folded EVERY read failure into an empty string
(`read_to_string(path).unwrap_or_default()`). `upsert("", …)` then produced a file containing only
our block, the one-time backup was skipped because `!existing.is_empty()` was false, and the write
replaced the user's config — returning `Ok(true)`.
So a single Latin-1 character in a comment in `~/.config/hypr/xdph.conf` or
`~/.config/xdg-desktop-portal-wlr/config` destroyed the operator's entire portal configuration, with
no backup and no warning. The module doc says flat-writing these files "destroyed [everything else]
on first connect, silently and permanently" and that this module exists so it cannot happen; that
one line re-opened the door. The same shape hit a transient EIO on an NFS or overlay config dir.
Now: bytes are read with an explicit match, only `NotFound` may mean "empty", a non-UTF-8 config is
refused by name rather than replaced, the backup is taken by BYTES, and the write is atomic
(temp + `sync_all` + rename in the same directory, permissions carried over). Five new tests, all
running on macOS — `a_non_utf8_config_is_refused_not_replaced` fails against the old code.
Also in the wlr/Mutter family:
* **Mutter's `Primary` rebuilt kept physicals from scratch** — scale forced to 1.0, transform to 0,
disabled heads re-enabled — so a rotated, 2x-scaled or deliberately-disabled monitor came back
wrong, while the code went to real trouble to preserve refresh. Each head now carries its
pre-connect scale and transform, and x advances by the LOGICAL width.
* Three availability probes read session env (`SWAYSOCK`, `XDG_CURRENT_DESKTOP`,
`HYPRLAND_INSTANCE_SIGNATURE`) with no `ENV_LOCK` while `apply_session_env` `set_var`s the same
keys from another thread — the glibc setenv/getenv race this crate's own lib.rs documents as UB.
* `wlroots::create_output` ran a statement before its `OutputGuard` existed, so a raced
`wait_new_output` orphaned the output permanently — hyprland takes the guard first. The
before/after name diff also ran outside any lock, so two concurrent creates could adopt each
other's output. Both now run under a create lock, with a stray sweep on the failure path.
* `select_and_cast`'s timeout arm dropped the portal thread's `stop` flag un-set — the same leak
Mutter was already fixed for. The guard is now built before the wait, in both copies.
* The xdpw chooser file was written per session and never removed, permanently shadowing the
config's fallback with the name of an already-unplugged output. Its lifetime is now the handshake,
not the session — scoped deliberately, because tying removal to the keepalive would let one
session delete another's selection hours later.
* Hyprland's headless outputs are now named `PF-<pid>-<n>` and reconciled at startup, so a crashed
host's leftovers are reclaimed while a live sibling host's outputs cannot be pulled out from under
it. `set_monitor_rule` no longer discards hyprctl's rejection text and then hard-codes a
GBM/dmabuf diagnosis it never verified.
* Both wlr backends silently dropped the `topology` policy axis: `Primary`/`Exclusive` was accepted,
echoed by the mgmt API, applied on three backends and a no-op on two. They now say so.
Item 8.1: `swaymsg`, `hyprctl` and the portal `systemctl --user try-restart` calls are bounded
through `proc` with named budgets.