root/usr/src/uts/common/io/openprom.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 * Copyright 2019 Peter Tribble.
 */

/*
 * Ported from 4.1.1_PSRA: "@(#)openprom.c 1.19 91/02/19 SMI";
 *
 * Porting notes:
 *
 * OPROMU2P unsupported after SunOS 4.x.
 *
 * Only one of these devices per system is allowed.
 */

/*
 * Openprom eeprom options/devinfo driver.
 */

#include <sys/types.h>
#include <sys/errno.h>
#include <sys/file.h>
#include <sys/cmn_err.h>
#include <sys/kmem.h>
#include <sys/openpromio.h>
#include <sys/conf.h>
#include <sys/stat.h>
#include <sys/modctl.h>
#include <sys/debug.h>
#include <sys/autoconf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/promif.h>
#include <sys/sysmacros.h>      /* offsetof */
#include <sys/nvpair.h>
#include <sys/zone.h>
#include <sys/consplat.h>
#include <sys/bootconf.h>
#include <sys/systm.h>
#include <sys/bootprops.h>

#define MAX_OPENS       32      /* Up to this many simultaneous opens */

#define IOC_IDLE        0       /* snapshot ioctl states */
#define IOC_SNAP        1       /* snapshot in progress */
#define IOC_DONE        2       /* snapshot done, but not copied out */
#define IOC_COPY        3       /* copyout in progress */

/*
 * XXX  Make this dynamic.. or (better still) make the interface stateless
 */
static struct oprom_state {
        pnode_t current_id;     /* node we're fetching props from */
        int16_t already_open;   /* if true, this instance is 'active' */
        int16_t ioc_state;      /* snapshot ioctl state */
        char    *snapshot;      /* snapshot of all prom nodes */
        size_t  size;           /* size of snapshot */
        prom_generation_cookie_t tree_gen;
} oprom_state[MAX_OPENS];

static kmutex_t oprom_lock;     /* serialize instance assignment */

static int opromopen(dev_t *, int, int, cred_t *);
static int opromioctl(dev_t, int, intptr_t, int, cred_t *, int *);
static int opromclose(dev_t, int, int, cred_t *);

static int opinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg,
                void **result);
static int opattach(dev_info_t *, ddi_attach_cmd_t cmd);
static int opdetach(dev_info_t *, ddi_detach_cmd_t cmd);

/* help functions */
static int oprom_checknodeid(pnode_t, pnode_t);
static int oprom_copyinstr(intptr_t, char *, size_t, size_t);
static int oprom_copynode(pnode_t, uint_t, char **, size_t *);
static int oprom_snapshot(struct oprom_state *, intptr_t);
static int oprom_copyout(struct oprom_state *, intptr_t);
static int oprom_setstate(struct oprom_state *, int16_t);

static struct cb_ops openeepr_cb_ops = {
        opromopen,              /* open */
        opromclose,             /* close */
        nodev,                  /* strategy */
        nodev,                  /* print */
        nodev,                  /* dump */
        nodev,                  /* read */
        nodev,                  /* write */
        opromioctl,             /* ioctl */
        nodev,                  /* devmap */
        nodev,                  /* mmap */
        nodev,                  /* segmap */
        nochpoll,               /* poll */
        ddi_prop_op,            /* prop_op */
        NULL,                   /* streamtab  */
        D_NEW | D_MP            /* Driver compatibility flag */
};

static struct dev_ops openeepr_ops = {
        DEVO_REV,               /* devo_rev, */
        0,                      /* refcnt  */
        opinfo,                 /* info */
        nulldev,                /* identify */
        nulldev,                /* probe */
        opattach,               /* attach */
        opdetach,               /* detach */
        nodev,                  /* reset */
        &openeepr_cb_ops,       /* driver operations */
        NULL,                   /* bus operations */
        NULL,                   /* power */
        ddi_quiesce_not_needed,         /* quiesce */
};

/*
 * Module linkage information for the kernel.
 */
static struct modldrv modldrv = {
        &mod_driverops,
        "OPENPROM/NVRAM Driver",
        &openeepr_ops
};

static struct modlinkage modlinkage = {
        MODREV_1,
        &modldrv,
        NULL
};

int
_init(void)
{
        int     error;

        mutex_init(&oprom_lock, NULL, MUTEX_DRIVER, NULL);

        error = mod_install(&modlinkage);
        if (error != 0) {
                mutex_destroy(&oprom_lock);
                return (error);
        }

        return (0);
}

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

int
_fini(void)
{
        int     error;

        error = mod_remove(&modlinkage);
        if (error != 0)
                return (error);

        mutex_destroy(&oprom_lock);
        return (0);
}

static dev_info_t *opdip;
static pnode_t options_nodeid;

/*ARGSUSED*/
static int
opinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
        int error = DDI_FAILURE;

        switch (infocmd) {
        case DDI_INFO_DEVT2DEVINFO:
                *result = (void *)opdip;
                error = DDI_SUCCESS;
                break;
        case DDI_INFO_DEVT2INSTANCE:
                /* All dev_t's map to the same, single instance */
                *result = (void *)0;
                error = DDI_SUCCESS;
                break;
        default:
                break;
        }

        return (error);
}

static int
opattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        switch (cmd) {

        case DDI_ATTACH:
                if (prom_is_openprom()) {
                        options_nodeid = prom_optionsnode();
                } else {
                        options_nodeid = OBP_BADNODE;
                }

                opdip = dip;

                if (ddi_create_minor_node(dip, "openprom", S_IFCHR,
                    0, DDI_PSEUDO, 0) == DDI_FAILURE) {
                        return (DDI_FAILURE);
                }

                return (DDI_SUCCESS);

        default:
                return (DDI_FAILURE);
        }
}

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

        ddi_remove_minor_node(dip, NULL);
        opdip = NULL;

        return (DDI_SUCCESS);
}

/*
 * Allow multiple opens by tweaking the dev_t such that it looks like each
 * open is getting a different minor device.  Each minor gets a separate
 * entry in the oprom_state[] table.
 */
/*ARGSUSED*/
static int
opromopen(dev_t *devp, int flag, int otyp, cred_t *credp)
{
        int m;
        struct oprom_state *st = oprom_state;

        if (getminor(*devp) != 0)
                return (ENXIO);

        mutex_enter(&oprom_lock);
        for (m = 0; m < MAX_OPENS; m++)
                if (st->already_open)
                        st++;
                else {
                        st->already_open = 1;
                        /*
                         * It's ours.
                         */
                        st->current_id = (pnode_t)0;
                        ASSERT(st->snapshot == NULL && st->size == 0);
                        ASSERT(st->ioc_state == IOC_IDLE);
                        break;
                }
        mutex_exit(&oprom_lock);

        if (m == MAX_OPENS)  {
                /*
                 * "Thank you for calling, but all our lines are
                 * busy at the moment.."
                 *
                 * We could get sophisticated here, and go into a
                 * sleep-retry loop .. but hey, I just can't see
                 * that many processes sitting in this driver.
                 *
                 * (And if it does become possible, then we should
                 * change the interface so that the 'state' is held
                 * external to the driver)
                 */
                return (EAGAIN);
        }

        *devp = makedevice(getmajor(*devp), (minor_t)m);

        return (0);
}

/*ARGSUSED*/
static int
opromclose(dev_t dev, int flag, int otype, cred_t *cred_p)
{
        struct oprom_state *st;

        st = &oprom_state[getminor(dev)];
        ASSERT(getminor(dev) < MAX_OPENS && st->already_open != 0);
        if (st->snapshot) {
                kmem_free(st->snapshot, st->size);
                st->snapshot = NULL;
                st->size = 0;
                st->ioc_state = IOC_IDLE;
        }
        mutex_enter(&oprom_lock);
        st->already_open = 0;
        mutex_exit(&oprom_lock);

        return (0);
}

#ifdef __sparc
static int
get_bootpath_prop(char *bootpath)
{
        if (root_is_ramdisk) {
                if (BOP_GETPROP(bootops, "bootarchive", bootpath) == -1)
                        return (-1);
                (void) strlcat(bootpath, ":a", BO_MAXOBJNAME);
        } else {
                if ((BOP_GETPROP(bootops, "bootpath", bootpath) == -1) ||
                    strlen(bootpath) == 0) {
                        if (BOP_GETPROP(bootops,
                            "boot-path", bootpath) == -1)
                                return (-1);
                }
                if (memcmp(bootpath, BP_ISCSI_DISK,
                    strlen(BP_ISCSI_DISK)) == 0) {
                        get_iscsi_bootpath_vhci(bootpath);
                }
        }
        return (0);
}
#endif

struct opromioctl_args {
        struct oprom_state *st;
        int cmd;
        intptr_t arg;
        int mode;
};

/*ARGSUSED*/
static int
opromioctl_cb(void *avp, int has_changed)
{
        struct opromioctl_args *argp = avp;
        int cmd;
        intptr_t arg;
        int mode;
        struct oprom_state *st;
        struct openpromio *opp;
        int valsize;
        char *valbuf;
        int error = 0;
        uint_t userbufsize;
        pnode_t node_id = OBP_NONODE;
        char propname[OBP_MAXPROPNAME];

        st = argp->st;
        cmd = argp->cmd;
        arg = argp->arg;
        mode = argp->mode;

        if (has_changed) {
                /*
                 * The prom tree has changed since we last used current_id,
                 * so we need to check it.
                 */
                if ((st->current_id != OBP_NONODE) &&
                    (st->current_id != OBP_BADNODE)) {
                        if (oprom_checknodeid(st->current_id, OBP_NONODE) == 0)
                                st->current_id = OBP_BADNODE;
                }
        }

        /*
         * Check permissions
         * and weed out unsupported commands on x86 platform
         */
        switch (cmd) {
#if !defined(__x86)
        case OPROMLISTKEYSLEN:
                valsize = prom_asr_list_keys_len();
                opp = (struct openpromio *)kmem_zalloc(
                    sizeof (uint_t) + 1, KM_SLEEP);
                opp->oprom_size = valsize;
                if (copyout(opp, (void *)arg, (sizeof (uint_t))) != 0)
                        error = EFAULT;
                kmem_free(opp, sizeof (uint_t) + 1);
                break;
        case OPROMLISTKEYS:
                valsize = prom_asr_list_keys_len();
                if (copyin((void *)arg, &userbufsize, sizeof (uint_t)) != 0)
                        return (EFAULT);
                if (valsize > userbufsize)
                        return (EINVAL);
                valbuf = (char *)kmem_zalloc(valsize + 1, KM_SLEEP);
                if (prom_asr_list_keys((caddr_t)valbuf) == -1) {
                        kmem_free(valbuf, valsize + 1);
                        return (EFAULT);
                }
                opp = (struct openpromio *)kmem_zalloc(
                    valsize + sizeof (uint_t) + 1, KM_SLEEP);
                opp->oprom_size = valsize;
                bcopy(valbuf, opp->oprom_array, valsize);
                if (copyout(opp, (void *)arg, (valsize + sizeof (uint_t))) != 0)
                        error = EFAULT;
                kmem_free(valbuf, valsize + 1);
                kmem_free(opp, valsize + sizeof (uint_t) + 1);
                break;
        case OPROMEXPORT:
                valsize = prom_asr_export_len();
                if (copyin((void *)arg, &userbufsize, sizeof (uint_t)) != 0)
                        return (EFAULT);
                if (valsize > userbufsize)
                        return (EINVAL);
                valbuf = (char *)kmem_zalloc(valsize + 1, KM_SLEEP);
                if (prom_asr_export((caddr_t)valbuf) == -1) {
                        kmem_free(valbuf, valsize + 1);
                        return (EFAULT);
                }
                opp = (struct openpromio *)kmem_zalloc(
                    valsize + sizeof (uint_t) + 1, KM_SLEEP);
                opp->oprom_size = valsize;
                bcopy(valbuf, opp->oprom_array, valsize);
                if (copyout(opp, (void *)arg, (valsize + sizeof (uint_t))) != 0)
                        error = EFAULT;
                kmem_free(valbuf, valsize + 1);
                kmem_free(opp, valsize + sizeof (uint_t) + 1);
                break;
        case OPROMEXPORTLEN:
                valsize = prom_asr_export_len();
                opp = (struct openpromio *)kmem_zalloc(
                    sizeof (uint_t) + 1, KM_SLEEP);
                opp->oprom_size = valsize;
                if (copyout(opp, (void *)arg, (sizeof (uint_t))) != 0)
                        error = EFAULT;
                kmem_free(opp, sizeof (uint_t) + 1);
                break;
#endif
        case OPROMGETOPT:
        case OPROMNXTOPT:
                if ((mode & FREAD) == 0) {
                        return (EPERM);
                }
                node_id = options_nodeid;
                break;

        case OPROMSETOPT:
        case OPROMSETOPT2:
#if !defined(__x86)
                if (mode & FWRITE) {
                        node_id = options_nodeid;
                        break;
                }
#endif /* !__x86 */
                return (EPERM);

        case OPROMNEXT:
        case OPROMCHILD:
        case OPROMGETPROP:
        case OPROMGETPROPLEN:
        case OPROMNXTPROP:
        case OPROMSETNODEID:
                if ((mode & FREAD) == 0) {
                        return (EPERM);
                }
                node_id = st->current_id;
                break;
        case OPROMCOPYOUT:
                if (st->snapshot == NULL)
                        return (EINVAL);
                /*FALLTHROUGH*/
        case OPROMSNAPSHOT:
        case OPROMGETCONS:
        case OPROMGETBOOTARGS:
        case OPROMGETBOOTPATH:
        case OPROMGETVERSION:
        case OPROMPATH2DRV:
        case OPROMPROM2DEVNAME:
#if !defined(__x86)
        case OPROMGETFBNAME:
        case OPROMDEV2PROMNAME:
#endif  /* !__x86 */
                if ((mode & FREAD) == 0) {
                        return (EPERM);
                }
                break;

        default:
                return (EINVAL);
        }

        /*
         * Deal with SNAPSHOT and COPYOUT ioctls first
         */
        switch (cmd) {
        case OPROMCOPYOUT:
                return (oprom_copyout(st, arg));

        case OPROMSNAPSHOT:
                return (oprom_snapshot(st, arg));
        }

        /*
         * Copy in user argument length and allocation memory
         *
         * NB do not copyin the entire buffer we may not need
         *      to. userbufsize can be as big as 32 K.
         */
        if (copyin((void *)arg, &userbufsize, sizeof (uint_t)) != 0)
                return (EFAULT);

        if (userbufsize == 0 || userbufsize > OPROMMAXPARAM)
                return (EINVAL);

        opp = (struct openpromio *)kmem_zalloc(
            userbufsize + sizeof (uint_t) + 1, KM_SLEEP);

        /*
         * Execute command
         */
        switch (cmd) {

        case OPROMGETOPT:
        case OPROMGETPROP:
        case OPROMGETPROPLEN:

                if ((prom_is_openprom() == 0) ||
                    (node_id == OBP_NONODE) || (node_id == OBP_BADNODE)) {
                        error = EINVAL;
                        break;
                }

                /*
                 * The argument, a NULL terminated string, is a prop name.
                 */
                if ((error = oprom_copyinstr(arg, opp->oprom_array,
                    (size_t)userbufsize, OBP_MAXPROPNAME)) != 0) {
                        break;
                }
                (void) strcpy(propname, opp->oprom_array);
                valsize = prom_getproplen(node_id, propname);

                /*
                 * 4010173: 'name' is a property, but not an option.
                 */
                if ((cmd == OPROMGETOPT) && (strcmp("name", propname) == 0))
                        valsize = -1;

                if (cmd == OPROMGETPROPLEN)  {
                        int proplen = valsize;

                        if (userbufsize < sizeof (int)) {
                                error = EINVAL;
                                break;
                        }
                        opp->oprom_size = valsize = sizeof (int);
                        bcopy(&proplen, opp->oprom_array, valsize);
                } else if (valsize > 0 && valsize <= userbufsize) {
                        bzero(opp->oprom_array, valsize + 1);
                        (void) prom_getprop(node_id, propname,
                            opp->oprom_array);
                        opp->oprom_size = valsize;
                        if (valsize < userbufsize)
                                ++valsize;      /* Forces NULL termination */
                                                /* If space permits */
                } else {
                        /*
                         * XXX: There is no error code if the buf is too small.
                         * which is consistent with the current behavior.
                         *
                         * NB: This clause also handles the non-error
                         * zero length (boolean) property value case.
                         */
                        opp->oprom_size = 0;
                        (void) strcpy(opp->oprom_array, "");
                        valsize = 1;
                }
                if (copyout(opp, (void *)arg, (valsize + sizeof (uint_t))) != 0)
                        error = EFAULT;
                break;

        case OPROMNXTOPT:
        case OPROMNXTPROP:
                if ((prom_is_openprom() == 0) ||
                    (node_id == OBP_NONODE) || (node_id == OBP_BADNODE)) {
                        error = EINVAL;
                        break;
                }

                /*
                 * The argument, a NULL terminated string, is a prop name.
                 */
                if ((error = oprom_copyinstr(arg, opp->oprom_array,
                    (size_t)userbufsize, OBP_MAXPROPNAME)) != 0) {
                        break;
                }
                valbuf = (char *)prom_nextprop(node_id, opp->oprom_array,
                    propname);
                valsize = strlen(valbuf);

                /*
                 * 4010173: 'name' is a property, but it's not an option.
                 */
                if ((cmd == OPROMNXTOPT) && valsize &&
                    (strcmp(valbuf, "name") == 0)) {
                        valbuf = (char *)prom_nextprop(node_id, "name",
                            propname);
                        valsize = strlen(valbuf);
                }

                if (valsize == 0) {
                        opp->oprom_size = 0;
                } else if (++valsize <= userbufsize) {
                        opp->oprom_size = valsize;
                        bzero((caddr_t)opp->oprom_array, (size_t)valsize);
                        bcopy((caddr_t)valbuf, (caddr_t)opp->oprom_array,
                            (size_t)valsize);
                }

                if (copyout(opp, (void *)arg, valsize + sizeof (uint_t)) != 0)
                        error = EFAULT;
                break;

        case OPROMNEXT:
        case OPROMCHILD:
        case OPROMSETNODEID:

                if (prom_is_openprom() == 0 ||
                    userbufsize < sizeof (pnode_t)) {
                        error = EINVAL;
                        break;
                }

                /*
                 * The argument is a phandle. (aka pnode_t)
                 */
                if (copyin(((caddr_t)arg + sizeof (uint_t)),
                    opp->oprom_array, sizeof (pnode_t)) != 0) {
                        error = EFAULT;
                        break;
                }

                /*
                 * If pnode_t from userland is garbage, we
                 * could confuse the PROM.
                 */
                node_id = *(pnode_t *)opp->oprom_array;
                if (oprom_checknodeid(node_id, st->current_id) == 0) {
                        cmn_err(CE_NOTE, "!nodeid 0x%x not found",
                            (int)node_id);
                        error = EINVAL;
                        break;
                }

                if (cmd == OPROMNEXT)
                        st->current_id = prom_nextnode(node_id);
                else if (cmd == OPROMCHILD)
                        st->current_id = prom_childnode(node_id);
                else {
                        /* OPROMSETNODEID */
                        st->current_id = node_id;
                        break;
                }

                opp->oprom_size = sizeof (pnode_t);
                *(pnode_t *)opp->oprom_array = st->current_id;

                if (copyout(opp, (void *)arg,
                    sizeof (pnode_t) + sizeof (uint_t)) != 0)
                        error = EFAULT;
                break;

        case OPROMGETCONS:
                /*
                 * Is openboot supported on this machine?
                 * This ioctl used to return the console device,
                 * information; this is now done via modctl()
                 * in libdevinfo.
                 */
                opp->oprom_size = sizeof (char);

                opp->oprom_array[0] |= prom_is_openprom() ?
                    OPROMCONS_OPENPROM : 0;

                /*
                 * The rest of the info is needed by Install to
                 * decide if graphics should be started.
                 */
                if ((getzoneid() == GLOBAL_ZONEID) &&
                    plat_stdin_is_keyboard()) {
                        opp->oprom_array[0] |= OPROMCONS_STDIN_IS_KBD;
                }

                if ((getzoneid() == GLOBAL_ZONEID) &&
                    plat_stdout_is_framebuffer()) {
                        opp->oprom_array[0] |= OPROMCONS_STDOUT_IS_FB;
                }

                if (copyout(opp, (void *)arg,
                    sizeof (char) + sizeof (uint_t)) != 0)
                        error = EFAULT;
                break;

        case OPROMGETBOOTARGS: {
                extern char kern_bootargs[];

                valsize = strlen(kern_bootargs) + 1;
                if (valsize > userbufsize) {
                        error = EINVAL;
                        break;
                }
                (void) strcpy(opp->oprom_array, kern_bootargs);
                opp->oprom_size = valsize - 1;

                if (copyout(opp, (void *)arg, valsize + sizeof (uint_t)) != 0)
                        error = EFAULT;
                break;
        }

        case OPROMGETBOOTPATH: {
#if defined(__sparc) && defined(_OBP)

                char bpath[OBP_MAXPATHLEN];
                if (get_bootpath_prop(bpath) != 0) {
                        error = EINVAL;
                        break;
                }
                valsize = strlen(bpath) + 1;
                if (valsize > userbufsize) {
                        error = EINVAL;
                        break;
                }
                (void) strcpy(opp->oprom_array, bpath);

#elif defined(__x86)

                extern char saved_cmdline[];
                valsize = strlen(saved_cmdline) + 1;
                if (valsize > userbufsize) {
                        error = EINVAL;
                        break;
                }
                (void) strcpy(opp->oprom_array, saved_cmdline);
#endif
                opp->oprom_size = valsize - 1;
                if (copyout(opp, (void *)arg, valsize + sizeof (uint_t)) != 0)
                        error = EFAULT;
                break;
        }

        /*
         * convert a prom device path to an equivalent devfs path
         */
        case OPROMPROM2DEVNAME: {
                char *dev_name;

                /*
                 * The input argument, a pathname, is a NULL terminated string.
                 */
                if ((error = oprom_copyinstr(arg, opp->oprom_array,
                    (size_t)userbufsize, MAXPATHLEN)) != 0) {
                        break;
                }

                dev_name = kmem_alloc(MAXPATHLEN, KM_SLEEP);

                error = i_promname_to_devname(opp->oprom_array, dev_name);
                if (error != 0) {
                        kmem_free(dev_name, MAXPATHLEN);
                        break;
                }
                valsize = opp->oprom_size = strlen(dev_name);
                if (++valsize > userbufsize) {
                        kmem_free(dev_name, MAXPATHLEN);
                        error = EINVAL;
                        break;
                }
                (void) strcpy(opp->oprom_array, dev_name);
                if (copyout(opp, (void *)arg, sizeof (uint_t) + valsize) != 0)
                        error = EFAULT;

                kmem_free(dev_name, MAXPATHLEN);
                break;
        }

        /*
         * Convert a prom device path name to a driver name
         */
        case OPROMPATH2DRV: {
                char *drv_name;
                major_t maj;

                /*
                 * The input argument, a pathname, is a NULL terminated string.
                 */
                if ((error = oprom_copyinstr(arg, opp->oprom_array,
                    (size_t)userbufsize, MAXPATHLEN)) != 0) {
                        break;
                }

                /*
                 * convert path to a driver binding name
                 */
                maj = path_to_major((char *)opp->oprom_array);
                if (maj == DDI_MAJOR_T_NONE) {
                        error = EINVAL;
                        break;
                }

                /*
                 * resolve any aliases
                 */
                if ((drv_name = ddi_major_to_name(maj)) == NULL) {
                        error = EINVAL;
                        break;
                }

                (void) strcpy(opp->oprom_array, drv_name);
                opp->oprom_size = strlen(drv_name);
                if (copyout(opp, (void *)arg,
                    sizeof (uint_t) + opp->oprom_size + 1) != 0)
                        error = EFAULT;
                break;
        }

        case OPROMGETVERSION:
                /*
                 * Get a string representing the running version of the
                 * prom. How to create such a string is platform dependent,
                 * so we just defer to a promif function. If no such
                 * association exists, the promif implementation
                 * may copy the string "unknown" into the given buffer,
                 * and return its length (incl. NULL terminator).
                 *
                 * We expect prom_version_name to return the actual
                 * length of the string, but copy at most userbufsize
                 * bytes into the given buffer, including NULL termination.
                 */

                valsize = prom_version_name(opp->oprom_array, userbufsize);
                if (valsize < 0) {
                        error = EINVAL;
                        break;
                }

                /*
                 * copyout only the part of the user buffer we need to.
                 */
                if (copyout(opp, (void *)arg,
                    (size_t)(min((uint_t)valsize, userbufsize) +
                    sizeof (uint_t))) != 0)
                        error = EFAULT;
                break;

#if !defined(__x86)
        case OPROMGETFBNAME:
                /*
                 * Return stdoutpath, if it's a frame buffer.
                 * Yes, we are comparing a possibly longer string against
                 * the size we're really going to copy, but so what?
                 */
                if ((getzoneid() == GLOBAL_ZONEID) &&
                    (prom_stdout_is_framebuffer() != 0) &&
                    (userbufsize > strlen(prom_stdoutpath()))) {
                        prom_strip_options(prom_stdoutpath(),
                            opp->oprom_array);  /* strip options and copy */
                        valsize = opp->oprom_size = strlen(opp->oprom_array);
                        if (copyout(opp, (void *)arg,
                            valsize + 1 + sizeof (uint_t)) != 0)
                                error = EFAULT;
                } else
                        error = EINVAL;
                break;

        /*
         * Convert a logical or physical device path to prom device path
         */
        case OPROMDEV2PROMNAME: {
                char *prom_name;

                /*
                 * The input argument, a pathname, is a NULL terminated string.
                 */
                if ((error = oprom_copyinstr(arg, opp->oprom_array,
                    (size_t)userbufsize, MAXPATHLEN)) != 0) {
                        break;
                }

                prom_name = kmem_alloc(userbufsize, KM_SLEEP);

                /*
                 * convert the devfs path to an equivalent prom path
                 */
                error = i_devname_to_promname(opp->oprom_array, prom_name,
                    userbufsize);

                if (error != 0) {
                        kmem_free(prom_name, userbufsize);
                        break;
                }

                for (valsize = 0; valsize < userbufsize; valsize++) {
                        opp->oprom_array[valsize] = prom_name[valsize];

                        if ((valsize > 0) && (prom_name[valsize] == '\0') &&
                            (prom_name[valsize-1] == '\0')) {
                                break;
                        }
                }
                opp->oprom_size = valsize;

                kmem_free(prom_name, userbufsize);
                if (copyout(opp, (void *)arg, sizeof (uint_t) + valsize) != 0)
                        error = EFAULT;

                break;
        }

        case OPROMSETOPT:
        case OPROMSETOPT2: {
                int namebuflen;
                int valbuflen;

                if ((prom_is_openprom() == 0) ||
                    (node_id == OBP_NONODE) || (node_id == OBP_BADNODE)) {
                        error = EINVAL;
                        break;
                }

                /*
                 * The arguments are a property name and a value.
                 * Copy in the entire user buffer.
                 */
                if (copyin(((caddr_t)arg + sizeof (uint_t)),
                    opp->oprom_array, userbufsize) != 0) {
                        error = EFAULT;
                        break;
                }

                /*
                 * The property name is the first string, value second
                 */
                namebuflen = strlen(opp->oprom_array);
                valbuf = opp->oprom_array + namebuflen + 1;
                valbuflen = strlen(valbuf);

                if (cmd == OPROMSETOPT) {
                        valsize = valbuflen + 1;  /* +1 for the '\0' */
                } else {
                        if ((namebuflen + 1 + valbuflen + 1) > userbufsize) {
                                error = EINVAL;
                                break;
                        }
                        valsize = (opp->oprom_array + userbufsize) - valbuf;
                }

                /*
                 * 4010173: 'name' is not an option, but it is a property.
                 */
                if (strcmp(opp->oprom_array, "name") == 0)
                        error = EINVAL;
                else if (prom_setprop(node_id, opp->oprom_array,
                    valbuf, valsize) < 0)
                        error = EINVAL;

                break;
        }
#endif  /* !__x86 */
        }       /* switch (cmd) */

        kmem_free(opp, userbufsize + sizeof (uint_t) + 1);
        return (error);
}

/*ARGSUSED*/
static int
opromioctl(dev_t dev, int cmd, intptr_t arg, int mode,
    cred_t *credp, int *rvalp)
{
        struct oprom_state *st;
        struct opromioctl_args arg_block;

        if (getminor(dev) >= MAX_OPENS)
                return (ENXIO);

        st = &oprom_state[getminor(dev)];
        ASSERT(st->already_open);
        arg_block.st = st;
        arg_block.cmd = cmd;
        arg_block.arg = arg;
        arg_block.mode = mode;
        return (prom_tree_access(opromioctl_cb, &arg_block, &st->tree_gen));
}

/*
 * Copyin string and verify the actual string length is less than maxsize
 * specified by the caller.
 *
 * Currently, maxsize is either OBP_MAXPROPNAME for property names
 * or MAXPATHLEN for device path names. userbufsize is specified
 * by the userland caller.
 */
static int
oprom_copyinstr(intptr_t arg, char *buf, size_t bufsize, size_t maxsize)
{
        int error;
        size_t actual_len;

        if ((error = copyinstr(((caddr_t)arg + sizeof (uint_t)),
            buf, bufsize, &actual_len)) != 0) {
                return (error);
        }
        if ((actual_len == 0) || (actual_len > maxsize)) {
                return (EINVAL);
        }

        return (0);
}

/*
 * Check pnode_t passed in from userland
 */
static int
oprom_checknodeid(pnode_t node_id, pnode_t current_id)
{
        int depth;
        pnode_t id[OBP_STACKDEPTH];

        /*
         * optimized path
         */
        if (node_id == 0) {
                return (1);
        }
        if (node_id == OBP_BADNODE) {
                return (0);
        }
        if ((current_id != OBP_BADNODE) && ((node_id == current_id) ||
            (node_id == prom_nextnode(current_id)) ||
            (node_id == prom_childnode(current_id)))) {
                return (1);
        }

        /*
         * long path: walk from root till we find node_id
         */
        depth = 1;
        id[0] = prom_nextnode((pnode_t)0);

        while (depth) {
                if (id[depth - 1] == node_id)
                        return (1);     /* node_id found */

                if (id[depth] = prom_childnode(id[depth - 1])) {
                        depth++;
                        continue;
                }

                while (depth &&
                    ((id[depth - 1] = prom_nextnode(id[depth - 1])) == 0))
                        depth--;
        }
        return (0);     /* node_id not found */
}

static int
oprom_copytree(struct oprom_state *st, uint_t flag)
{
        ASSERT(st->snapshot == NULL && st->size == 0);
        return (oprom_copynode(
            prom_nextnode(0), flag, &st->snapshot, &st->size));
}

static int
oprom_snapshot(struct oprom_state *st, intptr_t arg)
{
        uint_t flag;

        if (oprom_setstate(st, IOC_SNAP) == -1)
                return (EBUSY);

        /* copyin flag and create snapshot */
        if ((copyin((void *)arg, &flag, sizeof (uint_t)) != 0) ||
            (oprom_copytree(st, flag) != 0)) {
                (void) oprom_setstate(st, IOC_IDLE);
                return (EFAULT);
        }


        /* copyout the size of the snapshot */
        flag = (uint_t)st->size;
        if (copyout(&flag, (void *)arg, sizeof (uint_t)) != 0) {
                kmem_free(st->snapshot, st->size);
                st->snapshot = NULL;
                st->size = 0;
                (void) oprom_setstate(st, IOC_IDLE);
                return (EFAULT);
        }

        (void) oprom_setstate(st, IOC_DONE);
        return (0);
}

static int
oprom_copyout(struct oprom_state *st, intptr_t arg)
{
        int error = 0;
        uint_t size;

        if (oprom_setstate(st, IOC_COPY) == -1)
                return (EBUSY);

        /* copyin size and copyout snapshot */
        if (copyin((void *)arg, &size, sizeof (uint_t)) != 0)
                error = EFAULT;
        else if (size < st->size)
                error = EINVAL;
        else if (copyout(st->snapshot, (void *)arg, st->size) != 0)
                error = EFAULT;

        if (error) {
                /*
                 * on error keep the snapshot until a successful
                 * copyout or when the driver is closed.
                 */
                (void) oprom_setstate(st, IOC_DONE);
                return (error);
        }

        kmem_free(st->snapshot, st->size);
        st->snapshot = NULL;
        st->size = 0;
        (void) oprom_setstate(st, IOC_IDLE);
        return (0);
}

/*
 * Copy all properties of nodeid into a single packed nvlist
 */
static int
oprom_copyprop(pnode_t nodeid, uint_t flag, nvlist_t *nvl)
{
        int proplen;
        char *propname, *propval, *buf1, *buf2;

        ASSERT(nvl != NULL);

        /*
         * non verbose mode, get the "name" property only
         */
        if (flag == 0) {
                proplen = prom_getproplen(nodeid, "name");
                if (proplen <= 0) {
                        cmn_err(CE_WARN,
                            "failed to get the name of openprom node 0x%x",
                            nodeid);
                        (void) nvlist_add_string(nvl, "name", "");
                        return (0);
                }
                propval = kmem_zalloc(proplen + 1, KM_SLEEP);
                (void) prom_getprop(nodeid, "name", propval);
                (void) nvlist_add_string(nvl, "name", propval);
                kmem_free(propval, proplen + 1);
                return (0);
        }

        /*
         * Ask for first property by passing a NULL string
         */
        buf1 = kmem_alloc(OBP_MAXPROPNAME, KM_SLEEP);
        buf2 = kmem_zalloc(OBP_MAXPROPNAME, KM_SLEEP);
        buf1[0] = '\0';
        while (propname = (char *)prom_nextprop(nodeid, buf1, buf2)) {
                if (strlen(propname) == 0)
                        break;          /* end of prop list */
                (void) strcpy(buf1, propname);

                proplen = prom_getproplen(nodeid, propname);
                if (proplen == 0) {
                        /* boolean property */
                        (void) nvlist_add_boolean(nvl, propname);
                        continue;
                }
                /* add 1 for null termination in case of a string */
                propval = kmem_zalloc(proplen + 1, KM_SLEEP);
                (void) prom_getprop(nodeid, propname, propval);
                (void) nvlist_add_byte_array(nvl, propname,
                    (uchar_t *)propval, proplen + 1);
                kmem_free(propval, proplen + 1);
                bzero(buf2, OBP_MAXPROPNAME);
        }

        kmem_free(buf1, OBP_MAXPROPNAME);
        kmem_free(buf2, OBP_MAXPROPNAME);

        return (0);
}

/*
 * Copy all children and descendents into a a packed nvlist
 */
static int
oprom_copychild(pnode_t nodeid, uint_t flag, char **buf, size_t *size)
{
        nvlist_t *nvl;
        pnode_t child = prom_childnode(nodeid);

        if (child == 0)
                return (0);

        (void) nvlist_alloc(&nvl, 0, KM_SLEEP);
        while (child != 0) {
                char *nodebuf = NULL;
                size_t nodesize = 0;
                if (oprom_copynode(child, flag, &nodebuf, &nodesize)) {
                        nvlist_free(nvl);
                        cmn_err(CE_WARN, "failed to copy nodeid 0x%x", child);
                        return (-1);
                }
                (void) nvlist_add_byte_array(nvl, "node",
                    (uchar_t *)nodebuf, nodesize);
                kmem_free(nodebuf, nodesize);
                child = prom_nextnode(child);
        }

        (void) nvlist_pack(nvl, buf, size, NV_ENCODE_NATIVE, KM_SLEEP);
        nvlist_free(nvl);
        return (0);
}

/*
 * Copy a node into a packed nvlist
 */
static int
oprom_copynode(pnode_t nodeid, uint_t flag, char **buf, size_t *size)
{
        int error = 0;
        nvlist_t *nvl;
        char *childlist = NULL;
        size_t childsize = 0;

        (void) nvlist_alloc(&nvl, NV_UNIQUE_NAME, KM_SLEEP);
        ASSERT(nvl != NULL);

        /* @nodeid -- @ is not a legal char in a 1275 property name */
        (void) nvlist_add_int32(nvl, "@nodeid", (int32_t)nodeid);

        /* properties */
        if (error = oprom_copyprop(nodeid, flag, nvl))
                goto fail;

        /* children */
        error = oprom_copychild(nodeid, flag, &childlist, &childsize);
        if (error != 0)
                goto fail;
        if (childlist != NULL) {
                (void) nvlist_add_byte_array(nvl, "@child",
                    (uchar_t *)childlist, (uint_t)childsize);
                kmem_free(childlist, childsize);
        }

        /* pack into contiguous buffer */
        error = nvlist_pack(nvl, buf, size, NV_ENCODE_NATIVE, KM_SLEEP);

fail:
        nvlist_free(nvl);
        return (error);
}

/*
 * The driver is stateful across OPROMSNAPSHOT and OPROMCOPYOUT.
 * This function encapsulates the state machine:
 *
 *      -> IOC_IDLE -> IOC_SNAP -> IOC_DONE -> IOC_COPY ->
 *      |               SNAPSHOT                COPYOUT  |
 *      --------------------------------------------------
 *
 * Returns 0 on success and -1 on failure
 */
static int
oprom_setstate(struct oprom_state *st, int16_t new_state)
{
        int ret = 0;

        mutex_enter(&oprom_lock);
        switch (new_state) {
        case IOC_IDLE:
        case IOC_DONE:
                break;
        case IOC_SNAP:
                if (st->ioc_state != IOC_IDLE)
                        ret = -1;
                break;
        case IOC_COPY:
                if (st->ioc_state != IOC_DONE)
                        ret = -1;
                break;
        default:
                ret = -1;
        }

        if (ret == 0)
                st->ioc_state = new_state;
        else
                cmn_err(CE_NOTE, "incorrect state transition from %d to %d",
                    st->ioc_state, new_state);
        mutex_exit(&oprom_lock);
        return (ret);
}