Replace crash guard with SIGUSR1 unlock and crash logging
Remove sys.excepthook that unlocked on crash — this violated ext-session-lock-v1 security model where the compositor must keep the screen locked if the client dies (per protocol spec and hyprlock reference). Now: crashes are logged but session stays locked. SIGUSR1 handler added for external recovery (e.g. wrapper script).
This commit is contained in:
parent
3f31387305
commit
5fda0dce0c
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "moonlock"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||
requires-python = ">=3.11"
|
||||
license = "MIT"
|
||||
|
||||
@ -112,8 +112,7 @@ class MoonlockApp(Gtk.Application):
|
||||
logger.exception("Failed to create lockscreen window for monitor %d", i)
|
||||
|
||||
if not self._windows:
|
||||
logger.critical("No lockscreen windows created — unlocking session to prevent lockout")
|
||||
self._lock_instance.unlock()
|
||||
logger.critical("No lockscreen windows created — screen stays locked (compositor policy)")
|
||||
|
||||
def _activate_without_lock(self) -> None:
|
||||
"""Fallback for development — no session lock, just a window."""
|
||||
@ -146,20 +145,26 @@ class MoonlockApp(Gtk.Application):
|
||||
logger.exception("Failed to load CSS stylesheet")
|
||||
|
||||
|
||||
def _install_crash_guard(app: MoonlockApp) -> None:
|
||||
"""Install a global exception handler that unlocks the session on crash."""
|
||||
_original_excepthook = sys.excepthook
|
||||
def _install_signal_handlers(app: MoonlockApp) -> None:
|
||||
"""Install signal handlers for external unlock (SIGUSR1) and crash logging."""
|
||||
import signal
|
||||
from gi.repository import GLib
|
||||
|
||||
def _crash_guard(exc_type, exc_value, exc_tb):
|
||||
logger.critical("Unhandled exception — unlocking session to prevent lockout", exc_info=(exc_type, exc_value, exc_tb))
|
||||
if app._lock_instance:
|
||||
try:
|
||||
app._lock_instance.unlock()
|
||||
except Exception:
|
||||
pass
|
||||
_original_excepthook(exc_type, exc_value, exc_tb)
|
||||
def _handle_unlock(signum, frame):
|
||||
"""SIGUSR1: External unlock request (e.g. from recovery wrapper)."""
|
||||
logger.info("Received SIGUSR1 — unlocking session")
|
||||
GLib.idle_add(app._unlock)
|
||||
|
||||
sys.excepthook = _crash_guard
|
||||
def _handle_crash_log(signum, frame):
|
||||
"""Log unhandled exceptions but do NOT unlock — compositor keeps screen locked."""
|
||||
logger.critical("Unhandled exception in moonlock", exc_info=True)
|
||||
|
||||
signal.signal(signal.SIGUSR1, _handle_unlock)
|
||||
sys.excepthook = lambda exc_type, exc_value, exc_tb: (
|
||||
logger.critical("Unhandled exception — screen stays locked (compositor policy)",
|
||||
exc_info=(exc_type, exc_value, exc_tb)),
|
||||
sys.__excepthook__(exc_type, exc_value, exc_tb),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -167,7 +172,7 @@ def main() -> None:
|
||||
_setup_logging()
|
||||
logger.info("Moonlock starting")
|
||||
app = MoonlockApp()
|
||||
_install_crash_guard(app)
|
||||
_install_signal_handlers(app)
|
||||
app.run(sys.argv)
|
||||
|
||||
|
||||
|
||||
@ -170,10 +170,10 @@ class TestDefensiveWindowCreation:
|
||||
@patch("moonlock.main.FingerprintListener")
|
||||
@patch("moonlock.main.Gdk.Display.get_default")
|
||||
@patch("moonlock.main.Gtk4SessionLock")
|
||||
def test_all_windows_fail_unlocks_session(
|
||||
def test_all_windows_fail_does_not_unlock_session(
|
||||
self, mock_session_lock, mock_display, mock_fp, mock_window_cls
|
||||
):
|
||||
"""If ALL windows fail to create, session should be unlocked to prevent lockout."""
|
||||
"""If ALL windows fail, session stays locked (compositor policy)."""
|
||||
MoonlockApp, _ = _import_main()
|
||||
app = MoonlockApp.__new__(MoonlockApp)
|
||||
app._config = MagicMock()
|
||||
@ -195,48 +195,43 @@ class TestDefensiveWindowCreation:
|
||||
with patch("moonlock.main.logger"):
|
||||
app._activate_with_session_lock()
|
||||
|
||||
# Session should have been unlocked to prevent permanent lockout
|
||||
lock_instance.unlock.assert_called_once()
|
||||
# Session must NOT be unlocked — compositor keeps screen locked
|
||||
lock_instance.unlock.assert_not_called()
|
||||
|
||||
|
||||
class TestCrashGuard:
|
||||
"""Tests for the global exception handler that prevents lockout on crash."""
|
||||
class TestSignalHandlers:
|
||||
"""Tests for signal handlers and excepthook behavior."""
|
||||
|
||||
def test_crash_guard_unlocks_session_on_unhandled_exception(self):
|
||||
"""sys.excepthook should unlock the session when an exception reaches it."""
|
||||
def test_sigusr1_triggers_unlock(self):
|
||||
"""SIGUSR1 should schedule an unlock via GLib.idle_add."""
|
||||
MoonlockApp, _ = _import_main()
|
||||
from moonlock.main import _install_crash_guard
|
||||
from moonlock.main import _install_signal_handlers
|
||||
import signal
|
||||
|
||||
app = MoonlockApp.__new__(MoonlockApp)
|
||||
app._lock_instance = MagicMock()
|
||||
|
||||
_install_signal_handlers(app)
|
||||
|
||||
handler = signal.getsignal(signal.SIGUSR1)
|
||||
assert handler is not signal.SIG_DFL
|
||||
|
||||
def test_excepthook_does_not_unlock(self):
|
||||
"""Unhandled exceptions must NOT unlock the session."""
|
||||
MoonlockApp, _ = _import_main()
|
||||
from moonlock.main import _install_signal_handlers
|
||||
|
||||
app = MoonlockApp.__new__(MoonlockApp)
|
||||
app._lock_instance = MagicMock()
|
||||
|
||||
original_hook = sys.excepthook
|
||||
_install_crash_guard(app)
|
||||
|
||||
try:
|
||||
# Simulate an unhandled exception reaching the hook
|
||||
with patch("moonlock.main.logger"):
|
||||
sys.excepthook(RuntimeError, RuntimeError("segfault"), None)
|
||||
|
||||
app._lock_instance.unlock.assert_called_once()
|
||||
finally:
|
||||
sys.excepthook = original_hook
|
||||
|
||||
def test_crash_guard_survives_unlock_failure(self):
|
||||
"""Crash guard should not raise even if unlock() itself fails."""
|
||||
MoonlockApp, _ = _import_main()
|
||||
from moonlock.main import _install_crash_guard
|
||||
|
||||
app = MoonlockApp.__new__(MoonlockApp)
|
||||
app._lock_instance = MagicMock()
|
||||
app._lock_instance.unlock.side_effect = Exception("protocol error")
|
||||
|
||||
original_hook = sys.excepthook
|
||||
_install_crash_guard(app)
|
||||
_install_signal_handlers(app)
|
||||
|
||||
try:
|
||||
with patch("moonlock.main.logger"):
|
||||
# Should not raise despite unlock failure
|
||||
sys.excepthook(RuntimeError, RuntimeError("crash"), None)
|
||||
|
||||
# Must NOT have called unlock
|
||||
app._lock_instance.unlock.assert_not_called()
|
||||
finally:
|
||||
sys.excepthook = original_hook
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user