objtool: Add entry UNRET validation
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>
This commit is contained in:

committed by
Greg Kroah-Hartman

parent
fbab1c94eb
commit
0d1a8a16e6
@@ -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"),
|
||||
|
@@ -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);
|
||||
|
@@ -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)
|
||||
|
@@ -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 ||
|
||||
|
Reference in New Issue
Block a user