xref: /illumos-gate/usr/src/lib/libnvme/common/libnvme_ocp.c (revision e8d3efa1c56e5f2b5368600a2baeb7b1d54a07f8)
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 2026 Oxide Computer Company
14  */
15 
16 /*
17  * libnvme logic that covers the OCP Datacenter NVMe SSD specification.
18  */
19 
20 #include <string.h>
21 #include <sys/sysmacros.h>
22 #include <sys/nvme/ocp.h>
23 
24 #include "libnvme_impl.h"
25 
26 static bool
27 nvme_ocp_telstr_var_len(uint64_t *outp, const void *data, size_t len)
28 {
29 	ocp_vul_telstr_t telstr;
30 	size_t need = offsetof(ocp_vul_telstr_t, ots_rsvd40);
31 
32 	if (len < offsetof(ocp_vul_telstr_t, ots_rsvd40)) {
33 		return (false);
34 	}
35 
36 	(void) memcpy(&telstr, data, need);
37 	if (telstr.ots_sls / sizeof (uint32_t) >= SIZE_MAX) {
38 		return (false);
39 	}
40 	*outp = telstr.ots_sls * sizeof (uint32_t);
41 	return (true);
42 }
43 
44 static bool
45 nvme_ocp_hwcomp_var_len(uint64_t *outp, const void *data, size_t len)
46 {
47 	ocp_vul_hw_comp_t comp;
48 	uint32_t dlen;
49 	uint64_t mult;
50 
51 	if (len < sizeof (ocp_vul_hw_comp_t)) {
52 		return (false);
53 	}
54 
55 
56 	(void) memcpy(&comp, data, sizeof (ocp_vul_hw_comp_t));
57 
58 	/*
59 	 * The hardware component log has a 16-byte number that is used to
60 	 * indicate the log page length. In version 1 of the log page this is a
61 	 * value of uint32_t's. In version 2 of the log page this is in bytes.
62 	 * Because of these changes we require a known version to know how to
63 	 * deal with these things.
64 	 *
65 	 * When we encounter a log page with more than 4 GiB of data in it, we
66 	 * can come back to this as we're going to need to improve the logic
67 	 * elsewhere to stream this rather than use a single buffer.
68 	 */
69 	switch (comp.ohc_vers) {
70 	case 1:
71 		mult = sizeof (uint32_t);
72 		break;
73 	case 2:
74 		mult = 1;
75 		break;
76 	default:
77 		return (false);
78 	}
79 
80 	for (size_t i = 4; i < 16; i++) {
81 		if (comp.ohc_len[i] != 0) {
82 			return (false);
83 		}
84 	}
85 
86 	(void) memcpy(&dlen, comp.ohc_len, sizeof (dlen));
87 	*outp = (uint64_t)dlen * mult;
88 	if (*outp < sizeof (ocp_vul_hw_comp_t)) {
89 		return (false);
90 	}
91 
92 	return (true);
93 }
94 
95 const nvme_log_page_info_t ocp_log_smart = {
96 	.nlpi_short = "ocp/smart",
97 	.nlpi_human = "OCP SMART / Health Information",
98 	.nlpi_lid = OCP_LOG_DSSD_SMART,
99 	.nlpi_csi = NVME_CSI_NVM,
100 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
101 	.nlpi_source = NVME_LOG_DISC_S_DB,
102 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
103 	.nlpi_len = sizeof (ocp_vul_smart_t),
104 };
105 
106 const nvme_log_page_info_t ocp_log_errrec = {
107 	.nlpi_short = "ocp/errrec",
108 	.nlpi_human = "OCP Error Recovery",
109 	.nlpi_lid = OCP_LOG_DSSD_ERROR_REC,
110 	.nlpi_csi = NVME_CSI_NVM,
111 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
112 	.nlpi_source = NVME_LOG_DISC_S_DB,
113 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
114 	.nlpi_len = sizeof (ocp_vul_errrec_t),
115 };
116 
117 const nvme_log_page_info_t ocp_log_fwact = {
118 	.nlpi_short = "ocp/fwact",
119 	.nlpi_human = "OCP Firmware Activation",
120 	.nlpi_lid = OCP_LOG_DSSD_FWACT,
121 	.nlpi_csi = NVME_CSI_NVM,
122 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
123 	.nlpi_source = NVME_LOG_DISC_S_DB,
124 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
125 	.nlpi_len = sizeof (ocp_vul_fwact_t),
126 };
127 
128 const nvme_log_page_info_t ocp_log_lat = {
129 	.nlpi_short = "ocp/latency",
130 	.nlpi_human = "OCP Latency Monitor",
131 	.nlpi_lid = OCP_LOG_DSSD_LATENCY,
132 	.nlpi_csi = NVME_CSI_NVM,
133 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
134 	.nlpi_source = NVME_LOG_DISC_S_DB,
135 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
136 	.nlpi_len = sizeof (ocp_vul_lat_t),
137 };
138 
139 const nvme_log_page_info_t ocp_log_devcap = {
140 	.nlpi_short = "ocp/devcap",
141 	.nlpi_human = "OCP Device Capabilities",
142 	.nlpi_lid = OCP_LOG_DSSD_DEV_CAP,
143 	.nlpi_csi = NVME_CSI_NVM,
144 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
145 	.nlpi_source = NVME_LOG_DISC_S_DB,
146 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
147 	.nlpi_len = sizeof (ocp_vul_devcap_t),
148 };
149 
150 const nvme_log_page_info_t ocp_log_unsup = {
151 	.nlpi_short = "ocp/unsup",
152 	.nlpi_human = "OCP Unsupported Requirements",
153 	.nlpi_lid = OCP_LOG_DSSD_UNSUP_REQ,
154 	.nlpi_csi = NVME_CSI_NVM,
155 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
156 	.nlpi_source = NVME_LOG_DISC_S_DB,
157 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
158 	.nlpi_len = sizeof (ocp_vul_unsup_req_t),
159 };
160 
161 const nvme_log_page_info_t ocp_log_hwcomp = {
162 	.nlpi_short = "ocp/hwcomp",
163 	.nlpi_human = "Hardware Component",
164 	.nlpi_lid = OCP_LOG_DSSD_HW_COMP,
165 	.nlpi_csi = NVME_CSI_NVM,
166 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
167 	.nlpi_source = NVME_LOG_DISC_S_DB,
168 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
169 	.nlpi_len = sizeof (ocp_vul_hw_comp_t),
170 	.nlpi_var_func = nvme_ocp_hwcomp_var_len
171 };
172 
173 const nvme_log_page_info_t ocp_log_telstr = {
174 	.nlpi_short = "ocp/telstr",
175 	.nlpi_human = "OCP Telemetry String",
176 	.nlpi_lid = OCP_LOG_DSSD_TELEMETRY,
177 	.nlpi_csi = NVME_CSI_NVM,
178 	.nlpi_kind = NVME_LOG_ID_VENDOR_SPECIFIC,
179 	.nlpi_source = NVME_LOG_DISC_S_DB,
180 	.nlpi_scope = NVME_LOG_SCOPE_NVM,
181 	.nlpi_len = offsetof(ocp_vul_telstr_t, ots_rsvd40),
182 	.nlpi_var_func = nvme_ocp_telstr_var_len
183 };
184 
185 const nvme_feat_info_t ocp_feat_errinj = {
186 	.nfeat_short = "ocp/errinj",
187 	.nfeat_spec = "OCP Error Injection",
188 	.nfeat_fid = OCP_FEAT_DSSD_ERR_INJ,
189 	.nfeat_kind = NVME_FEAT_VENDOR_SPECIFIC,
190 	.nfeat_scope = NVME_FEAT_SCOPE_NVM,
191 	.nfeat_csi = NVME_FEAT_CSI_NONE,
192 	.nfeat_in_get = NVME_GET_FEAT_F_DATA,
193 	.nfeat_in_set = NVME_SET_FEAT_F_CDW11 | NVME_SET_FEAT_F_DATA,
194 	.nfeat_out_get = NVME_FEAT_OUTPUT_CDW0 | NVME_FEAT_OUTPUT_DATA,
195 	/*
196 	 * The OCP specification reqiurement SERRI-9 says that this must always
197 	 * be a 4K payload that has [0, 127] entries.
198 	 */
199 	.nfeat_len = (OCP_ERRINJ_MAX_INJECT + 1) * sizeof (ocp_vuf_errinj_t)
200 };
201 
202 /*
203  * This was originally called the EOL/PLP Failure Mode, but the EOL was dropped
204  * starting in version 2.6 of the OCP DSSD NVMe specification.
205  */
206 const nvme_feat_info_t ocp_feat_plpfail = {
207 	.nfeat_short = "ocp/plpfail",
208 	.nfeat_spec = "OCP PLP Failure Mode",
209 	.nfeat_fid = OCP_FEAT_DSSD_EOLPLP,
210 	.nfeat_kind = NVME_FEAT_VENDOR_SPECIFIC,
211 	.nfeat_scope = NVME_FEAT_SCOPE_NVM,
212 	.nfeat_csi = NVME_FEAT_CSI_NONE,
213 	.nfeat_in_set = NVME_SET_FEAT_F_CDW11,
214 	.nfeat_out_get = NVME_FEAT_OUTPUT_CDW0
215 };
216 
217 const nvme_feat_info_t ocp_feat_plphealth = {
218 	.nfeat_short = "ocp/plphealth",
219 	.nfeat_spec = "OCP PLP Health Check Interval",
220 	.nfeat_fid = OCP_FEAT_DSSD_PLP_HEALTH,
221 	.nfeat_kind = NVME_FEAT_VENDOR_SPECIFIC,
222 	.nfeat_scope = NVME_FEAT_SCOPE_NVM,
223 	.nfeat_csi = NVME_FEAT_CSI_NONE,
224 	.nfeat_in_set = NVME_SET_FEAT_F_CDW11,
225 	.nfeat_out_get = NVME_FEAT_OUTPUT_CDW0
226 };
227 
228 typedef enum {
229 	NVME_OCP_ERRINJ_FIELD_TYPE	= 0,
230 	NVME_OCP_ERRINJ_FIELD_NRTDE,
231 	NVME_OCP_ERRINJ_FIELD_LD
232 } nvme_ocp_errinj_req_field_t;
233 
234 const nvme_field_info_t nvme_ocp_errinj_fields[] = {
235 	[NVME_OCP_ERRINJ_FIELD_TYPE] = {
236 		.nlfi_vers = &nvme_vers_1v0,
237 		.nlfi_max_size = UINT16_MAX,
238 		.nlfi_spec = "eit",
239 		.nlfi_human = "error injection type",
240 		.nlfi_def_req = true,
241 		.nlfi_def_allow = true
242 	},
243 	[NVME_OCP_ERRINJ_FIELD_NRTDE] = {
244 		.nlfi_vers = &nvme_vers_1v0,
245 		.nlfi_max_size = UINT16_MAX,
246 		.nlfi_spec = "nrtde",
247 		.nlfi_human = "number of reads to trigger device panic",
248 		.nlfi_def_req = false,
249 		.nlfi_def_allow = true
250 	},
251 	/*
252 	 * This field was added in OCP DSSD v2.7. It only applies to event 0xe.
253 	 * While we could get the DSSD spec version out of the health and error
254 	 * reporting to see if this is supported as part of a version check, for
255 	 * now we just let it be set and allow the device to allow or deny it.
256 	 */
257 	[NVME_OCP_ERRINJ_FIELD_LD] = {
258 		.nlfi_vers = &nvme_vers_1v0,
259 		.nlfi_max_size = UINT32_MAX,
260 		.nlfi_spec = "ld",
261 		.nlfi_human = "latency duration",
262 		.nlfi_def_req = false,
263 		.nlfi_def_allow = true
264 	}
265 };
266 
267 void
268 nvme_ocp_errinj_req_fini(nvme_ocp_errinj_req_t *req)
269 {
270 	if (req != NULL) {
271 		nvme_set_feat_req_fini(req->oer_feat);
272 		free(req);
273 	}
274 }
275 
276 bool
277 nvme_ocp_errinj_req_init(nvme_ctrl_t *ctrl, nvme_ocp_errinj_req_t **reqp)
278 {
279 	nvme_ocp_errinj_req_t *req;
280 
281 	if (reqp == NULL) {
282 		return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
283 		    "encountered invalid nvme_ocp_errinj_req_t output pointer: "
284 		    "%p", reqp));
285 	}
286 
287 	req = calloc(1, sizeof (nvme_ocp_errinj_req_t));
288 	if (req == NULL) {
289 		int e = errno;
290 		return (nvme_ctrl_error(ctrl, NVME_ERR_NO_MEM, e, "failed to "
291 		    "allocate memory for a new nvm_ocp_errinj_req_t: %s",
292 		    strerror(e)));
293 	}
294 
295 	if (!nvme_vendor_feature_supported(ctrl, "ocp/errinj") ||
296 	    !nvme_set_feat_req_init_by_name(ctrl, "ocp/errinj", 0, NULL,
297 	    &req->oer_feat)) {
298 		nvme_ocp_errinj_req_fini(req);
299 		return (false);
300 	}
301 
302 	for (size_t i = 0; i < ARRAY_SIZE(nvme_ocp_errinj_fields); i++) {
303 		if (nvme_ocp_errinj_fields[i].nlfi_def_req) {
304 			req->oer_need |= 1 << i;
305 		}
306 
307 		if (nvme_ocp_errinj_fields[i].nlfi_def_allow) {
308 			req->oer_allow |= 1 << i;
309 		}
310 	}
311 
312 	*reqp = req;
313 	return (nvme_ctrl_success(ctrl));
314 }
315 
316 static void
317 nvme_ocp_errinj_req_clear_need(nvme_ocp_errinj_req_t *req,
318     nvme_ocp_errinj_req_field_t field)
319 {
320 	req->oer_need &= ~(1 << field);
321 }
322 
323 static const nvme_field_check_t nvme_ocp_errinj_check_type = {
324 	nvme_ocp_errinj_fields, NVME_OCP_ERRINJ_FIELD_TYPE,
325 	NVME_ERR_OCP_ERRINJ_TYPE_RANGE, 0, 0
326 };
327 
328 bool
329 nvme_ocp_errinj_req_set_type(nvme_ocp_errinj_req_t *req, uint32_t type)
330 {
331 	nvme_ctrl_t *ctrl = req->oer_feat->sfr_ctrl;
332 
333 	if (!nvme_field_check_one(ctrl, type, "ocp error injection",
334 	    &nvme_ocp_errinj_check_type, req->oer_allow)) {
335 		return (false);
336 	}
337 
338 	req->oer_err[0].oei_type = (uint16_t)type;
339 	nvme_ocp_errinj_req_clear_need(req, NVME_OCP_ERRINJ_FIELD_TYPE);
340 	return (nvme_ctrl_success(ctrl));
341 }
342 
343 static const nvme_field_check_t nvme_ocp_errinj_check_nread = {
344 	nvme_ocp_errinj_fields, NVME_OCP_ERRINJ_FIELD_NRTDE,
345 	NVME_ERR_OCP_ERRINJ_NRTDE_RANGE, 0, 0
346 };
347 
348 bool
349 nvme_ocp_errinj_req_set_nrtde(nvme_ocp_errinj_req_t *req, uint32_t nreads)
350 {
351 	nvme_ctrl_t *ctrl = req->oer_feat->sfr_ctrl;
352 
353 	if (!nvme_field_check_one(ctrl, nreads, "ocp error injection",
354 	    &nvme_ocp_errinj_check_nread, req->oer_allow)) {
355 		return (false);
356 	}
357 
358 	req->oer_err[0].oei_nrtde = (uint16_t)nreads;
359 	nvme_ocp_errinj_req_clear_need(req, NVME_OCP_ERRINJ_FIELD_NRTDE);
360 	return (nvme_ctrl_success(ctrl));
361 }
362 
363 static const nvme_field_check_t nvme_ocp_errinj_check_ld = {
364 	nvme_ocp_errinj_fields, NVME_OCP_ERRINJ_FIELD_LD,
365 	NVME_ERR_OCP_ERRINJ_LD_RANGE, NVME_ERR_OCP_ERRINJ_LD_UNSUP,
366 	NVME_ERR_OCP_ERRINJ_LD_UNUSE
367 };
368 
369 /*
370  * We can't check if this field can be set until after an event is set. We defer
371  * checking it right now until exec. We don't have a copy of the OCP
372  * SMART/Health log which means we can't right now send the unsupported error
373  * check. The error constant exists so we can figure out a means of doing so in
374  * the future if that makes sense.
375  */
376 bool
377 nvme_ocp_errinj_req_set_ld(nvme_ocp_errinj_req_t *req, uint32_t lat)
378 {
379 	nvme_ctrl_t *ctrl = req->oer_feat->sfr_ctrl;
380 
381 	if (!nvme_field_check_one(ctrl, lat, "ocp error injection",
382 	    &nvme_ocp_errinj_check_ld, req->oer_allow)) {
383 		return (false);
384 	}
385 
386 	req->oer_err[0].oei_lat = lat;
387 	nvme_ocp_errinj_req_clear_need(req, NVME_OCP_ERRINJ_FIELD_LD);
388 	return (nvme_ctrl_success(ctrl));
389 }
390 
391 bool
392 nvme_ocp_errinj_req_exec(nvme_ocp_errinj_req_t *req)
393 {
394 	nvme_ctrl_t *ctrl = req->oer_feat->sfr_ctrl;
395 
396 	if (req->oer_need != 0) {
397 		return (nvme_field_miss_err(ctrl, nvme_ocp_errinj_fields,
398 		    ARRAY_SIZE(nvme_ocp_errinj_fields),
399 		    NVME_ERR_OCP_ERRINJ_REQ_MISSING_FIELDS,
400 		    "ocp error injection", req->oer_need));
401 	}
402 
403 	/*
404 	 * Now that we know the tpye of request, check that the type makes sense
405 	 * and evaluate if the latency duration makes sense.
406 	 */
407 	if (req->oer_err[0].oei_lat != 0 && req->oer_err[0].oei_type !=
408 	    OCP_ERRINJ_T_LATENCY_SPIKE) {
409 		return (nvme_ctrl_error(ctrl, NVME_ERR_OCP_ERRINJ_LD_UNUSE, 0,
410 		    "the latency duration field may only be used with the "
411 		    "latency spike event type (0x%x): found 0x%x",
412 		    OCP_ERRINJ_T_LATENCY_SPIKE, req->oer_err[0].oei_type));
413 	}
414 
415 	/*
416 	 * Beginning in OCP DSSD version 2.7 injecting a single event and
417 	 * ensuring only a single instance was present became required. This is
418 	 * likely a constraint of older devices which is why we only allow a
419 	 * single event to be injected and always set that.
420 	 */
421 	req->oer_err[0].oei_flags = OCP_ERRINJ_F_ENABLE | OCP_ERRINJ_F_SINGLE;
422 	if (!nvme_set_feat_req_set_cdw11(req->oer_feat, 1) ||
423 	    !nvme_set_feat_req_set_input(req->oer_feat, req->oer_err,
424 	    sizeof (req->oer_err))) {
425 		return (false);
426 	}
427 
428 	return (nvme_set_feat_req_exec(req->oer_feat));
429 }
430 
431 /*
432  * Send a request to clear all pending errors.
433  */
434 bool
435 nvme_ocp_errinj_clear(nvme_ctrl_t *ctrl)
436 {
437 	nvme_set_feat_req_t *req;
438 	bool ret = false;
439 	ocp_vuf_errinj_t table[OCP_ERRINJ_MAX_INJECT + 1];
440 
441 	if (!nvme_vendor_feature_supported(ctrl, "ocp/errinj") ||
442 	    !nvme_set_feat_req_init_by_name(ctrl, "ocp/errinj", 0, NULL,
443 	    &req)) {
444 		return (false);
445 	}
446 
447 	/*
448 	 * The specification isn't quite clear if no table should be sent if the
449 	 * event count is zero, so we always send this regardless. A zeroed
450 	 * table suggests no entries are valid and coincides with the cdw11
451 	 * value.
452 	 */
453 	(void) memset(table, 0, sizeof (table));
454 	if (nvme_set_feat_req_set_cdw11(req, 0) &&
455 	    nvme_set_feat_req_set_input(req, table, sizeof (table)) &&
456 	    nvme_set_feat_req_exec(req)) {
457 		ret = nvme_ctrl_success(ctrl);
458 	}
459 
460 	nvme_set_feat_req_fini(req);
461 	return (ret);
462 }
463