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):
"""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_ = [
("resp", c_char_p),
("resp", c_void_p),
("resp_retcode", c_int),
]
@ -56,6 +58,19 @@ def _get_libpam() -> ctypes.CDLL:
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:
"""Create a PAM conversation callback that provides the password."""
@ -65,13 +80,19 @@ def _make_conv_func(password: str) -> PamConvFunc:
resp: POINTER(POINTER(PamResponse)),
appdata_ptr: c_void_p,
) -> int:
# Allocate response array
response = (PamResponse * num_msg)()
# PAM expects malloc'd memory — it will free() the responses and resp strings
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):
response[i].resp = password.encode("utf-8")
response[i].resp_retcode = 0
# PAM expects malloc'd memory; ctypes handles this via the array
resp[0] = ctypes.cast(response, POINTER(PamResponse))
# strdup allocates with malloc, which PAM can safely free()
resp_ptr[i].resp = libc.strdup(password.encode("utf-8"))
resp_ptr[i].resp_retcode = 0
resp[0] = resp_ptr
return PAM_SUCCESS
return PamConvFunc(_conv)

View File

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