From 75ee225c91f0801e930f22e47f72f1985fc4bf2c Mon Sep 17 00:00:00 2001 From: unknown <985400330@qq.com> Date: Sun, 25 Dec 2022 11:27:28 +0800 Subject: [PATCH 1/2] add 20221207-static-call-part3-analysis-static-call.md --- ...-static-call-part3-analysis-static-call.md | 792 ++++++++++++++++++ 1 file changed, 792 insertions(+) create mode 100644 articles/20221207-static-call-part3-analysis-static-call.md diff --git a/articles/20221207-static-call-part3-analysis-static-call.md b/articles/20221207-static-call-part3-analysis-static-call.md new file mode 100644 index 0000000..a403ace --- /dev/null +++ b/articles/20221207-static-call-part3-analysis-static-call.md @@ -0,0 +1,792 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [pangu]
+> Author: 牛工 - 通天塔 985400330@qq.com
+> Date: 2022/12/07
+> Revisor: Falcon ; iOSDevLog
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [【老师提案】Static Call 技术分析与 RISC-V 移植 · Issue #I5Y585 · 泰晓科技/RISCV-Linux - Gitee.com](https://gitee.com/tinylab/riscv-linux/issues/I5Y585)
+> Sponsor: PLCT Lab, ISCAS + +# Static call + +## 前言 + +在文章 [20221020-missing-features-tools-for-riscv-part2.md][004] 中,提到了 RISC-V 架构下缺失的内核功能:Avoiding retpolines with static calls。 + +该功能的提出,是有历史原因的: + +- 2018 年发现漏洞 Meltdown 和 Spectre +- 谷歌提出 Retpolines 解决了这个安全问题,但引入了 4% 的性能影响 +- 开发者们不断寻求解决方法:[Relief for retpoline pain][002] +- 2020 年使用 static calls 方法避免使用 retpolines,性能影响降低至 1.6%:[Avoiding retpolines with static calls][003] + +前两篇文章 + +1、讲了 Meltdown 和 Spectre + +2、讲了 retpolines 原理 + +本篇文章对 static call 进行分析,分析 static call 如何避免了 Meltdown 和 Spectre 两个漏洞,又怎么减小了对性能的影响。 + +## static call 分析 + +### static call 资料 + +#### static call 用法 + +[Static calls [LWN.net]][012] 关于 static call 的补丁介绍 + +> Static calls use code patching to hard-code function pointers into direct branch instructions. They give the flexibility of function pointers, but with improved performance. This is especially important for cases where retpolines would otherwise be used, as retpolines can significantly impact performance. + +静态调用使用代码补丁将函数指针硬编码为直接的分支指令。它们**放弃了函数指针的灵活性**,但提高了性能。这对于将使用 retpolins 的情况尤其重要,因为 retpolines 会显著影响性能。 + +> Implementation details: +> This requires some arch-specific code (CONFIG_HAVE_STATIC_CALL),Otherwise basic indirect calls are used (with function pointers). +> Each static_call() site calls into a trampoline associated with the name.The trampoline has a direct branch to the default function. +> Updates to a name will modify the trampoline's branch destination.If the arch has CONFIG_HAVE_STATIC_CALL_INLINE, then the call sites themselves will be patched at runtime to call the functions directly,rather than calling through the trampoline. This requires objtool or a compiler plugin to detect all the static_call() sites and annotate them in the static_call_sites section. + +实现细节: + +这需要一些特定于架构的代码(`CONFIG_HAVE_STATIC_CALL`),否则基础的间接调用会被使用(通过函数指针)。 + +每个 `static_call()` 站点都会调用与其名称相关的蹦床。蹦床会有一个直接的分支到默认函数中。 + +更新名称将修改蹦床的分支目的地。如果 arch 有 `CONFIG_HAVE_STATIC_CALL_INLINE`,那么为了直接调用函数,调用站点本身将在运行时被作为补丁打入,而不是通过调用蹦床。这需要 objtools 或编译器插件来检测所有 `static_call()` 站点,并在 `static_call_site` 段中注释它们。 + +```c +API overview: + + DECLARE_STATIC_CALL(key, func); // 声明外部静态调用 + DEFINE_STATIC_CALL(key, func); // 定义静态调用 + static_call(key, args...); // 实际调用 + static_call_update(key, func); // 更换 key 函数 + +Usage example: + + # Start with the following functions (with identical prototypes): + int func_a(int arg1, int arg2); + int func_b(int arg1, int arg2); + + # Define a 'my_key' reference, associated with func_a() by default + # 定义一个'my_key'引用,默认情况下与 func_a()关联 + DEFINE_STATIC_CALL(my_key, func_a); + + # Call func_a() + static_call(my_key, arg1, arg2); + + # Update 'my_key' to point to func_b() + static_call_update(my_key, func_b); + + # Call func_b() + static_call(my_key, arg1, arg2); +``` + +#### static call 如何预防幽灵攻击 + +以上讲了 static call 的用法,但无法看出 static call 是如何防御幽灵攻击的。 + +在了解 static call 如何预防幽灵攻击前,再重温一下 retpolines 是如何防止被幽灵攻击的。 + +retpolines 将间接调用转化成了一个复杂的代码序列,来防止幽灵攻击,但是同样带来了开销。 + +在上一篇文章 [articles/20221107-static-call-part2-retpoline.md · 泰晓科技/RISCV-Linux - 码云 - 开源中国(gitee.com)][007] 对 retpoline 进行了分析,采用的是反蹦床的方案,示意图如下: + +![img](images/static-call/image-20221123235854753.png) + +retpoline 实际部署参考资料:[retpoline: 原理与部署(terenceli.github.io)][020] + +总的来说,就是使用 retpolines 更换了以前的 JMP 指令,使再被预测时,直接被预测成了死循环,不再跳转至被常训练的执行分支。 + +[Linux Kernel 5.10 Introduces Static Calls to Prevent Speculative Execution Attacks - The New Stack][017] static call 是如何防御幽灵攻击的。 + +下面讲了 static call 如何防止幽灵攻击: + +> Since the inception of retpolines, kernel developers have been scrambling to find a better solution, one that doesn’t work from a location within writable memory where the indirect jumps can be found. +> +> That’s where static calls come into play. A static call uses a location in executable memory (instead of writable memory) that contains a jump instruction pointing to a target function. Executing a static call requires a call to the special location, which then jumps to the actual target. This is called a classic code trampoline and completely avoids the use of retpolines. + +自 retpolines 开始以来,内核开发人员一直在努力寻找更好的解决方案,这个解决方案不能在可写内存中的位置工作,因为那里可以找到间接跳转。 + +这就是静态调用发挥作用的地方。静态调用**使用可执行内存(而不是可写内存)**中的一个位置,该位置包含指向目标函数的跳转指令。执行静态调用需要调用特定位置,然后跳转到实际目标。这被称为经典代码蹦床,完全避免使用反蹦床。 + +下面举一个实例进一步解释为什么 static call 能够防止幽灵攻击。 + +``` +#include +#include + +typedef void (*function_point)(); + +void testfunction() +{ + printf("testfunc\n"); +} +int main() +{ + int i=0; + function_point main_function = testfunction; + for(i=0;i<100;i++) + main_function(); +} +``` + +以上代码就是在不断的训练 `main_function` 的跳转,当突然替换掉 `main_function` 的目标地址为非法地址(要攻击的地址)时,预测执行会继续跳转至 `main_function` 的地址执行,但后续 CPU 又会反应过来,而不会产生结果,但结果已经放置到了缓存当中。使用了 static call 之后,中途更换函数指针是可以的,但是无法更换成攻击者想要运行的那个函数指针,因为 static call 不再暴露函数指针。 + +以下内容引自:[Retpoline - caijiqhx notes][014] + +> 分支目标注入漏洞利用的五个组成元素: +> +> 1. 受害者拥有想要获取的秘密数据,对于 OS 内核,包括用户权限之外的任何数据。 +> 2. 攻击者需要引用秘密数据,通常是受害者地址空间的指针。受害者和攻击者之前通过公开信道传递指针,如系统调用接口。 +> 3. 在受害者执行的包含易被利用的间接分支部分,引用必须可用。 +> 4. 攻击者影响分支预测推测地错误预测并执行 gadget,通过侧信道(通常是缓存侧信道)泄露秘密数据。 +> 5. gadget 必须在推测窗口执行,当处理器发现预测错误就会关闭窗口。 + +static_call 避免了间接调用,直接不把 `function_point` 指针暴露出来,而是采用一个临时跳转地址放到**可执行内存**当中,该地址是公用的跳转地址,通过 update 进行跳转指令的替换,**不需要再存储函数指针**,也就不容易被幽灵攻击利用。 + +例如使用内核模块,获取其他模块操作函数的指针地址,再利用分支预测强行运行函数获取结果的方法就不好用了,因为其本身没有存储函数指针。 + +当前 static call 的方案不再使用反蹦床,而是使用固定的跳转地点,但是该跳转地点的代码可以动态调整的方案。 + +[Avoiding retpolines with static calls [LWN.net]][003] 使用 static calls 来避免 retpolines,可以有效提高性能。 + +### 补丁代码分析 + +#### Add basic static call infrastructure + +[[PATCH v2 02/13] static_call: Add basic static call infrastructure - Peter Zijlstra (kernel.org)][008] 第一次添加 static call 指令 + +先分析第一次 static 引入时的提交: +首先是 `include/linux/static_call_types.h`,做了一些宏定义,这些宏定义在 `include/linux/static_call.h` 有使用到。 + +```c +--- /dev/null ++++ b/include/linux/static_call_types.h +@@ -0,0 +1,15 @@ ++/* SPDX-License-Identifier: GPL-2.0 */ ++#ifndef _STATIC_CALL_TYPES_H ++#define _STATIC_CALL_TYPES_H ++ ++#include ++ ++#define STATIC_CALL_PREFIX ____static_call_ ++#define STATIC_CALL_PREFIX_STR __stringify(STATIC_CALL_PREFIX) /* 转换为单纯的字符串 */ ++ ++#define STATIC_CALL_NAME(name) __PASTE(STATIC_CALL_PREFIX, name) ++/* STATIC_CALL_NAME(name) 宏定义最终为:____static_call_“name” */ ++ ++#define STATIC_CALL_TRAMP(name) STATIC_CALL_NAME(name##_tramp) ++/* STATIC_CALL_TRAMP(name) 宏定义最终为:____static_call_“name”_tramp */ ++#define STATIC_CALL_TRAMP_STR(name) __stringify(STATIC_CALL_TRAMP(name)) /* 转换为单纯的字符串 */ ++ ++#endif /* _STATIC_CALL_TYPES_H */ +``` + +以下是 static_call 的具体实现 + +```c +--- /dev/null ++++ b/include/linux/static_call.h +@@ -0,0 +1,134 @@ ++/* SPDX-License-Identifier: GPL-2.0 */ ++#ifndef _LINUX_STATIC_CALL_H ++#define _LINUX_STATIC_CALL_H +.... ++#include ++#include ++#include ++ ++#ifdef CONFIG_HAVE_STATIC_CALL ++#include ++/* ++ * Either @site or @tramp can be NULL. ++ * site 和 trap 都不能为空。 ++ */ ++extern void arch_static_call_transform(void *site, void *tramp, void *func); // 外部声明,一般在各架构中实现。 ++#endif ++ ++// 声明 static call ++#define DECLARE_STATIC_CALL(name, func) \ ++ extern struct static_call_key STATIC_CALL_NAME(name); \ ++ extern typeof(func) STATIC_CALL_TRAMP(name) ++ ++#define static_call_update(name, func) \ ++({ \ ++ BUILD_BUG_ON(!__same_type(*(func), STATIC_CALL_TRAMP(name))); \ ++ __static_call_update(&STATIC_CALL_NAME(name), \ ++ &STATIC_CALL_TRAMP(name), func); \ ++}) // 封装一层 static_call_update ++ ++#if defined(CONFIG_HAVE_STATIC_CALL) ++ ++struct static_call_key { ++ void *func; ++}; // 定义一个函数指针 ++ ++ /* 封装,调用了前边的一些宏定义 */ ++#define DEFINE_STATIC_CALL(name, _func) \ ++ DECLARE_STATIC_CALL(name, _func); // 外部声明 \ ++ struct static_call_key STATIC_CALL_NAME(name) = { \ ++ .func = _func, \ ++ }; // 函数定义 \ ++ ARCH_DEFINE_STATIC_CALL_TRAMP(name, _func) // 各架构自行实现 ++ ++#define static_call(name) STATIC_CALL_TRAMP(name) ++ ++static inline ++void __static_call_update(struct static_call_key *key, void *tramp, void *func) ++{ ++ cpus_read_lock(); ++ WRITE_ONCE(key->func, func); // 防止被编译器优化的赋值 ++ arch_static_call_transform(NULL, tramp, func); // 架构中实现 ++ cpus_read_unlock(); ++} ++ ++#define EXPORT_STATIC_CALL(name) EXPORT_SYMBOL(STATIC_CALL_TRAMP(name)) ++#define EXPORT_STATIC_CALL_GPL(name) EXPORT_SYMBOL_GPL(STATIC_CALL_TRAMP(name)) ++ ++#else /* Generic implementation */ ++/* 通用的执行方法 */ ++struct static_call_key { ++ void *func; ++}; ++ ++#define DEFINE_STATIC_CALL(name, _func) \ ++ DECLARE_STATIC_CALL(name, _func); \ ++ struct static_call_key STATIC_CALL_NAME(name) = { \ ++ .func = _func, \ ++ } // 不指定架构实现 ++ ++#define static_call(name) \ ++ ((typeof(STATIC_CALL_TRAMP(name))*)(STATIC_CALL_NAME(name).func)) // 函数 ++ ++static inline ++void __static_call_update(struct static_call_key *key, void *tramp, void *func) ++{ ++ WRITE_ONCE(key->func, func); // 防止被编译器优化的赋值 ++} ++ ++#define EXPORT_STATIC_CALL(name) EXPORT_SYMBOL(STATIC_CALL_NAME(name)) ++#define EXPORT_STATIC_CALL_GPL(key) EXPORT_SYMBOL_GPL(STATIC_CALL_NAME(name)) ++ ++#endif /* CONFIG_HAVE_STATIC_CALL */ ++ ++#endif /* _LINUX_STATIC_CALL_H */ +``` + +以上完成了 static call 头文件的实现,做了很多的宏定义,定义了 USER API 的所有接口。 + +```c +API overview: + DECLARE_STATIC_CALL(key, func); // 声明外部静态调用 + DEFINE_STATIC_CALL(key, func); // 定义静态调用 + static_call(key, args...); // 实际调用 + static_call_update(key, func); // 更换 key 函数 +``` + +#### Add inline static call infrastructure + +[[PATCH v2 03/13] static_call: Add inline static call infrastructure - Peter Zijlstra (kernel.org)][009] 头文件添加完成后,又对 static_call.c 文件进行了实现。 + +> Add infrastructure for an arch-specific CONFIG_HAVE_STATIC_CALL_INLINE option, which is a faster version of CONFIG_HAVE_STATIC_CALL. At runtime, the static call sites are patched directly, rather than using the out-of-line trampolines. + +为特定架构的 `CONFIG_HAVE_STATIC_CALL_INLINE` 选项添加结构体,该选项是 `CONFIG_HAVE_STATIC_CALL` 的更快版本。在运行时,直接修补静态调用站点,而不是使用离线蹦床。 + +> Compared to out-of-line static calls, the performance benefits are more modest, but still measurable. Steven Rostedt did some tracepoint measurements: +> https://lkml.kernel.org/r/20181126155405.72b4f718@gandalf.local.home + +与行外静态调用相比,性能方面的好处比较有限,但仍然是可以衡量的。Steven Rostedt 做了一些跟踪测量: + + https://lkml.kernel.org/r/20181126155405.72b4f718@gandalf.local.home + +> This code is heavily inspired by the jump label code (aka "static jumps"), as some of the concepts are very similar.For more details, see the comments in include/linux/static_call.h. + +这段代码很大程度上受到跳转标签代码(又名“静态跳转”)的启发,因为其中一些概念非常相似。有关更多详细信息,请参阅 `include/linux/static_call.h` 中的注释。 + +``` + arch/Kconfig | 4 + include/asm-generic/vmlinux.lds.h | 7 + include/linux/module.h | 10 + + include/linux/static_call.h | 37 ++++ + include/linux/static_call_types.h | 9 + + kernel/Makefile | 1 + kernel/module.c | 5 + kernel/static_call.c | 302 ++++++++++++++++++++++++++++++++++++++ + 8 files changed, 374 insertions(+), 1 deletion(-) + create mode 100644 kernel/static_call.c +``` + +首先在 `vmlinux.lds.h` 中添加了代码,用于 vmlinux 生成 static call 的 data 段数据。 + +```c +--- a/include/asm-generic/vmlinux.lds.h ++++ b/include/asm-generic/vmlinux.lds.h +@@ -333,6 +333,12 @@ + KEEP(*(__jump_table)) \ + __stop___jump_table = .; + ++#define STATIC_CALL_DATA \ ++ . = ALIGN(8); \ ++ __start_static_call_sites = .; \ ++ KEEP(*(.static_call_sites)) \ ++ __stop_static_call_sites = .; ++ + /* + * Allow architectures to handle ro_after_init data on their + * own by defining an empty RO_AFTER_INIT_DATA. +@@ -342,6 +348,7 @@ + __start_ro_after_init = .; \ + *(.data..ro_after_init) \ + JUMP_TABLE_DATA \ ++ STATIC_CALL_DATA \ /* 添加 static call 数据段 */ + __end_ro_after_init = .; + #endif + +``` + +`module.h` 也新增了 static call 的结构体声明,用于 static call 站点的统计。 + +```c +--- a/include/linux/module.h ++++ b/include/linux/module.h +@@ -22,6 +22,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -476,6 +477,10 @@ struct module { + unsigned int num_ftrace_callsites; + unsigned long *ftrace_callsites; + #endif ++#ifdef CONFIG_HAVE_STATIC_CALL_INLINE ++ int num_static_call_sites; ++ struct static_call_site *static_call_sites; ++#endif + + #ifdef CONFIG_LIVEPATCH + bool klp; /* Is this a livepatch module? */ +@@ -732,6 +737,11 @@ static inline bool within_module(unsigne + { + return false; + } ++ ++static inline bool within_module_init(unsigned long addr, const struct module *mod) ++{ ++ return false; ++} + + /* Get/put a kernel symbol (calls should be symmetric) */ + #define symbol_get(x) ({ extern typeof(x) x __attribute__((weak)); &(x); }) +``` + +在 `static_call.h` 中,新增了关于内联型 static call 的函数及宏定义实现。 + +```c +--- a/include/linux/static_call.h ++++ b/include/linux/static_call.h +@@ -78,7 +78,42 @@ extern void arch_static_call_transform(v + &STATIC_CALL_TRAMP(name), func); \ + }) + +-#if defined(CONFIG_HAVE_STATIC_CALL) ++#ifdef CONFIG_HAVE_STATIC_CALL_INLINE ++ ++struct static_call_mod { ++ struct static_call_mod *next; ++ struct module *mod; /* for vmlinux, mod == NULL */ ++ struct static_call_site *sites; ++}; ++ ++struct static_call_key { ++ void *func; ++ struct static_call_mod *next; ++}; ++ ++extern void __static_call_update(struct static_call_key *key, void *tramp, void *func); ++extern int static_call_mod_init(struct module *mod); ++ ++#define DEFINE_STATIC_CALL(name, _func) \ ++ DECLARE_STATIC_CALL(name, _func); \ ++ struct static_call_key STATIC_CALL_NAME(name) = { \ ++ .func = _func, \ ++ .next = NULL, \ ++ }; \ ++ __ADDRESSABLE(STATIC_CALL_NAME(name)); \ ++ ARCH_DEFINE_STATIC_CALL_TRAMP(name, _func) ++ ++#define static_call(name) STATIC_CALL_TRAMP(name) ++ ++#define EXPORT_STATIC_CALL(name) \ ++ EXPORT_SYMBOL(STATIC_CALL_NAME(name)); \ ++ EXPORT_SYMBOL(STATIC_CALL_TRAMP(name)) ++ ++#define EXPORT_STATIC_CALL_GPL(name) \ ++ EXPORT_SYMBOL_GPL(STATIC_CALL_NAME(name)); \ ++ EXPORT_SYMBOL_GPL(STATIC_CALL_TRAMP(name)) ++ ++#elif defined(CONFIG_HAVE_STATIC_CALL) + + struct static_call_key { + void *func; +--- a/include/linux/static_call_types.h ++++ b/include/linux/static_call_types.h +@@ -12,4 +12,13 @@ + #define STATIC_CALL_TRAMP(name) STATIC_CALL_NAME(name##_tramp) + #define STATIC_CALL_TRAMP_STR(name) __stringify(STATIC_CALL_TRAMP(name)) + ++/* ++ * The static call site table needs to be created by external tooling (objtool ++ * or a compiler plugin). ++ */ ++struct static_call_site { ++ s32 addr; ++ s32 key; ++}; ++ + #endif /* _STATIC_CALL_TYPES_H */ +``` + +最后是 `static_call.c` 的实现。 + +```c +--- /dev/null ++++ b/kernel/static_call.c +@@ -0,0 +1,302 @@ ++// SPDX-License-Identifier: GPL-2.0 ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++extern struct static_call_site __start_static_call_sites[], ++ __stop_static_call_sites[]; ++ ++static bool static_call_initialized; ++ ++#define STATIC_CALL_INIT 1UL ++ ++/* mutex to protect key modules/sites */ ++static DEFINE_MUTEX(static_call_mutex); ++ +... ++static inline void *static_call_addr(struct static_call_site *site) ++{ ++ return (void *)((long)site->addr + (long)&site->addr); ++} // 调用站点 ++ ++ ++static inline struct static_call_key *static_call_key(const struct static_call_site *site) ++{ ++ return (struct static_call_key *) ++ (((long)site->key + (long)&site->key) & ~STATIC_CALL_INIT); ++} ++ ++/* These assume the key is word-aligned. */ ++static inline bool static_call_is_init(struct static_call_site *site) ++{ ++ return ((long)site->key + (long)&site->key) & STATIC_CALL_INIT; ++} // 判断 static call 是否已经经过初始化 ++ ++static inline void static_call_set_init(struct static_call_site *site) ++{ ++ site->key = ((long)static_call_key(site) | STATIC_CALL_INIT) - ++ (long)&site->key; ++} // 对 static call 进行初始化 ++ ++static int static_call_site_cmp(const void *_a, const void *_b) ++{ +... ++} // 比较站点 ++ ++static void static_call_site_swap(void *_a, void *_b, int size) ++{ +... ++} // 站点交换 ++ ++static inline void static_call_sort_entries(struct static_call_site *start, ++ struct static_call_site *stop) ++{ ++ sort(start, stop - start, sizeof(struct static_call_site), ++ static_call_site_cmp, static_call_site_swap); ++} // 站点排序 ++ ++void __static_call_update(struct static_call_key *key, void *tramp, void *func) ++{ // static call 升级 ++ struct static_call_site *site, *stop; ++ struct static_call_mod *site_mod; ++ ++ cpus_read_lock(); ++ static_call_lock(); ++ ++ if (key->func == func) // 如果 static call key 函数与要升级的函数一致,则不升级。 ++ goto done; ++ ++ key->func = func; // 赋值函数 ++ ++ arch_static_call_transform(NULL, tramp, func);// 架构相关实现 ++ ++ /* ++ * If uninitialized, we'll not update the callsites, but they still ++ * point to the trampoline and we just patched that. ++ */ ++ if (WARN_ON_ONCE(!static_call_initialized))// 未初始化 ++ goto done; ++ ++ for (site_mod = key->next; site_mod; site_mod = site_mod->next) { ++ if (!site_mod->sites) { ++ /* ++ * This can happen if the static call key is defined in ++ * a module which doesn't use it. ++ */ ++ continue; ++ } // 将站点添加到链表 ++ ++ stop = __stop_static_call_sites; // 停止站点 ++ ++#ifdef CONFIG_MODULES ++ if (site_mod->mod) { ++ stop = site_mod->mod->static_call_sites + ++ site_mod->mod->num_static_call_sites; ++ } // 模块化设置 ++#endif ++ ++ for (site = site_mod->sites; ++ site < stop && static_call_key(site) == key; site++) { ++ void *site_addr = static_call_addr(site); ++ struct module *mod = site_mod->mod; ++ ++ if (static_call_is_init(site)) { ++ /* ++ * Don't write to call sites which were in ++ * initmem and have since been freed. ++ */ ++ if (!mod && system_state >= SYSTEM_RUNNING) ++ continue; ++ if (mod && !within_module_init((unsigned long)site_addr, mod)) ++ continue; ++ } ++ ++ if (!kernel_text_address((unsigned long)site_addr)) { ++ WARN_ONCE(1, "can't patch static call site at %pS", ++ site_addr); ++ continue; ++ } ++ ++ arch_static_call_transform(site_addr, NULL, func); ++ } // 遍历所有站点,对所有站点进行转换 ++ } ++ ++done: ++ static_call_unlock(); ++ cpus_read_unlock(); ++} ++EXPORT_SYMBOL_GPL(__static_call_update); ++ ++static int __static_call_init(struct module *mod, ++ struct static_call_site *start, ++ struct static_call_site *stop) ++{ ++ struct static_call_site *site; ++ struct static_call_key *key, *prev_key = NULL; ++ struct static_call_mod *site_mod; ++ ++ if (start == stop) ++ return 0; ++ ++ static_call_sort_entries(start, stop); // 对所有站点进行排序 ++ ++ for (site = start; site < stop; site++) { ++ void *site_addr = static_call_addr(site); // 对 site 进行地址转换 ++ ++ if ((mod && within_module_init((unsigned long)site_addr, mod)) || ++ (!mod && init_section_contains(site_addr, 1))) ++ static_call_set_init(site); // 初始化站点 ++ ++ key = static_call_key(site); ++ if (key != prev_key) { ++ prev_key = key; ++ ++ site_mod = kzalloc(sizeof(*site_mod), GFP_KERNEL); ++ if (!site_mod) ++ return -ENOMEM; ++ ++ site_mod->mod = mod; ++ site_mod->sites = site; ++ site_mod->next = key->next; ++ key->next = site_mod; ++ } ++ ++ arch_static_call_transform(site_addr, NULL, key->func); ++ } ++ ++ return 0; ++} ++ +``` + +#### Add inline static call implementation for x86-64(给 x86 添加实现) + +给 x86 添加实现是 static call 移植的最重要的一步,前边的架构实现暂时未能分析的十分透彻,在 x86 单独的架构实现中,再进行深入了解,分析完成之后,再返回来看架构实现。 + +[[PATCH v2 05/13] x86/static_call: Add out-of-line static call implementation - Peter Zijlstra (kernel.org)][010] x86 的 static call 从无到有的补丁。 + +[[PATCH v2 06/13] x86/static_call: Add inline static call implementation for x86-64 - Peter Zijlstra (kernel.org)][011] 优化,新增 inline static call + +提交记录 + +> Add the x86 out-of-line static call implementation. +> +> For each key, a permanent trampoline is created which is the destination for all static +> calls for the given key. +> +> The trampoline has a direct jump which gets patched by static_call_update() when the destination function changes. + +添加 x86 离线静态调用实现。 + +对于每个键,将创建一个永久蹦床,它是给定键的所有静态调用的目的地。 + +蹦床有一个直接跳转,当目标函数改变时,通过 `static_call_update()` 打补丁跳转到改变后的目标函数。 + +``` + arch/x86/Kconfig | 1 + + arch/x86/include/asm/static_call.h | 22 ++++++++++++++++++++++ + arch/x86/kernel/Makefile | 1 + + arch/x86/kernel/static_call.c | 31 +++++++++++++++++++++++++++++++ + 4 files changed, 55 insertions(+) + create mode 100644 arch/x86/include/asm/static_call.h + create mode 100644 arch/x86/kernel/static_call.c +``` + +Kconfig 和 Makefile 是编译相关的实现,不进行分析。 + +`arch/x86/include/asm/static_call.h` 是在 x86 架构上第一次实现 + +```c +--- /dev/null ++++ b/arch/x86/include/asm/static_call.h +@@ -0,0 +1,22 @@ ++/* SPDX-License-Identifier: GPL-2.0 */ ++#ifndef _ASM_STATIC_CALL_H ++#define _ASM_STATIC_CALL_H ++ ++#include ++ ++/* ++ * For CONFIG_HAVE_STATIC_CALL, this is a permanent trampoline which ++ * does a direct jump to the function. The direct jump gets patched by ++ * static_call_update(). ++ */ ++ /* 这是一个永久的,可以直接跳转到某函数的蹦床,直接跳转的地址通过 static_call_update() 修改 */ ++#define ARCH_DEFINE_STATIC_CALL_TRAMP(name, func) \ ++ asm(".pushsection .text, \"ax\" \n" \ // 把下述代码添加到 text 段当中 ++ ".align 4 \n" \ // 按 4 个字节的倍数对齐下一个符号,空隙默认用 0 来填充 ++ ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \ // 声明为外部程序可访问的标签 ++ STATIC_CALL_TRAMP_STR(name) ": \n" \ // 标号 ++ " jmp.d32 " #func " \n" \ // 跳转至 func 地址,并执行 ++ ".type " STATIC_CALL_TRAMP_STR(name) ", @function \n" \ // 将符号 STATIC_CALL_TRAMP_STR(name) 的 type 属性设为 function。 ++ ".size " STATIC_CALL_TRAMP_STR(name) ", . - " STATIC_CALL_TRAMP_STR(name) " \n" \ // 当前地址 - 标号地址,即整个函数的大小 ++ ".popsection \n") // 添加代码结束 ++ ++#endif /* _ASM_STATIC_CALL_H */ +``` + + 以上代码的分析,参考了吴老师的这篇文章:[吴章金:通过操作 Section 为 Linux ELF 程序新增数据][005] + +汇编指令参考:[arm 汇编指令 - cogitoergosum - 博客园(cnblogs.com)][018]、[arm 汇编语法 - 简书(jianshu.com)][019] + +`arch/x86/kernel/static_call.c` 第一次进行实现的代码非常少。 + +`text_poke_bp` 指令参考资料:[eBPF 动态观测之指令跳板 | fuweid][006] + +`text_gen_insn` 指令参考资料:[探秘 ftrace - Kernel Exploring (gitbook.io)][016] + +```c +--- /dev/null ++++ b/arch/x86/kernel/static_call.c +@@ -0,0 +1,31 @@ ++// SPDX-License-Identifier: GPL-2.0 ++#include ++#include ++#include ++#include ++ ++static void __static_call_transform(void *insn, u8 opcode, void *func) ++{ ++ const void *code = text_gen_insn(opcode, (long)insn, (long)func); // 生成一个 insn+func 新的指令,用于后续的跳转 ++ ++ if (WARN_ONCE(*(u8 *)insn != opcode, ++ "unexpected static call insn opcode 0x%x at %pS\n", ++ opcode, insn)) ++ return; ++ ++ if (memcmp(insn, code, CALL_INSN_SIZE) == 0) // 内存比较,如果指令一致,则不再执行替换 ++ return; ++ ++ text_poke_bp(insn, code, CALL_INSN_SIZE, NULL); // insn 地址指令升级为 code,并通知所有 CPU ++} ++ ++void arch_static_call_transform(void *site, void *tramp, void *func) // 封装代码,并加锁 ++{ ++ mutex_lock(&text_mutex); ++ ++ if (tramp) ++ __static_call_transform(tramp, JMP32_INSN_OPCODE, func); // 调用替换函数,生成新的跳转指令。 ++ ++ mutex_unlock(&text_mutex); ++} ++EXPORT_SYMBOL_GPL(arch_static_call_transform); +``` + +### static 应用 + +static call 应用场景分析参考文章:[Linux static_call - caijiqhx notes][015] + +## 小结 + +本文讲了 static call 的预防幽灵攻击的原理,以及 static call 补丁的实现,对代码进行了分析注释,最后附上了一篇对于 static call 在内核中实际应用的一篇文章,本文在 static call 整体架构的分析还存在不足,感兴趣的读者可以尝试再分析一下,下一步作者将对 Risc-V 的 static call 进行移植。 + +## 参考资料 + +- [Static calls [LWN.net]][012] +- [articles/20221107-static-call-part2-retpoline.md · 泰晓科技/RISCV-Linux - 码云 - 开源中国(gitee.com)][007] +- [retpoline: 原理与部署(terenceli.github.io)][020] +- [Linux Kernel 5.10 Introduces Static Calls to Prevent Speculative Execution Attacks - The New Stack][017] +- [Retpoline - caijiqhx notes][014] +- [Avoiding retpolines with static calls [LWN.net]][003] +- [[PATCH v2 02/13] static_call: Add basic static call infrastructure - Peter Zijlstra (kernel.org)][008] +- [[PATCH v2 03/13] static_call: Add inline static call infrastructure - Peter Zijlstra (kernel.org)][009] +- [[PATCH v2 05/13] x86/static_call: Add out-of-line static call implementation - Peter Zijlstra (kernel.org)][010] +- [[PATCH v2 06/13] x86/static_call: Add inline static call implementation for x86-64 - Peter Zijlstra (kernel.org)][011] +- [吴章金:通过操作 Section 为 Linux ELF 程序新增数据 - 腾讯云开发者社区 - 腾讯云(tencent.com)][005] +- [arm 汇编指令 - cogitoergosum - 博客园(cnblogs.com)][018] +- [arm 汇编语法 - 简书(jianshu.com)][019] +- [eBPF 动态观测之指令跳板 | fuweid][006] +- [探秘 ftrace - Kernel Exploring (gitbook.io)][016] +- [Linux static_call - caijiqhx notes][015] + +[002]: https://lwn.net/Articles/774743/ +[003]: https://lwn.net/Articles/815908/ +[004]: https://gitee.com/nfk1996/riscv-linux/blob/a6b5ac6a507c106bb9471cbc06f8d43e9f912411/articles/20221020-missing-features-tools-for-riscv-part2.md +[005]: https://cloud.tencent.com/developer/article/1544362 +[006]: https://fuweid.com/post/2022-bpf-kprobe-fentry-poke/ +[007]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20221107-static-call-part2-retpoline.md +[008]: https://lore.kernel.org/lkml/20191007083830.64667428.5@infradead.org/ +[009]: https://lore.kernel.org/lkml/20191007083830.70301561.0@infradead.org/ +[010]: https://lore.kernel.org/lkml/20191007083830.81563732.6@infradead.org/ +[011]: https://lore.kernel.org/lkml/20191007083830.87232371.5@infradead.org/ +[012]: https://lwn.net/Articles/771209/ +[014]: https://notes.caijiqhx.top/ucas/linux_kernel/retpoline/ +[015]: https://notes.caijiqhx.top/ucas/linux_kernel/static_call/ +[016]: https://richardweiyang-2.gitbook.io/kernel-exploring/00-index-3/04-ftrace_internal?from=timeline +[017]: https://thenewstack.io/linux-kernel-5-10-introduces-static-calls-to-prevent-speculative-execution-attacks/ +[018]: https://www.cnblogs.com/hzijone/p/12005813.html +[019]: https://www.jianshu.com/p/1edc652351f4 +[020]: http://terenceli.github.io/技术/2018/03/24/retpoline -- Gitee From fdb23ecd1991e566640eebf18e9f6d9fcf808171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=80=9A=E5=A4=A9=E5=A1=94?= <985400330@qq.com> Date: Tue, 27 Dec 2022 07:22:23 +0000 Subject: [PATCH 2/2] Apply all suggestions from code review --- ...-static-call-part3-analysis-static-call.md | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/articles/20221207-static-call-part3-analysis-static-call.md b/articles/20221207-static-call-part3-analysis-static-call.md index a403ace..10bc341 100644 --- a/articles/20221207-static-call-part3-analysis-static-call.md +++ b/articles/20221207-static-call-part3-analysis-static-call.md @@ -6,7 +6,7 @@ > Proposal: [【老师提案】Static Call 技术分析与 RISC-V 移植 · Issue #I5Y585 · 泰晓科技/RISCV-Linux - Gitee.com](https://gitee.com/tinylab/riscv-linux/issues/I5Y585)
> Sponsor: PLCT Lab, ISCAS -# Static call +# Static call 系列(3):如何避免 Meltdown 和 Spectre 并减少对性能的影响 ## 前言 @@ -19,11 +19,7 @@ - 开发者们不断寻求解决方法:[Relief for retpoline pain][002] - 2020 年使用 static calls 方法避免使用 retpolines,性能影响降低至 1.6%:[Avoiding retpolines with static calls][003] -前两篇文章 - -1、讲了 Meltdown 和 Spectre - -2、讲了 retpolines 原理 +前两篇文章,讲了 Meltdown 和 Spectre 以及 retpolines 原理。 本篇文章对 static call 进行分析,分析 static call 如何避免了 Meltdown 和 Spectre 两个漏洞,又怎么减小了对性能的影响。 @@ -33,11 +29,11 @@ #### static call 用法 -[Static calls [LWN.net]][012] 关于 static call 的补丁介绍 +[Static calls [LWN.net]][012] 关于 static call 的补丁介绍如下: > Static calls use code patching to hard-code function pointers into direct branch instructions. They give the flexibility of function pointers, but with improved performance. This is especially important for cases where retpolines would otherwise be used, as retpolines can significantly impact performance. -静态调用使用代码补丁将函数指针硬编码为直接的分支指令。它们**放弃了函数指针的灵活性**,但提高了性能。这对于将使用 retpolins 的情况尤其重要,因为 retpolines 会显著影响性能。 +静态调用使用代码补丁将函数指针硬编码为直接的分支指令。它们 **放弃了函数指针的灵活性**,但提高了性能。这对于将使用 retpolins 的情况尤其重要,因为 retpolines 会显著影响性能。 > Implementation details: > This requires some arch-specific code (CONFIG_HAVE_STATIC_CALL),Otherwise basic indirect calls are used (with function pointers). @@ -57,7 +53,7 @@ API overview: DECLARE_STATIC_CALL(key, func); // 声明外部静态调用 DEFINE_STATIC_CALL(key, func); // 定义静态调用 - static_call(key, args...); // 实际调用 + static_call(key, args...); // 实际调用 static_call_update(key, func); // 更换 key 函数 Usage example: @@ -80,7 +76,7 @@ Usage example: static_call(my_key, arg1, arg2); ``` -#### static call 如何预防幽灵攻击 +## Static Calls 原理 - 如何预防幽灵攻击 以上讲了 static call 的用法,但无法看出 static call 是如何防御幽灵攻击的。 @@ -88,7 +84,7 @@ Usage example: retpolines 将间接调用转化成了一个复杂的代码序列,来防止幽灵攻击,但是同样带来了开销。 -在上一篇文章 [articles/20221107-static-call-part2-retpoline.md · 泰晓科技/RISCV-Linux - 码云 - 开源中国(gitee.com)][007] 对 retpoline 进行了分析,采用的是反蹦床的方案,示意图如下: +在上一篇文章 [20221107-static-call-part2-retpoline.md][007] 对 retpoline 进行了分析,采用的是反蹦床的方案,示意图如下: ![img](images/static-call/image-20221123235854753.png) @@ -96,9 +92,9 @@ retpoline 实际部署参考资料:[retpoline: 原理与部署(terenceli.git 总的来说,就是使用 retpolines 更换了以前的 JMP 指令,使再被预测时,直接被预测成了死循环,不再跳转至被常训练的执行分支。 -[Linux Kernel 5.10 Introduces Static Calls to Prevent Speculative Execution Attacks - The New Stack][017] static call 是如何防御幽灵攻击的。 +这篇文章:[Linux Kernel 5.10 Introduces Static Calls to Prevent Speculative Execution Attacks - The New Stack][017] 介绍了 static call 是如何防御幽灵攻击的。 -下面讲了 static call 如何防止幽灵攻击: +其中讲了 static call 如何防止幽灵攻击: > Since the inception of retpolines, kernel developers have been scrambling to find a better solution, one that doesn’t work from a location within writable memory where the indirect jumps can be found. > @@ -124,12 +120,12 @@ int main() { int i=0; function_point main_function = testfunction; - for(i=0;i<100;i++) + for(i = 0; i < 100; i++) main_function(); } ``` -以上代码就是在不断的训练 `main_function` 的跳转,当突然替换掉 `main_function` 的目标地址为非法地址(要攻击的地址)时,预测执行会继续跳转至 `main_function` 的地址执行,但后续 CPU 又会反应过来,而不会产生结果,但结果已经放置到了缓存当中。使用了 static call 之后,中途更换函数指针是可以的,但是无法更换成攻击者想要运行的那个函数指针,因为 static call 不再暴露函数指针。 +以上代码就是在不断地训练 `main_function` 的跳转,当突然替换掉 `main_function` 的目标地址为非法地址(要攻击的地址)时,预测执行会继续跳转至 `main_function` 的地址执行,但后续 CPU 又会反应过来,而不会产生结果,但结果已经放置到了缓存当中。使用了 static call 之后,中途更换函数指针是可以的,但是无法更换成攻击者想要运行的那个函数指针,因为 static call 不再暴露函数指针。 以下内容引自:[Retpoline - caijiqhx notes][014] @@ -147,7 +143,7 @@ static_call 避免了间接调用,直接不把 `function_point` 指针暴露 当前 static call 的方案不再使用反蹦床,而是使用固定的跳转地点,但是该跳转地点的代码可以动态调整的方案。 -[Avoiding retpolines with static calls [LWN.net]][003] 使用 static calls 来避免 retpolines,可以有效提高性能。 +[Avoiding retpolines with static calls [LWN.net]][003] 介绍了如何使用 static calls 来避免 retpolines,可以有效提高性能。 ### 补丁代码分析 @@ -294,7 +290,7 @@ API overview: > Compared to out-of-line static calls, the performance benefits are more modest, but still measurable. Steven Rostedt did some tracepoint measurements: > https://lkml.kernel.org/r/20181126155405.72b4f718@gandalf.local.home -与行外静态调用相比,性能方面的好处比较有限,但仍然是可以衡量的。Steven Rostedt 做了一些跟踪测量: +与非内联静态调用相比,性能方面的好处比较有限,但仍然是可以衡量的。Steven Rostedt 做了一些跟踪测量: https://lkml.kernel.org/r/20181126155405.72b4f718@gandalf.local.home @@ -634,11 +630,13 @@ API overview: #### Add inline static call implementation for x86-64(给 x86 添加实现) -给 x86 添加实现是 static call 移植的最重要的一步,前边的架构实现暂时未能分析的十分透彻,在 x86 单独的架构实现中,再进行深入了解,分析完成之后,再返回来看架构实现。 +给 x86 添加实现是 static call 移植的最重要的一步,前边的架构实现暂时未能分析得十分透彻,在 x86 单独的架构实现中,再进行深入了解,分析完成之后,再返回来看架构实现。 -[[PATCH v2 05/13] x86/static_call: Add out-of-line static call implementation - Peter Zijlstra (kernel.org)][010] x86 的 static call 从无到有的补丁。 +- [[PATCH v2 05/13] x86/static_call: Add out-of-line static call implementation - Peter Zijlstra (kernel.org)][010] + - x86 的 static call 从无到有的补丁。 -[[PATCH v2 06/13] x86/static_call: Add inline static call implementation for x86-64 - Peter Zijlstra (kernel.org)][011] 优化,新增 inline static call +- [[PATCH v2 06/13] x86/static_call: Add inline static call implementation for x86-64 - Peter Zijlstra (kernel.org)][011] + - 优化,新增 inline static call 提交记录 @@ -686,27 +684,25 @@ Kconfig 和 Makefile 是编译相关的实现,不进行分析。 + */ + /* 这是一个永久的,可以直接跳转到某函数的蹦床,直接跳转的地址通过 static_call_update() 修改 */ +#define ARCH_DEFINE_STATIC_CALL_TRAMP(name, func) \ -+ asm(".pushsection .text, \"ax\" \n" \ // 把下述代码添加到 text 段当中 -+ ".align 4 \n" \ // 按 4 个字节的倍数对齐下一个符号,空隙默认用 0 来填充 -+ ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \ // 声明为外部程序可访问的标签 -+ STATIC_CALL_TRAMP_STR(name) ": \n" \ // 标号 -+ " jmp.d32 " #func " \n" \ // 跳转至 func 地址,并执行 ++ asm(".pushsection .text, \"ax\" \n" \ // 把下述代码添加到 text 段当中 ++ ".align 4 \n" \ // 按 4 个字节的倍数对齐下一个符号,空隙默认用 0 来填充 ++ ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \ // 声明为外部程序可访问的标签 ++ STATIC_CALL_TRAMP_STR(name) ": \n" \ // 标号 ++ " jmp.d32 " #func " \n" \ // 跳转至 func 地址,并执行 + ".type " STATIC_CALL_TRAMP_STR(name) ", @function \n" \ // 将符号 STATIC_CALL_TRAMP_STR(name) 的 type 属性设为 function。 + ".size " STATIC_CALL_TRAMP_STR(name) ", . - " STATIC_CALL_TRAMP_STR(name) " \n" \ // 当前地址 - 标号地址,即整个函数的大小 -+ ".popsection \n") // 添加代码结束 ++ ".popsection \n") // 添加代码结束 + +#endif /* _ASM_STATIC_CALL_H */ ``` - 以上代码的分析,参考了吴老师的这篇文章:[吴章金:通过操作 Section 为 Linux ELF 程序新增数据][005] +以上代码的分析,参考了如下相关资料: -汇编指令参考:[arm 汇编指令 - cogitoergosum - 博客园(cnblogs.com)][018]、[arm 汇编语法 - 简书(jianshu.com)][019] - -`arch/x86/kernel/static_call.c` 第一次进行实现的代码非常少。 - -`text_poke_bp` 指令参考资料:[eBPF 动态观测之指令跳板 | fuweid][006] - -`text_gen_insn` 指令参考资料:[探秘 ftrace - Kernel Exploring (gitbook.io)][016] +- [吴章金:通过操作 Section 为 Linux ELF 程序新增数据][005] +- [arm 汇编指令 - cogitoergosum - 博客园(cnblogs.com)][018] +- [arm 汇编语法 - 简书(jianshu.com)][019] +- [eBPF 动态观测之指令跳板 | fuweid][006] (`text_poke_bp`) +- [探秘 ftrace - Kernel Exploring (gitbook.io)][016] (`text_gen_insn`) ```c --- /dev/null @@ -751,7 +747,7 @@ static call 应用场景分析参考文章:[Linux static_call - caijiqhx notes ## 小结 -本文讲了 static call 的预防幽灵攻击的原理,以及 static call 补丁的实现,对代码进行了分析注释,最后附上了一篇对于 static call 在内核中实际应用的一篇文章,本文在 static call 整体架构的分析还存在不足,感兴趣的读者可以尝试再分析一下,下一步作者将对 Risc-V 的 static call 进行移植。 +本文讲了 static call 的预防幽灵攻击的原理,以及 static call 补丁的实现,对代码进行了分析注释,最后附上了一篇对于 static call 在内核中实际应用的一篇文章,本文在 static call 整体架构的分析还存在不足,感兴趣的读者可以尝试再分析一下,下一步作者将对 RISC-V 的 static call 进行移植。 ## 参考资料 -- Gitee