blob: 0080278d457d7f7672379f5dcd3c1320e4984414 [file] [log] [blame]
Lorenz Brunf9c65e92022-11-22 12:50:56 +00001// 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".
9package smbios
10
11import (
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
23type 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
33type 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.
43type Table struct {
44 BIOSInformationRaw *BIOSInformationRaw
45 SystemInformationRaw *SystemInformationRaw
46 BaseboardsInformationRaw []*BaseboardInformationRaw
47 SystemSlotsRaw []*SystemSlotRaw
48 MemoryDevicesRaw []*MemoryDeviceRaw
49
50 Structures []Structure
51}
52
53const (
54 structTypeInactive = 126
55 structTypeEndOfTable = 127
56)
57
58func 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 Brunf9c65e92022-11-22 12:50:56 +0000125 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.
152type Version struct {
153 Major uint8
154 Minor uint8
155}
156
157func (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.
162func (v *Version) AtLeast(major, minor uint8) bool {
Lorenz Brun1cd26962023-04-19 16:10:17 +0200163 if v.Major > major {
164 return true
165 }
166 return v.Major == major && v.Minor >= minor
Lorenz Brunf9c65e92022-11-22 12:50:56 +0000167}
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.
180func 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
252func 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}