blob: 1c8ae6918abeb7461b39516511026c3b7058ea57 [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 {
163 return v.Major >= major && v.Minor >= minor
164}
165
166// UnmarshalStructureRaw unmarshals a SMBIOS structure into a Go struct which
167// has some constraints. The first two fields need to be a `uint16 handle` and
168// a `StructureVersion Version` field. After that any number of fields may
169// follow as long as they are either of type `string` (which will be looked up
170// in the string table) or readable by binary.Read. To determine the structure
171// version, the smbios_min_vers struct tag needs to be put on the first field
172// of a newer structure version. The version implicitly starts with 2.0.
173// The version determined is written to the second target struct field.
174// Fields which do not have a fixed size need to be typed as a slice and tagged
175// with smbios_repeat set to the name of the field containing the count. The
176// count field itself needs to be some width of uint.
177func UnmarshalStructureRaw(rawStruct Structure, target any) error {
178 v := reflect.ValueOf(target)
179 if v.Kind() != reflect.Pointer {
180 return errors.New("target needs to be a pointer")
181 }
182 v = v.Elem()
183 if v.Kind() != reflect.Struct {
184 return errors.New("target needs to be a pointer to a struct")
185 }
186 v.Field(0).SetUint(uint64(rawStruct.Handle))
187 r := bytes.NewReader(rawStruct.FormattedSection)
188 completedVersion := Version{Major: 0, Minor: 0}
189 parsingVersion := Version{Major: 2, Minor: 0}
190 numFields := v.NumField()
191 hasAborted := false
192 for i := 2; i < numFields; i++ {
193 fieldType := v.Type().Field(i)
194 if min := fieldType.Tag.Get("smbios_min_ver"); min != "" {
195 var ver Version
196 if _, err := fmt.Sscanf(min, "%d.%d", &ver.Major, &ver.Minor); err != nil {
197 panic(fmt.Sprintf("invalid smbios_min_ver tag in %v: %v", fieldType.Name, err))
198 }
199 completedVersion = parsingVersion
200 parsingVersion = ver
201 }
202 f := v.Field(i)
203
204 if repeat := fieldType.Tag.Get("smbios_repeat"); repeat != "" {
205 repeatCountField := v.FieldByName(repeat)
206 if !repeatCountField.IsValid() {
207 panic(fmt.Sprintf("invalid smbios_repeat tag in %v: no such field %q", fieldType.Name, repeat))
208 }
209 if !repeatCountField.CanUint() {
210 panic(fmt.Sprintf("invalid smbios_repeat tag in %v: referenced field %q is not uint-compatible", fieldType.Name, repeat))
211 }
212 if f.Kind() != reflect.Slice {
213 panic(fmt.Sprintf("cannot repeat a field (%q) which is not a slice", fieldType.Name))
214 }
215 if repeatCountField.Uint() > 65536 {
216 return fmt.Errorf("refusing to read a field repeated more than 65536 times (given %d times)", repeatCountField.Uint())
217 }
218 repeatCount := int(repeatCountField.Uint())
219 f.Set(reflect.MakeSlice(f.Type(), repeatCount, repeatCount))
220 for j := 0; j < repeatCount; j++ {
221 fs := f.Index(j)
222 err := unmarshalField(&rawStruct, fs, r)
223 if errors.Is(err, io.EOF) {
224 hasAborted = true
225 break
226 } else if err != nil {
227 return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
228 }
229 }
230 }
231 err := unmarshalField(&rawStruct, f, r)
232 if errors.Is(err, io.EOF) {
233 hasAborted = true
234 break
235 } else if err != nil {
236 return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
237 }
238 }
239 if !hasAborted {
240 completedVersion = parsingVersion
241 }
242 if completedVersion.Major == 0 {
243 return fmt.Errorf("structure's formatted section (%d bytes) is smaller than its minimal size", len(rawStruct.FormattedSection))
244 }
245 v.Field(1).Set(reflect.ValueOf(completedVersion))
246 return nil
247}
248
249func unmarshalField(rawStruct *Structure, field reflect.Value, r *bytes.Reader) error {
250 if field.Kind() == reflect.String {
251 var stringTableIdx uint8
252 err := binary.Read(r, binary.LittleEndian, &stringTableIdx)
253 if err != nil {
254 return err
255 }
256 if stringTableIdx == 0 {
257 return nil
258 }
259 if int(stringTableIdx)-1 >= len(rawStruct.Strings) {
260 return fmt.Errorf("string index (%d) bigger than string table (%q)", stringTableIdx-1, rawStruct.Strings)
261 }
262 field.SetString(rawStruct.Strings[stringTableIdx-1])
263 return nil
264 }
265 return binary.Read(r, binary.LittleEndian, field.Addr().Interface())
266}