pkg/smbios: add SMBIOS package
This adds a new SMBIOS package which contains common structures from
SMBIOS as well as corresponding parsers.
I originally explored an approach where I manually designed optimized Go
types for each structure, but that would have led to a huge amount of
code that reading a structure of this type would cause if done
literally. I also considered code generation, but if the generated types
are to be close to the manually-designed ones it would be an incredibly
complex piece of code as well.
Finally I went with a design based on reflection which is much more
compact than the first two and consists of plain Go code at the expense
some niceness in the types.
I called the current types SomeTypeRaw in case I want to come back later
introduce a small layer mapping the current structures into nicer ones.
But for our current purposes the raw ones are good enough already.
This has been tested against our deployment targets, but as the SMBIOS
data contains uniquely identifying information these small tests are not
part of this CL. Sadly I haven't found any public SMBIOS test-cases.
Change-Id: I55d746ada0801de456f2a0eb961821abd9d58fa2
Reviewed-on: https://review.monogon.dev/c/monogon/+/983
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/smbios/BUILD.bazel b/metropolis/pkg/smbios/BUILD.bazel
new file mode 100644
index 0000000..f29f559
--- /dev/null
+++ b/metropolis/pkg/smbios/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "smbios",
+ srcs = [
+ "smbios.go",
+ "structures.go",
+ ],
+ importpath = "source.monogon.dev/metropolis/pkg/smbios",
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/pkg/smbios/smbios.go b/metropolis/pkg/smbios/smbios.go
new file mode 100644
index 0000000..0749110
--- /dev/null
+++ b/metropolis/pkg/smbios/smbios.go
@@ -0,0 +1,267 @@
+// Package smbios implements parsing of SMBIOS data structures.
+// SMBIOS data is commonly populated by platform firmware to convey various
+// metadata (including name, vendor, slots and serial numbers) about the
+// platform to the operating system.
+// The SMBIOS standard is maintained by DMTF and available at
+// https://www.dmtf.org/sites/default/files/standards/documents/
+// DSP0134_3.6.0.pdf. The rest of this package just refers to it as "the
+// standard".
+package smbios
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "reflect"
+ "strings"
+)
+
+// See spec section 6.1.2
+type structureHeader struct {
+ // Types 128 through 256 are reserved for OEM and system-specific use.
+ Type uint8
+ // Length of the structure including this header, excluding the string
+ // set.
+ Length uint8
+ // Unique handle for this structure.
+ Handle uint16
+}
+
+type Structure struct {
+ Type uint8
+ Handle uint16
+ FormattedSection []byte
+ Strings []string
+}
+
+// Table represents a decoded SMBIOS table consisting of its structures.
+// A few known structures are parsed if present, the rest is put into
+// Structures unparsed.
+type Table struct {
+ BIOSInformationRaw *BIOSInformationRaw
+ SystemInformationRaw *SystemInformationRaw
+ BaseboardsInformationRaw []*BaseboardInformationRaw
+ SystemSlotsRaw []*SystemSlotRaw
+ MemoryDevicesRaw []*MemoryDeviceRaw
+
+ Structures []Structure
+}
+
+const (
+ structTypeInactive = 126
+ structTypeEndOfTable = 127
+)
+
+func Unmarshal(table *bufio.Reader) (*Table, error) {
+ var tbl Table
+ for {
+ var structHdr structureHeader
+ if err := binary.Read(table, binary.LittleEndian, &structHdr); err != nil {
+ if err == io.EOF {
+ // Be tolerant of EOFs on structure boundaries even though
+ // the EOT marker is specified as a type 127 structure.
+ break
+ }
+ return nil, fmt.Errorf("unable to read structure header: %w", err)
+ }
+ if int(structHdr.Length) < binary.Size(structHdr) {
+ return nil, fmt.Errorf("invalid structure: header length (%d) smaller than header", structHdr.Length)
+ }
+ if structHdr.Type == structTypeEndOfTable {
+ break
+ }
+ var s Structure
+ s.Type = structHdr.Type
+ s.Handle = structHdr.Handle
+ s.FormattedSection = make([]byte, structHdr.Length-uint8(binary.Size(structHdr)))
+ if _, err := io.ReadFull(table, s.FormattedSection); err != nil {
+ return nil, fmt.Errorf("error while reading structure (handle %d) contents: %w", structHdr.Handle, err)
+ }
+ // Read string-set
+ for {
+ str, err := table.ReadString(0x00)
+ if err != nil {
+ return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err)
+ }
+ // Remove trailing null byte
+ str = strings.TrimSuffix(str, "\x00")
+ // Don't populate a zero-length first string if the string-set is
+ // empty.
+ if len(str) != 0 {
+ s.Strings = append(s.Strings, str)
+ }
+ maybeTerminator, err := table.ReadByte()
+ if err != nil {
+ return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err)
+ }
+ if maybeTerminator == 0 {
+ // We have a valid string-set terminator, exit the loop
+ break
+ }
+ // The next byte was not a terminator, put it back
+ if err := table.UnreadByte(); err != nil {
+ panic(err) // Cannot happen operationally
+ }
+ }
+ switch structHdr.Type {
+ case structTypeInactive:
+ continue
+ case structTypeBIOSInformation:
+ var biosInfo BIOSInformationRaw
+ if err := UnmarshalStructureRaw(s, &biosInfo); err != nil {
+ return nil, fmt.Errorf("failed unmarshaling BIOS Information: %w", err)
+ }
+ tbl.BIOSInformationRaw = &biosInfo
+ case structTypeSystemInformation:
+ var systemInfo SystemInformationRaw
+ if err := UnmarshalStructureRaw(s, &systemInfo); err != nil {
+ return nil, fmt.Errorf("failed unmarshaling System Information: %w", err)
+ }
+ tbl.SystemInformationRaw = &systemInfo
+ case structTypeBaseboardInformation:
+ fmt.Println(s)
+ var baseboardInfo BaseboardInformationRaw
+ if err := UnmarshalStructureRaw(s, &baseboardInfo); err != nil {
+ return nil, fmt.Errorf("failed unmarshaling Baseboard Information: %w", err)
+ }
+ tbl.BaseboardsInformationRaw = append(tbl.BaseboardsInformationRaw, &baseboardInfo)
+ case structTypeSystemSlot:
+ var sysSlot SystemSlotRaw
+ if err := UnmarshalStructureRaw(s, &sysSlot); err != nil {
+ return nil, fmt.Errorf("failed unmarshaling System Slot: %w", err)
+ }
+ tbl.SystemSlotsRaw = append(tbl.SystemSlotsRaw, &sysSlot)
+ case structTypeMemoryDevice:
+ var memoryDev MemoryDeviceRaw
+ if err := UnmarshalStructureRaw(s, &memoryDev); err != nil {
+ return nil, fmt.Errorf("failed unmarshaling Memory Device: %w", err)
+ }
+ tbl.MemoryDevicesRaw = append(tbl.MemoryDevicesRaw, &memoryDev)
+ default:
+ // Just pass through the raw structure
+ tbl.Structures = append(tbl.Structures, s)
+ }
+ }
+ return &tbl, nil
+}
+
+// Version contains a two-part version number consisting of a major and minor
+// version. This is a common structure in SMBIOS.
+type Version struct {
+ Major uint8
+ Minor uint8
+}
+
+func (v *Version) String() string {
+ return fmt.Sprintf("%d.%d", v.Major, v.Minor)
+}
+
+// AtLeast returns true if the version in v is at least the given version.
+func (v *Version) AtLeast(major, minor uint8) bool {
+ return v.Major >= major && v.Minor >= minor
+}
+
+// UnmarshalStructureRaw unmarshals a SMBIOS structure into a Go struct which
+// has some constraints. The first two fields need to be a `uint16 handle` and
+// a `StructureVersion Version` field. After that any number of fields may
+// follow as long as they are either of type `string` (which will be looked up
+// in the string table) or readable by binary.Read. To determine the structure
+// version, the smbios_min_vers struct tag needs to be put on the first field
+// of a newer structure version. The version implicitly starts with 2.0.
+// The version determined is written to the second target struct field.
+// Fields which do not have a fixed size need to be typed as a slice and tagged
+// with smbios_repeat set to the name of the field containing the count. The
+// count field itself needs to be some width of uint.
+func UnmarshalStructureRaw(rawStruct Structure, target any) error {
+ v := reflect.ValueOf(target)
+ if v.Kind() != reflect.Pointer {
+ return errors.New("target needs to be a pointer")
+ }
+ v = v.Elem()
+ if v.Kind() != reflect.Struct {
+ return errors.New("target needs to be a pointer to a struct")
+ }
+ v.Field(0).SetUint(uint64(rawStruct.Handle))
+ r := bytes.NewReader(rawStruct.FormattedSection)
+ completedVersion := Version{Major: 0, Minor: 0}
+ parsingVersion := Version{Major: 2, Minor: 0}
+ numFields := v.NumField()
+ hasAborted := false
+ for i := 2; i < numFields; i++ {
+ fieldType := v.Type().Field(i)
+ if min := fieldType.Tag.Get("smbios_min_ver"); min != "" {
+ var ver Version
+ if _, err := fmt.Sscanf(min, "%d.%d", &ver.Major, &ver.Minor); err != nil {
+ panic(fmt.Sprintf("invalid smbios_min_ver tag in %v: %v", fieldType.Name, err))
+ }
+ completedVersion = parsingVersion
+ parsingVersion = ver
+ }
+ f := v.Field(i)
+
+ if repeat := fieldType.Tag.Get("smbios_repeat"); repeat != "" {
+ repeatCountField := v.FieldByName(repeat)
+ if !repeatCountField.IsValid() {
+ panic(fmt.Sprintf("invalid smbios_repeat tag in %v: no such field %q", fieldType.Name, repeat))
+ }
+ if !repeatCountField.CanUint() {
+ panic(fmt.Sprintf("invalid smbios_repeat tag in %v: referenced field %q is not uint-compatible", fieldType.Name, repeat))
+ }
+ if f.Kind() != reflect.Slice {
+ panic(fmt.Sprintf("cannot repeat a field (%q) which is not a slice", fieldType.Name))
+ }
+ if repeatCountField.Uint() > 65536 {
+ return fmt.Errorf("refusing to read a field repeated more than 65536 times (given %d times)", repeatCountField.Uint())
+ }
+ repeatCount := int(repeatCountField.Uint())
+ f.Set(reflect.MakeSlice(f.Type(), repeatCount, repeatCount))
+ for j := 0; j < repeatCount; j++ {
+ fs := f.Index(j)
+ err := unmarshalField(&rawStruct, fs, r)
+ if errors.Is(err, io.EOF) {
+ hasAborted = true
+ break
+ } else if err != nil {
+ return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
+ }
+ }
+ }
+ err := unmarshalField(&rawStruct, f, r)
+ if errors.Is(err, io.EOF) {
+ hasAborted = true
+ break
+ } else if err != nil {
+ return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
+ }
+ }
+ if !hasAborted {
+ completedVersion = parsingVersion
+ }
+ if completedVersion.Major == 0 {
+ return fmt.Errorf("structure's formatted section (%d bytes) is smaller than its minimal size", len(rawStruct.FormattedSection))
+ }
+ v.Field(1).Set(reflect.ValueOf(completedVersion))
+ return nil
+}
+
+func unmarshalField(rawStruct *Structure, field reflect.Value, r *bytes.Reader) error {
+ if field.Kind() == reflect.String {
+ var stringTableIdx uint8
+ err := binary.Read(r, binary.LittleEndian, &stringTableIdx)
+ if err != nil {
+ return err
+ }
+ if stringTableIdx == 0 {
+ return nil
+ }
+ if int(stringTableIdx)-1 >= len(rawStruct.Strings) {
+ return fmt.Errorf("string index (%d) bigger than string table (%q)", stringTableIdx-1, rawStruct.Strings)
+ }
+ field.SetString(rawStruct.Strings[stringTableIdx-1])
+ return nil
+ }
+ return binary.Read(r, binary.LittleEndian, field.Addr().Interface())
+}
diff --git a/metropolis/pkg/smbios/structures.go b/metropolis/pkg/smbios/structures.go
new file mode 100644
index 0000000..64ef523
--- /dev/null
+++ b/metropolis/pkg/smbios/structures.go
@@ -0,0 +1,177 @@
+package smbios
+
+import (
+ "time"
+)
+
+const (
+ structTypeBIOSInformation = 0
+ structTypeSystemInformation = 1
+ structTypeBaseboardInformation = 2
+ structTypeSystemSlot = 9
+ structTypeMemoryDevice = 17
+)
+
+// BIOSInformationRaw contains decoded data from the BIOS Information structure
+// (SMBIOS Type 0). See Table 6 in the specification for detailed documentation
+// about the individual fields. Note that structure versions 2.1 and 2.2 are
+// "invented" here as both characteristics extensions bytes were optional
+// between 2.0 and 2.4.
+type BIOSInformationRaw struct {
+ Handle uint16
+ StructureVersion Version
+ Vendor string
+ BIOSVersion string
+ BIOSStartingAddressSegment uint16
+ BIOSReleaseDate string
+ BIOSROMSize uint8
+ BIOSCharacteristics uint64
+ BIOSCharacteristicsExtensionByte1 uint8 `smbios_min_ver:"2.1"`
+ BIOSCharacteristicsExtensionByte2 uint8 `smbios_min_ver:"2.2"`
+ SystemBIOSMajorRelease uint8 `smbios_min_ver:"2.4"`
+ SystemBIOSMinorRelease uint8
+ EmbeddedControllerFirmwareMajorRelease uint8
+ EmbeddedControllerFirmwareMinorRelease uint8
+ ExtendedBIOSROMSize uint16 `smbios_min_ver:"3.1"`
+}
+
+// ROMSizeBytes returns the ROM size in bytes
+func (rb *BIOSInformationRaw) ROMSizeBytes() uint64 {
+ if rb.StructureVersion.AtLeast(3, 1) && rb.BIOSROMSize == 0xFF {
+ // Top 2 bits are SI prefix (starting at mega, i.e. 1024^2), lower 14
+ // are value. x*1024^n => x << log2(1024)*n => x << 10*n
+ return uint64(rb.ExtendedBIOSROMSize&0x3fff) << 10 * uint64(rb.ExtendedBIOSROMSize&0xc00+2)
+ } else {
+ // (n+1) * 64KiB
+ return (uint64(rb.BIOSROMSize) + 1) * (64 * 1024)
+ }
+}
+
+// ReleaseDate returns the release date of the BIOS as a time.Time value.
+func (rb *BIOSInformationRaw) ReleaseDate() (time.Time, error) {
+ return time.Parse("01/02/2006", rb.BIOSReleaseDate)
+}
+
+// SystemInformationRaw contains decoded data from the System Information
+// structure (SMBIOS Type 1). See Table 10 in the specification for detailed
+// documentation about the individual fields.
+type SystemInformationRaw struct {
+ Handle uint16
+ StructureVersion Version
+ Manufacturer string
+ ProductName string
+ Version string
+ SerialNumber string
+ UUID [16]byte `smbios_min_ver:"2.1"`
+ WakeupType uint8
+ SKUNumber string `smbios_min_ver:"2.4"`
+ Family string
+}
+
+// BaseboardInformationRaw contains decoded data from the BIOS Information
+// structure (SMBIOS Type 3). See Table 13 in the specification for detailed
+// documentation about the individual fields.
+type BaseboardInformationRaw struct {
+ Handle uint16
+ StructureVersion Version
+ Manufacturer string
+ Product string
+ Version string
+ SerialNumber string
+ AssetTag string `smbios_min_ver:"2.1"`
+ FeatureFlags uint8
+ LocationInChassis string
+ ChassisHandle uint16
+ BoardType uint8
+ NumberOfContainedObjectHandles uint8
+ ContainedObjectHandles []uint16 `smbios_repeat:"NumberOfContainedObjectHandles"`
+}
+
+// SystemSlotRaw contains decoded data from the System Slot structure
+// (SMBIOS Type 9). See Table 44 in the specification for detailed documentation
+// about the individual fields.
+type SystemSlotRaw struct {
+ Handle uint16
+ StructureVersion Version
+ SlotDesignation string
+ SlotType uint8
+ SlotDataBusWidth uint8
+ CurrentUsage uint8
+ SlotLength uint8
+ SlotID uint16
+ SlotCharacteristics1 uint8
+ SlotCharacteristics2 uint8 `smbios_min_ver:"2.1"`
+ SegmentGroupNumber uint16 `smbios_min_ver:"2.6"`
+ BusNumber uint8
+ DeviceFunctionNumber uint8
+ DataBusWidth uint8 `smbios_min_ver:"3.2"`
+ PeerGroupingCount uint8
+ PeerGroups []SystemSlotPeerRaw `smbios_repeat:"PeerGroupingCount"`
+ SlotInformation uint8 `smbios_min_ver:"3.4"`
+ SlotPhysicalWidth uint8
+ SlotPitch uint16
+ SlotHeight uint8 `smbios_min_ver:"3.5"`
+}
+
+type SystemSlotPeerRaw struct {
+ SegmentGroupNumber uint16
+ BusNumber uint8
+ DeviceFunctionNumber uint8
+ DataBusWidth uint8
+}
+
+// MemoryDeviceRaw contains decoded data from the BIOS Information structure
+// (SMBIOS Type 17). See Table 76 in the specification for detailed
+// documentation about the individual fields.
+type MemoryDeviceRaw struct {
+ Handle uint16
+ StructureVersion Version
+ PhysicalMemoryArrayHandle uint16 `smbios_min_ver:"2.1"`
+ MemoryErrorInformationHandle uint16
+ TotalWidth uint16
+ DataWidth uint16
+ Size uint16
+ FormFactor uint8
+ DeviceSet uint8
+ DeviceLocator string
+ BankLocator string
+ MemoryType uint8
+ TypeDetail uint16
+ Speed uint16 `smbios_min_ver:"2.3"`
+ Manufacturer string
+ SerialNumber string
+ AssetTag string
+ PartNumber string
+ Attributes uint8 `smbios_min_ver:"2.6"`
+ ExtendedSize uint32 `smbios_min_ver:"2.7"`
+ ConfiguredMemorySpeed uint16
+ MinimumVoltage uint16 `smbios_min_ver:"2.8"`
+ MaximumVoltage uint16
+ ConfiguredVoltage uint16
+ MemoryTechnology uint8 `smbios_min_ver:"3.2"`
+ MemoryOperatingModeCapability uint16
+ FirmwareVersion uint8
+ ModuleManufacturerID uint16
+ ModuleProductID uint16
+ MemorySubsystemControllerManufacturerID uint16
+ MemorySubsystemControllerProductID uint16
+ NonVolatileSize uint64
+ VolatileSize uint64
+ CacheSize uint64
+ LogicalSize uint64
+ ExtendedSpeed uint32 `smbios_min_ver:"3.3"`
+ ExtendedConfiguredMemorySpeed uint32
+}
+
+func (md *MemoryDeviceRaw) SizeBytes() (uint64, bool) {
+ if md.Size == 0 || md.Size == 0xFFFF {
+ // Device unpopulated / unknown memory, return ok false
+ return 0, false
+ }
+ if md.Size == 0x7FFF && md.StructureVersion.AtLeast(2, 7) {
+ // Bit 31 is reserved, rest is memory size in MiB
+ return uint64(md.ExtendedSize&0x7FFFFFFF) * (1024 * 1024), true
+ }
+ // Bit 15 flips between KiB and MiB, rest is size
+ return uint64(md.Size&0x7FFF) << 10 * uint64(md.Size&0x8000+1), true
+}