// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2025 Rivos Inc.
 *
 * Authors:
 *     Clément Léger <cleger@rivosinc.com>
 */

#include <linux/errno.h>
#include <linux/err.h>
#include <linux/kvm_host.h>
#include <asm/cpufeature.h>
#include <asm/sbi.h>
#include <asm/kvm_vcpu_sbi.h>
#include <asm/kvm_vcpu_sbi_fwft.h>

#define MIS_DELEG (BIT_ULL(EXC_LOAD_MISALIGNED) | BIT_ULL(EXC_STORE_MISALIGNED))

struct kvm_sbi_fwft_feature {
	/**
	 * @id: Feature ID
	 */
	enum sbi_fwft_feature_t id;

	/**
	 * @first_reg_num: ONE_REG index of the first ONE_REG register
	 */
	unsigned long first_reg_num;

	/**
	 * @supported: Check if the feature is supported on the vcpu
	 *
	 * This callback is optional, if not provided the feature is assumed to
	 * be supported
	 */
	bool (*supported)(struct kvm_vcpu *vcpu);

	/**
	 * @reset: Reset the feature value irrespective whether feature is supported or not
	 *
	 * This callback is mandatory
	 */
	void (*reset)(struct kvm_vcpu *vcpu);

	/**
	 * @set: Set the feature value
	 *
	 * Return SBI_SUCCESS on success or an SBI error (SBI_ERR_*)
	 *
	 * This callback is mandatory
	 */
	long (*set)(struct kvm_vcpu *vcpu, struct kvm_sbi_fwft_config *conf,
		    bool one_reg_access, unsigned long value);

	/**
	 * @get: Get the feature current value
	 *
	 * Return SBI_SUCCESS on success or an SBI error (SBI_ERR_*)
	 *
	 * This callback is mandatory
	 */
	long (*get)(struct kvm_vcpu *vcpu, struct kvm_sbi_fwft_config *conf,
		    bool one_reg_access, unsigned long *value);
};

static const enum sbi_fwft_feature_t kvm_fwft_defined_features[] = {
	SBI_FWFT_MISALIGNED_EXC_DELEG,
	SBI_FWFT_LANDING_PAD,
	SBI_FWFT_SHADOW_STACK,
	SBI_FWFT_DOUBLE_TRAP,
	SBI_FWFT_PTE_AD_HW_UPDATING,
	SBI_FWFT_POINTER_MASKING_PMLEN,
};

static bool kvm_fwft_is_defined_feature(enum sbi_fwft_feature_t feature)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(kvm_fwft_defined_features); i++) {
		if (kvm_fwft_defined_features[i] == feature)
			return true;
	}

	return false;
}

static bool kvm_sbi_fwft_misaligned_delegation_supported(struct kvm_vcpu *vcpu)
{
	return misaligned_traps_can_delegate();
}

static void kvm_sbi_fwft_reset_misaligned_delegation(struct kvm_vcpu *vcpu)
{
	struct kvm_vcpu_config *cfg = &vcpu->arch.cfg;

	cfg->hedeleg &= ~MIS_DELEG;
}

static long kvm_sbi_fwft_set_misaligned_delegation(struct kvm_vcpu *vcpu,
					struct kvm_sbi_fwft_config *conf,
					bool one_reg_access, unsigned long value)
{
	struct kvm_vcpu_config *cfg = &vcpu->arch.cfg;

	if (value == 1) {
		cfg->hedeleg |= MIS_DELEG;
		if (!one_reg_access)
			csr_set(CSR_HEDELEG, MIS_DELEG);
	} else if (value == 0) {
		cfg->hedeleg &= ~MIS_DELEG;
		if (!one_reg_access)
			csr_clear(CSR_HEDELEG, MIS_DELEG);
	} else {
		return SBI_ERR_INVALID_PARAM;
	}

	return SBI_SUCCESS;
}

static long kvm_sbi_fwft_get_misaligned_delegation(struct kvm_vcpu *vcpu,
					struct kvm_sbi_fwft_config *conf,
					bool one_reg_access, unsigned long *value)
{
	struct kvm_vcpu_config *cfg = &vcpu->arch.cfg;

	*value = (cfg->hedeleg & MIS_DELEG) == MIS_DELEG;
	return SBI_SUCCESS;
}

#ifndef CONFIG_32BIT

static bool try_to_set_pmm(unsigned long value)
{
	csr_set(CSR_HENVCFG, value);
	return (csr_read_clear(CSR_HENVCFG, ENVCFG_PMM) & ENVCFG_PMM) == value;
}

static bool kvm_sbi_fwft_pointer_masking_pmlen_supported(struct kvm_vcpu *vcpu)
{
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);

	if (!riscv_isa_extension_available(vcpu->arch.isa, SMNPM))
		return false;

	fwft->have_vs_pmlen_7 = try_to_set_pmm(ENVCFG_PMM_PMLEN_7);
	fwft->have_vs_pmlen_16 = try_to_set_pmm(ENVCFG_PMM_PMLEN_16);

	return fwft->have_vs_pmlen_7 || fwft->have_vs_pmlen_16;
}

static void kvm_sbi_fwft_reset_pointer_masking_pmlen(struct kvm_vcpu *vcpu)
{
	vcpu->arch.cfg.henvcfg &= ~ENVCFG_PMM;
}

static long kvm_sbi_fwft_set_pointer_masking_pmlen(struct kvm_vcpu *vcpu,
						   struct kvm_sbi_fwft_config *conf,
						   bool one_reg_access, unsigned long value)
{
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);
	unsigned long pmm;

	switch (value) {
	case 0:
		pmm = ENVCFG_PMM_PMLEN_0;
		break;
	case 7:
		if (!fwft->have_vs_pmlen_7)
			return SBI_ERR_INVALID_PARAM;
		pmm = ENVCFG_PMM_PMLEN_7;
		break;
	case 16:
		if (!fwft->have_vs_pmlen_16)
			return SBI_ERR_INVALID_PARAM;
		pmm = ENVCFG_PMM_PMLEN_16;
		break;
	default:
		return SBI_ERR_INVALID_PARAM;
	}

	vcpu->arch.cfg.henvcfg &= ~ENVCFG_PMM;
	vcpu->arch.cfg.henvcfg |= pmm;

	/*
	 * Instead of waiting for vcpu_load/put() to update HENVCFG CSR,
	 * update here so that VCPU see's pointer masking mode change
	 * immediately.
	 */
	if (!one_reg_access)
		csr_write(CSR_HENVCFG, vcpu->arch.cfg.henvcfg);

	return SBI_SUCCESS;
}

static long kvm_sbi_fwft_get_pointer_masking_pmlen(struct kvm_vcpu *vcpu,
						   struct kvm_sbi_fwft_config *conf,
						   bool one_reg_access, unsigned long *value)
{
	switch (vcpu->arch.cfg.henvcfg & ENVCFG_PMM) {
	case ENVCFG_PMM_PMLEN_0:
		*value = 0;
		break;
	case ENVCFG_PMM_PMLEN_7:
		*value = 7;
		break;
	case ENVCFG_PMM_PMLEN_16:
		*value = 16;
		break;
	default:
		return SBI_ERR_FAILURE;
	}

	return SBI_SUCCESS;
}

#endif

static const struct kvm_sbi_fwft_feature features[] = {
	{
		.id = SBI_FWFT_MISALIGNED_EXC_DELEG,
		.first_reg_num = offsetof(struct kvm_riscv_sbi_fwft, misaligned_deleg.enable) /
				 sizeof(unsigned long),
		.supported = kvm_sbi_fwft_misaligned_delegation_supported,
		.reset = kvm_sbi_fwft_reset_misaligned_delegation,
		.set = kvm_sbi_fwft_set_misaligned_delegation,
		.get = kvm_sbi_fwft_get_misaligned_delegation,
	},
#ifndef CONFIG_32BIT
	{
		.id = SBI_FWFT_POINTER_MASKING_PMLEN,
		.first_reg_num = offsetof(struct kvm_riscv_sbi_fwft, pointer_masking.enable) /
				 sizeof(unsigned long),
		.supported = kvm_sbi_fwft_pointer_masking_pmlen_supported,
		.reset = kvm_sbi_fwft_reset_pointer_masking_pmlen,
		.set = kvm_sbi_fwft_set_pointer_masking_pmlen,
		.get = kvm_sbi_fwft_get_pointer_masking_pmlen,
	},
#endif
};

static const struct kvm_sbi_fwft_feature *kvm_sbi_fwft_regnum_to_feature(unsigned long reg_num)
{
	const struct kvm_sbi_fwft_feature *feature;
	int i;

	for (i = 0; i < ARRAY_SIZE(features); i++) {
		feature = &features[i];
		if (feature->first_reg_num <= reg_num && reg_num < (feature->first_reg_num + 3))
			return feature;
	}

	return NULL;
}

static struct kvm_sbi_fwft_config *
kvm_sbi_fwft_get_config(struct kvm_vcpu *vcpu, enum sbi_fwft_feature_t feature)
{
	int i;
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);

	for (i = 0; i < ARRAY_SIZE(features); i++) {
		if (fwft->configs[i].feature->id == feature)
			return &fwft->configs[i];
	}

	return NULL;
}

static int kvm_fwft_get_feature(struct kvm_vcpu *vcpu, u32 feature,
				struct kvm_sbi_fwft_config **conf)
{
	struct kvm_sbi_fwft_config *tconf;

	tconf = kvm_sbi_fwft_get_config(vcpu, feature);
	if (!tconf) {
		if (kvm_fwft_is_defined_feature(feature))
			return SBI_ERR_NOT_SUPPORTED;

		return SBI_ERR_DENIED;
	}

	if (!tconf->supported || !tconf->enabled)
		return SBI_ERR_NOT_SUPPORTED;

	*conf = tconf;

	return SBI_SUCCESS;
}

static int kvm_sbi_fwft_set(struct kvm_vcpu *vcpu, u32 feature,
			    unsigned long value, unsigned long flags)
{
	int ret;
	struct kvm_sbi_fwft_config *conf;

	ret = kvm_fwft_get_feature(vcpu, feature, &conf);
	if (ret)
		return ret;

	if ((flags & ~SBI_FWFT_SET_FLAG_LOCK) != 0)
		return SBI_ERR_INVALID_PARAM;

	if (conf->flags & SBI_FWFT_SET_FLAG_LOCK)
		return SBI_ERR_DENIED_LOCKED;

	conf->flags = flags;

	return conf->feature->set(vcpu, conf, false, value);
}

static int kvm_sbi_fwft_get(struct kvm_vcpu *vcpu, unsigned long feature,
			    unsigned long *value)
{
	int ret;
	struct kvm_sbi_fwft_config *conf;

	ret = kvm_fwft_get_feature(vcpu, feature, &conf);
	if (ret)
		return ret;

	return conf->feature->get(vcpu, conf, false, value);
}

static int kvm_sbi_ext_fwft_handler(struct kvm_vcpu *vcpu, struct kvm_run *run,
				    struct kvm_vcpu_sbi_return *retdata)
{
	int ret;
	struct kvm_cpu_context *cp = &vcpu->arch.guest_context;
	unsigned long funcid = cp->a6;

	switch (funcid) {
	case SBI_EXT_FWFT_SET:
		ret = kvm_sbi_fwft_set(vcpu, cp->a0, cp->a1, cp->a2);
		break;
	case SBI_EXT_FWFT_GET:
		ret = kvm_sbi_fwft_get(vcpu, cp->a0, &retdata->out_val);
		break;
	default:
		ret = SBI_ERR_NOT_SUPPORTED;
		break;
	}

	retdata->err_val = ret;

	return 0;
}

static int kvm_sbi_ext_fwft_init(struct kvm_vcpu *vcpu)
{
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);
	const struct kvm_sbi_fwft_feature *feature;
	struct kvm_sbi_fwft_config *conf;
	int i;

	fwft->configs = kzalloc_objs(struct kvm_sbi_fwft_config,
				     ARRAY_SIZE(features));
	if (!fwft->configs)
		return -ENOMEM;

	for (i = 0; i < ARRAY_SIZE(features); i++) {
		feature = &features[i];
		conf = &fwft->configs[i];
		if (feature->supported)
			conf->supported = feature->supported(vcpu);
		else
			conf->supported = true;

		conf->enabled = conf->supported;
		conf->feature = feature;
	}

	return 0;
}

static void kvm_sbi_ext_fwft_deinit(struct kvm_vcpu *vcpu)
{
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);

	kfree(fwft->configs);
}

static void kvm_sbi_ext_fwft_reset(struct kvm_vcpu *vcpu)
{
	struct kvm_sbi_fwft *fwft = vcpu_to_fwft(vcpu);
	const struct kvm_sbi_fwft_feature *feature;
	int i;

	for (i = 0; i < ARRAY_SIZE(features); i++) {
		fwft->configs[i].flags = 0;
		feature = &features[i];
		if (feature->reset)
			feature->reset(vcpu);
	}
}

static unsigned long kvm_sbi_ext_fwft_get_reg_count(struct kvm_vcpu *vcpu)
{
	unsigned long max_reg_count = sizeof(struct kvm_riscv_sbi_fwft) / sizeof(unsigned long);
	const struct kvm_sbi_fwft_feature *feature;
	struct kvm_sbi_fwft_config *conf;
	unsigned long reg, ret = 0;

	for (reg = 0; reg < max_reg_count; reg++) {
		feature = kvm_sbi_fwft_regnum_to_feature(reg);
		if (!feature)
			continue;

		conf = kvm_sbi_fwft_get_config(vcpu, feature->id);
		if (!conf || !conf->supported)
			continue;

		ret++;
	}

	return ret;
}

static int kvm_sbi_ext_fwft_get_reg_id(struct kvm_vcpu *vcpu, int index, u64 *reg_id)
{
	int reg, max_reg_count = sizeof(struct kvm_riscv_sbi_fwft) / sizeof(unsigned long);
	const struct kvm_sbi_fwft_feature *feature;
	struct kvm_sbi_fwft_config *conf;
	int idx = 0;

	for (reg = 0; reg < max_reg_count; reg++) {
		feature = kvm_sbi_fwft_regnum_to_feature(reg);
		if (!feature)
			continue;

		conf = kvm_sbi_fwft_get_config(vcpu, feature->id);
		if (!conf || !conf->supported)
			continue;

		if (index == idx) {
			*reg_id = KVM_REG_RISCV |
				  (IS_ENABLED(CONFIG_32BIT) ?
				   KVM_REG_SIZE_U32 : KVM_REG_SIZE_U64) |
				  KVM_REG_RISCV_SBI_STATE |
				  KVM_REG_RISCV_SBI_FWFT | reg;
			return 0;
		}

		idx++;
	}

	return -ENOENT;
}

static int kvm_sbi_ext_fwft_get_reg(struct kvm_vcpu *vcpu, unsigned long reg_num,
				    unsigned long reg_size, void *reg_val)
{
	const struct kvm_sbi_fwft_feature *feature;
	struct kvm_sbi_fwft_config *conf;
	unsigned long *value;
	int ret = 0;

	if (reg_size != sizeof(unsigned long))
		return -EINVAL;
	value = reg_val;

	feature = kvm_sbi_fwft_regnum_to_feature(reg_num);
	if (!feature)
		return -ENOENT;

	conf = kvm_sbi_fwft_get_config(vcpu, feature->id);
	if (!conf || !conf->supported)
		return -ENOENT;

	switch (reg_num - feature->first_reg_num) {
	case 0:
		*value = conf->enabled;
		break;
	case 1:
		*value = conf->flags;
		break;
	case 2:
		ret = conf->feature->get(vcpu, conf, true, value);
		break;
	default:
		return -ENOENT;
	}

	return sbi_err_map_linux_errno(ret);
}

static int kvm_sbi_ext_fwft_set_reg(struct kvm_vcpu *vcpu, unsigned long reg_num,
				    unsigned long reg_size, const void *reg_val)
{
	const struct kvm_sbi_fwft_feature *feature;
	struct kvm_sbi_fwft_config *conf;
	unsigned long value;
	int ret = 0;

	if (reg_size != sizeof(unsigned long))
		return -EINVAL;
	value = *(const unsigned long *)reg_val;

	feature = kvm_sbi_fwft_regnum_to_feature(reg_num);
	if (!feature)
		return -ENOENT;

	conf = kvm_sbi_fwft_get_config(vcpu, feature->id);
	if (!conf || !conf->supported)
		return -ENOENT;

	switch (reg_num - feature->first_reg_num) {
	case 0:
		switch (value) {
		case 0:
			conf->enabled = false;
			break;
		case 1:
			conf->enabled = true;
			break;
		default:
			return -EINVAL;
		}
		break;
	case 1:
		conf->flags = value & SBI_FWFT_SET_FLAG_LOCK;
		break;
	case 2:
		ret = conf->feature->set(vcpu, conf, true, value);
		break;
	default:
		return -ENOENT;
	}

	return sbi_err_map_linux_errno(ret);
}

const struct kvm_vcpu_sbi_extension vcpu_sbi_ext_fwft = {
	.extid_start = SBI_EXT_FWFT,
	.extid_end = SBI_EXT_FWFT,
	.handler = kvm_sbi_ext_fwft_handler,
	.init = kvm_sbi_ext_fwft_init,
	.deinit = kvm_sbi_ext_fwft_deinit,
	.reset = kvm_sbi_ext_fwft_reset,
	.state_reg_subtype = KVM_REG_RISCV_SBI_FWFT,
	.get_state_reg_count = kvm_sbi_ext_fwft_get_reg_count,
	.get_state_reg_id = kvm_sbi_ext_fwft_get_reg_id,
	.get_state_reg = kvm_sbi_ext_fwft_get_reg,
	.set_state_reg = kvm_sbi_ext_fwft_set_reg,
};
