475 lines
12 KiB
C
475 lines
12 KiB
C
|
|
// SPDX-License-Identifier: GPL-2.0-only
|
||
|
|
/*
|
||
|
|
* Copyright (c) 2020-2021, The Linux Foundation. All rights reserved.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <linux/completion.h>
|
||
|
|
#include <linux/list.h>
|
||
|
|
#include <linux/kernel.h>
|
||
|
|
#include <linux/module.h>
|
||
|
|
#include <linux/notifier.h>
|
||
|
|
#include <linux/of.h>
|
||
|
|
#include <linux/platform_device.h>
|
||
|
|
#include <linux/slab.h>
|
||
|
|
#include <linux/cpumask.h>
|
||
|
|
#include <linux/ktime.h>
|
||
|
|
#include <linux/sched.h>
|
||
|
|
#include <linux/workqueue.h>
|
||
|
|
#include <linux/gunyah/gh_rm_drv.h>
|
||
|
|
#include <linux/cpu.h>
|
||
|
|
#include <linux/of_address.h>
|
||
|
|
#include <linux/qcom_scm.h>
|
||
|
|
#include <linux/firmware.h>
|
||
|
|
#include <linux/soc/qcom/mdt_loader.h>
|
||
|
|
|
||
|
|
#define MAX_LEN 256
|
||
|
|
#define DEFAULT_UNISO_TIMEOUT_MS 12000
|
||
|
|
#define NUM_RESERVED_CPUS 2
|
||
|
|
|
||
|
|
const static struct {
|
||
|
|
enum gh_vm_names val;
|
||
|
|
const char *str;
|
||
|
|
} conversion[] = {
|
||
|
|
{GH_PRIMARY_VM, "pvm"},
|
||
|
|
{GH_TRUSTED_VM, "trustedvm"},
|
||
|
|
{GH_CPUSYS_VM, "cpusys_vm"},
|
||
|
|
{GH_OEM_VM, "oemvm"},
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct kobj_type guestvm_kobj_type = {
|
||
|
|
.sysfs_ops = &kobj_sysfs_ops,
|
||
|
|
};
|
||
|
|
|
||
|
|
struct guestvm_loader_private {
|
||
|
|
struct notifier_block guestvm_nb;
|
||
|
|
struct completion vm_start;
|
||
|
|
struct kobject vm_loader_kobj;
|
||
|
|
struct device *dev;
|
||
|
|
ktime_t vm_isol_start;
|
||
|
|
char vm_name[MAX_LEN];
|
||
|
|
bool vm_loaded;
|
||
|
|
bool iso_needed;
|
||
|
|
int pas_id;
|
||
|
|
int vmid;
|
||
|
|
u8 vm_status;
|
||
|
|
u8 os_status;
|
||
|
|
u16 app_status;
|
||
|
|
struct timer_list guestvm_cpu_isolate_timer;
|
||
|
|
struct completion isolation_done;
|
||
|
|
struct work_struct unisolation_work;
|
||
|
|
cpumask_t guestvm_isolated_cpus;
|
||
|
|
cpumask_t guestvm_reserve_cpus;
|
||
|
|
u32 guestvm_unisolate_timeout;
|
||
|
|
};
|
||
|
|
|
||
|
|
static void guestvm_isolate_cpu(struct guestvm_loader_private *priv)
|
||
|
|
{
|
||
|
|
int cpu, ret;
|
||
|
|
|
||
|
|
for_each_cpu_and(cpu, &priv->guestvm_reserve_cpus, cpu_online_mask) {
|
||
|
|
ret = remove_cpu(cpu);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("fail to offline CPU%d. ret=%d\n", cpu, ret);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
pr_info("%s: offlined cpu : %d\n", __func__, cpu);
|
||
|
|
cpumask_set_cpu(cpu, &priv->guestvm_isolated_cpus);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void guestvm_unisolate_cpu(struct guestvm_loader_private *priv)
|
||
|
|
{
|
||
|
|
int i, ret;
|
||
|
|
|
||
|
|
for_each_cpu(i, &priv->guestvm_isolated_cpus) {
|
||
|
|
ret = add_cpu(i);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("fail to online CPU%d. ret=%d\n", i, ret);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
pr_info("%s: onlined cpu : %d\n", __func__, i);
|
||
|
|
|
||
|
|
cpumask_clear_cpu(i, &priv->guestvm_isolated_cpus);
|
||
|
|
}
|
||
|
|
|
||
|
|
del_timer(&priv->guestvm_cpu_isolate_timer);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void guestvm_unisolate_work(struct work_struct *work)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv;
|
||
|
|
|
||
|
|
priv = container_of(work, struct guestvm_loader_private, unisolation_work);
|
||
|
|
|
||
|
|
if (wait_for_completion_interruptible(&priv->isolation_done))
|
||
|
|
pr_err("%s: CPU unisolation is interrupted\n", __func__);
|
||
|
|
|
||
|
|
guestvm_unisolate_cpu(priv);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void guestvm_timer_callback(struct timer_list *t)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv;
|
||
|
|
|
||
|
|
priv = container_of(t, struct guestvm_loader_private,
|
||
|
|
guestvm_cpu_isolate_timer);
|
||
|
|
|
||
|
|
pr_err("%s: expired: VM app status not set\n", __func__);
|
||
|
|
complete(&priv->isolation_done);
|
||
|
|
}
|
||
|
|
|
||
|
|
static inline enum gh_vm_names get_gh_vm_name(const char *str)
|
||
|
|
{
|
||
|
|
int vmid;
|
||
|
|
|
||
|
|
for (vmid = 0; vmid < ARRAY_SIZE(conversion); ++vmid) {
|
||
|
|
if (!strcmp(str, conversion[vmid].str))
|
||
|
|
return conversion[vmid].val;
|
||
|
|
}
|
||
|
|
return GH_VM_MAX;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int guestvm_loader_nb_handler(struct notifier_block *this,
|
||
|
|
unsigned long cmd, void *data)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv;
|
||
|
|
struct gh_rm_notif_vm_status_payload *vm_status_payload = data;
|
||
|
|
u8 vm_status = vm_status_payload->vm_status;
|
||
|
|
u8 os_status = vm_status_payload->os_status;
|
||
|
|
u16 app_status = vm_status_payload->app_status;
|
||
|
|
u64 delta;
|
||
|
|
ktime_t now;
|
||
|
|
int ret;
|
||
|
|
|
||
|
|
priv = container_of(this, struct guestvm_loader_private, guestvm_nb);
|
||
|
|
|
||
|
|
if (cmd != GH_RM_NOTIF_VM_STATUS)
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
|
||
|
|
if (priv->vmid != vm_status_payload->vmid) {
|
||
|
|
dev_warn(priv->dev, "Expected a notification from vmid = %d, but received one from vmid = %d\n",
|
||
|
|
priv->vmid, vm_status_payload->vmid);
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Listen to STATUS_READY or STATUS_RUNNING notifications from RM.
|
||
|
|
* These notifications come from RM after PIL loading the VM images.
|
||
|
|
* Query GET_HYP_RESOURCES to populate other entities such as MessageQ
|
||
|
|
* and DBL.
|
||
|
|
*/
|
||
|
|
switch (vm_status) {
|
||
|
|
case GH_RM_VM_STATUS_READY:
|
||
|
|
priv->vm_status = GH_RM_VM_STATUS_READY;
|
||
|
|
ret = gh_rm_populate_hyp_res(vm_status_payload->vmid, priv->vm_name);
|
||
|
|
if (ret < 0) {
|
||
|
|
dev_err(priv->dev, "Failed to get hyp resources for vmid = %d ret = %d\n",
|
||
|
|
vm_status_payload->vmid, ret);
|
||
|
|
complete_all(&priv->vm_start);
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
}
|
||
|
|
ret = gh_rm_get_vm_id_info(priv->vmid);
|
||
|
|
if (ret < 0)
|
||
|
|
dev_err(priv->dev, "Couldn't obtain VM ID info.\n");
|
||
|
|
|
||
|
|
complete_all(&priv->vm_start);
|
||
|
|
break;
|
||
|
|
case GH_RM_VM_STATUS_RUNNING:
|
||
|
|
dev_info(priv->dev, "vm(%d) started running\n", vm_status_payload->vmid);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
dev_err(priv->dev, "Unknown notification receieved for vmid = %d vm_status = %d\n",
|
||
|
|
vm_status_payload->vmid, vm_status);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (priv->os_status != os_status) {
|
||
|
|
if (os_status == GH_RM_OS_STATUS_BOOT) {
|
||
|
|
priv->os_status = os_status;
|
||
|
|
now = ktime_get();
|
||
|
|
delta = ktime_to_ns(ktime_sub(now, priv->vm_isol_start));
|
||
|
|
dev_info(priv->dev, "VM(%d) booted in %lu ns\n",
|
||
|
|
priv->vmid, delta);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (priv->app_status != app_status) {
|
||
|
|
if (app_status == GH_RM_APP_STATUS_TUI_SERVICE_BOOT) {
|
||
|
|
priv->app_status = app_status;
|
||
|
|
now = ktime_get();
|
||
|
|
delta = ktime_to_ns(ktime_sub(now, priv->vm_isol_start));
|
||
|
|
dev_info(priv->dev, "Unisolating VM(%d) cpus after %lu ns: VM app status = %d\n",
|
||
|
|
priv->vmid, delta, app_status);
|
||
|
|
complete(&priv->isolation_done);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int vm_load(struct guestvm_loader_private *priv)
|
||
|
|
{
|
||
|
|
const struct firmware *fw;
|
||
|
|
char fw_name[32];
|
||
|
|
struct device_node *node;
|
||
|
|
struct resource res;
|
||
|
|
phys_addr_t phys;
|
||
|
|
ssize_t size;
|
||
|
|
void *virt;
|
||
|
|
int ret;
|
||
|
|
struct device *dev = priv->dev;
|
||
|
|
|
||
|
|
node = of_parse_phandle(dev->of_node, "memory-region", 0);
|
||
|
|
if (!node) {
|
||
|
|
dev_err(dev, "DT error getting \"memory-region\" property\n");
|
||
|
|
return -EINVAL;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = of_address_to_resource(node, 0, &res);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(dev, "error %d getting \"memory-region\" resource\n",
|
||
|
|
ret);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
scnprintf(fw_name, ARRAY_SIZE(fw_name), "%s.mdt", priv->vm_name);
|
||
|
|
|
||
|
|
ret = of_property_read_u32(dev->of_node, "qcom,pas-id", &priv->pas_id);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(dev, "error %d getting pas-id\n", ret);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = request_firmware(&fw, fw_name, dev);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(dev, "error %d requesting \"%s\"\n", ret, fw_name);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
phys = res.start;
|
||
|
|
size = (size_t)resource_size(&res);
|
||
|
|
virt = memremap(phys, size, MEMREMAP_WC);
|
||
|
|
if (!virt) {
|
||
|
|
dev_err(dev, "unable to remap firmware memory\n");
|
||
|
|
ret = -ENOMEM;
|
||
|
|
goto release_firmware;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = qcom_mdt_load(dev, fw, fw_name, priv->pas_id, virt, phys, size, NULL);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(dev, "error %d loading \"%s\"\n", ret, fw_name);
|
||
|
|
goto mem_unmap;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = qcom_scm_pas_auth_and_reset(priv->pas_id);
|
||
|
|
if (ret)
|
||
|
|
dev_err(dev, "error %d authenticating \"%s\"\n", ret, fw_name);
|
||
|
|
|
||
|
|
mem_unmap:
|
||
|
|
memunmap(virt);
|
||
|
|
release_firmware:
|
||
|
|
release_firmware(fw);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t guestvm_loader_start(struct kobject *kobj,
|
||
|
|
struct kobj_attribute *attr,
|
||
|
|
const char *buf,
|
||
|
|
size_t count)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv;
|
||
|
|
int ret = 0;
|
||
|
|
bool boot = false;
|
||
|
|
|
||
|
|
ret = kstrtobool(buf, &boot);
|
||
|
|
if (ret)
|
||
|
|
return -EINVAL;
|
||
|
|
|
||
|
|
priv = container_of(kobj, struct guestvm_loader_private,
|
||
|
|
vm_loader_kobj);
|
||
|
|
if (priv->vm_loaded) {
|
||
|
|
dev_err(priv->dev, "VM load has already been started\n");
|
||
|
|
return -EINVAL;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (boot) {
|
||
|
|
priv->vm_status = GH_RM_VM_STATUS_INIT;
|
||
|
|
ret = gh_rm_vm_alloc_vmid(get_gh_vm_name(priv->vm_name),
|
||
|
|
&priv->vmid);
|
||
|
|
if (ret < 0) {
|
||
|
|
dev_err(priv->dev, "Couldn't allocate VMID.\n");
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = vm_load(priv);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(priv->dev,
|
||
|
|
"vm_load failed with error %d\n", ret);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
priv->vm_loaded = true;
|
||
|
|
|
||
|
|
if (wait_for_completion_interruptible(&priv->vm_start)) {
|
||
|
|
dev_err(priv->dev, "VM start completion interrupted\n");
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
priv->vm_status = GH_RM_VM_STATUS_RUNNING;
|
||
|
|
|
||
|
|
priv->vm_isol_start = ktime_get();
|
||
|
|
if (priv->iso_needed) {
|
||
|
|
INIT_WORK(&priv->unisolation_work, guestvm_unisolate_work);
|
||
|
|
schedule_work(&priv->unisolation_work);
|
||
|
|
guestvm_isolate_cpu(priv);
|
||
|
|
mod_timer(&priv->guestvm_cpu_isolate_timer, jiffies +
|
||
|
|
msecs_to_jiffies(priv->guestvm_unisolate_timeout));
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = gh_rm_vm_set_time_base(priv->vmid);
|
||
|
|
if (ret)
|
||
|
|
dev_err(priv->dev, "Failed to set time base for vmid = %d ret = %d\n",
|
||
|
|
priv->vmid, ret);
|
||
|
|
|
||
|
|
ret = gh_rm_vm_start(priv->vmid);
|
||
|
|
if (ret)
|
||
|
|
dev_err(priv->dev, "VM start failed for vmid = %d ret = %d\n",
|
||
|
|
priv->vmid, ret);
|
||
|
|
}
|
||
|
|
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
static struct kobj_attribute guestvm_loader_attribute =
|
||
|
|
__ATTR(boot_guestvm, 0220, NULL, guestvm_loader_start);
|
||
|
|
|
||
|
|
static struct attribute *attrs[] = {
|
||
|
|
&guestvm_loader_attribute.attr,
|
||
|
|
NULL,
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct attribute_group attr_group = {
|
||
|
|
.attrs = attrs,
|
||
|
|
};
|
||
|
|
|
||
|
|
static int guestvm_loader_probe(struct platform_device *pdev)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv = NULL;
|
||
|
|
const char *sub_sys;
|
||
|
|
int ret = 0, i, reserve_cpus_len;
|
||
|
|
u32 reserve_cpus[NUM_RESERVED_CPUS] = {0};
|
||
|
|
|
||
|
|
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
|
||
|
|
if (!priv)
|
||
|
|
return -ENOMEM;
|
||
|
|
|
||
|
|
platform_set_drvdata(pdev, priv);
|
||
|
|
priv->dev = &pdev->dev;
|
||
|
|
|
||
|
|
ret = of_property_read_string(pdev->dev.of_node, "qcom,firmware-name",
|
||
|
|
&sub_sys);
|
||
|
|
if (ret)
|
||
|
|
return -EINVAL;
|
||
|
|
strscpy(priv->vm_name, sub_sys, sizeof(priv->vm_name));
|
||
|
|
|
||
|
|
ret = kobject_init_and_add(&priv->vm_loader_kobj, &guestvm_kobj_type,
|
||
|
|
kernel_kobj, "%s_%s", "load_guestvm", priv->vm_name);
|
||
|
|
|
||
|
|
if (ret) {
|
||
|
|
dev_err(&pdev->dev, "sysfs create and add failed\n");
|
||
|
|
ret = -ENOMEM;
|
||
|
|
goto error_return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = sysfs_create_group(&priv->vm_loader_kobj, &attr_group);
|
||
|
|
if (ret) {
|
||
|
|
dev_err(&pdev->dev, "sysfs create group failed %d\n", ret);
|
||
|
|
goto error_return;
|
||
|
|
}
|
||
|
|
|
||
|
|
init_completion(&priv->vm_start);
|
||
|
|
init_completion(&priv->isolation_done);
|
||
|
|
priv->guestvm_nb.notifier_call = guestvm_loader_nb_handler;
|
||
|
|
priv->guestvm_nb.priority = 1;
|
||
|
|
ret = gh_rm_register_notifier(&priv->guestvm_nb);
|
||
|
|
if (ret)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
ret = of_property_read_u32(pdev->dev.of_node, "qcom,vmid",
|
||
|
|
&priv->vmid);
|
||
|
|
if (ret)
|
||
|
|
dev_err(&pdev->dev, "Unable to get vmid from DT, ret=%d\n", ret);
|
||
|
|
|
||
|
|
priv->iso_needed = of_property_read_bool(pdev->dev.of_node,
|
||
|
|
"qcom,isolate-cpus");
|
||
|
|
if (!priv->iso_needed) {
|
||
|
|
pr_err("%s: no isolation needed for %s\n", __func__, priv->vm_name);
|
||
|
|
goto no_iso;
|
||
|
|
}
|
||
|
|
|
||
|
|
reserve_cpus_len = of_property_read_variable_u32_array(
|
||
|
|
pdev->dev.of_node,
|
||
|
|
"qcom,reserved-cpus",
|
||
|
|
reserve_cpus, 0, NUM_RESERVED_CPUS);
|
||
|
|
for (i = 0; i < reserve_cpus_len; i++)
|
||
|
|
if (reserve_cpus[i] < num_possible_cpus())
|
||
|
|
cpumask_set_cpu(reserve_cpus[i], &priv->guestvm_reserve_cpus);
|
||
|
|
|
||
|
|
ret = of_property_read_u32(pdev->dev.of_node, "qcom,unisolate-timeout-ms",
|
||
|
|
&priv->guestvm_unisolate_timeout);
|
||
|
|
if (ret) {
|
||
|
|
pr_warn("%s: no unisolate timeout specified\n", __func__);
|
||
|
|
priv->guestvm_unisolate_timeout = DEFAULT_UNISO_TIMEOUT_MS;
|
||
|
|
}
|
||
|
|
timer_setup(&priv->guestvm_cpu_isolate_timer, guestvm_timer_callback, 0);
|
||
|
|
|
||
|
|
no_iso:
|
||
|
|
priv->vm_status = GH_RM_VM_STATUS_NO_STATE;
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
error_return:
|
||
|
|
if (kobject_name(&priv->vm_loader_kobj) != NULL) {
|
||
|
|
kobject_del(&priv->vm_loader_kobj);
|
||
|
|
kobject_put(&priv->vm_loader_kobj);
|
||
|
|
|
||
|
|
memset(&priv->vm_loader_kobj, 0, sizeof(priv->vm_loader_kobj));
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int guestvm_loader_remove(struct platform_device *pdev)
|
||
|
|
{
|
||
|
|
struct guestvm_loader_private *priv = platform_get_drvdata(pdev);
|
||
|
|
|
||
|
|
if (priv->vm_loaded) {
|
||
|
|
qcom_scm_pas_shutdown(priv->pas_id);
|
||
|
|
priv->vm_loaded = false;
|
||
|
|
init_completion(&priv->vm_start);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (kobject_name(&priv->vm_loader_kobj) != NULL) {
|
||
|
|
kobject_del(&priv->vm_loader_kobj);
|
||
|
|
kobject_put(&priv->vm_loader_kobj);
|
||
|
|
|
||
|
|
memset(&priv->vm_loader_kobj, 0, sizeof(priv->vm_loader_kobj));
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static const struct of_device_id guestvm_loader_match_table[] = {
|
||
|
|
{ .compatible = "qcom,guestvm-loader" },
|
||
|
|
{},
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct platform_driver guestvm_loader_driver = {
|
||
|
|
.probe = guestvm_loader_probe,
|
||
|
|
.remove = guestvm_loader_remove,
|
||
|
|
.driver = {
|
||
|
|
.name = "qcom_guestvm_loader",
|
||
|
|
.of_match_table = guestvm_loader_match_table,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
module_platform_driver(guestvm_loader_driver);
|
||
|
|
MODULE_DESCRIPTION("Qualcomm Technologies, Inc. GuestVM loader");
|
||
|
|
MODULE_LICENSE("GPL v2");
|