Skip to content

Commit c9c324d

Browse files
committed
objtool: Support stack layout changes in alternatives
The ORC unwinder showed a warning [1] which revealed the stack layout didn't match what was expected. The problem was that paravirt patching had replaced "CALL *pv_ops.irq.save_fl" with "PUSHF;POP". That changed the stack layout between the PUSHF and the POP, so unwinding from an interrupt which occurred between those two instructions would fail. Part of the agreed upon solution was to rework the custom paravirt patching code to use alternatives instead, since objtool already knows how to read alternatives (and converging runtime patching infrastructure is always a good thing anyway). But the main problem still remains, which is that runtime patching can change the stack layout. Making stack layout changes in alternatives was disallowed with commit 7117f16 ("objtool: Fix ORC vs alternatives"), but now that paravirt is going to be doing it, it needs to be supported. One way to do so would be to modify the ORC table when the code gets patched. But ORC is simple -- a good thing! -- and it's best to leave it alone. Instead, support stack layout changes by "flattening" all possible stack states (CFI) from parallel alternative code streams into a single set of linear states. The only necessary limitation is that CFI conflicts are disallowed at all possible instruction boundaries. For example, this scenario is allowed: Alt1 Alt2 Alt3 0x00 CALL *pv_ops.save_fl CALL xen_save_fl PUSHF 0x01 POP %RAX 0x02 NOP ... 0x05 NOP ... 0x07 <insn> The unwind information for offset-0x00 is identical for all 3 alternatives. Similarly offset-0x05 and higher also are identical (and the same as 0x00). However offset-0x01 has deviating CFI, but that is only relevant for Alt3, neither of the other alternative instruction streams will ever hit that offset. This scenario is NOT allowed: Alt1 Alt2 0x00 CALL *pv_ops.save_fl PUSHF 0x01 NOP6 ... 0x07 NOP POP %RAX The problem here is that offset-0x7, which is an instruction boundary in both possible instruction patch streams, has two conflicting stack layouts. [ The above examples were stolen from Peter Zijlstra. ] The new flattened CFI array is used both for the detection of conflicts (like the second example above) and the generation of linear ORC entries. BTW, another benefit of these changes is that, thanks to some related cleanups (new fake nops and alt_group struct) objtool can finally be rid of fake jumps, which were a constant source of headaches. [1] https://lkml.kernel.org/r/20201111170536.arx2zbn4ngvjoov7@treble Cc: Shinichiro Kawasaki <[email protected]> Signed-off-by: Josh Poimboeuf <[email protected]>
1 parent b23cc71 commit c9c324d

File tree

4 files changed

+159
-111
lines changed

4 files changed

+159
-111
lines changed

tools/objtool/Documentation/stack-validation.txt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,15 @@ they mean, and suggestions for how to fix them.
315315
function tracing inserts additional calls, which is not obvious from the
316316
sources).
317317

318-
10. file.o: warning: func()+0x5c: alternative modifies stack
319-
320-
This means that an alternative includes instructions that modify the
321-
stack. The problem is that there is only one ORC unwind table, this means
322-
that the ORC unwind entries must be valid for each of the alternatives.
323-
The easiest way to enforce this is to ensure alternatives do not contain
324-
any ORC entries, which in turn implies the above constraint.
318+
10. file.o: warning: func()+0x5c: stack layout conflict in alternatives
319+
320+
This means that in the use of the alternative() or ALTERNATIVE()
321+
macro, the code paths have conflicting modifications to the stack.
322+
The problem is that there is only one ORC unwind table, which means
323+
that the ORC unwind entries must be consistent for all possible
324+
instruction boundaries regardless of which code has been patched.
325+
This limitation can be overcome by massaging the alternatives with
326+
NOPs to shift the stack changes around so they no longer conflict.
325327

326328
11. file.o: warning: unannotated intra-function call
327329

tools/objtool/check.c

Lines changed: 96 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
#include <linux/kernel.h>
2121
#include <linux/static_call_types.h>
2222

23-
#define FAKE_JUMP_OFFSET -1
24-
2523
struct alternative {
2624
struct list_head list;
2725
struct instruction *insn;
@@ -775,9 +773,6 @@ static int add_jump_destinations(struct objtool_file *file)
775773
if (!is_static_jump(insn))
776774
continue;
777775

778-
if (insn->offset == FAKE_JUMP_OFFSET)
779-
continue;
780-
781776
reloc = find_reloc_by_dest_range(file->elf, insn->sec,
782777
insn->offset, insn->len);
783778
if (!reloc) {
@@ -971,28 +966,15 @@ static int add_call_destinations(struct objtool_file *file)
971966
}
972967

973968
/*
974-
* The .alternatives section requires some extra special care, over and above
975-
* what other special sections require:
976-
*
977-
* 1. Because alternatives are patched in-place, we need to insert a fake jump
978-
* instruction at the end so that validate_branch() skips all the original
979-
* replaced instructions when validating the new instruction path.
980-
*
981-
* 2. An added wrinkle is that the new instruction length might be zero. In
982-
* that case the old instructions are replaced with noops. We simulate that
983-
* by creating a fake jump as the only new instruction.
984-
*
985-
* 3. In some cases, the alternative section includes an instruction which
986-
* conditionally jumps to the _end_ of the entry. We have to modify these
987-
* jumps' destinations to point back to .text rather than the end of the
988-
* entry in .altinstr_replacement.
969+
* The .alternatives section requires some extra special care over and above
970+
* other special sections because alternatives are patched in place.
989971
*/
990972
static int handle_group_alt(struct objtool_file *file,
991973
struct special_alt *special_alt,
992974
struct instruction *orig_insn,
993975
struct instruction **new_insn)
994976
{
995-
struct instruction *last_orig_insn, *last_new_insn, *insn, *fake_jump = NULL;
977+
struct instruction *last_orig_insn, *last_new_insn = NULL, *insn, *nop = NULL;
996978
struct alt_group *orig_alt_group, *new_alt_group;
997979
unsigned long dest_off;
998980

@@ -1002,6 +984,13 @@ static int handle_group_alt(struct objtool_file *file,
1002984
WARN("malloc failed");
1003985
return -1;
1004986
}
987+
orig_alt_group->cfi = calloc(special_alt->orig_len,
988+
sizeof(struct cfi_state *));
989+
if (!orig_alt_group->cfi) {
990+
WARN("calloc failed");
991+
return -1;
992+
}
993+
1005994
last_orig_insn = NULL;
1006995
insn = orig_insn;
1007996
sec_for_each_insn_from(file, insn) {
@@ -1015,42 +1004,45 @@ static int handle_group_alt(struct objtool_file *file,
10151004
orig_alt_group->first_insn = orig_insn;
10161005
orig_alt_group->last_insn = last_orig_insn;
10171006

1018-
if (next_insn_same_sec(file, last_orig_insn)) {
1019-
fake_jump = malloc(sizeof(*fake_jump));
1020-
if (!fake_jump) {
1021-
WARN("malloc failed");
1022-
return -1;
1023-
}
1024-
memset(fake_jump, 0, sizeof(*fake_jump));
1025-
INIT_LIST_HEAD(&fake_jump->alts);
1026-
INIT_LIST_HEAD(&fake_jump->stack_ops);
1027-
init_cfi_state(&fake_jump->cfi);
10281007

1029-
fake_jump->sec = special_alt->new_sec;
1030-
fake_jump->offset = FAKE_JUMP_OFFSET;
1031-
fake_jump->type = INSN_JUMP_UNCONDITIONAL;
1032-
fake_jump->jump_dest = list_next_entry(last_orig_insn, list);
1033-
fake_jump->func = orig_insn->func;
1008+
new_alt_group = malloc(sizeof(*new_alt_group));
1009+
if (!new_alt_group) {
1010+
WARN("malloc failed");
1011+
return -1;
10341012
}
10351013

1036-
if (!special_alt->new_len) {
1037-
if (!fake_jump) {
1038-
WARN("%s: empty alternative at end of section",
1039-
special_alt->orig_sec->name);
1014+
if (special_alt->new_len < special_alt->orig_len) {
1015+
/*
1016+
* Insert a fake nop at the end to make the replacement
1017+
* alt_group the same size as the original. This is needed to
1018+
* allow propagate_alt_cfi() to do its magic. When the last
1019+
* instruction affects the stack, the instruction after it (the
1020+
* nop) will propagate the new state to the shared CFI array.
1021+
*/
1022+
nop = malloc(sizeof(*nop));
1023+
if (!nop) {
1024+
WARN("malloc failed");
10401025
return -1;
10411026
}
1027+
memset(nop, 0, sizeof(*nop));
1028+
INIT_LIST_HEAD(&nop->alts);
1029+
INIT_LIST_HEAD(&nop->stack_ops);
1030+
init_cfi_state(&nop->cfi);
10421031

1043-
*new_insn = fake_jump;
1044-
return 0;
1032+
nop->sec = special_alt->new_sec;
1033+
nop->offset = special_alt->new_off + special_alt->new_len;
1034+
nop->len = special_alt->orig_len - special_alt->new_len;
1035+
nop->type = INSN_NOP;
1036+
nop->func = orig_insn->func;
1037+
nop->alt_group = new_alt_group;
1038+
nop->ignore = orig_insn->ignore_alts;
10451039
}
10461040

1047-
new_alt_group = malloc(sizeof(*new_alt_group));
1048-
if (!new_alt_group) {
1049-
WARN("malloc failed");
1050-
return -1;
1041+
if (!special_alt->new_len) {
1042+
*new_insn = nop;
1043+
goto end;
10511044
}
10521045

1053-
last_new_insn = NULL;
10541046
insn = *new_insn;
10551047
sec_for_each_insn_from(file, insn) {
10561048
struct reloc *alt_reloc;
@@ -1089,14 +1081,8 @@ static int handle_group_alt(struct objtool_file *file,
10891081
continue;
10901082

10911083
dest_off = arch_jump_destination(insn);
1092-
if (dest_off == special_alt->new_off + special_alt->new_len) {
1093-
if (!fake_jump) {
1094-
WARN("%s: alternative jump to end of section",
1095-
special_alt->orig_sec->name);
1096-
return -1;
1097-
}
1098-
insn->jump_dest = fake_jump;
1099-
}
1084+
if (dest_off == special_alt->new_off + special_alt->new_len)
1085+
insn->jump_dest = next_insn_same_sec(file, last_orig_insn);
11001086

11011087
if (!insn->jump_dest) {
11021088
WARN_FUNC("can't find alternative jump destination",
@@ -1111,13 +1097,13 @@ static int handle_group_alt(struct objtool_file *file,
11111097
return -1;
11121098
}
11131099

1100+
if (nop)
1101+
list_add(&nop->list, &last_new_insn->list);
1102+
end:
11141103
new_alt_group->orig_group = orig_alt_group;
11151104
new_alt_group->first_insn = *new_insn;
1116-
new_alt_group->last_insn = last_new_insn;
1117-
1118-
if (fake_jump)
1119-
list_add(&fake_jump->list, &last_new_insn->list);
1120-
1105+
new_alt_group->last_insn = nop ? : last_new_insn;
1106+
new_alt_group->cfi = orig_alt_group->cfi;
11211107
return 0;
11221108
}
11231109

@@ -2248,22 +2234,47 @@ static int update_cfi_state(struct instruction *insn, struct cfi_state *cfi,
22482234
return 0;
22492235
}
22502236

2251-
static int handle_insn_ops(struct instruction *insn, struct insn_state *state)
2237+
/*
2238+
* The stack layouts of alternatives instructions can sometimes diverge when
2239+
* they have stack modifications. That's fine as long as the potential stack
2240+
* layouts don't conflict at any given potential instruction boundary.
2241+
*
2242+
* Flatten the CFIs of the different alternative code streams (both original
2243+
* and replacement) into a single shared CFI array which can be used to detect
2244+
* conflicts and nicely feed a linear array of ORC entries to the unwinder.
2245+
*/
2246+
static int propagate_alt_cfi(struct objtool_file *file, struct instruction *insn)
22522247
{
2253-
struct stack_op *op;
2248+
struct cfi_state **alt_cfi;
2249+
int group_off;
22542250

2255-
list_for_each_entry(op, &insn->stack_ops, list) {
2256-
struct cfi_state old_cfi = state->cfi;
2257-
int res;
2251+
if (!insn->alt_group)
2252+
return 0;
22582253

2259-
res = update_cfi_state(insn, &state->cfi, op);
2260-
if (res)
2261-
return res;
2254+
alt_cfi = insn->alt_group->cfi;
2255+
group_off = insn->offset - insn->alt_group->first_insn->offset;
22622256

2263-
if (insn->alt_group && memcmp(&state->cfi, &old_cfi, sizeof(struct cfi_state))) {
2264-
WARN_FUNC("alternative modifies stack", insn->sec, insn->offset);
2257+
if (!alt_cfi[group_off]) {
2258+
alt_cfi[group_off] = &insn->cfi;
2259+
} else {
2260+
if (memcmp(alt_cfi[group_off], &insn->cfi, sizeof(struct cfi_state))) {
2261+
WARN_FUNC("stack layout conflict in alternatives",
2262+
insn->sec, insn->offset);
22652263
return -1;
22662264
}
2265+
}
2266+
2267+
return 0;
2268+
}
2269+
2270+
static int handle_insn_ops(struct instruction *insn, struct insn_state *state)
2271+
{
2272+
struct stack_op *op;
2273+
2274+
list_for_each_entry(op, &insn->stack_ops, list) {
2275+
2276+
if (update_cfi_state(insn, &state->cfi, op))
2277+
return 1;
22672278

22682279
if (op->dest.type == OP_DEST_PUSHF) {
22692280
if (!state->uaccess_stack) {
@@ -2453,28 +2464,20 @@ static int validate_return(struct symbol *func, struct instruction *insn, struct
24532464
return 0;
24542465
}
24552466

2456-
/*
2457-
* Alternatives should not contain any ORC entries, this in turn means they
2458-
* should not contain any CFI ops, which implies all instructions should have
2459-
* the same same CFI state.
2460-
*
2461-
* It is possible to constuct alternatives that have unreachable holes that go
2462-
* unreported (because they're NOPs), such holes would result in CFI_UNDEFINED
2463-
* states which then results in ORC entries, which we just said we didn't want.
2464-
*
2465-
* Avoid them by copying the CFI entry of the first instruction into the whole
2466-
* alternative.
2467-
*/
2468-
static void fill_alternative_cfi(struct objtool_file *file, struct instruction *insn)
2467+
static struct instruction *next_insn_to_validate(struct objtool_file *file,
2468+
struct instruction *insn)
24692469
{
2470-
struct instruction *first_insn = insn;
24712470
struct alt_group *alt_group = insn->alt_group;
24722471

2473-
sec_for_each_insn_continue(file, insn) {
2474-
if (insn->alt_group != alt_group)
2475-
break;
2476-
insn->cfi = first_insn->cfi;
2477-
}
2472+
/*
2473+
* Simulate the fact that alternatives are patched in-place. When the
2474+
* end of a replacement alt_group is reached, redirect objtool flow to
2475+
* the end of the original alt_group.
2476+
*/
2477+
if (alt_group && insn == alt_group->last_insn && alt_group->orig_group)
2478+
return next_insn_same_sec(file, alt_group->orig_group->last_insn);
2479+
2480+
return next_insn_same_sec(file, insn);
24782481
}
24792482

24802483
/*
@@ -2495,7 +2498,7 @@ static int validate_branch(struct objtool_file *file, struct symbol *func,
24952498
sec = insn->sec;
24962499

24972500
while (1) {
2498-
next_insn = next_insn_same_sec(file, insn);
2501+
next_insn = next_insn_to_validate(file, insn);
24992502

25002503
if (file->c_file && func && insn->func && func != insn->func->pfunc) {
25012504
WARN("%s() falls through to next function %s()",
@@ -2528,6 +2531,9 @@ static int validate_branch(struct objtool_file *file, struct symbol *func,
25282531

25292532
insn->visited |= visited;
25302533

2534+
if (propagate_alt_cfi(file, insn))
2535+
return 1;
2536+
25312537
if (!insn->ignore_alts && !list_empty(&insn->alts)) {
25322538
bool skip_orig = false;
25332539

@@ -2543,9 +2549,6 @@ static int validate_branch(struct objtool_file *file, struct symbol *func,
25432549
}
25442550
}
25452551

2546-
if (insn->alt_group)
2547-
fill_alternative_cfi(file, insn);
2548-
25492552
if (skip_orig)
25502553
return 0;
25512554
}
@@ -2779,9 +2782,6 @@ static bool ignore_unreachable_insn(struct objtool_file *file, struct instructio
27792782
!strcmp(insn->sec->name, ".altinstr_aux"))
27802783
return true;
27812784

2782-
if (insn->type == INSN_JUMP_UNCONDITIONAL && insn->offset == FAKE_JUMP_OFFSET)
2783-
return true;
2784-
27852785
if (!insn->func)
27862786
return false;
27872787

tools/objtool/include/objtool/check.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ struct alt_group {
2828

2929
/* First and last instructions in the group */
3030
struct instruction *first_insn, *last_insn;
31+
32+
/*
33+
* Byte-offset-addressed len-sized array of pointers to CFI structs.
34+
* This is shared with the other alt_groups in the same alternative.
35+
*/
36+
struct cfi_state **cfi;
3137
};
3238

3339
struct instruction {

0 commit comments

Comments
 (0)