Fix PAM conversation segfault causing unrecoverable red lockscreen

ctypes auto-converts c_char_p return values to Python bytes, losing
the original malloc'd pointer from strdup(). When PAM called free()
on the response, it hit a ctypes-internal buffer instead — segfault.
Use c_void_p for PamResponse.resp and strdup restype to preserve raw
pointers. Also use calloc/strdup for proper malloc'd memory that PAM
can safely free().

Add try/except in auth thread so UI stays interactive on PAM errors.
This commit is contained in:
nevaforget 2026-03-26 12:43:53 +01:00
parent db05df36d4
commit d0d390d0cb
2 changed files with 32 additions and 8 deletions

View File

@ -23,8 +23,10 @@ class PamMessage(Structure):
class PamResponse(Structure): class PamResponse(Structure):
"""PAM response structure (pam_response).""" """PAM response structure (pam_response)."""
# resp is c_void_p (not c_char_p) to preserve raw malloc'd pointers —
# ctypes auto-converts c_char_p returns to Python bytes, losing the pointer.
_fields_ = [ _fields_ = [
("resp", c_char_p), ("resp", c_void_p),
("resp_retcode", c_int), ("resp_retcode", c_int),
] ]
@ -56,6 +58,19 @@ def _get_libpam() -> ctypes.CDLL:
return ctypes.CDLL(pam_path) return ctypes.CDLL(pam_path)
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
def _make_conv_func(password: str) -> PamConvFunc: def _make_conv_func(password: str) -> PamConvFunc:
"""Create a PAM conversation callback that provides the password.""" """Create a PAM conversation callback that provides the password."""
@ -65,13 +80,19 @@ def _make_conv_func(password: str) -> PamConvFunc:
resp: POINTER(POINTER(PamResponse)), resp: POINTER(POINTER(PamResponse)),
appdata_ptr: c_void_p, appdata_ptr: c_void_p,
) -> int: ) -> int:
# Allocate response array # PAM expects malloc'd memory — it will free() the responses and resp strings
response = (PamResponse * num_msg)() libc = _get_libc()
resp_array = libc.calloc(num_msg, ctypes.sizeof(PamResponse))
if not resp_array:
return PAM_AUTH_ERR
resp_ptr = ctypes.cast(resp_array, POINTER(PamResponse))
for i in range(num_msg): for i in range(num_msg):
response[i].resp = password.encode("utf-8") # strdup allocates with malloc, which PAM can safely free()
response[i].resp_retcode = 0 resp_ptr[i].resp = libc.strdup(password.encode("utf-8"))
# PAM expects malloc'd memory; ctypes handles this via the array resp_ptr[i].resp_retcode = 0
resp[0] = ctypes.cast(response, POINTER(PamResponse))
resp[0] = resp_ptr
return PAM_SUCCESS return PAM_SUCCESS
return PamConvFunc(_conv) return PamConvFunc(_conv)

View File

@ -181,7 +181,10 @@ class LockscreenWindow(Gtk.ApplicationWindow):
# Use GLib thread pool to avoid blocking GTK mainloop # Use GLib thread pool to avoid blocking GTK mainloop
def _auth_thread() -> bool: def _auth_thread() -> bool:
try:
result = _do_auth() result = _do_auth()
except Exception:
result = False
GLib.idle_add(_on_auth_done, result) GLib.idle_add(_on_auth_done, result)
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE