libfprint/tests/test-validity-verify.c
Leonardo Francisco ae3f3a2479 validity: add 16 regression tests for Iter6 audit fixes
Adds test-validity-verify.c with 16 unit tests that prevent regression
of all 8 issues found during the Iteration 6 code audit (b05657f):

  R1: parse_match_result TLV parsing (5 tests)
    - valid payload with all fields extracted correctly
    - multi-tag iteration (tag ordering independence)
    - empty dict returns no-match
    - truncated/malformed data handled gracefully
    - unknown tags skipped without error

  R1f: match_result_clear frees hash and zeros struct

  R2: identity builder NULL rejection (2 tests)
    - NULL uuid returns NULL (prevented g_variant_new_string crash)
    - valid UUID produces correct identity bytes

  R3: gallery matching by subtype (3 tests)
    - matches correct print by finger subtype
    - falls back to first entry when subtype not found
    - returns NULL for empty/NULL gallery

  R4: struct field separation — enroll_user_dbid != delete_storage_dbid

  R5: del_record command format — cmd 0x48 with dbid(2LE)

  R6: match_finger single allocation — exactly 13 bytes

  R7: SSM state enums exist (2 tests)
    - CLEAR_* states 0-5
    - DELETE_* states 0-7

To make the tests possible, extracted previously-static functions:
  - parse_match_result → validity_parse_match_result (public)
  - ValidityMatchResult struct moved to validity_db.h
  - validity_match_result_clear added to validity_db.c
  - validity_find_gallery_match helper extracted from verify SSM
2026-04-22 03:06:34 +00:00

551 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <glib.h>
#include <string.h>
#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 ();
}