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