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
@@ -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)
|
||||
|
Reference in New Issue
Block a user