[RESEND][REGRESSION][SECURITY] bpf, s390: verifier/JIT mismatch allows unprivileged kernel memory access

From: Min-gyu Kim

Date: Thu Jul 23 2026 - 04:17:03 EST


Hello,

I am reporting a potential security regression involving eBPF verifier state
pruning and the s390x JIT. I reproduced arbitrary kernel read and write access
and local privilege escalation as an unprivileged user.

AI assistance was used during discovery and analysis. In accordance with the
kernel security-bug reporting guidance, I am not requesting an embargo. The
reproducer and exploit are not attached or published; I can provide them
privately to maintainers on request.

Summary and affected versions

A verifier regression can cause the verifier and the s390x JIT to disagree
about the upper 32 bits of scalar registers. A crafted socket-filter program
can consequently form an unchecked 64-bit offset from an ARRAY map-value
pointer and access attacker-selected kernel addresses.

The regression was introduced by:

107e169799057bc6a379ddb625cbe1e51cfc7d72
("bpf: disable and remove registers chain based liveness")

The commit first appears in v6.18-rc1 and is not present in v6.17. The cause
was still present in the v6.18, v6.19, v7.0, and v7.1 stable branches and in
v7.2-rc4, bpf, and bpf-next when checked by source comparison on 2026-07-22.
I also confirmed the relevant logic in mainline commit
4539944e515183668109bdf4d0c3d7d228383d88 on 2026-07-23. Dynamic
reproduction was performed on v6.18.38.

Affected code:

kernel/bpf/verifier.c
kernel/bpf/states.c in current development trees
arch/s390/net/bpf_jit_comp.c

Required conditions:

Architecture: 64-bit s390x
Program type: BPF_PROG_TYPE_SOCKET_FILTER
Map type: BPF_MAP_TYPE_ARRAY
CONFIG_BPF_SYSCALL: enabled
BPF JIT: enabled and active
kernel.unprivileged_bpf_disabled: 0
Required capabilities: none
User namespace: not required

Root cause

The verifier records an ALU32 definition in subreg_def. A later 64-bit read
causes mark_insn_zext() to mark that instruction for explicit zero extension.
Before 107e16979905, a cache hit in is_state_visited() propagated the 64-bit
read to the alternate path's ALU32 definition.

Commit 107e16979905 removed that propagation when register-chain liveness was
replaced with static liveness. The static live-register mask does not track
read width or path-specific subreg_def information. In current states.c,
regs_exact() compares only fields before id, while subreg_def follows id in
struct bpf_reg_state. The SCALAR_VALUE case in regsafe() also does not compare
subreg_def. States with equal tracked scalar values but different ALU32
definitions can therefore compare as equal, leaving the pruned path's ALU32
instruction without a zero-extension mark.

On s390, ALU32 ADD updates the low 32 bits while preserving the register's
high half. EMIT_ZERO() emits llgfr only when the program-wide verifier_zext
flag is clear. If another ALU32 definition is marked, verifier_zext is set and
the JIT relies on each instruction's zext_dst flag. The pruned instruction
lacks that flag and can execute with attacker-controlled high bits intact.

Reproduction and impact

I ran the test as uid 1000 with all capability sets empty:

Linux syzkaller 6.18.38 s390x
kernel.unprivileged_bpf_disabled = 0
net.core.bpf_jit_enable = 1

The exploit disclosed array_map_ops through the existing map->ops pointer,
derived the KASLR slide, and obtained arbitrary kernel read and write
primitives. It used those primitives to locate and modify the calling
process's credentials. map->ops was read only and was not overwritten.

The resulting shell reported:

uid=0(root) gid=0(root) groups=0(root),27(sudo),100(users),1000(poc)
context=system_u:system_r:kernel_t:s0

A standalone C reproducer and exploit, verifier log, kernel configuration,
and build details are available privately on request.

Mitigation and fix direction

Setting kernel.unprivileged_bpf_disabled to a non-zero value prevents this
attack by unprivileged users. Disabling the s390 BPF JIT also prevents the
demonstrated mismatch.

I have not yet tested a patch. A conservative verifier fix may be to avoid
pruning live scalar states whose relevant subreg_def values differ, or to
preserve the read-width information needed to mark the current path's ALU32
instruction. As defense in depth, the s390 JIT could avoid relying only on
the program-wide verifier_zext flag. I can prepare a regression test and test
proposed changes in the s390x QEMU environment used for reproduction.

References

Regression-introducing commit:
https://github.com/torvalds/linux/commit/107e169799057bc6a379ddb625cbe1e51cfc7d72

Public review discussion related to read width and zero extension:
https://www.spinics.net/lists/bpf/msg139280.html

Merged patch discussion:
https://www.spinics.net/lists/bpf/msg152234.html

If appropriate, I would appreciate a Reported-by tag as:

Min-gyu Kim <gimm78064@xxxxxxxxx>

Please let me know if you need additional information or would like the
private reproducer and exploit.

Thank you for your time.

Best regards,
Min-gyu Kim