| 0/0 |
2026/08/09 08:16 |
flow |
assessment-security |
42m
Results: map[DenialOfService:true Explanation:Based on the analysis of the bug report and the kernel source code, here is the
assessment of the vulnerability:
**Bug Mechanism:**
The bug is a NULL pointer dereference in `fput()` triggered by a race condition
in `o2hb_region_dev_store()` within the OCFS2 cluster heartbeat code
(`fs/ocfs2/cluster/heartbeat.c`).
When multiple threads concurrently write to the `dev` attribute of an OCFS2
heartbeat region in configfs, they can both pass the initial `if
(reg->hr_bdev_file)` check since it is not protected by a lock. Both threads
will then open the block device and overwrite the shared `reg->hr_bdev_file`
pointer.
If one thread encounters an error later in the function (e.g., due to a block
size mismatch or slot mapping failure), it jumps to the `out` error-handling
label, calls `fput(reg->hr_bdev_file)`, and sets `reg->hr_bdev_file = NULL`.
When the second thread also fails and reaches the `out` label, it reads the now
`NULL` pointer and passes it to `fput()`, which unconditionally dereferences it,
causing a kernel crash.
Additionally, this race condition can lead to a **Use-After-Free**. If one
thread successfully starts the heartbeat thread while the other thread fails and
frees the `struct file`, the heartbeat thread will continue to use the freed
`struct file` (e.g., in `reg_bdev()`), leading to memory corruption.
**Security Impact:**
* **Exploitable:** Yes. While the immediate crash is a NULL pointer dereference
(which typically results in a Denial of Service), the underlying race condition
can also cause a Use-After-Free of a `struct file` object if the threads
interleave differently. `struct file` UAFs are generally considered exploitable
for privilege escalation or arbitrary code execution.
* **Denial of Service:** Yes. The NULL pointer dereference will cause a kernel
oops, killing the triggering process and potentially panicking the system if
`panic_on_oops` is enabled.
* **Accessible From Unprivileged Processes:** No. The `dev` attribute in
configfs is created with `0644` permissions and is owned by root. Unprivileged
users cannot write to it.
* **Accessible From User Namespaces:** No. Configfs does not have the
`FS_USERNS_MOUNT` flag, meaning it cannot be mounted inside an unprivileged user
namespace. Even if it is already mounted by the host, the files are owned by the
global root (`GLOBAL_ROOT_UID`), so a root user inside a user namespace will not
have the necessary DAC overrides to write to them.
**Other Triggers:**
The bug cannot be triggered via network traffic, malicious filesystems,
peripheral devices, or from a VM guest/host context. It strictly requires local
root access to write to the specific configfs interface. Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/08/09 08:16 |
action |
syz-repro-to-c-repro |
0m
Results: map[SimplifiedCRepro:// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
uint64_t r[9] = {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff};
int main(void)
{
syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
const char* reason;
(void)reason;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 00} (length 0x9)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000000, "./config\000", 9);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000000ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// mount arguments: [
// src: nil
// dst: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 00} (length 0x9)
// }
// type: ptr[in, buffer] {
// buffer: {63 6f 6e 66 69 67 66 73 00} (length 0x9)
// }
// flags: mount_flags = 0x0 (8 bytes)
// data: nil
// ]
memcpy((void*)0x200000000000, "./config\000", 9);
memcpy((void*)0x200000000040, "configfs\000", 9);
syscall(__NR_mount, /*src=*/0ul, /*dst=*/0x200000000000ul, /*type=*/0x200000000040ul, /*flags=*/0ul, /*data=*/0ul);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 00} (length 0x1b)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000080, "./config/cluster/mycluster\000", 27);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000080ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 00} (length 0x27)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000100, "./config/cluster/mycluster/node/mynode\000", 39);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000100ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 69 70 76 34 5f 61 64 64 72 65 73 73 00} (length 0x34)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000140, "./config/cluster/mycluster/node/mynode/ipv4_address\000", 52);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000140ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[0] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 32 37 2e 30 2e 30 2e 31 00} (length 0xa)
// }
// count: len = 0x9 (8 bytes)
// ]
memcpy((void*)0x200000000180, "127.0.0.1\000", 10);
syscall(__NR_write, /*fd=*/r[0], /*buf=*/0x200000000180ul, /*count=*/9ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[0]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 69 70 76 34 5f 70 6f 72 74 00} (length 0x31)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x2000000001c0, "./config/cluster/mycluster/node/mynode/ipv4_port\000", 49);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x2000000001c0ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[1] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {37 37 37 37 00} (length 0x5)
// }
// count: len = 0x4 (8 bytes)
// ]
memcpy((void*)0x200000000200, "7777\000", 5);
syscall(__NR_write, /*fd=*/r[1], /*buf=*/0x200000000200ul, /*count=*/4ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[1]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 6e 75 6d 00} (length 0x2b)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000240, "./config/cluster/mycluster/node/mynode/num\000", 43);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000240ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[2] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {30 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x200000000280, "0\000", 2);
syscall(__NR_write, /*fd=*/r[2], /*buf=*/0x200000000280ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[2]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 6c 6f 63 61 6c 00} (length 0x2d)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x2000000002c0, "./config/cluster/mycluster/node/mynode/local\000", 45);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x2000000002c0ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[3] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x200000000300, "1\000", 2);
syscall(__NR_write, /*fd=*/r[3], /*buf=*/0x200000000300ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[3]);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 00} (length 0x2e)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000340, "./config/cluster/mycluster/heartbeat/myregion\000", 46);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000340ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 62 6c 6f 63 6b 5f 62 79 74 65 73 00} (length 0x3a)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000380, "./config/cluster/mycluster/heartbeat/myregion/block_bytes\000", 58);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000380ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[4] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {35 31 32 00} (length 0x4)
// }
// count: len = 0x3 (8 bytes)
// ]
memcpy((void*)0x2000000003c0, "512\000", 4);
syscall(__NR_write, /*fd=*/r[4], /*buf=*/0x2000000003c0ul, /*count=*/3ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[4]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 62 6c 6f 63 6b 73 00} (length 0x35)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000400, "./config/cluster/mycluster/heartbeat/myregion/blocks\000", 53);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000400ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[5] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {32 35 35 00} (length 0x4)
// }
// count: len = 0x3 (8 bytes)
// ]
memcpy((void*)0x200000000440, "255\000", 4);
syscall(__NR_write, /*fd=*/r[5], /*buf=*/0x200000000440ul, /*count=*/3ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[5]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 73 74 61 72 74 5f 62 6c 6f 63 6b 00} (length 0x3a)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000480, "./config/cluster/mycluster/heartbeat/myregion/start_block\000", 58);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000480ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[6] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x2000000004c0, "1\000", 2);
syscall(__NR_write, /*fd=*/r[6], /*buf=*/0x2000000004c0ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[6]);
// mknodat arguments: [
// dirfd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 6c 6f 6f 70 30 00} (length 0x8)
// }
// mode: mknod_mode = 0x61c0 (8 bytes)
// dev: int32 = 0x700 (4 bytes)
// ]
memcpy((void*)0x200000000540, "./loop0\000", 8);
syscall(__NR_mknodat, /*dirfd=*/0xffffff9c, /*file=*/0x200000000540ul, /*mode=S_IFBLK|0x1c0*/0x61c0ul, /*dev=*/0x700);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 6c 6f 6f 70 30 00} (length 0x8)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000540, "./loop0\000", 8);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000540ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[7] = res;
// ioctl$LOOP_SET_FD arguments: [
// fd: fd_loop (resource)
// cmd: const = 0x4c00 (4 bytes)
// arg: fd (resource)
// ]
syscall(__NR_ioctl, /*fd=*/r[7], /*cmd=*/0x4c00, /*arg=*/(intptr_t)-1);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 64 65 76 00} (length 0x32)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000580, "./config/cluster/mycluster/heartbeat/myregion/dev\000", 50);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000580ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[8] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {33 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x2000000005c0, "3\000", 2);
syscall(__NR_write, /*fd=*/r[8], /*buf=*/0x2000000005c0ul, /*count=*/1ul);
return 0;
}
]
|
| 2/1 |
2026/08/09 08:16 |
action |
kernel-checkouter |
0m
Results: map[KernelSrc:/app/workdir/cache/src/0bd482cc9fd6a5c3cdf3e7f636a3906397462859]
|
| 3/1 |
2026/08/09 08:16 |
action |
kernel-builder |
27m
Results: map[KernelObj:/app/workdir/cache/build/c6c7b2d9e4d1d7e5a1cffa7566d7d92fb1f9026a]
|
| 4/1 |
2026/08/09 08:43 |
action |
codesearch-prepare |
11m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/08/09 08:55 |
agent |
expert |
3m
Model:
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption, elevated privileges, or an information leak.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Information leaks are exploitable on their own and should be classified as such. A bug that copies kernel
memory contents to userspace (e.g. an out-of-bounds read whose result is returned to the caller, or
uninitialized stack/heap bytes written to a user buffer) is exploitable: it can reveal kernel pointer
values and defeat KASLR, expose sensitive data such as cryptographic keys or other processes' memory, and
serves as a necessary building block in most modern kernel privilege-escalation exploit chains. Do not classify
an information leak as non-exploitable solely because it does not directly cause a memory write or control-flow
hijack; the leak itself is the exploit primitive.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The kernel bug report is:
(syz-executor157,5664,1):o2hb_bio_end_io:499 ERROR: IO Error 10
(syz-executor157,5664,1):o2hb_region_dev_store:1850 ERROR: status = -5
==================================================================
BUG: KASAN: null-ptr-deref in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
BUG: KASAN: null-ptr-deref in atomic_long_dec_return include/linux/atomic/atomic-instrumented.h:3766 [inline]
BUG: KASAN: null-ptr-deref in file_ref_put include/linux/file_ref.h:157 [inline]
BUG: KASAN: null-ptr-deref in fput+0x2e/0x100 fs/file_table.c:588
Write of size 8 at addr 0000000000000158 by task syz-executor157/5664
CPU: 1 UID: 0 PID: 5664 Comm: syz-executor157 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 07/16/2026
Call Trace:
<TASK>
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
kasan_report+0xdf/0x1c0 mm/kasan/report.c:595
check_region_inline mm/kasan/generic.c:186 [inline]
kasan_check_range+0x10f/0x1e0 mm/kasan/generic.c:200
instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
atomic_long_dec_return include/linux/atomic/atomic-instrumented.h:3766 [inline]
file_ref_put include/linux/file_ref.h:157 [inline]
fput+0x2e/0x100 fs/file_table.c:588
o2hb_region_dev_store+0x1230/0x1bc0 fs/ocfs2/cluster/heartbeat.c:1935
flush_write_buffer fs/configfs/file.c:207 [inline]
configfs_write_iter+0x302/0x4e0 fs/configfs/file.c:229
new_sync_write fs/read_write.c:595 [inline]
vfs_write+0x6ac/0x1050 fs/read_write.c:687
ksys_write+0x12a/0x250 fs/read_write.c:739
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x115/0x870 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fd8eccb5ab9
Code: c0 79 93 eb d5 48 8d 7c 1d 00 eb 99 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 c7 c1 d8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007ffc7d881e78 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
RAX: ffffffffffffffda RBX: 0000200000000540 RCX: 00007fd8eccb5ab9
RDX: 0000000000000001 RSI: 00002000000005c0 RDI: 0000000000000004
RBP: 0030706f6f6c2f2e R08: 0000000000000009 R09: 0000000000000009
R10: 0000000000000009 R11: 0000000000000246 R12: 000000000001009b
R13: 00007ffc7d881e8c R14: 00007ffc7d881ea0 R15: 0000200000000008
</TASK>
==================================================================
It is reproducible with the followint program.
Keep in mind that it may lack the precise threading, sandboxing, and some arguments of a working reproducer.
But it should give an idea of the involved syscalls.
// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
uint64_t r[9] = {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff};
int main(void)
{
syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
const char* reason;
(void)reason;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 00} (length 0x9)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000000, "./config\000", 9);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000000ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// mount arguments: [
// src: nil
// dst: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 00} (length 0x9)
// }
// type: ptr[in, buffer] {
// buffer: {63 6f 6e 66 69 67 66 73 00} (length 0x9)
// }
// flags: mount_flags = 0x0 (8 bytes)
// data: nil
// ]
memcpy((void*)0x200000000000, "./config\000", 9);
memcpy((void*)0x200000000040, "configfs\000", 9);
syscall(__NR_mount, /*src=*/0ul, /*dst=*/0x200000000000ul, /*type=*/0x200000000040ul, /*flags=*/0ul, /*data=*/0ul);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 00} (length 0x1b)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000080, "./config/cluster/mycluster\000", 27);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000080ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 00} (length 0x27)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000100, "./config/cluster/mycluster/node/mynode\000", 39);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000100ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 69 70 76 34 5f 61 64 64 72 65 73 73 00} (length 0x34)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000140, "./config/cluster/mycluster/node/mynode/ipv4_address\000", 52);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000140ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[0] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 32 37 2e 30 2e 30 2e 31 00} (length 0xa)
// }
// count: len = 0x9 (8 bytes)
// ]
memcpy((void*)0x200000000180, "127.0.0.1\000", 10);
syscall(__NR_write, /*fd=*/r[0], /*buf=*/0x200000000180ul, /*count=*/9ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[0]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 69 70 76 34 5f 70 6f 72 74 00} (length 0x31)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x2000000001c0, "./config/cluster/mycluster/node/mynode/ipv4_port\000", 49);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x2000000001c0ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[1] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {37 37 37 37 00} (length 0x5)
// }
// count: len = 0x4 (8 bytes)
// ]
memcpy((void*)0x200000000200, "7777\000", 5);
syscall(__NR_write, /*fd=*/r[1], /*buf=*/0x200000000200ul, /*count=*/4ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[1]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 6e 75 6d 00} (length 0x2b)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000240, "./config/cluster/mycluster/node/mynode/num\000", 43);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000240ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[2] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {30 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x200000000280, "0\000", 2);
syscall(__NR_write, /*fd=*/r[2], /*buf=*/0x200000000280ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[2]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 6e 6f 64 65 2f 6d 79 6e 6f 64 65 2f 6c 6f 63 61 6c 00} (length 0x2d)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x2000000002c0, "./config/cluster/mycluster/node/mynode/local\000", 45);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x2000000002c0ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[3] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x200000000300, "1\000", 2);
syscall(__NR_write, /*fd=*/r[3], /*buf=*/0x200000000300ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[3]);
// mkdirat arguments: [
// fd: fd_dir (resource)
// path: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 00} (length 0x2e)
// }
// mode: open_mode = 0x1ff (8 bytes)
// ]
memcpy((void*)0x200000000340, "./config/cluster/mycluster/heartbeat/myregion\000", 46);
syscall(__NR_mkdirat, /*fd=*/0xffffff9c, /*path=*/0x200000000340ul, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1fful);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 62 6c 6f 63 6b 5f 62 79 74 65 73 00} (length 0x3a)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000380, "./config/cluster/mycluster/heartbeat/myregion/block_bytes\000", 58);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000380ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[4] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {35 31 32 00} (length 0x4)
// }
// count: len = 0x3 (8 bytes)
// ]
memcpy((void*)0x2000000003c0, "512\000", 4);
syscall(__NR_write, /*fd=*/r[4], /*buf=*/0x2000000003c0ul, /*count=*/3ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[4]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 62 6c 6f 63 6b 73 00} (length 0x35)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000400, "./config/cluster/mycluster/heartbeat/myregion/blocks\000", 53);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000400ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[5] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {32 35 35 00} (length 0x4)
// }
// count: len = 0x3 (8 bytes)
// ]
memcpy((void*)0x200000000440, "255\000", 4);
syscall(__NR_write, /*fd=*/r[5], /*buf=*/0x200000000440ul, /*count=*/3ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[5]);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 73 74 61 72 74 5f 62 6c 6f 63 6b 00} (length 0x3a)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000480, "./config/cluster/mycluster/heartbeat/myregion/start_block\000", 58);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000480ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[6] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {31 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x2000000004c0, "1\000", 2);
syscall(__NR_write, /*fd=*/r[6], /*buf=*/0x2000000004c0ul, /*count=*/1ul);
// close arguments: [
// fd: fd (resource)
// ]
syscall(__NR_close, /*fd=*/r[6]);
// mknodat arguments: [
// dirfd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 6c 6f 6f 70 30 00} (length 0x8)
// }
// mode: mknod_mode = 0x61c0 (8 bytes)
// dev: int32 = 0x700 (4 bytes)
// ]
memcpy((void*)0x200000000540, "./loop0\000", 8);
syscall(__NR_mknodat, /*dirfd=*/0xffffff9c, /*file=*/0x200000000540ul, /*mode=S_IFBLK|0x1c0*/0x61c0ul, /*dev=*/0x700);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 6c 6f 6f 70 30 00} (length 0x8)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000540, "./loop0\000", 8);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000540ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[7] = res;
// ioctl$LOOP_SET_FD arguments: [
// fd: fd_loop (resource)
// cmd: const = 0x4c00 (4 bytes)
// arg: fd (resource)
// ]
syscall(__NR_ioctl, /*fd=*/r[7], /*cmd=*/0x4c00, /*arg=*/(intptr_t)-1);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 63 6f 6e 66 69 67 2f 63 6c 75 73 74 65 72 2f 6d 79 63 6c 75 73 74 65 72 2f 68 65 61 72 74 62 65 61 74 2f 6d 79 72 65 67 69 6f 6e 2f 64 65 76 00} (length 0x32)
// }
// flags: open_flags = 0x2 (4 bytes)
// mode: open_mode = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000580, "./config/cluster/mycluster/heartbeat/myregion/dev\000", 50);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000580ul, /*flags=O_RDWR*/2, /*mode=*/0);
if (res != -1)
r[8] = res;
// write arguments: [
// fd: fd (resource)
// buf: ptr[in, buffer] {
// buffer: {33 00} (length 0x2)
// }
// count: len = 0x1 (8 bytes)
// ]
memcpy((void*)0x2000000005c0, "3\000", 2);
syscall(__NR_write, /*fd=*/r[8], /*buf=*/0x2000000005c0ul, /*count=*/1ul);
return 0;
}
Reply: Based on the analysis of the bug report and the kernel source code, here is the assessment of the vulnerability:
**Bug Mechanism:**
The bug is a NULL pointer dereference in `fput()` triggered by a race condition in `o2hb_region_dev_store()` within the OCFS2 cluster heartbeat code (`fs/ocfs2/cluster/heartbeat.c`).
When multiple threads concurrently write to the `dev` attribute of an OCFS2 heartbeat region in configfs, they can both pass the initial `if (reg->hr_bdev_file)` check since it is not protected by a lock. Both threads will then open the block device and overwrite the shared `reg->hr_bdev_file` pointer.
If one thread encounters an error later in the function (e.g., due to a block size mismatch or slot mapping failure), it jumps to the `out` error-handling label, calls `fput(reg->hr_bdev_file)`, and sets `reg->hr_bdev_file = NULL`. When the second thread also fails and reaches the `out` label, it reads the now `NULL` pointer and passes it to `fput()`, which unconditionally dereferences it, causing a kernel crash.
Additionally, this race condition can lead to a **Use-After-Free**. If one thread successfully starts the heartbeat thread while the other thread fails and frees the `struct file`, the heartbeat thread will continue to use the freed `struct file` (e.g., in `reg_bdev()`), leading to memory corruption.
**Security Impact:**
* **Exploitable:** Yes. While the immediate crash is a NULL pointer dereference (which typically results in a Denial of Service), the underlying race condition can also cause a Use-After-Free of a `struct file` object if the threads interleave differently. `struct file` UAFs are generally considered exploitable for privilege escalation or arbitrary code execution.
* **Denial of Service:** Yes. The NULL pointer dereference will cause a kernel oops, killing the triggering process and potentially panicking the system if `panic_on_oops` is enabled.
* **Accessible From Unprivileged Processes:** No. The `dev` attribute in configfs is created with `0644` permissions and is owned by root. Unprivileged users cannot write to it.
* **Accessible From User Namespaces:** No. Configfs does not have the `FS_USERNS_MOUNT` flag, meaning it cannot be mounted inside an unprivileged user namespace. Even if it is already mounted by the host, the files are owned by the global root (`GLOBAL_ROOT_UID`), so a root user inside a user namespace will not have the necessary DAC overrides to write to them.
**Other Triggers:**
The bug cannot be triggered via network traffic, malicious filesystems, peripheral devices, or from a VM guest/host context. It strictly requires local root access to write to the specific configfs interface.
|
| 6/2 |
2026/08/09 08:55 |
llm |
expert |
0m
Model:
Tokens:
input: 14042
output: 45
thoughts: 391
Thoughts: **Analyzing a KASAN Null-Pointer Dereference in OCFS2 Heartbeat**
Okay, so I've got a KASAN null-pointer dereference, specifically in `file_ref_put`, which is triggered through `fput`. This is happening within the `o2hb_region_dev_store` function. The bug report is pointing to a write of size 8 to address 0x158, clearly a memory access violation.
The stack trace provided is key here. It shows the call path: `fput` -> `o2hb_region_dev_store` -> `configfs_write_iter`, with a stop in `configfs/file.c`. My immediate thought is, "What's `o2hb_region_dev_store` doing with a file descriptor that's causing this issue?" It seems like the file is coming from `configfs`, based on the trace. I need to examine `fs/ocfs2/cluster/heartbeat.c` at line 1935, which is where the problem is originating, to fully understand the context. This must be the part that's calling `fput` and likely passing a bad file pointer. From there, I need to understand what's happening during the `configfs_write_iter` call, because it looks like we're writing something using a bad file pointer to the file, and that writes are the result of the `configfs` implementation. Let's see if we can deduce what the bad file is and why it's bad.
|
| 7/2 |
2026/08/09 08:55 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_region_dev_store]
Results: map[SourceCode:1773: /*
1774: * this is acting as commit; we set up all of hr_bdev_file and hr_task or
1775: * nothing
1776: */
1777: static ssize_t o2hb_region_dev_store(struct config_item *item,
1778: const char *page,
1779: size_t count)
1780: {
1781: struct o2hb_region *reg = to_o2hb_region(item);
1782: struct task_struct *hb_task;
1783: long fd;
1784: int sectsize;
1785: char *p = (char *)page;
1786: ssize_t ret = -EINVAL;
1787: int live_threshold;
1788:
1789: if (reg->hr_bdev_file)
1790: return -EINVAL;
1791:
1792: /* We can't heartbeat without having had our node number
1793: * configured yet. */
1794: if (o2nm_this_node() == O2NM_MAX_NODES)
1795: return -EINVAL;
1796:
1797: ret = kstrtol(p, 0, &fd);
1798: if (ret < 0)
1799: return -EINVAL;
1800:
1801: if (fd < 0 || fd >= INT_MAX)
1802: return -EINVAL;
1803:
1804: CLASS(fd, f)(fd);
1805: if (fd_empty(f))
1806: return -EINVAL;
1807:
1808: if (reg->hr_blocks == 0 || reg->hr_start_block == 0 ||
1809: reg->hr_block_bytes == 0)
1810: return -EINVAL;
1811:
1812: if (!S_ISBLK(fd_file(f)->f_mapping->host->i_mode))
1813: return -EINVAL;
1814:
1815: reg->hr_bdev_file = bdev_file_open_by_dev(fd_file(f)->f_mapping->host->i_rdev,
1816: BLK_OPEN_WRITE | BLK_OPEN_READ, NULL, NULL);
1817: if (IS_ERR(reg->hr_bdev_file)) {
1818: ret = PTR_ERR(reg->hr_bdev_file);
1819: reg->hr_bdev_file = NULL;
1820: return ret;
1821: }
1822:
1823: sectsize = bdev_logical_block_size(reg_bdev(reg));
1824: if (sectsize != reg->hr_block_bytes) {
1825: mlog(ML_ERROR,
1826: "blocksize %u incorrect for device, expected %d",
1827: reg->hr_block_bytes, sectsize);
1828: ret = -EINVAL;
1829: goto out;
1830: }
1831:
1832: reg->hr_aborted_start = 0;
1833: reg->hr_node_deleted = 0;
1834: o2hb_init_region_params(reg);
1835:
1836: /* Generation of zero is invalid */
1837: do {
1838: get_random_bytes(®->hr_generation,
1839: sizeof(reg->hr_generation));
1840: } while (reg->hr_generation == 0);
1841:
1842: ret = o2hb_map_slot_data(reg);
1843: if (ret) {
1844: mlog_errno(ret);
1845: goto out;
1846: }
1847:
1848: ret = o2hb_populate_slot_data(reg);
1849: if (ret) {
1850: mlog_errno(ret);
1851: goto out;
1852: }
1853:
1854: INIT_DELAYED_WORK(®->hr_write_timeout_work, o2hb_write_timeout);
1855: INIT_DELAYED_WORK(®->hr_nego_timeout_work, o2hb_nego_timeout);
1856:
1857: /*
1858: * A node is considered live after it has beat LIVE_THRESHOLD
1859: * times. We're not steady until we've given them a chance
1860: * _after_ our first read.
1861: * The default threshold is bare minimum so as to limit the delay
1862: * during mounts. For global heartbeat, the threshold doubled for the
1863: * first region.
1864: */
1865: live_threshold = O2HB_LIVE_THRESHOLD;
1866: if (o2hb_global_heartbeat_active()) {
1867: spin_lock(&o2hb_live_lock);
1868: if (bitmap_weight(o2hb_region_bitmap, O2NM_MAX_REGIONS) == 1)
1869: live_threshold <<= 1;
1870: spin_unlock(&o2hb_live_lock);
1871: }
1872: ++live_threshold;
1873: atomic_set(®->hr_steady_iterations, live_threshold);
1874: /* unsteady_iterations is triple the steady_iterations */
1875: atomic_set(®->hr_unsteady_iterations, (live_threshold * 3));
1876:
1877: hb_task = kthread_run(o2hb_thread, reg, "o2hb-%s",
1878: reg->hr_item.ci_name);
1879: if (IS_ERR(hb_task)) {
1880: ret = PTR_ERR(hb_task);
1881: mlog_errno(ret);
1882: goto out;
1883: }
1884:
1885: spin_lock(&o2hb_live_lock);
1886: reg->hr_task = hb_task;
1887: spin_unlock(&o2hb_live_lock);
1888:
1889: ret = wait_event_interruptible(o2hb_steady_queue,
1890: atomic_read(®->hr_steady_iterations) == 0 ||
1891: reg->hr_node_deleted);
1892: if (ret) {
1893: atomic_set(®->hr_steady_iterations, 0);
1894: reg->hr_aborted_start = 1;
1895: }
1896:
1897: if (reg->hr_aborted_start) {
1898: ret = -EIO;
1899: goto out;
1900: }
1901:
1902: if (reg->hr_node_deleted) {
1903: ret = -EINVAL;
1904: goto out;
1905: }
1906:
1907: /* Ok, we were woken. Make sure it wasn't by drop_item() */
1908: spin_lock(&o2hb_live_lock);
1909: hb_task = reg->hr_task;
1910: if (o2hb_global_heartbeat_active())
1911: set_bit(reg->hr_region_num, o2hb_live_region_bitmap);
1912: spin_unlock(&o2hb_live_lock);
1913:
1914: if (hb_task)
1915: ret = count;
1916: else
1917: ret = -EIO;
1918:
1919: if (hb_task && o2hb_global_heartbeat_active())
1920: printk(KERN_NOTICE "o2hb: Heartbeat started on region %s (%pg)\n",
1921: config_item_name(®->hr_item), reg_bdev(reg));
1922:
1923: out:
1924: if (ret < 0) {
1925: spin_lock(&o2hb_live_lock);
1926: hb_task = reg->hr_task;
1927: reg->hr_task = NULL;
1928: spin_unlock(&o2hb_live_lock);
1929:
1930: if (hb_task)
1931: kthread_stop(hb_task);
1932:
1933: o2hb_unmap_slot_data(reg);
1934:
1935: fput(reg->hr_bdev_file);
1936: reg->hr_bdev_file = NULL;
1937: }
1938: return ret;
1939: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 8/2 |
2026/08/09 08:55 |
llm |
expert |
0m
Model:
Tokens:
input: 17391
output: 19
thoughts: 1210
Thoughts: **Debugging a Null Pointer Dereference in fput**
Okay, so the KASAN report points to a null-pointer dereference within `fput(reg->hr_bdev_file)` at line 1935. Immediately, I know that `fput` is the culprit. But wait, `fput` itself *shouldn't* handle NULL pointers, right? I'll need to dig deeper. `reg->hr_bdev_file` is a `struct file *`, and `fput` takes a `struct file *`, so that seems right.
I need to trace where `reg->hr_bdev_file` gets set. `bdev_file_open_by_dev` returns a `struct file *`, or potentially `ERR_PTR`. Let me see. Line 1815:
```c
reg->hr_bdev_file = bdev_file_open_by_dev(fd_file(f)->f_mapping->host->i_rdev,
BLK_OPEN_WRITE | BLK_OPEN_READ, NULL, NULL);
if (IS_ERR(reg->hr_bdev_file)) {
ret = PTR_ERR(reg->hr_bdev_file);
reg->hr_bdev_file = NULL;
return ret;
}
```
Okay, if `bdev_file_open_by_dev` succeeds, `reg->hr_bdev_file` is a valid file pointer. If it fails, it sets `reg->hr_bdev_file` to NULL and returns an error code. Then it goes to `out` label if something fails later. So, if `fput` is called with `reg->hr_bdev_file` being NULL, that's a problem.
Now, if `ret < 0`, `fput` is called unconditionally on line 1935. But wait, if there is a problem *before* `reg->hr_bdev_file` is set, the function returns early. What if `ret < 0` happens at line 1829?
```c
sectsize = bdev_logical_block_size(reg_bdev(reg));
if (sectsize != reg->hr_block_bytes) {
mlog(ML_ERROR,
"blocksize %u incorrect for device, expected %d",
reg->hr_block_bytes, sectsize);
ret = -EINVAL;
goto out;
}
```
If it goes to `out`, `reg->hr_bdev_file` is valid. What about `o2hb_map_slot_data`? If that fails and goes to `out`, it's valid as well.
```c
ret = o2hb_map_slot_data(reg);
if (ret) {
mlog_errno(ret);
goto out;
}
```
But what if, for some reason, `fput` is called after the file is closed or something similar? Looking at the definition of `fput`:
```c
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count)) {
...
}
}
```
Aha! If `file` is NULL, then `file->f_count` will be a null pointer dereference. So, is it possible that `reg->hr_bdev_file` is NULL when `goto out` is executed? Let me check all the `goto out` locations to confirm that `reg->hr_bdev_file` is not assigned prior to the `goto out` statement. Nope. They are all after the file pointer is set.
Wait a minute, what if the crash is NOT on a NULL `hr_bdev_file`, but instead it crashes inside the fput operation? This crash report from the debugger is more specific:
```
BUG: KASAN: null-ptr-deref in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
BUG: KASAN: null-ptr-deref in atomic_long_dec_return include/linux/atomic/atomic-instrumented.h:3766 [inline]
BUG: KASAN: null-ptr-deref in file_ref_put include/linux/file_ref.h:157 [inline]
BUG: KASAN: null-ptr-deref in fput+0x2e/
|
| 9/2 |
2026/08/09 08:55 |
tool |
codesearch-struct-layout |
0m
Args:
Results: map[Fields:[map[Name:f_lock OffsetBits:0 SizeBits:512] map[Name:f_mode OffsetBits:512 SizeBits:32] map[Name:f_op OffsetBits:576 SizeBits:64] map[Name:f_mapping OffsetBits:640 SizeBits:64] map[Name:private_data OffsetBits:704 SizeBits:64] map[Name:f_inode OffsetBits:768 SizeBits:64] map[Name:f_flags OffsetBits:832 SizeBits:32] map[Name:f_iocb_flags OffsetBits:864 SizeBits:32] map[Name:f_cred OffsetBits:896 SizeBits:64] map[Name:f_owner OffsetBits:960 SizeBits:64] map[Name: OffsetBits:1024 SizeBits:128] map[Name: OffsetBits:1152 SizeBits:1088] map[Name:f_pos OffsetBits:2240 SizeBits:64] map[Name:f_security OffsetBits:2304 SizeBits:64] map[Name:f_wb_err OffsetBits:2368 SizeBits:32] map[Name:f_sb_err OffsetBits:2400 SizeBits:32] map[Name:f_ep OffsetBits:2432 SizeBits:64] map[Name: OffsetBits:2496 SizeBits:256] map[Name:f_ref OffsetBits:2752 SizeBits:64]]]
|
| 10/2 |
2026/08/09 08:55 |
llm |
expert |
0m
Model:
Tokens:
input: 20068
output: 37
thoughts: 727
Thoughts: **Debugging Concurrency in `o2hb_region_dev_store`**
Okay, so I'm tracing through this code, and it's hitting me that `f_ref` is at offset 2752 bits, which is 344 bytes, or `0x158`. This points me directly to the issue where `file` is indeed NULL. But now I'm confused β how can `reg->hr_bdev_file` be NULL when a `goto out` is executed? That doesn't add up.
Let's revisit `o2hb_region_dev_store`. I see the logic flow: if `reg->hr_bdev_file` is already set, it returns -EINVAL. It checks a few other conditions before opening the block device file. The critical part is calling `bdev_file_open_by_dev`. Aha! When that call fails, it *doesn't* `goto out` directly. It returns `ret` immediately after setting `reg->hr_bdev_file = NULL`.
So, where else could `goto out` be triggered? Hmm, nothing obvious jumps out. This is a bit unsettling.
Could this be a concurrency issue? What if `o2hb_region_dev_store` is called concurrently? Does `configfs` use a lock to protect the store function? I don't *think* so, but let me double-check.
Okay, I've got it. I'm focusing on the `out:` label at the end of the function. I remember `reg->hr_bdev_file` is set to NULL within the `out` block:
```c
out:
if (ret < 0) {
spin_lock(&o2hb_live_lock);
hb_task = reg->hr_task;
reg->hr_task = NULL;
spin_unlock(&o2hb_live_lock);
if (hb_task)
kthread_stop(hb_task);
o2hb_unmap_slot_data(reg);
fput(reg->hr_bdev_file);
reg->hr_bdev_file = NULL;
}
return ret;
```
If multiple threads call `o2hb_region_dev_store` concurrently, and `configfs` doesn't provide locking, a race condition could exist. One thread might successfully open the block device file, but before it completes, another thread, failing in the `bdev_file_open_by_dev` call, sets `reg->hr_bdev_file` to NULL during its `out` path, and then the first thread proceeds to access a NULL pointer, leading to a crash.
I'm pretty certain now - `configfs` does *not* protect the `store` with a lock, so I need to check how `configfs_write_iter` works to confirm this.
|
| 11/2 |
2026/08/09 08:55 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/configfs/file.c Name:flush_write_buffer]
Results: map[SourceCode: 199: static int
200: flush_write_buffer(struct file *file, struct configfs_buffer *buffer, size_t count)
201: {
202: struct configfs_fragment *frag = to_frag(file);
203: int res = -ENOENT;
204:
205: down_read(&frag->frag_sem);
206: if (!frag->frag_dead)
207: res = buffer->attr->store(buffer->item, buffer->page, count);
208: up_read(&frag->frag_sem);
209: return res;
210: }
SourceFile:fs/configfs/file.c]
|
| 12/2 |
2026/08/09 08:55 |
llm |
expert |
0m
Model:
Tokens:
input: 21695
output: 41
thoughts: 1118
Thoughts: **Race Condition Analysis in `o2hb_region_dev_store`**
Okay, so I'm looking at this `o2hb_region_dev_store` function, and it seems like there's a serious concurrency issue here. The use of `down_read(&frag->frag_sem)` suggests that multiple threads can call `store` concurrently. My concern is that multiple threads could be running `o2hb_region_dev_store` at the same time. The code starts with a check: `if (reg->hr_bdev_file) return -EINVAL;`. If Thread 1 finds `reg->hr_bdev_file` is `NULL`, it proceeds to open the block device file and assigns the file descriptor to `reg->hr_bdev_file`. Simultaneously, Thread 2, also finding a `NULL` `reg->hr_bdev_file`, does the same.
The problem lies in what happens if one thread fails later on. If Thread 1 succeeds but then, later, fails for some reason (maybe `o2hb_map_slot_data` failing or something else), it's going to do: `fput(reg->hr_bdev_file); reg->hr_bdev_file = NULL;`. Now, if Thread 2 still thinks it owns `reg->hr_bdev_file`, it might then call `fput(reg->hr_bdev_file)` as well, resulting in a use-after-free or, if Thread 1 has already set it to `NULL`, a NULL pointer dereference! And I see, this is the reported issue: a `BUG: KASAN: null-ptr-deref in fput`.
This KASAN report indicates a write of size 8 at address 0x158 by `syz-executor`. Since `mmap_min_addr` is typically 65536, address `0x158` is unmapped, so it cannot be mapped by user space. A write to this address means a page fault in kernel mode, causing an Oops. If it's in a process context, the process will be killed. It's unlikely to cause a kernel panic unless `panic_on_oops` is set, but it will definitely be a denial-of-service, killing the task and potentially holding locks, which is not good.
It's essentially a classic race where multiple threads are competing to initialize and then de-initialize the `hr_bdev_file` without proper locking. And, as suspected, there is no other lock protecting `o2hb_region_dev_store`, only that check. This is not exploitable to escalate privileges, but still a Denial-of-Service.
I'm thinking deeper... what if both threads succeed in opening the file, but then one fails? If Thread 1 sets `reg->hr_bdev_file = file1;` and Thread 2 sets `reg->hr_bdev_file = file2;`, and then Thread 2 fails: It stops the `hb_task` (which was started by Thread 1), unmaps some data, and then calls `fput(reg->hr_bdev_file)`. This could lead to a use-after-free scenario if the heartbeat thread, started by Thread 1, continues to use `reg->hr_bdev_file` after it has been freed by Thread 2! I need to trace this... the heartbeat thread, `o2hb_thread`, definitely uses `reg->hr_bdev_file`. And here we have it, the Heartbeat thread `o2hb_thread` can use the same potentially freed `file`. Time to check `o2hb_thread` in detail.
|
| 13/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_thread]
Results: map[SourceCode:1191: /*
1192: * we ride the region ref that the region dir holds. before the region
1193: * dir is removed and drops it ref it will wait to tear down this
1194: * thread.
1195: */
1196: static int o2hb_thread(void *data)
1197: {
1198: int i, ret;
1199: struct o2hb_region *reg = data;
1200: struct o2hb_bio_wait_ctxt write_wc;
1201: ktime_t before_hb, after_hb;
1202: unsigned int elapsed_msec;
1203:
1204: mlog(ML_HEARTBEAT|ML_KTHREAD, "hb thread running\n");
1205:
1206: set_user_nice(current, MIN_NICE);
1207:
1208: /* Pin node */
1209: ret = o2nm_depend_this_node();
1210: if (ret) {
1211: mlog(ML_ERROR, "Node has been deleted, ret = %d\n", ret);
1212: reg->hr_node_deleted = 1;
1213: wake_up(&o2hb_steady_queue);
1214: return 0;
1215: }
1216:
1217: while (!kthread_should_stop() &&
1218: !reg->hr_unclean_stop && !reg->hr_aborted_start) {
1219: /* We track the time spent inside
1220: * o2hb_do_disk_heartbeat so that we avoid more than
1221: * hr_timeout_ms between disk writes. On busy systems
1222: * this should result in a heartbeat which is less
1223: * likely to time itself out. */
1224: before_hb = ktime_get_real();
1225:
1226: ret = o2hb_do_disk_heartbeat(reg);
1227: reg->hr_last_hb_status = ret;
1228:
1229: after_hb = ktime_get_real();
1230:
1231: elapsed_msec = (unsigned int)
1232: ktime_ms_delta(after_hb, before_hb);
1233:
1234: mlog(ML_HEARTBEAT,
1235: "start = %lld, end = %lld, msec = %u, ret = %d\n",
1236: before_hb, after_hb, elapsed_msec, ret);
1237:
1238: if (!kthread_should_stop() &&
1239: elapsed_msec < reg->hr_timeout_ms) {
1240: /* the kthread api has blocked signals for us so no
1241: * need to record the return value. */
1242: msleep_interruptible(reg->hr_timeout_ms - elapsed_msec);
1243: }
1244: }
1245:
1246: o2hb_disarm_timeout(reg);
1247:
1248: /* unclean stop is only used in very bad situation */
1249: for(i = 0; !reg->hr_unclean_stop && i < reg->hr_blocks; i++)
1250: o2hb_shutdown_slot(®->hr_slots[i]);
1251:
1252: /* Explicit down notification - avoid forcing the other nodes
1253: * to timeout on this region when we could just as easily
1254: * write a clear generation - thus indicating to them that
1255: * this node has left this region.
1256: */
1257: if (!reg->hr_unclean_stop && !reg->hr_aborted_start) {
1258: o2hb_prepare_block(reg, 0);
1259: ret = o2hb_issue_node_write(reg, &write_wc);
1260: if (ret == 0)
1261: o2hb_wait_on_io(&write_wc);
1262: else
1263: mlog_errno(ret);
1264: }
1265:
1266: /* Unpin node */
1267: o2nm_undepend_this_node();
1268:
1269: mlog(ML_HEARTBEAT|ML_KTHREAD, "o2hb thread exiting\n");
1270:
1271: return 0;
1272: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 14/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 24997
output: 45
thoughts: 38
Thoughts: Here's my take on those thoughts:
**Dissecting the Heartbeat Path**
Okay, so I'm tracing this `o2hb_do_disk_heartbeat` function, and it's calling `o2hb_issue_node_write`. Now *that* function is interesting, because it relies on `reg->hr_bdev_file`. So, it seems like the disk heartbeat logic is ultimately interacting with a block device file through that `hr_bdev_file` member of some registry structure (`reg`). This is where the core interaction with storage is actually happening. My brain's kicking into gear to figure out the implications... is this a direct write? Is it using a buffered I/O? What locking is involved? Need to dive deeper into the `o2hb_issue_node_write` to see what kind of I/O operations are being conducted, and assess the performance characteristics.
|
| 15/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_issue_node_write]
Results: map[SourceCode: 595: static int o2hb_issue_node_write(struct o2hb_region *reg,
596: struct o2hb_bio_wait_ctxt *write_wc)
597: {
598: int status;
599: unsigned int slot;
600: struct bio *bio;
601:
602: o2hb_bio_wait_init(write_wc);
603:
604: slot = o2nm_this_node();
605:
606: bio = o2hb_setup_one_bio(reg, write_wc, &slot, slot+1,
607: REQ_OP_WRITE | REQ_SYNC);
608: if (IS_ERR(bio)) {
609: status = PTR_ERR(bio);
610: mlog_errno(status);
611: goto bail;
612: }
613:
614: atomic_inc(&write_wc->wc_num_reqs);
615: submit_bio(bio);
616:
617: status = 0;
618: bail:
619: return status;
620: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 16/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 25664
output: 40
thoughts: 23
Thoughts: Here's my take on those thoughts, keeping in mind my expertise in this area:
**Analyzing `o2hb_setup_one_bio` and Its Registration**
Okay, so I'm looking at `o2hb_setup_one_bio`. My immediate thought is, "Ah, right, this is where the bio setup happens." But then I see it: `reg_bdev(reg)`. *That* is what I'm focusing on. It's using `reg_bdev()`. That function. I have to immediately think about the implications. This function suggests how the device is registered with the system. What flags are used? Which queues are being set up at that point? Knowing `reg_bdev()` is called here tells me a lot about the foundation of how this bio will function and the subsequent interactions with the block device. I need to keep this in mind as I continue to analyze the overall workflow.
|
| 17/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:reg_bdev]
Results: map[SourceCode: 265: static inline struct block_device *reg_bdev(struct o2hb_region *reg)
266: {
267: return reg->hr_bdev_file ? file_bdev(reg->hr_bdev_file) : NULL;
268: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 18/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 26039
output: 45
thoughts: 85
Thoughts: **Analyzing a Potential Kernel Crash**
Okay, hold on a second. I'm following the logic here, and something feels off. I'm focusing on `reg_bdev` β it's crucial, obviously. It checks `reg->hr_bdev_file`. If that's *null*, it's designed to return *null*. Fine, that's expected.
But then, here's where it gets hairy. The code calls `o2hb_setup_one_bio`. And *that* function, if `reg_bdev` returns *null*, it will use that *null* value as the `bdev` argument. Now, I'm pretty certain that passing a `null` `bdev` to `bio_alloc` or `bio_set_dev` is going to be a complete disaster; it's practically guaranteed to trigger a kernel panic. I've seen that behavior before.
I need to dive into `o2hb_setup_one_bio` right now. I have to verify my hunch and see if my assumptions are correct about how the `null` bdev gets used. There's a crash brewing somewhere in there, and I need to figure out where and how to resolve it before the system goes down!
|
| 19/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_setup_one_bio]
Results: map[SourceCode: 507: /* Setup a Bio to cover I/O against num_slots slots starting at
508: * start_slot. */
509: static struct bio *o2hb_setup_one_bio(struct o2hb_region *reg,
510: struct o2hb_bio_wait_ctxt *wc,
511: unsigned int *current_slot,
512: unsigned int max_slots, blk_opf_t opf)
513: {
514: int len, current_page;
515: unsigned int vec_len, vec_start;
516: unsigned int bits = reg->hr_block_bits;
517: unsigned int spp = reg->hr_slots_per_page;
518: unsigned int cs = *current_slot;
519: struct bio *bio;
520: struct page *page;
521:
522: /* Testing has shown this allocation to take long enough under
523: * GFP_KERNEL that the local node can get fenced. It would be
524: * nicest if we could pre-allocate these bios and avoid this
525: * all together. */
526: bio = bio_alloc(reg_bdev(reg), 16, opf, GFP_ATOMIC);
527: if (!bio) {
528: mlog(ML_ERROR, "Could not alloc slots BIO!\n");
529: bio = ERR_PTR(-ENOMEM);
530: goto bail;
531: }
532:
533: /* Must put everything in 512 byte sectors for the bio... */
534: bio->bi_iter.bi_sector = (reg->hr_start_block + cs) << (bits - 9);
535: bio->bi_private = wc;
536: bio->bi_end_io = o2hb_bio_end_io;
537:
538: vec_start = (cs << bits) % PAGE_SIZE;
539: while(cs < max_slots) {
540: current_page = cs / spp;
541: page = reg->hr_slot_data[current_page];
542:
543: vec_len = min(PAGE_SIZE - vec_start,
544: (max_slots-cs) * (PAGE_SIZE/spp) );
545:
546: mlog(ML_HB_BIO, "page %d, vec_len = %u, vec_start = %u\n",
547: current_page, vec_len, vec_start);
548:
549: len = bio_add_page(bio, page, vec_len, vec_start);
550: if (len != vec_len) break;
551:
552: cs += vec_len / (PAGE_SIZE/spp);
553: vec_start = 0;
554: }
555:
556: bail:
557: *current_slot = cs;
558: return bio;
559: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 20/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 27352
output: 45
thoughts: 1916
Thoughts: **Analysis of the NULL Pointer Dereference in `fput`**
Okay, let's break this down. My initial thought was that a `NULL` `bdev` might be the root cause, leading to crashes in `bio_init` or `submit_bio`. But that's not it. `bio_alloc` just sets `bio->bi_bdev = NULL` and the crash is elsewhere. The key observation is that the crash report points to `fput` with a `NULL` pointer. That suggests an issue with the file descriptor.
Digging deeper, it looks like a race condition in `o2hb_region_dev_store` is the culprit. We have two threads, Thread 1 and Thread 2, both attempting to open and store a device file (`reg->hr_bdev_file`). The critical section is not properly protected.
The core scenario plays out like this: Both threads see `reg->hr_bdev_file` as `NULL` initially, because there is no locking. Both threads then call `bdev_file_open_by_dev`. Thread 1 sets `reg->hr_bdev_file` to `file1`, and Thread 2 sets `reg->hr_bdev_file` to `file2`. It's a race, right? Then one might fail later and call `fput`.
I considered multiple scenarios, and it came to me - What if the *second* thread fails *after* setting the file but *before* taking the lock and setting the file to null in its `out` path? Thread 1 gets the value for `file2` and calls `fput(file2)` and then sets `reg->hr_bdev_file` to `NULL`.
Now the second thread goes to its `out` path, where it also calls `fput(reg->hr_bdev_file)`... But it's `NULL`! Bingo!
The fundamental flaw is this: multiple threads contend for the `reg->hr_bdev_file` variable, overwriting the value before it's properly protected, leading to a race where a thread can free the file before another thread is even done using it. The `fput(NULL)` call is the direct result of that race.
This also means that `file1` will be leaked. I'm concerned about the consequences of one thread calling `fput(file2)` (dropping the ref count to zero, freeing it), while another thread is concurrently accessing that file descriptor, potentially leading to a Use-After-Free situation. I'll need to double check the code paths in `o2hb_map_slot_data` that use `reg->hr_bdev_file` to confirm that the file is not being accessed after a free.
|
| 21/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_map_slot_data]
Results: map[SourceCode:1679: static int o2hb_map_slot_data(struct o2hb_region *reg)
1680: {
1681: int i, j;
1682: int ret = -ENOMEM;
1683: unsigned int last_slot;
1684: unsigned int spp = reg->hr_slots_per_page;
1685: struct page *page;
1686: char *raw;
1687: struct o2hb_disk_slot *slot;
1688:
1689: reg->hr_tmp_block = kmalloc(reg->hr_block_bytes, GFP_KERNEL);
1690: if (!reg->hr_tmp_block)
1691: goto out;
1692:
1693: reg->hr_slots = kzalloc_objs(struct o2hb_disk_slot, reg->hr_blocks);
1694: if (!reg->hr_slots)
1695: goto out;
1696:
1697: for (i = 0; i < reg->hr_blocks; i++) {
1698: slot = ®->hr_slots[i];
1699: slot->ds_node_num = i;
1700: INIT_LIST_HEAD(&slot->ds_live_item);
1701: slot->ds_raw_block = NULL;
1702: }
1703:
1704: reg->hr_num_pages = (reg->hr_blocks + spp - 1) / spp;
1705: mlog(ML_HEARTBEAT, "Going to require %u pages to cover %u blocks "
1706: "at %u blocks per page\n",
1707: reg->hr_num_pages, reg->hr_blocks, spp);
1708:
1709: reg->hr_slot_data = kzalloc_objs(struct page *, reg->hr_num_pages);
1710: if (!reg->hr_slot_data)
1711: goto out;
1712:
1713: for (i = 0; i < reg->hr_num_pages; i++) {
1714: page = alloc_page(GFP_KERNEL);
1715: if (!page)
1716: goto out;
1717:
1718: reg->hr_slot_data[i] = page;
1719:
1720: last_slot = i * spp;
1721: raw = page_address(page);
1722: for (j = 0;
1723: (j < spp) && ((j + last_slot) < reg->hr_blocks);
1724: j++) {
1725: BUG_ON((j + last_slot) >= reg->hr_blocks);
1726:
1727: slot = ®->hr_slots[j + last_slot];
1728: slot->ds_raw_block =
1729: (struct o2hb_disk_heartbeat_block *) raw;
1730:
1731: raw += reg->hr_block_bytes;
1732: }
1733: }
1734:
1735: return 0;
1736:
1737: out:
1738: o2hb_unmap_slot_data(reg);
1739: return ret;
1740: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 22/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 30892
output: 45
thoughts: 39
Thoughts: Okay, here's my take on those thoughts, framed as my own mental processing as an expert in the field:
**Investigating the Data Flow in the Heartbeat Mechanism**
Alright, I'm diving back into this `o2hb` code, and something's bugging me. Specifically, I was just looking at `o2hb_map_slot_data`. It's pretty clear that it doesn't use the `reg->hr_bdev_file` member. That's a point to make a mental note of, and likely to investigate later if issues arise down the line.
But then my mind jumped to `o2hb_populate_slot_data`. Now, that's where the rubber meets the road, isn't it? It has to be that this function does make the use of `reg->hr_bdev_file`. I need to review that function's implementation as it is where the link could take place. Is that a correct assumption? I should confirm that and see how that is related to the data flow as this function likely does use the `reg->hr_bdev_file`. I have to drill down into the details of the heartbeat data population process. This is the place where I would expect that file descriptor to come into play, if at all. It's likely that it has a role to play here, or else why would it be declared?
|
| 23/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_populate_slot_data]
Results: map[SourceCode:1742: /* Read in all the slots available and populate the tracking
1743: * structures so that we can start with a baseline idea of what's
1744: * there. */
1745: static int o2hb_populate_slot_data(struct o2hb_region *reg)
1746: {
1747: int ret, i;
1748: struct o2hb_disk_slot *slot;
1749: struct o2hb_disk_heartbeat_block *hb_block;
1750:
1751: ret = o2hb_read_slots(reg, 0, reg->hr_blocks);
1752: if (ret)
1753: goto out;
1754:
1755: /* We only want to get an idea of the values initially in each
1756: * slot, so we do no verification - o2hb_check_slot will
1757: * actually determine if each configured slot is valid and
1758: * whether any values have changed. */
1759: for(i = 0; i < reg->hr_blocks; i++) {
1760: slot = ®->hr_slots[i];
1761: hb_block = (struct o2hb_disk_heartbeat_block *) slot->ds_raw_block;
1762:
1763: /* Only fill the values that o2hb_check_slot uses to
1764: * determine changing slots */
1765: slot->ds_last_time = le64_to_cpu(hb_block->hb_seq);
1766: slot->ds_last_generation = le64_to_cpu(hb_block->hb_generation);
1767: }
1768:
1769: out:
1770: return ret;
1771: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 24/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 31809
output: 43
thoughts: 21
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience and presented in the first person:
**Decoding That `o2hb_read_slots` Call**
Alright, so the key piece here is that `o2hb_read_slots` function is pulling data from `reg->hr_bdev_file`. That's the crux of this. My initial impression is that this is how heartbeats are read off the block device. It's using that `reg` struct, which I immediately recognize is likely holding all the heartbeat registration data. The `hr_bdev_file` must be the file descriptor or struct representing the backing block device used to persist the heartbeat state. This suggests we're reading heartbeat slot information directly from a block device file. My mind is already racing: what is the actual format of those slots? Is it a simple bitfield, or a more complex structure? Does the `o2hb_read_slots` function handle potential errors, like stale or corrupt data on the block device? And is this read part of a more extensive synchronization sequence?
|
| 25/2 |
2026/08/09 08:56 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_read_slots]
Results: map[SourceCode: 561: static int o2hb_read_slots(struct o2hb_region *reg,
562: unsigned int begin_slot,
563: unsigned int max_slots)
564: {
565: unsigned int current_slot = begin_slot;
566: int status;
567: struct o2hb_bio_wait_ctxt wc;
568: struct bio *bio;
569:
570: o2hb_bio_wait_init(&wc);
571:
572: while(current_slot < max_slots) {
573: bio = o2hb_setup_one_bio(reg, &wc, ¤t_slot, max_slots,
574: REQ_OP_READ);
575: if (IS_ERR(bio)) {
576: status = PTR_ERR(bio);
577: mlog_errno(status);
578: goto bail_and_wait;
579: }
580:
581: atomic_inc(&wc.wc_num_reqs);
582: submit_bio(bio);
583: }
584:
585: status = 0;
586:
587: bail_and_wait:
588: o2hb_wait_on_io(&wc);
589: if (wc.wc_error && !status)
590: status = wc.wc_error;
591:
592: return status;
593: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 26/2 |
2026/08/09 08:56 |
llm |
expert |
0m
Model:
Tokens:
input: 32588
output: 38
thoughts: 956
Thoughts: **Analyzing a Potential Use-After-Free Vulnerability in o2hb_read_slots**
Okay, so I'm looking at this code and, if I understand it correctly, `o2hb_read_slots` calls `o2hb_setup_one_bio`, which eventually calls `reg_bdev(reg)`. `reg_bdev` uses `reg->hr_bdev_file`. The issue is, if Thread 1 frees `file2` (which Thread 2 set as `reg->hr_bdev_file`), and Thread 2 is in `o2hb_read_slots`, then Thread 2 will attempt to use `file2` after it's been freed.
Now, `fput` doesn't immediately free the file, as it drops the refcount to 0 and schedules asynchronous freeing, because of `task_work_add`. This looks like:
```c
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count)) {
struct task_struct *task = current;
if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
init_task_work(&file->f_rcuhead, ____fput);
if (!task_work_add(task, &file->f_rcuhead, TWA_RESUME))
return;
/*
* After this task has run exit_task_work(),
* task_work_add() will fail. Fall through to delayed
* fput to avoid leaking *file.
*/
}
if (llist_add(&file->f_llist, &delayed_fput_list))
schedule_delayed_work(&delayed_fput_work, 1);
}
}
```
So the file is freed asynchronously. This means there's a window of opportunity for a Use-After-Free! Specifically, in `reg_bdev(reg)` where:
```c
static inline struct block_device *reg_bdev(struct o2hb_region *reg)
{
return reg->hr_bdev_file ? file_bdev(reg->hr_bdev_file) : NULL;
}
```
`file_bdev` accesses `file->f_mapping->host->i_bdev`. So if `file` is freed while the second thread attempts to access these values, it's a Use-After-Free read! Then `bio_alloc` will use the `bdev`, meaning the race condition can lead to a Use-After-Free.
Is this exploitable? A Use-After-Free on `struct file` is usually exploitable because of function pointers (`f_op`) and other crucial data in the struct. Here, it seems like `file_bdev` is *reading* data from the freed file, so that's a key detail, but it could still be a vulnerability.
If the file is freed, an attacker could potentially spray memory to control `file->f_mapping` and `host->i_bdev`. Now the next critical point is, where is this code reachable? `o2hb_region_dev_store` is only accessible to users who can write to configfs. Let's check how the reproducer does it.
The reproducer mounts configfs:
```c
syscall(__NR_mount, /*src=*/0ul, /*dst=*/0x200000000000ul, /*type=*/0x200000000040ul, /*flags=*/0ul, /*data=*/0ul);
```
So, can an unprivileged user mount configfs? No, `mount` requires `CAP_SYS_ADMIN` in the user namespace. However, the reproducer doesn't create a new user namespace.
Let's check if `syz-executor` runs as root:
```
CPU: 1 UID: 0 PID: 5664 Comm: syz-executor157 Not tainted syzkaller #0 PREEMPT(full)
```
The UID is 0. So it runs as root. This means an unprivileged user can't trigger this because the mount needs `CAP_SYS_ADMIN`. The bug can only be triggered by a process running with root privileges, likely within a privileged environment, which limits the attack surface.
|
| 27/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/configfs/mount.c Name:configfs_fs_type]
Results: map[SourceCode: 115: static struct file_system_type configfs_fs_type = {
116: .owner = THIS_MODULE,
117: .name = "configfs",
118: .init_fs_context = configfs_init_fs_context,
119: .kill_sb = kill_anon_super,
120: };
SourceFile:fs/configfs/mount.c]
|
| 28/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 34705
output: 38
thoughts: 101
Thoughts: **Analyzing Configfs Mounting in User Namespaces**
Okay, so I'm looking at `configfs_fs_type` and I'm realizing something immediately: it *doesn't* have the `FS_USERNS_MOUNT` flag set. That's a pretty big deal. It means, definitively, that an unprivileged user, even within a user namespace, *cannot* directly mount configfs. That's a fundamental restriction baked right into the filesystem's definition.
But hold on, there's a potential wrinkle. What about the case where configfs is *already* mounted by root? That's the typical scenario, right? It's usually found at `/sys/kernel/config`. I'm going to have to make the assumption that it is, given that this is the usual location. Let me confirm that.
If it's at the standard location, `/sys/kernel/config` will have the permissions `drwxr-xr-x root root`. This means that unprivileged users *can* read it. Now the question is, *can* they modify directories and files within that mount point, even with a user namespace? That's the real test of what is possible. I'll need to check the permissions on the actual configfs directories to see what the practical implications of this are. This is where the security puzzle gets interesting.
|
| 29/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/configfs/dir.c Name:configfs_create_dir]
Results: map[SourceCode: 293: static int configfs_create_dir(struct config_item *item, struct dentry *dentry,
294: struct configfs_fragment *frag)
295: {
296: int error;
297: umode_t mode = S_IFDIR| S_IRWXU | S_IRUGO | S_IXUGO;
298: struct dentry *p = dentry->d_parent;
299: struct inode *p_inode = d_inode(p);
300: struct inode *inode;
301:
302: BUG_ON(!item);
303:
304: error = configfs_make_dirent(p->d_fsdata, dentry, item, mode,
305: CONFIGFS_DIR | CONFIGFS_USET_CREATING,
306: frag);
307: if (unlikely(error))
308: return error;
309:
310: configfs_set_dir_dirent_depth(p->d_fsdata, dentry->d_fsdata);
311: inode = configfs_create(dentry, mode);
312: if (IS_ERR(inode))
313: goto out_remove;
314:
315: inode->i_op = &configfs_dir_inode_operations;
316: inode->i_fop = &configfs_dir_operations;
317: /* directory inodes start off with i_nlink == 2 (for "." entry) */
318: inc_nlink(inode);
319: d_make_persistent(dentry, inode);
320: inc_nlink(p_inode);
321: inode_set_mtime_to_ts(p_inode, inode_set_ctime_current(p_inode));
322: item->ci_dentry = dentry;
323: return 0;
324:
325: out_remove:
326: configfs_put(dentry->d_fsdata);
327: configfs_remove_dirent(dentry);
328: return PTR_ERR(inode);
329: }
SourceFile:fs/configfs/dir.c]
|
| 30/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 35784
output: 38
thoughts: 57
Thoughts: Okay, here's my interpretation of those thoughts, tailored for an expert audience:
**Analyzing ConfigFS Directory Permissions**
Alright, let's break down this mode β `S_IFDIR | S_IRWXU | S_IRUGO | S_IXUGO`, which translates to `0755`. That bitwise combination indicates a directory and sets the permissions as follows: The owner has full read, write, and execute permissions (`rwx`, `0700`), while group and others have read and execute permissions only (`r-x`, `0055`).
So, the immediate implication is that, by default, only the root user can create new subdirectories within this configfs directory structure. That's pretty standard operating procedure for many configuration file systems.
But, a nagging thought arises β this only dictates the permissions *on the directory itself*. What about the files that *reside inside* that directory? The standard Unix model doesn't automatically propagate permissions down. Are there any default umask settings at play here? Is there something in the configfs implementation itself that provides further restrictions on file creation within this directory? My immediate concern is that this setup feels a bit insecure, and needs more investigation to guarantee that a non-root user can't maliciously create a file with insecure permissions.
|
| 31/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/configfs/dir.c Name:configfs_create_file]
Results: map[SourceCode: 446: int configfs_create_file(struct config_item * item, const struct configfs_attribute * attr)
447: {
448: struct dentry *dir = item->ci_dentry;
449: struct configfs_dirent *parent_sd = dir->d_fsdata;
450: umode_t mode = (attr->ca_mode & S_IALLUGO) | S_IFREG;
451: int error = 0;
452:
453: inode_lock_nested(d_inode(dir), I_MUTEX_NORMAL);
454: error = configfs_make_dirent(parent_sd, NULL, (void *) attr, mode,
455: CONFIGFS_ITEM_ATTR, parent_sd->s_frag);
456: inode_unlock(d_inode(dir));
457:
458: return error;
459: }
SourceFile:fs/configfs/file.c]
|
| 32/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 36445
output: 43
thoughts: 32
Thoughts: Okay, here's my take on those thoughts, framed as my own mental process, given my expertise:
**Checking Attribute Mode in `o2hb_region_dev_store`**
Right, let me just pause and think this through. The `attr` pointer... yes, of course, `attr->ca_mode` is *the* crucial field here. That's the one that dictates the mode we're dealing with, isn't it? Okay, so that means I need to zero in on how the `o2hb_region_dev_store` function, or the related data structure, actually *uses* or *sets* that specific `ca_mode` attribute. I should verify that. What kind of interactions are happening with the attribute in that context? I'm picturing the memory layout and the surrounding code, and I know it's probably critical to understand the behavior of any subsequent operation based on the mode. Let me quickly refresh my understanding of where that attribute is defined and how it gets set in this particular function. No, wait, I can see the code now... let me see how to verify this is what is expected. I have to think about this in the context of the larger architecture. Let me go back and double check how this attribute plays into how this device store interacts with the heartbeat logic overall.
|
| 33/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_region_attrs]
Results: map[SourceCode:1963: static struct configfs_attribute *o2hb_region_attrs[] = {
1964: &o2hb_region_attr_block_bytes,
1965: &o2hb_region_attr_start_block,
1966: &o2hb_region_attr_blocks,
1967: &o2hb_region_attr_dev,
1968: &o2hb_region_attr_pid,
1969: NULL,
1970: };
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 34/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 36972
output: 45
thoughts: 15
Thoughts: Okay, here's my summary, taking the role of the thinker:
**Checking `o2hb_region_attr_dev`**
Alright, let's dive in. I need to make sure I understand the current state of things, so first thing's first, I should investigate `o2hb_region_attr_dev`. This variable, or whatever it represents, is clearly central to the discussion, and I need a clear understanding of what it entails before I move any further. I need to understand what this thing is, what it does, and how it is involved in the overall problem. Let's see what we can find.
|
| 35/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_region_attr_dev]
Results: map[SourceCode:1960: CONFIGFS_ATTR(o2hb_region_, dev);
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 36/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 37224
output: 36
thoughts: 13
Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience:
**Analyzing the `CONFIGFS_ATTR` Macro**
Okay, wait a minute. I need to take a closer look at this `CONFIGFS_ATTR` macro. It looks like, based on the context, the primary purpose of this macro is to define the attribute's mode. I'm assuming that it encapsulates the bitwise operations necessary to configure the permissions and access flags associated with a configfs attribute, likely for read, write, and execute. Itβs probably taking care of the tedious boilerplate involved in setting up the file permissions appropriately within the configfs structure, allowing attributes to be accessed in the expected manner. I need to review its definition, but I'm betting that's where the file system mode is coming from. That's the key piece.
|
| 37/2 |
2026/08/09 08:57 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:include/linux/configfs.h Name:CONFIGFS_ATTR]
Results: map[SourceCode: SourceFile:]
|
| 38/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 37472
output: 20
thoughts: 11
Thoughts: Here's my thought process, as if I were considering those instructions:
**Analyzing the Search Term: `CONFIGFS_ATTR`**
Okay, first things first, let's take a look at this. The prompt mentions "grep" and the term `CONFIGFS_ATTR`. Immediately, I'm thinking this likely relates to the Linux kernel and its configuration filesystem, configfs. The underscore-delimited name strongly suggests we're dealing with an attribute within that system. `CONFIGFS_ATTR` likely refers to a macro or identifier used to define attributes within the configfs framework. This could be in the header files or source code. Time to dig in!
|
| 39/2 |
2026/08/09 08:57 |
tool |
grepper |
0m
Args: map[Expression:#define CONFIGFS_ATTR]
Results: map[Output:include/linux/configfs.h=115=struct configfs_attribute {
--
include/linux/configfs.h-122-
include/linux/configfs.h:123:#define CONFIGFS_ATTR_PERM(_pfx, _name, _perm) \
include/linux/configfs.h-124-static struct configfs_attribute _pfx##attr_##_name = { \
--
include/linux/configfs.h-131-
include/linux/configfs.h:132:#define CONFIGFS_ATTR(_pfx, _name) CONFIGFS_ATTR_PERM( \
include/linux/configfs.h-133- _pfx, _name, S_IRUGO | S_IWUSR \
--
include/linux/configfs.h-135-
include/linux/configfs.h:136:#define CONFIGFS_ATTR_RO(_pfx, _name) \
include/linux/configfs.h-137-static struct configfs_attribute _pfx##attr_##_name = { \
--
include/linux/configfs.h-143-
include/linux/configfs.h:144:#define CONFIGFS_ATTR_WO(_pfx, _name) \
include/linux/configfs.h-145-static struct configfs_attribute _pfx##attr_##_name = { \
--
lib/fault-inject.c=334=static ssize_t fault_atomic_t_attr_store(atomic_t *val, const char *page, size_t count)
--
lib/fault-inject.c-346-
lib/fault-inject.c:347:#define CONFIGFS_ATTR_NAMED(_pfx, _name, _attr_name) \
lib/fault-inject.c-348-static struct configfs_attribute _pfx##attr_##_name = { \
]
|
| 40/2 |
2026/08/09 08:57 |
llm |
expert |
0m
Model:
Tokens:
input: 38129
output: 45
thoughts: 1938
Thoughts: **My Analysis of the Kernel Bug**
Okay, let's break this down systematically. The core issue appears to be related to a race condition involving `struct file` pointers within the `configfs` framework, specifically when writing to the `/sys/kernel/config/cluster/.../heartbeat/.../dev` file. I'm focusing on the potential for a Use-After-Free and NULL pointer dereference.
First, I established the attack surface. The file in question uses `0644` permissions (`CONFIGFS_ATTR` uses `S_IRUGO | S_IWUSR`), meaning only root can write to it by default. Therefore, unprivileged processes and user namespaces are immediately ruled out unless the administrator modifies permissions. I'm operating under the assumption of default configurations, so this is a root-level bug.
Is it a DoS? Yes, the bug results in a kernel panic or process termination due to a crash. It could also lead to held locks or Use-After-Free scenarios that crash the kernel later.
Is it exploitable? Yes, this seems to be a Use-After-Free scenario of a `struct file` pointer. Initially, the bug report focuses on a NULL pointer dereference in `fput`, but my analysis reveals a more complex race. The race is caused by two threads using `reg->hr_bdev_file` at the same time. The threads may be able to run `fput(file)` *after* it's been set to `NULL` by another thread.
I've traced the execution: one thread can call `fput(file2)` (when `file2` is used by the `hb_task`), and set `reg->hr_bdev_file` to `NULL`. The heartbeat thread (which I'll call Thread 2) then attempts to use `reg->hr_bdev_file`, now `NULL`, leading to a crash. Even if the Thread 2 doesn't crash, the heartbeat thread may still cause a use-after-free because `fput` is asynchronous.
The heartbeat thread uses `reg->hr_bdev_file` which is now `NULL`, and it then calls `reg_bdev(reg)`, which returns NULL. Then `bio_alloc` uses this `NULL` `bdev`, and then `submit_bio` will crash because of the `bio->bi_bdev` is NULL.
I considered an alternative scenario where Thread 1 fails and calls `fput(file2)` *but* doesn't set `reg->hr_bdev_file` to `NULL` immediately. If Thread 2's heartbeat thread accesses the freed `file2` before it is released, this results in a Use-After-Free read. `fput` drops the refcount and the file is freed asynchronously. If the heartbeat thread accesses `file2->f_mapping->host->i_bdev` after `file2` is freed, it's a Use-After-Free read.
The root privilege requirement initially gave me pause. While the vulnerability requires root access (CAP_SYS_ADMIN), it's still a significant bug because it causes a kernel crash and memory corruption.
I re-examined the possibility of an unprivileged user reaching this vulnerability. No, permissions on `/sys/kernel/config/cluster/.../heartbeat/.../dev` are `0644`. Configfs cannot be mounted in a user namespace. The files in configfs are owned by root, and a user namespace root wouldn't have permissions to write to them. I've confirmed there is no other path to `o2hb_region_dev_store` other than a direct configfs write. No malicious filesystem, peripheral device, network traffic, or VM-related vectors are possible here.
Finally, I checked for other ways a NULL could reach `fput`, but confirmed that `bdev_file_open_by_dev` doesn't return NULL, and the code path in `o2hb_region_dev_store` only calls `fput` on a `NULL` pointer if a write is successful, followed by a failure on a subsequent write (i.e. `goto out` only happens after checking a non-NULL value for the target of the `fput`).
Therefore:
* **Exploitable: Yes** (Use-After-Free and NULL pointer dereference).
* **Denial of Service: Yes** (Kernel crash).
* **Unprivileged: No**.
* **User Namespace: No**.
The bug is a clear security issue, even if it requires root, as it causes memory corruption.
|
| 41/2 |
2026/08/09 08:58 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/ocfs2/cluster/heartbeat.c Name:o2hb_region_dev_store]
Results: map[SourceCode:1773: /*
1774: * this is acting as commit; we set up all of hr_bdev_file and hr_task or
1775: * nothing
1776: */
1777: static ssize_t o2hb_region_dev_store(struct config_item *item,
1778: const char *page,
1779: size_t count)
1780: {
1781: struct o2hb_region *reg = to_o2hb_region(item);
1782: struct task_struct *hb_task;
1783: long fd;
1784: int sectsize;
1785: char *p = (char *)page;
1786: ssize_t ret = -EINVAL;
1787: int live_threshold;
1788:
1789: if (reg->hr_bdev_file)
1790: return -EINVAL;
1791:
1792: /* We can't heartbeat without having had our node number
1793: * configured yet. */
1794: if (o2nm_this_node() == O2NM_MAX_NODES)
1795: return -EINVAL;
1796:
1797: ret = kstrtol(p, 0, &fd);
1798: if (ret < 0)
1799: return -EINVAL;
1800:
1801: if (fd < 0 || fd >= INT_MAX)
1802: return -EINVAL;
1803:
1804: CLASS(fd, f)(fd);
1805: if (fd_empty(f))
1806: return -EINVAL;
1807:
1808: if (reg->hr_blocks == 0 || reg->hr_start_block == 0 ||
1809: reg->hr_block_bytes == 0)
1810: return -EINVAL;
1811:
1812: if (!S_ISBLK(fd_file(f)->f_mapping->host->i_mode))
1813: return -EINVAL;
1814:
1815: reg->hr_bdev_file = bdev_file_open_by_dev(fd_file(f)->f_mapping->host->i_rdev,
1816: BLK_OPEN_WRITE | BLK_OPEN_READ, NULL, NULL);
1817: if (IS_ERR(reg->hr_bdev_file)) {
1818: ret = PTR_ERR(reg->hr_bdev_file);
1819: reg->hr_bdev_file = NULL;
1820: return ret;
1821: }
1822:
1823: sectsize = bdev_logical_block_size(reg_bdev(reg));
1824: if (sectsize != reg->hr_block_bytes) {
1825: mlog(ML_ERROR,
1826: "blocksize %u incorrect for device, expected %d",
1827: reg->hr_block_bytes, sectsize);
1828: ret = -EINVAL;
1829: goto out;
1830: }
1831:
1832: reg->hr_aborted_start = 0;
1833: reg->hr_node_deleted = 0;
1834: o2hb_init_region_params(reg);
1835:
1836: /* Generation of zero is invalid */
1837: do {
1838: get_random_bytes(®->hr_generation,
1839: sizeof(reg->hr_generation));
1840: } while (reg->hr_generation == 0);
1841:
1842: ret = o2hb_map_slot_data(reg);
1843: if (ret) {
1844: mlog_errno(ret);
1845: goto out;
1846: }
1847:
1848: ret = o2hb_populate_slot_data(reg);
1849: if (ret) {
1850: mlog_errno(ret);
1851: goto out;
1852: }
1853:
1854: INIT_DELAYED_WORK(®->hr_write_timeout_work, o2hb_write_timeout);
1855: INIT_DELAYED_WORK(®->hr_nego_timeout_work, o2hb_nego_timeout);
1856:
1857: /*
1858: * A node is considered live after it has beat LIVE_THRESHOLD
1859: * times. We're not steady until we've given them a chance
1860: * _after_ our first read.
1861: * The default threshold is bare minimum so as to limit the delay
1862: * during mounts. For global heartbeat, the threshold doubled for the
1863: * first region.
1864: */
1865: live_threshold = O2HB_LIVE_THRESHOLD;
1866: if (o2hb_global_heartbeat_active()) {
1867: spin_lock(&o2hb_live_lock);
1868: if (bitmap_weight(o2hb_region_bitmap, O2NM_MAX_REGIONS) == 1)
1869: live_threshold <<= 1;
1870: spin_unlock(&o2hb_live_lock);
1871: }
1872: ++live_threshold;
1873: atomic_set(®->hr_steady_iterations, live_threshold);
1874: /* unsteady_iterations is triple the steady_iterations */
1875: atomic_set(®->hr_unsteady_iterations, (live_threshold * 3));
1876:
1877: hb_task = kthread_run(o2hb_thread, reg, "o2hb-%s",
1878: reg->hr_item.ci_name);
1879: if (IS_ERR(hb_task)) {
1880: ret = PTR_ERR(hb_task);
1881: mlog_errno(ret);
1882: goto out;
1883: }
1884:
1885: spin_lock(&o2hb_live_lock);
1886: reg->hr_task = hb_task;
1887: spin_unlock(&o2hb_live_lock);
1888:
1889: ret = wait_event_interruptible(o2hb_steady_queue,
1890: atomic_read(®->hr_steady_iterations) == 0 ||
1891: reg->hr_node_deleted);
1892: if (ret) {
1893: atomic_set(®->hr_steady_iterations, 0);
1894: reg->hr_aborted_start = 1;
1895: }
1896:
1897: if (reg->hr_aborted_start) {
1898: ret = -EIO;
1899: goto out;
1900: }
1901:
1902: if (reg->hr_node_deleted) {
1903: ret = -EINVAL;
1904: goto out;
1905: }
1906:
1907: /* Ok, we were woken. Make sure it wasn't by drop_item() */
1908: spin_lock(&o2hb_live_lock);
1909: hb_task = reg->hr_task;
1910: if (o2hb_global_heartbeat_active())
1911: set_bit(reg->hr_region_num, o2hb_live_region_bitmap);
1912: spin_unlock(&o2hb_live_lock);
1913:
1914: if (hb_task)
1915: ret = count;
1916: else
1917: ret = -EIO;
1918:
1919: if (hb_task && o2hb_global_heartbeat_active())
1920: printk(KERN_NOTICE "o2hb: Heartbeat started on region %s (%pg)\n",
1921: config_item_name(®->hr_item), reg_bdev(reg));
1922:
1923: out:
1924: if (ret < 0) {
1925: spin_lock(&o2hb_live_lock);
1926: hb_task = reg->hr_task;
1927: reg->hr_task = NULL;
1928: spin_unlock(&o2hb_live_lock);
1929:
1930: if (hb_task)
1931: kthread_stop(hb_task);
1932:
1933: o2hb_unmap_slot_data(reg);
1934:
1935: fput(reg->hr_bdev_file);
1936: reg->hr_bdev_file = NULL;
1937: }
1938: return ret;
1939: }
SourceFile:fs/ocfs2/cluster/heartbeat.c]
|
| 42/2 |
2026/08/09 08:58 |
llm |
expert |
0m
Model:
Tokens:
input: 43685
output: 66
thoughts: 683
Thoughts: **Race Condition Analysis in `o2hb_region_dev_store`**
Okay, let's break this down. My initial thought process goes like this: If `o2hb_region_dev_store` is entered, and `reg->hr_bdev_file` is already assigned, the function directly returns an `-EINVAL` error. That means it doesn't even hit the `out` label.
The only way to hit the `out` label, is if something fails *after* calling `bdev_file_open_by_dev`. Therefore, if `goto out` is reached, `reg->hr_bdev_file` must be set to a valid pointer. The function `fput` is called with a potentially NULL file pointer. So the only way `fput` is called with NULL, is if `reg->hr_bdev_file` has been overwritten with NULL by another thread, right?
Looking at where `reg->hr_bdev_file` can be set to NULL, there are only a few places:
1. `bdev_file_open_by_dev` failure - but this does not go to the `out` label, as mentioned above.
2. The `out` label itself: `reg->hr_bdev_file = NULL;`
3. `o2hb_region_dev_drop`. Which is called when the configfs item is dropped.
Aha! Now it's becoming clear. If one thread is in `o2hb_region_dev_store` and hits the `out` label and sets `reg->hr_bdev_file` to NULL, another thread might also be in the same function and *reads* this value as NULL and calls `fput(NULL)`. That confirms the race condition.
Let's quickly check if `fput` handles a NULL argument. No, it doesn't. Looking at the code: `if (atomic_long_dec_and_test(&file->f_count))` this will crash.
So, this is a clear race condition leading to a use-after-free, which translates to a crash.
Now, can this be triggered by any other means? It requires the `dev` in configfs to be written to, and that requires root privileges. So, is it triggered by some malicious filesystem? No, since configfs is a pseudo-filesystem, not block-based. Is it a peripheral issue? Nope.
Okay, that's everything. I can now confidently set the vulnerability results.
|
| 43/2 |
2026/08/09 08:58 |
tool |
set-results |
0m
Args: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 44/2 |
2026/08/09 08:58 |
llm |
expert |
0m
Model:
Tokens:
input: 45047
output: 635
thoughts: 409
Thoughts: **My Assessment of the `fput` NULL Pointer Dereference**
Alright, the analysis is done, and it's time to put together the final response. Here's what I've found.
The root of this issue is a classic NULL pointer dereference within `fput`, and it stems from a race condition I identified in `o2hb_region_dev_store`, specifically within the `fs/ocfs2/cluster/heartbeat.c` file. It boils down to multiple threads concurrently writing to the `dev` attribute of an ocfs2 heartbeat region within configfs. The crux of the problem lies in the fact that the `if (reg->hr_bdev_file)` check isn't properly locked down. This means multiple threads can bypass that safety check.
What happens is this: Both threads end up opening the block device and overwriting the value of `reg->hr_bdev_file`. If one thread subsequently hits a snagβsay, a block size mismatch or a slot mapping failureβit'll jump to the `out` label, call `fput(reg->hr_bdev_file)`, and, critically, set `reg->hr_bdev_file = NULL`. The second thread, also hitting a failure, will follow suit and then encounter the fatal error. When it calls `fput()` on what is now a `NULL` pointer, boom β that's the dereference.
To add insult to injury, this race condition also creates a Use-After-Free scenario. If one thread manages to successfully initiate the heartbeat thread *before* the other fails and frees the `struct file`, the heartbeat thread will continue to use this freed data, like in `reg_bdev()`. This, as we know, opens the door to memory corruption.
However, I need to acknowledge that this bug isn't something that can be exploited by just anyone. Access to this particular bug is tightly controlled. The `dev` attribute in configfs is created with `0644` permissions and is owned by root. Furthermore, configfs can't be mounted inside an unprivileged user namespace because `FS_USERNS_MOUNT` isn't set for `configfs_fs_type`. Even if it *could* be mounted within a user namespace, the files would still be owned by the global root. Therefore, triggering this vulnerability requires global root privileges.
|
| 45/1 |
2026/08/09 08:58 |
action |
format-explanation |
0m
Results: map[Explanation:Based on the analysis of the bug report and the kernel source code, here is the
assessment of the vulnerability:
**Bug Mechanism:**
The bug is a NULL pointer dereference in `fput()` triggered by a race condition
in `o2hb_region_dev_store()` within the OCFS2 cluster heartbeat code
(`fs/ocfs2/cluster/heartbeat.c`).
When multiple threads concurrently write to the `dev` attribute of an OCFS2
heartbeat region in configfs, they can both pass the initial `if
(reg->hr_bdev_file)` check since it is not protected by a lock. Both threads
will then open the block device and overwrite the shared `reg->hr_bdev_file`
pointer.
If one thread encounters an error later in the function (e.g., due to a block
size mismatch or slot mapping failure), it jumps to the `out` error-handling
label, calls `fput(reg->hr_bdev_file)`, and sets `reg->hr_bdev_file = NULL`.
When the second thread also fails and reaches the `out` label, it reads the now
`NULL` pointer and passes it to `fput()`, which unconditionally dereferences it,
causing a kernel crash.
Additionally, this race condition can lead to a **Use-After-Free**. If one
thread successfully starts the heartbeat thread while the other thread fails and
frees the `struct file`, the heartbeat thread will continue to use the freed
`struct file` (e.g., in `reg_bdev()`), leading to memory corruption.
**Security Impact:**
* **Exploitable:** Yes. While the immediate crash is a NULL pointer dereference
(which typically results in a Denial of Service), the underlying race condition
can also cause a Use-After-Free of a `struct file` object if the threads
interleave differently. `struct file` UAFs are generally considered exploitable
for privilege escalation or arbitrary code execution.
* **Denial of Service:** Yes. The NULL pointer dereference will cause a kernel
oops, killing the triggering process and potentially panicking the system if
`panic_on_oops` is enabled.
* **Accessible From Unprivileged Processes:** No. The `dev` attribute in
configfs is created with `0644` permissions and is owned by root. Unprivileged
users cannot write to it.
* **Accessible From User Namespaces:** No. Configfs does not have the
`FS_USERNS_MOUNT` flag, meaning it cannot be mounted inside an unprivileged user
namespace. Even if it is already mounted by the host, the files are owned by the
global root (`GLOBAL_ROOT_UID`), so a root user inside a user namespace will not
have the necessary DAC overrides to write to them.
**Other Triggers:**
The bug cannot be triggered via network traffic, malicious filesystems,
peripheral devices, or from a VM guest/host context. It strictly requires local
root access to write to the specific configfs interface.]
|