/* * Regression tests for validity verify/identify/delete/clear operations. * * These tests cover issues found during the Iteration 6 code audit: * 1. parse_match_result dead while loop — TLV dict not iterated * 2. ENROLL_CREATE_USER NULL user_id — validity_db_build_identity(NULL) crash * 3. Identify always returns first gallery print — subtype not matched * 4. delete_storage_dbid field reused for enrollment — struct field abuse * 5. Delete SSM non-functional — del_record never called * 6. match_finger double allocation — 12 bytes freed then 13 allocated * 7. clear_storage returned NOT_SUPPORTED * 8. Stale TODOs * * Copyright (C) 2024 libfprint contributors * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. */ #include #include #include "fp-enums.h" #include "fpi-device.h" #include "fpi-byte-utils.h" #include "fp-print.h" #include "test-device-fake.h" #include "drivers/validity/validity.h" #include "drivers/validity/validity_db.h" #include "drivers/validity/validity_capture.h" #include "drivers/validity/vcsfw_protocol.h" /* ================================================================ * Helper: build a TLV dict entry tag(2LE) | len(2LE) | data[len] * Returns bytes written. * ================================================================ */ static gsize build_tlv_entry (guint8 *buf, guint16 tag, const guint8 *data, guint16 len) { FP_WRITE_UINT16_LE (&buf[0], tag); FP_WRITE_UINT16_LE (&buf[2], len); if (len > 0) memcpy (&buf[4], data, len); return 4 + len; } /* ================================================================ * Helper: Build a complete match result payload: * total_len(2LE) | TLV entries... * ================================================================ */ static guint8 * build_match_payload (guint32 user_dbid, guint16 subtype, const guint8 *hash, gsize hash_len, gsize *out_len) { /* Max size: 2 (total_len) + 3 entries × (4 header + max data) */ guint8 *buf = g_new0 (guint8, 256); gsize pos = 2; /* skip total_len placeholder */ /* Tag 1: user_dbid (4 bytes LE) */ guint8 dbid_data[4]; FP_WRITE_UINT32_LE (dbid_data, user_dbid); pos += build_tlv_entry (&buf[pos], 1, dbid_data, 4); /* Tag 3: subtype (2 bytes LE) */ guint8 sub_data[2]; FP_WRITE_UINT16_LE (sub_data, subtype); pos += build_tlv_entry (&buf[pos], 3, sub_data, 2); /* Tag 4: hash */ if (hash && hash_len > 0) pos += build_tlv_entry (&buf[pos], 4, hash, hash_len); /* Write total_len at offset 0 */ FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); *out_len = pos; return buf; } /* ================================================================ * R1: parse_match_result with valid TLV data * * Regression: Issue #1 — dead while loop would never extract fields. * Verifies that user_dbid, subtype, and hash are correctly parsed * from a TLV dictionary matching python-validity's parse_dict() format. * ================================================================ */ static void test_parse_match_result_valid (void) { guint8 hash[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; gsize payload_len; g_autofree guint8 *payload = build_match_payload ( 0x00001234, 3, hash, sizeof (hash), &payload_len); ValidityMatchResult result = { 0 }; gboolean ok = validity_parse_match_result (payload, payload_len, &result); g_assert_true (ok); g_assert_true (result.matched); g_assert_cmpuint (result.user_dbid, ==, 0x00001234); g_assert_cmpuint (result.subtype, ==, 3); g_assert_nonnull (result.hash); g_assert_cmpuint (result.hash_len, ==, sizeof (hash)); g_assert_cmpmem (result.hash, result.hash_len, hash, sizeof (hash)); validity_match_result_clear (&result); } /* ================================================================ * R1b: parse_match_result iterates ALL TLV entries * * Regression: The dead while loop would break after first entry. * Build a dict with tag 3 (subtype) BEFORE tag 1 (user_dbid) to * ensure the parser doesn't stop after the first entry. * ================================================================ */ static void test_parse_match_result_multi_tags (void) { /* Manually build: total_len(2) | tag3(2+2+2) | tag1(2+2+4) | tag4(2+2+3) */ guint8 buf[256]; gsize pos = 2; /* Tag 3 first: subtype = 7 */ guint8 sub[2]; FP_WRITE_UINT16_LE (sub, 7); pos += build_tlv_entry (&buf[pos], 3, sub, 2); /* Tag 1 second: user_dbid = 0xDEADBEEF */ guint8 dbid[4]; FP_WRITE_UINT32_LE (dbid, 0xDEADBEEF); pos += build_tlv_entry (&buf[pos], 1, dbid, 4); /* Tag 4 third: hash = {0x11, 0x22, 0x33} */ guint8 hash[] = { 0x11, 0x22, 0x33 }; pos += build_tlv_entry (&buf[pos], 4, hash, 3); FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); ValidityMatchResult result = { 0 }; gboolean ok = validity_parse_match_result (buf, pos, &result); g_assert_true (ok); g_assert_true (result.matched); g_assert_cmpuint (result.user_dbid, ==, 0xDEADBEEF); g_assert_cmpuint (result.subtype, ==, 7); g_assert_nonnull (result.hash); g_assert_cmpuint (result.hash_len, ==, 3); g_assert_cmpmem (result.hash, result.hash_len, hash, 3); validity_match_result_clear (&result); } /* ================================================================ * R1c: parse_match_result with empty dict (no match) * * Regression: A no-match scenario should return ok=TRUE but matched=FALSE. * ================================================================ */ static void test_parse_match_result_empty (void) { /* total_len = 0, no TLV entries */ guint8 buf[2] = { 0x00, 0x00 }; ValidityMatchResult result = { 0 }; gboolean ok = validity_parse_match_result (buf, sizeof (buf), &result); g_assert_true (ok); g_assert_false (result.matched); g_assert_cmpuint (result.user_dbid, ==, 0); g_assert_cmpuint (result.subtype, ==, 0); g_assert_null (result.hash); } /* ================================================================ * R1d: parse_match_result with truncated data * * Ensure graceful handling of malformed/truncated payloads. * ================================================================ */ static void test_parse_match_result_truncated (void) { /* Only 1 byte — too short for total_len */ guint8 buf1[1] = { 0x05 }; ValidityMatchResult result = { 0 }; g_assert_false (validity_parse_match_result (buf1, 1, &result)); /* total_len says 20 but only 6 bytes follow (partial TLV entry) */ guint8 buf2[8]; FP_WRITE_UINT16_LE (&buf2[0], 20); FP_WRITE_UINT16_LE (&buf2[2], 1); /* tag = 1 */ FP_WRITE_UINT16_LE (&buf2[4], 10); /* len = 10, but only 2 bytes remain */ buf2[6] = 0xFF; buf2[7] = 0xFF; memset (&result, 0, sizeof (result)); gboolean ok = validity_parse_match_result (buf2, sizeof (buf2), &result); /* Should return TRUE (parsing succeeded) but matched=FALSE (incomplete entry) */ g_assert_true (ok); g_assert_false (result.matched); } /* ================================================================ * R1e: parse_match_result ignores unknown tags * * Unknown tags should be skipped without error. * ================================================================ */ static void test_parse_match_result_unknown_tags (void) { guint8 buf[256]; gsize pos = 2; /* Unknown tag 99 with 2 bytes of data */ guint8 unk[] = { 0x42, 0x43 }; pos += build_tlv_entry (&buf[pos], 99, unk, 2); /* Tag 1: user_dbid = 0x0042 */ guint8 dbid[4]; FP_WRITE_UINT32_LE (dbid, 0x0042); pos += build_tlv_entry (&buf[pos], 1, dbid, 4); FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); ValidityMatchResult result = { 0 }; gboolean ok = validity_parse_match_result (buf, pos, &result); g_assert_true (ok); g_assert_true (result.matched); g_assert_cmpuint (result.user_dbid, ==, 0x0042); validity_match_result_clear (&result); } /* ================================================================ * R2: validity_db_build_identity rejects NULL * * Regression: Issue #2 — NULL user_id was passed to build_identity * which would then be passed to g_variant_new_string(NULL) → crash. * The guard should return NULL. * ================================================================ */ static void test_build_identity_null (void) { gsize len = 999; /* The function uses g_return_val_if_fail which emits g_critical. * With G_DEBUG=fatal-warnings the critical would be fatal, * so we must expect the message. */ g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_CRITICAL, "*assertion*uuid_str*failed*"); guint8 *id = validity_db_build_identity (NULL, &len); g_test_assert_expected_messages (); g_assert_null (id); } /* ================================================================ * R2b: validity_db_build_identity with valid UUID * * Regression: Ensures UUID → identity bytes works correctly * (complementary to the NULL test above). * ================================================================ */ static void test_build_identity_valid_uuid (void) { const gchar *uuid = "12345678-1234-5678-1234-567812345678"; gsize len; g_autofree guint8 *id = validity_db_build_identity (uuid, &len); g_assert_nonnull (id); g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); /* Type should be SID (3) */ g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); /* Length field should be UUID string length */ g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, strlen (uuid)); /* UUID payload should be present */ g_assert_cmpmem (&id[8], strlen (uuid), uuid, strlen (uuid)); } /* ================================================================ * R3: Gallery matching by subtype * * Regression: Issue #3 — identify always returned first gallery print * regardless of actual subtype. Now it should match by finger subtype. * ================================================================ */ static void test_gallery_match_by_subtype (void) { g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); /* Create 3 prints with fingers: LEFT_THUMB(1), LEFT_INDEX(2), RIGHT_MIDDLE(8) */ FpPrint *p1 = fp_print_new (device); fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); g_ptr_array_add (gallery, g_object_ref_sink (p1)); FpPrint *p2 = fp_print_new (device); fp_print_set_finger (p2, FP_FINGER_LEFT_INDEX); g_ptr_array_add (gallery, g_object_ref_sink (p2)); FpPrint *p3 = fp_print_new (device); fp_print_set_finger (p3, FP_FINGER_RIGHT_MIDDLE); g_ptr_array_add (gallery, g_object_ref_sink (p3)); /* Subtype 2 = LEFT_INDEX → should match p2, not p1 */ guint16 subtype_left_index = validity_finger_to_subtype (FP_FINGER_LEFT_INDEX); FpPrint *match = validity_find_gallery_match (gallery, subtype_left_index); g_assert_true (match == p2); /* Subtype 8 = RIGHT_MIDDLE → should match p3 */ guint16 subtype_right_middle = validity_finger_to_subtype (FP_FINGER_RIGHT_MIDDLE); match = validity_find_gallery_match (gallery, subtype_right_middle); g_assert_true (match == p3); /* Subtype 1 = LEFT_THUMB → should match p1 */ guint16 subtype_left_thumb = validity_finger_to_subtype (FP_FINGER_LEFT_THUMB); match = validity_find_gallery_match (gallery, subtype_left_thumb); g_assert_true (match == p1); } /* ================================================================ * R3b: Gallery match falls back to first when subtype doesn't match * * The sensor confirmed a match but the subtype can't be correlated * to any gallery entry — should fall back to first. * ================================================================ */ static void test_gallery_match_fallback (void) { g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); FpPrint *p1 = fp_print_new (device); fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); g_ptr_array_add (gallery, g_object_ref_sink (p1)); /* Subtype 9 = RIGHT_RING, not in gallery → should fall back to p1 */ guint16 subtype_right_ring = validity_finger_to_subtype (FP_FINGER_RIGHT_RING); FpPrint *match = validity_find_gallery_match (gallery, subtype_right_ring); g_assert_true (match == p1); } /* ================================================================ * R3c: Gallery match with NULL/empty gallery * * Should return NULL when gallery is empty or NULL. * ================================================================ */ static void test_gallery_match_empty (void) { g_autoptr(GPtrArray) empty = g_ptr_array_new_with_free_func (g_object_unref); g_assert_null (validity_find_gallery_match (NULL, 1)); g_assert_null (validity_find_gallery_match (empty, 1)); } /* ================================================================ * R4: enroll_user_dbid field exists separately from delete_storage_dbid * * Regression: Issue #4 — delete_storage_dbid was abused for enrollment. * This compile-time test verifies both fields exist independently. * ================================================================ */ static void test_struct_separate_fields (void) { /* Verify both fields exist and are at different offsets */ g_assert_cmpuint ( G_STRUCT_OFFSET (FpiDeviceValidity, enroll_user_dbid), !=, G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); /* Also verify delete_finger_subtype and delete_finger_dbid exist * (needed for the functional delete SSM) */ g_assert_cmpuint ( G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_subtype), !=, G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); g_assert_cmpuint ( G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_dbid), !=, G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); } /* ================================================================ * R5: del_record command format * * Regression: Issue #5 — delete SSM never sent del_record cmd. * Verify cmd 0x48 produces correct format: 0x48 | dbid(2LE). * (This already exists in test-validity-db.c but we double-check * the format critical for delete functionality.) * ================================================================ */ static void test_del_record_format (void) { gsize len; g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0x4321, &len); g_assert_nonnull (cmd); g_assert_cmpuint (len, ==, 3); g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x4321); } /* ================================================================ * R6: match_finger command is exactly 13 bytes (single allocation) * * Regression: Issue #6 — build_cmd_match_finger allocated 12 bytes, * freed, then re-allocated 13 bytes. Now single allocation. * ================================================================ */ static void test_match_finger_size (void) { gsize len; g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); g_assert_nonnull (cmd); g_assert_cmpuint (len, ==, 13); g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); g_assert_cmpuint (cmd[1], ==, 0x02); g_assert_cmpuint (cmd[2], ==, 0xFF); /* Verify all 5 uint16_le fields */ g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* stg_id */ g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); /* usr_id */ g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); } /* ================================================================ * R7: Clear storage SSM states exist * * Regression: Issue #7 — clear_storage was a stub returning NOT_SUPPORTED. * Verify the CLEAR_* enum states exist (compile-time regression test). * ================================================================ */ static void test_clear_storage_states_exist (void) { /* Verify clear SSM states exist and are ordered correctly */ g_assert_cmpint (CLEAR_GET_STORAGE, ==, 0); g_assert_cmpint (CLEAR_GET_STORAGE_RECV, ==, 1); g_assert_cmpint (CLEAR_DEL_USER, ==, 2); g_assert_cmpint (CLEAR_DEL_USER_RECV, ==, 3); g_assert_cmpint (CLEAR_DONE, ==, 4); g_assert_cmpint (CLEAR_NUM_STATES, ==, 5); } /* ================================================================ * R7b: Delete SSM states are complete * * Verify the delete SSM has all required states including * DEL_RECORD and DEL_RECORD_RECV (which were previously dead code). * ================================================================ */ static void test_delete_states_exist (void) { g_assert_cmpint (DELETE_GET_STORAGE, ==, 0); g_assert_cmpint (DELETE_GET_STORAGE_RECV, ==, 1); g_assert_cmpint (DELETE_LOOKUP_USER, ==, 2); g_assert_cmpint (DELETE_LOOKUP_USER_RECV, ==, 3); g_assert_cmpint (DELETE_DEL_RECORD, ==, 4); g_assert_cmpint (DELETE_DEL_RECORD_RECV, ==, 5); g_assert_cmpint (DELETE_DONE, ==, 6); g_assert_cmpint (DELETE_NUM_STATES, ==, 7); } /* ================================================================ * R1f: match_result_clear frees hash * * Ensure the clear function properly frees the hash allocation. * ================================================================ */ static void test_match_result_clear (void) { ValidityMatchResult result = { 0 }; result.matched = TRUE; result.user_dbid = 42; result.subtype = 5; result.hash = g_memdup2 ((guint8[]){0x01, 0x02}, 2); result.hash_len = 2; validity_match_result_clear (&result); g_assert_false (result.matched); g_assert_cmpuint (result.user_dbid, ==, 0); g_assert_cmpuint (result.subtype, ==, 0); g_assert_null (result.hash); g_assert_cmpuint (result.hash_len, ==, 0); } int main (int argc, char *argv[]) { g_test_init (&argc, &argv, NULL); /* R1: parse_match_result regression tests (Issue #1: dead while loop) */ g_test_add_func ("/validity/verify/parse_match_result_valid", test_parse_match_result_valid); g_test_add_func ("/validity/verify/parse_match_result_multi_tags", test_parse_match_result_multi_tags); g_test_add_func ("/validity/verify/parse_match_result_empty", test_parse_match_result_empty); g_test_add_func ("/validity/verify/parse_match_result_truncated", test_parse_match_result_truncated); g_test_add_func ("/validity/verify/parse_match_result_unknown_tags", test_parse_match_result_unknown_tags); g_test_add_func ("/validity/verify/match_result_clear", test_match_result_clear); /* R2: identity builder NULL regression (Issue #2: NULL crash) */ g_test_add_func ("/validity/verify/build_identity_null", test_build_identity_null); g_test_add_func ("/validity/verify/build_identity_valid_uuid", test_build_identity_valid_uuid); /* R3: gallery matching by subtype (Issue #3: always returned first) */ g_test_add_func ("/validity/verify/gallery_match_by_subtype", test_gallery_match_by_subtype); g_test_add_func ("/validity/verify/gallery_match_fallback", test_gallery_match_fallback); g_test_add_func ("/validity/verify/gallery_match_empty", test_gallery_match_empty); /* R4: struct field separation (Issue #4: field abuse) */ g_test_add_func ("/validity/verify/struct_separate_fields", test_struct_separate_fields); /* R5: del_record command format (Issue #5: delete SSM non-functional) */ g_test_add_func ("/validity/verify/del_record_format", test_del_record_format); /* R6: match_finger single allocation (Issue #6: double alloc) */ g_test_add_func ("/validity/verify/match_finger_size", test_match_finger_size); /* R7: clear/delete storage SSM states (Issue #7: stub) */ g_test_add_func ("/validity/verify/clear_storage_states", test_clear_storage_states_exist); g_test_add_func ("/validity/verify/delete_states", test_delete_states_exist); return g_test_run (); }