fix: Audit-Findings — Login-Thread, Traversable-Assets, Session-Exec

- Login-IPC in Background-Thread ausgelagert, UI friert nicht mehr ein
  bei langsamem PAM/greetd (PERF-2/BUG-4)
- importlib.resources korrekt via as_file()/read_text() statt
  Path(str()) — funktioniert in ZIP-Wheels (BUG-1)
- is_absolute()-Check für Session-Exec entfernt, greetd löst PATH
  selbst auf — relative Executables wie 'sway' blockieren nicht mehr (LOGIC-1)
- ValueError (json.JSONDecodeError) im except abgefangen (BUG-2)
This commit is contained in:
nevaforget 2026-03-26 12:34:19 +01:00
parent 9a964aaecb
commit 65d3ba64f9

View File

@ -6,7 +6,8 @@ import shlex
import socket
import stat
import subprocess
from importlib.resources import files
import threading
from importlib.resources import as_file, files
from pathlib import Path
import gi
@ -72,7 +73,9 @@ class GreeterWindow(Gtk.ApplicationWindow):
# Background wallpaper (blurred and darkened)
bg_path = self._config.background
if not bg_path or not bg_path.exists():
bg_path = Path(str(DEFAULT_WALLPAPER_PATH))
# Extract package wallpaper to a real filesystem path (works in ZIP wheels too)
self._wallpaper_ctx = as_file(DEFAULT_WALLPAPER_PATH)
bg_path = self._wallpaper_ctx.__enter__()
if bg_path.exists():
background = Gtk.Picture()
background.set_filename(str(bg_path))
@ -249,10 +252,10 @@ class GreeterWindow(Gtk.ApplicationWindow):
)
if avatar_path and avatar_path.exists():
self._set_avatar_from_file(avatar_path, user.username)
elif DEFAULT_AVATAR_PATH.exists():
self._set_default_avatar()
else:
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
# Default avatar — _set_default_avatar uses Traversable.read_text()
# which works in ZIP wheels too, no exists() check needed
self._set_default_avatar()
# Apply user's GTK theme if available
self._apply_user_theme(user)
@ -379,6 +382,11 @@ class GreeterWindow(Gtk.ApplicationWindow):
pass
self._greetd_sock = None
def _set_login_sensitive(self, sensitive: bool) -> None:
"""Enable or disable login controls during authentication."""
self._password_entry.set_sensitive(sensitive)
self._session_dropdown.set_sensitive(sensitive)
def _attempt_login(self, user: User, password: str, session: Session) -> None:
"""Attempt to authenticate and start a session via greetd IPC."""
sock_path = os.environ.get("GREETD_SOCK")
@ -389,6 +397,17 @@ class GreeterWindow(Gtk.ApplicationWindow):
if not self._validate_greetd_sock(sock_path):
return
# Disable UI while authenticating — the IPC runs in a background thread
self._set_login_sensitive(False)
thread = threading.Thread(
target=self._login_worker,
args=(user, password, session, sock_path),
daemon=True,
)
thread.start()
def _login_worker(self, user: User, password: str, session: Session, sock_path: str) -> None:
"""Run greetd IPC in a background thread to avoid blocking the GTK main loop."""
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(10.0)
@ -399,8 +418,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
response = create_session(sock, user.username)
if response.get("type") == "error":
self._show_greetd_error(response, self._strings.auth_failed)
self._close_greetd_sock()
GLib.idle_add(self._on_login_error, response, self._strings.auth_failed)
return
# Step 2: Send password if auth message received
@ -409,47 +427,59 @@ class GreeterWindow(Gtk.ApplicationWindow):
if response.get("type") == "error":
self._failed_attempts[user.username] = self._failed_attempts.get(user.username, 0) + 1
self._show_greetd_error(response, self._strings.wrong_password)
warning = faillock_warning(self._failed_attempts[user.username], self._strings)
if warning:
current = self._error_label.get_text()
self._error_label.set_text(f"{current}\n{warning}")
self._close_greetd_sock()
GLib.idle_add(self._on_login_auth_error, response, warning)
return
if response.get("type") == "auth_message":
# Multi-stage auth (e.g. TOTP) is not supported
cancel_session(sock)
self._close_greetd_sock()
self._show_error(self._strings.multi_stage_unsupported)
GLib.idle_add(self._on_login_error, None, self._strings.multi_stage_unsupported)
return
# Step 3: Start session
if response.get("type") == "success":
cmd = shlex.split(session.exec_cmd)
if not cmd or not Path(cmd[0]).is_absolute():
self._show_error(self._strings.invalid_session_command)
if not cmd:
cancel_session(sock)
self._close_greetd_sock()
GLib.idle_add(self._on_login_error, None, self._strings.invalid_session_command)
return
response = start_session(sock, cmd)
if response.get("type") == "success":
self._save_last_user(user.username)
self._close_greetd_sock()
self.get_application().quit()
GLib.idle_add(self.get_application().quit)
return
else:
self._show_greetd_error(response, self._strings.session_start_failed)
GLib.idle_add(self._on_login_error, response, self._strings.session_start_failed)
self._close_greetd_sock()
except ConnectionError as e:
self._close_greetd_sock()
self._show_error(self._strings.connection_error.format(error=e))
except OSError as e:
GLib.idle_add(self._on_login_error, None, self._strings.connection_error.format(error=e))
except (OSError, ValueError) as e:
self._close_greetd_sock()
self._show_error(self._strings.socket_error.format(error=e))
GLib.idle_add(self._on_login_error, None, self._strings.socket_error.format(error=e))
def _on_login_error(self, response: dict | None, message: str) -> None:
"""Handle login error on the GTK main thread."""
if response:
self._show_greetd_error(response, message)
else:
self._show_error(message)
self._close_greetd_sock()
self._set_login_sensitive(True)
def _on_login_auth_error(self, response: dict, warning: str | None) -> None:
"""Handle authentication failure with optional faillock warning on the GTK main thread."""
self._show_greetd_error(response, self._strings.wrong_password)
if warning:
current = self._error_label.get_text()
self._error_label.set_text(f"{current}\n{warning}")
self._close_greetd_sock()
self._set_login_sensitive(True)
def _cancel_pending_session(self) -> None:
"""Cancel any in-progress greetd session."""