Re: [PATCH bpf-next 3/5] bpf: infer zext_dst based on static register liveness analysis

[email protected] Fri, 31 Jul 2026 19:26:34 +0000
Newsgroups org.kernel.vger.bpf
Message-ID <[email protected]>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Incorrect static liveness tracking for 64-bit conditional jumps leads to missing zero-extensions (`zext_dst`), resulting in verifier/runtime state divergence and potential privilege escalation.
--

--- Patch [3]: [PATCH bpf-next 3/5] bpf: infer zext_dst based on static register liveness analysis ---
commit ab8592d6a434dbfed0cbb0bd067d9032b308db8d
Author: Eduard Zingerman <[email protected]>

bpf: infer zext_dst based on static register liveness analysis

This commit replaces the dynamic subreg_def based zero-extension
tracking with static liveness analysis to correctly infer zext_dst
and prevent missing zero-extensions on architectures like s390x.

Link: https://lore.kernel.org/bpf/CAGKGUv=sOuqQtA1Ub-5JXfA4FPosJFYKAQE4B79cK+P1erxqtg@mail.gmail.com/

> diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
> index c996b8a03e792..ea002550671a8 100644
> --- a/kernel/bpf/liveness.c
> +++ b/kernel/bpf/liveness.c
> @@ -2317,9 +2318,18 @@ int bpf_compute_live_registers(struct bpf_verifier_env *env)
>  	}
>  
>  	for (i = 0; i < insn_cnt; ++i) {
> +		int def32 = bpf_insn_def32(&insns[i]);
> +		u32 out = state[i].out;
>  		u32 in = state[i].in;
>  
>  		insn_aux[i].live_regs_before = mask_lo(in) | mask_hi(in);
> +		/*
> +		 * On architectures where 32-bit operations do not reset upper halves
> +		 * of the registers, the verifier needs to zero extend a destination
> +		 * register if an instruction defines a 32-bit subregister and the
> +		 * upper half of that register is alive after the instruction.
> +		 */
> +		insn_aux[i].zext_dst = def32 >= 0 && (mask_hi(out) & BIT(def32));

[Severity: Critical]
Does this code correctly evaluate liveness for 64-bit conditional jumps?

The new zext_dst logic relies on mask_hi(out) accurately reflecting if the
upper half of a register is alive.

However, looking at compute_insn_live_regs() in kernel/bpf/liveness.c for
conditional jumps:

    const u8 class = BPF_CLASS(insn->code);
    const u8 code = BPF_OP(insn->code);
    ...
    case BPF_JMP:
    case BPF_JMP32:
        switch (code) {
        ...
        default:
            def = 0;
            if (BPF_SRC(insn->code) == BPF_K)
                use = dst;
            else
                use = dst | (code == BPF_JMP32 ? src : src32);
        }

Can the code variable ever equal BPF_JMP32?

BPF_OP() extracts the operation code (which is a multiple of 16), while
BPF_JMP32 is a class (0x06). This means the check code == BPF_JMP32 evaluates
to false, and the source register usage always falls back to src32, even for
64-bit jumps (BPF_JMP).

If a 32-bit operation defines a register, and it is followed by a 64-bit
conditional jump using that register as a source with no other uses of the
upper 32 bits, the upper half won't be marked as alive.

As a result, zext_dst wouldn't be set, which could lead to the JIT skipping
the mandatory zero extension. This would cause the 64-bit jump to compare
against uninitialized garbage in the upper bits, potentially allowing
out-of-bounds access.

Should this check use class == BPF_JMP32 instead?

>  	}

-- 
Sashiko AI review ยท https://sashiko.dev/#/patchset/[email protected]?part=3