root/usr/src/uts/common/io/smbios.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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


/*
 * smbios(4D) driver
 *
 * This pseudo-driver makes available a snapshot of the system's SMBIOS image
 * that can be accessed using libsmbios.  Clients may access a snapshot using
 * either read(2) or mmap(2).  The driver returns the SMBIOS entry point data
 * followed by the SMBIOS structure table.  The entry point has its 'staddr'
 * field set to indicate the byte offset of the structure table.  The driver
 * uses the common SMBIOS API defined in <sys/smbios.h> to access the image.
 *
 * At present, the kernel takes a single snapshot of SMBIOS at boot time and
 * stores a handle for this snapshot in 'ksmbios'.  To keep track of driver
 * opens, we simply compare-and-swap this handle into an 'smb_clones' array.
 * Future x86 systems may need to support dynamic SMBIOS updates: when that
 * happens the SMBIOS API can be extended to support reference counting and
 * handles for different snapshots can be stored in smb_clones[].
 */

#include <sys/smbios.h>
#include <sys/sysmacros.h>
#include <sys/cmn_err.h>
#include <sys/vmsystm.h>
#include <vm/seg_vn.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/modctl.h>
#include <sys/conf.h>
#include <sys/stat.h>

typedef struct smb_clone {
        smbios_hdl_t *c_hdl;
        size_t c_eplen;
        size_t c_stlen;
} smb_clone_t;

static dev_info_t *smb_devi;
static smb_clone_t *smb_clones;
static int smb_nclones;

/*ARGSUSED*/
static int
smb_open(dev_t *dp, int flag, int otyp, cred_t *cred)
{
        minor_t c;

        if (ksmbios == NULL)
                return (ENXIO);

        /*
         * Locate and reserve a clone structure.  We skip clone 0 as that is
         * the real minor number, and we assign a new minor to each clone.
         */
        for (c = 1; c < smb_nclones; c++) {
                if (atomic_cas_ptr(&smb_clones[c].c_hdl, NULL, ksmbios) == NULL)
                        break;
        }

        if (c >= smb_nclones)
                return (EAGAIN);

        smb_clones[c].c_eplen = P2ROUNDUP(sizeof (smbios_entry_t), 16);
        smb_clones[c].c_stlen = smbios_buflen(smb_clones[c].c_hdl);

        *dp = makedevice(getemajor(*dp), c);

        (void) ddi_prop_update_int(*dp, smb_devi, "size",
            smb_clones[c].c_eplen + smb_clones[c].c_stlen);

        return (0);
}

/*ARGSUSED*/
static int
smb_close(dev_t dev, int flag, int otyp, cred_t *cred)
{
        (void) ddi_prop_remove(dev, smb_devi, "size");
        smb_clones[getminor(dev)].c_hdl = NULL;
        return (0);
}

/*
 * Common code to copy out the SMBIOS snapshot used for both read and mmap.
 * The caller must validate uio_offset for us since semantics differ there.
 * The copy is done in two stages, either of which can be skipped based on the
 * offset and length: first we copy the entry point, with 'staddr' recalculated
 * to indicate the offset of the data buffer, and second we copy the table.
 */
static int
smb_uiomove(smb_clone_t *cp, uio_t *uio)
{
        off_t off = uio->uio_offset;
        size_t len = uio->uio_resid;
        int err = 0;

        if (off + len > cp->c_eplen + cp->c_stlen)
                len = cp->c_eplen + cp->c_stlen - off;

        if (off < cp->c_eplen) {
                smbios_entry_t *ep = kmem_zalloc(cp->c_eplen, KM_SLEEP);
                size_t eprlen = MIN(len, cp->c_eplen - off);

                switch (smbios_info_smbios(cp->c_hdl, ep)) {
                case SMBIOS_ENTRY_POINT_21:
                        ep->ep21.smbe_staddr = (uint32_t)cp->c_eplen;
                        break;
                case SMBIOS_ENTRY_POINT_30:
                        ep->ep30.smbe_staddr = (uint64_t)cp->c_eplen;
                        break;
                }
                smbios_checksum(cp->c_hdl, ep);

                err = uiomove((char *)ep + off, eprlen, UIO_READ, uio);
                kmem_free(ep, cp->c_eplen);

                off += eprlen;
                len -= eprlen;
        }

        if (err == 0 && off >= cp->c_eplen) {
                char *buf = (char *)smbios_buf(cp->c_hdl);
                size_t bufoff = off - cp->c_eplen;

                err = uiomove(buf + bufoff,
                    MIN(len, cp->c_stlen - bufoff), UIO_READ, uio);
        }

        return (err);
}

/*ARGSUSED*/
static int
smb_read(dev_t dev, uio_t *uio, cred_t *cred)
{
        smb_clone_t *cp = &smb_clones[getminor(dev)];

        if (uio->uio_offset < 0 ||
            uio->uio_offset >= cp->c_eplen + cp->c_stlen)
                return (0);

        return (smb_uiomove(cp, uio));
}

/*ARGSUSED*/
static int
smb_segmap(dev_t dev, off_t off, struct as *as, caddr_t *addrp, off_t len,
    uint_t prot, uint_t maxprot, uint_t flags, cred_t *cred)
{
        smb_clone_t *cp = &smb_clones[getminor(dev)];

        size_t alen = P2ROUNDUP(len, PAGESIZE);
        caddr_t addr = NULL;

        iovec_t iov;
        uio_t uio;
        int err;

        if (len <= 0 || (flags & MAP_FIXED))
                return (EINVAL);

        if ((prot & PROT_WRITE) && (flags & MAP_SHARED))
                return (EACCES);

        if (off < 0 || sum_overflows_off(off, len) ||
            off + len > cp->c_eplen + cp->c_stlen) {
                return (ENXIO);
        }

        as_rangelock(as);
        map_addr(&addr, alen, 0, 1, 0);

        if (addr != NULL)
                err = as_map(as, addr, alen, segvn_create, zfod_argsp);
        else
                err = ENOMEM;

        as_rangeunlock(as);
        *addrp = addr;

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

        iov.iov_base = addr;
        iov.iov_len = len;

        bzero(&uio, sizeof (uio_t));
        uio.uio_iov = &iov;
        uio.uio_iovcnt = 1;
        uio.uio_offset = off;
        uio.uio_segflg = UIO_USERSPACE;
        uio.uio_extflg = UIO_COPY_DEFAULT;
        uio.uio_resid = len;

        if ((err = smb_uiomove(cp, &uio)) != 0)
                (void) as_unmap(as, addr, alen);

        return (err);
}

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

static int
smb_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
        if (cmd != DDI_ATTACH)
                return (DDI_FAILURE);

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

        smb_devi = devi;
        return (DDI_SUCCESS);
}

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

        ddi_remove_minor_node(devi, NULL);
        return (DDI_SUCCESS);
}

static struct cb_ops smb_cb_ops = {
        smb_open,               /* open */
        smb_close,              /* close */
        nodev,                  /* strategy */
        nodev,                  /* print */
        nodev,                  /* dump */
        smb_read,               /* read */
        nodev,                  /* write */
        nodev,                  /* ioctl */
        nodev,                  /* devmap */
        nodev,                  /* mmap */
        smb_segmap,             /* segmap */
        nochpoll,               /* poll */
        ddi_prop_op,            /* prop_op */
        NULL,                   /* streamtab */
        D_NEW | D_MP            /* flags */
};

static struct dev_ops smb_ops = {
        DEVO_REV,               /* rev */
        0,                      /* refcnt */
        smb_info,               /* info */
        nulldev,                /* identify */
        nulldev,                /* probe */
        smb_attach,             /* attach */
        smb_detach,             /* detach */
        nodev,                  /* reset */
        &smb_cb_ops,            /* cb ops */
        NULL,                   /* bus ops */
        NULL,                   /* power */
        ddi_quiesce_not_needed,         /* quiesce */
};

static struct modldrv modldrv = {
        &mod_driverops, "System Management BIOS driver", &smb_ops,
};

static struct modlinkage modlinkage = {
        MODREV_1, { (void *)&modldrv }
};

int
_init(void)
{
        int err;

        if (smb_nclones <= 0)
                smb_nclones = maxusers;

        smb_clones = kmem_zalloc(sizeof (smb_clone_t) * smb_nclones, KM_SLEEP);

        if ((err = mod_install(&modlinkage)) != 0)
                kmem_free(smb_clones, sizeof (smb_clone_t) * smb_nclones);

        return (err);
}

int
_fini(void)
{
        int err;

        if ((err = mod_remove(&modlinkage)) == 0)
                kmem_free(smb_clones, sizeof (smb_clone_t) * smb_nclones);

        return (err);
}

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