Lorenz Brun | f9c65e9 | 2022-11-22 12:50:56 +0000 | [diff] [blame] | 1 | // Package smbios implements parsing of SMBIOS data structures. |
| 2 | // SMBIOS data is commonly populated by platform firmware to convey various |
| 3 | // metadata (including name, vendor, slots and serial numbers) about the |
| 4 | // platform to the operating system. |
| 5 | // The SMBIOS standard is maintained by DMTF and available at |
| 6 | // https://www.dmtf.org/sites/default/files/standards/documents/ |
| 7 | // DSP0134_3.6.0.pdf. The rest of this package just refers to it as "the |
| 8 | // standard". |
| 9 | package smbios |
| 10 | |
| 11 | import ( |
| 12 | "bufio" |
| 13 | "bytes" |
| 14 | "encoding/binary" |
| 15 | "errors" |
| 16 | "fmt" |
| 17 | "io" |
| 18 | "reflect" |
| 19 | "strings" |
| 20 | ) |
| 21 | |
| 22 | // See spec section 6.1.2 |
| 23 | type structureHeader struct { |
| 24 | // Types 128 through 256 are reserved for OEM and system-specific use. |
| 25 | Type uint8 |
| 26 | // Length of the structure including this header, excluding the string |
| 27 | // set. |
| 28 | Length uint8 |
| 29 | // Unique handle for this structure. |
| 30 | Handle uint16 |
| 31 | } |
| 32 | |
| 33 | type Structure struct { |
| 34 | Type uint8 |
| 35 | Handle uint16 |
| 36 | FormattedSection []byte |
| 37 | Strings []string |
| 38 | } |
| 39 | |
| 40 | // Table represents a decoded SMBIOS table consisting of its structures. |
| 41 | // A few known structures are parsed if present, the rest is put into |
| 42 | // Structures unparsed. |
| 43 | type Table struct { |
| 44 | BIOSInformationRaw *BIOSInformationRaw |
| 45 | SystemInformationRaw *SystemInformationRaw |
| 46 | BaseboardsInformationRaw []*BaseboardInformationRaw |
| 47 | SystemSlotsRaw []*SystemSlotRaw |
| 48 | MemoryDevicesRaw []*MemoryDeviceRaw |
| 49 | |
| 50 | Structures []Structure |
| 51 | } |
| 52 | |
| 53 | const ( |
| 54 | structTypeInactive = 126 |
| 55 | structTypeEndOfTable = 127 |
| 56 | ) |
| 57 | |
| 58 | func Unmarshal(table *bufio.Reader) (*Table, error) { |
| 59 | var tbl Table |
| 60 | for { |
| 61 | var structHdr structureHeader |
| 62 | if err := binary.Read(table, binary.LittleEndian, &structHdr); err != nil { |
| 63 | if err == io.EOF { |
| 64 | // Be tolerant of EOFs on structure boundaries even though |
| 65 | // the EOT marker is specified as a type 127 structure. |
| 66 | break |
| 67 | } |
| 68 | return nil, fmt.Errorf("unable to read structure header: %w", err) |
| 69 | } |
| 70 | if int(structHdr.Length) < binary.Size(structHdr) { |
| 71 | return nil, fmt.Errorf("invalid structure: header length (%d) smaller than header", structHdr.Length) |
| 72 | } |
| 73 | if structHdr.Type == structTypeEndOfTable { |
| 74 | break |
| 75 | } |
| 76 | var s Structure |
| 77 | s.Type = structHdr.Type |
| 78 | s.Handle = structHdr.Handle |
| 79 | s.FormattedSection = make([]byte, structHdr.Length-uint8(binary.Size(structHdr))) |
| 80 | if _, err := io.ReadFull(table, s.FormattedSection); err != nil { |
| 81 | return nil, fmt.Errorf("error while reading structure (handle %d) contents: %w", structHdr.Handle, err) |
| 82 | } |
| 83 | // Read string-set |
| 84 | for { |
| 85 | str, err := table.ReadString(0x00) |
| 86 | if err != nil { |
| 87 | return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err) |
| 88 | } |
| 89 | // Remove trailing null byte |
| 90 | str = strings.TrimSuffix(str, "\x00") |
| 91 | // Don't populate a zero-length first string if the string-set is |
| 92 | // empty. |
| 93 | if len(str) != 0 { |
| 94 | s.Strings = append(s.Strings, str) |
| 95 | } |
| 96 | maybeTerminator, err := table.ReadByte() |
| 97 | if err != nil { |
| 98 | return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err) |
| 99 | } |
| 100 | if maybeTerminator == 0 { |
| 101 | // We have a valid string-set terminator, exit the loop |
| 102 | break |
| 103 | } |
| 104 | // The next byte was not a terminator, put it back |
| 105 | if err := table.UnreadByte(); err != nil { |
| 106 | panic(err) // Cannot happen operationally |
| 107 | } |
| 108 | } |
| 109 | switch structHdr.Type { |
| 110 | case structTypeInactive: |
| 111 | continue |
| 112 | case structTypeBIOSInformation: |
| 113 | var biosInfo BIOSInformationRaw |
| 114 | if err := UnmarshalStructureRaw(s, &biosInfo); err != nil { |
| 115 | return nil, fmt.Errorf("failed unmarshaling BIOS Information: %w", err) |
| 116 | } |
| 117 | tbl.BIOSInformationRaw = &biosInfo |
| 118 | case structTypeSystemInformation: |
| 119 | var systemInfo SystemInformationRaw |
| 120 | if err := UnmarshalStructureRaw(s, &systemInfo); err != nil { |
| 121 | return nil, fmt.Errorf("failed unmarshaling System Information: %w", err) |
| 122 | } |
| 123 | tbl.SystemInformationRaw = &systemInfo |
| 124 | case structTypeBaseboardInformation: |
Lorenz Brun | f9c65e9 | 2022-11-22 12:50:56 +0000 | [diff] [blame] | 125 | var baseboardInfo BaseboardInformationRaw |
| 126 | if err := UnmarshalStructureRaw(s, &baseboardInfo); err != nil { |
| 127 | return nil, fmt.Errorf("failed unmarshaling Baseboard Information: %w", err) |
| 128 | } |
| 129 | tbl.BaseboardsInformationRaw = append(tbl.BaseboardsInformationRaw, &baseboardInfo) |
| 130 | case structTypeSystemSlot: |
| 131 | var sysSlot SystemSlotRaw |
| 132 | if err := UnmarshalStructureRaw(s, &sysSlot); err != nil { |
| 133 | return nil, fmt.Errorf("failed unmarshaling System Slot: %w", err) |
| 134 | } |
| 135 | tbl.SystemSlotsRaw = append(tbl.SystemSlotsRaw, &sysSlot) |
| 136 | case structTypeMemoryDevice: |
| 137 | var memoryDev MemoryDeviceRaw |
| 138 | if err := UnmarshalStructureRaw(s, &memoryDev); err != nil { |
| 139 | return nil, fmt.Errorf("failed unmarshaling Memory Device: %w", err) |
| 140 | } |
| 141 | tbl.MemoryDevicesRaw = append(tbl.MemoryDevicesRaw, &memoryDev) |
| 142 | default: |
| 143 | // Just pass through the raw structure |
| 144 | tbl.Structures = append(tbl.Structures, s) |
| 145 | } |
| 146 | } |
| 147 | return &tbl, nil |
| 148 | } |
| 149 | |
| 150 | // Version contains a two-part version number consisting of a major and minor |
| 151 | // version. This is a common structure in SMBIOS. |
| 152 | type Version struct { |
| 153 | Major uint8 |
| 154 | Minor uint8 |
| 155 | } |
| 156 | |
| 157 | func (v *Version) String() string { |
| 158 | return fmt.Sprintf("%d.%d", v.Major, v.Minor) |
| 159 | } |
| 160 | |
| 161 | // AtLeast returns true if the version in v is at least the given version. |
| 162 | func (v *Version) AtLeast(major, minor uint8) bool { |
Lorenz Brun | 1cd2696 | 2023-04-19 16:10:17 +0200 | [diff] [blame^] | 163 | if v.Major > major { |
| 164 | return true |
| 165 | } |
| 166 | return v.Major == major && v.Minor >= minor |
Lorenz Brun | f9c65e9 | 2022-11-22 12:50:56 +0000 | [diff] [blame] | 167 | } |
| 168 | |
| 169 | // UnmarshalStructureRaw unmarshals a SMBIOS structure into a Go struct which |
| 170 | // has some constraints. The first two fields need to be a `uint16 handle` and |
| 171 | // a `StructureVersion Version` field. After that any number of fields may |
| 172 | // follow as long as they are either of type `string` (which will be looked up |
| 173 | // in the string table) or readable by binary.Read. To determine the structure |
| 174 | // version, the smbios_min_vers struct tag needs to be put on the first field |
| 175 | // of a newer structure version. The version implicitly starts with 2.0. |
| 176 | // The version determined is written to the second target struct field. |
| 177 | // Fields which do not have a fixed size need to be typed as a slice and tagged |
| 178 | // with smbios_repeat set to the name of the field containing the count. The |
| 179 | // count field itself needs to be some width of uint. |
| 180 | func UnmarshalStructureRaw(rawStruct Structure, target any) error { |
| 181 | v := reflect.ValueOf(target) |
| 182 | if v.Kind() != reflect.Pointer { |
| 183 | return errors.New("target needs to be a pointer") |
| 184 | } |
| 185 | v = v.Elem() |
| 186 | if v.Kind() != reflect.Struct { |
| 187 | return errors.New("target needs to be a pointer to a struct") |
| 188 | } |
| 189 | v.Field(0).SetUint(uint64(rawStruct.Handle)) |
| 190 | r := bytes.NewReader(rawStruct.FormattedSection) |
| 191 | completedVersion := Version{Major: 0, Minor: 0} |
| 192 | parsingVersion := Version{Major: 2, Minor: 0} |
| 193 | numFields := v.NumField() |
| 194 | hasAborted := false |
| 195 | for i := 2; i < numFields; i++ { |
| 196 | fieldType := v.Type().Field(i) |
| 197 | if min := fieldType.Tag.Get("smbios_min_ver"); min != "" { |
| 198 | var ver Version |
| 199 | if _, err := fmt.Sscanf(min, "%d.%d", &ver.Major, &ver.Minor); err != nil { |
| 200 | panic(fmt.Sprintf("invalid smbios_min_ver tag in %v: %v", fieldType.Name, err)) |
| 201 | } |
| 202 | completedVersion = parsingVersion |
| 203 | parsingVersion = ver |
| 204 | } |
| 205 | f := v.Field(i) |
| 206 | |
| 207 | if repeat := fieldType.Tag.Get("smbios_repeat"); repeat != "" { |
| 208 | repeatCountField := v.FieldByName(repeat) |
| 209 | if !repeatCountField.IsValid() { |
| 210 | panic(fmt.Sprintf("invalid smbios_repeat tag in %v: no such field %q", fieldType.Name, repeat)) |
| 211 | } |
| 212 | if !repeatCountField.CanUint() { |
| 213 | panic(fmt.Sprintf("invalid smbios_repeat tag in %v: referenced field %q is not uint-compatible", fieldType.Name, repeat)) |
| 214 | } |
| 215 | if f.Kind() != reflect.Slice { |
| 216 | panic(fmt.Sprintf("cannot repeat a field (%q) which is not a slice", fieldType.Name)) |
| 217 | } |
| 218 | if repeatCountField.Uint() > 65536 { |
| 219 | return fmt.Errorf("refusing to read a field repeated more than 65536 times (given %d times)", repeatCountField.Uint()) |
| 220 | } |
| 221 | repeatCount := int(repeatCountField.Uint()) |
| 222 | f.Set(reflect.MakeSlice(f.Type(), repeatCount, repeatCount)) |
| 223 | for j := 0; j < repeatCount; j++ { |
| 224 | fs := f.Index(j) |
| 225 | err := unmarshalField(&rawStruct, fs, r) |
| 226 | if errors.Is(err, io.EOF) { |
| 227 | hasAborted = true |
| 228 | break |
| 229 | } else if err != nil { |
| 230 | return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err) |
| 231 | } |
| 232 | } |
| 233 | } |
| 234 | err := unmarshalField(&rawStruct, f, r) |
| 235 | if errors.Is(err, io.EOF) { |
| 236 | hasAborted = true |
| 237 | break |
| 238 | } else if err != nil { |
| 239 | return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err) |
| 240 | } |
| 241 | } |
| 242 | if !hasAborted { |
| 243 | completedVersion = parsingVersion |
| 244 | } |
| 245 | if completedVersion.Major == 0 { |
| 246 | return fmt.Errorf("structure's formatted section (%d bytes) is smaller than its minimal size", len(rawStruct.FormattedSection)) |
| 247 | } |
| 248 | v.Field(1).Set(reflect.ValueOf(completedVersion)) |
| 249 | return nil |
| 250 | } |
| 251 | |
| 252 | func unmarshalField(rawStruct *Structure, field reflect.Value, r *bytes.Reader) error { |
| 253 | if field.Kind() == reflect.String { |
| 254 | var stringTableIdx uint8 |
| 255 | err := binary.Read(r, binary.LittleEndian, &stringTableIdx) |
| 256 | if err != nil { |
| 257 | return err |
| 258 | } |
| 259 | if stringTableIdx == 0 { |
| 260 | return nil |
| 261 | } |
| 262 | if int(stringTableIdx)-1 >= len(rawStruct.Strings) { |
| 263 | return fmt.Errorf("string index (%d) bigger than string table (%q)", stringTableIdx-1, rawStruct.Strings) |
| 264 | } |
| 265 | field.SetString(rawStruct.Strings[stringTableIdx-1]) |
| 266 | return nil |
| 267 | } |
| 268 | return binary.Read(r, binary.LittleEndian, field.Addr().Interface()) |
| 269 | } |