From b8e3ae2b96aece445d563e63bb6fee53800859a9 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 29 Jul 2026 05:59:09 -0500 Subject: docs(todo): record the lock-chain regression and the watch collision The night watch and the screen-lock watchdog fight over the session lock. The watch kills hyprlock by design to reveal its kiosk, and the watchdog reads that SIGTERM as a crash and relaunches. That is the root cause of the lockdead screens, and it retires both earlier theories: the AMD parameter was live throughout, and nothing ever crashed. Also files the renderer regression that dropped the wrapper out of lock_cmd, and the "SCREEN OFF" label that reads as a status report rather than the action its stage performs. Three gradings corrected after review. Two claimed a security carve-out for what is really an availability problem, which inflated them past what the matrix gives. A third reached [#C] and wrote [#B] beside it, which overrides the letter instead of fixing the input. Each correction is recorded in its own task body rather than quietly applied, so the reasoning can be argued with. The reboot task's claim that amdgpu.runpm=0 was the root cause is marked superseded for the same reason. --- todo.org | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/todo.org b/todo.org index f5578b3..25cd5a9 100644 --- a/todo.org +++ b/todo.org @@ -59,7 +59,9 @@ That is the root cause under the 2026-07-27 lockdead screen. The screen-lock flo Not :solo: — Craig closes his own session and runs topgrade first. -Rebooted 2026-07-28 08:59. =amdgpu.runpm=0= confirmed present in =/proc/cmdline= afterward, so the parameter is finally live and the root-cause fix for the hyprlock lockout is in effect. +Rebooted 2026-07-28 08:59. =amdgpu.runpm=0= confirmed present in =/proc/cmdline= afterward, so the parameter is finally live. + +Correction, 2026-07-29: the claim above and in the body that this is "the root cause under the 2026-07-27 lockdead screen" is wrong, and superseded. hyprlock was never crashing. Every logged exit is =rc=143=, SIGTERM, from =settings-watch= killing it by design. See =[#B] Night watch and the lock watchdog fight each other=. The reboot was still worth doing (the parameter is a genuine mitigation for a real AMD defect) but it did not fix this, and the lockdead screens continued after it. ** DONE [#B] Caffeine state is unreadable on both surfaces :bug:dotfiles:design:solo: CLOSED: [2026-07-28 Tue] @@ -116,7 +118,106 @@ Launched by hand afterward it runs fine and survives, so gammastep is not broken Worth its own task — the fix is a readiness wait or a retry around that exec-once, not a persistence change. Filed here for now because it surfaced during this investigation. -** TODO [#B] hyprlock still exits mid-lock; the watchdog relaunch is silent :bug:hyprland:dotfiles: +** TODO [#A] Idle commits silently drop the screen-lock wrapper :bug:hyprland:dotfiles:security: +DEADLINE: <2026-07-29 Wed> +:PROPERTIES: +:CREATED: [2026-07-29 Wed] +:LAST_REVIEWED: 2026-07-29 +:END: +Caught live 2026-07-29 05:30, seconds after it happened, while verifying that Craig's watch-stage change had landed. + +=idle.py= renders the *whole* hypridle.conf, including a hardcoded =GENERAL= block. That block said =lock_cmd = pidof hyprlock || hyprlock=. The live config said =|| screen-lock=. So every idle-stage commit through the panel rewrote =lock_cmd= and dropped the wrapper out of the chain. + +The wrapper is not incidental. It carries the flock duplicate guard (the fix for the 2026-07-27 unrecoverable wedge), the crash-relaunch watchdog, and the relaunch log. Parking one stage removed all three in a single write, and nothing said so. + +=tests/settings/test_settings.py:562= asserted the bare =|| hyprlock= form, so the suite *enforced* the regression. That is why 3845 tests stayed green through a day of work on exactly this subsystem. A test can pin the bug as readily as the fix. + +The false-negative this sets up is worth naming: with the wrapper gone the relaunch log stops receiving entries, and an empty log reads as "the problem is fixed" when it means "the instrument was removed". The =screen-lock= header already warns that an empty file is not proof; this is the mechanism that would have produced one. + +FIXED here, TDD, in the working tree pending commit: +- =idle.py= =GENERAL= now names =screen-lock=, with a comment saying why the line is load-bearing. +- The test now pins the wrapper form. Red first against the old template. +- Live config rewritten through the panel's own path and hypridle restarted; =lock_cmd= confirmed back to =screen-lock=, one hypridle running. + +Grading: Critical severity (=write_conf= truncates, so any hypridle key the renderer does not model is silently deleted rather than preserved — that is configuration data loss, and the =lock_cmd= case proved it happens in the field) x some users sometimes (only when an idle stage is committed, which is rare) = P2 = [#B]. + +An earlier draft graded this [#A] on a "security carve-out". That was wrong: disarming the guard is an availability problem, not a leak, and the carve-out is for privacy, security, compliance and safety. The severity band is what carries the weight here, and silent deletion of configuration is the =Critical= band's data-loss case. + +Two further fixes came out of an independent review of the first one: + +- *Fail-open restored.* =pidof hyprlock || screen-lock= made the wrapper the end of the chain, and =screen-lock= is a stow symlink in =~/.local/bin=, not a system binary. An unstowed tree, or a hypridle started without =~/.local/bin= on PATH, resolves it to 127 — so the screen would never lock *at all*. That is worse than the duplicate client the wrapper prevents. The chain now ends =|| hyprlock=, matching the wrapper's own fail-open discipline. +- *The file now says it is generated.* Three comment lines at the top of the rendered output name the renderer and warn that edits are overwritten. The absence of that header is how the divergence survived unnoticed. + +Still open, and why this stays a task rather than closing with the fixes: the header warns, but nothing *prevents* the next divergence, and the exposure is wider than =lock_cmd= alone. The review enumerated it: + +- =before_sleep_cmd= and =after_sleep_cmd= sit in the same hardcoded block, at identical risk. +- Every stage command is hardcoded in =_stage_commands= (brightness level, lock, watch, dpms, suspend), same one-way overwrite. +- =write_conf= *truncates* rather than merges, so any hypridle key the renderer does not know about (=ignore_dbus_inhibit=, =ignore_systemd_inhibit=, =inhibit_sleep=, =on-lock=, =on-unlock=) is deleted rather than preserved. That is the largest hole: a key nobody has added yet would vanish the first time a stage is parked. + +Options: have the renderer preserve the existing general block and unknown keys instead of emitting its own, or accept the template as the single source and move every hypridle setting into the panel. A design call for Craig, and the truncation half is the part that will bite next. + +** TODO [#B] "SCREEN OFF" reads as a state, not a stage :bug:dotfiles:design: +:PROPERTIES: +:CREATED: [2026-07-29 Wed] +:LAST_REVIEWED: 2026-07-29 +:END: +Craig, 2026-07-29: "The module said SCREEN OFF, which I took to mean the screensaver was off." + +=panel.py:43= labels the DPMS stage =SCREEN OFF=. It names the *action that stage performs* (power the display off), but it reads as a *status report* (the screen feature is off). Craig read the second meaning, concluded the screensaver was disabled, and reported the lock bug on that basis. It cost a wrong turn in a live diagnosis. + +Two compounding factors. Nothing in the panel or the bar uses the word "screensaver" at all, so the thing he calls the screensaver is labelled =WATCH= and is not findable by its own name. And the rail's other labels are unambiguous verbs or nouns (=DIM=, =LOCK=, =SUSPEND=), so =SCREEN OFF= is the only one that parses two ways. + +Fix direction: rename so the label names the action consistently with its siblings (=BLANK=, or =DISPLAY OFF=, or =SLEEP SCREEN=), and give =WATCH= a name that connects to what it is (=SCREENSAVER=, or =WATCH FACE=). Worth a look at the whole rail's vocabulary at once rather than patching one label. + +Grading: Major severity (the label doesn't just look wrong, it produces a confident false belief about whether a security-adjacent control is active, and that belief drove a wrong turn in a live investigation) x most users frequently (every reading of the rail) = P2 = [#B]. + +An earlier draft graded this Minor, arrived at [#C], and then wrote [#B] beside it anyway with a justification. That is overriding the letter, which the format rule specifically forbids because it turns the matrix into a formality. The input that was wrong is the severity band: a label that reliably misinforms about state is more than cosmetic. Corrected the band, and the letter now follows from it. + +Not :solo: — naming is Craig's taste call, and the rail's vocabulary should be decided as a set. + +** TODO [#A] Night watch and the lock watchdog fight each other :bug:hyprland:dotfiles: +DEADLINE: <2026-07-31 Fri> +:PROPERTIES: +:CREATED: [2026-07-29 Wed] +:LAST_REVIEWED: 2026-07-29 +:END: +ROOT CAUSE of the lockdead screens, found 2026-07-29 00:50 within minutes of the relaunch logging going live. hyprlock is not crashing. It is being killed on purpose, by us. + +=settings/src/settings/watch.py:116= runs =pkill -x hyprlock=. That is deliberate and documented in the module's own header: the night watch *is* the lock screen, a normal window cannot paint over hyprlock's ext-session-lock surface, so =start()= maps the kiosk behind the lock and then kills hyprlock to reveal it already drawn. =stop()= re-locks first, then kills the kiosk, so the desktop never flashes. + +=screen-lock='s watchdog cannot tell that apart from a crash. Any non-zero exit means "died while still locked, relaunch it", and SIGTERM reports 143. + +The collision, on hypridle's own timings: +- t=450s: =loginctl lock-session= → screen-lock → hyprlock up, watchdog waiting on it. +- t=480s: =settings-watch start= maps the kiosk and kills hyprlock. Exit 143. +- t=480s + =LOCK_RELAUNCH_DELAY= (0.5s): the watchdog relaunches hyprlock, which comes back *on top of* the night watch. +- In that half-second gap the session is locked with no client, so Hyprland draws lockdead. The replacement hyprlock then blurs it. + +That is exactly what Craig saw: lockdead text blurred behind a working lock screen, clearing on authentication. + +Two consequences, and the second is worse than the cosmetic one: +1. The lockdead artifact. +2. *The night watch is silently broken.* It reveals itself and is covered by a relaunched hyprlock half a second later, every time. The feature has not worked since the watchdog shipped. + +Evidence: =~/.local/var/log/screen-lock.log= carries nine entries, every one =rc=143=: three on 2026-07-28 (19:00:00, 22:53:56, 23:17:58) and six on 2026-07-29 (02:59:54 through 05:08:01). Zero entries from any other cause. + +The AMD theory is dead. =amdgpu.runpm=0= was live for all three, there is no DPMS idle rule, and no coredump exists because nothing crashed. The wf-recorder hypothesis is also unsupported: a deliberate lock at 00:50 with a recorder running produced no relaunch, because I unlocked at 12 seconds and never reached 480s. + +Grading: Major severity (the lock client is killed mid-lock on every cycle, the night-watch feature never actually shows, and a sustained collision can exhaust the watchdog's 30-attempt cap and leave the session locked with no client, recoverable only from another console) x most users frequently (every idle period reaching the watch stage; nine occurrences logged in eleven hours) = P2 = [#B]. + +An earlier draft graded this [#A] under the security carve-out. That was wrong and the correction is worth keeping. The carve-out covers privacy or security leaks, compliance violations, and safety issues. Nothing leaks here: the session stays locked throughout, the lockdead frame clears on authentication, and the harm is availability plus a silently broken feature. Invoking the carve-out inflated a P2 two bands into the always-visible [#A] gate, which is the exact abuse the matrix exists to prevent. + +Not :solo: — the fix is a design decision between two subsystems, both of which Craig owns: +1. Teach the watchdog that SIGTERM is not a crash (skip relaunch on 143). Simple, but it weakens the "a kill re-locks rather than unlocks" property the watchdog was written for. +2. Have =settings-watch= tell the watchdog to stand down before it kills, via the flag file it already maintains at =$XDG_RUNTIME_DIR/settings-watch-relock=. Keeps the kill-re-locks property intact and is the more honest handshake. +3. Stop killing hyprlock at all and find another way to reveal the kiosk. + +Option 2 is the one I would argue for, but it is Craig's call. + +Previous title and framing of this task, kept for the record: "hyprlock still exits mid-lock; the watchdog relaunch is silent". The instrumentation that closed that gap is dotfiles =5bbe2c3=, and it paid for itself in about six hours. + +** DONE [#B] hyprlock still exits mid-lock; the watchdog relaunch is silent :bug:hyprland:dotfiles: +CLOSED: [2026-07-29 Wed] :PROPERTIES: :CREATED: [2026-07-28 Tue] :LAST_REVIEWED: 2026-07-28 @@ -141,6 +242,8 @@ Solo for the instrumentation half only: adding the relaunch logging is buildable Next step when picked up: land the relaunch logging first so the next occurrence produces evidence, then try to reproduce by locking with =wf-recorder= running. +Superseded 2026-07-29 by =[#A] Night watch and the lock watchdog fight each other=. The logging landed (dotfiles =5bbe2c3=) and answered it within hours: three =rc=143= entries, SIGTERM, from =settings-watch= killing hyprlock by design. Nothing was crashing, so both the AMD-iGPU and the screen-recorder hypotheses in this task are wrong. Kept closed rather than deleted because the reasoning that led here is worth the record. + ** TODO [#B] The wireguard gpg convention is inert; the installer can't read it :bug:security:network: :PROPERTIES: :CREATED: [2026-07-28 Tue] -- cgit v1.2.3