root/usr/src/uts/common/io/ufm.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 2019 Joyent, Inc.
 * Copyright 2020 Oxide Computer Company
 */

/*
 * The ufm(4D) pseudo driver provides an ioctl interface for DDI UFM
 * information.  See ddi_ufm.h.
 */
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/esunddi.h>
#include <sys/ddi_ufm.h>
#include <sys/ddi_ufm_impl.h>
#include <sys/conf.h>
#include <sys/debug.h>
#include <sys/file.h>
#include <sys/kmem.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>

#define UFM_READ_SIZE           (1 * 1024 * 1024)

#define UFMTEST_IOC             ('u' << 24) | ('f' << 16) | ('t' << 8)
#define UFMTEST_IOC_SETFW       (UFMTEST_IOC | 1)

static dev_info_t *ufm_devi = NULL;

static int ufm_open(dev_t *, int, int, cred_t *);
static int ufm_close(dev_t, int, int, cred_t *);
static int ufm_ioctl(dev_t, int, intptr_t, int, cred_t *, int *);

static struct cb_ops ufm_cb_ops = {
        .cb_open =      ufm_open,
        .cb_close =     ufm_close,
        .cb_strategy =  nodev,
        .cb_print =     nodev,
        .cb_dump =      nodev,
        .cb_read =      nodev,
        .cb_write =     nodev,
        .cb_ioctl =     ufm_ioctl,
        .cb_devmap =    nodev,
        .cb_mmap =      nodev,
        .cb_segmap =    nodev,
        .cb_chpoll =    nochpoll,
        .cb_prop_op =   ddi_prop_op,
        .cb_str =       NULL,
        .cb_flag =      D_NEW | D_MP,
        .cb_rev =       CB_REV,
        .cb_aread =     nodev,
        .cb_awrite =    nodev
};

static int ufm_info(dev_info_t *, ddi_info_cmd_t, void *, void **);
static int ufm_attach(dev_info_t *, ddi_attach_cmd_t);
static int ufm_detach(dev_info_t *, ddi_detach_cmd_t);

static struct dev_ops ufm_ops = {
        .devo_rev =             DEVO_REV,
        .devo_refcnt =          0,
        .devo_getinfo =         ufm_info,
        .devo_identify =        nulldev,
        .devo_probe =           nulldev,
        .devo_attach =          ufm_attach,
        .devo_detach =          ufm_detach,
        .devo_reset =           nodev,
        .devo_cb_ops =          &ufm_cb_ops,
        .devo_bus_ops =         NULL,
        .devo_power =           NULL,
        .devo_quiesce =         ddi_quiesce_not_needed
};

static struct modldrv modldrv = {
        .drv_modops =           &mod_driverops,
        .drv_linkinfo =         "Upgradeable FW Module driver",
        .drv_dev_ops =          &ufm_ops
};

static struct modlinkage modlinkage = {
        .ml_rev =               MODREV_1,
        .ml_linkage =           { (void *)&modldrv, NULL }
};

int
_init(void)
{
        return (mod_install(&modlinkage));
}

int
_fini(void)
{
        return (mod_remove(&modlinkage));
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&modlinkage, modinfop));
}

static int
ufm_info(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
        switch (infocmd) {
        case DDI_INFO_DEVT2DEVINFO:
                *result = ufm_devi;
                return (DDI_SUCCESS);
        case DDI_INFO_DEVT2INSTANCE:
                *result = 0;
                return (DDI_SUCCESS);
        }
        return (DDI_FAILURE);
}

static int
ufm_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
        if (cmd != DDI_ATTACH || ufm_devi != NULL)
                return (DDI_FAILURE);

        if (ddi_create_minor_node(devi, "ufm", S_IFCHR, 0, DDI_PSEUDO, 0) ==
            DDI_FAILURE) {
                ddi_remove_minor_node(devi, NULL);
                return (DDI_FAILURE);
        }

        ufm_devi = devi;
        return (DDI_SUCCESS);
}

static int
ufm_detach(dev_info_t *devi, ddi_detach_cmd_t cmd)
{
        if (cmd != DDI_DETACH)
                return (DDI_FAILURE);

        if (devi != NULL)
                ddi_remove_minor_node(devi, NULL);

        ufm_devi = NULL;
        return (DDI_SUCCESS);
}

static int
ufm_open(dev_t *devp, int flag, int otyp, cred_t *credp)
{
        const int inv_flags = FWRITE | FEXCL | FNDELAY | FNONBLOCK;

        if (otyp != OTYP_CHR)
                return (EINVAL);

        if (flag & inv_flags)
                return (EINVAL);

        if (drv_priv(credp) != 0)
                return (EPERM);

        return (0);
}

static int
ufm_close(dev_t dev, int flag, int otyp, cred_t *credp)
{
        return (0);
}

static boolean_t
ufm_driver_ready(ddi_ufm_handle_t *ufmh)
{
        VERIFY(ufmh != NULL);

        if (ufmh->ufmh_state & DDI_UFM_STATE_SHUTTING_DOWN ||
            !(ufmh->ufmh_state & DDI_UFM_STATE_READY)) {
                return (B_FALSE);
        }
        return (B_TRUE);
}

static int
ufm_do_getcaps(intptr_t data, int mode)
{
        ddi_ufm_handle_t *ufmh;
        ddi_ufm_cap_t caps;
        ufm_ioc_getcaps_t ugc;
        dev_info_t *dip;
        int ret;
        char devpath[MAXPATHLEN];

        if (ddi_copyin((void *)data, &ugc, sizeof (ufm_ioc_getcaps_t),
            mode) != 0)
                return (EFAULT);

        if (strlcpy(devpath, ugc.ufmg_devpath, MAXPATHLEN) >= MAXPATHLEN)
                return (EOVERFLOW);

        if ((dip = e_ddi_hold_devi_by_path(devpath, 0)) == NULL) {
                return (ENOTSUP);
        }
        if ((ufmh = ufm_find(devpath)) == NULL) {
                ddi_release_devi(dip);
                return (ENOTSUP);
        }
        ASSERT(MUTEX_HELD(&ufmh->ufmh_lock));

        if (!ufm_driver_ready(ufmh)) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (EAGAIN);
        }

        if (ugc.ufmg_version != ufmh->ufmh_version) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ENOTSUP);
        }

        if ((ret = ufm_cache_fill(ufmh)) != 0) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ret);
        }

        ret = ufmh->ufmh_ops->ddi_ufm_op_getcaps(ufmh, ufmh->ufmh_arg, &caps);
        mutex_exit(&ufmh->ufmh_lock);
        ddi_release_devi(dip);

        if (ret != 0)
                return (ret);

        ugc.ufmg_caps = caps;

        if (ddi_copyout(&ugc, (void *)data, sizeof (ufm_ioc_getcaps_t),
            mode) != 0)
                return (EFAULT);

        return (0);
}

static int
ufm_do_reportsz(intptr_t data, int mode)
{
        ddi_ufm_handle_t *ufmh;
        dev_info_t *dip;
        uint_t model;
        size_t sz;
        int ret;
        char devpath[MAXPATHLEN];
        ufm_ioc_bufsz_t ufbz;
#ifdef _MULTI_DATAMODEL
        ufm_ioc_bufsz32_t ufbz32;
#endif

        model = ddi_model_convert_from(mode);

        switch (model) {
#ifdef _MULTI_DATAMODEL
        case DDI_MODEL_ILP32:
                if (ddi_copyin((void *)data, &ufbz32,
                    sizeof (ufm_ioc_bufsz32_t), mode) != 0)
                        return (EFAULT);
                ufbz.ufbz_version = ufbz32.ufbz_version;
                if (strlcpy(ufbz.ufbz_devpath, ufbz32.ufbz_devpath,
                    MAXPATHLEN) >= MAXPATHLEN) {
                        return (EOVERFLOW);
                }
                break;
#endif /* _MULTI_DATAMODEL */
        case DDI_MODEL_NONE:
        default:
                if (ddi_copyin((void *)data, &ufbz,
                    sizeof (ufm_ioc_bufsz_t), mode) != 0)
                        return (EFAULT);
        }

        if (strlcpy(devpath, ufbz.ufbz_devpath, MAXPATHLEN) >= MAXPATHLEN)
                return (EOVERFLOW);

        if ((dip = e_ddi_hold_devi_by_path(devpath, 0)) == NULL) {
                return (ENOTSUP);
        }
        if ((ufmh = ufm_find(devpath)) == NULL) {
                ddi_release_devi(dip);
                return (ENOTSUP);
        }
        ASSERT(MUTEX_HELD(&ufmh->ufmh_lock));

        if (!ufm_driver_ready(ufmh)) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (EAGAIN);
        }

        if (ufbz.ufbz_version != ufmh->ufmh_version) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ENOTSUP);
        }

        /*
         * Note - ufm_cache_fill() also takes care of verifying that the driver
         * supports the DDI_UFM_CAP_REPORT capability and will return non-zero,
         * if not supported.
         */
        if ((ret = ufm_cache_fill(ufmh)) != 0) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ret);
        }
        ddi_release_devi(dip);

        ret = nvlist_size(ufmh->ufmh_report, &sz, NV_ENCODE_NATIVE);
        mutex_exit(&ufmh->ufmh_lock);
        if (ret != 0)
                return (ret);

        switch (model) {
#ifdef _MULTI_DATAMODEL
        case DDI_MODEL_ILP32:
                ufbz32.ufbz_size = sz;
                if (ddi_copyout(&ufbz32, (void *)data,
                    sizeof (ufm_ioc_bufsz32_t), mode) != 0)
                        return (EFAULT);
                break;
#endif /* _MULTI_DATAMODEL */
        case DDI_MODEL_NONE:
        default:
                ufbz.ufbz_size = sz;
                if (ddi_copyout(&ufbz, (void *)data,
                    sizeof (ufm_ioc_bufsz_t), mode) != 0)
                        return (EFAULT);
        }
        return (0);
}

static int
ufm_do_report(intptr_t data, int mode)
{
        ddi_ufm_handle_t *ufmh;
        uint_t model;
        int ret = 0;
        char *buf;
        size_t sz;
        dev_info_t *dip;
        char devpath[MAXPATHLEN];
        ufm_ioc_report_t ufmr;
#ifdef _MULTI_DATAMODEL
        ufm_ioc_report32_t ufmr32;
#endif

        model = ddi_model_convert_from(mode);

        switch (model) {
#ifdef _MULTI_DATAMODEL
        case DDI_MODEL_ILP32:
                if (ddi_copyin((void *)data, &ufmr32,
                    sizeof (ufm_ioc_report32_t), mode) != 0)
                        return (EFAULT);
                ufmr.ufmr_version = ufmr32.ufmr_version;
                if (strlcpy(ufmr.ufmr_devpath, ufmr32.ufmr_devpath,
                    MAXPATHLEN) >= MAXPATHLEN) {
                        return (EOVERFLOW);
                }
                ufmr.ufmr_bufsz = ufmr32.ufmr_bufsz;
                ufmr.ufmr_buf = (caddr_t)(uintptr_t)ufmr32.ufmr_buf;
                break;
#endif /* _MULTI_DATAMODEL */
        case DDI_MODEL_NONE:
        default:
                if (ddi_copyin((void *)data, &ufmr,
                    sizeof (ufm_ioc_report_t), mode) != 0)
                        return (EFAULT);
        }

        if (strlcpy(devpath, ufmr.ufmr_devpath, MAXPATHLEN) >= MAXPATHLEN)
                return (EOVERFLOW);

        if ((dip = e_ddi_hold_devi_by_path(devpath, 0)) == NULL) {
                        return (ENOTSUP);
        }
        if ((ufmh = ufm_find(devpath)) == NULL) {
                ddi_release_devi(dip);
                return (ENOTSUP);
        }
        ASSERT(MUTEX_HELD(&ufmh->ufmh_lock));

        if (!ufm_driver_ready(ufmh)) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (EAGAIN);
        }

        if (ufmr.ufmr_version != ufmh->ufmh_version) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ENOTSUP);
        }

        /*
         * Note - ufm_cache_fill() also takes care of verifying that the driver
         * supports the DDI_UFM_CAP_REPORT capability and will return non-zero,
         * if not supported.
         */
        if ((ret = ufm_cache_fill(ufmh)) != 0) {
                ddi_release_devi(dip);
                mutex_exit(&ufmh->ufmh_lock);
                return (ret);
        }
        ddi_release_devi(dip);

        if ((ret = nvlist_size(ufmh->ufmh_report, &sz, NV_ENCODE_NATIVE)) !=
            0) {
                mutex_exit(&ufmh->ufmh_lock);
                return (ret);
        }
        if (sz > ufmr.ufmr_bufsz) {
                mutex_exit(&ufmh->ufmh_lock);
                return (EOVERFLOW);
        }

        buf = fnvlist_pack(ufmh->ufmh_report, &sz);
        mutex_exit(&ufmh->ufmh_lock);

        if (ddi_copyout(buf, ufmr.ufmr_buf, sz, mode) != 0) {
                kmem_free(buf, sz);
                return (EFAULT);
        }
        kmem_free(buf, sz);

        switch (model) {
#ifdef _MULTI_DATAMODEL
        case DDI_MODEL_ILP32:
                ufmr32.ufmr_bufsz = sz;
                if (ddi_copyout(&ufmr32, (void *)data,
                    sizeof (ufm_ioc_report32_t), mode) != 0)
                        return (EFAULT);
                break;
#endif /* _MULTI_DATAMODEL */
        case DDI_MODEL_NONE:
        default:
                ufmr.ufmr_bufsz = sz;
                if (ddi_copyout(&ufmr, (void *)data,
                    sizeof (ufm_ioc_report_t), mode) != 0)
                        return (EFAULT);
        }

        return (0);
}

static int
ufm_do_readimg(intptr_t data, int mode)
{
        int ret;
        uint_t model;
        ufm_ioc_readimg_t ufri;
        char devpath[MAXPATHLEN];
        ddi_ufm_handle_t *ufmh;
        dev_info_t *dip;
#ifdef _MULTI_DATAMODEL
        ufm_ioc_readimg32_t ufri32;
#endif

        model = ddi_model_convert_from(mode);
        switch (model) {
#ifdef _MULTI_DATAMODEL
        case DDI_MODEL_ILP32:
                if (ddi_copyin((void *)data, &ufri32, sizeof (ufri32),
                    mode) != 0) {
                        return (EFAULT);
                }
                ufri.ufri_version = ufri32.ufri_version;
                ufri.ufri_imageno = ufri32.ufri_imageno;
                ufri.ufri_slotno = ufri32.ufri_slotno;
                ufri.ufri_offset = ufri32.ufri_offset;
                ufri.ufri_len = ufri32.ufri_len;
                ufri.ufri_nread = ufri32.ufri_nread;

                if (strlcpy(ufri.ufri_devpath, ufri32.ufri_devpath,
                    MAXPATHLEN) >= MAXPATHLEN) {
                        return (EOVERFLOW);
                }
                ufri.ufri_buf = (caddr_t)(uintptr_t)ufri32.ufri_buf;
                break;
#endif /* _MULTI_DATAMODEL */
        case DDI_MODEL_NONE:
        default:
                if (ddi_copyin((void *)data, &ufri, sizeof (ufri), mode) != 0) {
                        return (EFAULT);
                }
        }

        if (strlcpy(devpath, ufri.ufri_devpath, MAXPATHLEN) >= MAXPATHLEN)
                return (EOVERFLOW);

        if ((dip = e_ddi_hold_devi_by_path(devpath, 0)) == NULL) {
                        return (ENOTSUP);
        }
        if ((ufmh = ufm_find(devpath)) == NULL) {
                ddi_release_devi(dip);
                return (ENOTSUP);
        }
        ASSERT(MUTEX_HELD(&ufmh->ufmh_lock));

        if (!ufm_driver_ready(ufmh)) {
                ret = EAGAIN;
                goto out;
        }

        if (ufri.ufri_version != ufmh->ufmh_version) {
                ret = ENOTSUP;
                goto out;
        }

        ret = ufm_read_img(ufmh, ufri.ufri_imageno, ufri.ufri_slotno,
            ufri.ufri_len, ufri.ufri_offset, (uintptr_t)ufri.ufri_buf,
            &ufri.ufri_nread, mode);

out:
        mutex_exit(&ufmh->ufmh_lock);
        ddi_release_devi(dip);

        if (ret == 0) {
                switch (model) {
#ifdef _MULTI_DATAMODEL
                case DDI_MODEL_ILP32:
                        ufri32.ufri_nread = ufri.ufri_nread;
                        if (ddi_copyout(&ufri32, (void *)data, sizeof (ufri32),
                            mode) != 0) {
                                return (EFAULT);
                        }
                        break;
#endif /* _MULTI_DATAMODEL */
                case DDI_MODEL_NONE:
                default:
                        if (ddi_copyout(&ufri, (void *)data, sizeof (ufri),
                            mode) != 0) {
                                return (EFAULT);
                        }
                }
        }

        return (ret);
}

static int
ufm_ioctl(dev_t dev, int cmd, intptr_t data, int mode, cred_t *credp,
    int *rvalp)
{
        int ret = 0;

        if (drv_priv(credp) != 0)
                return (EPERM);

        switch (cmd) {
        case UFM_IOC_GETCAPS:
                ret = ufm_do_getcaps(data, mode);
                break;

        case UFM_IOC_REPORTSZ:
                ret = ufm_do_reportsz(data, mode);
                break;

        case UFM_IOC_REPORT:
                ret = ufm_do_report(data, mode);
                break;

        case UFM_IOC_READIMG:
                ret = ufm_do_readimg(data, mode);
                break;
        default:
                return (ENOTTY);
        }
        return (ret);

}