Search the available CHIDs to determine the device on which U-Boot is running. Use this to select the correct compatible string. Signed-off-by: Simon Glass <sjg@chromium.org>
516 lines
15 KiB
C
516 lines
15 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Computer Hardware Identifiers (Windows CHID) support
|
|
*
|
|
* This implements the Microsoft Computer Hardware ID specification
|
|
* used by Windows Update and fwupd for hardware identification.
|
|
*
|
|
* See: https://github.com/fwupd/fwupd/blob/main/docs/hwids.md
|
|
* See: https://docs.microsoft.com/en-us/windows-hardware/drivers/install/specifying-hardware-ids-for-a-computer
|
|
*
|
|
* Copyright 2025 Simon Glass <sjg@chromium.org>
|
|
*
|
|
* Credit: Richard Hughes
|
|
* https://blogs.gnome.org/hughsie/2017/04/25/reverse-engineering-computerhardwareids-exe-with-winedbg/
|
|
*/
|
|
|
|
#define LOG_CATEGORY LOGC_ACPI
|
|
|
|
#include <chid.h>
|
|
#include <errno.h>
|
|
#include <log.h>
|
|
#include <malloc.h>
|
|
#include <smbios.h>
|
|
#include <asm/global_data.h>
|
|
#include <dm/device.h>
|
|
#include <dm/ofnode.h>
|
|
#include <linux/bitops.h>
|
|
#include <linux/utf.h>
|
|
#include <linux/kernel.h>
|
|
#include <u-boot/uuid.h>
|
|
|
|
DECLARE_GLOBAL_DATA_PTR;
|
|
|
|
/**
|
|
* struct dt_chid_node - contains CHID retrievd from the devicetree
|
|
*
|
|
* @node: devicetree node containing CHID info
|
|
* @compatible: compatible string for this node
|
|
* @variant: CHID variant number (0-14)
|
|
* @fields: bitmask of fields used in CHID generation
|
|
* @chid: 16-byte CHID (UUID)
|
|
*/
|
|
struct dt_chid_node {
|
|
ofnode node;
|
|
const char *compatible;
|
|
int variant;
|
|
u32 fields;
|
|
u8 chid[UUID_LEN];
|
|
};
|
|
|
|
/* field names for display purposes */
|
|
static const char *fields[CHID_COUNT] = {
|
|
[CHID_MANUF] = "Manufacturer",
|
|
[CHID_FAMILY] = "Family",
|
|
[CHID_PRODUCT_NAME] = "ProductName",
|
|
[CHID_PRODUCT_SKU] = "ProductSku",
|
|
[CHID_BOARD_MANUF] = "BaseboardManufacturer",
|
|
[CHID_BOARD_PRODUCT] = "BaseboardProduct",
|
|
[CHID_BIOS_VENDOR] = "BiosVendor",
|
|
[CHID_BIOS_VERSION] = "BiosVersion",
|
|
[CHID_BIOS_MAJOR] = "BiosMajorRelease",
|
|
[CHID_BIOS_MINOR] = "BiosMinorRelease",
|
|
[CHID_ENCLOSURE_TYPE] = "EnclosureKind",
|
|
};
|
|
|
|
/*
|
|
* Microsoft CHID variants table
|
|
*
|
|
* Each entry defines which SMBIOS fields are combined to create
|
|
* a specific Hardware ID variant. The variants are ordered from
|
|
* most specific (HardwareID-00) to least specific (HardwareID-14).
|
|
*/
|
|
static const struct chid_variant variants[CHID_VARIANT_COUNT] = {{
|
|
/* HardwareID-00: Most specific - includes all identifying fields */
|
|
.name = "HardwareID-00",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_PRODUCT_SKU) | BIT(CHID_BIOS_VENDOR) |
|
|
BIT(CHID_BIOS_VERSION) | BIT(CHID_BIOS_MAJOR) |
|
|
BIT(CHID_BIOS_MINOR),
|
|
}, {
|
|
/* HardwareID-01: Without SKU */
|
|
.name = "HardwareID-01",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_BIOS_VENDOR) | BIT(CHID_BIOS_VERSION) |
|
|
BIT(CHID_BIOS_MAJOR) | BIT(CHID_BIOS_MINOR),
|
|
}, {
|
|
/* HardwareID-02: Without family */
|
|
.name = "HardwareID-02",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_BIOS_VENDOR) | BIT(CHID_BIOS_VERSION) |
|
|
BIT(CHID_BIOS_MAJOR) | BIT(CHID_BIOS_MINOR),
|
|
}, {
|
|
/* HardwareID-03: With baseboard info, no BIOS version */
|
|
.name = "HardwareID-03",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_PRODUCT_SKU) | BIT(CHID_BOARD_MANUF) |
|
|
BIT(CHID_BOARD_PRODUCT),
|
|
}, {
|
|
/* HardwareID-04: Basic product identification */
|
|
.name = "HardwareID-04",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_PRODUCT_SKU),
|
|
}, {
|
|
/* HardwareID-05: Without SKU */
|
|
.name = "HardwareID-05",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_PRODUCT_NAME),
|
|
}, {
|
|
/* HardwareID-06: SKU with baseboard */
|
|
.name = "HardwareID-06",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_PRODUCT_SKU) |
|
|
BIT(CHID_BOARD_MANUF) | BIT(CHID_BOARD_PRODUCT),
|
|
}, {
|
|
/* HardwareID-07: Just manufacturer and SKU */
|
|
.name = "HardwareID-07",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_PRODUCT_SKU),
|
|
}, {
|
|
/* HardwareID-08: Product name with baseboard */
|
|
.name = "HardwareID-08",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_PRODUCT_NAME) |
|
|
BIT(CHID_BOARD_MANUF) | BIT(CHID_BOARD_PRODUCT),
|
|
}, {
|
|
/* HardwareID-09: Just manufacturer and product name */
|
|
.name = "HardwareID-09",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_PRODUCT_NAME),
|
|
}, {
|
|
/* HardwareID-10: Family with baseboard */
|
|
.name = "HardwareID-10",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY) | BIT(CHID_BOARD_MANUF) |
|
|
BIT(CHID_BOARD_PRODUCT),
|
|
}, {
|
|
/* HardwareID-11: Just manufacturer and family */
|
|
.name = "HardwareID-11",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_FAMILY),
|
|
}, {
|
|
/* HardwareID-12: Manufacturer and enclosure type */
|
|
.name = "HardwareID-12",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_ENCLOSURE_TYPE),
|
|
}, {
|
|
/* HardwareID-13: Manufacturer with baseboard only */
|
|
.name = "HardwareID-13",
|
|
.fields = BIT(CHID_MANUF) | BIT(CHID_BOARD_MANUF) |
|
|
BIT(CHID_BOARD_PRODUCT),
|
|
}, {
|
|
/* HardwareID-14: Least specific - manufacturer only */
|
|
.name = "HardwareID-14",
|
|
.fields = BIT(CHID_MANUF),
|
|
}
|
|
};
|
|
|
|
int chid_from_smbios(struct chid_data *chid)
|
|
{
|
|
const struct smbios_type0 *bios;
|
|
const struct smbios_type1 *sys;
|
|
const struct smbios_type2 *board;
|
|
const struct smbios_type3 *encl;
|
|
struct smbios_info info;
|
|
int ret;
|
|
|
|
/* Clear the structure first */
|
|
memset(chid, '\0', sizeof(*chid));
|
|
|
|
ret = smbios_locate(gd_smbios_start(), &info);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Extract BIOS Information (Type 0) */
|
|
bios = smbios_get_header(&info, SMBIOS_BIOS_INFORMATION);
|
|
if (!bios)
|
|
return log_msg_ret("cf0", -ENOENT);
|
|
|
|
chid->bios_vendor = smbios_string(&bios->hdr, bios->vendor);
|
|
chid->bios_version = smbios_string(&bios->hdr, bios->bios_ver);
|
|
chid->bios_major = bios->bios_major_release;
|
|
chid->bios_minor = bios->bios_minor_release;
|
|
|
|
/* Extract System Information (Type 1) */
|
|
sys = smbios_get_header(&info, SMBIOS_SYSTEM_INFORMATION);
|
|
if (!sys)
|
|
return log_msg_ret("cf1", -ENOENT);
|
|
chid->manuf = smbios_string(&sys->hdr, sys->manufacturer);
|
|
chid->product_name = smbios_string(&sys->hdr, sys->product_name);
|
|
chid->family = smbios_string(&sys->hdr, sys->family);
|
|
chid->product_sku = smbios_string(&sys->hdr, sys->sku_number);
|
|
|
|
/* Extract Baseboard Information (Type 2) */
|
|
board = smbios_get_header(&info, SMBIOS_BOARD_INFORMATION);
|
|
if (board) {
|
|
chid->board_manuf = smbios_string(&board->hdr,
|
|
board->manufacturer);
|
|
chid->board_product = smbios_string(&board->hdr,
|
|
board->product_name);
|
|
}
|
|
|
|
/* Extract System Enclosure Information (Type 3) */
|
|
encl = smbios_get_header(&info, SMBIOS_SYSTEM_ENCLOSURE);
|
|
if (encl)
|
|
chid->enclosure_type = encl->chassis_type;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* add_item() - Add a string to a buffer if the field is enabled
|
|
*
|
|
* Adds a string and then an '&', but only if the field is enabled in the mask
|
|
*
|
|
* @ptr: Current position in the buffer
|
|
* @end: Pointer to the end of the buffer (one past the last byte)
|
|
* @fields: Bitmask of enabled fields
|
|
* @field: Which field this is (CHID_xxx)
|
|
* @str: String to add, or NULL if none, in which case nothing is added
|
|
* Return: Pointer updated to after the written string and '&' (non-terminated),
|
|
* or @end if out of space
|
|
*/
|
|
static char *add_item(char *ptr, char *end, u32 fields, enum chid_field_t field,
|
|
const char *str)
|
|
{
|
|
char trimmed[256];
|
|
int len;
|
|
|
|
if (!(fields & BIT(field)) || !str)
|
|
return ptr;
|
|
|
|
/* Copy string to temporary buffer and trim spaces */
|
|
strlcpy(trimmed, str, sizeof(trimmed));
|
|
str = strim(trimmed);
|
|
if (!*str)
|
|
return ptr;
|
|
|
|
len = strlen(str);
|
|
if (end - ptr <= len + 1)
|
|
return end;
|
|
memcpy(ptr, str, len);
|
|
ptr += len;
|
|
*ptr++ = '&';
|
|
|
|
return ptr;
|
|
}
|
|
|
|
int chid_generate(int variant, const struct chid_data *data, u8 chid[16])
|
|
{
|
|
const struct chid_variant *var;
|
|
struct uuid namespace = {
|
|
.time_low = cpu_to_be32(0x70ffd812),
|
|
.time_mid = cpu_to_be16(0x4c7f),
|
|
.time_hi_and_version = cpu_to_be16(0x4c7d),
|
|
};
|
|
__le16 utf16_data[1024];
|
|
char *ptr, *end;
|
|
int utf16_chars;
|
|
char str[512];
|
|
u32 fields;
|
|
|
|
/* Validate input parameters */
|
|
if (variant < 0 || variant >= CHID_VARIANT_COUNT || !data || !chid)
|
|
return -EINVAL;
|
|
|
|
var = &variants[variant];
|
|
fields = var->fields;
|
|
ptr = str;
|
|
end = str + sizeof(str) - 1;
|
|
|
|
/* build the input string based on the variant's field mask */
|
|
ptr = add_item(ptr, end, fields, CHID_MANUF, data->manuf);
|
|
ptr = add_item(ptr, end, fields, CHID_FAMILY, data->family);
|
|
ptr = add_item(ptr, end, fields, CHID_PRODUCT_NAME, data->product_name);
|
|
ptr = add_item(ptr, end, fields, CHID_PRODUCT_SKU, data->product_sku);
|
|
ptr = add_item(ptr, end, fields, CHID_BOARD_MANUF, data->board_manuf);
|
|
ptr = add_item(ptr, end, fields, CHID_BOARD_PRODUCT,
|
|
data->board_product);
|
|
ptr = add_item(ptr, end, fields, CHID_BIOS_VENDOR, data->bios_vendor);
|
|
ptr = add_item(ptr, end, fields, CHID_BIOS_VERSION, data->bios_version);
|
|
ptr = add_item(ptr, end, fields, CHID_BIOS_MAJOR,
|
|
simple_itoa(data->bios_major));
|
|
ptr = add_item(ptr, end, fields, CHID_BIOS_MINOR,
|
|
simple_itoa(data->bios_minor));
|
|
ptr = add_item(ptr, end, fields, CHID_ENCLOSURE_TYPE,
|
|
simple_itoa(data->enclosure_type));
|
|
|
|
/* Check if we ran out of buffer space */
|
|
if (ptr == end)
|
|
return log_msg_ret("cgs", -ENOSPC);
|
|
|
|
/* If no fields were added, we can't generate a CHID */
|
|
if (ptr == str)
|
|
return log_msg_ret("cgn", -ENODATA);
|
|
|
|
/* remove the trailing '&' and nul-terminate the string */
|
|
ptr--;
|
|
*ptr = '\0';
|
|
|
|
/*
|
|
* convert to UTF-16LE and generate v5 UUID using Microsoft's namespace
|
|
* This matches Microsoft's ComputerHardwareIds.exe implementation
|
|
*/
|
|
utf16_chars = utf8_to_utf16le(str, utf16_data, ARRAY_SIZE(utf16_data));
|
|
if (utf16_chars < 0)
|
|
return log_msg_ret("cgu", -ENOMEM);
|
|
|
|
gen_v5_guid_be(&namespace, (struct efi_guid *)chid, utf16_data,
|
|
utf16_chars * 2, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
const char *chid_get_field_name(enum chid_field_t field)
|
|
{
|
|
if (field >= CHID_COUNT)
|
|
return "Unknown";
|
|
|
|
return fields[field];
|
|
}
|
|
|
|
u32 chid_get_variant_fields(int variant)
|
|
{
|
|
if (variant < 0 || variant >= CHID_VARIANT_COUNT)
|
|
return 0;
|
|
|
|
return variants[variant].fields;
|
|
}
|
|
|
|
const char *chid_get_variant_name(int variant)
|
|
{
|
|
if (variant < 0 || variant >= CHID_VARIANT_COUNT)
|
|
return "Invalid";
|
|
|
|
return variants[variant].name;
|
|
}
|
|
|
|
bool chid_variant_allowed(enum chid_variant_id variant)
|
|
{
|
|
/* Check for invalid variant */
|
|
if (variant < 0 || variant >= CHID_VARIANT_COUNT)
|
|
return false;
|
|
|
|
/* Check for prohibited variants */
|
|
switch (variant) {
|
|
case CHID_11: /* Manufacturer + Family */
|
|
case CHID_12: /* Manufacturer + EnclosureKind */
|
|
/* Manufacturer + BaseboardManufacturer + BaseboardProduct */
|
|
case CHID_13:
|
|
case CHID_14: /* Manufacturer only */
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* chid_extract() - Extract CHID info from hardware-id node
|
|
*
|
|
* @hw_id_node: devicetree hardware-id node to examine
|
|
* @device_node: parent device node (for compatible string)
|
|
* @dt_chid: structure to fill with extracted data
|
|
*
|
|
* Return: 0 if OK, -ve error code on failure
|
|
*/
|
|
static int chid_extract(ofnode hw_id_node, ofnode device_node,
|
|
struct dt_chid_node *dt_chid)
|
|
{
|
|
const char *compatible;
|
|
const u32 *chid_data;
|
|
int len;
|
|
|
|
/* Get the compatible string from the parent device node */
|
|
compatible = ofnode_read_string(device_node, "compatible");
|
|
if (!compatible)
|
|
return -ENOENT;
|
|
|
|
/* Get CHID variant and fields from hardware-id node */
|
|
dt_chid->variant = ofnode_read_u32_default(hw_id_node, "variant", -1);
|
|
dt_chid->fields = ofnode_read_u32_default(hw_id_node, "fields", 0);
|
|
|
|
/* Get the CHID binary data from hardware-id node */
|
|
chid_data = ofnode_read_prop(hw_id_node, "chid", &len);
|
|
if (!chid_data || len != UUID_LEN)
|
|
return -EINVAL;
|
|
|
|
/* Fill the structure */
|
|
dt_chid->node = hw_id_node;
|
|
dt_chid->compatible = compatible;
|
|
|
|
/* Copy CHID data - handle both byte array and u32 array formats */
|
|
memcpy(dt_chid->chid, chid_data, UUID_LEN);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* check_id() - Check if hardware-id node matches CHID data
|
|
*
|
|
* @hw_id_node: hardware-id node to check
|
|
* @device_node: parent device node (for compatible string)
|
|
* @chid_data: CHID data to match against
|
|
*
|
|
* Return: true if this hardware-id node matches the CHID data, false otherwise
|
|
*/
|
|
static bool check_id(ofnode hw_id_node, ofnode device_node,
|
|
const struct chid_data *chid_data)
|
|
{
|
|
u8 generated_chid[UUID_LEN];
|
|
struct dt_chid_node info;
|
|
int ret;
|
|
|
|
/* Extract CHID info from this hardware-id node */
|
|
ret = chid_extract(hw_id_node, device_node, &info);
|
|
if (ret)
|
|
return false;
|
|
|
|
/* Skip prohibited variants */
|
|
if (!chid_variant_allowed(info.variant)) {
|
|
log_debug("chid: skipping prohibited variant %d (%s)\n",
|
|
info.variant, chid_get_variant_name(info.variant));
|
|
return false;
|
|
}
|
|
|
|
/* Generate CHID for this variant and compare */
|
|
ret = chid_generate(info.variant, chid_data, generated_chid);
|
|
if (!ret) {
|
|
/* Check for exact CHID match */
|
|
if (!memcmp(info.chid, generated_chid, UUID_LEN)) {
|
|
log_debug("chid: matched compatible '%s' (variant=%d)\n",
|
|
info.compatible, info.variant);
|
|
return true;
|
|
}
|
|
log_debug("chid: node %s: variant=%d CHID mismatch\n",
|
|
info.compatible, info.variant);
|
|
} else {
|
|
log_debug("chid: node %s: variant=%d generate failed: %d\n",
|
|
info.compatible, info.variant, ret);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* chid_find_node() - Find a matching CHID device node in devicetree
|
|
*
|
|
* @chid_data: CHID data to match against
|
|
*
|
|
* Searches the devicetree for a device node under /chid that has
|
|
* a hardware-id child node with a CHID that matches the generated CHID.
|
|
*
|
|
* Return: ofnode of matching device, or ofnode_null() if no match
|
|
*/
|
|
static ofnode chid_find_node(const struct chid_data *chid_data)
|
|
{
|
|
ofnode chid_root, node, hw_id_node;
|
|
|
|
/* Find the /chid node */
|
|
chid_root = ofnode_path("/chid");
|
|
if (!ofnode_valid(chid_root))
|
|
return ofnode_null();
|
|
|
|
/* Iterate through device nodes (test-device-1, test-device-2, etc.) */
|
|
ofnode_for_each_subnode(node, chid_root) {
|
|
/* Iterate through hardware-id child nodes */
|
|
ofnode_for_each_subnode(hw_id_node, node) {
|
|
if (check_id(hw_id_node, node, chid_data))
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return ofnode_null();
|
|
}
|
|
|
|
int chid_select_data(const struct chid_data *chid_data, const char **compatp)
|
|
{
|
|
const char *compat;
|
|
ofnode node;
|
|
|
|
if (!chid_data || !compatp) {
|
|
log_debug("chid: invalid parameters\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Find matching device node */
|
|
node = chid_find_node(chid_data);
|
|
if (!ofnode_valid(node)) {
|
|
log_debug("chid: no matching CHID found\n");
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* Get compatible string from the matched device node */
|
|
compat = ofnode_read_string(node, "compatible");
|
|
if (!compat) {
|
|
log_debug("chid: no compatible string found in matched node\n");
|
|
return -ENOENT;
|
|
}
|
|
|
|
*compatp = compat;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int chid_select(const char **compatp)
|
|
{
|
|
struct chid_data smbios_data;
|
|
const char *compat;
|
|
int ret;
|
|
|
|
/* Extract SMBIOS data from current system */
|
|
ret = chid_from_smbios(&smbios_data);
|
|
if (ret) {
|
|
debug("chid: failed to extract SMBIOS data: %d\n", ret);
|
|
return log_msg_ret("cis", ret);
|
|
}
|
|
|
|
ret = chid_select_data(&smbios_data, &compat);
|
|
if (ret)
|
|
return log_msg_ret("csd", ret);
|
|
*compatp = compat;
|
|
|
|
return 0;
|
|
}
|