// SPDX-License-Identifier: GPL-2.0-only
/*
 * smccc_filter - Tests for the SMCCC filter UAPI.
 *
 * Copyright (c) 2023 Google LLC
 *
 * This test includes:
 *  - Tests that the UAPI constraints are upheld by KVM. For example, userspace
 *    is prevented from filtering the architecture range of SMCCC calls.
 *  - Test that the filter actions (DENIED, FWD_TO_USER) work as intended.
 */

#include <linux/arm-smccc.h>
#include <linux/psci.h>
#include <stdint.h>

#include "processor.h"
#include "test_util.h"

enum smccc_conduit {
	HVC_INSN,
	SMC_INSN,
};

#define for_each_conduit(conduit)					\
	for (conduit = HVC_INSN; conduit <= SMC_INSN; conduit++)

static void guest_main(uint32_t func_id, enum smccc_conduit conduit)
{
	struct arm_smccc_res res;

	if (conduit == SMC_INSN)
		smccc_smc(func_id, 0, 0, 0, 0, 0, 0, 0, &res);
	else
		smccc_hvc(func_id, 0, 0, 0, 0, 0, 0, 0, &res);

	GUEST_SYNC(res.a0);
}

static int __set_smccc_filter(struct kvm_vm *vm, uint32_t start, uint32_t nr_functions,
			      enum kvm_smccc_filter_action action)
{
	struct kvm_smccc_filter filter = {
		.base		= start,
		.nr_functions	= nr_functions,
		.action		= action,
	};

	return __kvm_device_attr_set(vm->fd, KVM_ARM_VM_SMCCC_CTRL,
				     KVM_ARM_VM_SMCCC_FILTER, &filter);
}

static void set_smccc_filter(struct kvm_vm *vm, uint32_t start, uint32_t nr_functions,
			     enum kvm_smccc_filter_action action)
{
	int ret = __set_smccc_filter(vm, start, nr_functions, action);

	TEST_ASSERT(!ret, "failed to configure SMCCC filter: %d", ret);
}

static struct kvm_vm *setup_vm(struct kvm_vcpu **vcpu)
{
	struct kvm_vcpu_init init;
	struct kvm_vm *vm;

	vm = vm_create(1);
	vm_ioctl(vm, KVM_ARM_PREFERRED_TARGET, &init);

	/*
	 * Enable in-kernel emulation of PSCI to ensure that calls are denied
	 * due to the SMCCC filter, not because of KVM.
	 */
	init.features[0] |= (1 << KVM_ARM_VCPU_PSCI_0_2);

	*vcpu = aarch64_vcpu_add(vm, 0, &init, guest_main);
	return vm;
}

static void test_pad_must_be_zero(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	struct kvm_smccc_filter filter = {
		.base		= PSCI_0_2_FN_PSCI_VERSION,
		.nr_functions	= 1,
		.action		= KVM_SMCCC_FILTER_DENY,
		.pad		= { -1 },
	};
	int r;

	r = __kvm_device_attr_set(vm->fd, KVM_ARM_VM_SMCCC_CTRL,
				  KVM_ARM_VM_SMCCC_FILTER, &filter);
	TEST_ASSERT(r < 0 && errno == EINVAL,
		    "Setting filter with nonzero padding should return EINVAL");
}

/* Ensure that userspace cannot filter the Arm Architecture SMCCC range */
static void test_filter_reserved_range(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	uint32_t smc64_fn;
	int r;

	r = __set_smccc_filter(vm, ARM_SMCCC_ARCH_WORKAROUND_1,
			       1, KVM_SMCCC_FILTER_DENY);
	TEST_ASSERT(r < 0 && errno == EEXIST,
		    "Attempt to filter reserved range should return EEXIST");

	smc64_fn = ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_64,
				      0, 0);

	r = __set_smccc_filter(vm, smc64_fn, 1, KVM_SMCCC_FILTER_DENY);
	TEST_ASSERT(r < 0 && errno == EEXIST,
		    "Attempt to filter reserved range should return EEXIST");

	kvm_vm_free(vm);
}

static void test_invalid_nr_functions(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	int r;

	r = __set_smccc_filter(vm, PSCI_0_2_FN64_CPU_ON, 0, KVM_SMCCC_FILTER_DENY);
	TEST_ASSERT(r < 0 && errno == EINVAL,
		    "Attempt to filter 0 functions should return EINVAL");

	kvm_vm_free(vm);
}

static void test_overflow_nr_functions(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	int r;

	r = __set_smccc_filter(vm, ~0, ~0, KVM_SMCCC_FILTER_DENY);
	TEST_ASSERT(r < 0 && errno == EINVAL,
		    "Attempt to overflow filter range should return EINVAL");

	kvm_vm_free(vm);
}

static void test_reserved_action(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	int r;

	r = __set_smccc_filter(vm, PSCI_0_2_FN64_CPU_ON, 1, -1);
	TEST_ASSERT(r < 0 && errno == EINVAL,
		    "Attempt to use reserved filter action should return EINVAL");

	kvm_vm_free(vm);
}


/* Test that overlapping configurations of the SMCCC filter are rejected */
static void test_filter_overlap(void)
{
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm = setup_vm(&vcpu);
	int r;

	set_smccc_filter(vm, PSCI_0_2_FN64_CPU_ON, 1, KVM_SMCCC_FILTER_DENY);

	r = __set_smccc_filter(vm, PSCI_0_2_FN64_CPU_ON, 1, KVM_SMCCC_FILTER_DENY);
	TEST_ASSERT(r < 0 && errno == EEXIST,
		    "Attempt to filter already configured range should return EEXIST");

	kvm_vm_free(vm);
}

static void expect_call_denied(struct kvm_vcpu *vcpu)
{
	struct ucall uc;

	if (get_ucall(vcpu, &uc) != UCALL_SYNC)
		TEST_FAIL("Unexpected ucall: %lu", uc.cmd);

	TEST_ASSERT(uc.args[1] == SMCCC_RET_NOT_SUPPORTED,
		    "Unexpected SMCCC return code: %lu", uc.args[1]);
}

/* Denied SMCCC calls have a return code of SMCCC_RET_NOT_SUPPORTED */
static void test_filter_denied(void)
{
	enum smccc_conduit conduit;
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm;

	for_each_conduit(conduit) {
		vm = setup_vm(&vcpu);

		set_smccc_filter(vm, PSCI_0_2_FN_PSCI_VERSION, 1, KVM_SMCCC_FILTER_DENY);
		vcpu_args_set(vcpu, 2, PSCI_0_2_FN_PSCI_VERSION, conduit);

		vcpu_run(vcpu);
		expect_call_denied(vcpu);

		kvm_vm_free(vm);
	}
}

static void expect_call_fwd_to_user(struct kvm_vcpu *vcpu, uint32_t func_id,
				    enum smccc_conduit conduit)
{
	struct kvm_run *run = vcpu->run;

	TEST_ASSERT(run->exit_reason == KVM_EXIT_HYPERCALL,
		    "Unexpected exit reason: %u", run->exit_reason);
	TEST_ASSERT(run->hypercall.nr == func_id,
		    "Unexpected SMCCC function: %llu", run->hypercall.nr);

	if (conduit == SMC_INSN)
		TEST_ASSERT(run->hypercall.flags & KVM_HYPERCALL_EXIT_SMC,
			    "KVM_HYPERCALL_EXIT_SMC is not set");
	else
		TEST_ASSERT(!(run->hypercall.flags & KVM_HYPERCALL_EXIT_SMC),
			    "KVM_HYPERCALL_EXIT_SMC is set");
}

/* SMCCC calls forwarded to userspace cause KVM_EXIT_HYPERCALL exits */
static void test_filter_fwd_to_user(void)
{
	enum smccc_conduit conduit;
	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm;

	for_each_conduit(conduit) {
		vm = setup_vm(&vcpu);

		set_smccc_filter(vm, PSCI_0_2_FN_PSCI_VERSION, 1, KVM_SMCCC_FILTER_FWD_TO_USER);
		vcpu_args_set(vcpu, 2, PSCI_0_2_FN_PSCI_VERSION, conduit);

		vcpu_run(vcpu);
		expect_call_fwd_to_user(vcpu, PSCI_0_2_FN_PSCI_VERSION, conduit);

		kvm_vm_free(vm);
	}
}

static bool kvm_supports_smccc_filter(void)
{
	struct kvm_vm *vm = vm_create_barebones();
	int r;

	r = __kvm_has_device_attr(vm->fd, KVM_ARM_VM_SMCCC_CTRL, KVM_ARM_VM_SMCCC_FILTER);

	kvm_vm_free(vm);
	return !r;
}

int main(void)
{
	TEST_REQUIRE(kvm_supports_smccc_filter());

	test_pad_must_be_zero();
	test_invalid_nr_functions();
	test_overflow_nr_functions();
	test_reserved_action();
	test_filter_reserved_range();
	test_filter_overlap();
	test_filter_denied();
	test_filter_fwd_to_user();
}
