mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
dmi: add BIOS, baseboard, chassis, product family and SKU labels (#2692)
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
ae58ced3b4
commit
cd7acb5618
+24
-5
@@ -16,9 +16,26 @@ None
|
||||
|
||||
## Metrics
|
||||
|
||||
| Name | Description | Type | Labels |
|
||||
|---------------------|-----------------------------------------------------------------------------------------------------|-------|----------------------------------------------------------------------------|
|
||||
| `windows_dmi_info` | A metric with a constant '1' value labeled by SMBIOS Type 1 (System Information) fields. | gauge | `product_name`, `product_serial`, `product_uuid`, `product_version`, `system_vendor` |
|
||||
| Name | Description | Type | Labels |
|
||||
|--------------------|-----------------------------------------------------------------------------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `windows_dmi_info` | A metric with a constant '1' value labeled by SMBIOS identification fields. | gauge | `bios_date`, `bios_release`, `bios_vendor`, `bios_version`, `board_asset_tag`, `board_name`, `board_serial`, `board_vendor`, `board_version`, `chassis_asset_tag`, `chassis_serial`, `chassis_vendor`, `chassis_version`, `product_family`, `product_name`, `product_serial`, `product_sku`, `product_uuid`, `product_version`, `system_vendor` |
|
||||
|
||||
The labels come from these SMBIOS structures, using the first structure of
|
||||
each type like the Linux kernel's `/sys/class/dmi/id` attributes that
|
||||
node_exporter reads:
|
||||
|
||||
| SMBIOS structure | Labels |
|
||||
|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Type 0 (BIOS Information) | `bios_vendor`, `bios_version`, `bios_date`, `bios_release` (2.4+) |
|
||||
| Type 1 (System Information) | `system_vendor`, `product_name`, `product_version`, `product_serial`, `product_uuid`, `product_sku` (2.4+), `product_family` (2.4+) |
|
||||
| Type 2 (Baseboard Information) | `board_vendor`, `board_name`, `board_version`, `board_serial`, `board_asset_tag` |
|
||||
| Type 3 (System Enclosure) | `chassis_vendor`, `chassis_version`, `chassis_serial`, `chassis_asset_tag` |
|
||||
|
||||
Fields the firmware does not provide are exposed as empty labels, which
|
||||
Prometheus drops on ingestion. `bios_release` is the system
|
||||
BIOS major.minor release, e.g. `2.17`; some firmware, such as QEMU's SeaBIOS,
|
||||
reports `0.0`. Firmware placeholders like `Not Specified` or
|
||||
`Default string` are passed through unchanged, as on Linux.
|
||||
|
||||
On VMs the labels reflect the hypervisor-assigned identity; on physical hosts
|
||||
they reflect the hardware SMBIOS data. If SMBIOS data cannot be read (e.g. in
|
||||
@@ -27,11 +44,13 @@ some container environments), the collector will fail to build.
|
||||
### Example metric
|
||||
|
||||
```
|
||||
# HELP windows_dmi_info A metric with a constant '1' value labeled by product_name, product_serial, product_uuid, product_version, system_vendor from SMBIOS Type 1 (System Information).
|
||||
# HELP windows_dmi_info A metric with a constant '1' value labeled by bios_date, bios_release, bios_vendor, bios_version, board_asset_tag, board_name, board_serial, board_vendor, board_version, chassis_asset_tag, chassis_serial, chassis_vendor, chassis_version, product_family, product_name, product_serial, product_sku, product_uuid, product_version, system_vendor from SMBIOS.
|
||||
# TYPE windows_dmi_info gauge
|
||||
windows_dmi_info{product_name="Standard PC (Q35 + ICH9, 2009)",product_serial="Not Specified",product_uuid="12345678-1234-1234-1234-123456789abc",product_version="pc-q35-9.2",system_vendor="QEMU"} 1
|
||||
windows_dmi_info{bios_date="04/01/2014",bios_release="0.0",bios_vendor="SeaBIOS",bios_version="rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org",chassis_vendor="QEMU",chassis_version="pc-q35-9.2",product_name="Standard PC (Q35 + ICH9, 2009)",product_uuid="12345678-1234-1234-1234-123456789abc",product_version="pc-q35-9.2",system_vendor="QEMU"} 1
|
||||
```
|
||||
|
||||
Empty labels are left out of this example for brevity.
|
||||
|
||||
## Useful queries
|
||||
|
||||
### Cross-exporter compatibility
|
||||
|
||||
@@ -69,9 +69,9 @@ func (c *Collector) Close() error {
|
||||
}
|
||||
|
||||
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
info, err := sysinfoapi.GetSMBIOSSystemInfo()
|
||||
info, err := sysinfoapi.GetSMBIOSInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMBIOS system info unavailable: %w", err)
|
||||
return fmt.Errorf("SMBIOS info unavailable: %w", err)
|
||||
}
|
||||
|
||||
// Sanitize label values: firmware strings may contain invalid UTF-8 bytes.
|
||||
@@ -80,17 +80,36 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
return strings.ToValidUTF8(strings.TrimSpace(s), "�")
|
||||
}
|
||||
|
||||
// Label names match node_exporter's node_dmi_info. Fields the firmware
|
||||
// does not provide stay empty; Prometheus drops empty labels on ingestion.
|
||||
desc := prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "info"),
|
||||
"A metric with a constant '1' value labeled by product_name, product_serial, "+
|
||||
"product_uuid, product_version, system_vendor from SMBIOS Type 1 (System Information).",
|
||||
"A metric with a constant '1' value labeled by bios_date, bios_release, bios_vendor, bios_version, "+
|
||||
"board_asset_tag, board_name, board_serial, board_vendor, board_version, chassis_asset_tag, "+
|
||||
"chassis_serial, chassis_vendor, chassis_version, product_family, product_name, product_serial, "+
|
||||
"product_sku, product_uuid, product_version, system_vendor from SMBIOS.",
|
||||
nil,
|
||||
prometheus.Labels{
|
||||
"product_name": cleanLabel(info.ProductName),
|
||||
"product_serial": cleanLabel(info.SerialNumber),
|
||||
"product_uuid": strings.ToLower(cleanLabel(info.UUID)),
|
||||
"product_version": cleanLabel(info.Version),
|
||||
"system_vendor": cleanLabel(info.Manufacturer),
|
||||
"bios_date": cleanLabel(info.BIOS.ReleaseDate),
|
||||
"bios_release": cleanLabel(info.BIOS.Release),
|
||||
"bios_vendor": cleanLabel(info.BIOS.Vendor),
|
||||
"bios_version": cleanLabel(info.BIOS.Version),
|
||||
"board_asset_tag": cleanLabel(info.Board.AssetTag),
|
||||
"board_name": cleanLabel(info.Board.Product),
|
||||
"board_serial": cleanLabel(info.Board.SerialNumber),
|
||||
"board_vendor": cleanLabel(info.Board.Manufacturer),
|
||||
"board_version": cleanLabel(info.Board.Version),
|
||||
"chassis_asset_tag": cleanLabel(info.Chassis.AssetTag),
|
||||
"chassis_serial": cleanLabel(info.Chassis.SerialNumber),
|
||||
"chassis_vendor": cleanLabel(info.Chassis.Manufacturer),
|
||||
"chassis_version": cleanLabel(info.Chassis.Version),
|
||||
"product_family": cleanLabel(info.System.Family),
|
||||
"product_name": cleanLabel(info.System.ProductName),
|
||||
"product_serial": cleanLabel(info.System.SerialNumber),
|
||||
"product_sku": cleanLabel(info.System.SKUNumber),
|
||||
"product_uuid": strings.ToLower(cleanLabel(info.System.UUID)),
|
||||
"product_version": cleanLabel(info.System.Version),
|
||||
"system_vendor": cleanLabel(info.System.Manufacturer),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package dmi_test
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dmi"
|
||||
@@ -37,13 +39,18 @@ func TestCollector(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
labels := make(map[string]string, len(metric.GetLabel()))
|
||||
for _, label := range metric.GetLabel() {
|
||||
if label.GetName() == "product_uuid" {
|
||||
require.NotEmpty(t, label.GetValue(), "SMBIOS system UUID is empty")
|
||||
|
||||
return
|
||||
}
|
||||
labels[label.GetName()] = label.GetValue()
|
||||
}
|
||||
|
||||
t.Fatalf("windows_dmi_info has no product_uuid label: %s", metric)
|
||||
require.ElementsMatch(t, []string{
|
||||
"bios_date", "bios_release", "bios_vendor", "bios_version",
|
||||
"board_asset_tag", "board_name", "board_serial", "board_vendor", "board_version",
|
||||
"chassis_asset_tag", "chassis_serial", "chassis_vendor", "chassis_version",
|
||||
"product_family", "product_name", "product_serial", "product_sku",
|
||||
"product_uuid", "product_version", "system_vendor",
|
||||
}, slices.Collect(maps.Keys(labels)), "windows_dmi_info must carry node_exporter's node_dmi_info label names")
|
||||
require.NotEmpty(t, labels["product_uuid"], "SMBIOS system UUID is empty: %s", metric)
|
||||
require.NotEmpty(t, labels["bios_vendor"], "SMBIOS BIOS vendor is empty: %s", metric)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ var procGetSystemFirmwareTable = kernel32.NewProc("GetSystemFirmwareTable")
|
||||
// SMBIOS table provider signature ('RSMB').
|
||||
const firmwareTableProviderSigRSMB uint32 = 0x52534D42
|
||||
|
||||
// GetSMBIOSSystemInfo retrieves the SMBIOS Type 1 (System Information) data
|
||||
// using the GetSystemFirmwareTable Win32 API. No WMI dependency.
|
||||
func GetSMBIOSSystemInfo() (*SMBIOSSystemInfo, error) {
|
||||
// GetSMBIOSInfo retrieves the SMBIOS BIOS, System, Baseboard and Chassis
|
||||
// information using the GetSystemFirmwareTable Win32 API. No WMI dependency.
|
||||
func GetSMBIOSInfo() (*SMBIOSInfo, error) {
|
||||
data, err := getSystemFirmwareTable(firmwareTableProviderSigRSMB, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetSystemFirmwareTable: %w", err)
|
||||
}
|
||||
|
||||
return parseSMBIOSSystemInfo(data)
|
||||
return parseSMBIOSInfo(data)
|
||||
}
|
||||
|
||||
// getSystemFirmwareTable wraps the GetSystemFirmwareTable Win32 API.
|
||||
|
||||
@@ -21,6 +21,26 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SMBIOSInfo holds the identification strings of the SMBIOS structures that
|
||||
// node_exporter exposes through /sys/class/dmi/id on Linux.
|
||||
type SMBIOSInfo struct {
|
||||
BIOS SMBIOSBIOSInfo
|
||||
System SMBIOSSystemInfo
|
||||
Board SMBIOSBoardInfo
|
||||
Chassis SMBIOSChassisInfo
|
||||
}
|
||||
|
||||
// SMBIOSBIOSInfo holds the fields extracted from the SMBIOS Type 0
|
||||
// (BIOS Information) structure.
|
||||
type SMBIOSBIOSInfo struct {
|
||||
Vendor string
|
||||
Version string
|
||||
ReleaseDate string
|
||||
// Release is the system BIOS major.minor release (SMBIOS 2.4+),
|
||||
// formatted like the Linux kernel's bios_release attribute.
|
||||
Release string
|
||||
}
|
||||
|
||||
// SMBIOSSystemInfo holds the fields extracted from the SMBIOS Type 1
|
||||
// (System Information) structure.
|
||||
type SMBIOSSystemInfo struct {
|
||||
@@ -29,6 +49,27 @@ type SMBIOSSystemInfo struct {
|
||||
Version string
|
||||
SerialNumber string
|
||||
UUID string
|
||||
SKUNumber string
|
||||
Family string
|
||||
}
|
||||
|
||||
// SMBIOSBoardInfo holds the fields extracted from the first SMBIOS Type 2
|
||||
// (Baseboard Information) structure.
|
||||
type SMBIOSBoardInfo struct {
|
||||
Manufacturer string
|
||||
Product string
|
||||
Version string
|
||||
SerialNumber string
|
||||
AssetTag string
|
||||
}
|
||||
|
||||
// SMBIOSChassisInfo holds the fields extracted from the first SMBIOS Type 3
|
||||
// (System Enclosure or Chassis) structure.
|
||||
type SMBIOSChassisInfo struct {
|
||||
Manufacturer string
|
||||
Version string
|
||||
SerialNumber string
|
||||
AssetTag string
|
||||
}
|
||||
|
||||
// rawSMBIOSDataHeaderSize is the size of the RawSMBIOSData header returned
|
||||
@@ -37,9 +78,48 @@ type SMBIOSSystemInfo struct {
|
||||
// DMARevision (1) + Length (4) = 8 bytes.
|
||||
const rawSMBIOSDataHeaderSize = 8
|
||||
|
||||
// parseSMBIOSSystemInfo parses the raw RSMB firmware table and extracts
|
||||
// the Type 1 (System Information) structure.
|
||||
func parseSMBIOSSystemInfo(data []byte) (*SMBIOSSystemInfo, error) {
|
||||
const (
|
||||
smbiosTypeBIOS = 0
|
||||
smbiosTypeSystem = 1
|
||||
smbiosTypeBaseboard = 2
|
||||
smbiosTypeChassis = 3
|
||||
)
|
||||
|
||||
// smbiosStructure is one structure of the SMBIOS table: its formatted area
|
||||
// and the strings of its unformatted area.
|
||||
type smbiosStructure struct {
|
||||
formatted []byte
|
||||
strs []string
|
||||
}
|
||||
|
||||
// byteAt returns the byte at offset in the formatted area and whether the
|
||||
// structure is long enough to contain it.
|
||||
func (s smbiosStructure) byteAt(offset int) (byte, bool) {
|
||||
if offset >= len(s.formatted) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return s.formatted[offset], true
|
||||
}
|
||||
|
||||
// stringAt resolves the string index stored at offset in the formatted area.
|
||||
// It returns an empty string when the structure is too short for the field.
|
||||
func (s smbiosStructure) stringAt(offset int) string {
|
||||
index, ok := s.byteAt(offset)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return getStringByIndex(s.strs, index)
|
||||
}
|
||||
|
||||
// parseSMBIOSInfo parses the raw RSMB firmware table and extracts the
|
||||
// Type 0 (BIOS), Type 1 (System), Type 2 (Baseboard) and Type 3 (Chassis)
|
||||
// structures. Only the first structure of each type is used, matching the
|
||||
// Linux kernel. Type 1 is required; the others are optional. The walk
|
||||
// continues past the Type 127 end-of-table marker, as the Linux kernel does
|
||||
// for SMBIOS 2.x tables, since some firmware places structures after it.
|
||||
func parseSMBIOSInfo(data []byte) (*SMBIOSInfo, error) {
|
||||
if len(data) < rawSMBIOSDataHeaderSize {
|
||||
return nil, errors.New("SMBIOS data too short for header")
|
||||
}
|
||||
@@ -56,7 +136,11 @@ func parseSMBIOSSystemInfo(data []byte) (*SMBIOSSystemInfo, error) {
|
||||
|
||||
tableData = tableData[:tableLen]
|
||||
|
||||
// Walk structures looking for Type 1.
|
||||
info := &SMBIOSInfo{}
|
||||
seen := map[byte]bool{}
|
||||
|
||||
var walkErr error
|
||||
|
||||
offset := 0
|
||||
for offset < len(tableData) {
|
||||
if offset+4 > len(tableData) {
|
||||
@@ -88,41 +172,115 @@ func parseSMBIOSSystemInfo(data []byte) (*SMBIOSSystemInfo, error) {
|
||||
}
|
||||
|
||||
if !foundTerminator {
|
||||
return nil, errors.New("SMBIOS string area not properly terminated (missing double-NUL)")
|
||||
walkErr = errors.New("SMBIOS string area not properly terminated (missing double-NUL)")
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if structType == 1 && structLen >= 0x08 {
|
||||
// Type 1: System Information
|
||||
// Offsets (within the formatted area):
|
||||
// 04h: Manufacturer (string index)
|
||||
// 05h: Product Name (string index)
|
||||
// 06h: Version (string index)
|
||||
// 07h: Serial Number (string index)
|
||||
// 08h-17h: UUID (16 bytes) — present only when structLen >= 0x19
|
||||
strs := extractStrings(tableData[stringsStart:stringsEnd])
|
||||
|
||||
info := &SMBIOSSystemInfo{
|
||||
Manufacturer: getStringByIndex(strs, tableData[offset+0x04]),
|
||||
ProductName: getStringByIndex(strs, tableData[offset+0x05]),
|
||||
Version: getStringByIndex(strs, tableData[offset+0x06]),
|
||||
SerialNumber: getStringByIndex(strs, tableData[offset+0x07]),
|
||||
if structType <= smbiosTypeChassis && !seen[structType] {
|
||||
s := smbiosStructure{
|
||||
formatted: tableData[offset : offset+structLen],
|
||||
strs: extractStrings(tableData[stringsStart:stringsEnd]),
|
||||
}
|
||||
|
||||
// UUID is only available in SMBIOS 2.1+ (structLen >= 0x19).
|
||||
if structLen >= 0x19 {
|
||||
// SMBIOS 2.6+ uses little-endian byte order for the first three UUID fields;
|
||||
// earlier versions use network (big-endian) order throughout.
|
||||
smbios26Plus := smbiosMajor > 2 || (smbiosMajor == 2 && smbiosMinor >= 6)
|
||||
info.UUID = formatUUID(tableData[offset+0x08:offset+0x18], smbios26Plus)
|
||||
switch structType {
|
||||
case smbiosTypeBIOS:
|
||||
seen[structType] = true
|
||||
info.BIOS = parseSMBIOSBIOSInfo(s)
|
||||
case smbiosTypeSystem:
|
||||
// The Type 1 fields this parser reads require at least 8 bytes.
|
||||
if structLen >= 0x08 {
|
||||
seen[structType] = true
|
||||
info.System = parseSMBIOSSystemInfo(s, smbiosMajor, smbiosMinor)
|
||||
}
|
||||
case smbiosTypeBaseboard:
|
||||
seen[structType] = true
|
||||
info.Board = SMBIOSBoardInfo{
|
||||
Manufacturer: s.stringAt(0x04),
|
||||
Product: s.stringAt(0x05),
|
||||
Version: s.stringAt(0x06),
|
||||
SerialNumber: s.stringAt(0x07),
|
||||
AssetTag: s.stringAt(0x08),
|
||||
}
|
||||
case smbiosTypeChassis:
|
||||
seen[structType] = true
|
||||
info.Chassis = SMBIOSChassisInfo{
|
||||
Manufacturer: s.stringAt(0x04),
|
||||
Version: s.stringAt(0x06),
|
||||
SerialNumber: s.stringAt(0x07),
|
||||
AssetTag: s.stringAt(0x08),
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
offset = stringsEnd
|
||||
}
|
||||
|
||||
return nil, errors.New("SMBIOS Type 1 (System Information) structure not found")
|
||||
// A malformed structure after Type 1 only loses the structures that follow it.
|
||||
if !seen[smbiosTypeSystem] {
|
||||
if walkErr != nil {
|
||||
return nil, walkErr
|
||||
}
|
||||
|
||||
return nil, errors.New("SMBIOS Type 1 (System Information) structure not found")
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseSMBIOSBIOSInfo decodes a Type 0 (BIOS Information) structure.
|
||||
//
|
||||
// 04h: Vendor (string index)
|
||||
// 05h: BIOS Version (string index)
|
||||
// 08h: BIOS Release Date (string index)
|
||||
// 14h: System BIOS Major Release (2.4+)
|
||||
// 15h: System BIOS Minor Release (2.4+)
|
||||
func parseSMBIOSBIOSInfo(s smbiosStructure) SMBIOSBIOSInfo {
|
||||
bios := SMBIOSBIOSInfo{
|
||||
Vendor: s.stringAt(0x04),
|
||||
Version: s.stringAt(0x05),
|
||||
ReleaseDate: s.stringAt(0x08),
|
||||
}
|
||||
|
||||
major, okMajor := s.byteAt(0x14)
|
||||
minor, okMinor := s.byteAt(0x15)
|
||||
|
||||
// 0xFF in both fields means the system does not support them.
|
||||
if okMajor && okMinor && (major != 0xFF || minor != 0xFF) {
|
||||
bios.Release = fmt.Sprintf("%d.%d", major, minor)
|
||||
}
|
||||
|
||||
return bios
|
||||
}
|
||||
|
||||
// parseSMBIOSSystemInfo decodes a Type 1 (System Information) structure.
|
||||
//
|
||||
// 04h: Manufacturer (string index)
|
||||
// 05h: Product Name (string index)
|
||||
// 06h: Version (string index)
|
||||
// 07h: Serial Number (string index)
|
||||
// 08h-17h: UUID (16 bytes, 2.1+)
|
||||
// 19h: SKU Number (string index, 2.4+)
|
||||
// 1Ah: Family (string index, 2.4+)
|
||||
func parseSMBIOSSystemInfo(s smbiosStructure, smbiosMajor, smbiosMinor byte) SMBIOSSystemInfo {
|
||||
system := SMBIOSSystemInfo{
|
||||
Manufacturer: s.stringAt(0x04),
|
||||
ProductName: s.stringAt(0x05),
|
||||
Version: s.stringAt(0x06),
|
||||
SerialNumber: s.stringAt(0x07),
|
||||
SKUNumber: s.stringAt(0x19),
|
||||
Family: s.stringAt(0x1A),
|
||||
}
|
||||
|
||||
// UUID is only available in SMBIOS 2.1+ (structLen >= 0x19).
|
||||
if len(s.formatted) >= 0x19 {
|
||||
// SMBIOS 2.6+ uses little-endian byte order for the first three UUID fields;
|
||||
// earlier versions use network (big-endian) order throughout.
|
||||
smbios26Plus := smbiosMajor > 2 || (smbiosMajor == 2 && smbiosMinor >= 6)
|
||||
system.UUID = formatUUID(s.formatted[0x08:0x18], smbios26Plus)
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
// extractStrings splits the unformatted string area into individual strings.
|
||||
|
||||
@@ -153,13 +153,13 @@ func TestParseSMBIOSSystemInfo(t *testing.T) {
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
info, err := parseSMBIOSInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "QEMU", info.Manufacturer)
|
||||
assert.Equal(t, "Standard PC (Q35 + ICH9, 2009)", info.ProductName)
|
||||
assert.Equal(t, "pc-q35-9.2", info.Version)
|
||||
assert.Equal(t, "SN-12345", info.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
assert.Equal(t, "QEMU", info.System.Manufacturer)
|
||||
assert.Equal(t, "Standard PC (Q35 + ICH9, 2009)", info.System.ProductName)
|
||||
assert.Equal(t, "pc-q35-9.2", info.System.Version)
|
||||
assert.Equal(t, "SN-12345", info.System.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.System.UUID)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoPre26(t *testing.T) {
|
||||
@@ -196,10 +196,10 @@ func TestParseSMBIOSSystemInfoPre26(t *testing.T) {
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
info, err := parseSMBIOSInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
assert.Equal(t, "QEMU", info.Manufacturer)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.System.UUID)
|
||||
assert.Equal(t, "QEMU", info.System.Manufacturer)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoSMBIOS20(t *testing.T) {
|
||||
@@ -233,13 +233,13 @@ func TestParseSMBIOSSystemInfoSMBIOS20(t *testing.T) {
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
info, err := parseSMBIOSInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OldBIOS", info.Manufacturer)
|
||||
assert.Equal(t, "Legacy PC", info.ProductName)
|
||||
assert.Equal(t, "v0.1", info.Version)
|
||||
assert.Equal(t, "SN-OLD", info.SerialNumber)
|
||||
assert.Empty(t, info.UUID, "UUID should be empty for 8-byte SMBIOS 2.0 Type 1 structures")
|
||||
assert.Equal(t, "OldBIOS", info.System.Manufacturer)
|
||||
assert.Equal(t, "Legacy PC", info.System.ProductName)
|
||||
assert.Equal(t, "v0.1", info.System.Version)
|
||||
assert.Equal(t, "SN-OLD", info.System.SerialNumber)
|
||||
assert.Empty(t, info.System.UUID, "UUID should be empty for 8-byte SMBIOS 2.0 Type 1 structures")
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoWithType0Preceding(t *testing.T) {
|
||||
@@ -286,13 +286,13 @@ func TestParseSMBIOSSystemInfoWithType0Preceding(t *testing.T) {
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
info, err := parseSMBIOSInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Dell Inc.", info.Manufacturer)
|
||||
assert.Equal(t, "PowerEdge R740", info.ProductName)
|
||||
assert.Equal(t, "v2.0", info.Version)
|
||||
assert.Equal(t, "SN-ABCDE", info.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
assert.Equal(t, "Dell Inc.", info.System.Manufacturer)
|
||||
assert.Equal(t, "PowerEdge R740", info.System.ProductName)
|
||||
assert.Equal(t, "v2.0", info.System.Version)
|
||||
assert.Equal(t, "SN-ABCDE", info.System.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.System.UUID)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoUnterminatedStrings(t *testing.T) {
|
||||
@@ -324,12 +324,184 @@ func TestParseSMBIOSSystemInfoUnterminatedStrings(t *testing.T) {
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
_, err := parseSMBIOSSystemInfo(data)
|
||||
_, err := parseSMBIOSInfo(data)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not properly terminated")
|
||||
}
|
||||
|
||||
func FuzzParseSMBIOSSystemInfo(f *testing.F) {
|
||||
// buildRSMB wraps SMBIOS structures in a RawSMBIOSData header for SMBIOS 3.2.
|
||||
func buildRSMB(structures ...[]byte) []byte {
|
||||
tableData := slices.Concat(structures...)
|
||||
tableLen := len(tableData)
|
||||
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x03, // SMBIOSMajorVersion
|
||||
0x02, // SMBIOSMinorVersion
|
||||
0x00, // DMARevision
|
||||
byte(tableLen), byte(tableLen >> 8), byte(tableLen >> 16), byte(tableLen >> 24),
|
||||
}
|
||||
|
||||
return slices.Concat(header, tableData)
|
||||
}
|
||||
|
||||
// smbiosStruct builds one SMBIOS structure with a formatted area of length
|
||||
// bytes, the given fields set and the given strings.
|
||||
func smbiosStruct(structType byte, length int, fields map[int]byte, strs ...string) []byte {
|
||||
formatted := make([]byte, length)
|
||||
formatted[0] = structType
|
||||
formatted[1] = byte(length)
|
||||
|
||||
for offset, value := range fields {
|
||||
formatted[offset] = value
|
||||
}
|
||||
|
||||
var stringArea []byte
|
||||
|
||||
for _, str := range strs {
|
||||
stringArea = append(stringArea, str...)
|
||||
stringArea = append(stringArea, 0)
|
||||
}
|
||||
|
||||
if len(stringArea) == 0 {
|
||||
stringArea = []byte{0}
|
||||
}
|
||||
|
||||
return slices.Concat(formatted, stringArea, []byte{0})
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoAllTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type0 := smbiosStruct(0, 0x1A,
|
||||
map[int]byte{0x04: 1, 0x05: 2, 0x08: 3, 0x14: 2, 0x15: 17},
|
||||
"American Megatrends Inc.", "1.4.2", "05/23/2024",
|
||||
)
|
||||
type1 := smbiosStruct(1, 0x1B,
|
||||
map[int]byte{0x04: 1, 0x05: 2, 0x06: 3, 0x07: 4, 0x19: 5, 0x1A: 6},
|
||||
"Dell Inc.", "PowerEdge R740", "v2.0", "SN-SYS", "SKU=0715", "PowerEdge",
|
||||
)
|
||||
type2 := smbiosStruct(2, 0x0F,
|
||||
map[int]byte{0x04: 1, 0x05: 2, 0x06: 3, 0x07: 4, 0x08: 5},
|
||||
"Dell Inc.", "0WXD1Y", "A05", "SN-BOARD", "BOARD-TAG",
|
||||
)
|
||||
// A second baseboard must not override the first.
|
||||
type2b := smbiosStruct(2, 0x0F, map[int]byte{0x04: 1, 0x05: 2}, "Other", "Second")
|
||||
type3 := smbiosStruct(3, 0x15,
|
||||
map[int]byte{0x04: 1, 0x05: 0x17, 0x06: 2, 0x07: 3, 0x08: 4},
|
||||
"Dell Inc.", "C1", "SN-CHASSIS", "CHASSIS-TAG",
|
||||
)
|
||||
end := smbiosStruct(127, 4, nil)
|
||||
|
||||
info, err := parseSMBIOSInfo(buildRSMB(type0, end, type1, type2, type2b, type3))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, SMBIOSBIOSInfo{
|
||||
Vendor: "American Megatrends Inc.",
|
||||
Version: "1.4.2",
|
||||
ReleaseDate: "05/23/2024",
|
||||
Release: "2.17",
|
||||
}, info.BIOS)
|
||||
assert.Equal(t, "SKU=0715", info.System.SKUNumber)
|
||||
assert.Equal(t, "PowerEdge", info.System.Family)
|
||||
assert.Equal(t, "PowerEdge R740", info.System.ProductName)
|
||||
assert.Equal(t, SMBIOSBoardInfo{
|
||||
Manufacturer: "Dell Inc.",
|
||||
Product: "0WXD1Y",
|
||||
Version: "A05",
|
||||
SerialNumber: "SN-BOARD",
|
||||
AssetTag: "BOARD-TAG",
|
||||
}, info.Board)
|
||||
assert.Equal(t, SMBIOSChassisInfo{
|
||||
Manufacturer: "Dell Inc.",
|
||||
Version: "C1",
|
||||
SerialNumber: "SN-CHASSIS",
|
||||
AssetTag: "CHASSIS-TAG",
|
||||
}, info.Chassis)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoShortStructures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// SMBIOS 2.0-sized structures: no BIOS release, SKU, family or asset tags.
|
||||
type0 := smbiosStruct(0, 0x12, map[int]byte{0x04: 1, 0x05: 2, 0x08: 3}, "Vendor", "1.0", "01/01/2000")
|
||||
type1 := smbiosStruct(1, 0x08, map[int]byte{0x04: 1}, "Maker")
|
||||
type2 := smbiosStruct(2, 0x08, map[int]byte{0x04: 1, 0x05: 2, 0x06: 3, 0x07: 4}, "BV", "BN", "BVer", "BS")
|
||||
type3 := smbiosStruct(3, 0x08, map[int]byte{0x04: 1, 0x06: 2, 0x07: 3}, "CV", "CVer", "CS")
|
||||
|
||||
info, err := parseSMBIOSInfo(buildRSMB(type0, type1, type2, type3))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Vendor", info.BIOS.Vendor)
|
||||
assert.Equal(t, "01/01/2000", info.BIOS.ReleaseDate)
|
||||
assert.Empty(t, info.BIOS.Release)
|
||||
assert.Empty(t, info.System.SKUNumber)
|
||||
assert.Empty(t, info.System.Family)
|
||||
assert.Equal(t, "BS", info.Board.SerialNumber)
|
||||
assert.Empty(t, info.Board.AssetTag)
|
||||
assert.Equal(t, "CS", info.Chassis.SerialNumber)
|
||||
assert.Empty(t, info.Chassis.AssetTag)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoBIOSReleaseNotSupported(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type0 := smbiosStruct(0, 0x18, map[int]byte{0x04: 1, 0x14: 0xFF, 0x15: 0xFF}, "Vendor")
|
||||
type1 := smbiosStruct(1, 0x08, map[int]byte{0x04: 1}, "Maker")
|
||||
|
||||
info, err := parseSMBIOSInfo(buildRSMB(type0, type1))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, info.BIOS.Release)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoBIOSReleaseMinorMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Length 0x15 covers the major release byte but not the minor one.
|
||||
type0 := smbiosStruct(0, 0x15, map[int]byte{0x04: 1, 0x14: 2}, "Vendor")
|
||||
type1 := smbiosStruct(1, 0x08, map[int]byte{0x04: 1}, "Maker")
|
||||
|
||||
info, err := parseSMBIOSInfo(buildRSMB(type0, type1))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, info.BIOS.Release)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoMalformedBeforeSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type0 := smbiosStruct(0, 0x12, map[int]byte{0x04: 1}, "Vendor")
|
||||
broken := slices.Concat(smbiosStruct(2, 0x08, nil)[:0x08], []byte("Board\x00"))
|
||||
|
||||
_, err := parseSMBIOSInfo(buildRSMB(type0, broken))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not properly terminated")
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoMalformedAfterSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A structure with an unterminated string area after Type 1 must not
|
||||
// discard the System Information that was already parsed.
|
||||
type1 := smbiosStruct(1, 0x08, map[int]byte{0x04: 1}, "Maker")
|
||||
broken := slices.Concat(smbiosStruct(2, 0x08, map[int]byte{0x04: 1})[:0x08], []byte("Board\x00"))
|
||||
|
||||
info, err := parseSMBIOSInfo(buildRSMB(type1, broken))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Maker", info.System.Manufacturer)
|
||||
assert.Empty(t, info.Board.Manufacturer)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSInfoMissingSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type0 := smbiosStruct(0, 0x12, map[int]byte{0x04: 1}, "Vendor")
|
||||
|
||||
_, err := parseSMBIOSInfo(buildRSMB(type0))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Type 1")
|
||||
}
|
||||
|
||||
func FuzzParseSMBIOSInfo(f *testing.F) {
|
||||
// Seed with a valid RSMB blob.
|
||||
header := []byte{
|
||||
0x00, 0x03, 0x02, 0x00,
|
||||
@@ -356,12 +528,25 @@ func FuzzParseSMBIOSSystemInfo(f *testing.F) {
|
||||
|
||||
f.Add(slices.Concat(header, tableData))
|
||||
|
||||
// Seed with every parsed structure type and a partial BIOS release.
|
||||
f.Add(buildRSMB(
|
||||
smbiosStruct(0, 0x1A, map[int]byte{0x04: 1, 0x05: 2, 0x08: 3, 0x14: 2, 0x15: 17}, "Vendor", "1.0", "01/01/2024"),
|
||||
smbiosStruct(127, 4, nil),
|
||||
smbiosStruct(1, 0x1B, map[int]byte{0x04: 1, 0x19: 2, 0x1A: 3}, "Maker", "SKU", "Family"),
|
||||
smbiosStruct(2, 0x0F, map[int]byte{0x04: 1, 0x08: 2}, "Board", "Tag"),
|
||||
smbiosStruct(3, 0x15, map[int]byte{0x04: 1, 0x05: 0x17}, "Chassis"),
|
||||
))
|
||||
f.Add(buildRSMB(
|
||||
smbiosStruct(0, 0x15, map[int]byte{0x04: 1, 0x14: 2}, "Vendor"),
|
||||
smbiosStruct(1, 0x08, map[int]byte{0x04: 1}, "Maker"),
|
||||
))
|
||||
|
||||
// Also seed with an empty input and a truncated one.
|
||||
f.Add([]byte{})
|
||||
f.Add(header[:4])
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
// Must not panic on any input.
|
||||
_, _ = parseSMBIOSSystemInfo(data)
|
||||
_, _ = parseSMBIOSInfo(data)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user