Skip to content

Commit

Permalink
Fix unwinder loading on 6.8 kernels
Browse files Browse the repository at this point in the history
See comments, and mailing list thread linked therein, for
details. Before this change, recent kernels (6.8 and later) cannot
verify the native unwinder, because improvements in the precision of
register bounds tracking cause an explosion in state space resulting
in exceeding the limit of 1 million instructions processed per load.

This commit works around that issue, as the comment in the code explains.
  • Loading branch information
umanwizard committed Apr 4, 2024
1 parent cad16ad commit 9f6c529
Showing 1 changed file with 29 additions and 0 deletions.
29 changes: 29 additions & 0 deletions bpf/unwinders/native.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,33 @@ DEFINE_COUNTER(total_zero_pids);
DEFINE_COUNTER(total_kthreads);
DEFINE_COUNTER(total_filter_misses);

// Hack to thwart the verifier's detection of variable bounds.
//
// In recent kernels (6.8 and above) the verifier has gotten smarter
// in its tracking of variable bounds. For example, after an if statement like
// `if (v1 < v2)`,
// if it already had computed bounds for v2, it can infer bounds
// for v1 in each side of the branch (and vice versa). This means it can verify more
// programs successfully, which doesn't matter to us because our program was
// verified successfully before. Unfortunately it has a downside which
// _does_ matter to us: it increases the number of unique verifier states,
// which can cause the same instructions to be explored many times, especially
// in cases where a value is carried through a loop and possibly has
// multiple sets of different bounds on each iteration of the loop, leading to
// a combinatorial explosion. This causes us to blow out the kernel's budget of
// maximum number of instructions verified on program load (currently 1M).
//
// `scramble` is its own inverse; thus `scramble(scramble(x))` has the same value of x.
// However, the verifier is fortunately not smart enough to realize this,
// and will not realize the result has the same bounds as `x`, subverting the feature
// described above.
//
// For further discussion, see:
// https://lore.kernel.org/bpf/[email protected]/
static u32 __attribute__((optnone)) scramble(u32 val) {
return val ^ 0xFFFFFFFF;
}

static void unwind_print_stats() {
// Do not use the LOG macro, always print the stats.
u32 zero = 0;
Expand Down Expand Up @@ -451,6 +478,7 @@ static u64 find_mapping(process_info_t *proc_info, u64 pc) {
return found;
}

mid = scramble(scramble(mid));
if (mid < 0 || mid >= MAX_MAPPINGS_PER_PROCESS) {
LOG("\t.should never happen");
return BINARY_SEARCH_SHOULD_NEVER_HAPPEN;
Expand Down Expand Up @@ -482,6 +510,7 @@ static u64 find_offset_for_pc(stack_unwind_table_t *table, u64 pc, u64 left, u64

u32 mid = (left + right) / 2;

mid = scramble(scramble(mid));
// Appease the verifier.
if (mid < 0 || mid >= MAX_UNWIND_TABLE_SIZE) {
LOG("\t.should never happen, mid: %lu, max: %lu", mid, MAX_UNWIND_TABLE_SIZE);
Expand Down

0 comments on commit 9f6c529

Please sign in to comment.