/*
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * uefi vars device - EfiSmmVariableProtocol implementation
 */
#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "system/dma.h"
#include "migration/vmstate.h"

#include "hw/uefi/var-service.h"
#include "hw/uefi/var-service-api.h"
#include "hw/uefi/var-service-edk2.h"

#include "trace.h"

#define EFI_VARIABLE_ATTRIBUTE_SUPPORTED                                \
    (EFI_VARIABLE_NON_VOLATILE |                                        \
     EFI_VARIABLE_BOOTSERVICE_ACCESS |                                  \
     EFI_VARIABLE_RUNTIME_ACCESS |                                      \
     EFI_VARIABLE_HARDWARE_ERROR_RECORD |                               \
     EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS |               \
     EFI_VARIABLE_APPEND_WRITE)


const VMStateDescription vmstate_uefi_time = {
    .name = "uefi-time",
    .fields = (VMStateField[]) {
        VMSTATE_UINT16(year, efi_time),
        VMSTATE_UINT8(month, efi_time),
        VMSTATE_UINT8(day, efi_time),
        VMSTATE_UINT8(hour, efi_time),
        VMSTATE_UINT8(minute, efi_time),
        VMSTATE_UINT8(second, efi_time),
        VMSTATE_UINT32(nanosecond, efi_time),
        VMSTATE_END_OF_LIST()
    },
};

const VMStateDescription vmstate_uefi_variable = {
    .name = "uefi-variable",
    .fields = (VMStateField[]) {
        VMSTATE_UINT8_ARRAY_V(guid.data, uefi_variable, sizeof(QemuUUID), 0),
        VMSTATE_UINT32(name_size, uefi_variable),
        VMSTATE_UINT32(data_size, uefi_variable),
        VMSTATE_UINT32(attributes, uefi_variable),
        VMSTATE_VBUFFER_ALLOC_UINT32(name, uefi_variable, 0, NULL, name_size),
        VMSTATE_VBUFFER_ALLOC_UINT32(data, uefi_variable, 0, NULL, data_size),
        VMSTATE_STRUCT(time, uefi_variable, 0, vmstate_uefi_time, efi_time),
        VMSTATE_END_OF_LIST()
    },
};

uefi_variable *uefi_vars_find_variable(uefi_vars_state *uv, QemuUUID guid,
                                       const uint16_t *name, uint64_t name_size)
{
    uefi_variable *var;

    QTAILQ_FOREACH(var, &uv->variables, next) {
        if (!uefi_str_equal(var->name, var->name_size,
                            name, name_size)) {
            continue;
        }
        if (!qemu_uuid_is_equal(&var->guid, &guid)) {
            continue;
        }
        if (!var->data_size) {
            /* in process of being created/updated */
            continue;
        }
        return var;
    }
    return NULL;
}

static uefi_variable *add_variable(uefi_vars_state *uv, QemuUUID guid,
                                   const uint16_t *name, uint64_t name_size,
                                   uint32_t attributes)
{
    uefi_variable *var;

    var = g_new0(uefi_variable, 1);
    var->guid = guid;
    var->name = g_malloc(name_size);
    memcpy(var->name, name, name_size);
    var->name_size = name_size;
    var->attributes = attributes;

    var->attributes &= ~EFI_VARIABLE_APPEND_WRITE;

    QTAILQ_INSERT_TAIL(&uv->variables, var, next);
    return var;
}

static void del_variable(uefi_vars_state *uv, uefi_variable *var)
{
    if (!var) {
        return;
    }

    QTAILQ_REMOVE(&uv->variables, var, next);
    g_free(var->data);
    g_free(var->name);
    g_free(var->digest);
    g_free(var);
}

static size_t variable_size(uefi_variable *var)
{
    size_t size;

    size  = sizeof(*var);
    size += var->name_size;
    size += var->data_size;
    size += var->digest_size;
    return size;
}

void uefi_vars_set_variable(uefi_vars_state *uv, QemuUUID guid,
                            const uint16_t *name, uint64_t name_size,
                            uint32_t attributes,
                            void *data, uint64_t data_size)
{
    uefi_variable *old_var, *new_var;

    uefi_trace_variable(__func__, guid, name, name_size);

    old_var = uefi_vars_find_variable(uv, guid, name, name_size);
    if (old_var) {
        uv->used_storage -= variable_size(old_var);
        del_variable(uv, old_var);
    }

    new_var = add_variable(uv, guid, name, name_size, attributes);
    new_var->data = g_malloc(data_size);
    new_var->data_size = data_size;
    memcpy(new_var->data, data, data_size);
    uv->used_storage += variable_size(new_var);
}

void uefi_vars_clear_volatile(uefi_vars_state *uv)
{
    uefi_variable *var, *n;

    QTAILQ_FOREACH_SAFE(var, &uv->variables, next, n) {
        if (var->attributes & EFI_VARIABLE_NON_VOLATILE) {
            continue;
        }
        uv->used_storage -= variable_size(var);
        del_variable(uv, var);
    }
}

void uefi_vars_clear_all(uefi_vars_state *uv)
{
    uefi_variable *var, *n;

    QTAILQ_FOREACH_SAFE(var, &uv->variables, next, n) {
        del_variable(uv, var);
    }
    uv->used_storage = 0;
}

void uefi_vars_update_storage(uefi_vars_state *uv)
{
    uefi_variable *var;

    uv->used_storage = 0;
    QTAILQ_FOREACH(var, &uv->variables, next) {
        uv->used_storage += variable_size(var);
    }
}

static gboolean check_access(uefi_vars_state *uv, uefi_variable *var)
{
    if (!uv->exit_boot_service) {
        if (!(var->attributes & EFI_VARIABLE_BOOTSERVICE_ACCESS)) {
            return false;
        }
    } else {
        if (!(var->attributes & EFI_VARIABLE_RUNTIME_ACCESS)) {
            return false;
        }
    }
    return true;
}

static efi_status check_update(uefi_vars_state *uv, uefi_variable *old_var,
                               uefi_variable *new_var)
{
    efi_status status;

    if (old_var) {
        if (!check_access(uv, old_var)) {
            return EFI_ACCESS_DENIED;
        }
    }

    if (new_var) {
        if (new_var->attributes & ~EFI_VARIABLE_ATTRIBUTE_SUPPORTED) {
            return EFI_UNSUPPORTED;
        }
        if (!check_access(uv, new_var)) {
            return EFI_ACCESS_DENIED;
        }
    }

    if (old_var && new_var) {
        if (old_var->attributes != new_var->attributes) {
            return EFI_INVALID_PARAMETER;
        }
    }

    if (new_var) {
        /* create + update */
        status = uefi_vars_policy_check(uv, new_var, old_var == NULL);
    } else {
        /* delete */
        g_assert(old_var);
        status = uefi_vars_policy_check(uv, old_var, false);
    }
    if (status != EFI_SUCCESS) {
        return status;
    }

    status = uefi_vars_check_secure_boot(uv, new_var ?: old_var);
    if (status != EFI_SUCCESS) {
        return status;
    }

    return EFI_SUCCESS;
}

static void append_write(uefi_variable *old_var,
                         uefi_variable *new_var)
{
    uefi_vars_siglist siglist;
    uint64_t size;
    void *data;

    uefi_vars_siglist_init(&siglist);
    uefi_vars_siglist_parse(&siglist, old_var->data, old_var->data_size);
    uefi_vars_siglist_parse(&siglist, new_var->data, new_var->data_size);

    size = uefi_vars_siglist_blob_size(&siglist);
    data = g_malloc(size);
    uefi_vars_siglist_blob_generate(&siglist, data, size);

    g_free(new_var->data);
    new_var->data = data;
    new_var->data_size = size;

    uefi_vars_siglist_free(&siglist);
}

static size_t uefi_vars_mm_error(mm_header *mhdr, mm_variable *mvar,
                                 uint64_t status)
{
    mvar->status = status;
    return sizeof(*mvar);
}

static size_t uefi_vars_mm_get_variable(uefi_vars_state *uv, mm_header *mhdr,
                                        mm_variable *mvar, void *func)
{
    mm_variable_access *va = func;
    uint16_t *name;
    void *data;
    uefi_variable *var;
    uint64_t length;

    length = sizeof(*mvar) + sizeof(*va);
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    if (va->name_size > uv->max_storage ||
        va->data_size > uv->max_storage) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_OUT_OF_RESOURCES);
    }

    name = func + sizeof(*va);
    if (uadd64_overflow(length, va->name_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    if (!uefi_str_is_valid(name, va->name_size, true)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_INVALID_PARAMETER);
    }

    uefi_trace_variable(__func__, va->guid, name, va->name_size);

    var = uefi_vars_find_variable(uv, va->guid, name, va->name_size);
    if (!var) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_NOT_FOUND);
    }

    /* check permissions etc. */
    if (!check_access(uv, var)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_ACCESS_DENIED);
    }

    data = func + sizeof(*va) + va->name_size;
    if (uadd64_overflow(length, va->data_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (uv->buf_size < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    va->attributes = var->attributes;
    if (va->data_size < var->data_size) {
        va->data_size = var->data_size;
        length -= va->data_size;
        mvar->status = EFI_BUFFER_TOO_SMALL;
    } else {
        va->data_size = var->data_size;
        memcpy(data, var->data, var->data_size);
        mvar->status = EFI_SUCCESS;
    }
    return length;
}

static size_t
uefi_vars_mm_get_next_variable(uefi_vars_state *uv, mm_header *mhdr,
                               mm_variable *mvar, void *func)
{
    mm_next_variable *nv = func;
    uefi_variable *var;
    uint16_t *name;
    uint64_t length;

    length = sizeof(*mvar) + sizeof(*nv);
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    if (nv->name_size > uv->max_storage) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_OUT_OF_RESOURCES);
    }

    name = func + sizeof(*nv);
    if (uadd64_overflow(length, nv->name_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    if (!uefi_str_is_valid(name, nv->name_size, true)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_INVALID_PARAMETER);
    }

    if (uefi_strlen(name, nv->name_size) == 0) {
        /* empty string -> first */
        var = QTAILQ_FIRST(&uv->variables);
        while (var && !check_access(uv, var)) {
            var = QTAILQ_NEXT(var, next);
        }
        if (!var) {
            return uefi_vars_mm_error(mhdr, mvar, EFI_NOT_FOUND);
        }
    } else {
        var = uefi_vars_find_variable(uv, nv->guid, name, nv->name_size);
        if (!var) {
            return uefi_vars_mm_error(mhdr, mvar, EFI_INVALID_PARAMETER);
        }
        do {
            var = QTAILQ_NEXT(var, next);
        } while (var && !check_access(uv, var));
        if (!var) {
            return uefi_vars_mm_error(mhdr, mvar, EFI_NOT_FOUND);
        }
    }

    length = sizeof(*mvar) + sizeof(*nv) + var->name_size;
    if (uv->buf_size < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    nv->guid = var->guid;
    nv->name_size = var->name_size;
    memcpy(name, var->name, var->name_size);
    mvar->status = EFI_SUCCESS;
    return length;
}

static bool uefi_vars_mm_digest_compare(uefi_variable *old_var,
                                        uefi_variable *new_var)
{
    if (!old_var->digest ||
        !new_var->digest ||
        !old_var->digest_size ||
        !new_var->digest_size) {
        /* should not happen */
        trace_uefi_vars_security_violation("inconsistent authvar digest state");
        return false;
    }
    if (old_var->digest_size != new_var->digest_size) {
        trace_uefi_vars_security_violation("authvar digest size mismatch");
        return false;
    }
    if (memcmp(old_var->digest, new_var->digest,
               old_var->digest_size) != 0) {
        trace_uefi_vars_security_violation("authvar digest data mismatch");
        return false;
    }
    return true;
}

static size_t uefi_vars_mm_set_variable(uefi_vars_state *uv, mm_header *mhdr,
                                        mm_variable *mvar, void *func)
{
    mm_variable_access *va = func;
    uint32_t attributes = 0;
    uint16_t *name;
    void *data;
    uefi_variable *old_var, *new_var;
    uint64_t length;
    size_t new_storage;
    efi_status status;

    length = sizeof(*mvar) + sizeof(*va);
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    if (va->name_size > uv->max_storage ||
        va->data_size > uv->max_storage) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_OUT_OF_RESOURCES);
    }

    name = func + sizeof(*va);
    if (uadd64_overflow(length, va->name_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    data = func + sizeof(*va) + va->name_size;
    if (uadd64_overflow(length, va->data_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    g_assert(va->name_size < G_MAXUINT32);
    g_assert(va->data_size < G_MAXUINT32);

    if (!uefi_str_is_valid(name, va->name_size, true)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_INVALID_PARAMETER);
    }

    uefi_trace_variable(__func__, va->guid, name, va->name_size);

    old_var = uefi_vars_find_variable(uv, va->guid, name, va->name_size);
    if (va->data_size) {
        new_var = add_variable(uv, va->guid, name, va->name_size,
                               va->attributes);
        if (va->attributes & EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS) {
            /* not implemented (deprecated in uefi spec) */
            warn_report("%s: AUTHENTICATED_WRITE_ACCESS", __func__);
            mvar->status = EFI_UNSUPPORTED;
            goto rollback;
        } else if (va->attributes &
                   EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS) {
            status = uefi_vars_check_auth_2(uv, new_var, va, data);
            if (status != EFI_SUCCESS) {
                mvar->status = status;
                goto rollback;
            }
            if (old_var && new_var) {
                if (uefi_time_compare(&old_var->time, &new_var->time) > 0) {
                    trace_uefi_vars_security_violation("time check failed");
                    mvar->status = EFI_SECURITY_VIOLATION;
                    goto rollback;
                }
                if (old_var->digest_size || new_var->digest_size) {
                    if (!uefi_vars_mm_digest_compare(old_var, new_var)) {
                        mvar->status = EFI_SECURITY_VIOLATION;
                        goto rollback;
                    }
                }
            }
        } else {
            new_var->data = g_malloc(va->data_size);
            memcpy(new_var->data, data, va->data_size);
            new_var->data_size = va->data_size;
        }
        if (!new_var->data) {
            /* we land here when deleting authenticated variables */
            del_variable(uv, new_var);
            new_var = NULL;
        }
    } else {
        new_var = NULL;
    }

    if (!old_var && !new_var) {
        /* delete non-existing variable -> nothing to do */
        mvar->status = EFI_SUCCESS;
        return sizeof(*mvar);
    }

    /* check permissions etc. */
    status = check_update(uv, old_var, new_var);
    if (status != EFI_SUCCESS) {
        mvar->status = status;
        goto rollback;
    }

    if (va->attributes & EFI_VARIABLE_APPEND_WRITE && old_var && new_var) {
        /* merge signature databases */
        if (!uefi_vars_is_sb_any(new_var)) {
            mvar->status = EFI_UNSUPPORTED;
            goto rollback;
        }
        append_write(old_var, new_var);
    }

    /* check storage space */
    new_storage = uv->used_storage;
    if (old_var) {
        new_storage -= variable_size(old_var);
    }
    if (new_var) {
        new_storage += variable_size(new_var);
    }
    if (new_storage > uv->max_storage) {
        mvar->status = EFI_OUT_OF_RESOURCES;
        goto rollback;
    }

    attributes = new_var
        ? new_var->attributes
        : old_var->attributes;

    /* all good, commit */
    del_variable(uv, old_var);
    uv->used_storage = new_storage;

    if (attributes & EFI_VARIABLE_NON_VOLATILE) {
        uefi_vars_json_save(uv);
    }

    if (new_var && uefi_vars_is_sb_pk(new_var)) {
        uefi_vars_auth_init(uv);
    }

    mvar->status = EFI_SUCCESS;
    return sizeof(*mvar);

rollback:
    del_variable(uv, new_var);
    return sizeof(*mvar);
}

static size_t uefi_vars_mm_variable_info(uefi_vars_state *uv, mm_header *mhdr,
                                         mm_variable *mvar, void *func)
{
    mm_variable_info *vi = func;
    uint64_t length;

    length = sizeof(*mvar) + sizeof(*vi);
    if (uv->buf_size < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    vi->max_storage_size  = uv->max_storage;
    vi->free_storage_size = uv->max_storage - uv->used_storage;
    vi->max_variable_size = uv->max_storage >> 2;
    vi->attributes        = 0;

    mvar->status = EFI_SUCCESS;
    return length;
}

static size_t
uefi_vars_mm_get_payload_size(uefi_vars_state *uv, mm_header *mhdr,
                              mm_variable *mvar, void *func)
{
    mm_get_payload_size *ps = func;
    uint64_t length;

    length = sizeof(*mvar) + sizeof(*ps);
    if (uv->buf_size < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    ps->payload_size = uv->buf_size;
    mvar->status = EFI_SUCCESS;
    return length;
}

static size_t
uefi_vars_mm_lock_variable(uefi_vars_state *uv, mm_header *mhdr,
                           mm_variable *mvar, void *func)
{
    mm_lock_variable *lv = func;
    variable_policy_entry *pe;
    uint16_t *name, *dest;
    uint64_t length;

    length = sizeof(*mvar) + sizeof(*lv);
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    name = func + sizeof(*lv);
    if (uadd64_overflow(length, lv->name_size, &length)) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }
    if (mhdr->length < length) {
        return uefi_vars_mm_error(mhdr, mvar, EFI_BAD_BUFFER_SIZE);
    }

    uefi_trace_variable(__func__, lv->guid, name, lv->name_size);

    pe = g_malloc0(sizeof(*pe) + lv->name_size);
    pe->version               = VARIABLE_POLICY_ENTRY_REVISION;
    pe->size                  = sizeof(*pe) + lv->name_size;
    pe->offset_to_name        = sizeof(*pe);
    pe->namespace             = lv->guid;
    pe->min_size              = 0;
    pe->max_size              = UINT32_MAX;
    pe->attributes_must_have  = 0;
    pe->attributes_cant_have  = 0;
    pe->lock_policy_type      = VARIABLE_POLICY_TYPE_LOCK_NOW;

    dest = (void *)pe + pe->offset_to_name;
    memcpy(dest, name, lv->name_size);

    uefi_vars_add_policy(uv, pe);
    g_free(pe);

    mvar->status = EFI_SUCCESS;
    return length;
}

uint32_t uefi_vars_mm_vars_proto(uefi_vars_state *uv)
{
    static const char *fnames[] = {
        "zero",
        "get-variable",
        "get-next-variable-name",
        "set-variable",
        "query-variable-info",
        "ready-to-boot",
        "exit-boot-service",
        "get-statistics",
        "lock-variable",
        "var-check-prop-set",
        "var-check-prop-get",
        "get-payload-size",
        "init-runtime-cache-contect",
        "sync-runtime-cache",
        "get-runtime-cache-info",
    };
    const char  *fname;
    uint64_t    length;

    mm_header   *mhdr = (mm_header *) uv->buffer;
    mm_variable *mvar = (mm_variable *) (uv->buffer + sizeof(*mhdr));
    void        *func = (uv->buffer + sizeof(*mhdr) + sizeof(*mvar));

    if (mhdr->length < sizeof(*mvar)) {
        return UEFI_VARS_STS_ERR_BAD_BUFFER_SIZE;
    }

    fname = mvar->function < ARRAY_SIZE(fnames)
        ? fnames[mvar->function]
        : "unknown";
    trace_uefi_vars_proto_cmd(fname);

    switch (mvar->function) {
    case SMM_VARIABLE_FUNCTION_GET_VARIABLE:
        length = uefi_vars_mm_get_variable(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_GET_NEXT_VARIABLE_NAME:
        length = uefi_vars_mm_get_next_variable(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_SET_VARIABLE:
        length = uefi_vars_mm_set_variable(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_QUERY_VARIABLE_INFO:
        length = uefi_vars_mm_variable_info(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_LOCK_VARIABLE:
        length = uefi_vars_mm_lock_variable(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_GET_PAYLOAD_SIZE:
        length = uefi_vars_mm_get_payload_size(uv, mhdr, mvar, func);
        break;

    case SMM_VARIABLE_FUNCTION_READY_TO_BOOT:
        trace_uefi_event("ready-to-boot");
        uv->ready_to_boot = true;
        mvar->status = EFI_SUCCESS;
        length = 0;
        break;

    case SMM_VARIABLE_FUNCTION_EXIT_BOOT_SERVICE:
        trace_uefi_event("exit-boot-service");
        uv->exit_boot_service = true;
        mvar->status = EFI_SUCCESS;
        length = 0;
        break;

    default:
        length = uefi_vars_mm_error(mhdr, mvar, EFI_UNSUPPORTED);
        break;
    }

    if (mhdr->length < length) {
        mvar->status = EFI_BUFFER_TOO_SMALL;
    }

    uefi_trace_status(__func__, mvar->status);
    return UEFI_VARS_STS_SUCCESS;
}
