| /* |
| * Copyright (C) 2013 Google, Inc. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <asm/compiler.h> |
| #include <linux/delay.h> |
| #include <linux/of.h> |
| #include <linux/of_platform.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| #include <linux/stat.h> |
| #include <linux/string.h> |
| #include <linux/trusty/smcall.h> |
| #include <linux/trusty/sm_err.h> |
| #include <linux/trusty/trusty.h> |
| |
| struct trusty_state { |
| struct mutex smc_lock; |
| struct atomic_notifier_head notifier; |
| char *version_str; |
| }; |
| |
| #ifdef CONFIG_ARM64 |
| #define SMC_ARG0 "x0" |
| #define SMC_ARG1 "x1" |
| #define SMC_ARG2 "x2" |
| #define SMC_ARG3 "x3" |
| #define SMC_ARCH_EXTENSION "" |
| #define SMC_REGISTERS_TRASHED "x4","x5","x6","x7","x8","x9","x10","x11", \ |
| "x12","x13","x14","x15","x16","x17" |
| #else |
| #define SMC_ARG0 "r0" |
| #define SMC_ARG1 "r1" |
| #define SMC_ARG2 "r2" |
| #define SMC_ARG3 "r3" |
| #define SMC_ARCH_EXTENSION ".arch_extension sec\n" |
| #define SMC_REGISTERS_TRASHED "ip" |
| #endif |
| |
| static inline ulong smc(ulong r0, ulong r1, ulong r2, ulong r3) |
| { |
| register ulong _r0 asm(SMC_ARG0) = r0; |
| register ulong _r1 asm(SMC_ARG1) = r1; |
| register ulong _r2 asm(SMC_ARG2) = r2; |
| register ulong _r3 asm(SMC_ARG3) = r3; |
| |
| asm volatile( |
| __asmeq("%0", SMC_ARG0) |
| __asmeq("%1", SMC_ARG1) |
| __asmeq("%2", SMC_ARG2) |
| __asmeq("%3", SMC_ARG3) |
| __asmeq("%4", SMC_ARG0) |
| __asmeq("%5", SMC_ARG1) |
| __asmeq("%6", SMC_ARG2) |
| __asmeq("%7", SMC_ARG3) |
| SMC_ARCH_EXTENSION |
| "smc #0" /* switch to secure world */ |
| : "=r" (_r0), "=r" (_r1), "=r" (_r2), "=r" (_r3) |
| : "r" (_r0), "r" (_r1), "r" (_r2), "r" (_r3) |
| : SMC_REGISTERS_TRASHED); |
| return _r0; |
| } |
| |
| s32 trusty_fast_call32(struct device *dev, u32 smcnr, u32 a0, u32 a1, u32 a2) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| BUG_ON(!s); |
| BUG_ON(!SMC_IS_FASTCALL(smcnr)); |
| BUG_ON(SMC_IS_SMC64(smcnr)); |
| |
| return smc(smcnr, a0, a1, a2); |
| } |
| EXPORT_SYMBOL(trusty_fast_call32); |
| |
| #ifdef CONFIG_64BIT |
| s64 trusty_fast_call64(struct device *dev, u64 smcnr, u64 a0, u64 a1, u64 a2) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| BUG_ON(!s); |
| BUG_ON(!SMC_IS_FASTCALL(smcnr)); |
| BUG_ON(!SMC_IS_SMC64(smcnr)); |
| |
| return smc(smcnr, a0, a1, a2); |
| } |
| #endif |
| |
| static ulong trusty_std_call_inner(struct device *dev, ulong smcnr, |
| ulong a0, ulong a1, ulong a2) |
| { |
| ulong ret; |
| int retry = 5; |
| |
| dev_dbg(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx)\n", |
| __func__, smcnr, a0, a1, a2); |
| while (true) { |
| ret = smc(smcnr, a0, a1, a2); |
| if ((int)ret != SM_ERR_BUSY || !retry) |
| break; |
| |
| dev_dbg(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx) returned busy, retry\n", |
| __func__, smcnr, a0, a1, a2); |
| retry--; |
| } |
| |
| return ret; |
| } |
| |
| static ulong trusty_std_call_helper(struct device *dev, ulong smcnr, |
| ulong a0, ulong a1, ulong a2) |
| { |
| ulong ret; |
| int sleep_time = 1; |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| while (true) { |
| local_irq_disable(); |
| atomic_notifier_call_chain(&s->notifier, TRUSTY_CALL_PREPARE, |
| NULL); |
| ret = trusty_std_call_inner(dev, smcnr, a0, a1, a2); |
| atomic_notifier_call_chain(&s->notifier, TRUSTY_CALL_RETURNED, |
| NULL); |
| local_irq_enable(); |
| |
| if ((int)ret != SM_ERR_BUSY) |
| break; |
| |
| if (sleep_time == 256) |
| dev_warn(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx) returned busy\n", |
| __func__, smcnr, a0, a1, a2); |
| dev_dbg(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx) returned busy, wait %d ms\n", |
| __func__, smcnr, a0, a1, a2, sleep_time); |
| |
| msleep(sleep_time); |
| if (sleep_time < 1000) |
| sleep_time <<= 1; |
| |
| dev_dbg(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx) retry\n", |
| __func__, smcnr, a0, a1, a2); |
| } |
| |
| if (sleep_time > 256) |
| dev_warn(dev, "%s(0x%lx 0x%lx 0x%lx 0x%lx) busy cleared\n", |
| __func__, smcnr, a0, a1, a2); |
| |
| return ret; |
| } |
| |
| s32 trusty_std_call32(struct device *dev, u32 smcnr, u32 a0, u32 a1, u32 a2) |
| { |
| int ret; |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| BUG_ON(SMC_IS_FASTCALL(smcnr)); |
| BUG_ON(SMC_IS_SMC64(smcnr)); |
| |
| mutex_lock(&s->smc_lock); |
| |
| dev_dbg(dev, "%s(0x%x 0x%x 0x%x 0x%x) started\n", |
| __func__, smcnr, a0, a1, a2); |
| |
| ret = trusty_std_call_helper(dev, smcnr, a0, a1, a2); |
| while (ret == SM_ERR_INTERRUPTED) { |
| dev_dbg(dev, "%s(0x%x 0x%x 0x%x 0x%x) interrupted\n", |
| __func__, smcnr, a0, a1, a2); |
| ret = trusty_std_call_helper(dev, SMC_SC_RESTART_LAST, 0, 0, 0); |
| } |
| dev_dbg(dev, "%s(0x%x 0x%x 0x%x 0x%x) returned 0x%x\n", |
| __func__, smcnr, a0, a1, a2, ret); |
| |
| mutex_unlock(&s->smc_lock); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL(trusty_std_call32); |
| |
| int trusty_call_notifier_register(struct device *dev, struct notifier_block *n) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| return atomic_notifier_chain_register(&s->notifier, n); |
| } |
| EXPORT_SYMBOL(trusty_call_notifier_register); |
| |
| int trusty_call_notifier_unregister(struct device *dev, |
| struct notifier_block *n) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| return atomic_notifier_chain_unregister(&s->notifier, n); |
| } |
| EXPORT_SYMBOL(trusty_call_notifier_unregister); |
| |
| static int trusty_remove_child(struct device *dev, void *data) |
| { |
| platform_device_unregister(to_platform_device(dev)); |
| return 0; |
| } |
| |
| ssize_t trusty_version_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| return scnprintf(buf, PAGE_SIZE, "%s\n", s->version_str); |
| } |
| |
| DEVICE_ATTR(trusty_version, S_IRUSR, trusty_version_show, NULL); |
| |
| const char *trusty_version_str_get(struct device *dev) |
| { |
| struct trusty_state *s = platform_get_drvdata(to_platform_device(dev)); |
| |
| return s->version_str; |
| } |
| EXPORT_SYMBOL(trusty_version_str_get); |
| |
| static void trusty_init_version(struct trusty_state *s, struct device *dev) |
| { |
| int ret; |
| int i; |
| int version_str_len; |
| |
| ret = trusty_fast_call32(dev, SMC_FC_GET_VERSION_STR, -1, 0, 0); |
| if (ret <= 0) |
| goto err_get_size; |
| |
| version_str_len = ret; |
| |
| s->version_str = kmalloc(version_str_len + 1, GFP_KERNEL); |
| for (i = 0; i < version_str_len; i++) { |
| ret = trusty_fast_call32(dev, SMC_FC_GET_VERSION_STR, i, 0, 0); |
| if (ret < 0) |
| goto err_get_char; |
| s->version_str[i] = ret; |
| } |
| s->version_str[i] = '\0'; |
| |
| dev_info(dev, "trusty version: %s\n", s->version_str); |
| |
| ret = device_create_file(dev, &dev_attr_trusty_version); |
| if (ret) |
| goto err_create_file; |
| return; |
| |
| err_create_file: |
| err_get_char: |
| kfree(s->version_str); |
| s->version_str = NULL; |
| err_get_size: |
| dev_err(dev, "failed to get version: %d\n", ret); |
| } |
| |
| static int trusty_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct trusty_state *s; |
| struct device_node *node = pdev->dev.of_node; |
| |
| if (!node) { |
| dev_err(&pdev->dev, "of_node required\n"); |
| return -EINVAL; |
| } |
| |
| s = kzalloc(sizeof(*s), GFP_KERNEL); |
| if (!s) { |
| ret = -ENOMEM; |
| goto err_allocate_state; |
| } |
| mutex_init(&s->smc_lock); |
| ATOMIC_INIT_NOTIFIER_HEAD(&s->notifier); |
| platform_set_drvdata(pdev, s); |
| |
| trusty_init_version(s, &pdev->dev); |
| |
| ret = of_platform_populate(pdev->dev.of_node, NULL, NULL, &pdev->dev); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "Failed to add children: %d\n", ret); |
| goto err_add_children; |
| } |
| |
| return 0; |
| |
| err_add_children: |
| if (s->version_str) { |
| device_remove_file(&pdev->dev, &dev_attr_trusty_version); |
| kfree(s->version_str); |
| } |
| device_for_each_child(&pdev->dev, NULL, trusty_remove_child); |
| mutex_destroy(&s->smc_lock); |
| kfree(s); |
| err_allocate_state: |
| return ret; |
| } |
| |
| static int trusty_remove(struct platform_device *pdev) |
| { |
| struct trusty_state *s = platform_get_drvdata(pdev); |
| |
| device_for_each_child(&pdev->dev, NULL, trusty_remove_child); |
| mutex_destroy(&s->smc_lock); |
| if (s->version_str) { |
| device_remove_file(&pdev->dev, &dev_attr_trusty_version); |
| kfree(s->version_str); |
| } |
| kfree(s); |
| return 0; |
| } |
| |
| static const struct of_device_id trusty_of_match[] = { |
| { .compatible = "android,trusty-smc-v1", }, |
| {}, |
| }; |
| |
| static struct platform_driver trusty_driver = { |
| .probe = trusty_probe, |
| .remove = trusty_remove, |
| .driver = { |
| .name = "trusty", |
| .owner = THIS_MODULE, |
| .of_match_table = trusty_of_match, |
| }, |
| }; |
| |
| static int __init trusty_driver_init(void) |
| { |
| return platform_driver_register(&trusty_driver); |
| } |
| |
| static void __exit trusty_driver_exit(void) |
| { |
| platform_driver_unregister(&trusty_driver); |
| } |
| |
| subsys_initcall(trusty_driver_init); |
| module_exit(trusty_driver_exit); |