root/sys/dev/efi.c
/* $NetBSD: efi.c,v 1.10 2025/03/30 14:36:48 riastradh Exp $ */

/*-
 * Copyright (c) 2021 Jared McNeill <jmcneill@invisible.ca>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * This pseudo-driver implements a /dev/efi character device that provides
 * ioctls for using UEFI runtime time and variable services.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: efi.c,v 1.10 2025/03/30 14:36:48 riastradh Exp $");

#include <sys/param.h>
#include <sys/conf.h>
#include <sys/kmem.h>
#include <sys/atomic.h>
#include <sys/efiio.h>

#include <uvm/uvm_extern.h>

#include <dev/efivar.h>
#include <dev/mm.h>

#include "ioconf.h"

/*
 * Maximum length of an EFI variable name in bytes. The UEFI spec
 * doesn't specify a constraint, but we want to limit the size to act
 * as a guard rail against allocating too much kernel memory.
 */
#define EFI_VARNAME_MAXBYTES            EFI_PAGE_SIZE

/*
 * Pointer to arch specific EFI backend.
 */
static const struct efi_ops *efi_ops = NULL;

/*
 * Only allow one user of /dev/efi at a time. Even though the MD EFI backends
 * should serialize individual UEFI RT calls, the UEFI specification says
 * that a SetVariable() call between calls to GetNextVariableName() may
 * produce unpredictable results, and we want to avoid this.
 */
static volatile u_int efi_isopen = 0;

static dev_type_open(efi_open);
static dev_type_close(efi_close);
static dev_type_ioctl(efi_ioctl);

const struct cdevsw efi_cdevsw = {
        .d_open =       efi_open,
        .d_close =      efi_close,
        .d_ioctl =      efi_ioctl,
        .d_read =       noread,
        .d_write =      nowrite,
        .d_stop =       nostop,
        .d_tty =        notty,
        .d_poll =       nopoll,
        .d_mmap =       nommap,
        .d_kqfilter =   nokqfilter,
        .d_discard =    nodiscard,
        .d_flag =       D_OTHER | D_MPSAFE,
};

static int
efi_open(dev_t dev, int flags, int type, struct lwp *l)
{

        if (efi_ops == NULL) {
                return ENXIO;
        }
        if (atomic_swap_uint(&efi_isopen, 1) == 1) {
                return EBUSY;
        }
        membar_acquire();
        return 0;
}

static int
efi_close(dev_t dev, int flags, int type, struct lwp *l)
{

        KASSERT(efi_isopen);
        atomic_store_release(&efi_isopen, 0);
        return 0;
}

static int
efi_status_to_error(efi_status status)
{
        switch (status) {
        case EFI_SUCCESS:
                return 0;
        case EFI_INVALID_PARAMETER:
                return EINVAL;
        case EFI_UNSUPPORTED:
                return EOPNOTSUPP;
        case EFI_BUFFER_TOO_SMALL:
                return ERANGE;
        case EFI_DEVICE_ERROR:
                return EIO;
        case EFI_WRITE_PROTECTED:
                return EROFS;
        case EFI_OUT_OF_RESOURCES:
                return ENOMEM;
        case EFI_NOT_FOUND:
                return ENOENT;
        case EFI_SECURITY_VIOLATION:
                return EACCES;
        default:
                return EIO;
        }
}

/* XXX move to efi.h */
#define EFI_SYSTEM_RESOURCE_TABLE_GUID                                        \
        {0xb122a263,0x3661,0x4f68,0x99,0x29,{0x78,0xf8,0xb0,0xd6,0x21,0x80}}
#define EFI_PROPERTIES_TABLE                                                  \
        {0x880aaca3,0x4adc,0x4a04,0x90,0x79,{0xb7,0x47,0x34,0x08,0x25,0xe5}}

#define EFI_SYSTEM_RESOURCE_TABLE_FIRMWARE_RESOURCE_VERSION     1

struct EFI_SYSTEM_RESOURCE_ENTRY {
        struct uuid     FwClass;
        uint32_t        FwType;
        uint32_t        FwVersion;
        uint32_t        LowestSupportedFwVersion;
        uint32_t        CapsuleFlags;
        uint32_t        LastAttemptVersion;
        uint32_t        LastAttemptStatus;
};

struct EFI_SYSTEM_RESOURCE_TABLE {
        uint32_t        FwResourceCount;
        uint32_t        FwResourceCountMax;
        uint64_t        FwResourceVersion;
        struct EFI_SYSTEM_RESOURCE_ENTRY        Entries[];
};

static void *
efi_map_pa(uint64_t addr, bool *directp)
{
        paddr_t pa = addr;
        vaddr_t va;

        /*
         * Verify the address is not truncated by conversion to
         * paddr_t.  This might happen with a 64-bit EFI booting a
         * 32-bit OS.
         */
        if (pa != addr)
                return NULL;

        /*
         * Try direct-map if we have it.  If it works, note that it was
         * direct-mapped for efi_unmap.
         */
#ifdef __HAVE_MM_MD_DIRECT_MAPPED_PHYS
        if (mm_md_direct_mapped_phys(pa, &va)) {
                *directp = true;
                return (void *)va;
        }
#endif

        /*
         * No direct map.  Reserve a page of kernel virtual address
         * space, with no backing, to map to the physical address.
         */
        va = uvm_km_alloc(kernel_map, PAGE_SIZE, 0,
            UVM_KMF_VAONLY|UVM_KMF_WAITVA);
        KASSERT(va != 0);

        /*
         * Map the kva page to the physical address and update the
         * kernel pmap so we can use it.
         */
        pmap_kenter_pa(va, pa, VM_PROT_READ, 0);
        pmap_update(pmap_kernel());

        /*
         * Success!  Return the VA and note that it was not
         * direct-mapped for efi_unmap.
         */
        *directp = false;
        return (void *)va;
}

static void
efi_unmap(void *ptr, bool direct)
{
        vaddr_t va = (vaddr_t)ptr;

        /*
         * If it was direct-mapped, nothing to do here.
         */
        if (direct)
                return;

        /*
         * First remove the mapping from the kernel pmap so that it can
         * be reused, before we free the kva and let anyone else reuse
         * it.
         */
        pmap_kremove(va, PAGE_SIZE);
        pmap_update(pmap_kernel());

        /*
         * Next free the kva so it can be reused by someone else.
         */
        uvm_km_free(kernel_map, va, PAGE_SIZE, UVM_KMF_VAONLY);
}

static int
efi_ioctl_got_table(struct efi_get_table_ioc *ioc, void *ptr, size_t len)
{

        /*
         * Return the actual table length.
         */
        ioc->table_len = len;

        /*
         * Copy out as much as we can into the user's allocated buffer.
         */
        return copyout(ptr, ioc->buf, MIN(ioc->buf_len, len));
}

static int
efi_ioctl_get_esrt(struct efi_get_table_ioc *ioc,
    struct EFI_SYSTEM_RESOURCE_TABLE *tab)
{

        /*
         * Verify the firmware resource version is one we understand.
         */
        if (tab->FwResourceVersion !=
            EFI_SYSTEM_RESOURCE_TABLE_FIRMWARE_RESOURCE_VERSION)
                return ENOENT;

        /*
         * Verify the resource count fits within the single page we
         * have mapped.
         *
         * XXX What happens if it doesn't?  Are we expected to map more
         * than one page, according to the table header?  The UEFI spec
         * is unclear on this.
         */
        const size_t entry_space = PAGE_SIZE -
            offsetof(struct EFI_SYSTEM_RESOURCE_TABLE, Entries);
        if (tab->FwResourceCount > entry_space/sizeof(tab->Entries[0]))
                return ENOENT;

        /*
         * Success!  Return everything through the last table entry.
         */
        const size_t len = offsetof(struct EFI_SYSTEM_RESOURCE_TABLE,
            Entries[tab->FwResourceCount]);
        return efi_ioctl_got_table(ioc, tab, len);
}

static int
efi_ioctl_get_table(struct efi_get_table_ioc *ioc)
{
        uint64_t addr;
        bool direct;
        efi_status status;
        int error;

        /*
         * If the platform doesn't support it yet, fail now.
         */
        if (efi_ops->efi_gettab == NULL)
                return ENODEV;

        /*
         * Get the address of the requested table out of the EFI
         * configuration table.
         */
        status = efi_ops->efi_gettab(&ioc->uuid, &addr);
        if (status != EFI_SUCCESS)
                return efi_status_to_error(status);

        /*
         * UEFI provides no generic way to identify the size of the
         * table, so we have to bake knowledge of every vendor GUID
         * into this code to safely expose the right amount of data to
         * userland.
         *
         * We even have to bake knowledge of which ones are physically
         * addressed and which ones might be virtually addressed
         * according to the vendor GUID into this code, although for
         * the moment we never use RT->SetVirtualAddressMap so we only
         * ever have to deal with physical addressing.
         */
        if (memcmp(&ioc->uuid, &(struct uuid)EFI_SYSTEM_RESOURCE_TABLE_GUID,
                sizeof(ioc->uuid)) == 0) {
                struct EFI_SYSTEM_RESOURCE_TABLE *tab;

                if ((tab = efi_map_pa(addr, &direct)) == NULL)
                        return ENOENT;
                error = efi_ioctl_get_esrt(ioc, tab);
                efi_unmap(tab, direct);
        } else {
                error = ENOENT;
        }

        return error;
}

static int
efi_ioctl_var_get(struct efi_var_ioc *var)
{
        uint16_t *namebuf;
        void *databuf = NULL;
        size_t databufsize;
        unsigned long datasize;
        efi_status status;
        int error;

        if (var->name == NULL || var->namesize == 0 ||
            (var->data != NULL && var->datasize == 0)) {
                return EINVAL;
        }
        if (var->namesize > EFI_VARNAME_MAXBYTES) {
                return ENOMEM;
        }
        if (var->datasize > ULONG_MAX) { /* XXX stricter limit */
                return ENOMEM;
        }

        namebuf = kmem_alloc(var->namesize, KM_SLEEP);
        error = copyin(var->name, namebuf, var->namesize);
        if (error != 0) {
                goto done;
        }
        if (namebuf[var->namesize / 2 - 1] != '\0') {
                error = EINVAL;
                goto done;
        }
        databufsize = var->datasize;
        if (databufsize != 0) {
                databuf = kmem_alloc(databufsize, KM_SLEEP);
                error = copyin(var->data, databuf, databufsize);
                if (error != 0) {
                        goto done;
                }
        }

        datasize = databufsize;
        status = efi_ops->efi_getvar(namebuf, &var->vendor, &var->attrib,
            &datasize, databuf);
        if (status != EFI_SUCCESS && status != EFI_BUFFER_TOO_SMALL) {
                error = efi_status_to_error(status);
                goto done;
        }
        var->datasize = datasize;
        if (status == EFI_SUCCESS && databufsize != 0) {
                error = copyout(databuf, var->data,
                    MIN(datasize, databufsize));
        } else {
                var->data = NULL;
        }

done:
        kmem_free(namebuf, var->namesize);
        if (databuf != NULL) {
                kmem_free(databuf, databufsize);
        }
        return error;
}

static int
efi_ioctl_var_next(struct efi_var_ioc *var)
{
        efi_status status;
        uint16_t *namebuf;
        size_t namebufsize;
        unsigned long namesize;
        int error;

        if (var->name == NULL || var->namesize == 0) {
                return EINVAL;
        }
        if (var->namesize > EFI_VARNAME_MAXBYTES) {
                return ENOMEM;
        }

        namebufsize = var->namesize;
        namebuf = kmem_alloc(namebufsize, KM_SLEEP);
        error = copyin(var->name, namebuf, namebufsize);
        if (error != 0) {
                goto done;
        }

        CTASSERT(EFI_VARNAME_MAXBYTES <= ULONG_MAX);
        namesize = namebufsize;
        status = efi_ops->efi_nextvar(&namesize, namebuf, &var->vendor);
        if (status != EFI_SUCCESS && status != EFI_BUFFER_TOO_SMALL) {
                error = efi_status_to_error(status);
                goto done;
        }
        var->namesize = namesize;
        if (status == EFI_SUCCESS) {
                error = copyout(namebuf, var->name,
                    MIN(namesize, namebufsize));
        } else {
                var->name = NULL;
        }

done:
        kmem_free(namebuf, namebufsize);
        return error;
}

static int
efi_ioctl_var_set(struct efi_var_ioc *var)
{
        efi_status status;
        uint16_t *namebuf;
        uint16_t *databuf = NULL;
        int error;

        if (var->name == NULL || var->namesize == 0) {
                return EINVAL;
        }

        namebuf = kmem_alloc(var->namesize, KM_SLEEP);
        error = copyin(var->name, namebuf, var->namesize);
        if (error != 0) {
                goto done;
        }
        if (namebuf[var->namesize / 2 - 1] != '\0') {
                error = EINVAL;
                goto done;
        }
        if (var->datasize != 0) {
                databuf = kmem_alloc(var->datasize, KM_SLEEP);
                error = copyin(var->data, databuf, var->datasize);
                if (error != 0) {
                        goto done;
                }
        }

        status = efi_ops->efi_setvar(namebuf, &var->vendor, var->attrib,
            var->datasize, databuf);
        error = efi_status_to_error(status);

done:
        kmem_free(namebuf, var->namesize);
        if (databuf != NULL) {
                kmem_free(databuf, var->datasize);
        }
        return error;
}

static int
efi_ioctl(dev_t dev, u_long cmd, void *data, int flags, struct lwp *l)
{
        KASSERT(efi_ops != NULL);

        switch (cmd) {
        case EFIIOC_GET_TABLE:
                return efi_ioctl_get_table(data);
        case EFIIOC_VAR_GET:
                return efi_ioctl_var_get(data);
        case EFIIOC_VAR_NEXT:
                return efi_ioctl_var_next(data);
        case EFIIOC_VAR_SET:
                return efi_ioctl_var_set(data);
        }

        return ENOTTY;
}

void
efi_register_ops(const struct efi_ops *ops)
{
        KASSERT(efi_ops == NULL);
        efi_ops = ops;
}

void
efiattach(int count)
{
}