[PATCH] [PATCH V5]ARM64: SCS: Add gcc plugin to support Shadow Call Stack

Dan Li
2021-10-15T22:33:27Z

This patch supports gcc-based SCS protection on ARM64 by adding a plugin.

For each function that x30 will be pushed onto the stack during execution,
this plugin, similar to gcc's pac implementation, will normally:
1) insert "str x30, [x18], #8" at the beginning of function's prologue
2) insert "ldr x30, [x18, #-8]!" immediately before function's
epilogue return/sibling calls

If pac is enabled, scs push/pop will be inserted between paciasp/autiasp.

At present, this patch has been successfully compiled in the following
gcc versions based on defconfig with kernel 5.14 and startup normally
with commands:

make ARCH=arm64 defconfig
./scripts/config -e CONFIG_GCC_PLUGINS -e CONFIG_SHADOW_CALL_STACK \
-e CONFIG_GCC_PLUGIN_SHADOW_CALL_STACK
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

Tested gcc version:
* 6.3.1
* 7.3.1
* 7.5.0
* 8.2.1
* 9.2.0
* 10.3.1

A similar feature request has also been sent to gcc.
Link: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102768

Signed-off-by: Dan Li <ashimida@linux.alibaba.com>
Acked-by: Nick Desaulniers <ndesaulniers@google.com>

---
V5:
- add gcc feature request's URL in commit message

V4:
- change CC_HAVE_SHADOW_CALL_STACK def_bool

V3:
- fix incorrect description of the compilation option

V2:
- fix incorrect config dependency
- tested against DYNAMIC_FTRACE_WITH_REGS
- add support for sibling call
- add support for ARM64_PTR_AUTH_KERNEL/ARM64_BTI_KERNEL

FYI:
- The kernel compiled by Linaro GCC 7.3-2018.05 runs for more than 10
days, ltp> 24 hours without crash.
- This function can be used to test whether the shadow stack is effective:
//noinline void __noscs scs_test(void)
noinline void scs_test(void)
{
register unsigned long *sp asm("sp");
unsigned long * lr = sp + 1;

asm volatile("":::"x30");
*lr = 0;
}

when compiled with:
CONFIG_DYNAMIC_FTRACE_WITH_REGS=y
CONFIG_ARM64_PTR_AUTH_KERNEL=y
CONFIG_ARM64_BTI_KERNEL=y

ffff800010013b60 <scs_test>:
ffff800010013b60: d503245f bti c
ffff800010013b64: d503201f nop
ffff800010013b68: d503201f nop
ffff800010013b6c: d503233f paciasp
ffff800010013b70: f800865e str x30, [x18], #8
ffff800010013b74: a9bf7bfd stp x29, x30, [sp, #-16]!
ffff800010013b78: 910003fd mov x29, sp
ffff800010013b7c: 910003e0 mov x0, sp
ffff800010013b80: f900041f str xzr, [x0, #8]
ffff800010013b84: a8c17bfd ldp x29, x30, [sp], #16
ffff800010013b88: f85f8e5e ldr x30, [x18, #-8]!
ffff800010013b8c: d50323bf autiasp
ffff800010013b90: d65f03c0 ret

If SCS protection is enabled, this function will return normally.
If the function has __noscs attribute (scs disabled), it will crash due to 0
address access.

Makefile | 20 ++-
arch/Kconfig | 13 +-
arch/arm64/Kconfig | 4 +-
include/linux/compiler-gcc.h | 4 +
scripts/Makefile.gcc-plugins | 7 +
scripts/gcc-plugins/Kconfig | 9 +
scripts/gcc-plugins/arm64_scs_plugin.c | 299 +++++++++++++++++++++++++++++++++
scripts/gcc-plugins/gcc-common.h | 4 +
8 files changed, 346 insertions(+), 14 deletions(-)
create mode 100644 scripts/gcc-plugins/arm64_scs_plugin.c

diff --git a/Makefile b/Makefile
index 61741e9..8039e61 100644
--- a/Makefile
+++ b/Makefile
@@ -923,12 +923,6 @@ KBUILD_CFLAGS_KERNEL += -ffunction-sections -fdata-sections
LDFLAGS_vmlinux += --gc-sections
endif

-ifdef CONFIG_SHADOW_CALL_STACK
-CC_FLAGS_SCS := -fsanitize=shadow-call-stack
-KBUILD_CFLAGS += $(CC_FLAGS_SCS)
-export CC_FLAGS_SCS
-endif
-
ifdef CONFIG_LTO_CLANG
ifdef CONFIG_LTO_CLANG_THIN
CC_FLAGS_LTO := -flto=thin -fsplit-lto-unit
@@ -1034,6 +1028,20 @@ include-$(CONFIG_GCC_PLUGINS) += scripts/Makefile.gcc-plugins

include $(addprefix $(srctree)/, $(include-y))

+ifdef CONFIG_SHADOW_CALL_STACK
+
+ifdef CONFIG_CC_IS_CLANG
+CC_FLAGS_SCS := -fsanitize=shadow-call-stack
+endif
+
+ifdef CONFIG_CC_IS_GCC
+CC_FLAGS_SCS := $(ENABLE_SHADOW_CALL_STACK_PLUGIN)
+endif
+
+KBUILD_CFLAGS += $(CC_FLAGS_SCS)
+export CC_FLAGS_SCS
+endif
+
# scripts/Makefile.gcc-plugins is intentionally included last.
# Do not add $(call cc-option,...) below this line. When you build the kernel
# from the clean source tree, the GCC plugins do not exist at this point.
diff --git a/arch/Kconfig b/arch/Kconfig
index 98db634..1065cd0 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -593,14 +593,15 @@ config ARCH_SUPPORTS_SHADOW_CALL_STACK
switching.

config SHADOW_CALL_STACK
- bool "Clang Shadow Call Stack"
- depends on CC_IS_CLANG && ARCH_SUPPORTS_SHADOW_CALL_STACK
+ bool "Shadow Call Stack"
+ depends on ARCH_SUPPORTS_SHADOW_CALL_STACK
depends on DYNAMIC_FTRACE_WITH_REGS || !FUNCTION_GRAPH_TRACER
help
- This option enables Clang's Shadow Call Stack, which uses a
- shadow stack to protect function return addresses from being
- overwritten by an attacker. More information can be found in
- Clang's documentation:
+ This option enables Shadow Call Stack(supported as a compiler
+ option in the case of clang, supported as a plugin in the case
+ of gcc), which uses a shadow stack to protect function return
+ addresses from being overwritten by an attacker.
+ More information can be found in Clang's documentation:

https://clang.llvm.org/docs/ShadowCallStack.html

diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index 62c3c1d..8aaa16e 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -81,7 +81,7 @@ config ARM64
select ARCH_SUPPORTS_DEBUG_PAGEALLOC
select ARCH_SUPPORTS_HUGETLBFS
select ARCH_SUPPORTS_MEMORY_FAILURE
- select ARCH_SUPPORTS_SHADOW_CALL_STACK if CC_HAVE_SHADOW_CALL_STACK
+ select ARCH_SUPPORTS_SHADOW_CALL_STACK if (CC_HAVE_SHADOW_CALL_STACK || GCC_PLUGIN_SHADOW_CALL_STACK)
select ARCH_SUPPORTS_LTO_CLANG if CPU_LITTLE_ENDIAN
select ARCH_SUPPORTS_LTO_CLANG_THIN
select ARCH_SUPPORTS_CFI_CLANG
@@ -1062,7 +1062,7 @@ config ARCH_HAS_FILTER_PGPROT

# Supported by clang >= 7.0
config CC_HAVE_SHADOW_CALL_STACK
- def_bool $(cc-option, -fsanitize=shadow-call-stack -ffixed-x18)
+ def_bool CC_IS_CLANG || $(cc-option, -fsanitize=shadow-call-stack -ffixed-x18)

config PARAVIRT
bool "Enable paravirtualization code"
diff --git a/include/linux/compiler-gcc.h b/include/linux/compiler-gcc.h
index cb9217f..426c8e5 100644
--- a/include/linux/compiler-gcc.h
+++ b/include/linux/compiler-gcc.h
@@ -50,6 +50,10 @@
#define __latent_entropy __attribute__((latent_entropy))
#endif

+#if defined(SHADOW_CALL_STACK_PLUGIN) && !defined(__CHECKER__)
+#define __noscs __attribute__((no_shadow_call_stack))
+#endif
+
/*
* calling noreturn functions, __builtin_unreachable() and __builtin_trap()
* confuse the stack allocation in gcc, leading to overly large stack
diff --git a/scripts/Makefile.gcc-plugins b/scripts/Makefile.gcc-plugins
index 952e468..b45bd8c 100644
--- a/scripts/Makefile.gcc-plugins
+++ b/scripts/Makefile.gcc-plugins
@@ -46,6 +46,13 @@ ifdef CONFIG_GCC_PLUGIN_ARM_SSP_PER_TASK
endif
export DISABLE_ARM_SSP_PER_TASK_PLUGIN

+gcc-plugin-$(CONFIG_GCC_PLUGIN_SHADOW_CALL_STACK) += arm64_scs_plugin.so
+gcc-plugin-cflags-$(CONFIG_GCC_PLUGIN_SHADOW_CALL_STACK) \
+ += -DSHADOW_CALL_STACK_PLUGIN
+ifdef CONFIG_GCC_PLUGIN_SHADOW_CALL_STACK
+ ENABLE_SHADOW_CALL_STACK_PLUGIN += -fplugin-arg-arm64_scs_plugin-enable
+endif
+
# All the plugin CFLAGS are collected here in case a build target needs to
# filter them out of the KBUILD_CFLAGS.
GCC_PLUGINS_CFLAGS := $(strip $(addprefix -fplugin=$(objtree)/scripts/gcc-plugins/, $(gcc-plugin-y)) $(gcc-plugin-cflags-y))
diff --git a/scripts/gcc-plugins/Kconfig b/scripts/gcc-plugins/Kconfig
index ab9eb4c..92f7f76 100644
--- a/scripts/gcc-plugins/Kconfig
+++ b/scripts/gcc-plugins/Kconfig
@@ -19,6 +19,15 @@ menuconfig GCC_PLUGINS

if GCC_PLUGINS

+config GCC_PLUGIN_SHADOW_CALL_STACK
+ bool "Plugin for ARM64 Shadow Call Stack"
+ depends on (!CC_HAVE_SHADOW_CALL_STACK) && ARM64
+ help
+ This plugin is used to support kernel CONFIG_SHADOW_CALL_STACK
+ compiled by gcc. Its principle is basically the same as that of
+ CLANG.
+ For more information, please refer to "config SHADOW_CALL_STACK"
+
config GCC_PLUGIN_CYC_COMPLEXITY
bool "Compute the cyclomatic complexity of a function" if EXPERT
depends on !COMPILE_TEST # too noisy
diff --git a/scripts/gcc-plugins/arm64_scs_plugin.c b/scripts/gcc-plugins/arm64_scs_plugin.c
new file mode 100644
index 0000000..bb72baa
--- /dev/null
+++ b/scripts/gcc-plugins/arm64_scs_plugin.c
@@ -0,0 +1,299 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "gcc-common.h"
+
+#define v_info(fmt, ...) \
+ do { \
+ if (verbose) \
+ fprintf(stderr, "[SCS]:" fmt, ## __VA_ARGS__); \
+ } while (0)
+
+#define NOSCS_ATTR_STR "no_shadow_call_stack"
+#define SCS_ASM_PUSH_STR "str x30, [x18], #8\n\t"
+#define SCS_ASM_POP_STR "ldr x30, [x18, #-8]!\n\t"
+
+__visible int plugin_is_GPL_compatible;
+
+static struct plugin_info arm64_scs_plugin_info = {
+ .version = "20210926vanilla",
+ .help = "enable\tactivate plugin\n"
+ "verbose\tprint all debug infos\n",
+};
+
+static bool verbose;
+
+#if BUILDING_GCC_VERSION >= 10001
+enum insn_code paciasp_num = CODE_FOR_paciasp;
+enum insn_code autiasp_num = CODE_FOR_autiasp;
+#elif BUILDING_GCC_VERSION >= 7003
+enum insn_code paciasp_num = CODE_FOR_pacisp;
+enum insn_code autiasp_num = CODE_FOR_autisp;
+#else
+enum insn_code paciasp_num = CODE_FOR_nothing;
+enum insn_code autiasp_num = CODE_FOR_nothing;
+#define TARGET_ARMV8_3 0
+#endif
+
+static rtx_insn * (*old_gen_prologue)(void);
+static rtx_insn * (*old_gen_epilogue)(void);
+static rtx_insn * (*old_gen_sibcall_epilogue)(void);
+
+static rtx gen_scs_push(location_t loc)
+{
+ rtx insn = gen_rtx_ASM_INPUT_loc(VOIDmode, ggc_strdup(SCS_ASM_PUSH_STR), loc);
+
+ MEM_VOLATILE_P(insn) = 1;
+ return insn;
+}
+
+static rtx gen_scs_pop(location_t loc)
+{
+ rtx insn = gen_rtx_ASM_INPUT_loc(VOIDmode, ggc_strdup(SCS_ASM_POP_STR), loc);
+
+ MEM_VOLATILE_P(insn) = 1;
+ return insn;
+}
+
+static bool scs_func_ignored(void)
+{
+ bool is_ignored;
+
+#if BUILDING_GCC_VERSION >= 8002
+ is_ignored = !cfun->machine->frame.emit_frame_chain;
+#else
+ is_ignored = !frame_pointer_needed;
+#endif
+
+ /*
+ * Functions that do not push LR into stack are not protected.
+ * Functions that call __builin_eh_return is not protected(consistent with gcc's PAC).
+ */
+ if (is_ignored || crtl->calls_eh_return) {
+ v_info("No protection code inserted into func:%s in file:%s\n",
+ get_name(current_function_decl), main_input_filename);
+ return 1;
+ }
+
+ /* Functions with attribute NOSCS_ATTR_STR need to be unprotected */
+ if (lookup_attribute(NOSCS_ATTR_STR, DECL_ATTRIBUTES(current_function_decl))) {
+ v_info("No protection code inserted into %s func:%s in file:%s\n", NOSCS_ATTR_STR,
+ get_name(current_function_decl), main_input_filename);
+ return 1;
+ }
+
+ return 0;
+}
+
+static rtx_insn *search_insn(enum insn_code code, rtx_insn *seq)
+{
+ rtx_insn *insn;
+
+ for (insn = get_insns(); insn; insn = NEXT_INSN(insn)) {
+ if (code == recog(PATTERN(insn), insn, 0))
+ return insn;
+ }
+
+ return NULL;
+}
+
+static bool scs_return_address_signing_enabled(void)
+{
+#if BUILDING_GCC_VERSION >= 7003
+ return aarch64_return_address_signing_enabled();
+#else
+ return false;
+#endif
+}
+
+static rtx_insn *scs_gen_prologue(void)
+{
+ rtx_insn *seq = NULL, *mark;
+ rtx tmp;
+ bool ret_sign_enabled;
+
+ if (old_gen_prologue)
+ seq = old_gen_prologue();
+
+ if ((!seq) || scs_func_ignored())
+ return seq;
+
+ ret_sign_enabled = scs_return_address_signing_enabled();
+ tmp = gen_scs_push(RESERVED_LOCATION_COUNT);
+
+ start_sequence();
+ emit_insn(seq);
+
+ if (ret_sign_enabled) {
+ /* For functions with pac enabled, insert scs push after the 'paciasp' insn */
+ mark = search_insn(paciasp_num, get_insns());
+ if (!mark)
+ error(G_("Non-standard insn seqs found:\n"
+ "__noscs attr should be added on func:%s,file:%s\n"),
+ get_name(current_function_decl), main_input_filename);
+
+ emit_insn_after(tmp, mark);
+ } else {
+ /* For functions that do not enable pac, insert scs push at the start of insns */
+ mark = get_insns();
+ emit_insn_before(tmp, mark);
+ }
+
+ seq = get_insns();
+ end_sequence();
+ return seq;
+}
+
+static rtx_insn *scs_gen_epilogue(void)
+{
+ rtx_insn *seq = NULL, *mark;
+ rtx tmp;
+ bool ret_sign_enabled;
+
+ if (old_gen_epilogue)
+ seq = old_gen_epilogue();
+
+ if ((!seq) || scs_func_ignored())
+ return seq;
+
+ ret_sign_enabled = scs_return_address_signing_enabled();
+ tmp = gen_scs_pop(RESERVED_LOCATION_COUNT);
+
+ start_sequence();
+ emit_insn(seq);
+
+ if (ret_sign_enabled && (!TARGET_ARMV8_3)) {
+ /* For functions with pac enabled, if 'autiasp' is used in epilogue
+ * (!TARGET_ARMV8_3), scs pop should inserted before this insn.
+ */
+ mark = search_insn(autiasp_num, get_insns());
+ } else {
+ /* For functions do not enabled pac or used 'retaa' as pac check,
+ * scs pop inserted before the last 'return" insn
+ */
+ mark = get_last_insn();
+ }
+
+ if (!mark)
+ error(G_("Non-standard insn seqs found:\n"
+ "__noscs attr should be added on func:%s,file:%s\n"),
+ get_name(current_function_decl), main_input_filename);
+
+ emit_insn_before(tmp, mark);
+
+ seq = get_insns();
+ end_sequence();
+ return seq;
+}
+
+static rtx_insn *scs_gen_sibcall_epilogue(void)
+{
+ rtx_insn *seq = NULL, *mark;
+ rtx tmp;
+ bool ret_sign_enabled;
+
+ if (old_gen_sibcall_epilogue)
+ seq = old_gen_sibcall_epilogue();
+
+ if ((!seq) || scs_func_ignored())
+ return seq;
+
+ ret_sign_enabled = scs_return_address_signing_enabled();
+ tmp = gen_scs_pop(RESERVED_LOCATION_COUNT);
+
+ start_sequence();
+ emit_insn(seq);
+
+ if (ret_sign_enabled) {
+ /* If pac is enabled, sibling_call will always use 'autiasp' as pac check */
+ mark = search_insn(autiasp_num, get_insns());
+ if (!mark)
+ error(G_("Non-standard insn seqs found:\n"
+ "__noscs attr should be added on func:%s,file:%s\n"),
+ get_name(current_function_decl), main_input_filename);
+ emit_insn_before(tmp, mark);
+ } else {
+ /* If pac is disabled, insert scs pop at the end of insns */
+ mark = get_last_insn();
+ emit_insn_after(tmp, mark);
+ }
+
+ seq = get_insns();
+ end_sequence();
+
+ return seq;
+}
+
+static void callback_before_start_unit(void *gcc_data __unused, void *user_data __unused)
+{
+ old_gen_prologue = targetm.gen_prologue;
+ old_gen_epilogue = targetm.gen_epilogue;
+ old_gen_sibcall_epilogue = targetm.gen_sibcall_epilogue;
+
+ targetm.gen_prologue = scs_gen_prologue;
+ targetm.gen_epilogue = scs_gen_epilogue;
+ targetm.gen_sibcall_epilogue = scs_gen_sibcall_epilogue;
+}
+
+static tree handle_noscs_attribute(tree *node, tree name, tree args __unused, int flags,
+ bool *no_add_attrs)
+{
+ /* NOSCS_ATTR_STR can only be used for function declarations */
+ switch (TREE_CODE(*node)) {
+ case FUNCTION_DECL:
+ break;
+ default:
+ error(G_("%qE attribute can be applies to function decl only (%qE)"), name, *node);
+ gcc_unreachable();
+ }
+
+ *no_add_attrs = false;
+ return NULL_TREE;
+}
+
+static struct attribute_spec noscs_attr = {};
+
+static void scs_register_attributes(void *event_data __unused, void *data __unused)
+{
+ noscs_attr.name = NOSCS_ATTR_STR;
+ noscs_attr.decl_required = true;
+ noscs_attr.handler = handle_noscs_attribute;
+ register_attribute(&noscs_attr);
+}
+
+__visible int plugin_init(struct plugin_name_args *plugin_info, struct plugin_gcc_version *version)
+{
+ int i;
+ bool enable = false;
+ const char * const plugin_name = plugin_info->base_name;
+ const int argc = plugin_info->argc;
+ const struct plugin_argument * const argv = plugin_info->argv;
+
+ if (!plugin_default_version_check(version, &gcc_version)) {
+ error(G_("Incompatible gcc/plugin versions"));
+ return 1;
+ }
+
+ for (i = 0; i < argc; ++i) {
+ if (!strcmp(argv[i].key, "enable")) {
+ enable = true;
+ continue;
+ }
+ if (!strcmp(argv[i].key, "verbose")) {
+ verbose = true;
+ continue;
+ }
+ error(G_("unknown option '-fplugin-arg-%s-%s'"), plugin_name, argv[i].key);
+ }
+
+ if (!enable) {
+ v_info("Plugin disabled for file:%s\n", main_input_filename);
+ return 0;
+ }
+
+ register_callback(plugin_name, PLUGIN_INFO, NULL, &arm64_scs_plugin_info);
+
+ register_callback(plugin_name, PLUGIN_ATTRIBUTES, scs_register_attributes, NULL);
+
+ register_callback(plugin_name, PLUGIN_START_UNIT, callback_before_start_unit, NULL);
+
+ return 0;
+}
diff --git a/scripts/gcc-plugins/gcc-common.h b/scripts/gcc-plugins/gcc-common.h
index 0c08761..7251b00 100644
--- a/scripts/gcc-plugins/gcc-common.h
+++ b/scripts/gcc-plugins/gcc-common.h
@@ -27,6 +27,7 @@
#include "except.h"
#include "function.h"
#include "toplev.h"
+#include "insn-codes.h"
#if BUILDING_GCC_VERSION >= 5000
#include "expr.h"
#endif
@@ -535,6 +536,9 @@ static inline void ipa_remove_stmt_references(symtab_node *referring_node, gimpl
}
#endif

+/* RTL related */
+extern int recog(rtx, rtx_insn *, int *);
+
#if BUILDING_GCC_VERSION < 6000
#define get_inner_reference(exp, pbitsize, pbitpos, poffset, pmode, punsignedp, preversep, pvolatilep, keep_aligning) \
get_inner_reference(exp, pbitsize, pbitpos, poffset, pmode, punsignedp, pvolatilep, keep_aligning)
--
2.7.4