libfprint/tests/test-validity-fwext.c
Leonardo Francisco b028a3ebf5 validity: Add firmware extension upload (Iteration 3)
Implement the firmware extension (fwext) upload module for
Validity/Synaptics VCSFW sensors. When the sensor reports no
firmware loaded (GET_FW_INFO returns status 0xB004), the driver
uploads the .xpfwext firmware file using the following sequence:

  1. WRITE_HW_REG32 (0x08) to prepare hardware register
  2. READ_HW_REG32 (0x07) to verify register state
  3. Load .xpfwext file from filesystem search paths
  4. For each 4KB chunk:
     a. Send db_write_enable blob (encrypted auth token)
     b. WRITE_FLASH (0x41) with chunk payload
     c. CLEANUP (0x1A) to commit chunk
  5. WRITE_FW_SIG (0x42) to upload RSA signature
  6. GET_FW_INFO (0x43) to verify successful upload
  7. REBOOT (0x05 0x02 0x00) to activate new firmware

Architecture: Uses the NULL-callback subsm pattern where SEND
states call vcsfw_cmd_send(self, ssm, cmd, len, NULL) and RECV
states read self->cmd_response_status/data directly. This avoids
the double-advance bug with fpi_ssm_start_subsm auto-advancing
the parent.

New files:
  - validity_fwext.h: Structures, SSM state enum, API declarations
  - validity_fwext.c: Upload SSM, file parser, command builders
  - validity_blob_dbe_009a.inc: db_write_enable blob for 06cb:009a
  - test-validity-fwext.c: 19 unit tests covering all pure functions

Modified files:
  - validity.h: Add cmd_response_status field to FpiDeviceValidity
  - validity.c: Add OPEN_UPLOAD_FWEXT state to open sequence
  - vcsfw_protocol.c: Save status in cmd_receive_cb for RECV states
  - meson.build: Add validity_fwext.c to driver sources

Test results: 34 OK, 0 Fail, 2 Skipped
2026-04-04 23:40:40 -04:00

643 lines
19 KiB
C

/*
* Unit tests for validity firmware extension upload functions
*
* 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 <glib/gstdio.h>
#include <string.h>
#include "fpi-device.h"
#include "fpi-ssm.h"
#include "fpi-byte-utils.h"
#include "drivers/validity/validity_fwext.h"
#include "drivers/validity/vcsfw_protocol.h"
/* ================================================================
* T3.1: test_fw_info_parse_present
*
* Verify that a valid GET_FW_INFO response (status=OK) with 1 module
* is parsed correctly into ValidityFwInfo.
* ================================================================ */
static void
test_fw_info_parse_present (void)
{
ValidityFwInfo info;
/* Build a synthetic GET_FW_INFO response:
* major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 header bytes
* + 1 module * 12 bytes = 12
* Total = 22 bytes (data after 2-byte status, which is stripped) */
guint8 data[22];
memset (data, 0, sizeof (data));
/* major = 6 */
data[0] = 6;
data[1] = 0;
/* minor = 7 */
data[2] = 7;
data[3] = 0;
/* module_count = 1 */
data[4] = 1;
data[5] = 0;
/* buildtime = 0x12345678 LE */
data[6] = 0x78;
data[7] = 0x56;
data[8] = 0x34;
data[9] = 0x12;
/* Module 0: type=3, subtype=4, major=1, minor=2, size=0x1000 */
data[10] = 3;
data[11] = 0;
data[12] = 4;
data[13] = 0;
data[14] = 1;
data[15] = 0;
data[16] = 2;
data[17] = 0;
data[18] = 0x00;
data[19] = 0x10;
data[20] = 0x00;
data[21] = 0x00;
gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data),
VCSFW_STATUS_OK, &info);
g_assert_true (ok);
g_assert_true (info.loaded);
g_assert_cmpuint (info.major, ==, 6);
g_assert_cmpuint (info.minor, ==, 7);
g_assert_cmpuint (info.module_count, ==, 1);
g_assert_cmpuint (info.buildtime, ==, 0x12345678);
g_assert_cmpuint (info.modules[0].type, ==, 3);
g_assert_cmpuint (info.modules[0].subtype, ==, 4);
g_assert_cmpuint (info.modules[0].major, ==, 1);
g_assert_cmpuint (info.modules[0].minor, ==, 2);
g_assert_cmpuint (info.modules[0].size, ==, 0x1000);
}
/* ================================================================
* T3.2: test_fw_info_parse_absent
*
* Verify that status VCSFW_STATUS_NO_FW sets loaded=FALSE.
* ================================================================ */
static void
test_fw_info_parse_absent (void)
{
ValidityFwInfo info;
gboolean ok = validity_fwext_parse_fw_info (NULL, 0,
VCSFW_STATUS_NO_FW, &info);
g_assert_true (ok);
g_assert_false (info.loaded);
}
/* ================================================================
* T3.2b: test_fw_info_parse_unknown_status
*
* Verify that an unexpected status returns FALSE.
* ================================================================ */
static void
test_fw_info_parse_unknown_status (void)
{
ValidityFwInfo info;
g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING,
"*unexpected status*");
gboolean ok = validity_fwext_parse_fw_info (NULL, 0, 0x9999, &info);
g_test_assert_expected_messages ();
g_assert_false (ok);
g_assert_false (info.loaded);
}
/* ================================================================
* T3.2c: test_fw_info_parse_truncated
*
* Verify that status=OK but data too short returns FALSE.
* ================================================================ */
static void
test_fw_info_parse_truncated (void)
{
ValidityFwInfo info;
guint8 data[5] = { 0 };
g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING,
"*too short*");
gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data),
VCSFW_STATUS_OK, &info);
g_test_assert_expected_messages ();
g_assert_false (ok);
}
/* ================================================================
* T3.3: test_xpfwext_file_parse
*
* Create a synthetic .xpfwext file in a temp dir and verify parsing.
* Format: header + 0x1A + payload + 256-byte signature
* ================================================================ */
static void
test_xpfwext_file_parse (void)
{
g_autoptr (GError) error = NULL;
ValidityFwextFile fwext;
gchar *tmpdir;
g_autofree gchar *path = NULL;
tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error);
g_assert_no_error (error);
path = g_build_filename (tmpdir, "test.xpfwext", NULL);
/* Build file: "HDR" + 0x1A + 8 bytes payload + 256 bytes sig */
gsize header_len = 4; /* "HDR" + 0x1A */
gsize payload_len = 8;
gsize sig_len = 256;
gsize total = header_len + payload_len + sig_len;
guint8 *content = g_malloc0 (total);
content[0] = 'H';
content[1] = 'D';
content[2] = 'R';
content[3] = 0x1A;
/* Payload: 0x01..0x08 */
for (gsize i = 0; i < payload_len; i++)
content[header_len + i] = (guint8) (i + 1);
/* Signature: 0xAA repeated */
memset (content + header_len + payload_len, 0xAA, sig_len);
g_file_set_contents (path, (gchar *) content, total, &error);
g_assert_no_error (error);
gboolean ok = validity_fwext_load_file (path, &fwext, &error);
g_assert_no_error (error);
g_assert_true (ok);
g_assert_cmpuint (fwext.payload_len, ==, payload_len);
/* Check payload content */
for (gsize i = 0; i < payload_len; i++)
g_assert_cmpuint (fwext.payload[i], ==, i + 1);
/* Check signature */
for (gsize i = 0; i < sig_len; i++)
g_assert_cmpuint (fwext.signature[i], ==, 0xAA);
validity_fwext_file_clear (&fwext);
g_assert_null (fwext.payload);
g_assert_cmpuint (fwext.payload_len, ==, 0);
/* Cleanup */
g_unlink (path);
g_rmdir (tmpdir);
g_free (content);
g_free (tmpdir);
}
/* ================================================================
* T3.3b: test_xpfwext_file_no_delimiter
*
* Verify that a file without 0x1A delimiter fails to parse.
* ================================================================ */
static void
test_xpfwext_file_no_delimiter (void)
{
g_autoptr (GError) error = NULL;
ValidityFwextFile fwext;
gchar *tmpdir;
g_autofree gchar *path = NULL;
tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error);
g_assert_no_error (error);
path = g_build_filename (tmpdir, "bad.xpfwext", NULL);
/* All 0xFF — no 0x1A delimiter */
guint8 content[300];
memset (content, 0xFF, sizeof (content));
g_file_set_contents (path, (gchar *) content, sizeof (content), &error);
g_assert_no_error (error);
gboolean ok = validity_fwext_load_file (path, &fwext, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_unlink (path);
g_rmdir (tmpdir);
g_free (tmpdir);
}
/* ================================================================
* T3.3c: test_xpfwext_file_too_short
*
* Verify that a file with valid header but data shorter than
* signature size fails.
* ================================================================ */
static void
test_xpfwext_file_too_short (void)
{
g_autoptr (GError) error = NULL;
ValidityFwextFile fwext;
gchar *tmpdir;
g_autofree gchar *path = NULL;
tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error);
g_assert_no_error (error);
path = g_build_filename (tmpdir, "short.xpfwext", NULL);
/* "X" + 0x1A + 10 bytes (< 257 needed for sig + 1 byte payload) */
guint8 content[12];
content[0] = 'X';
content[1] = 0x1A;
memset (content + 2, 0, 10);
g_file_set_contents (path, (gchar *) content, sizeof (content), &error);
g_assert_no_error (error);
gboolean ok = validity_fwext_load_file (path, &fwext, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_unlink (path);
g_rmdir (tmpdir);
g_free (tmpdir);
}
/* ================================================================
* T3.4: test_flash_write_cmd_format
*
* Verify that build_write_flash produces the correct wire format:
* [0x41, partition, 1, 0, 0, offset_LE32, len_LE32, data...]
* ================================================================ */
static void
test_flash_write_cmd_format (void)
{
guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF };
guint8 cmd[13 + sizeof (payload)];
gsize cmd_len;
validity_fwext_build_write_flash (2, 0x1000, payload, sizeof (payload),
cmd, &cmd_len);
g_assert_cmpuint (cmd_len, ==, 13 + sizeof (payload));
g_assert_cmpuint (cmd[0], ==, 0x41); /* WRITE_FLASH command */
g_assert_cmpuint (cmd[1], ==, 2); /* partition */
g_assert_cmpuint (cmd[2], ==, 1); /* flag */
g_assert_cmpuint (cmd[3], ==, 0); /* reserved low */
g_assert_cmpuint (cmd[4], ==, 0); /* reserved high */
/* offset = 0x1000 LE */
g_assert_cmpuint (cmd[5], ==, 0x00);
g_assert_cmpuint (cmd[6], ==, 0x10);
g_assert_cmpuint (cmd[7], ==, 0x00);
g_assert_cmpuint (cmd[8], ==, 0x00);
/* length = 4 LE */
g_assert_cmpuint (cmd[9], ==, 0x04);
g_assert_cmpuint (cmd[10], ==, 0x00);
g_assert_cmpuint (cmd[11], ==, 0x00);
g_assert_cmpuint (cmd[12], ==, 0x00);
/* data */
g_assert_cmpmem (cmd + 13, sizeof (payload), payload, sizeof (payload));
}
/* ================================================================
* T3.5: test_fw_sig_cmd_format
*
* Verify that build_write_fw_sig produces the correct wire format:
* [0x42, partition, 0, len_LE16, signature...]
* ================================================================ */
static void
test_fw_sig_cmd_format (void)
{
guint8 sig[256];
guint8 cmd[5 + 256];
gsize cmd_len;
memset (sig, 0xBB, sizeof (sig));
validity_fwext_build_write_fw_sig (2, sig, sizeof (sig), cmd, &cmd_len);
g_assert_cmpuint (cmd_len, ==, 5 + 256);
g_assert_cmpuint (cmd[0], ==, 0x42); /* WRITE_FW_SIG command */
g_assert_cmpuint (cmd[1], ==, 2); /* partition */
g_assert_cmpuint (cmd[2], ==, 0); /* reserved */
/* sig length = 256 LE */
g_assert_cmpuint (cmd[3], ==, 0x00);
g_assert_cmpuint (cmd[4], ==, 0x01);
g_assert_cmpmem (cmd + 5, 256, sig, 256);
}
/* ================================================================
* T3.6: test_chunk_iteration
*
* Verify that building write_flash with increasing offsets covers
* the entire payload. Simulate the chunk loop used by the SSM.
* ================================================================ */
static void
test_chunk_iteration (void)
{
gsize payload_len = 0x2800; /* 10 KB = 2.5 chunks of 4 KB */
gsize write_offset = 0;
guint chunk_count = 0;
const gsize CHUNK_SIZE = 0x1000;
while (write_offset < payload_len)
{
gsize remaining = payload_len - write_offset;
gsize chunk_size = MIN (remaining, CHUNK_SIZE);
g_assert_cmpuint (chunk_size, >, 0);
g_assert_cmpuint (chunk_size, <=, CHUNK_SIZE);
write_offset += chunk_size;
chunk_count++;
}
g_assert_cmpuint (write_offset, ==, payload_len);
g_assert_cmpuint (chunk_count, ==, 3); /* 4096 + 4096 + 2048 */
}
/* ================================================================
* T3.7: test_hw_reg_cmd_format
*
* Verify WRITE_HW_REG32 and READ_HW_REG32 command formats.
* ================================================================ */
static void
test_hw_reg_write_cmd_format (void)
{
guint8 cmd[10];
gsize cmd_len;
validity_fwext_build_write_hw_reg32 (0x8000205C, 7, cmd, &cmd_len);
g_assert_cmpuint (cmd_len, ==, 10);
g_assert_cmpuint (cmd[0], ==, 0x08); /* WRITE_HW_REG32 command */
/* address = 0x8000205C LE */
g_assert_cmpuint (cmd[1], ==, 0x5C);
g_assert_cmpuint (cmd[2], ==, 0x20);
g_assert_cmpuint (cmd[3], ==, 0x00);
g_assert_cmpuint (cmd[4], ==, 0x80);
/* value = 7 LE */
g_assert_cmpuint (cmd[5], ==, 0x07);
g_assert_cmpuint (cmd[6], ==, 0x00);
g_assert_cmpuint (cmd[7], ==, 0x00);
g_assert_cmpuint (cmd[8], ==, 0x00);
/* size = 4 */
g_assert_cmpuint (cmd[9], ==, 4);
}
static void
test_hw_reg_read_cmd_format (void)
{
guint8 cmd[6];
gsize cmd_len;
validity_fwext_build_read_hw_reg32 (0x80002080, cmd, &cmd_len);
g_assert_cmpuint (cmd_len, ==, 6);
g_assert_cmpuint (cmd[0], ==, 0x07); /* READ_HW_REG32 command */
/* address = 0x80002080 LE */
g_assert_cmpuint (cmd[1], ==, 0x80);
g_assert_cmpuint (cmd[2], ==, 0x20);
g_assert_cmpuint (cmd[3], ==, 0x00);
g_assert_cmpuint (cmd[4], ==, 0x80);
/* size = 4 */
g_assert_cmpuint (cmd[5], ==, 4);
}
static void
test_hw_reg_read_parse (void)
{
guint32 value;
guint8 data[] = { 0x02, 0x00, 0x00, 0x00 };
gboolean ok = validity_fwext_parse_read_hw_reg32 (data, sizeof (data),
&value);
g_assert_true (ok);
g_assert_cmpuint (value, ==, 2);
/* Too short should fail */
ok = validity_fwext_parse_read_hw_reg32 (data, 3, &value);
g_assert_false (ok);
}
/* ================================================================
* T3.8: test_firmware_filename
*
* Verify firmware filename mapping for known PIDs.
* ================================================================ */
static void
test_firmware_filename (void)
{
const gchar *name;
name = validity_fwext_get_firmware_name (0x06cb, 0x009a);
g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext");
name = validity_fwext_get_firmware_name (0x138a, 0x0090);
g_assert_cmpstr (name, ==, "6_07f_Lenovo.xpfwext");
name = validity_fwext_get_firmware_name (0x138a, 0x0097);
g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext");
name = validity_fwext_get_firmware_name (0x138a, 0x009d);
g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext");
/* Unknown PID should return NULL */
name = validity_fwext_get_firmware_name (0x1234, 0x5678);
g_assert_null (name);
}
/* ================================================================
* T3.9: test_missing_firmware_file
*
* Verify that find_firmware returns an error when the file is not
* found in any search path.
* ================================================================ */
static void
test_missing_firmware_file (void)
{
g_autoptr (GError) error = NULL;
/* This should fail since firmware files aren't installed in CI */
g_autofree gchar *path = validity_fwext_find_firmware (0x06cb, 0x009a,
&error);
/* It either found a file or returned an error — both are valid.
* In CI, it should be an error. On a real system, it might succeed. */
if (path == NULL)
{
g_assert_nonnull (error);
g_assert_true (g_error_matches (error, FP_DEVICE_ERROR,
FP_DEVICE_ERROR_DATA_NOT_FOUND));
}
else
{
g_assert_no_error (error);
g_assert_true (g_file_test (path, G_FILE_TEST_IS_REGULAR));
}
}
/* ================================================================
* T3.9b: test_unsupported_pid_firmware
*
* Verify that find_firmware for an unknown PID returns NOT_SUPPORTED.
* ================================================================ */
static void
test_unsupported_pid_firmware (void)
{
g_autoptr (GError) error = NULL;
g_autofree gchar *path = validity_fwext_find_firmware (0x1234, 0x5678,
&error);
g_assert_null (path);
g_assert_nonnull (error);
g_assert_true (g_error_matches (error, FP_DEVICE_ERROR,
FP_DEVICE_ERROR_NOT_SUPPORTED));
}
/* ================================================================
* T3.10: test_db_write_enable_blob
*
* Verify that db_write_enable blob is returned for supported PID
* and NULL for unsupported PID.
* ================================================================ */
static void
test_db_write_enable_blob (void)
{
gsize len;
const guint8 *blob;
blob = validity_fwext_get_db_write_enable (0x06cb, 0x009a, &len);
g_assert_nonnull (blob);
g_assert_cmpuint (len, ==, 3621);
blob = validity_fwext_get_db_write_enable (0x1234, 0x5678, &len);
g_assert_null (blob);
g_assert_cmpuint (len, ==, 0);
}
/* ================================================================
* T3.11: test_reboot_cmd_format
*
* Verify reboot command: 0x05, 0x02, 0x00
* ================================================================ */
static void
test_reboot_cmd_format (void)
{
guint8 cmd[3];
gsize cmd_len;
validity_fwext_build_reboot (cmd, &cmd_len);
g_assert_cmpuint (cmd_len, ==, 3);
g_assert_cmpuint (cmd[0], ==, 0x05);
g_assert_cmpuint (cmd[1], ==, 0x02);
g_assert_cmpuint (cmd[2], ==, 0x00);
}
/* ================================================================
* Regression: fwext_file_clear on already-cleared struct
*
* Double-free guard.
* ================================================================ */
static void
test_file_clear_idempotent (void)
{
ValidityFwextFile fwext = { 0 };
/* Clear an empty struct — should not crash */
validity_fwext_file_clear (&fwext);
validity_fwext_file_clear (&fwext);
g_assert_null (fwext.payload);
g_assert_cmpuint (fwext.payload_len, ==, 0);
}
int
main (int argc,
char *argv[])
{
g_test_init (&argc, &argv, NULL);
/* Firmware info parsing */
g_test_add_func ("/validity/fwext/fw-info/parse-present",
test_fw_info_parse_present);
g_test_add_func ("/validity/fwext/fw-info/parse-absent",
test_fw_info_parse_absent);
g_test_add_func ("/validity/fwext/fw-info/parse-unknown-status",
test_fw_info_parse_unknown_status);
g_test_add_func ("/validity/fwext/fw-info/parse-truncated",
test_fw_info_parse_truncated);
/* File parsing */
g_test_add_func ("/validity/fwext/file/parse",
test_xpfwext_file_parse);
g_test_add_func ("/validity/fwext/file/no-delimiter",
test_xpfwext_file_no_delimiter);
g_test_add_func ("/validity/fwext/file/too-short",
test_xpfwext_file_too_short);
g_test_add_func ("/validity/fwext/file/clear-idempotent",
test_file_clear_idempotent);
/* Command format */
g_test_add_func ("/validity/fwext/cmd/write-flash",
test_flash_write_cmd_format);
g_test_add_func ("/validity/fwext/cmd/write-fw-sig",
test_fw_sig_cmd_format);
g_test_add_func ("/validity/fwext/cmd/write-hw-reg",
test_hw_reg_write_cmd_format);
g_test_add_func ("/validity/fwext/cmd/read-hw-reg",
test_hw_reg_read_cmd_format);
g_test_add_func ("/validity/fwext/cmd/read-hw-reg-parse",
test_hw_reg_read_parse);
g_test_add_func ("/validity/fwext/cmd/reboot",
test_reboot_cmd_format);
/* Chunk iteration */
g_test_add_func ("/validity/fwext/chunk-iteration",
test_chunk_iteration);
/* Firmware filename mapping */
g_test_add_func ("/validity/fwext/firmware-name",
test_firmware_filename);
g_test_add_func ("/validity/fwext/find-firmware/missing",
test_missing_firmware_file);
g_test_add_func ("/validity/fwext/find-firmware/unsupported-pid",
test_unsupported_pid_firmware);
/* Blob lookup */
g_test_add_func ("/validity/fwext/db-write-enable",
test_db_write_enable_blob);
return g_test_run ();
}