root/usr/src/lib/libnvme/common/libnvme_vuc.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2024 Oxide Computer Company
 */

/*
 * NVMe Vendor Unique Command (VUC) support. The NVMe standard offers support
 * for a 'standard' format for vendor unique admin and NVMe commands. We provide
 * both a discovery mechanism and a way to construct and execute vendor unique
 * commands. Unlike with log page and feature discovery there is not a way to
 * turn the discovery information into a request structure. Rather, our
 * expectation is that more intrinsic library functions for these would be added
 * based on the specifics of the unique commands.
 */

#include <strings.h>
#include <unistd.h>

#include "libnvme_impl.h"

void
nvme_vuc_disc_free(nvme_vuc_disc_t *disc)
{
        free(disc);
}

bool
nvme_vuc_disc_dup(nvme_ctrl_t *ctrl, const nvme_vuc_disc_t *src,
    nvme_vuc_disc_t **discp)
{
        nvme_vuc_disc_t *disc;

        if (src == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_disc_t pointer to duplicate: "
                    "%p", discp));
        }

        if (discp == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_disc_t output pointer: %p",
                    discp));
        }

        disc = calloc(1, sizeof (nvme_vuc_disc_t));
        if (disc == NULL) {
                int e = errno;
                return (nvme_ctrl_error(ctrl, NVME_ERR_NO_MEM, e, "failed to "
                    "allocate memory for a new nvme_vuc_disc_t: %s",
                    strerror(e)));
        }

        (void) memcpy(disc, src, sizeof (nvme_vuc_disc_t));
        *discp = disc;
        return (nvme_ctrl_success(ctrl));

}

const char *
nvme_vuc_disc_name(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_short);
}

const char *
nvme_vuc_disc_desc(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_desc);
}

uint32_t
nvme_vuc_disc_opcode(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_opc);
}

nvme_vuc_disc_io_t
nvme_vuc_disc_dt(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_dt);
}

nvme_vuc_disc_impact_t
nvme_vuc_disc_impact(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_impact);
}

nvme_vuc_disc_lock_t
nvme_vuc_disc_lock(const nvme_vuc_disc_t *disc)
{
        return (disc->nvd_lock);
}

void
nvme_vuc_discover_fini(nvme_vuc_iter_t *iter)
{
        free(iter);
}

nvme_iter_t
nvme_vuc_discover_step(nvme_vuc_iter_t *iter, const nvme_vuc_disc_t **outp)
{
        nvme_ctrl_t *ctrl = iter->nvi_ctrl;

        if (ctrl->nc_vsd == NULL) {
                return (NVME_ITER_DONE);
        }

        if (iter->nvi_cur_idx >= ctrl->nc_vsd->nvd_nvuc) {
                return (NVME_ITER_DONE);
        }

        *outp = &ctrl->nc_vsd->nvd_vuc[iter->nvi_cur_idx];
        iter->nvi_cur_idx++;
        return (NVME_ITER_VALID);
}

bool
nvme_vuc_discover_init(nvme_ctrl_t *ctrl, uint32_t flags,
    nvme_vuc_iter_t **iterp)
{
        nvme_vuc_iter_t *iter;

        if (flags != 0) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_FLAG, 0,
                    "encountered invalid discovery flags: 0x%x", flags));
        }

        if (iterp == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_iter_t output pointer: %p",
                    iterp));
        }

        iter = calloc(1, sizeof (nvme_vuc_iter_t));
        if (iter == NULL) {
                int e = errno;
                return (nvme_ctrl_error(ctrl, NVME_ERR_NO_MEM, e, "failed to "
                    "allocate memory for a new nvme_vuc_iter_t: %s",
                    strerror(e)));
        }

        iter->nvi_ctrl = ctrl;

        *iterp = iter;
        return (nvme_ctrl_success(ctrl));
}

bool
nvme_vuc_discover(nvme_ctrl_t *ctrl, uint32_t flags, nvme_vuc_disc_f func,
    void *arg)
{
        nvme_vuc_iter_t *iter;
        nvme_iter_t ret;
        const nvme_vuc_disc_t *disc;

        if (func == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_disc_f function pointer: %p",
                    func));
        }

        if (!nvme_vuc_discover_init(ctrl, flags, &iter)) {
                return (false);
        }

        while ((ret = nvme_vuc_discover_step(iter, &disc)) == NVME_ITER_VALID) {
                if (!func(ctrl, disc, arg))
                        break;
        }

        nvme_vuc_discover_fini(iter);
        if (ret == NVME_ITER_ERROR) {
                return (false);
        }

        return (nvme_ctrl_success(ctrl));
}

bool
nvme_vuc_discover_by_name(nvme_ctrl_t *ctrl, const char *name, uint32_t flags,
    nvme_vuc_disc_t **discp)
{
        nvme_vuc_iter_t *iter;
        nvme_iter_t ret;
        const nvme_vuc_disc_t *disc;

        if (discp == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_disc_t output pointer: %p",
                    discp));
        }

        if (name == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid pointer for name: %p", name));
        }

        if (!nvme_vuc_discover_init(ctrl, flags, &iter)) {
                return (false);
        }

        *discp = NULL;
        while ((ret = nvme_vuc_discover_step(iter, &disc)) == NVME_ITER_VALID) {
                if (strcmp(name, nvme_vuc_disc_name(disc)) == 0) {
                        break;
                }
        }

        if (ret == NVME_ITER_VALID && !nvme_vuc_disc_dup(ctrl, disc, discp)) {
                nvme_err_data_t err;

                nvme_ctrl_err_save(ctrl, &err);
                nvme_vuc_discover_fini(iter);
                nvme_ctrl_err_set(ctrl, &err);
                return (false);
        }

        nvme_vuc_discover_fini(iter);
        if (ret == NVME_ITER_ERROR) {
                return (false);
        }

        if (*discp == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_VUC_UNKNOWN, 0, "failed "
                    "to map %s to a known vendor unique command", name));
        }

        return (nvme_ctrl_success(ctrl));
}

void
nvme_vuc_req_fini(nvme_vuc_req_t *req)
{
        free(req);
}

bool
nvme_vuc_req_init(nvme_ctrl_t *ctrl, nvme_vuc_req_t **reqp)
{
        nvme_vuc_req_t *req;

        if (reqp == NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid nvme_vuc_req_t output pointer: %p",
                    reqp));
        }

        if (ctrl->nc_info.id_nvscc.nv_spec == 0) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_VUC_UNSUP_BY_DEV, 0,
                    "cannot create vuc request because the controller does "
                    "not support the NVMe standard vendor unique command "
                    "interface"));
        }

        req = calloc(1, sizeof (nvme_vuc_req_t));
        if (req == NULL) {
                int e = errno;
                return (nvme_ctrl_error(ctrl, NVME_ERR_NO_MEM, e, "failed to "
                    "allocate memory for a new nvme_vuc_req_t: %s",
                    strerror(e)));
        }

        req->nvr_ctrl = ctrl;

        for (size_t i = 0; i < nvme_vuc_nfields; i++) {
                if (nvme_vuc_fields[i].nlfi_def_req) {
                        req->nvr_need |= 1 << i;
                }
        }

        *reqp = req;
        return (nvme_ctrl_success(ctrl));
}

static void
nvme_vuc_req_clear_need(nvme_vuc_req_t *req, nvme_vuc_req_field_t field)
{
        req->nvr_need &= ~(1 << field);
}

/*
 * We have no way to validate any of the cdw1[2-5] values as these are all
 * vendor-specific commands and the semantics of these are not something we can
 * know. Therefore there are no calls to validate these fields.
 */
bool
nvme_vuc_req_set_cdw12(nvme_vuc_req_t *req, uint32_t cdw12)
{
        req->nvr_cdw12 = cdw12;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_CDW12);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_set_cdw13(nvme_vuc_req_t *req, uint32_t cdw13)
{
        req->nvr_cdw13 = cdw13;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_CDW13);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_set_cdw14(nvme_vuc_req_t *req, uint32_t cdw14)
{
        req->nvr_cdw14 = cdw14;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_CDW14);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_set_cdw15(nvme_vuc_req_t *req, uint32_t cdw15)
{
        req->nvr_cdw15 = cdw15;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_CDW15);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

static const nvme_field_check_t nvme_vuc_check_opcode = {
        nvme_vuc_fields, NVME_VUC_REQ_FIELD_OPC,
        NVME_ERR_VUC_OPCODE_RANGE, 0, 0
};

bool
nvme_vuc_req_set_opcode(nvme_vuc_req_t *req, uint32_t opc)
{
        if (!nvme_field_check_one(req->nvr_ctrl, opc, "vendor unique command",
            &nvme_vuc_check_opcode, 0)) {
                return (false);
        }

        req->nvr_opcode = opc;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_OPC);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

static const nvme_field_check_t nvme_vuc_check_nsid = {
        nvme_vuc_fields, NVME_VUC_REQ_FIELD_NSID,
        NVME_ERR_NS_RANGE, 0, 0
};

bool
nvme_vuc_req_set_nsid(nvme_vuc_req_t *req, uint32_t nsid)
{
        if (!nvme_field_check_one(req->nvr_ctrl, nsid, "vendor unique command",
            &nvme_vuc_check_nsid, 0)) {
                return (false);
        }

        req->nvr_nsid = nsid;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_NSID);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

static const nvme_field_check_t nvme_vuc_check_to = {
        nvme_vuc_fields, NVME_VUC_REQ_FIELD_TO,
        NVME_ERR_VUC_TIMEOUT_RANGE, 0, 0
};

bool
nvme_vuc_req_set_timeout(nvme_vuc_req_t *req, uint32_t to)
{
        if (!nvme_field_check_one(req->nvr_ctrl, to, "vendor unique command",
            &nvme_vuc_check_to, 0)) {
                return (false);
        }

        req->nvr_timeout = to;
        nvme_vuc_req_clear_need(req, NVME_VUC_REQ_FIELD_TO);
        return (nvme_ctrl_success(req->nvr_ctrl));
}

/*
 * Check common parts of a VUC data transfer. While the kernel is going to
 * further constrain our length, we will still check the specified length
 * against the actual specification max.
 */
static const nvme_field_check_t nvme_vuc_check_ndt = {
        nvme_vuc_fields, NVME_VUC_REQ_FIELD_NDT,
        NVME_ERR_VUC_NDT_RANGE, 0, 0
};

static bool
nvme_vuc_req_data_validate(nvme_vuc_req_t *req, const void *buf, size_t len,
    bool in)
{
        nvme_ctrl_t *ctrl = req->nvr_ctrl;
        const char *dir = in ? "input" : "output";
        const char *alt_dir = in ? "output" : "input";
        const void *alt_buf = in ? req->nvr_output : req->nvr_input;

        if (buf == NULL && len > 0) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_BAD_PTR, 0, "vendor "
                    "unique command output output buffer cannot be NULL when "
                    "the length is non-zero"));
        } else if (buf != NULL && len == 0) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_VUC_NDT_RANGE, 0,
                    "vendor unique command buffer size may not be zero when "
                    "given a non-NULL pointer (%p)", buf));
        }

        if (alt_buf != NULL && buf != NULL) {
                return (nvme_ctrl_error(ctrl, NVME_ERR_VUC_CANNOT_RW, 0,
                    "an %s buffer is already set and therefore an %s buffer "
                    "cannot also be added", alt_dir, dir));

        }

        /*
         * This takes care of alignment and the upper bound.
         */
        if (!nvme_field_check_one(req->nvr_ctrl, len, "vendor unique command",
            &nvme_vuc_check_ndt, 0)) {
                return (false);
        }

        return (true);
}

/*
 * The impact values are a libnvme specific item which maps things to the
 * kernel's values. Therefore we don't use the standard validation routines.
 */
bool
nvme_vuc_req_set_impact(nvme_vuc_req_t *req, nvme_vuc_disc_impact_t impact)
{
        const nvme_vuc_disc_impact_t all_impact = NVME_VUC_DISC_IMPACT_DATA |
            NVME_VUC_DISC_IMPACT_NS;

        if ((impact & ~all_impact) != 0) {
                return (nvme_ctrl_error(req->nvr_ctrl,
                    NVME_ERR_VUC_IMPACT_RANGE, 0, "encountered unknown impact "
                    "flags: 0x%x", impact & ~all_impact));
        }

        req->nvr_impact = impact;
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_set_output(nvme_vuc_req_t *req, void *buf, size_t len)
{
        if (!nvme_vuc_req_data_validate(req, buf, len, false)) {
                return (false);
        }

        req->nvr_output = buf;
        req->nvr_outlen = len;
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_clear_output(nvme_vuc_req_t *req)
{
        req->nvr_output = NULL;
        req->nvr_outlen = 0;
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_set_input(nvme_vuc_req_t *req, const void *buf, size_t len)
{
        if (!nvme_vuc_req_data_validate(req, buf, len, true)) {
                return (false);
        }

        req->nvr_input = buf;
        req->nvr_inlen = len;
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_get_cdw0(nvme_vuc_req_t *req, uint32_t *cdw0)
{
        if (cdw0 == NULL) {
                return (nvme_ctrl_error(req->nvr_ctrl, NVME_ERR_BAD_PTR, 0,
                    "encountered invalid cdw0 output pointer: %p", cdw0));
        }

        if (!req->nvr_results_valid) {
                return (nvme_ctrl_error(req->nvr_ctrl, NVME_ERR_VUC_NO_RESULTS,
                    0, "vendor unique command results are not currently valid "
                    "and cannot be returned"));
        }

        *cdw0 = req->nvr_cdw0;
        return (nvme_ctrl_success(req->nvr_ctrl));
}

bool
nvme_vuc_req_exec(nvme_vuc_req_t *req)
{
        nvme_ctrl_t *ctrl = req->nvr_ctrl;
        nvme_ioctl_passthru_t pass;

        /*
         * Immediately invalidate our stored data.
         */
        req->nvr_results_valid = false;
        req->nvr_cdw0 = 0;

        if (req->nvr_need != 0) {
                return (nvme_field_miss_err(ctrl, nvme_vuc_fields,
                    nvme_vuc_nfields, NVME_ERR_VUC_REQ_MISSING_FIELDS,
                    "vendor unique command", req->nvr_need));
        }

        (void) memset(&pass, 0, sizeof (nvme_ioctl_passthru_t));
        pass.npc_common.nioc_nsid = req->nvr_nsid;
        pass.npc_opcode = req->nvr_opcode;
        pass.npc_timeout = req->nvr_timeout;
        pass.npc_cdw12 = req->nvr_cdw12;
        pass.npc_cdw13 = req->nvr_cdw13;
        pass.npc_cdw14 = req->nvr_cdw14;
        pass.npc_cdw15 = req->nvr_cdw14;

        if (req->nvr_input != NULL) {
                pass.npc_buflen = req->nvr_inlen;
                pass.npc_buf = (uintptr_t)req->nvr_input;
                pass.npc_flags = NVME_PASSTHRU_WRITE;
        } else if (req->nvr_output != NULL) {
                pass.npc_buflen = req->nvr_outlen;
                pass.npc_buf = (uintptr_t)req->nvr_output;
                pass.npc_flags = NVME_PASSTHRU_READ;
        }

        if ((req->nvr_impact & NVME_VUC_DISC_IMPACT_NS) != 0) {
                pass.npc_impact |= NVME_IMPACT_NS;
        }

        if (ioctl(ctrl->nc_fd, NVME_IOC_PASSTHRU, &pass) != 0) {
                int e = errno;
                return (nvme_ioctl_syserror(ctrl, e, "vendor unique command"));
        }

        if (pass.npc_common.nioc_drv_err != NVME_IOCTL_E_OK) {
                return (nvme_ioctl_error(ctrl, &pass.npc_common,
                    "vendor unique command"));
        }

        req->nvr_results_valid = true;
        req->nvr_cdw0 = pass.npc_cdw0;

        return (nvme_ctrl_success(ctrl));
}