Merge branch 'bpf-fix-stack-argument-precision-propagation'

Xu Yunxiang says:

====================
bpf: Fix stack argument precision propagation

Outgoing stack arguments retain scalar state separately from registers
and ordinary spills. Their precision dependencies must reach ancestor
states when verification reuses a state or falls back to marking all
scalars precise. Otherwise, an out-of-bounds map access can reuse a state
validated for an in-bounds argument.

Patch 1 covers outgoing scalar argument slots in both precision paths.
Patch 2 adds two load-only asm regressions in verifier_stack_arg.c, based
on Alexei's examples.

Changes in v2:
- Keep the patch 1 fix and explanation unchanged; rebase onto bpf/master
  at 03b0138974.
- Replace the standalone instruction/BTF test builder with two asm tests
  in the existing verifier_stack_arg.c, as requested by Alexei.
- Reuse that file's compiler capability and architecture guards, which
  already cover x86-64, arm64 and RV64. Drop the separate host-side guard.
- Rewrite patch 2's explanation for the two replacement tests.

v1:
https://lore.kernel.org/bpf/20261027061303.1754535-1-xyx2021@mail.ustc.edu.cn/
Review:
https://lore.kernel.org/bpf/DMJI3GWXQOC9.UA3K9X139AR@gmail.com/

Validation on x86-64 with Clang 23.1.4, JIT enabled and panic_on_warn=1:
- Built all four kernel variants and their BPF test modules, plus the
  complete test_progs runner with BPF_STRICT_BUILD=1.
- The two new tests distinguish the two precision paths:

                         State reuse       Fallback
  Unmodified baseline    wrongly accepts   wrongly accepts
  Propagation fix only   rejects           wrongly accepts
  Fallback fix only      wrongly accepts   rejects
  Complete fix           rejects           rejects

  Rejections report the intended map-value bounds violation. The two
  corresponding in-bounds controls load successfully on all four kernels.
  These programs are never attached or executed.
- All 274 subtests passed, with no skips, across verifier_precision,
  verifier_spill_fill, verifier_subprog_precision, verifier_scalar_ids,
  stack_arg_precision, verifier_stack_arg, verifier_stack_arg_order and
  iters on the complete candidate. No kernel WARN/Oops or panic occurred.
- Compiled the test object under x86-64, arm64 and RV64 guard definitions;
  Clang 20 correctly selects the existing unsupported-compiler dummy.
  Runtime testing was x86-64 only, and the unfiltered suite was not run.

Codex assisted with implementation, test integration, commit messages
and validation. The v2 test code was supplied by Alexei and is used
unchanged.
====================

Link: https://patch.msgid.link/20261102190350.788433-1-xyx2021@mail.ustc.edu.cn
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Alexei Starovoitov 2026-11-02 22:42:11 +00:00
commit 38bce81d47
No known key found for this signature in database
3 changed files with 140 additions and 0 deletions

View File

@ -746,6 +746,17 @@ void bpf_mark_all_scalars_precise(struct bpf_verifier_env *env,
i, -(j + 1) * 8);
}
}
for (j = 0; j < func->out_stack_arg_cnt; j++) {
reg = &func->stack_arg_regs[j];
if (reg->type != SCALAR_VALUE || reg->precise)
continue;
reg->precise = true;
if (env->log.level & BPF_LOG_LEVEL2) {
verbose(env,
"force_precise: frame%d: forcing r11%d to be precise\n",
i, -(j + 1) * BPF_REG_SIZE);
}
}
}
}
}

View File

@ -1116,6 +1116,20 @@ static int propagate_precision(struct bpf_verifier_env *env,
bpf_bt_set_frame_slot(&env->bt, fr, i);
first = false;
}
for (i = 0; i < state->out_stack_arg_cnt; i++) {
state_reg = &state->stack_arg_regs[i];
if (state_reg->type != SCALAR_VALUE || !state_reg->precise)
continue;
if (env->log.level & BPF_LOG_LEVEL2) {
if (first)
verbose(env, "frame %d: propagating r11%d",
fr, -(i + 1) * BPF_REG_SIZE);
else
verbose(env, ",r11%d", -(i + 1) * BPF_REG_SIZE);
}
bt_set_frame_stack_arg_slot(&env->bt, fr, i);
first = false;
}
if (!first && (env->log.level & BPF_LOG_LEVEL2))
verbose(env, "\n");
}

View File

@ -552,6 +552,121 @@ __naked void stack_arg_may_goto_in_caller(void)
);
}
/* arg6 is an offset into the value of map_hash_8b. 0-7 are in bounds. */
__naked __noinline __used
static long arg6_is_map_value_off(long a, long b, long c, long d, long e, long off)
{
asm volatile (
"r6 = *(u64 *)(r11 + 8);"
"r1 = 0;"
"*(u64 *)(r10 - 8) = r1;"
"r2 = r10;"
"r2 += -8;"
"r1 = %[map_hash_8b] ll;"
"call %[bpf_map_lookup_elem];"
"if r0 == 0 goto l0_%=;"
"r0 += r6;"
"r0 = *(u8 *)(r0 + 0);"
"l0_%=:"
"exit;"
:
: __imm(bpf_map_lookup_elem),
__imm_addr(map_hash_8b)
: __clobber_all
);
}
SEC("tc")
__description("stack_arg: a pruned state propagates precision of arg6")
__failure __msg("invalid access to map value")
__flag(BPF_F_TEST_STATE_FREQ)
__naked void stack_arg_precision_pruned_state(void)
{
asm volatile (
"call %[bpf_get_prandom_u32];"
"if r0 != 0 goto l0_%=;"
/* Path 1: arg6 = 0. It makes arg6 precise at l3. */
"*(u64 *)(r11 - 8) = 0;"
"goto l3_%=;"
"l0_%=:"
"if r0 == 1 goto l1_%=;"
/* Path 2: arg6 = r6 = 0. Pruned at l3. It has to make r6 precise at l2. */
"r6 = 0;"
"goto l2_%=;"
"l1_%=:"
/* Path 3: arg6 = r6 = 8. Out of bounds. */
"r6 = 8;"
"l2_%=:"
"*(u64 *)(r11 - 8) = r6;"
"l3_%=:"
"r1 = 0;"
"r2 = 0;"
"r3 = 0;"
"r4 = 0;"
"r5 = 0;"
"call arg6_is_map_value_off;"
"exit;"
:: __imm(bpf_get_prandom_u32)
: __clobber_all
);
}
/* The same, but backtracking of r1 sees a store through r1 and marks all scalars precise. */
__naked __noinline __used
static long arg6_is_map_value_off_fallback(long a, long b, long c, long d, long e, long off)
{
asm volatile (
"r6 = *(u64 *)(r11 + 8);"
"r1 = 0;"
"*(u64 *)(r10 - 8) = r1;"
"r2 = r10;"
"r2 += -8;"
"r1 = %[map_hash_8b] ll;"
"call %[bpf_map_lookup_elem];"
"if r0 == 0 goto l0_%=;"
"r1 = r0;"
"*(u8 *)(r1 + 0) = 0;"
"r1 -= r0;"
"r1 &= 0;"
"r1 += r6;"
"r0 += r1;"
"r0 = *(u8 *)(r0 + 0);"
"l0_%=:"
"exit;"
:
: __imm(bpf_map_lookup_elem),
__imm_addr(map_hash_8b)
: __clobber_all
);
}
SEC("tc")
__description("stack_arg: forcing all scalars precise covers arg6")
__failure __msg("invalid access to map value")
__flag(BPF_F_TEST_STATE_FREQ)
__naked void stack_arg_precision_fallback(void)
{
asm volatile (
"call %[bpf_get_prandom_u32];"
"if r0 != 0 goto l0_%=;"
"*(u64 *)(r11 - 8) = 0;"
"goto l1_%=;"
"l0_%=:"
/* Out of bounds. Must not be pruned at l1. */
"*(u64 *)(r11 - 8) = 8;"
"l1_%=:"
"r1 = 0;"
"r2 = 0;"
"r3 = 0;"
"r4 = 0;"
"r5 = 0;"
"call arg6_is_map_value_off_fallback;"
"exit;"
:: __imm(bpf_get_prandom_u32)
: __clobber_all
);
}
#else
SEC("socket")