]> www.infradead.org Git - users/dwmw2/linux.git/commitdiff
objtool: Add entry UNRET validation
authorPeter Zijlstra <peterz@infradead.org>
Tue, 14 Jun 2022 21:16:03 +0000 (23:16 +0200)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Mon, 25 Jul 2022 09:26:45 +0000 (11:26 +0200)
commit a09a6e2399ba0595c3042b3164f3ca68a3cff33e upstream.

Since entry asm is tricky, add a validation pass that ensures the
retbleed mitigation has been done before the first actual RET
instruction.

Entry points are those that either have UNWIND_HINT_ENTRY, which acts
as UNWIND_HINT_EMPTY but marks the instruction as an entry point, or
those that have UWIND_HINT_IRET_REGS at +0.

This is basically a variant of validate_branch() that is
intra-function and it will simply follow all branches from marked
entry points and ensures that all paths lead to ANNOTATE_UNRET_END.

If a path hits RET or an indirection the path is a fail and will be
reported.

There are 3 ANNOTATE_UNRET_END instances:

 - UNTRAIN_RET itself
 - exception from-kernel; this path doesn't need UNTRAIN_RET
 - all early exceptions; these also don't need UNTRAIN_RET

Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Signed-off-by: Borislav Petkov <bp@suse.de>
Reviewed-by: Josh Poimboeuf <jpoimboe@kernel.org>
Signed-off-by: Borislav Petkov <bp@suse.de>
[cascardo: arch/x86/entry/entry_64.S no pt_regs return at .Lerror_entry_done_lfence]
[cascardo: tools/objtool/builtin-check.c no link option validation]
[cascardo: tools/objtool/check.c opts.ibt is ibt]
[cascardo: tools/objtool/include/objtool/builtin.h leave unret option as bool, no struct opts]
[cascardo: objtool is still called from scripts/link-vmlinux.sh]
[cascardo: no IBT support]
Signed-off-by: Thadeu Lima de Souza Cascardo <cascardo@canonical.com>
[bwh: Backported to 5.10:
 - In scripts/link-vmlinux.sh, use "test -n" instead of is_enabled
 - Adjust context]
Signed-off-by: Ben Hutchings <ben@decadent.org.uk>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
13 files changed:
arch/x86/entry/entry_64.S
arch/x86/entry/entry_64_compat.S
arch/x86/include/asm/nospec-branch.h
arch/x86/include/asm/unwind_hints.h
arch/x86/kernel/head_64.S
arch/x86/xen/xen-asm.S
include/linux/objtool.h
scripts/link-vmlinux.sh
tools/include/linux/objtool.h
tools/objtool/builtin-check.c
tools/objtool/builtin.h
tools/objtool/check.c
tools/objtool/check.h

index f32bb2f943d63965e0a3d347653a532f72de252d..61e65e3ebd538aa47821e3a3f1fb5f42ad01cd32 100644 (file)
@@ -93,7 +93,7 @@ SYM_CODE_END(native_usergs_sysret64)
  */
 
 SYM_CODE_START(entry_SYSCALL_64)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
 
        swapgs
        /* tss.sp2 is scratch space. */
@@ -1094,6 +1094,7 @@ SYM_CODE_START_LOCAL(error_entry)
         */
 .Lerror_entry_done_lfence:
        FENCE_SWAPGS_KERNEL_ENTRY
+       ANNOTATE_UNRET_END
        RET
 
 .Lbstep_iret:
index e2f25403ec30720a0a59981689bc8b76cc41b964..4d637a965efbe5c64b36f68b96600732ffd49154 100644 (file)
@@ -49,7 +49,7 @@
  * 0(%ebp) arg6
  */
 SYM_CODE_START(entry_SYSENTER_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        /* Interrupts are off on entry. */
        SWAPGS
 
@@ -202,7 +202,7 @@ SYM_CODE_END(entry_SYSENTER_compat)
  * 0(%esp) arg6
  */
 SYM_CODE_START(entry_SYSCALL_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        /* Interrupts are off on entry. */
        swapgs
 
@@ -349,7 +349,7 @@ SYM_CODE_END(entry_SYSCALL_compat)
  * ebp  arg6
  */
 SYM_CODE_START(entry_INT80_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        /*
         * Interrupts are off on entry.
         */
index 19c02a2d632e1d2c8925d3d40fdb66080d7d3455..07c5642ab6d767a3090147f6df0273deb6be423d 100644 (file)
  */
 #define ANNOTATE_UNRET_SAFE ANNOTATE_RETPOLINE_SAFE
 
+/*
+ * Abuse ANNOTATE_RETPOLINE_SAFE on a NOP to indicate UNRET_END, should
+ * eventually turn into it's own annotation.
+ */
+.macro ANNOTATE_UNRET_END
+#ifdef CONFIG_DEBUG_ENTRY
+       ANNOTATE_RETPOLINE_SAFE
+       nop
+#endif
+.endm
+
 /*
  * JMP_NOSPEC and CALL_NOSPEC macros can be used instead of a simple
  * indirect jmp/call which may be susceptible to the Spectre variant 2
  */
 .macro UNTRAIN_RET
 #ifdef CONFIG_RETPOLINE
+       ANNOTATE_UNRET_END
        ALTERNATIVE_2 "",                                               \
                      "call zen_untrain_ret", X86_FEATURE_UNRET,        \
                      "call entry_ibpb", X86_FEATURE_ENTRY_IBPB
index 8e574c0afef80dfc49303eae60b5704a8dc0977f..9cd8214175b5249bf0117aa3abd0a436051850f2 100644 (file)
        UNWIND_HINT sp_reg=ORC_REG_UNDEFINED type=UNWIND_HINT_TYPE_CALL end=1
 .endm
 
+.macro UNWIND_HINT_ENTRY
+       UNWIND_HINT sp_reg=ORC_REG_UNDEFINED type=UNWIND_HINT_TYPE_ENTRY end=1
+.endm
+
 .macro UNWIND_HINT_REGS base=%rsp offset=0 indirect=0 extra=1 partial=0
        .if \base == %rsp
                .if \indirect
index 3c417734790f0390089beaa9da01e612c750c54a..0424c2a6c15b88d9f24b6fc7297707f66704d6b7 100644 (file)
@@ -321,6 +321,8 @@ SYM_CODE_END(start_cpu0)
 SYM_CODE_START_NOALIGN(vc_boot_ghcb)
        UNWIND_HINT_IRET_REGS offset=8
 
+       ANNOTATE_UNRET_END
+
        /* Build pt_regs */
        PUSH_AND_CLEAR_REGS
 
@@ -378,6 +380,7 @@ SYM_CODE_START(early_idt_handler_array)
 SYM_CODE_END(early_idt_handler_array)
 
 SYM_CODE_START_LOCAL(early_idt_handler_common)
+       ANNOTATE_UNRET_END
        /*
         * The stack is the hardware frame, an error code or zero, and the
         * vector number.
@@ -424,6 +427,8 @@ SYM_CODE_END(early_idt_handler_common)
 SYM_CODE_START_NOALIGN(vc_no_ghcb)
        UNWIND_HINT_IRET_REGS offset=8
 
+       ANNOTATE_UNRET_END
+
        /* Build pt_regs */
        PUSH_AND_CLEAR_REGS
 
index 70d3b21008e55ec83c4e4e40325d70494d5400f7..e3031afcb10399290e5358e71bce86668d4a04cb 100644 (file)
@@ -148,7 +148,7 @@ SYM_FUNC_END(xen_read_cr2_direct);
 
 .macro xen_pv_trap name
 SYM_CODE_START(xen_\name)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        pop %rcx
        pop %r11
        jmp  \name
@@ -277,7 +277,7 @@ SYM_CODE_END(xenpv_restore_regs_and_return_to_usermode)
 
 /* Normal 64-bit system call target */
 SYM_CODE_START(xen_entry_SYSCALL_64)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        popq %rcx
        popq %r11
 
@@ -296,7 +296,7 @@ SYM_CODE_END(xen_entry_SYSCALL_64)
 
 /* 32-bit compat syscall target */
 SYM_CODE_START(xen_entry_SYSCALL_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        popq %rcx
        popq %r11
 
@@ -313,7 +313,7 @@ SYM_CODE_END(xen_entry_SYSCALL_compat)
 
 /* 32-bit compat sysenter target */
 SYM_CODE_START(xen_entry_SYSENTER_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        /*
         * NB: Xen is polite and clears TF from EFLAGS for us.  This means
         * that we don't need to guard against single step exceptions here.
@@ -336,7 +336,7 @@ SYM_CODE_END(xen_entry_SYSENTER_compat)
 
 SYM_CODE_START(xen_entry_SYSCALL_compat)
 SYM_CODE_START(xen_entry_SYSENTER_compat)
-       UNWIND_HINT_EMPTY
+       UNWIND_HINT_ENTRY
        lea 16(%rsp), %rsp      /* strip %rcx, %r11 */
        mov $-ENOSYS, %rax
        pushq $0
index 8ed079c52c47d9b1bb1e2110c55d323a62439172..b2855f7fce53453ba08865d180886ccb2c781299 100644 (file)
@@ -32,11 +32,14 @@ struct unwind_hint {
  *
  * UNWIND_HINT_FUNC: Generate the unwind metadata of a callable function.
  * Useful for code which doesn't have an ELF function annotation.
+ *
+ * UNWIND_HINT_ENTRY: machine entry without stack, SYSCALL/SYSENTER etc.
  */
 #define UNWIND_HINT_TYPE_CALL          0
 #define UNWIND_HINT_TYPE_REGS          1
 #define UNWIND_HINT_TYPE_REGS_PARTIAL  2
 #define UNWIND_HINT_TYPE_FUNC          3
+#define UNWIND_HINT_TYPE_ENTRY         4
 
 #ifdef CONFIG_STACK_VALIDATION
 
index b184d94b90529ed54269950f0cdcda5a6141c98e..a7c72eb5a624d3b0f8577825395e4b169f6e507a 100755 (executable)
@@ -65,6 +65,9 @@ objtool_link()
 
        if [ -n "${CONFIG_VMLINUX_VALIDATION}" ]; then
                objtoolopt="check"
+               if [ -n "${CONFIG_RETPOLINE}" ]; then
+                       objtoolopt="${objtoolopt} --unret"
+               fi
                if [ -z "${CONFIG_FRAME_POINTER}" ]; then
                        objtoolopt="${objtoolopt} --no-fp"
                fi
index 8ed079c52c47d9b1bb1e2110c55d323a62439172..b2855f7fce53453ba08865d180886ccb2c781299 100644 (file)
@@ -32,11 +32,14 @@ struct unwind_hint {
  *
  * UNWIND_HINT_FUNC: Generate the unwind metadata of a callable function.
  * Useful for code which doesn't have an ELF function annotation.
+ *
+ * UNWIND_HINT_ENTRY: machine entry without stack, SYSCALL/SYSENTER etc.
  */
 #define UNWIND_HINT_TYPE_CALL          0
 #define UNWIND_HINT_TYPE_REGS          1
 #define UNWIND_HINT_TYPE_REGS_PARTIAL  2
 #define UNWIND_HINT_TYPE_FUNC          3
+#define UNWIND_HINT_TYPE_ENTRY         4
 
 #ifdef CONFIG_STACK_VALIDATION
 
index 758baf918d83aea433bb2cbff4c69e349697c173..d04eab7e77ae370b3c14568d45f34e6733c07c1c 100644 (file)
@@ -19,7 +19,7 @@
 #include "objtool.h"
 
 bool no_fp, no_unreachable, retpoline, module, backtrace, uaccess, stats,
-     validate_dup, vmlinux, sls;
+     validate_dup, vmlinux, sls, unret;
 
 static const char * const check_usage[] = {
        "objtool check [<options>] file.o",
@@ -30,6 +30,7 @@ const struct option check_options[] = {
        OPT_BOOLEAN('f', "no-fp", &no_fp, "Skip frame pointer validation"),
        OPT_BOOLEAN('u', "no-unreachable", &no_unreachable, "Skip 'unreachable instruction' warnings"),
        OPT_BOOLEAN('r', "retpoline", &retpoline, "Validate retpoline assumptions"),
+       OPT_BOOLEAN(0,   "unret", &unret, "validate entry unret placement"),
        OPT_BOOLEAN('m', "module", &module, "Indicates the object will be part of a kernel module"),
        OPT_BOOLEAN('b', "backtrace", &backtrace, "unwind on error"),
        OPT_BOOLEAN('a', "uaccess", &uaccess, "enable uaccess checking"),
index 33043fcb16db740bd2f763c93e79ab22a09d58f2..b6b486e28d5449c354c7f18208f79688d9a7f199 100644 (file)
@@ -9,7 +9,7 @@
 
 extern const struct option check_options[];
 extern bool no_fp, no_unreachable, retpoline, module, backtrace, uaccess, stats,
-            validate_dup, vmlinux, sls;
+            validate_dup, vmlinux, sls, unret;
 
 extern int cmd_check(int argc, const char **argv);
 extern int cmd_orc(int argc, const char **argv);
index eac6b89660b14acf38db183e93a10d5757156b82..ffc29b2603a13949a8761cf976dd1331498adb14 100644 (file)
@@ -1752,6 +1752,19 @@ static int read_unwind_hints(struct objtool_file *file)
 
                insn->hint = true;
 
+               if (hint->type == UNWIND_HINT_TYPE_REGS_PARTIAL) {
+                       struct symbol *sym = find_symbol_by_offset(insn->sec, insn->offset);
+
+                       if (sym && sym->bind == STB_GLOBAL) {
+                               insn->entry = 1;
+                       }
+               }
+
+               if (hint->type == UNWIND_HINT_TYPE_ENTRY) {
+                       hint->type = UNWIND_HINT_TYPE_CALL;
+                       insn->entry = 1;
+               }
+
                if (hint->type == UNWIND_HINT_TYPE_FUNC) {
                        insn->cfi = &func_cfi;
                        continue;
@@ -1800,8 +1813,9 @@ static int read_retpoline_hints(struct objtool_file *file)
 
                if (insn->type != INSN_JUMP_DYNAMIC &&
                    insn->type != INSN_CALL_DYNAMIC &&
-                   insn->type != INSN_RETURN) {
-                       WARN_FUNC("retpoline_safe hint not an indirect jump/call/ret",
+                   insn->type != INSN_RETURN &&
+                   insn->type != INSN_NOP) {
+                       WARN_FUNC("retpoline_safe hint not an indirect jump/call/ret/nop",
                                  insn->sec, insn->offset);
                        return -1;
                }
@@ -2818,8 +2832,8 @@ static int validate_branch(struct objtool_file *file, struct symbol *func,
                        return 1;
                }
 
-               visited = 1 << state.uaccess;
-               if (insn->visited) {
+               visited = VISITED_BRANCH << state.uaccess;
+               if (insn->visited & VISITED_BRANCH_MASK) {
                        if (!insn->hint && !insn_cfi_match(insn, &state.cfi))
                                return 1;
 
@@ -3045,6 +3059,145 @@ static int validate_unwind_hints(struct objtool_file *file, struct section *sec)
        return warnings;
 }
 
+/*
+ * Validate rethunk entry constraint: must untrain RET before the first RET.
+ *
+ * Follow every branch (intra-function) and ensure ANNOTATE_UNRET_END comes
+ * before an actual RET instruction.
+ */
+static int validate_entry(struct objtool_file *file, struct instruction *insn)
+{
+       struct instruction *next, *dest;
+       int ret, warnings = 0;
+
+       for (;;) {
+               next = next_insn_to_validate(file, insn);
+
+               if (insn->visited & VISITED_ENTRY)
+                       return 0;
+
+               insn->visited |= VISITED_ENTRY;
+
+               if (!insn->ignore_alts && !list_empty(&insn->alts)) {
+                       struct alternative *alt;
+                       bool skip_orig = false;
+
+                       list_for_each_entry(alt, &insn->alts, list) {
+                               if (alt->skip_orig)
+                                       skip_orig = true;
+
+                               ret = validate_entry(file, alt->insn);
+                               if (ret) {
+                                       if (backtrace)
+                                               BT_FUNC("(alt)", insn);
+                                       return ret;
+                               }
+                       }
+
+                       if (skip_orig)
+                               return 0;
+               }
+
+               switch (insn->type) {
+
+               case INSN_CALL_DYNAMIC:
+               case INSN_JUMP_DYNAMIC:
+               case INSN_JUMP_DYNAMIC_CONDITIONAL:
+                       WARN_FUNC("early indirect call", insn->sec, insn->offset);
+                       return 1;
+
+               case INSN_JUMP_UNCONDITIONAL:
+               case INSN_JUMP_CONDITIONAL:
+                       if (!is_sibling_call(insn)) {
+                               if (!insn->jump_dest) {
+                                       WARN_FUNC("unresolved jump target after linking?!?",
+                                                 insn->sec, insn->offset);
+                                       return -1;
+                               }
+                               ret = validate_entry(file, insn->jump_dest);
+                               if (ret) {
+                                       if (backtrace) {
+                                               BT_FUNC("(branch%s)", insn,
+                                                       insn->type == INSN_JUMP_CONDITIONAL ? "-cond" : "");
+                                       }
+                                       return ret;
+                               }
+
+                               if (insn->type == INSN_JUMP_UNCONDITIONAL)
+                                       return 0;
+
+                               break;
+                       }
+
+                       /* fallthrough */
+               case INSN_CALL:
+                       dest = find_insn(file, insn->call_dest->sec,
+                                        insn->call_dest->offset);
+                       if (!dest) {
+                               WARN("Unresolved function after linking!?: %s",
+                                    insn->call_dest->name);
+                               return -1;
+                       }
+
+                       ret = validate_entry(file, dest);
+                       if (ret) {
+                               if (backtrace)
+                                       BT_FUNC("(call)", insn);
+                               return ret;
+                       }
+                       /*
+                        * If a call returns without error, it must have seen UNTRAIN_RET.
+                        * Therefore any non-error return is a success.
+                        */
+                       return 0;
+
+               case INSN_RETURN:
+                       WARN_FUNC("RET before UNTRAIN", insn->sec, insn->offset);
+                       return 1;
+
+               case INSN_NOP:
+                       if (insn->retpoline_safe)
+                               return 0;
+                       break;
+
+               default:
+                       break;
+               }
+
+               if (!next) {
+                       WARN_FUNC("teh end!", insn->sec, insn->offset);
+                       return -1;
+               }
+               insn = next;
+       }
+
+       return warnings;
+}
+
+/*
+ * Validate that all branches starting at 'insn->entry' encounter UNRET_END
+ * before RET.
+ */
+static int validate_unret(struct objtool_file *file)
+{
+       struct instruction *insn;
+       int ret, warnings = 0;
+
+       for_each_insn(file, insn) {
+               if (!insn->entry)
+                       continue;
+
+               ret = validate_entry(file, insn);
+               if (ret < 0) {
+                       WARN_FUNC("Failed UNRET validation", insn->sec, insn->offset);
+                       return ret;
+               }
+               warnings += ret;
+       }
+
+       return warnings;
+}
+
 static int validate_retpoline(struct objtool_file *file)
 {
        struct instruction *insn;
@@ -3312,6 +3465,17 @@ int check(struct objtool_file *file)
                goto out;
        warnings += ret;
 
+       if (unret) {
+               /*
+                * Must be after validate_branch() and friends, it plays
+                * further games with insn->visited.
+                */
+               ret = validate_unret(file);
+               if (ret < 0)
+                       return ret;
+               warnings += ret;
+       }
+
        if (!warnings) {
                ret = validate_reachable_instructions(file);
                if (ret < 0)
index 508f712deba106cc33bf5ccc2978a50422be733b..e8fa948c053fb7df054845bb7cb6cc7e66632a22 100644 (file)
@@ -48,6 +48,7 @@ struct instruction {
        bool dead_end, ignore, ignore_alts;
        bool hint;
        bool retpoline_safe;
+       bool entry;
        s8 instr;
        u8 visited;
        struct alt_group *alt_group;
@@ -62,6 +63,11 @@ struct instruction {
        struct cfi_state *cfi;
 };
 
+#define VISITED_BRANCH         0x01
+#define VISITED_BRANCH_UACCESS 0x02
+#define VISITED_BRANCH_MASK    0x03
+#define VISITED_ENTRY          0x04
+
 static inline bool is_static_jump(struct instruction *insn)
 {
        return insn->type == INSN_JUMP_CONDITIONAL ||