root/usr/src/cmd/bhyve/common/qemu_loader.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2022 Beckhoff Automation GmbH & Co. KG
 * Author: Corvin Köhne <c.koehne@beckhoff.com>
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/endian.h>
#include <sys/queue.h>

#include <machine/vmm.h>

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vmmapi.h>

#include "qemu_fwcfg.h"
#include "qemu_loader.h"

#ifndef __FreeBSD__
/*
 * This is better than the number of ifdef blocks that would be otherwise
 * required throughout this code. Hopefully upstream will clear up the
 * char* /uint8_t * confusion.
 */
#define strlen(x)       strlen((char *)(x))
#define strncpy(p, q, n)        strncpy((char *)(p), (char *)(q), (n))
#endif

struct qemu_loader_entry {
        uint32_t cmd_le;
        union {
                struct {
                        uint8_t name[QEMU_FWCFG_MAX_NAME];
                        uint32_t alignment_le;
                        uint8_t zone;
                } alloc;
                struct {
                        uint8_t dest_name[QEMU_FWCFG_MAX_NAME];
                        uint8_t src_name[QEMU_FWCFG_MAX_NAME];
                        uint32_t off_le;
                        uint8_t size;
                } add_pointer;
                struct {
                        uint8_t name[QEMU_FWCFG_MAX_NAME];
                        uint32_t off_le;
                        uint32_t start_le;
                        uint32_t len_le;
                } add_checksum;
                struct {
                        uint8_t dest_name[QEMU_FWCFG_MAX_NAME];
                        uint8_t src_name[QEMU_FWCFG_MAX_NAME];
                        uint32_t dest_off_le;
                        uint32_t src_off_le;
                        uint8_t size;
                } write_pointer;

                /* padding */
                uint8_t pad[124];
        };
} __packed;

enum qemu_loader_command {
        QEMU_LOADER_CMD_ALLOC = 0x1,
        QEMU_LOADER_CMD_ADD_POINTER = 0x2,
        QEMU_LOADER_CMD_ADD_CHECKSUM = 0x3,
        QEMU_LOADER_CMD_WRITE_POINTER = 0x4,
};

struct qemu_loader_element {
        STAILQ_ENTRY(qemu_loader_element) chain;
        struct qemu_loader_entry entry;
};

struct qemu_loader {
        uint8_t fwcfg_name[QEMU_FWCFG_MAX_NAME];
        STAILQ_HEAD(qemu_loader_list, qemu_loader_element) list;
};

int
qemu_loader_alloc(struct qemu_loader *const loader, const uint8_t *name,
    const uint32_t alignment, const enum qemu_loader_zone zone)
{
        struct qemu_loader_element *element;

        if (strlen(name) >= QEMU_FWCFG_MAX_NAME)
                return (EINVAL);

        element = calloc(1, sizeof(struct qemu_loader_element));
        if (element == NULL) {
                warnx("%s: failed to allocate command", __func__);
                return (ENOMEM);
        }

        element->entry.cmd_le = htole32(QEMU_LOADER_CMD_ALLOC);
        strncpy(element->entry.alloc.name, name, QEMU_FWCFG_MAX_NAME);
        element->entry.alloc.alignment_le = htole32(alignment);
        element->entry.alloc.zone = zone;

        /*
         * The guest always works on copies of the fwcfg item, which where
         * loaded into guest memory. Loading a fwcfg item is caused by ALLOC.
         * For that reason, ALLOC should be scheduled in front of any other
         * commands.
         */
        STAILQ_INSERT_HEAD(&loader->list, element, chain);

        return (0);
}

int
qemu_loader_add_checksum(struct qemu_loader *const loader, const uint8_t *name,
    const uint32_t off, const uint32_t start, const uint32_t len)
{
        struct qemu_loader_element *element;

        if (strlen(name) >= QEMU_FWCFG_MAX_NAME)
                return (EINVAL);

        element = calloc(1, sizeof(struct qemu_loader_element));
        if (element == NULL) {
                warnx("%s: failed to allocate command", __func__);
                return (ENOMEM);
        }

        element->entry.cmd_le = htole32(QEMU_LOADER_CMD_ADD_CHECKSUM);
        strncpy(element->entry.add_checksum.name, name, QEMU_FWCFG_MAX_NAME);
        element->entry.add_checksum.off_le = htole32(off);
        element->entry.add_checksum.start_le = htole32(start);
        element->entry.add_checksum.len_le = htole32(len);

        STAILQ_INSERT_TAIL(&loader->list, element, chain);

        return (0);
}

int
qemu_loader_add_pointer(struct qemu_loader *const loader,
    const uint8_t *dest_name, const uint8_t *src_name, const uint32_t off,
    const uint8_t size)
{
        struct qemu_loader_element *element;

        if (strlen(dest_name) >= QEMU_FWCFG_MAX_NAME ||
            strlen(src_name) >= QEMU_FWCFG_MAX_NAME)
                return (EINVAL);

        element = calloc(1, sizeof(struct qemu_loader_element));
        if (element == NULL) {
                warnx("%s: failed to allocate command", __func__);
                return (ENOMEM);
        }

        element->entry.cmd_le = htole32(QEMU_LOADER_CMD_ADD_POINTER);
        strncpy(element->entry.add_pointer.dest_name, dest_name,
            QEMU_FWCFG_MAX_NAME);
        strncpy(element->entry.add_pointer.src_name, src_name,
            QEMU_FWCFG_MAX_NAME);
        element->entry.add_pointer.off_le = htole32(off);
        element->entry.add_pointer.size = size;

        STAILQ_INSERT_TAIL(&loader->list, element, chain);

        return (0);
}

int
qemu_loader_create(struct qemu_loader **const new_loader,
    const uint8_t *fwcfg_name)
{
        struct qemu_loader *loader;

        if (new_loader == NULL || strlen(fwcfg_name) >= QEMU_FWCFG_MAX_NAME) {
                return (EINVAL);
        }

        loader = calloc(1, sizeof(struct qemu_loader));
        if (loader == NULL) {
                warnx("%s: failed to allocate loader", __func__);
                return (ENOMEM);
        }

        strncpy(loader->fwcfg_name, fwcfg_name, QEMU_FWCFG_MAX_NAME);
        STAILQ_INIT(&loader->list);

        *new_loader = loader;

        return (0);
}

static const uint8_t *
qemu_loader_get_zone_name(const enum qemu_loader_zone zone)
{
#ifdef  __FreeBSD__
        switch (zone) {
        case QEMU_LOADER_ALLOC_HIGH:
                return ("HIGH");
        case QEMU_LOADER_ALLOC_FSEG:
                return ("FSEG");
        default:
                return ("Unknown");
        }
#else
        switch (zone) {
        case QEMU_LOADER_ALLOC_HIGH:
                return ((uint8_t *)"HIGH");
        case QEMU_LOADER_ALLOC_FSEG:
                return ((uint8_t *)"FSEG");
        default:
                return ((uint8_t *)"Unknown");
        }
#endif
}

static void __unused
qemu_loader_dump_entry(const struct qemu_loader_entry *const entry)
{
        switch (le32toh(entry->cmd_le)) {
        case QEMU_LOADER_CMD_ALLOC:
                printf("CMD_ALLOC\n\r");
                printf("  name     : %s\n\r", entry->alloc.name);
                printf("  alignment: %8x\n\r",
                    le32toh(entry->alloc.alignment_le));
                printf("  zone     : %s\n\r",
                    qemu_loader_get_zone_name(entry->alloc.zone));
                break;
        case QEMU_LOADER_CMD_ADD_POINTER:
                printf("CMD_ADD_POINTER\n\r");
                printf("  dest_name: %s\n\r", entry->add_pointer.dest_name);
                printf("  src_name : %s\n\r", entry->add_pointer.src_name);
                printf("  off      : %8x\n\r",
                    le32toh(entry->add_pointer.off_le));
                printf("  size     : %8x\n\r", entry->add_pointer.size);
                break;
        case QEMU_LOADER_CMD_ADD_CHECKSUM:
                printf("CMD_ADD_CHECKSUM\n\r");
                printf("  name     : %s\n\r", entry->add_checksum.name);
                printf("  off      : %8x\n\r",
                    le32toh(entry->add_checksum.off_le));
                printf("  start    : %8x\n\r",
                    le32toh(entry->add_checksum.start_le));
                printf("  length   : %8x\n\r",
                    le32toh(entry->add_checksum.len_le));
                break;
        case QEMU_LOADER_CMD_WRITE_POINTER:
                printf("CMD_WRITE_POINTER\n\r");
                printf("  dest_name: %s\n\r", entry->write_pointer.dest_name);
                printf("  src_name : %s\n\r", entry->write_pointer.src_name);
                printf("  dest_off : %8x\n\r",
                    le32toh(entry->write_pointer.dest_off_le));
                printf("  src_off  : %8x\n\r",
                    le32toh(entry->write_pointer.src_off_le));
                printf("  size     : %8x\n\r", entry->write_pointer.size);
                break;
        default:
                printf("UNKNOWN\n\r");
                break;
        }
}

int
qemu_loader_finish(struct qemu_loader *const loader)
{
        struct qemu_loader_element *element;
        struct qemu_loader_entry *data;
        size_t len = 0;

        STAILQ_FOREACH(element, &loader->list, chain) {
                len += sizeof(struct qemu_loader_entry);
        }
        if (len == 0) {
                warnx("%s: bios loader empty", __func__);
                return (EFAULT);
        }

        data = calloc(1, len);
        if (data == NULL) {
                warnx("%s: failed to allocate fwcfg data", __func__);
                return (ENOMEM);
        }

        int i = 0;
        STAILQ_FOREACH(element, &loader->list, chain) {
                memcpy(&data[i], &element->entry,
                    sizeof(struct qemu_loader_entry));
                ++i;
        }

#ifdef  __FreeBSD__
        return (qemu_fwcfg_add_file(loader->fwcfg_name, len, data));
#else
        return (qemu_fwcfg_add_file((const char *)loader->fwcfg_name,
            len, data));
#endif
}