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]
|
[project]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@ -112,8 +112,7 @@ class MoonlockApp(Gtk.Application):
|
|||||||
logger.exception("Failed to create lockscreen window for monitor %d", i)
|
logger.exception("Failed to create lockscreen window for monitor %d", i)
|
||||||
|
|
||||||
if not self._windows:
|
if not self._windows:
|
||||||
logger.critical("No lockscreen windows created — unlocking session to prevent lockout")
|
logger.critical("No lockscreen windows created — screen stays locked (compositor policy)")
|
||||||
self._lock_instance.unlock()
|
|
||||||
|
|
||||||
def _activate_without_lock(self) -> None:
|
def _activate_without_lock(self) -> None:
|
||||||
"""Fallback for development — no session lock, just a window."""
|
"""Fallback for development — no session lock, just a window."""
|
||||||
@ -146,20 +145,26 @@ class MoonlockApp(Gtk.Application):
|
|||||||
logger.exception("Failed to load CSS stylesheet")
|
logger.exception("Failed to load CSS stylesheet")
|
||||||
|
|
||||||
|
|
||||||
def _install_crash_guard(app: MoonlockApp) -> None:
|
def _install_signal_handlers(app: MoonlockApp) -> None:
|
||||||
"""Install a global exception handler that unlocks the session on crash."""
|
"""Install signal handlers for external unlock (SIGUSR1) and crash logging."""
|
||||||
_original_excepthook = sys.excepthook
|
import signal
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
def _crash_guard(exc_type, exc_value, exc_tb):
|
def _handle_unlock(signum, frame):
|
||||||
logger.critical("Unhandled exception — unlocking session to prevent lockout", exc_info=(exc_type, exc_value, exc_tb))
|
"""SIGUSR1: External unlock request (e.g. from recovery wrapper)."""
|
||||||
if app._lock_instance:
|
logger.info("Received SIGUSR1 — unlocking session")
|
||||||
try:
|
GLib.idle_add(app._unlock)
|
||||||
app._lock_instance.unlock()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
||||||
|
|
||||||
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:
|
def main() -> None:
|
||||||
@ -167,7 +172,7 @@ def main() -> None:
|
|||||||
_setup_logging()
|
_setup_logging()
|
||||||
logger.info("Moonlock starting")
|
logger.info("Moonlock starting")
|
||||||
app = MoonlockApp()
|
app = MoonlockApp()
|
||||||
_install_crash_guard(app)
|
_install_signal_handlers(app)
|
||||||
app.run(sys.argv)
|
app.run(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -170,10 +170,10 @@ class TestDefensiveWindowCreation:
|
|||||||
@patch("moonlock.main.FingerprintListener")
|
@patch("moonlock.main.FingerprintListener")
|
||||||
@patch("moonlock.main.Gdk.Display.get_default")
|
@patch("moonlock.main.Gdk.Display.get_default")
|
||||||
@patch("moonlock.main.Gtk4SessionLock")
|
@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
|
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()
|
MoonlockApp, _ = _import_main()
|
||||||
app = MoonlockApp.__new__(MoonlockApp)
|
app = MoonlockApp.__new__(MoonlockApp)
|
||||||
app._config = MagicMock()
|
app._config = MagicMock()
|
||||||
@ -195,48 +195,43 @@ class TestDefensiveWindowCreation:
|
|||||||
with patch("moonlock.main.logger"):
|
with patch("moonlock.main.logger"):
|
||||||
app._activate_with_session_lock()
|
app._activate_with_session_lock()
|
||||||
|
|
||||||
# Session should have been unlocked to prevent permanent lockout
|
# Session must NOT be unlocked — compositor keeps screen locked
|
||||||
lock_instance.unlock.assert_called_once()
|
lock_instance.unlock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TestCrashGuard:
|
class TestSignalHandlers:
|
||||||
"""Tests for the global exception handler that prevents lockout on crash."""
|
"""Tests for signal handlers and excepthook behavior."""
|
||||||
|
|
||||||
def test_crash_guard_unlocks_session_on_unhandled_exception(self):
|
def test_sigusr1_triggers_unlock(self):
|
||||||
"""sys.excepthook should unlock the session when an exception reaches it."""
|
"""SIGUSR1 should schedule an unlock via GLib.idle_add."""
|
||||||
MoonlockApp, _ = _import_main()
|
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 = MoonlockApp.__new__(MoonlockApp)
|
||||||
app._lock_instance = MagicMock()
|
app._lock_instance = MagicMock()
|
||||||
|
|
||||||
original_hook = sys.excepthook
|
original_hook = sys.excepthook
|
||||||
_install_crash_guard(app)
|
_install_signal_handlers(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)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with patch("moonlock.main.logger"):
|
with patch("moonlock.main.logger"):
|
||||||
# Should not raise despite unlock failure
|
|
||||||
sys.excepthook(RuntimeError, RuntimeError("crash"), None)
|
sys.excepthook(RuntimeError, RuntimeError("crash"), None)
|
||||||
|
|
||||||
|
# Must NOT have called unlock
|
||||||
|
app._lock_instance.unlock.assert_not_called()
|
||||||
finally:
|
finally:
|
||||||
sys.excepthook = original_hook
|
sys.excepthook = original_hook
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user