Harden auth, user detection, faillock, and LD_PRELOAD handling
- Replace os.getlogin() with pwd.getpwuid(os.getuid()) to prevent crashes in systemd/display-manager sessions without a controlling tty - Cache libpam and libc at module level instead of calling find_library() on every auth attempt (spawned ldconfig subprocess each time) - Disable password entry permanently after FAILLOCK_MAX_ATTEMPTS instead of just showing a warning while allowing unlimited retries - Fix LD_PRELOAD logic to append gtk4-layer-shell instead of skipping when LD_PRELOAD is already set (caused silent session lock fallback) - Ensure password entry keeps focus after errors and escape
This commit is contained in:
parent
dd9937b020
commit
e7ab4c2e73
@ -50,25 +50,35 @@ class PamConv(Structure):
|
||||
]
|
||||
|
||||
|
||||
_cached_libpam: ctypes.CDLL | None = None
|
||||
_cached_libc: ctypes.CDLL | None = None
|
||||
|
||||
|
||||
def _get_libpam() -> ctypes.CDLL:
|
||||
"""Load and return the libpam shared library."""
|
||||
pam_path = ctypes.util.find_library("pam")
|
||||
if not pam_path:
|
||||
raise OSError("libpam not found")
|
||||
return ctypes.CDLL(pam_path)
|
||||
"""Load and return the libpam shared library (cached after first call)."""
|
||||
global _cached_libpam
|
||||
if _cached_libpam is None:
|
||||
pam_path = ctypes.util.find_library("pam")
|
||||
if not pam_path:
|
||||
raise OSError("libpam not found")
|
||||
_cached_libpam = ctypes.CDLL(pam_path)
|
||||
return _cached_libpam
|
||||
|
||||
|
||||
def _get_libc() -> ctypes.CDLL:
|
||||
"""Load and return the libc shared library."""
|
||||
libc_path = ctypes.util.find_library("c")
|
||||
if not libc_path:
|
||||
raise OSError("libc not found")
|
||||
libc = ctypes.CDLL(libc_path)
|
||||
libc.calloc.restype = c_void_p
|
||||
libc.calloc.argtypes = [ctypes.c_size_t, ctypes.c_size_t]
|
||||
libc.strdup.restype = c_void_p
|
||||
libc.strdup.argtypes = [c_char_p]
|
||||
return libc
|
||||
"""Load and return the libc shared library (cached after first call)."""
|
||||
global _cached_libc
|
||||
if _cached_libc is None:
|
||||
libc_path = ctypes.util.find_library("c")
|
||||
if not libc_path:
|
||||
raise OSError("libc not found")
|
||||
libc = ctypes.CDLL(libc_path)
|
||||
libc.calloc.restype = c_void_p
|
||||
libc.calloc.argtypes = [ctypes.c_size_t, ctypes.c_size_t]
|
||||
libc.strdup.restype = c_void_p
|
||||
libc.strdup.argtypes = [c_char_p]
|
||||
_cached_libc = libc
|
||||
return _cached_libc
|
||||
|
||||
|
||||
def _make_conv_func(password: str) -> PamConvFunc:
|
||||
|
||||
@ -42,6 +42,7 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
|
||||
self._build_ui()
|
||||
self._setup_keyboard()
|
||||
self._password_entry.grab_focus()
|
||||
|
||||
# Start fingerprint listener if available
|
||||
if self._fp_available:
|
||||
@ -157,6 +158,7 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
self._password_entry.set_text("")
|
||||
self._error_label.set_visible(False)
|
||||
self._password_entry.grab_focus()
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -173,20 +175,27 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
return authenticate(self._user.username, password)
|
||||
|
||||
def _on_auth_done(result: bool) -> None:
|
||||
entry.set_sensitive(True)
|
||||
if result:
|
||||
self._unlock()
|
||||
return
|
||||
|
||||
self._failed_attempts += 1
|
||||
entry.set_text("")
|
||||
|
||||
if self._failed_attempts >= FAILLOCK_MAX_ATTEMPTS:
|
||||
# Permanently disable entry after max failed attempts
|
||||
self._show_error(self._strings.faillock_locked)
|
||||
entry.set_sensitive(False)
|
||||
else:
|
||||
self._failed_attempts += 1
|
||||
self._show_error(self._strings.wrong_password)
|
||||
if self._failed_attempts >= FAILLOCK_MAX_ATTEMPTS:
|
||||
self._show_error(self._strings.faillock_locked)
|
||||
elif self._failed_attempts >= FAILLOCK_MAX_ATTEMPTS - 1:
|
||||
entry.set_sensitive(True)
|
||||
entry.grab_focus()
|
||||
if self._failed_attempts >= FAILLOCK_MAX_ATTEMPTS - 1:
|
||||
remaining = FAILLOCK_MAX_ATTEMPTS - self._failed_attempts
|
||||
self._show_error(
|
||||
self._strings.faillock_attempts_remaining.format(n=remaining)
|
||||
)
|
||||
entry.set_text("")
|
||||
else:
|
||||
self._show_error(self._strings.wrong_password)
|
||||
|
||||
# Use GLib thread pool to avoid blocking GTK mainloop
|
||||
def _auth_thread() -> bool:
|
||||
|
||||
@ -7,8 +7,9 @@ from importlib.resources import files
|
||||
|
||||
# gtk4-layer-shell must be loaded before libwayland-client
|
||||
_LAYER_SHELL_LIB = "/usr/lib/libgtk4-layer-shell.so"
|
||||
if "LD_PRELOAD" not in os.environ and os.path.exists(_LAYER_SHELL_LIB):
|
||||
os.environ["LD_PRELOAD"] = _LAYER_SHELL_LIB
|
||||
_existing_preload = os.environ.get("LD_PRELOAD", "")
|
||||
if _LAYER_SHELL_LIB not in _existing_preload and os.path.exists(_LAYER_SHELL_LIB):
|
||||
os.environ["LD_PRELOAD"] = f"{_existing_preload}:{_LAYER_SHELL_LIB}".lstrip(":")
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "moonlock.main"] + sys.argv[1:])
|
||||
|
||||
import gi
|
||||
|
||||
@ -22,14 +22,15 @@ class User:
|
||||
|
||||
def get_current_user() -> User:
|
||||
"""Get the currently logged-in user's info from the system."""
|
||||
username = os.getlogin()
|
||||
pw = pwd.getpwnam(username)
|
||||
# Use getuid() instead of getlogin() — getlogin() fails without a controlling
|
||||
# terminal (systemd units, display-manager-started sessions).
|
||||
pw = pwd.getpwuid(os.getuid())
|
||||
|
||||
gecos = pw.pw_gecos
|
||||
# GECOS field may contain comma-separated values; first field is the full name
|
||||
display_name = gecos.split(",")[0] if gecos else username
|
||||
display_name = gecos.split(",")[0] if gecos else pw.pw_name
|
||||
if not display_name:
|
||||
display_name = username
|
||||
display_name = pw.pw_name
|
||||
|
||||
return User(
|
||||
username=pw.pw_name,
|
||||
|
||||
@ -11,9 +11,9 @@ from moonlock.users import get_current_user, get_avatar_path, get_default_avatar
|
||||
class TestGetCurrentUser:
|
||||
"""Tests for current user detection."""
|
||||
|
||||
@patch("moonlock.users.os.getlogin", return_value="testuser")
|
||||
@patch("moonlock.users.pwd.getpwnam")
|
||||
def test_returns_user_with_correct_username(self, mock_pwd, mock_login):
|
||||
@patch("moonlock.users.os.getuid", return_value=1000)
|
||||
@patch("moonlock.users.pwd.getpwuid")
|
||||
def test_returns_user_with_correct_username(self, mock_pwd, mock_uid):
|
||||
mock_pwd.return_value.pw_name = "testuser"
|
||||
mock_pwd.return_value.pw_gecos = "Test User"
|
||||
mock_pwd.return_value.pw_dir = "/home/testuser"
|
||||
@ -22,10 +22,11 @@ class TestGetCurrentUser:
|
||||
assert user.username == "testuser"
|
||||
assert user.display_name == "Test User"
|
||||
assert user.home == Path("/home/testuser")
|
||||
mock_pwd.assert_called_once_with(1000)
|
||||
|
||||
@patch("moonlock.users.os.getlogin", return_value="testuser")
|
||||
@patch("moonlock.users.pwd.getpwnam")
|
||||
def test_empty_gecos_falls_back_to_username(self, mock_pwd, mock_login):
|
||||
@patch("moonlock.users.os.getuid", return_value=1000)
|
||||
@patch("moonlock.users.pwd.getpwuid")
|
||||
def test_empty_gecos_falls_back_to_username(self, mock_pwd, mock_uid):
|
||||
mock_pwd.return_value.pw_name = "testuser"
|
||||
mock_pwd.return_value.pw_gecos = ""
|
||||
mock_pwd.return_value.pw_dir = "/home/testuser"
|
||||
@ -33,9 +34,9 @@ class TestGetCurrentUser:
|
||||
user = get_current_user()
|
||||
assert user.display_name == "testuser"
|
||||
|
||||
@patch("moonlock.users.os.getlogin", return_value="testuser")
|
||||
@patch("moonlock.users.pwd.getpwnam")
|
||||
def test_gecos_with_commas_uses_first_field(self, mock_pwd, mock_login):
|
||||
@patch("moonlock.users.os.getuid", return_value=1000)
|
||||
@patch("moonlock.users.pwd.getpwuid")
|
||||
def test_gecos_with_commas_uses_first_field(self, mock_pwd, mock_uid):
|
||||
mock_pwd.return_value.pw_name = "testuser"
|
||||
mock_pwd.return_value.pw_gecos = "Test User,,,Room 42"
|
||||
mock_pwd.return_value.pw_dir = "/home/testuser"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user