Compare commits

...
10 Commits
Author SHA1 Message Date
Vincent Huang 6f9479c3d5 synaptics: Add new PID 0x10b 2026-09-02 15:37:34 +08:00
Luan NguyenandMarco Trevisan 1f335cd58a goodixmoc: Update autosuspend hwdb for 27c6:6082 2026-09-01 08:24:38 +00:00
Luan NguyenandMarco Trevisan 2373a8f208 goodixmoc: Add support for Goodix 27c6:6082 2026-09-01 08:24:38 +00:00
Ge-org Brohammer dd0f15bf02 tests/realtek: Cover duplicate enrollment errors
Extend the 2541:fa03 storage-error fixture to enroll the same finger
twice and assert FP_DEVICE_ERROR_DATA_DUPLICATE, reusing the device
session the delete test already opened rather than starting a second
one. Compact redundant polling transactions while preserving the
recorded protocol state changes.
2026-09-01 09:55:22 +02:00
Ge-org Brohammer 629860c965 realtek: Report duplicate enrollment as DATA_DUPLICATE
When the sensor rejects an enrollment because the finger is already
present in on-chip storage, fp_check_duplicate_cb() raises
FP_DEVICE_ERROR_PROTO. That error class means "protocol error with the
device", which is not what happened; the device answered correctly.

The practical effect is that fprintd cannot classify the failure.
It already maps FP_DEVICE_ERROR_DATA_DUPLICATE to its "enroll-duplicate"
result, so with PROTO the user is shown "enroll-unknown-error" instead of
being told to try a different finger.

Use FP_DEVICE_ERROR_DATA_DUPLICATE, which exists for exactly this case.

Reproduced on a Realtek 2541:fa03 (Minisforum AI X1 Pro) by enrolling the
same finger twice:

  [realtek] SSM Enroll failed in state 5 with error:
            Current fingerprint is duplicate!
  Device reported enroll completion (print: (nil),
            error: [FP_DEVICE_ERROR_PROTO] Current fingerprint is duplicate!)
  fprintd: enroll_cb: result enroll-unknown-error
2026-08-31 17:46:55 +00:00
SbenazarandMarco Trevisan 3f1e2817ed tests/build: Iterate drivers_tests as a dict when skipping driver tests
bf91b0ed ("tests/build: Allow to define per-test parameters via a dict")
turned drivers_tests into a dict and updated the loop that defines the real
tests, but the fallback that only adds the skipping ones still asks for a
single iteration variable. Configuring with -Dintrospection=false stops right
there:

  tests/meson.build:295:25: ERROR: Foreach expects exactly 2 variables for
  iterating over objects of type dict

Hit it with meson 1.11 while building a single driver without introspection.
2026-08-29 23:01:47 +00:00
Ge-org Brohammer 3a41fa7ccc tests/realtek: Cover the storage error path that returned a freed GError
Deleting a print whose template is not in the sensor's storage makes the
driver fail its task SSM, which is the path that handed an already-freed
GError to fpi_device_delete_complete(). Reading the reported error is
therefore what the test is for.

No enrollment is involved: the test deserializes a stored realtek print
whose template is deliberately not on the device, so the recording needs
no finger presses and the capture stays at 90 packets.

Without the previous commit the test dies rather than fails:

  umockdev-run ... died with <Signals.SIGSEGV: 11>
  1/1 drivers+custom - libfprint:realtek-storage-errors FAIL

The duplicate-enrollment path reaches the same bug through
fp_enroll_ssm_done(), but capturing it needs two full enrollments and
some 1.07M packets of polling traffic, so it is left out here.
2026-08-28 15:19:30 +02:00
Ge-org Brohammer 26d38779e4 realtek: Fix use-after-free of the SSM error in completion handlers
fp_verify_ssm_done(), fp_enroll_ssm_done(), fp_init_ssm_done() and
fp_delete_ssm_done() each overwrite the GError they are given:

  if (fpi_ssm_get_error (ssm))
    error = fpi_ssm_get_error (ssm);

An SSM completion callback is handed an owned copy of the error --
fpi-ssm.c takes a g_error_copy() before invoking the callback -- whereas
fpi_ssm_get_error() is documented as (transfer none), and the machine's
own error is freed by the fpi_ssm_free() that immediately follows the
callback.

Since every fpi_device_*_complete() takes the error as (transfer full),
these handlers leak the copy they own and hand the consumer a pointer
that is freed moments later. The consumer is then left reading a
dangling GError. For fprintd that is fatal: it logs error->message and
dies with a general protection fault inside strlen(), taking the
session's authentication daemon with it.

  kernel: traps: fprintd[67901] general protection fault
                 ip:7fcef3b6429c sp:7ffd5f4128a8 error:0 in libc.so.6

  #0  __strlen_evex ()
  #3  g_vasprintf () at ../glib/gprintf.c:342
  #5  g_strdup_vprintf () at ../glib/gstrfuncs.c:515
  #9  delete_enrolled_fingers (user=... "ge-org",
                               finger=FP_FINGER_RIGHT_INDEX)
        at ../src/device.c:2420
        local_error = 0x563c2628bcb0

The assignment is redundant even where it is not harmful, because the
error passed to the callback is already a copy of the machine's error;
using it directly is both correct and sufficient. fpi_ssm_dup_error()
is available for callers that do need an owned copy. No other driver in
the tree assigns the borrowed SSM error this way.

Reproduced on a Realtek 2541:fa03 (Minisforum AI X1 Pro 470) by
enrolling a finger while a template was already present in on-chip
storage, which takes the delete path shown above. The same driver bug
reaches fprintd a second way, via fp_enroll_ssm_done() ->
fpi_device_enroll_complete() -> fprintd's enroll_cb().
2026-08-28 13:30:33 +02:00
jialei 83346fc8bc goodixmoc:Add pid 0x65BA, 0x6515, 0x66AC, 0x660A 2026-08-17 18:23:35 +08:00
Ken c4654fdc85 elanmoc: Add new PID 0xCB6 2026-07-28 11:28:20 +08:00
9 changed files with 453 additions and 14 deletions
+7
View File
@@ -176,6 +176,7 @@ usb:v04F3p0CA7*
usb:v04F3p0CA8*
usb:v04F3p0CB0*
usb:v04F3p0CB2*
usb:v04F3p0CB6*
ID_AUTOSUSPEND=1
ID_PERSIST=0
@@ -218,6 +219,7 @@ usb:v10A5p9B24*
# Supported by libfprint driver goodixmoc
usb:v27C6p5840*
usb:v27C6p6014*
usb:v27C6p6082*
usb:v27C6p6090*
usb:v27C6p6092*
usb:v27C6p6094*
@@ -253,6 +255,10 @@ usb:v27C6p6890*
usb:v27C6p689A*
usb:v27C6p66A9*
usb:v27C6p6984*
usb:v27C6p65BA*
usb:v27C6p6515*
usb:v27C6p66AC*
usb:v27C6p660A*
ID_AUTOSUSPEND=1
ID_PERSIST=0
@@ -298,6 +304,7 @@ usb:v06CBp0107*
usb:v06CBp0108*
usb:v06CBp0109*
usb:v06CBp010A*
usb:v06CBp010B*
usb:v06CBp010D*
usb:v06CBp010E*
usb:v06CBp0123*
+1
View File
@@ -41,6 +41,7 @@ static const FpIdEntry id_table[] = {
{ .vid = 0x04f3, .pid = 0x0ca8, },
{ .vid = 0x04f3, .pid = 0x0cb0, },
{ .vid = 0x04f3, .pid = 0x0cb2, },
{ .vid = 0x04f3, .pid = 0x0cb6, },
{ .vid = 0, .pid = 0, .driver_data = 0 }, /* terminating entry */
};
+10
View File
@@ -1423,9 +1423,14 @@ gx_fp_probe (FpDevice *device)
case 0x659A:
case 0x6890:
case 0x6984:
case 0x65BA:
self->max_enroll_stage = 12;
break;
case 0x6515:
self->max_enroll_stage = 17;
break;
default:
self->max_enroll_stage = DEFAULT_ENROLL_SAMPLES;
break;
@@ -1653,6 +1658,7 @@ fpi_device_goodixmoc_init (FpiDeviceGoodixMoc *self)
static const FpIdEntry id_table[] = {
{ .vid = 0x27c6, .pid = 0x5840, },
{ .vid = 0x27c6, .pid = 0x6014, },
{ .vid = 0x27c6, .pid = 0x6082, },
{ .vid = 0x27c6, .pid = 0x6090, },
{ .vid = 0x27c6, .pid = 0x6092, },
{ .vid = 0x27c6, .pid = 0x6094, },
@@ -1688,6 +1694,10 @@ static const FpIdEntry id_table[] = {
{ .vid = 0x27c6, .pid = 0x689A, },
{ .vid = 0x27c6, .pid = 0x66A9, },
{ .vid = 0x27c6, .pid = 0x6984, },
{ .vid = 0x27c6, .pid = 0x65BA, },
{ .vid = 0x27c6, .pid = 0x6515, },
{ .vid = 0x27c6, .pid = 0x66AC, },
{ .vid = 0x27c6, .pid = 0x660A, },
{ .vid = 0, .pid = 0, .driver_data = 0 }, /* terminating entry */
};
+1 -13
View File
@@ -516,7 +516,7 @@ fp_check_duplicate_cb (FpiDeviceRealtek *self,
if (in_status == FP_RTK_SUCCESS)
{
fpi_ssm_mark_failed (self->task_ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_DUPLICATE,
"Current fingerprint is duplicate!"));
}
else if (in_status == FP_RTK_MATCH_FAIL)
@@ -909,9 +909,6 @@ fp_verify_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error)
fp_info ("Verify complete!");
if (fpi_ssm_get_error (ssm))
error = fpi_ssm_get_error (ssm);
if (error && error->domain == FP_DEVICE_RETRY)
{
if (fpi_device_get_current_action (dev) == FPI_DEVICE_ACTION_VERIFY)
@@ -936,9 +933,6 @@ fp_enroll_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error)
fp_info ("Enrollment complete!");
if (fpi_ssm_get_error (ssm))
error = fpi_ssm_get_error (ssm);
if (error)
{
fpi_device_enroll_complete (dev, NULL, error);
@@ -958,9 +952,6 @@ fp_init_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error)
fp_info ("Init complete!");
if (fpi_ssm_get_error (ssm))
error = fpi_ssm_get_error (ssm);
fpi_device_open_complete (dev, error);
self->task_ssm = NULL;
}
@@ -972,9 +963,6 @@ fp_delete_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error)
fp_info ("Delete print complete!");
if (fpi_ssm_get_error (ssm))
error = fpi_ssm_get_error (ssm);
fpi_device_delete_complete (dev, error);
self->task_ssm = NULL;
}
+1
View File
@@ -48,6 +48,7 @@ static const FpIdEntry id_table[] = {
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x0108, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x0109, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x010A, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x010B, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x010D, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x010E, },
{ .vid = SYNAPTICS_VENDOR_ID, .pid = 0x0123, },
+2 -1
View File
@@ -59,6 +59,7 @@ drivers_tests = {
'fpcmoc': {},
'realtek': {},
'realtek-5816': {},
'realtek-storage-errors': {},
'focaltech_moc': { 'timeout': 30 },
'focaltech_moc-6553': {},
'mafpmoc': {},
@@ -292,7 +293,7 @@ else
args: ['-c', 'exit 77']
)
foreach driver_test: drivers_tests
foreach driver_test, _args : drivers_tests
test(driver_test,
sh,
args: ['-c', 'exit 77']
Binary file not shown.
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
# Deleting a print that is not in the sensor's storage makes the realtek driver
# fail its task SSM, which is the path that used to hand an already-freed GError
# to fpi_device_delete_complete(). Reading the reported error is therefore the
# point of this test: before the fix it read freed memory, and the GError was
# freed a second time when the GTask finalized.
#
# The print below is a stored realtek print, serialized, whose template is
# deliberately not on the device.
#
# The same session then enrolls a finger and enrolls it again, checking that the
# duplicate is reported as FP_DEVICE_ERROR_DATA_DUPLICATE rather than PROTO.
import base64
import traceback
import sys
import gi
gi.require_version('FPrint', '2.0')
from gi.repository import FPrint, GLib
# Exit with error on any exception, included those happening in async callbacks
sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1))
# A stored print for user 'testuser', right index finger, user id
# 'FP1-20260101-7-DEADBEEF-test'.
STORED_PRINT = base64.b64decode(
'RlAzAQAAAHJlYWx0ZWsAMAABB3Rlc3R1c2VyAABGUDEtMjAyNjAxMDEtNy1ERUFEQkVFRi10ZXN0AAAhSQsAAAAAAP9GUDEtMjAyNjAxMDEtNy1ERUFEQkVFRi10ZXN0ACh5YXkpAHZAOBoODA==')
ctx = GLib.main_context_default()
c = FPrint.Context()
c.enumerate()
devices = c.get_devices()
d = devices[0]
del devices
assert d.get_driver() == "realtek"
assert d.has_feature(FPrint.DeviceFeature.STORAGE)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
d.open_sync()
# Make sure the device holds no templates, so the print below cannot match.
d.clear_storage_sync()
p = FPrint.Print.deserialize(STORED_PRINT)
assert p.get_driver() == "realtek"
assert p.get_device_stored()
assert p.get_finger() == FPrint.Finger.RIGHT_INDEX
print("deleting a print that is not in the device storage")
try:
d.delete_print_sync(p)
except GLib.Error as error:
print("delete reported: {}".format(error.message))
assert error.matches(FPrint.DeviceError.quark(), FPrint.DeviceError.PROTO)
assert error.message
else:
assert False, "deleting a template that is not stored should fail"
del p
print("delete attempt done")
# Storage is still empty, so enrolling the same finger twice must report the
# error class expected by fprintd.
def enroll_progress(*args):
print('enroll progress: ' + str(args))
template = FPrint.Print.new(d)
print("enrolling")
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
p = d.enroll_sync(template, None, enroll_progress, None)
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
print("enroll done")
print("enrolling duplicate")
template = FPrint.Print.new(d)
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
try:
d.enroll_sync(template, None, enroll_progress, None)
except GLib.Error as error:
assert error.matches(FPrint.DeviceError.quark(),
FPrint.DeviceError.DATA_DUPLICATE)
else:
raise AssertionError("Duplicate enrollment unexpectedly succeeded")
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
print("duplicate enrollment rejected")
d.delete_print_sync(p)
d.close_sync()
del d
del c
File diff suppressed because one or more lines are too long