KCSAN: data-race in desc_read / prb_reserve_in_last

From: Jianzhou Zhao

Date: Tue Mar 10 2026 - 23:31:41 EST




Subject: [BUG] printk: KCSAN: data-race in desc_read / prb_reserve_in_last

Dear Maintainers,

We are writing to report a KCSAN-detected data-race vulnerability in the Linux kernel. This bug was found by our custom fuzzing tool, RacePilot. The bug occurs in the printk ringbuffer system, dealing with concurrent modifications and unannotated reads of the data block logical limits. We observed this on the Linux kernel version 6.18.0-08691-g2061f18ad76e-dirty.

Call Trace & Context
==================================================================
BUG: KCSAN: data-race in desc_read / prb_reserve_in_last

write to 0xffffffff869276a8 of 8 bytes by task 14248 on cpu 0:
data_realloc kernel/printk/printk_ringbuffer.c:1252 [inline]
prb_reserve_in_last+0x831/0xb20 kernel/printk/printk_ringbuffer.c:1529
vprintk_store+0x603/0x980 kernel/printk/printk.c:2283
vprintk_emit+0xfd/0x540 kernel/printk/printk.c:2412
vprintk_default+0x26/0x30 kernel/printk/printk.c:2451
vprintk+0x1d/0x30 kernel/printk/printk_safe.c:82
_printk+0x63/0x90 kernel/printk/printk.c:2461
disk_unlock_native_capacity block/partitions/core.c:520 [inline]
blk_add_partition block/partitions/core.c:543 [inline]
blk_add_partitions block/partitions/core.c:633 [inline]
bdev_disk_changed block/partitions/core.c:693 [inline]
bdev_disk_changed+0xae3/0xeb0 block/partitions/core.c:642
loop_reread_partitions+0x44/0xc0 drivers/block/loop.c:449
loop_set_status+0x41c/0x580 drivers/block/loop.c:1278
loop_set_status64 drivers/block/loop.c:1374 [inline]
lo_ioctl+0xf0/0x1170 drivers/block/loop.c:1560
blkdev_ioctl+0x377/0x420 block/ioctl.c:707
vfs_ioctl fs/ioctl.c:52 [inline]
__do_sys_ioctl fs/ioctl.c:605 [inline]
__se_sys_ioctl fs/ioctl.c:591 [inline]
__x64_sys_ioctl+0x121/0x170 fs/ioctl.c:591
x64_sys_call+0xc3a/0x2030 arch/x86/include/generated/asm/syscalls_64.h:17
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0xae/0x2c0 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f

read to 0xffffffff869276a0 of 16 bytes by task 3004 on cpu 1:
desc_read+0x115/0x250 kernel/printk/printk_ringbuffer.c:499
desc_read_finalized_seq+0x40/0x140 kernel/printk/printk_ringbuffer.c:1972
prb_read kernel/printk/printk_ringbuffer.c:2018 [inline]
_prb_read_valid+0xc1/0x550 kernel/printk/printk_ringbuffer.c:2213
prb_read_valid_info+0x74/0xa0 kernel/printk/printk_ringbuffer.c:2321
devkmsg_poll+0xa1/0x120 kernel/printk/printk.c:906
vfs_poll include/linux/poll.h:82 [inline]
ep_item_poll.isra.0+0xb0/0x110 fs/eventpoll.c:1059
ep_send_events+0x231/0x670 fs/eventpoll.c:1818
ep_try_send_events fs/eventpoll.c:1905 [inline]
ep_poll fs/eventpoll.c:1970 [inline]
do_epoll_wait+0x2a8/0x9c0 fs/eventpoll.c:2461
__do_sys_epoll_wait fs/eventpoll.c:2469 [inline]
__se_sys_epoll_wait fs/eventpoll.c:2464 [inline]
__x64_sys_epoll_wait+0xcb/0x190 fs/eventpoll.c:2464
x64_sys_call+0x194e/0x2030 arch/x86/include/generated/asm/syscalls_64.h:233
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0xae/0x2c0 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f

Reported by Kernel Concurrency Sanitizer on:
CPU: 1 UID: 0 PID: 3004 Comm: systemd-journal Not tainted 6.18.0-08691-g2061f18ad76e-dirty #50 PREEMPT(voluntary)
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
==================================================================

Execution Flow & Code Context
The writer task executes `prb_reserve_in_last()`, which concurrently reallocates and extends space for the newest record data by calling `data_realloc()`. Inside `data_realloc()`, it updates `blk_lpos->next` (which points to `desc->text_blk_lpos.next`) with a plain write:
```c
// kernel/printk/printk_ringbuffer.c
static char *data_realloc(struct printk_ringbuffer *rb, unsigned int size,
struct prb_data_blk_lpos *blk_lpos, unsigned long id)
{
...
blk_lpos->next = next_lpos; // <-- Write
return &blk->data[0];
}
```

Concurrently, another task acting as a reader accesses the ringbuffer via `desc_read()` to copy the descriptor structure to a local copy `desc_out`. It uses an unsynchronized `memcpy()` to read the `text_blk_lpos` structure which includes the 16 bytes representing both `begin` and `next` positions:
```c
// kernel/printk/printk_ringbuffer.c
static enum desc_state desc_read(struct prb_desc_ring *desc_ring,
unsigned long id, struct prb_desc *desc_out,
u64 *seq_out, u32 *caller_id_out)
{
...
if (desc_out) {
memcpy(&desc_out->text_blk_lpos, &desc->text_blk_lpos,
sizeof(desc_out->text_blk_lpos)); /* LMM(desc_read:C) */ // <-- Lockless Read
}
...
}
```

Root Cause Analysis
A data race occurs because the writer modifies `desc->text_blk_lpos.next` (via `blk_lpos->next = next_lpos` in `data_realloc()`) without concurrency synchronization, while a reader copies the entire `text_blk_lpos` structure (via `memcpy()`) concurrently. Since plain writes and `memcpy` lack atomic annotations, the compiler is free to tear the reads and stores or optimize them under the assumption of exclusivity. This can result in a torn or partially updated `text_blk_lpos` record being copied by the reader.
Unfortunately, we were unable to generate a reproducer for this bug.

Potential Impact
If `memcpy` in `desc_read()` tears the read, `desc_out->text_blk_lpos` may end up containing a logically inconsistent `begin` and `next` pair (e.g., an obsolete `begin` combined with an updated `next`, or simply a mangled 64-bit value). This could cause the printk readers to miscalculate the bounds of the data block, leading to reading corrupted text data, traversing out-of-bounds in the text data ring, or encountering infinite loops within the reader utilities, thereby causing a local DoS or syslog corruption.

Proposed Fix
To avoid tearing and resolve the sanitiser warnings while adhering to the memory model, we should replace the plain `memcpy` in `desc_read()` with individual `READ_ONCE()` calls for the `begin` and `next` fields. In addition, we should proactively wrap the concurrent assignments to `blk_lpos` in `data_alloc()` and `data_realloc()` with `WRITE_ONCE()`.

```diff
--- a/kernel/printk/printk_ringbuffer.c
+++ b/kernel/printk/printk_ringbuffer.c
@@ -496,8 +496,8 @@ static enum desc_state desc_read(struct prb_desc_ring *desc_ring,
* cannot be used because of the atomic_t @state_var field.
*/
if (desc_out) {
- memcpy(&desc_out->text_blk_lpos, &desc->text_blk_lpos,
- sizeof(desc_out->text_blk_lpos)); /* LMM(desc_read:C) */
+ desc_out->text_blk_lpos.begin = READ_ONCE(desc->text_blk_lpos.begin);
+ desc_out->text_blk_lpos.next = READ_ONCE(desc->text_blk_lpos.next);
}
if (seq_out) {
*seq_out = info->seq; /* also part of desc_read:C */
@@ -1083,8 +1083,8 @@ static char *data_alloc(struct printk_ringbuffer *rb, unsigned int size,
* reader will recognize these special lpos values and handle
* it appropriately.
*/
- blk_lpos->begin = EMPTY_LINE_LPOS;
- blk_lpos->next = EMPTY_LINE_LPOS;
+ WRITE_ONCE(blk_lpos->begin, EMPTY_LINE_LPOS);
+ WRITE_ONCE(blk_lpos->next, EMPTY_LINE_LPOS);
return NULL;
}

@@ -1107,8 +1107,8 @@ static char *data_alloc(struct printk_ringbuffer *rb, unsigned int size,
if (WARN_ON_ONCE(next_lpos - begin_lpos >
DATA_SIZE(data_ring)) ||
!data_push_tail(rb, next_lpos - DATA_SIZE(data_ring))) {
- blk_lpos->begin = FAILED_LPOS;
- blk_lpos->next = FAILED_LPOS;
+ WRITE_ONCE(blk_lpos->begin, FAILED_LPOS);
+ WRITE_ONCE(blk_lpos->next, FAILED_LPOS);
return NULL;
}

@@ -1148,8 +1148,8 @@ static char *data_alloc(struct printk_ringbuffer *rb, unsigned int size,
blk->id = id;
}

- blk_lpos->begin = begin_lpos;
- blk_lpos->next = next_lpos;
+ WRITE_ONCE(blk_lpos->begin, begin_lpos);
+ WRITE_ONCE(blk_lpos->next, next_lpos);

return &blk->data[0];
}
@@ -1249,7 +1249,7 @@ static char *data_realloc(struct printk_ringbuffer *rb, unsigned int size,
}
}

- blk_lpos->next = next_lpos;
+ WRITE_ONCE(blk_lpos->next, next_lpos);

return &blk->data[0];
}
```

We would be highly honored if this could be of any help.

Best regards,
RacePilot Team