treewide: introduce osbase package and move things around
All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.
Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/smbios/BUILD.bazel b/osbase/smbios/BUILD.bazel
new file mode 100644
index 0000000..b974095
--- /dev/null
+++ b/osbase/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/osbase/smbios",
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/smbios/smbios.go b/osbase/smbios/smbios.go
new file mode 100644
index 0000000..e9bcfc3
--- /dev/null
+++ b/osbase/smbios/smbios.go
@@ -0,0 +1,269 @@
+// 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:
+ 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 {
+ if v.Major > major {
+ return true
+ }
+ 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 minVer := fieldType.Tag.Get("smbios_min_ver"); minVer != "" {
+ var ver Version
+ if _, err := fmt.Sscanf(minVer, "%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/osbase/smbios/structures.go b/osbase/smbios/structures.go
new file mode 100644
index 0000000..4c75709
--- /dev/null
+++ b/osbase/smbios/structures.go
@@ -0,0 +1,191 @@
+package smbios
+
+import (
+ "time"
+)
+
+const (
+ structTypeBIOSInformation = 0
+ structTypeSystemInformation = 1
+ structTypeBaseboardInformation = 2
+ structTypeSystemSlot = 9
+ structTypeMemoryDevice = 17
+)
+
+// UEFISpecificationSupported is a bitmask for accessing the third bit,
+// which displays the support of UEFI in the smbios structure. For more
+// information check the SMBIOS documentation at Table 7.1.2.2 Bit 3.
+const UEFISpecificationSupported = 1 << 3
+
+// 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
+}
+
+const (
+ kibLeftShift = 10 // 2^10 = 1KiB
+ mibLeftShift = 20 // 2^20 = 1MiB
+)
+
+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) << mibLeftShift, true
+ }
+ // Bit 15 flips between MiB and KiB, rest is size
+ var shift uint64 = mibLeftShift
+ if (md.Size & 0x8000) != 0 { // Bit set means KiB
+ shift = kibLeftShift
+ }
+ return uint64(md.Size&0x7FFF) << shift, true
+}