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

/*-
* Copyright (c) 2021 Jared McNeill <[email protected]>
* 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)
{
}