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