1 /*
2 * This file and its contents are supplied under the terms of the
3 * Common Development and Distribution License ("CDDL"), version 1.0.
4 * You may only use this file in accordance with the terms of version
5 * 1.0 of the CDDL.
6 *
7 * A full copy of the text of the CDDL should have accompanied this
8 * source. A copy of the CDDL is also available via the Internet at
9 * http://www.illumos.org/license/CDDL.
10 */
11
12 /*
13 * Copyright 2023 Oxide Computer Company
14 */
15
16 /*
17 * This is the common file or parsing out SPD data of different generations. Our
18 * general goal is to create a single nvlist_t that has a few different sections
19 * present in it:
20 *
21 * o Metadata (e.g. DRAM type, Revision, overlay type, etc.)
22 * o Manufacturing Information
23 * o Common parameters: these are ultimately specific to a DDR type.
24 * o Overlay parameters: these are specific to both the DDR type and the
25 * module type.
26 *
27 * We try to only fail top-level parsing if we really can't understand anything
28 * or don't have enough information. We assume that we'll get relatively
29 * complete data. Errors are listed as keys for a given entry and will be
30 * skipped otherwise. For an overview of the actual fields and structures, see
31 * libjedec.h.
32 *
33 * Currently we support all of DDR4 and DDD5 based SPD information with the
34 * exception of some NVDIMM properties.
35 */
36
37 #include <string.h>
38 #include <sys/debug.h>
39 #include <sys/sysmacros.h>
40 #include <ctype.h>
41 #include <stdarg.h>
42 #include <errno.h>
43 #include <stdbool.h>
44
45 #include "libjedec_spd.h"
46
47 void
spd_nvl_err(spd_info_t * si,const char * key,spd_error_kind_t err,const char * fmt,...)48 spd_nvl_err(spd_info_t *si, const char *key, spd_error_kind_t err,
49 const char *fmt, ...)
50 {
51 int ret;
52 nvlist_t *nvl;
53 char msg[1024];
54 va_list ap;
55
56 if (si->si_error != LIBJEDEC_SPD_OK)
57 return;
58
59 ret = nvlist_alloc(&nvl, NV_UNIQUE_NAME, 0);
60 if (ret != 0) {
61 VERIFY3S(ret, ==, ENOMEM);
62 si->si_error = LIBJEDEC_SPD_NOMEM;
63 return;
64 }
65
66 ret = nvlist_add_uint32(nvl, SPD_KEY_ERRS_CODE, err);
67 if (ret != 0) {
68 VERIFY3S(ret, ==, ENOMEM);
69 nvlist_free(nvl);
70 si->si_error = LIBJEDEC_SPD_NOMEM;
71 return;
72 }
73
74 /*
75 * We cast this snprintf to void so we can try to get someone something
76 * at least in the face of it somehow being too large.
77 */
78 va_start(ap, fmt);
79 (void) vsnprintf(msg, sizeof (msg), fmt, ap);
80 va_end(ap);
81
82 ret = nvlist_add_string(nvl, SPD_KEY_ERRS_MSG, msg);
83 if (ret != 0) {
84 VERIFY3S(ret, ==, ENOMEM);
85 nvlist_free(nvl);
86 si->si_error = LIBJEDEC_SPD_NOMEM;
87 return;
88 }
89
90 ret = nvlist_add_nvlist(si->si_errs, key, nvl);
91 if (ret != 0) {
92 VERIFY3S(ret, ==, ENOMEM);
93 nvlist_free(nvl);
94 si->si_error = LIBJEDEC_SPD_NOMEM;
95 return;
96 }
97
98 nvlist_free(nvl);
99 }
100
101 void
spd_nvl_insert_str(spd_info_t * si,const char * key,const char * data)102 spd_nvl_insert_str(spd_info_t *si, const char *key, const char *data)
103 {
104 int ret;
105
106 if (si->si_error != LIBJEDEC_SPD_OK)
107 return;
108
109 ret = nvlist_add_string(si->si_nvl, key, data);
110 if (ret != 0) {
111 VERIFY3S(ret, ==, ENOMEM);
112 si->si_error = LIBJEDEC_SPD_NOMEM;
113 return;
114 }
115 }
116
117 void
spd_nvl_insert_u32(spd_info_t * si,const char * key,uint32_t data)118 spd_nvl_insert_u32(spd_info_t *si, const char *key, uint32_t data)
119 {
120 int ret;
121
122 if (si->si_error != LIBJEDEC_SPD_OK)
123 return;
124
125 ret = nvlist_add_uint32(si->si_nvl, key, data);
126 if (ret != 0) {
127 VERIFY3S(ret, ==, ENOMEM);
128 si->si_error = LIBJEDEC_SPD_NOMEM;
129 return;
130 }
131 }
132
133 void
spd_nvl_insert_u64(spd_info_t * si,const char * key,uint64_t data)134 spd_nvl_insert_u64(spd_info_t *si, const char *key, uint64_t data)
135 {
136 int ret;
137
138 if (si->si_error != LIBJEDEC_SPD_OK)
139 return;
140
141 ret = nvlist_add_uint64(si->si_nvl, key, data);
142 if (ret != 0) {
143 VERIFY3S(ret, ==, ENOMEM);
144 si->si_error = LIBJEDEC_SPD_NOMEM;
145 return;
146 }
147 }
148
149 void
spd_nvl_insert_u32_array(spd_info_t * si,const char * key,uint32_t * data,uint_t nent)150 spd_nvl_insert_u32_array(spd_info_t *si, const char *key,
151 uint32_t *data, uint_t nent)
152 {
153 int ret;
154
155 if (si->si_error != LIBJEDEC_SPD_OK)
156 return;
157
158 ret = nvlist_add_uint32_array(si->si_nvl, key, data, nent);
159 if (ret != 0) {
160 VERIFY3S(ret, ==, ENOMEM);
161 si->si_error = LIBJEDEC_SPD_NOMEM;
162 return;
163 }
164 }
165
166 void
spd_nvl_insert_key(spd_info_t * si,const char * key)167 spd_nvl_insert_key(spd_info_t *si, const char *key)
168 {
169 int ret;
170
171 if (si->si_error != LIBJEDEC_SPD_OK)
172 return;
173
174 ret = nvlist_add_boolean(si->si_nvl, key);
175 if (ret != 0) {
176 VERIFY3S(ret, ==, ENOMEM);
177 si->si_error = LIBJEDEC_SPD_NOMEM;
178 return;
179 }
180 }
181
182 void
spd_insert_map(spd_info_t * si,const char * key,uint8_t spd_val,const spd_value_map_t * maps,size_t nmaps)183 spd_insert_map(spd_info_t *si, const char *key, uint8_t spd_val,
184 const spd_value_map_t *maps, size_t nmaps)
185 {
186 for (size_t i = 0; i < nmaps; i++) {
187 if (maps[i].svm_spd != spd_val)
188 continue;
189 if (maps[i].svm_skip)
190 return;
191
192 spd_nvl_insert_u32(si, key, maps[i].svm_use);
193 return;
194 }
195
196 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "encountered unknown "
197 "value: 0x%x", spd_val);
198 }
199
200 void
spd_insert_map64(spd_info_t * si,const char * key,uint8_t spd_val,const spd_value_map64_t * maps,size_t nmaps)201 spd_insert_map64(spd_info_t *si, const char *key, uint8_t spd_val,
202 const spd_value_map64_t *maps, size_t nmaps)
203 {
204 for (size_t i = 0; i < nmaps; i++) {
205 if (maps[i].svm_spd != spd_val)
206 continue;
207 if (maps[i].svm_skip)
208 return;
209
210 spd_nvl_insert_u64(si, key, maps[i].svm_use);
211 return;
212 }
213
214 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "encountered unknown "
215 "value: 0x%x", spd_val);
216 }
217
218 void
spd_insert_str_map(spd_info_t * si,const char * key,uint8_t spd_val,const spd_str_map_t * maps,size_t nmaps)219 spd_insert_str_map(spd_info_t *si, const char *key, uint8_t spd_val,
220 const spd_str_map_t *maps, size_t nmaps)
221 {
222 for (size_t i = 0; i < nmaps; i++) {
223 if (maps[i].ssm_spd != spd_val)
224 continue;
225 if (maps[i].ssm_skip)
226 return;
227
228 spd_nvl_insert_str(si, key, maps[i].ssm_str);
229 return;
230 }
231
232 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "encountered unknown "
233 "value: 0x%x", spd_val);
234 }
235
236 /*
237 * Map an array in its entirety to a corresponding set of values. If any one
238 * value cannot be translated, then we fail the whole item.
239 */
240 void
spd_insert_map_array(spd_info_t * si,const char * key,const uint8_t * raw,size_t nraw,const spd_value_map_t * maps,size_t nmaps)241 spd_insert_map_array(spd_info_t *si, const char *key, const uint8_t *raw,
242 size_t nraw, const spd_value_map_t *maps, size_t nmaps)
243 {
244 uint32_t *trans;
245
246 trans = calloc(nraw, sizeof (uint32_t));
247 if (trans == NULL) {
248 si->si_error = LIBJEDEC_SPD_NOMEM;
249 return;
250 }
251
252 for (size_t i = 0; i < nraw; i++) {
253 bool found = false;
254 for (size_t map = 0; map < nmaps; map++) {
255 if (maps[map].svm_spd != raw[i])
256 continue;
257 ASSERT3U(maps[map].svm_skip, ==, false);
258 found = true;
259 trans[i] = maps[map].svm_use;
260 break;
261 }
262
263 if (!found) {
264 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "encountered "
265 "unknown array value: [%zu]=0x%x", i, raw[i]);
266 goto done;
267 }
268 }
269
270 spd_nvl_insert_u32_array(si, key, trans, nraw);
271 done:
272 free(trans);
273 }
274
275 void
spd_insert_range(spd_info_t * si,const char * key,uint8_t raw_val,const spd_value_range_t * range)276 spd_insert_range(spd_info_t *si, const char *key, uint8_t raw_val,
277 const spd_value_range_t *range)
278 {
279 /*
280 * Apply any base or multiple to the value. If the min or max are zero,
281 * then we ignore them. We apply the base before a multiple.
282 */
283 uint32_t min = 0, max = UINT32_MAX;
284 uint32_t act = raw_val + range->svr_base;
285
286 if (range->svr_mult != 0) {
287 act *= range->svr_mult;
288 }
289
290 if (range->svr_max != 0) {
291 max = range->svr_max;
292 }
293
294 if (range->svr_min != 0) {
295 min = range->svr_min;
296 } else if (range->svr_base != 0) {
297 min = range->svr_base;
298 }
299
300 if (act > max || act < min) {
301 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "found value "
302 "0x%x (raw 0x%x) outside range [0x%x, 0x%x]", act, raw_val,
303 min, max);
304 } else {
305 spd_nvl_insert_u32(si, key, act);
306 }
307 }
308
309 /*
310 * Either insert the given flag for a key or OR it in if it already exists.
311 */
312 void
spd_upsert_flag(spd_info_t * si,const char * key,uint32_t flag)313 spd_upsert_flag(spd_info_t *si, const char *key, uint32_t flag)
314 {
315 int ret;
316 uint32_t val;
317
318 ret = nvlist_lookup_uint32(si->si_nvl, key, &val);
319 if (ret != 0) {
320 VERIFY3S(ret, ==, ENOENT);
321 spd_nvl_insert_u32(si, key, flag);
322 return;
323 }
324
325 VERIFY0(val & flag);
326 val |= flag;
327 spd_nvl_insert_u32(si, key, val);
328 }
329
330 void
spd_parse_rev(spd_info_t * si,uint32_t off,uint32_t len,const char * key)331 spd_parse_rev(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
332 {
333 const uint8_t data = si->si_data[off];
334 const uint8_t enc = SPD_DDR4_SPD_REV_ENC(data);
335 const uint8_t add = SPD_DDR4_SPD_REV_ENC(data);
336
337 spd_nvl_insert_u32(si, SPD_KEY_REV_ENC, enc);
338 spd_nvl_insert_u32(si, SPD_KEY_REV_ADD, add);
339 }
340
341 void
spd_parse_jedec_id(spd_info_t * si,uint32_t off,uint32_t len,const char * key)342 spd_parse_jedec_id(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
343 {
344 uint32_t id[2];
345
346 VERIFY3U(len, ==, 2);
347 id[0] = SPD_MFG_ID0_CONT(si->si_data[off]);
348 id[1] = si->si_data[off + 1];
349
350 spd_nvl_insert_u32_array(si, key, id, ARRAY_SIZE(id));
351 }
352
353 void
spd_parse_jedec_id_str(spd_info_t * si,uint32_t off,uint32_t len,const char * key)354 spd_parse_jedec_id_str(spd_info_t *si, uint32_t off, uint32_t len,
355 const char *key)
356 {
357 uint8_t cont = SPD_MFG_ID0_CONT(si->si_data[off]);
358 const char *str;
359
360 VERIFY3U(len, ==, 2);
361 str = libjedec_vendor_string(cont, si->si_data[off + 1]);
362 if (str != NULL) {
363 spd_nvl_insert_str(si, key, str);
364 } else {
365 spd_nvl_err(si, key, SPD_ERROR_NO_XLATE, "no matching "
366 "libjedec vendor string for 0x%x,0x%x", cont,
367 si->si_data[off + 1]);
368 }
369 }
370
371 /*
372 * Parse a string that is at most len bytes wide and is padded with spaces. If
373 * the string contains an unprintable, then we will not pull this off and set an
374 * error for the string's key. 128 bytes should be larger than any ascii string
375 * that we encounter as that is the size of most regions in SPD data.
376 */
377 void
spd_parse_string(spd_info_t * si,uint32_t off,uint32_t len,const char * key)378 spd_parse_string(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
379 {
380 uint32_t nbytes = 0;
381 char buf[128];
382
383 VERIFY3U(sizeof (buf), >, len);
384 for (uint32_t i = 0; i < len; i++) {
385 if (si->si_data[off + i] == ' ') {
386 nbytes = i;
387 break;
388 }
389
390 if (isascii(si->si_data[off + i]) == 0 ||
391 isprint(si->si_data[off + i]) == 0) {
392 spd_nvl_err(si, key, SPD_ERROR_UNPRINT,
393 "byte %u for key %s (off: 0x%x, val: 0x%x) is not "
394 "printable", i, key, off + 1,
395 si->si_data[off + i]);
396 return;
397 }
398 }
399
400 if (nbytes == 0) {
401 spd_nvl_err(si, key, SPD_ERROR_NO_DATA, "key %s has "
402 "no valid bytes in the string", key);
403 return;
404 }
405
406 (void) memcpy(buf, &si->si_data[off], nbytes);
407 buf[nbytes] = '\0';
408 spd_nvl_insert_str(si, key, buf);
409 }
410
411 /*
412 * Turn an array of bytes into a hex string. We need to allocate up to two bytes
413 * per length that we have. We always zero pad such strings. We statically size
414 * our buffer because the largest such string we have right now is a 4-byte
415 * serial number. With the 128 byte buffer below, we could deal with a length up
416 * to 63 (far beyond what we expect to ever see).
417 */
418 void
spd_parse_hex_string(spd_info_t * si,uint32_t off,uint32_t len,const char * key)419 spd_parse_hex_string(spd_info_t *si, uint32_t off, uint32_t len,
420 const char *key)
421 {
422 char buf[128];
423 size_t nwrite = 0;
424
425 VERIFY3U(sizeof (buf), >=, len * 2 + 1);
426
427 for (uint32_t i = 0; i < len; i++) {
428 int ret = snprintf(buf + nwrite, sizeof (buf) - nwrite,
429 "%02X", si->si_data[off + i]);
430 if (ret < 0) {
431 spd_nvl_err(si, key, SPD_ERROR_INTERNAL,
432 "snprintf failed unexpectedly for key %s: %s",
433 key, strerror(errno));
434 return;
435 }
436
437 VERIFY3U(ret, ==, 2);
438 nwrite += ret;
439 }
440
441 spd_nvl_insert_str(si, key, buf);
442 }
443
444 /*
445 * Several SPD keys are explicit BCD major and minor versions in a given nibble.
446 * This is most common in DDR5, but otherwise one should probably use
447 * spd_parse_hex_string().
448 */
449 void
spd_parse_hex_vers(spd_info_t * si,uint32_t off,uint32_t len,const char * key)450 spd_parse_hex_vers(spd_info_t *si, uint32_t off, uint32_t len,
451 const char *key)
452 {
453 const uint8_t data = si->si_data[off];
454 const uint8_t maj = bitx8(data, 7, 4);
455 const uint8_t min = bitx8(data, 3, 0);
456 char buf[128];
457
458 VERIFY3U(len, ==, 1);
459
460 int ret = snprintf(buf, sizeof (buf), "%x.%x", maj, min);
461 if (ret < 0) {
462 spd_nvl_err(si, key, SPD_ERROR_INTERNAL,
463 "snprintf failed unexpectedly for key %s: %s",
464 key, strerror(errno));
465 return;
466 }
467
468 spd_nvl_insert_str(si, key, buf);
469 }
470
471 void
spd_parse_raw_u8(spd_info_t * si,uint32_t off,uint32_t len,const char * key)472 spd_parse_raw_u8(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
473 {
474 VERIFY3U(len, ==, 1);
475 spd_nvl_insert_u32(si, key, si->si_data[off]);
476 }
477
478 void
spd_parse_dram_step(spd_info_t * si,uint32_t off,uint32_t len,const char * key)479 spd_parse_dram_step(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
480 {
481 VERIFY3U(len, ==, 1);
482
483 if (si->si_data[off] == SPD_DRAM_STEP_NOINFO)
484 return;
485
486 spd_parse_hex_string(si, off, len, key);
487 }
488
489 /*
490 * Height and thickness have the same meaning across DDR3-DDR5.
491 */
492 static const spd_value_range_t spd_height_range = {
493 .svr_base = SPD_DDR5_COM_HEIGHT_BASE
494 };
495
496 static const spd_value_range_t spd_thick_range = {
497 .svr_base = SPD_DDR5_COM_THICK_BASE
498 };
499
500 void
spd_parse_height(spd_info_t * si,uint32_t off,uint32_t len,const char * key)501 spd_parse_height(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
502 {
503 const uint8_t data = si->si_data[off];
504 const uint8_t height = SPD_DDR5_COM_HEIGHT_MM(data);
505 spd_insert_range(si, key, height, &spd_height_range);
506 }
507
508 void
spd_parse_thickness(spd_info_t * si,uint32_t off,uint32_t len,const char * key)509 spd_parse_thickness(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
510 {
511 const uint8_t data = si->si_data[off];
512 const uint8_t front = SPD_DDR5_COM_THICK_FRONT(data);
513 const uint8_t back = SPD_DDR5_COM_THICK_FRONT(data);
514
515 spd_insert_range(si, SPD_KEY_MOD_FRONT_THICK, front, &spd_thick_range);
516 spd_insert_range(si, SPD_KEY_MOD_BACK_THICK, back, &spd_thick_range);
517 }
518
519 /*
520 * Calculate the DRAM CRC16. The crc ranges over [ off, off + len - 2). The crc
521 * lsb is at off + len - 2, and the msb is at off + len - 1. The JEDEC specs
522 * describe the algorithm (e.g. 21-C Annex L, 8.1.53).
523 */
524 void
spd_parse_crc(spd_info_t * si,uint32_t off,uint32_t len,const char * key)525 spd_parse_crc(spd_info_t *si, uint32_t off, uint32_t len, const char *key)
526 {
527 uint32_t crc = 0;
528 const uint16_t expect = si->si_data[off + len - 2] |
529 (si->si_data[off + len - 1] << 8);
530
531 for (uint32_t i = 0; i < len - 2; i++) {
532 crc = crc ^ (uint32_t)si->si_data[off + i] << 8;
533 for (uint32_t c = 0; c < 8; c++) {
534 if (crc & 0x8000) {
535 crc = crc << 1 ^ 0x1021;
536 } else {
537 crc = crc << 1;
538 }
539 }
540 }
541
542 crc &= 0xffff;
543 if (crc == expect) {
544 spd_nvl_insert_u32(si, key, crc);
545 } else {
546 spd_nvl_err(si, key, SPD_ERROR_BAD_DATA, "crc mismatch: "
547 "expected 0x%x, found 0x%x", expect, crc);
548 }
549 }
550
551 void
spd_parse(spd_info_t * sip,const spd_parse_t * parse,size_t nparse)552 spd_parse(spd_info_t *sip, const spd_parse_t *parse, size_t nparse)
553 {
554 for (size_t i = 0; i < nparse; i++) {
555 uint32_t len;
556
557 if (parse[i].sp_len != 0) {
558 len = parse[i].sp_len;
559 } else {
560 len = 1;
561 }
562
563 if (len + parse[i].sp_off >= sip->si_nbytes) {
564 if ((sip->si_flags & SPD_INFO_F_INCOMPLETE) != 0)
565 continue;
566 sip->si_flags |= SPD_INFO_F_INCOMPLETE;
567 ASSERT3U(parse[i].sp_off, <, UINT32_MAX);
568 spd_nvl_insert_u32(sip, SPD_KEY_INCOMPLETE,
569 (uint32_t)parse[i].sp_off);
570 } else {
571 parse[i].sp_parse(sip, parse[i].sp_off, len,
572 parse[i].sp_key);
573 }
574
575 if (sip->si_error != LIBJEDEC_SPD_OK) {
576 return;
577 }
578 }
579 }
580
581 static spd_error_t
spd_init_info(spd_info_t * sip)582 spd_init_info(spd_info_t *sip)
583 {
584 int ret;
585
586 if ((ret = nvlist_alloc(&sip->si_nvl, NV_UNIQUE_NAME, 0)) != 0) {
587 VERIFY3S(ret, ==, ENOMEM);
588 return (LIBJEDEC_SPD_NOMEM);
589 }
590
591 if ((ret = nvlist_alloc(&sip->si_errs, NV_UNIQUE_NAME, 0)) != 0) {
592 VERIFY3S(ret, ==, ENOMEM);
593 return (LIBJEDEC_SPD_NOMEM);
594 }
595
596 return (LIBJEDEC_SPD_OK);
597 }
598
599 static void
spd_fini_info(spd_info_t * sip)600 spd_fini_info(spd_info_t *sip)
601 {
602 nvlist_free(sip->si_nvl);
603 nvlist_free(sip->si_errs);
604 }
605
606 nvlist_t *
libjedec_spd(const uint8_t * buf,size_t nbytes,spd_error_t * err)607 libjedec_spd(const uint8_t *buf, size_t nbytes, spd_error_t *err)
608 {
609 int ret;
610 spd_error_t set;
611 spd_info_t si;
612
613 if (err == NULL) {
614 err = &set;
615 }
616
617 (void) memset(&si, 0, sizeof (spd_info_t));
618 si.si_data = buf;
619 si.si_nbytes = nbytes;
620
621 *err = spd_init_info(&si);
622 if (si.si_error != LIBJEDEC_SPD_OK) {
623 goto fatal;
624 }
625
626 /*
627 * To begin parsing the SPD, we must first look at byte 2, which appears
628 * to almost always be the Key Byte / Host Bus Command Protocol Type
629 * which then tells us how the rest of the data is formatted.
630 */
631 if (si.si_nbytes <= SPD_DRAM_TYPE) {
632 *err = LIBJEDEC_SPD_TOOSHORT;
633 goto fatal;
634 }
635
636 si.si_error = LIBJEDEC_SPD_OK;
637 si.si_dram = buf[SPD_DRAM_TYPE];
638 switch (si.si_dram) {
639 case SPD_DT_DDR4_SDRAM:
640 spd_parse_ddr4(&si);
641 break;
642 case SPD_DT_DDR5_SDRAM:
643 spd_parse_ddr5(&si);
644 break;
645 default:
646 *err = LIBJEDEC_SPD_UNSUP_TYPE;
647 goto fatal;
648 }
649
650 /*
651 * We got everything, at this point add the error nvlist here.
652 */
653 if (si.si_error == LIBJEDEC_SPD_OK) {
654 if (!nvlist_empty(si.si_errs) &&
655 (ret = nvlist_add_nvlist(si.si_nvl, "errors",
656 si.si_errs)) != 0) {
657 VERIFY3S(ret, ==, ENOMEM);
658 *err = LIBJEDEC_SPD_NOMEM;
659 goto fatal;
660 }
661 nvlist_free(si.si_errs);
662 return (si.si_nvl);
663 }
664
665 *err = si.si_error;
666 fatal:
667 spd_fini_info(&si);
668 return (NULL);
669 }
670