diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-24 23:16:56 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-24 23:16:56 -0500 |
| commit | a9391c90f07a564693bb1994f4f8fe6470efb3a2 (patch) | |
| tree | 73ac6d588491314615ef50dbffc8fdd07842019d /tests/installer-steps/test_hyprlock_pam.py | |
| parent | 082d870d6c4c3a3cce85506505582b17ef19487b (diff) | |
| download | archsetup-a9391c90f07a564693bb1994f4f8fe6470efb3a2.tar.gz archsetup-a9391c90f07a564693bb1994f4f8fe6470efb3a2.zip | |
fix(installer): harden the lock path against the AMD-iGPU DPMS lockout
An idle lock on this Strix Halo box wedged the whole session: hyprlock died and the compositor stayed locked with no prompt, recoverable only from a console. It's a documented AMD-integrated-Radeon failure (hyprlock#953, Hyprland#5822) -- a display power cycle via DPMS invalidates the GPU resources the lock client holds, so hyprlock loses its surface and exits without unlocking. No coredump, no OOM; the GPU pulls the rug out.
Two installer changes, both scoped and tested:
update_grub_cmdline adds amdgpu.runpm=0 on AMD machines only. Disabling GPU runtime power management keeps those resources valid across a display cycle -- the root fix. A no-op on Intel/NVIDIA, and it rides the existing merge so no boot-critical token is touched.
configure_hyprlock_pam writes a complete PAM stack. The hyprlock package ships only `auth include login`, leaving account and session uninitialised so pam_end() crashes on cleanup -- a separate documented lockout cause. All three phases now resolve through login, inheriting the keyring the graphical login uses.
CALL_SITES pins both new wirings. 372 unit tests, exit 0; each addition proven by reverting it.
Diffstat (limited to 'tests/installer-steps/test_hyprlock_pam.py')
| -rw-r--r-- | tests/installer-steps/test_hyprlock_pam.py | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/tests/installer-steps/test_hyprlock_pam.py b/tests/installer-steps/test_hyprlock_pam.py new file mode 100644 index 0000000..8d7295f --- /dev/null +++ b/tests/installer-steps/test_hyprlock_pam.py @@ -0,0 +1,85 @@ +"""Tests for configure_hyprlock_pam in the archsetup installer. + +hyprlock's packaged /etc/pam.d/hyprlock ships only `auth include login`, with no +account or session sections. pam_end() then crashes cleaning up handles that +were never initialised -- one of the documented causes of the hyprlock lockout +where the session wedges with no prompt (hyprlock#953). configure_hyprlock_pam +writes a complete stack so the account and session phases run. + +These tests exercise the REAL function body, extracted from the `archsetup` +script at run time, against a fixture PAM path. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_hyprlock_pam +""" + +import os +import subprocess +import tempfile +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + + +def run(path): + script = ( + 'logfile=/dev/null\n' + 'display() { :; }\n' + 'backup_system_file() { :; }\n' + 'error_warn() { echo "WARN:$1"; return 1; }\n' + "source <(sed -n '/^configure_hyprlock_pam() {/,/^}/p' \"%s\")\n" + 'configure_hyprlock_pam "%s"\n' + ) % (ARCHSETUP, path) + return subprocess.run(["bash", "-c", script], capture_output=True, + text=True, timeout=10) + + +class ConfigureHyprlockPam(unittest.TestCase): + def _write(self, initial="auth include login\n"): + fd, path = tempfile.mkstemp(prefix="pam-hyprlock-") + os.close(fd) + with open(path, "w") as f: + f.write(initial) + self.addCleanup(os.remove, path) + return path + + def _stack(self, path): + with open(path) as f: + return [l.split()[0] for l in f if l.strip() + and not l.lstrip().startswith("#")] + + # ---------------------------------------------------------- normal ---- + def test_writes_all_three_phases(self): + path = self._write() + run(path) + phases = set(self._stack(path)) + self.assertEqual(phases, {"auth", "account", "session"}, + "the complete stack must carry all three PAM phases") + + def test_account_and_session_are_the_added_ones(self): + path = self._write() + run(path) + stack = self._stack(path) + self.assertIn("account", stack) + self.assertIn("session", stack) + + # -------------------------------------------------------- boundary ---- + def test_idempotent_on_rerun(self): + path = self._write() + run(path) + first = open(path).read() + run(path) + self.assertEqual(open(path).read(), first, + "a second run must not stack duplicate lines") + + def test_overwrites_the_incomplete_package_default(self): + # The package default is exactly `auth include login`; a re-run replaces + # it wholesale rather than appending to it. + path = self._write("auth include login\n") + run(path) + self.assertEqual(self._stack(path).count("auth"), 1) + + +if __name__ == "__main__": + unittest.main() |
