/* * 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 #include #include #include #include #include #include #include #include #include #include #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); }