root/drivers/acpi/arm64/ffh.c
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/acpi.h>
#include <linux/arm-smccc.h>
#include <linux/slab.h>

/*
 * Implements ARM64 specific callbacks to support ACPI FFH Operation Region as
 * specified in https://developer.arm.com/docs/den0048/latest
 */
struct acpi_ffh_data {
        struct acpi_ffh_info info;
        void (*invoke_ffh_fn)(unsigned long a0, unsigned long a1,
                              unsigned long a2, unsigned long a3,
                              unsigned long a4, unsigned long a5,
                              unsigned long a6, unsigned long a7,
                              struct arm_smccc_res *args,
                              struct arm_smccc_quirk *res);
        void (*invoke_ffh64_fn)(const struct arm_smccc_1_2_regs *args,
                                struct arm_smccc_1_2_regs *res);
};

int acpi_ffh_address_space_arch_setup(void *handler_ctxt, void **region_ctxt)
{
        enum arm_smccc_conduit conduit;
        struct acpi_ffh_data *ffh_ctxt;

        if (arm_smccc_get_version() < ARM_SMCCC_VERSION_1_2)
                return -EOPNOTSUPP;

        conduit = arm_smccc_1_1_get_conduit();
        if (conduit == SMCCC_CONDUIT_NONE) {
                pr_err("%s: invalid SMCCC conduit\n", __func__);
                return -EOPNOTSUPP;
        }

        ffh_ctxt = kzalloc_obj(*ffh_ctxt);
        if (!ffh_ctxt)
                return -ENOMEM;

        if (conduit == SMCCC_CONDUIT_SMC) {
                ffh_ctxt->invoke_ffh_fn = __arm_smccc_smc;
                ffh_ctxt->invoke_ffh64_fn = arm_smccc_1_2_smc;
        } else {
                ffh_ctxt->invoke_ffh_fn = __arm_smccc_hvc;
                ffh_ctxt->invoke_ffh64_fn = arm_smccc_1_2_hvc;
        }

        memcpy(ffh_ctxt, handler_ctxt, sizeof(ffh_ctxt->info));

        *region_ctxt = ffh_ctxt;
        return AE_OK;
}

static bool acpi_ffh_smccc_owner_allowed(u32 fid)
{
        int owner = ARM_SMCCC_OWNER_NUM(fid);

        if (owner == ARM_SMCCC_OWNER_STANDARD ||
            owner == ARM_SMCCC_OWNER_SIP || owner == ARM_SMCCC_OWNER_OEM)
                return true;

        return false;
}

int acpi_ffh_address_space_arch_handler(acpi_integer *value, void *region_context)
{
        int ret = 0;
        struct acpi_ffh_data *ffh_ctxt = region_context;

        if (ffh_ctxt->info.offset == 0) {
                /* SMC/HVC 32bit call */
                struct arm_smccc_res res;
                u32 a[8] = { 0 }, *ptr = (u32 *)value;

                if (!ARM_SMCCC_IS_FAST_CALL(*ptr) || ARM_SMCCC_IS_64(*ptr) ||
                    !acpi_ffh_smccc_owner_allowed(*ptr) ||
                    ffh_ctxt->info.length > 32) {
                        ret = AE_ERROR;
                } else {
                        int idx, len = ffh_ctxt->info.length >> 2;

                        for (idx = 0; idx < len; idx++)
                                a[idx] = *(ptr + idx);

                        ffh_ctxt->invoke_ffh_fn(a[0], a[1], a[2], a[3], a[4],
                                                a[5], a[6], a[7], &res, NULL);
                        memcpy(value, &res, sizeof(res));
                }

        } else if (ffh_ctxt->info.offset == 1) {
                /* SMC/HVC 64bit call */
                struct arm_smccc_1_2_regs *r = (struct arm_smccc_1_2_regs *)value;

                if (!ARM_SMCCC_IS_FAST_CALL(r->a0) || !ARM_SMCCC_IS_64(r->a0) ||
                    !acpi_ffh_smccc_owner_allowed(r->a0) ||
                    ffh_ctxt->info.length > sizeof(*r)) {
                        ret = AE_ERROR;
                } else {
                        ffh_ctxt->invoke_ffh64_fn(r, r);
                        memcpy(value, r, ffh_ctxt->info.length);
                }
        } else {
                ret = AE_ERROR;
        }

        return ret;
}