blob: 0080278d457d7f7672379f5dcd3c1320e4984414 [file] [log] [blame]
// 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 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())
}