libfprint/libfprint/drivers/validity/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

693 lines
22 KiB
C

/*
* Validity/Synaptics VCSFW firmware extension upload
*
* 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.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define FP_COMPONENT "validity"
#include "drivers_api.h"
#include "fpi-byte-utils.h"
#include "validity.h"
#include "validity_fwext.h"
#include "vcsfw_protocol.h"
#include <gio/gio.h>
#include <string.h>
/* ---- Constants ---- */
#define FWEXT_CHUNK_SIZE 0x1000 /* 4 KB per write_flash chunk */
#define FWEXT_SIGNATURE_SIZE 256 /* RSA signature length */
#define FWEXT_HEADER_DELIMITER 0x1A /* .xpfwext header end marker */
#define FWEXT_HW_REG_WRITE_ADDR 0x8000205C
#define FWEXT_HW_REG_WRITE_VALUE 7
#define FWEXT_HW_REG_READ_ADDR 0x80002080
/* Firmware partition */
#define FWEXT_PARTITION 2
/* Reboot command: 0x05 0x02 0x00 */
#define VCSFW_CMD_REBOOT 0x05
#define VCSFW_REBOOT_SUBCMD 0x02
/* Cleanup command (call_cleanups): 0x1a */
#define VCSFW_CMD_CLEANUP 0x1A
/* ---- Firmware file search paths ---- */
static const gchar *firmware_search_paths[] = {
"/usr/share/libfprint/validity",
"/var/lib/python-validity",
"/var/run/python-validity",
"/usr/share/python-validity",
NULL,
};
/* ---- db_write_enable blob for 06cb:009a (3621 bytes) ----
* Opaque encrypted blob sent before each flash write.
* The sensor firmware decrypts this internally.
* Extracted from python-validity blobs_9a.py.
* TODO: Iteration 6 (HAL) will consolidate blobs for all PIDs. */
#include "validity_blob_dbe_009a.inc"
/* ================================================================
* Firmware info parsing
* ================================================================ */
gboolean
validity_fwext_parse_fw_info (const guint8 *data,
gsize data_len,
guint16 status,
ValidityFwInfo *info)
{
memset (info, 0, sizeof (*info));
/* Status 0xB004 (bytes: 0xb0 0x04) means no firmware loaded.
* python-validity checks: rsp[0]==0xb0 and rsp[1]==4
* Our vcsfw_cmd_send reads status as uint16 LE from the first 2 bytes,
* so wire bytes {0xb0, 0x04} -> status = 0x04B0 in LE. */
if (status == VCSFW_STATUS_NO_FW)
{
info->loaded = FALSE;
return TRUE;
}
if (status != VCSFW_STATUS_OK)
{
fp_warn ("GET_FW_INFO unexpected status: 0x%04x", status);
info->loaded = FALSE;
return FALSE;
}
/* Response data (after 2-byte status stripped by vcsfw_cmd_send):
* major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 bytes header
* + 12 bytes per module */
if (data_len < 10)
{
fp_warn ("GET_FW_INFO response too short: %zu", data_len);
info->loaded = FALSE;
return FALSE;
}
info->loaded = TRUE;
info->major = FP_READ_UINT16_LE (data);
info->minor = FP_READ_UINT16_LE (data + 2);
info->module_count = FP_READ_UINT16_LE (data + 4);
info->buildtime = FP_READ_UINT32_LE (data + 6);
if (info->module_count > 32)
info->module_count = 32;
for (guint16 i = 0; i < info->module_count && (10 + (i + 1) * 12) <= data_len; i++)
{
const guint8 *m = data + 10 + i * 12;
info->modules[i].type = FP_READ_UINT16_LE (m);
info->modules[i].subtype = FP_READ_UINT16_LE (m + 2);
info->modules[i].major = FP_READ_UINT16_LE (m + 4);
info->modules[i].minor = FP_READ_UINT16_LE (m + 6);
info->modules[i].size = FP_READ_UINT32_LE (m + 8);
}
return TRUE;
}
/* ================================================================
* Firmware filename mapping
* ================================================================ */
const gchar *
validity_fwext_get_firmware_name (guint16 vid,
guint16 pid)
{
if (vid == 0x138a && pid == 0x0090)
return "6_07f_Lenovo.xpfwext";
/* All other supported PIDs use the same firmware */
if ((vid == 0x138a && (pid == 0x0097 || pid == 0x009d)) ||
(vid == 0x06cb && pid == 0x009a))
return "6_07f_lenovo_mis_qm.xpfwext";
return NULL;
}
gchar *
validity_fwext_find_firmware (guint16 vid,
guint16 pid,
GError **error)
{
const gchar *filename = validity_fwext_get_firmware_name (vid, pid);
if (filename == NULL)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_SUPPORTED,
"No firmware filename known for %04x:%04x", vid, pid);
return NULL;
}
for (const gchar **path = firmware_search_paths; *path != NULL; path++)
{
g_autofree gchar *full_path = g_build_filename (*path, filename, NULL);
if (g_file_test (full_path, G_FILE_TEST_IS_REGULAR))
{
fp_info ("Found firmware file: %s", full_path);
return g_steal_pointer (&full_path);
}
}
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND,
"Firmware file '%s' not found. "
"Search paths: /usr/share/libfprint/validity/, "
"/var/lib/python-validity/, /var/run/python-validity/. "
"Extract from Lenovo driver installer (nz3gf09w.exe).",
filename);
return NULL;
}
/* ================================================================
* .xpfwext file parser
* ================================================================ */
gboolean
validity_fwext_load_file (const gchar *path,
ValidityFwextFile *fwext,
GError **error)
{
g_autofree guint8 *contents = NULL;
gsize length = 0;
memset (fwext, 0, sizeof (*fwext));
if (!g_file_get_contents (path, (gchar **) &contents, &length, error))
return FALSE;
/* Find 0x1A delimiter that ends the header */
const guint8 *delim = memchr (contents, FWEXT_HEADER_DELIMITER, length);
if (delim == NULL)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Firmware file has no 0x1A header delimiter");
return FALSE;
}
gsize header_len = (delim - contents) + 1; /* includes 0x1A itself */
gsize data_len = length - header_len;
if (data_len < FWEXT_SIGNATURE_SIZE + 1)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Firmware file too short after header (%zu bytes)", data_len);
return FALSE;
}
/* Split: payload = data[:-256], signature = data[-256:] */
fwext->payload_len = data_len - FWEXT_SIGNATURE_SIZE;
fwext->payload = g_memdup2 (contents + header_len, fwext->payload_len);
memcpy (fwext->signature,
contents + header_len + fwext->payload_len,
FWEXT_SIGNATURE_SIZE);
fp_info ("Firmware file loaded: %zu bytes payload, %d bytes signature",
fwext->payload_len, FWEXT_SIGNATURE_SIZE);
return TRUE;
}
void
validity_fwext_file_clear (ValidityFwextFile *fwext)
{
g_clear_pointer (&fwext->payload, g_free);
fwext->payload_len = 0;
}
/* ================================================================
* Command builders
* ================================================================ */
void
validity_fwext_build_write_hw_reg32 (guint32 addr,
guint32 value,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BLLB', 0x08, addr, val, 4) = 10 bytes */
cmd[0] = VCSFW_CMD_WRITE_HW_REG32;
FP_WRITE_UINT32_LE (cmd + 1, addr);
FP_WRITE_UINT32_LE (cmd + 5, value);
cmd[9] = 4;
*cmd_len = 10;
}
void
validity_fwext_build_read_hw_reg32 (guint32 addr,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BLB', 0x07, addr, 4) = 6 bytes */
cmd[0] = VCSFW_CMD_READ_HW_REG32;
FP_WRITE_UINT32_LE (cmd + 1, addr);
cmd[5] = 4;
*cmd_len = 6;
}
gboolean
validity_fwext_parse_read_hw_reg32 (const guint8 *data,
gsize data_len,
guint32 *value)
{
/* Response data (after 2-byte status): uint32 LE value */
if (data_len < 4)
return FALSE;
*value = FP_READ_UINT32_LE (data);
return TRUE;
}
void
validity_fwext_build_write_flash (guint8 partition,
guint32 offset,
const guint8 *data,
gsize data_len,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BBBHLL', 0x41, partition, 1, 0, addr, len) + data = 13 + data bytes */
cmd[0] = VCSFW_CMD_WRITE_FLASH;
cmd[1] = partition;
cmd[2] = 1; /* flag */
cmd[3] = 0; /* reserved LE low */
cmd[4] = 0; /* reserved LE high */
FP_WRITE_UINT32_LE (cmd + 5, offset);
FP_WRITE_UINT32_LE (cmd + 9, (guint32) data_len);
memcpy (cmd + 13, data, data_len);
*cmd_len = 13 + data_len;
}
void
validity_fwext_build_write_fw_sig (guint8 partition,
const guint8 *signature,
gsize sig_len,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BBxH', 0x42, partition, len) + signature = 5 + sig bytes */
cmd[0] = VCSFW_CMD_WRITE_FW_SIG;
cmd[1] = partition;
cmd[2] = 0; /* reserved byte (the 'x' in pack format) */
FP_WRITE_UINT16_LE (cmd + 3, (guint16) sig_len);
memcpy (cmd + 5, signature, sig_len);
*cmd_len = 5 + sig_len;
}
void
validity_fwext_build_reboot (guint8 *cmd,
gsize *cmd_len)
{
/* b'\x05\x02\x00' */
cmd[0] = VCSFW_CMD_REBOOT;
cmd[1] = VCSFW_REBOOT_SUBCMD;
cmd[2] = 0x00;
*cmd_len = 3;
}
/* ================================================================
* db_write_enable blob lookup
* ================================================================ */
const guint8 *
validity_fwext_get_db_write_enable (guint16 vid,
guint16 pid,
gsize *len)
{
/* Currently only 06cb:009a is supported.
* Iteration 6 (HAL) will add blobs for all PIDs. */
if (vid == 0x06cb && pid == 0x009a)
{
*len = sizeof (db_write_enable_009a);
return db_write_enable_009a;
}
*len = 0;
return NULL;
}
/* ================================================================
* Upload SSM -- firmware extension upload state machine
*
* This SSM is started as a standalone child from the open sequence
* when fwext_loaded == FALSE. It uses the subsm pattern:
* SEND states call vcsfw_cmd_send(self, ssm, ..., NULL) with a
* NULL callback. The child SSM auto-advances the parent to the
* RECV state on completion. RECV states read
* self->cmd_response_status and self->cmd_response_data.
* ================================================================ */
/* SSM data for the upload state machine */
typedef struct
{
ValidityFwextFile fwext;
gsize write_offset; /* current offset into payload */
guint16 vid;
guint16 pid;
} FwextUploadData;
static void
fwext_upload_data_free (gpointer data)
{
FwextUploadData *ud = data;
validity_fwext_file_clear (&ud->fwext);
g_free (ud);
}
void
validity_fwext_upload_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
FwextUploadData *ud = fpi_ssm_get_data (ssm);
switch (fpi_ssm_get_cur_state (ssm))
{
case FWEXT_SEND_WRITE_HW_REG:
{
guint8 cmd[10];
gsize cmd_len;
validity_fwext_build_write_hw_reg32 (FWEXT_HW_REG_WRITE_ADDR,
FWEXT_HW_REG_WRITE_VALUE,
cmd, &cmd_len);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_WRITE_HW_REG:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_HW_REG failed: status=0x%04x",
self->cmd_response_status));
return;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_READ_HW_REG:
{
guint8 cmd[6];
gsize cmd_len;
validity_fwext_build_read_hw_reg32 (FWEXT_HW_REG_READ_ADDR,
cmd, &cmd_len);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_READ_HW_REG:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"READ_HW_REG failed: status=0x%04x",
self->cmd_response_status));
return;
}
guint32 value;
if (!validity_fwext_parse_read_hw_reg32 (self->cmd_response_data,
self->cmd_response_len,
&value))
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"READ_HW_REG response too short"));
return;
}
if (value != 2 && value != 3)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Unexpected HW register value: %u "
"(expected 2 or 3)", value));
return;
}
fp_dbg ("FWEXT: HW register 0x%08x = %u (OK)", FWEXT_HW_REG_READ_ADDR, value);
fpi_ssm_next_state (ssm);
}
break;
case FWEXT_LOAD_FILE:
{
GError *error = NULL;
GUsbDevice *usb_dev = fpi_device_get_usb_device (dev);
guint16 vid = g_usb_device_get_vid (usb_dev);
guint16 pid = g_usb_device_get_pid (usb_dev);
ud->vid = vid;
ud->pid = pid;
g_autofree gchar *fw_path = validity_fwext_find_firmware (vid, pid, &error);
if (fw_path == NULL)
{
fpi_ssm_mark_failed (ssm, error);
return;
}
if (!validity_fwext_load_file (fw_path, &ud->fwext, &error))
{
fpi_ssm_mark_failed (ssm, error);
return;
}
ud->write_offset = 0;
fp_info ("FWEXT: Loaded firmware file, %zu bytes to write",
ud->fwext.payload_len);
fpi_ssm_next_state (ssm);
}
break;
case FWEXT_SEND_DB_WRITE_ENABLE:
{
gsize dbe_len;
const guint8 *dbe = validity_fwext_get_db_write_enable (ud->vid,
ud->pid,
&dbe_len);
if (dbe == NULL || dbe_len == 0)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
"No db_write_enable blob for "
"%04x:%04x", ud->vid, ud->pid));
return;
}
vcsfw_cmd_send (self, ssm, dbe, dbe_len, NULL);
}
break;
case FWEXT_RECV_DB_WRITE_ENABLE:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"db_write_enable failed: "
"status=0x%04x",
self->cmd_response_status));
return;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_WRITE_CHUNK:
{
gsize remaining = ud->fwext.payload_len - ud->write_offset;
gsize chunk_size = MIN (remaining, FWEXT_CHUNK_SIZE);
/* cmd buffer: 13-byte header + payload */
g_autofree guint8 *cmd = g_malloc (13 + chunk_size);
gsize cmd_len;
validity_fwext_build_write_flash (FWEXT_PARTITION,
(guint32) ud->write_offset,
ud->fwext.payload + ud->write_offset,
chunk_size,
cmd, &cmd_len);
fp_dbg ("FWEXT: Writing chunk at offset 0x%zx (%zu/%zu bytes)",
ud->write_offset, ud->write_offset + chunk_size,
ud->fwext.payload_len);
ud->write_offset += chunk_size;
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_WRITE_CHUNK:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_FLASH chunk failed: "
"status=0x%04x",
self->cmd_response_status));
return;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_CLEANUP:
{
guint8 cmd[] = { VCSFW_CMD_CLEANUP };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case FWEXT_RECV_CLEANUP:
/* Status 0x0491 means "nothing to commit" -- not an error */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Cleanup cmd failed: "
"status=0x%04x",
self->cmd_response_status));
return;
}
if (ud->write_offset < ud->fwext.payload_len)
{
/* More chunks to write -- loop back to db_write_enable */
fpi_ssm_jump_to_state (ssm, FWEXT_SEND_DB_WRITE_ENABLE);
}
else
{
/* All chunks written -- proceed to signature */
fpi_ssm_next_state (ssm);
}
break;
case FWEXT_SEND_WRITE_SIGNATURE:
{
guint8 cmd[5 + FWEXT_SIGNATURE_SIZE];
gsize cmd_len;
validity_fwext_build_write_fw_sig (FWEXT_PARTITION,
ud->fwext.signature,
FWEXT_SIGNATURE_SIZE,
cmd, &cmd_len);
fp_info ("FWEXT: Writing firmware signature (%d bytes)",
FWEXT_SIGNATURE_SIZE);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_WRITE_SIGNATURE:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_FW_SIG failed: "
"status=0x%04x",
self->cmd_response_status));
return;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_VERIFY:
{
guint8 cmd[] = { VCSFW_CMD_GET_FW_INFO, FWEXT_PARTITION };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case FWEXT_RECV_VERIFY:
{
ValidityFwInfo info;
if (!validity_fwext_parse_fw_info (self->cmd_response_data,
self->cmd_response_len,
self->cmd_response_status,
&info) ||
!info.loaded)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Firmware not detected after upload"));
return;
}
fp_info ("FWEXT: Upload verified -- firmware v%d.%d, %d modules",
info.major, info.minor, info.module_count);
fpi_ssm_next_state (ssm);
}
break;
case FWEXT_SEND_REBOOT:
{
guint8 cmd[3];
gsize cmd_len;
validity_fwext_build_reboot (cmd, &cmd_len);
fp_info ("FWEXT: Rebooting sensor to activate new firmware");
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_REBOOT:
/* Sensor will disconnect and re-enumerate on USB.
* We mark SSM completed -- the caller (open sequence)
* handles the post-reboot re-init. */
fp_info ("FWEXT: Reboot sent. Device will re-enumerate.");
fpi_ssm_mark_completed (ssm);
break;
}
}
/* ---- SSM factory ---- */
FpiSsm *
validity_fwext_upload_ssm_new (FpDevice *dev)
{
FpiSsm *ssm;
FwextUploadData *ud;
ssm = fpi_ssm_new (dev, validity_fwext_upload_run_state,
FWEXT_NUM_STATES);
ud = g_new0 (FwextUploadData, 1);
fpi_ssm_set_data (ssm, ud, fwext_upload_data_free);
return ssm;
}