#!/usr/bin/env python3 """Generate raw SMBIOS binary tables for QEMU -smbios file= injection. Produces per-spec SMBIOS structures (DSP0134 3.6) for types that QEMU's smbios_entry_add() cannot build via structured CLI args: - Type 7 (Cache Information) x3 - L1 Data, L2 Unified, L3 Unified - Type 26 (Voltage Probe) x1 - Type 27 (Cooling Device) x1 - Type 28 (Temperature Probe) x1 - Type 29 (Electrical Current Probe) x1 Binary format per structure: [type:u8][length:u8][handle:u16-LE][fields...][strings: NUL-terminated, double-NUL at end] The 'length' byte covers the formatted area only (header + fields, NOT strings). """ import argparse import os import struct import sys from dataclasses import dataclass # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def pack_strings(*strings: str) -> bytes: """Encode the unformatted (string) area of an SMBIOS structure. Each string is NUL-terminated. The area ends with an extra NUL (double-NUL). If there are no strings, emit two NULs (spec requirement). """ if not strings: return b"\x00\x00" return b"".join(s.encode("ascii") + b"\x00" for s in strings) + b"\x00" def encode_cache_size_legacy(size_kb: int) -> int: """Encode cache size for the legacy 16-bit Maximum/Installed Cache Size field. Bits 15: Granularity - 0 = 1 KB, 1 = 64 KB Bits 14-0: Size in granularity units If size_kb < 32768 (fits in 15 bits with 1 KB granularity), use 1 KB. Otherwise use 64 KB granularity. """ if size_kb <= 0x7FFF: return size_kb # 1 KB granularity, bit 15 = 0 return 0x8000 | (size_kb // 64) # 64 KB granularity, bit 15 = 1 def encode_cache_size2(size_kb: int) -> int: """Encode cache size for the 32-bit Maximum/Installed Cache Size 2 field (SMBIOS 3.1+). Bits 31: Granularity - 0 = 1 KB, 1 = 64 KB Bits 30-0: Size in granularity units """ if size_kb <= 0x7FFFFFFF: return size_kb return 0x80000000 | (size_kb // 64) # --------------------------------------------------------------------------- # Type 7 - Cache Information (SMBIOS 3.1+, length = 27) # --------------------------------------------------------------------------- # Cache Configuration (u16) bit layout: # Bits 0-2: Level (0 = L1, 1 = L2, 2 = L3) # Bit 3: Socketed (0 = not socketed) # Bit 4: Reserved # Bits 5-6: Location (0 = Internal) # Bit 7: Enabled/Disabled (1 = Enabled) # Bits 8-9: Operational Mode (01 = Write Back) # Bits 10-15: Reserved CACHE_CFG_L1 = 0x0180 # Level=0(L1), Internal, Enabled, Write-Back CACHE_CFG_L2 = 0x0181 # Level=1(L2), Internal, Enabled, Write-Back CACHE_CFG_L3 = 0x0182 # Level=2(L3), Internal, Enabled, Write-Back # Error Correction Type (u8) ECC_SINGLE_BIT = 5 ECC_MULTI_BIT = 6 # System Cache Type (u8) CACHE_TYPE_INSTRUCTION = 3 CACHE_TYPE_DATA = 4 CACHE_TYPE_UNIFIED = 5 # Associativity (u8) per DSP0134 Table 36: # 0x01 Other, 0x02 Unknown, 0x03 Direct Mapped, 0x04 2-way, # 0x05 4-way, 0x06 Fully Associative, 0x07 8-way, 0x08 12-way, # 0x09 16-way, 0x0A 20-way, 0x0B 24-way, 0x0C 32-way, ... ASSOC_OTHER = 1 ASSOC_8WAY = 7 ASSOC_16WAY = 9 # Defaults for AMD Zen 4/5 (consumer Ryzen): # L1d 8-way, L2 8-way, L3 (V-Cache) 16-way; no ECC on any consumer cache. ASSOC_L1_DEFAULT = ASSOC_8WAY ASSOC_L2_DEFAULT = ASSOC_8WAY ASSOC_L3_DEFAULT = ASSOC_16WAY ECC_DEFAULT = 0x03 # None per DSP0134 Table 39; consumer Ryzen has no cache ECC TYPE7_LENGTH = 27 # SMBIOS 3.1+ with extended size fields @dataclass class CacheEntry: handle: int designation: str config: int size_kb: int ecc: int cache_type: int associativity: int def build_type7(entry: CacheEntry) -> bytes: """Build a Type 7 (Cache Information) SMBIOS binary structure.""" legacy_size = encode_cache_size_legacy(entry.size_kb) extended_size = encode_cache_size2(entry.size_kb) # SRAM type: 0x0002 = Unknown sram_supported = 0x0002 sram_current = 0x0002 formatted = struct.pack( " bytes: """Build a Type 26 (Voltage Probe) SMBIOS binary structure.""" formatted = struct.pack( " bytes: """Build a Type 27 (Cooling Device) SMBIOS binary structure.""" formatted = struct.pack( " bytes: """Build a Type 28 (Temperature Probe) SMBIOS binary structure.""" formatted = struct.pack( " bytes: """Build a Type 29 (Electrical Current Probe) SMBIOS binary structure.""" formatted = struct.pack( " None: """Generate all SMBIOS binary table files into output_dir. assoc_l{1,2,3} and ecc are the SMBIOS Type 7 cache characteristics. Defaults match AMD Zen 4/5 (consumer): 8-way / 8-way / 16-way, no ECC. Override per-host (e.g. server-class CPUs with different associativity or with ECC/parity on L3) via generate-tables.py CLI args. """ os.makedirs(output_dir, exist_ok=True) caches = [ CacheEntry( handle=0x0700, designation="L1 Data Cache", config=CACHE_CFG_L1, size_kb=cache_l1, ecc=ecc, cache_type=CACHE_TYPE_DATA, associativity=assoc_l1, ), CacheEntry( handle=0x0701, designation="L2 Unified Cache", config=CACHE_CFG_L2, size_kb=cache_l2, ecc=ecc, cache_type=CACHE_TYPE_UNIFIED, associativity=assoc_l2, ), CacheEntry( handle=0x0702, designation="L3 Unified Cache", config=CACHE_CFG_L3, size_kb=cache_l3, ecc=ecc, cache_type=CACHE_TYPE_UNIFIED, associativity=assoc_l3, ), ] files = {} for i, entry in enumerate(caches): name = f"type7-l{i + 1}.bin" data = build_type7(entry) path = os.path.join(output_dir, name) with open(path, "wb") as f: f.write(data) files[name] = data probes = [ ("type26.bin", build_type26()), ("type27.bin", build_type27()), ("type28.bin", build_type28()), ("type29.bin", build_type29()), ] for name, data in probes: path = os.path.join(output_dir, name) with open(path, "wb") as f: f.write(data) files[name] = data return files # --------------------------------------------------------------------------- # Verification # --------------------------------------------------------------------------- EXPECTED_FILES = { "type7-l1.bin": (7, TYPE7_LENGTH), "type7-l2.bin": (7, TYPE7_LENGTH), "type7-l3.bin": (7, TYPE7_LENGTH), "type26.bin": (26, TYPE26_LENGTH), "type27.bin": (27, TYPE27_LENGTH), "type28.bin": (28, TYPE28_LENGTH), "type29.bin": (29, TYPE29_LENGTH), } def verify_table(path: str, expected_type: int, expected_length: int) -> list[str]: """Parse back a generated SMBIOS binary file and validate it. Returns a list of error strings (empty = pass). """ errors = [] name = os.path.basename(path) with open(path, "rb") as f: data = f.read() if len(data) < 4: errors.append(f"{name}: file too small ({len(data)} bytes, need >= 4)") return errors stype, slength, _shandle = struct.unpack_from(" max_ref: errors.append( f"{name}: socket designation string ref {ref} out of range [1, {max_ref}]" ) elif stype in (26, 28, 29): ref = struct.unpack_from(" max_ref: errors.append( f"{name}: description string ref {ref} out of range [1, {max_ref}]" ) elif stype == 27: # Description ref at offset 0Fh (last byte of formatted area) ref = struct.unpack_from(" max_ref: errors.append( f"{name}: description string ref {ref} out of range [1, {max_ref}]" ) # Cross-reference check: Type 27 temp_probe_handle must match Type 28 handle. # If the relationship is broken, Win32_Fan returns empty (wbenny 2025 research). if stype == 27 and slength >= TYPE27_LENGTH: temp_handle = struct.unpack_from("= TYPE7_LENGTH: config = struct.unpack_from(" 3: errors.append(f"{name}: cache level {level} out of expected range [1, 3]") ecc = struct.unpack_from(" bool: """Verify all expected SMBIOS binary files in directory. Returns True on success.""" all_errors = [] for filename, (expected_type, expected_length) in EXPECTED_FILES.items(): path = os.path.join(directory, filename) if not os.path.exists(path): all_errors.append(f"{filename}: file not found") continue all_errors.extend(verify_table(path, expected_type, expected_length)) if all_errors: print("SMBIOS verification FAILED:", file=sys.stderr) for err in all_errors: print(f" - {err}", file=sys.stderr) return False print(f"SMBIOS verification passed: {len(EXPECTED_FILES)} tables OK") return True # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="Generate or verify raw SMBIOS binary tables for QEMU injection" ) parser.add_argument( "--verify", metavar="DIR", help="Verify previously generated tables in DIR (no generation)", ) parser.add_argument( "--output-dir", metavar="DIR", help="Output directory for generated .bin files" ) parser.add_argument( "--cache-l1", type=int, default=512, help="L1 data cache size in KB (default: 512)", ) parser.add_argument( "--cache-l2", type=int, default=8192, help="L2 unified cache size in KB (default: 8192)", ) parser.add_argument( "--cache-l3", type=int, default=32768, help="L3 unified cache size in KB (default: 32768)", ) parser.add_argument( "--assoc-l1", type=int, default=ASSOC_L1_DEFAULT, help=f"L1 associativity (SMBIOS Type 7 byte, default: {ASSOC_L1_DEFAULT} = 8-way)", ) parser.add_argument( "--assoc-l2", type=int, default=ASSOC_L2_DEFAULT, help=f"L2 associativity (SMBIOS Type 7 byte, default: {ASSOC_L2_DEFAULT} = 8-way)", ) parser.add_argument( "--assoc-l3", type=int, default=ASSOC_L3_DEFAULT, help=f"L3 associativity (SMBIOS Type 7 byte, default: {ASSOC_L3_DEFAULT} = 16-way V-Cache)", ) parser.add_argument( "--ecc", type=int, default=ECC_DEFAULT, help=f"Error correction type (SMBIOS Type 7 byte, default: {ECC_DEFAULT} = Unknown; consumer Ryzen has no ECC)", ) args = parser.parse_args() if args.verify: if not verify_all(args.verify): sys.exit(1) return if not args.output_dir: parser.error("--output-dir is required when not using --verify") generate_all( args.output_dir, args.cache_l1, args.cache_l2, args.cache_l3, assoc_l1=args.assoc_l1, assoc_l2=args.assoc_l2, assoc_l3=args.assoc_l3, ecc=args.ecc, ) print(f"Generated {len(EXPECTED_FILES)} SMBIOS tables in {args.output_dir}") if __name__ == "__main__": main()