[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