root/usr/src/test/bhyve-tests/tests/inst_emul/inout.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2024 Oxide Computer Company
 */

#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <libgen.h>
#include <assert.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/sysmacros.h>
#include <sys/debug.h>
#include <sys/vmm.h>
#include <sys/vmm_dev.h>
#include <vmmapi.h>

#include "in_guest.h"

enum test_state {
        SEEKING_IN,
        SEEKING_OUT,
        SEEKING_END,
        DONE,
};

bool
advance_test_state(const struct vm_exit *vexit, struct vm_entry *ventry,
    struct vcpu *vcpu, enum vm_exit_kind kind, enum test_state *state,
    const bool have_decodeassist)
{
        if (*state != DONE && kind == VEK_REENTR) {
                return (true);
        }

        switch (*state) {
        case SEEKING_IN:
                if (kind != VEK_UNHANDLED) {
                        break;
                }

                if (have_decodeassist) {
                        if (vexit->exitcode != VM_EXITCODE_INOUT) {
                                break;
                        }

                        const bool match_inb =
                            vexit_match_inout(vexit, true, 0x55aa, 1, NULL);
                        if (!match_inb) {
                                break;
                        }

                        ventry_fulfill_inout(vexit, ventry, 0xee);
                } else {
                        if (vexit->exitcode != VM_EXITCODE_INST_EMUL) {
                                break;
                        }

                        const bool match_inb =
                            vexit->u.inst_emul.num_valid >= 2 &&
                            vexit->u.inst_emul.inst[0] == 0xf3 &&
                            vexit->u.inst_emul.inst[1] == 0x6c;
                        if (!match_inb) {
                                break;
                        }

                        int err = vm_set_register(vcpu, 0, vexit->rip + 2);
                        if (err != 0) {
                                test_fail_errno(err, "failed to set %rip");
                        }
                }
                *state = SEEKING_OUT;
                return (true);
        case SEEKING_OUT:
                if (kind != VEK_UNHANDLED) {
                        break;
                }

                if (have_decodeassist) {
                        if (vexit->exitcode != VM_EXITCODE_INOUT) {
                                break;
                        }

                        const bool match_outb =
                            vexit_match_inout(vexit, false, 0x55aa, 1, NULL);
                        if (!match_outb) {
                                break;
                        }

                        ventry_fulfill_inout(vexit, ventry, 0);
                } else {
                        if (vexit->exitcode != VM_EXITCODE_INST_EMUL) {
                                break;
                        }

                        const bool match_outb =
                            vexit->u.inst_emul.num_valid >= 2 &&
                            vexit->u.inst_emul.inst[0] == 0xf3 &&
                            vexit->u.inst_emul.inst[1] == 0x6e;
                        if (!match_outb) {
                                break;
                        }

                        int err = vm_set_register(vcpu, 0, vexit->rip + 2);
                        if (err != 0) {
                                test_fail_errno(err, "failed to set %rip");
                        }
                }
                *state = SEEKING_END;
                return (true);
        case SEEKING_END:
                if (kind == VEK_TEST_PASS) {
                        *state = DONE;
                        return (true);
                }
                break;
        case DONE:
                break;
        }

        return (false);
}

int
main(int argc, char *argv[])
{
        const char *test_suite_name = basename(argv[0]);
        struct vmctx *ctx = NULL;
        struct vcpu *vcpu;
        int err;

        ctx = test_initialize(test_suite_name);

        /*
         * Guest execution of `in` and `out` instructions (and their repeatable
         * string versions) can have substantially different outcomes depending
         * on available hardware support.
         *
         * For simple `in` or `out`, `byhve` will cause a VM exit with exit
         * code `VM_EXITCODE_INOUT`. This is the simplest case, and not
         * currently exercised in this test.
         *
         * For `ins` and `outs`, decoding the exact operation is more complex.
         * For processors with the DecodeAssist feature, we rely on the
         * processor to do that decoding and provide information about the
         * trapped instruction. As a result, `ins` and `outs` on such processors
         * result in a VM exit with code `VM_EXITCODE_INOUT`.
         *
         * Finally, if DecodeAssist is *not* available, we don't currently do
         * any in-kernel disassembly to backfill the missing functionality. So
         * instead, `ins`/`outs` on processors without DecodeAssist result in a
         * VM exit with code `VM_EXITCODE_INST_EMUL`.
         *
         * Since the kernel behavior varies based on hardware features, detect
         * DecodeAssist here as well to check for what we expect the kernel to
         * do on this processor.
         */
        uint_t regs[4];
        do_cpuid(0x8000000a, regs);
        const bool have_decodeassist = (regs[3] & (1 << 7)) != 0;

        if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
                test_fail_errno(errno, "Could not open vcpu0");
        }

        err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
        if (err != 0) {
                test_fail_errno(err, "Could not initialize vcpu0");
        }

        struct vm_entry ventry = { 0 };
        struct vm_exit vexit = { 0 };

        enum test_state state = SEEKING_IN;

        do {
                const enum vm_exit_kind kind =
                    test_run_vcpu(vcpu, &ventry, &vexit);

                const bool exit_ok = advance_test_state(&vexit, &ventry, vcpu,
                    kind, &state, have_decodeassist);

                if (!exit_ok) {
                        test_fail_vmexit(&vexit);
                        break;
                }
        } while (state != DONE);

        test_pass();
}