| 0/0 |
2026/05/26 16:00 |
flow |
assessment-security |
25m
Results: map[DenialOfService:false Explanation:**Analysis of the Bug Report**
The KCSAN report highlights a data race on the `pipe->files` field within a
`struct pipe_inode_info`. The race occurs between:
1. **Write:** `put_pipe_info()` (called via `pipe_release()` when a file
descriptor is closed), which decrements `pipe->files`.
2. **Read:** `pipe_lock()` (called via `pipe_wait_writable()` during
`vmsplice()`), which checks `if (pipe->files)`.
**Root Cause & Impact Analysis:**
The `pipe->files` field tracks the number of `struct file` instances referencing
the pipe. It is used in `pipe_lock()` and `pipe_unlock()` as a boolean flag to
distinguish between normal pipes (`pipe->files > 0`) and internal per-task pipes
used by `splice_direct_to_actor()` (`pipe->files == 0`). Internal pipes do not
require locking, while normal pipes do.
In the reported scenario, a user-space program is concurrently calling
`vmsplice()` on one file descriptor and `close()` on another file descriptor
pointing to the same pipe.
- The `vmsplice()` system call holds a reference to its file descriptor (via
`fdget()`), ensuring that the file cannot be freed and `pipe_release()` cannot
be called for it. Thus, `pipe->files` is guaranteed to be at least `1` for the
duration of the `vmsplice()` call.
- The concurrent `close()` of the other file descriptor causes `put_pipe_info()`
to decrement `pipe->files` from `2` to `1`.
- `pipe_lock()` reads `pipe->files` without a lock. However, since the value is
transitioning from `2` to `1`, the read will always observe a non-zero value.
- Because the observed value is always non-zero, `pipe_lock()` will correctly
evaluate the condition as true and proceed to lock the pipe's mutex
(`mutex_lock(&pipe->mutex)`).
**Security Implications:**
Since the logic only depends on `pipe->files` being non-zero, and the file
reference held by `vmsplice()` guarantees it remains non-zero, this data race is
completely benign. The compiler will emit a single load instruction for the
read, which cannot produce a torn value of `0` on supported architectures.
This data race does not lead to any memory corruption, use-after-free, logic
errors, or denial of service. It is a known pattern in the kernel where a benign
data race is flagged by KCSAN because it lacks explicit
`READ_ONCE()`/`WRITE_ONCE()` or `data_race()` annotations.
**Conclusion:**
This is a benign data race and a false positive from a security perspective. It
is not exploitable and does not cause a denial of service. Unprivileged users
can trigger the KCSAN warning by racing `vmsplice()` and `close()`, but it has
no adverse effects on production kernels where KCSAN is disabled. Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/05/26 16:00 |
action |
syz-repro-to-c-repro |
0m
Results:
|
| 2/1 |
2026/05/26 16:00 |
action |
kernel-checkouter |
1m
Results: map[KernelSrc:/app/workdir/cache/src/de2829276ac4973ff7bb50d225da160402a31231]
|
| 3/1 |
2026/05/26 16:01 |
action |
kernel-builder |
11m
Results: map[KernelObj:/app/workdir/cache/build/06092afd896eff1a64d01b734e354fc8b1ca5cac]
|
| 4/1 |
2026/05/26 16:12 |
action |
codesearch-prepare |
9m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/05/26 16:22 |
agent |
expert |
3m
Model:
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true 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 greeping
".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 or elevated privileges.
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 completly 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 tranfers, that's 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 exploitabability depends on how exactly the uninit value
is used in the code, and what it affects.
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 Confidetial 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:
==================================================================
BUG: KCSAN: data-race in pipe_release / pipe_wait_writable
read-write to 0xffff88811bdfb5ac of 4 bytes by task 9866 on cpu 1:
put_pipe_info fs/pipe.c:714 [inline]
pipe_release+0x16a/0x1d0 fs/pipe.c:744
__fput+0x29b/0x650 fs/file_table.c:469
fput_close_sync+0xad/0x190 fs/file_table.c:574
__do_sys_close fs/open.c:1509 [inline]
__se_sys_close fs/open.c:1494 [inline]
__x64_sys_close+0x55/0xf0 fs/open.c:1494
x64_sys_call+0x275d/0x3020 arch/x86/include/generated/asm/syscalls_64.h:4
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x12c/0x370 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
read to 0xffff88811bdfb5ac of 4 bytes by task 9863 on cpu 0:
pipe_lock fs/pipe.c:90 [inline]
pipe_wait_writable+0x202/0x250 fs/pipe.c:1084
wait_for_space fs/splice.c:1272 [inline]
vmsplice_to_pipe fs/splice.c:1551 [inline]
__do_sys_vmsplice fs/splice.c:1608 [inline]
__se_sys_vmsplice+0x42b/0xfd0 fs/splice.c:1578
__x64_sys_vmsplice+0x55/0x70 fs/splice.c:1578
x64_sys_call+0x28e9/0x3020 arch/x86/include/generated/asm/syscalls_64.h:279
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x12c/0x370 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
value changed: 0x00000002 -> 0x00000001
Reported by Kernel Concurrency Sanitizer on:
CPU: 0 UID: 0 PID: 9863 Comm: syz.1.2745 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 03/18/2026
==================================================================
Reply: **Analysis of the Bug Report**
The KCSAN report highlights a data race on the `pipe->files` field within a `struct pipe_inode_info`. The race occurs between:
1. **Write:** `put_pipe_info()` (called via `pipe_release()` when a file descriptor is closed), which decrements `pipe->files`.
2. **Read:** `pipe_lock()` (called via `pipe_wait_writable()` during `vmsplice()`), which checks `if (pipe->files)`.
**Root Cause & Impact Analysis:**
The `pipe->files` field tracks the number of `struct file` instances referencing the pipe. It is used in `pipe_lock()` and `pipe_unlock()` as a boolean flag to distinguish between normal pipes (`pipe->files > 0`) and internal per-task pipes used by `splice_direct_to_actor()` (`pipe->files == 0`). Internal pipes do not require locking, while normal pipes do.
In the reported scenario, a user-space program is concurrently calling `vmsplice()` on one file descriptor and `close()` on another file descriptor pointing to the same pipe.
- The `vmsplice()` system call holds a reference to its file descriptor (via `fdget()`), ensuring that the file cannot be freed and `pipe_release()` cannot be called for it. Thus, `pipe->files` is guaranteed to be at least `1` for the duration of the `vmsplice()` call.
- The concurrent `close()` of the other file descriptor causes `put_pipe_info()` to decrement `pipe->files` from `2` to `1`.
- `pipe_lock()` reads `pipe->files` without a lock. However, since the value is transitioning from `2` to `1`, the read will always observe a non-zero value.
- Because the observed value is always non-zero, `pipe_lock()` will correctly evaluate the condition as true and proceed to lock the pipe's mutex (`mutex_lock(&pipe->mutex)`).
**Security Implications:**
Since the logic only depends on `pipe->files` being non-zero, and the file reference held by `vmsplice()` guarantees it remains non-zero, this data race is completely benign. The compiler will emit a single load instruction for the read, which cannot produce a torn value of `0` on supported architectures.
This data race does not lead to any memory corruption, use-after-free, logic errors, or denial of service. It is a known pattern in the kernel where a benign data race is flagged by KCSAN because it lacks explicit `READ_ONCE()`/`WRITE_ONCE()` or `data_race()` annotations.
**Conclusion:**
This is a benign data race and a false positive from a security perspective. It is not exploitable and does not cause a denial of service. Unprivileged users can trigger the KCSAN warning by racing `vmsplice()` and `close()`, but it has no adverse effects on production kernels where KCSAN is disabled.
|
| 6/2 |
2026/05/26 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 6225
output: 122
thoughts: 96
Thoughts: Here's my analysis of the situation:
**Investigating a Kernel Concurrency Issue**
Okay, so I've got a KCSAN data-race report popping up, specifically in the `pipe_release` and `pipe_wait_writable` functions. That immediately flags a potential issue with concurrent access to shared data within the kernel's pipe implementation. The report points to a race condition β a classic situation where multiple threads or processes try to access and modify the same memory location, potentially leading to inconsistent or incorrect results.
The report details a 4-byte write happening in `put_pipe_info`, which is called within `pipe_release`. Simultaneously, a read is occurring in `pipe_lock`, which is called by `pipe_wait_writable`. This is the precise scenario I need to dissect. The core of the problem seems to be an unsynchronized read/write pattern for a 4-byte region.
My next step is clear: I need to get my hands dirty with the source code. I'll be diving into `fs/pipe.c`, focusing specifically on the areas around lines 714 and 1084. Those locations should pinpoint the `put_pipe_info` and `pipe_lock` calls. This is the starting point for tracing the execution paths and understanding the exact data being accessed in these locations. From there, I'll identify the shared memory or resource, and then diagnose how that interaction can lead to a race condition. I will identify the root cause, and formulate a viable solution to the bug.
|
| 7/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/pipe.c FirstLine:700 LineCount:50]
Results: map[Contents: 700: * behave exactly like pipes for poll().
701: */
702: if (!pipe->readers)
703: mask |= EPOLLERR;
704: }
705:
706: return mask;
707: }
708:
709: static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
710: {
711: int kill = 0;
712:
713: spin_lock(&inode->i_lock);
714: if (!--pipe->files) {
715: inode->i_pipe = NULL;
716: kill = 1;
717: }
718: spin_unlock(&inode->i_lock);
719:
720: if (kill)
721: free_pipe_info(pipe);
722: }
723:
724: static int
725: pipe_release(struct inode *inode, struct file *file)
726: {
727: struct pipe_inode_info *pipe = file->private_data;
728:
729: mutex_lock(&pipe->mutex);
730: if (file->f_mode & FMODE_READ)
731: pipe->readers--;
732: if (file->f_mode & FMODE_WRITE)
733: pipe->writers--;
734:
735: /* Was that the last reader or writer, but not the other side? */
736: if (!pipe->readers != !pipe->writers) {
737: wake_up_interruptible_all(&pipe->rd_wait);
738: wake_up_interruptible_all(&pipe->wr_wait);
739: kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
740: kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
741: }
742: mutex_unlock(&pipe->mutex);
743:
744: put_pipe_info(inode, pipe);
745: return 0;
746: }
747:
748: static int
749: pipe_fasync(int fd, struct file *filp, int on)
]
|
| 8/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/pipe.c FirstLine:1070 LineCount:50]
Results: map[Contents:1070: * if we then do not end up reading everything (ie the whole
1071: * "wake_next_reader/writer" logic in pipe_read/write()).
1072: */
1073: void pipe_wait_readable(struct pipe_inode_info *pipe)
1074: {
1075: pipe_unlock(pipe);
1076: wait_event_interruptible(pipe->rd_wait, pipe_readable(pipe));
1077: pipe_lock(pipe);
1078: }
1079:
1080: void pipe_wait_writable(struct pipe_inode_info *pipe)
1081: {
1082: pipe_unlock(pipe);
1083: wait_event_interruptible(pipe->wr_wait, pipe_writable(pipe));
1084: pipe_lock(pipe);
1085: }
1086:
1087: /*
1088: * This depends on both the wait (here) and the wakeup (wake_up_partner)
1089: * holding the pipe lock, so "*cnt" is stable and we know a wakeup cannot
1090: * race with the count check and waitqueue prep.
1091: *
1092: * Normally in order to avoid races, you'd do the prepare_to_wait() first,
1093: * then check the condition you're waiting for, and only then sleep. But
1094: * because of the pipe lock, we can check the condition before being on
1095: * the wait queue.
1096: *
1097: * We use the 'rd_wait' waitqueue for pipe partner waiting.
1098: */
1099: static int wait_for_partner(struct pipe_inode_info *pipe, unsigned int *cnt)
1100: {
1101: DEFINE_WAIT(rdwait);
1102: int cur = *cnt;
1103:
1104: while (cur == *cnt) {
1105: prepare_to_wait(&pipe->rd_wait, &rdwait, TASK_INTERRUPTIBLE);
1106: pipe_unlock(pipe);
1107: schedule();
1108: finish_wait(&pipe->rd_wait, &rdwait);
1109: pipe_lock(pipe);
1110: if (signal_pending(current))
1111: break;
1112: }
1113: return cur == *cnt ? -ERESTARTSYS : 0;
1114: }
1115:
1116: static void wake_up_partner(struct pipe_inode_info *pipe)
1117: {
1118: wake_up_interruptible_all(&pipe->rd_wait);
1119: }
]
|
| 9/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/pipe.c FirstLine:80 LineCount:20]
Results: map[Contents: 80: #ifdef CONFIG_PROVE_LOCKING
81: static int pipe_lock_cmp_fn(const struct lockdep_map *a,
82: const struct lockdep_map *b)
83: {
84: return cmp_int((unsigned long) a, (unsigned long) b);
85: }
86: #endif
87:
88: void pipe_lock(struct pipe_inode_info *pipe)
89: {
90: if (pipe->files)
91: mutex_lock(&pipe->mutex);
92: }
93: EXPORT_SYMBOL(pipe_lock);
94:
95: void pipe_unlock(struct pipe_inode_info *pipe)
96: {
97: if (pipe->files)
98: mutex_unlock(&pipe->mutex);
99: }
]
|
| 10/2 |
2026/05/26 16:22 |
tool |
codesearch-struct-layout |
0m
Args: map[Name:pipe_inode_info]
Results: map[Fields:[map[Name:mutex OffsetBits:0 SizeBits:256] map[Name:rd_wait OffsetBits:256 SizeBits:192] map[Name:wr_wait OffsetBits:448 SizeBits:192] map[Name: OffsetBits:640 SizeBits:64] map[Name:max_usage OffsetBits:704 SizeBits:32] map[Name:ring_size OffsetBits:736 SizeBits:32] map[Name:nr_accounted OffsetBits:768 SizeBits:32] map[Name:readers OffsetBits:800 SizeBits:32] map[Name:writers OffsetBits:832 SizeBits:32] map[Name:files OffsetBits:864 SizeBits:32] map[Name:r_counter OffsetBits:896 SizeBits:32] map[Name:w_counter OffsetBits:928 SizeBits:32] map[Name:poll_usage OffsetBits:960 SizeBits:8] map[Name:tmp_page OffsetBits:1024 SizeBits:128] map[Name:fasync_readers OffsetBits:1152 SizeBits:64] map[Name:fasync_writers OffsetBits:1216 SizeBits:64] map[Name:bufs OffsetBits:1280 SizeBits:64] map[Name:user OffsetBits:1344 SizeBits:64]]]
|
| 11/2 |
2026/05/26 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 8877
output: 68
thoughts: 507
Thoughts: **Analyzing a Potential Data Race in Pipe Management**
Okay, so I'm looking at this pipe implementation, specifically the interaction between `pipe->files` and the mutex protecting the pipe's internal data. The critical path involves `put_pipe_info`, `pipe_lock`, and `pipe_unlock`. Here's the breakdown as I see it.
The potential data race is on `pipe->files`. In `put_pipe_info`, we're decrementing `pipe->files` under `inode->i_lock`. If it goes to zero, the pipe's info is freed. Now, `pipe_lock` and `pipe_unlock` *read* `pipe->files` without any lock, to decide if they need to acquire or release the mutex `pipe->mutex`.
My immediate concern is: if `pipe->files` is being decremented in `put_pipe_info`, and concurrently `pipe_lock` (or `pipe_unlock`) reads it, we could have a problem. The write in `put_pipe_info` is protected by `inode->i_lock`, but `pipe_lock` doesn't hold `inode->i_lock`, hence, the potential data race.
If `pipe->files` is 0, `pipe_lock` will *not* lock the mutex. Is this a real issue? It seems like `pipe->files` becoming zero means the last file descriptor referencing the pipe is being closed. If the last file descriptor is gone, `put_pipe_info` will free the pipe info via `free_pipe_info(pipe)`.
Here's the crux of the problem: If `pipe_lock` is called while `pipe->files` is *being* decremented to zero, `pipe_lock` could read it as zero, thus not locking the mutex. But, if `pipe->files` is zero, `free_pipe_info(pipe)` will be called, freeing the pipe. If `pipe_lock` is running concurrently, it implies someone else still has a reference to the pipe, doesn't it? But, if someone *else* has a reference to the pipe, how can `pipe->files` *become* zero?
Aha! `vmsplice` is the key. It looks like `vmsplice` might be holding a reference to the pipe even after the file descriptor is closed. I need to look closer at `vmsplice_to_pipe` now. I need to figure out if it is possible that `vmsplice` can call `pipe_lock` after `pipe->files` is decremented to zero, but before `free_pipe_info(pipe)` is called.
|
| 12/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/splice.c FirstLine:1540 LineCount:50]
Results: map[Contents:1540:
1541: if (flags & SPLICE_F_GIFT)
1542: buf_flag = PIPE_BUF_FLAG_GIFT;
1543:
1544: pipe = get_pipe_info(file, true);
1545: if (!pipe)
1546: return -EBADF;
1547:
1548: pipe_clear_nowait(file);
1549:
1550: pipe_lock(pipe);
1551: ret = wait_for_space(pipe, flags);
1552: if (!ret)
1553: ret = iter_to_pipe(iter, pipe, buf_flag);
1554: pipe_unlock(pipe);
1555: if (ret > 0) {
1556: wakeup_pipe_readers(pipe);
1557: fsnotify_modify(file);
1558: }
1559: return ret;
1560: }
1561:
1562: /*
1563: * Note that vmsplice only really supports true splicing _from_ user memory
1564: * to a pipe, not the other way around. Splicing from user memory is a simple
1565: * operation that can be supported without any funky alignment restrictions
1566: * or nasty vm tricks. We simply map in the user memory and fill them into
1567: * a pipe. The reverse isn't quite as easy, though. There are two possible
1568: * solutions for that:
1569: *
1570: * - memcpy() the data internally, at which point we might as well just
1571: * do a regular read() on the buffer anyway.
1572: * - Lots of nasty vm tricks, that are neither fast nor flexible (it
1573: * has restriction limitations on both ends of the pipe).
1574: *
1575: * Currently we punt and implement it as a normal copy, see pipe_to_user().
1576: *
1577: */
1578: SYSCALL_DEFINE4(vmsplice, int, fd, const struct iovec __user *, uiov,
1579: unsigned long, nr_segs, unsigned int, flags)
1580: {
1581: struct iovec iovstack[UIO_FASTIOV];
1582: struct iovec *iov = iovstack;
1583: struct iov_iter iter;
1584: ssize_t error;
1585: int type;
1586:
1587: if (unlikely(flags & ~SPLICE_F_ALL))
1588: return -EINVAL;
1589:
]
|
| 13/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/splice.c FirstLine:1590 LineCount:50]
Results: map[Contents:1590: CLASS(fd, f)(fd);
1591: if (fd_empty(f))
1592: return -EBADF;
1593: if (fd_file(f)->f_mode & FMODE_WRITE)
1594: type = ITER_SOURCE;
1595: else if (fd_file(f)->f_mode & FMODE_READ)
1596: type = ITER_DEST;
1597: else
1598: return -EBADF;
1599:
1600: error = import_iovec(type, uiov, nr_segs,
1601: ARRAY_SIZE(iovstack), &iov, &iter);
1602: if (error < 0)
1603: return error;
1604:
1605: if (!iov_iter_count(&iter))
1606: error = 0;
1607: else if (type == ITER_SOURCE)
1608: error = vmsplice_to_pipe(fd_file(f), &iter, flags);
1609: else
1610: error = vmsplice_to_user(fd_file(f), &iter, flags);
1611:
1612: kfree(iov);
1613: return error;
1614: }
1615:
1616: SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in,
1617: int, fd_out, loff_t __user *, off_out,
1618: size_t, len, unsigned int, flags)
1619: {
1620: if (unlikely(!len))
1621: return 0;
1622:
1623: if (unlikely(flags & ~SPLICE_F_ALL))
1624: return -EINVAL;
1625:
1626: CLASS(fd, in)(fd_in);
1627: if (fd_empty(in))
1628: return -EBADF;
1629:
1630: CLASS(fd, out)(fd_out);
1631: if (fd_empty(out))
1632: return -EBADF;
1633:
1634: return __do_splice(fd_file(in), off_in, fd_file(out), off_out,
1635: len, flags);
1636: }
1637:
1638: /*
1639: * Make sure there's data to read. Wait for input if we can, otherwise
]
|
| 14/2 |
2026/05/26 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 11537
output: 25
thoughts: 641
Thoughts: **Analyzing a Potential Data Race in `vmsplice` and Pipe File Descriptors**
Okay, so I'm digging into this `vmsplice` code, and I see this use of `CLASS(fd, f)(fd)`. That's grabbing a reference to the file descriptor, which means `f.file` should remain valid, and the file shouldn't be prematurely freed. But then I started thinking about `pipe->files`... that's the counter for files referencing the pipe, right? If `vmsplice` is holding a reference to the file, `pipe->files` *shouldn't* be zero. `f.file` is one of the files referencing the pipe.
Alright, now itβs starting to make more sense. `pipe->files` gets decremented in `put_pipe_info`, which is called by `pipe_release`. And `pipe_release` is triggered when a file is closed. If `vmsplice` is holding a reference (using `fdget` in that `CLASS` macro), then the file's reference count, `f_count`, has been bumped up. So `pipe_release` shouldn't be called *for that particular file* while `vmsplice` is running.
However, `pipe->files` is supposed to count *all* files referencing the pipe. I get it, if `pipe->files` is 1, and `vmsplice` holds a reference to that *one* file, then `pipe_release` canβt be called. If there are 2 files, and another is closed, `pipe_release` will be called for the other file, and `pipe->files` will go from 2 to 1. Which makes sense.
That KCSAN report is interesting: "value changed: 0x00000002 -> 0x00000001". So, `pipe->files` was 2, and then it became 1. That tracks. I have to look at `pipe_lock` now:
```c
void pipe_lock(struct pipe_inode_info *pipe)
{
if (pipe->files)
mutex_lock(&pipe->mutex);
}
```
Okay, so if `pipe->files` goes from 2 to 1, the `if` statement will still be true (since 1 is not zero), and `mutex_lock` will still be called. That's good, so it's not a complete disaster. It'll read a non-zero value, either 2 or 1, and then it'll lock the mutex. Is there any danger here?
If `pipe->files` is read as 2 or 1, `mutex_lock` is called because itβs a non-zero value, and so the mutex will be locked. But what if `pipe->files` changes from 1 to 0 *during* the evaluation? That's when things might go sideways.
Okay, let's think it through. If `pipe->files` becomes 0, that means the *last* file referencing the pipe is closing. But if `vmsplice` is in execution, it should *also* be holding a reference to that same file, so it *shouldn't* be able to be closed. So, `pipe->files` should not be able to become 0.
Wait a second... am I missing something here? Is there a window where `pipe->files` *could* become 0 while `vmsplice` is still running? That's the real question. I need to go back and trace how `pipe->files` is initialized and updated. I need to be sure.
|
| 15/2 |
2026/05/26 16:22 |
tool |
codesearch-find-references |
0m
Args: map[Name:pipe_inode_info::files]
Results: map[References:[map[ReferenceKind:takes-address-of ReferencingEntityKind:function ReferencingEntityName:fifo_open SourceFile:fs/pipe.c SourceLine:1132] map[ReferenceKind:writes ReferencingEntityKind:function ReferencingEntityName:fifo_open SourceFile:fs/pipe.c SourceLine:1139] map[ReferenceKind:takes-address-of ReferencingEntityKind:function ReferencingEntityName:fifo_open SourceFile:fs/pipe.c SourceLine:1142] map[ReferenceKind:writes ReferencingEntityKind:function ReferencingEntityName:get_pipe_inode SourceFile:fs/pipe.c SourceLine:901] map[ReferenceKind:reads ReferencingEntityKind:function ReferencingEntityName:iter_file_splice_write SourceFile:fs/splice.c SourceLine:758] map[ReferenceKind:reads ReferencingEntityKind:function ReferencingEntityName:pipe_lock SourceFile:fs/pipe.c SourceLine:90] map[ReferenceKind:reads ReferencingEntityKind:function ReferencingEntityName:pipe_unlock SourceFile:fs/pipe.c SourceLine:97] map[ReferenceKind:takes-address-of ReferencingEntityKind:function ReferencingEntityName:put_pipe_info SourceFile:fs/pipe.c SourceLine:714] map[ReferenceKind:reads ReferencingEntityKind:function ReferencingEntityName:splice_from_pipe_feed SourceFile:fs/splice.c SourceLine:479] map[ReferenceKind:reads ReferencingEntityKind:function ReferencingEntityName:splice_to_socket SourceFile:fs/splice.c SourceLine:907]] TruncatedOutput:false]
|
| 16/2 |
2026/05/26 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 13520
output: 67
thoughts: 17
Thoughts: Here's my thought process, summarized as you requested:
**Investigating Potential Bottlenecks**
Okay, so I'm thinking about where things might be getting held up in this system, and my gut feeling says we need to drill down into the file system interactions, specifically when we're dealing with pipes. That's why I'm going to start by taking a closer look at `get_pipe_inode`. I need to understand how the inode for the pipe itself is being created and managed. Are there any contention points there? Any potential for locking issues? Or is it more of a structural bottleneck?
Then, to understand the interactions when opening the FIFO, I'll need to check `fifo_open`. I'll want to see how these operations might be interacting with others. I want to know about the general behaviour, in specific, the locks or signals that might be involved during the open call. What's the cost of that operation?
|
| 17/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/pipe.c FirstLine:890 LineCount:50]
Results: map[Contents: 890:
891: if (!inode)
892: goto fail_inode;
893:
894: inode->i_ino = get_next_ino();
895:
896: pipe = alloc_pipe_info();
897: if (!pipe)
898: goto fail_iput;
899:
900: inode->i_pipe = pipe;
901: pipe->files = 2;
902: pipe->readers = pipe->writers = 1;
903: inode->i_fop = &pipeanon_fops;
904:
905: /*
906: * Mark the inode dirty from the very beginning,
907: * that way it will never be moved to the dirty
908: * list because "mark_inode_dirty()" will think
909: * that it already _is_ on the dirty list.
910: */
911: inode_state_assign_raw(inode, I_DIRTY);
912: inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;
913: inode->i_uid = current_fsuid();
914: inode->i_gid = current_fsgid();
915: simple_inode_init_ts(inode);
916:
917: return inode;
918:
919: fail_iput:
920: iput(inode);
921:
922: fail_inode:
923: return NULL;
924: }
925:
926: int create_pipe_files(struct file **res, int flags)
927: {
928: struct inode *inode = get_pipe_inode();
929: struct file *f;
930: int error;
931:
932: if (!inode)
933: return -ENFILE;
934:
935: if (flags & O_NOTIFICATION_PIPE) {
936: error = watch_queue_init(inode->i_pipe);
937: if (error) {
938: free_pipe_info(inode->i_pipe);
939: iput(inode);
]
|
| 18/2 |
2026/05/26 16:22 |
tool |
read-file |
0m
Args: map[File:fs/pipe.c FirstLine:1120 LineCount:50]
Results: map[Contents:1120:
1121: static int fifo_open(struct inode *inode, struct file *filp)
1122: {
1123: bool is_pipe = inode->i_fop == &pipeanon_fops;
1124: struct pipe_inode_info *pipe;
1125: int ret;
1126:
1127: filp->f_pipe = 0;
1128:
1129: spin_lock(&inode->i_lock);
1130: if (inode->i_pipe) {
1131: pipe = inode->i_pipe;
1132: pipe->files++;
1133: spin_unlock(&inode->i_lock);
1134: } else {
1135: spin_unlock(&inode->i_lock);
1136: pipe = alloc_pipe_info();
1137: if (!pipe)
1138: return -ENOMEM;
1139: pipe->files = 1;
1140: spin_lock(&inode->i_lock);
1141: if (unlikely(inode->i_pipe)) {
1142: inode->i_pipe->files++;
1143: spin_unlock(&inode->i_lock);
1144: free_pipe_info(pipe);
1145: pipe = inode->i_pipe;
1146: } else {
1147: inode->i_pipe = pipe;
1148: spin_unlock(&inode->i_lock);
1149: }
1150: }
1151: filp->private_data = pipe;
1152: /* OK, we have a pipe and it's pinned down */
1153:
1154: mutex_lock(&pipe->mutex);
1155:
1156: /* We can only do regular read/write on fifos */
1157: stream_open(inode, filp);
1158:
1159: switch (filp->f_mode & (FMODE_READ | FMODE_WRITE)) {
1160: case FMODE_READ:
1161: /*
1162: * O_RDONLY
1163: * POSIX.1 says that O_NONBLOCK means return with the FIFO
1164: * opened, even when there is no process writing the FIFO.
1165: */
1166: pipe->r_counter++;
1167: if (pipe->readers++ == 0)
1168: wake_up_partner(pipe);
1169:
]
|
| 19/2 |
2026/05/26 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 15196
output: 39
thoughts: 500
Thoughts: **Analyzing the Pipe Locking Mechanism**
Okay, so I'm digging into this pipe implementation, and something's bugging me about the locking mechanism. The core of it seems to be centered around `pipe->files`, which, as I understand it, represents the number of file structs referencing a particular pipe. When this counter hits zero, the pipe is freed, right? `put_pipe_info` calls `free_pipe_info(pipe)`.
The part that's tripping me up is this check in `pipe_lock`:
```c
void pipe_lock(struct pipe_inode_info *pipe)
{
if (pipe->files)
mutex_lock(&pipe->mutex);
}
```
Why is that conditional there? Initially, I figured it was a safety check, anticipating a scenario where the pipe might be in the process of being destroyed concurrently, but if `pipe->files` is zero, `pipe_lock` essentially does nothing. The pipe *is* being freed! So it shouldn't be possible. If `pipe->files` is zero, the `put_pipe_info` function already called `free_pipe_info(pipe)`. This seems like a potential race condition! If `pipe_lock` is called concurrently, it might try to access freed memory β a critical bug!
But hold on...how is `pipe_lock` even *called*? Where does the `pipe` pointer come from? Looking at the `vmsplice_to_pipe` function:
```c
pipe = get_pipe_info(file, true);
if (!pipe)
return -EBADF;
pipe_clear_nowait(file);
pipe_lock(pipe);
```
Ah, I see. `get_pipe_info` pulls the `pipe` from `file->private_data`. Since `vmsplice` presumably holds a reference to `file` (probably via `fdget`), that `file` *cannot* be closed, which means `file->private_data` (and thus `pipe`) can't be freed while `vmsplice` has a hold of the `file`.
And here's where it clicks β `pipe->files` *must* be at least 1! That counter *is* tracking references, and `vmsplice`'s `file` is one of them. So `pipe->files` *can't* be zero.
So, *why* is the check there? Why the seemingly redundant `if (pipe->files)`? It doesn't make logical sense in this current implementation. Time to crack open `git blame` or `git log` and see the history of this function. Let's find out when this conditional was introduced and the reasoning behind it.
|
| 20/2 |
2026/05/26 16:22 |
tool |
git-log |
0m
Args: map[CodeRegexp:if \(pipe->files\) Count:10 PathPrefix:fs/pipe.c]
Results: map[Output:85f273a6a1e4 fs/pipe: Convert to lockdep_cmp_fn
6447a3cf19da get rid of pipe->inode
]
|
| 21/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 16395
output: 25
thoughts: 17
Thoughts: Okay, here's my interpretation of what's going through my mind as I approach commit `6447a3cf19da`:
**Initial Scan of a Commit**
Alright, let's dive into commit `6447a3cf19da`. Immediately, I need to get a high-level sense of what changed. I'm not going to immediately drill down into every line, I need to understand *why* this commit exists first. What problem were we trying to solve? Or what feature were we implementing? I'll probably start by looking at the commit message; hopefully, it's clear and concise. If not, I'll have to rely on context clues from the files that were touched. Was it a bug fix? A performance improvement? A new feature addition? Or perhaps just a refactor? I need a roadmap for my inspection.
|
| 22/2 |
2026/05/26 16:23 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 6447a3cf19da8c4653283d1c491e2e775633f348
Author: Al Viro <viro@zeniv.linux.org.uk>
Date: Thu Mar 21 11:01:38 2013 -0400
get rid of pipe->inode
it's used only as a flag to distinguish normal pipes/FIFOs from the
internal per-task one used by file-to-file splice. And pipe->files
would work just as well for that purpose...
Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 11dfa0c3fb46..9bfd1a3214e6 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -1319,7 +1319,7 @@ static ssize_t fuse_dev_splice_read(struct file *in, loff_t *ppos,
page_nr++;
ret += buf->len;
- if (pipe->inode)
+ if (pipe->files)
do_wakeup = 1;
}
diff --git a/fs/pipe.c b/fs/pipe.c
index e2fc5ccb0d49..39bdec06fe2b 100644
--- a/fs/pipe.c
+++ b/fs/pipe.c
@@ -55,7 +55,7 @@ unsigned int pipe_min_size = PAGE_SIZE;
static void pipe_lock_nested(struct pipe_inode_info *pipe, int subclass)
{
- if (pipe->inode)
+ if (pipe->files)
mutex_lock_nested(&pipe->mutex, subclass);
}
@@ -70,7 +70,7 @@ EXPORT_SYMBOL(pipe_lock);
void pipe_unlock(struct pipe_inode_info *pipe)
{
- if (pipe->inode)
+ if (pipe->files)
mutex_unlock(&pipe->mutex);
}
EXPORT_SYMBOL(pipe_unlock);
@@ -785,7 +785,6 @@ struct pipe_inode_info * alloc_pipe_info(struct inode *inode)
if (pipe->bufs) {
init_waitqueue_head(&pipe->wait);
pipe->r_counter = pipe->w_counter = 1;
- pipe->inode = inode;
pipe->buffers = PIPE_DEF_BUFFERS;
mutex_init(&pipe->mutex);
return pipe;
diff --git a/fs/splice.c b/fs/splice.c
index 7efc2f5057fb..9f2a4447da50 100644
--- a/fs/splice.c
+++ b/fs/splice.c
@@ -218,7 +218,7 @@ ssize_t splice_to_pipe(struct pipe_inode_info *pipe,
page_nr++;
ret += buf->len;
- if (pipe->inode)
+ if (pipe->files)
do_wakeup = 1;
if (!--spd->nr_pages)
@@ -828,7 +828,7 @@ int splice_from_pipe_feed(struct pipe_inode_info *pipe, struct splice_desc *sd,
ops->release(pipe, buf);
pipe->curbuf = (pipe->curbuf + 1) & (pipe->buffers - 1);
pipe->nrbufs--;
- if (pipe->inode)
+ if (pipe->files)
sd->need_wakeup = true;
}
diff --git a/include/linux/pipe_fs_i.h b/include/linux/pipe_fs_i.h
index d803a85a64b6..ed8eeeb10811 100644
--- a/include/linux/pipe_fs_i.h
+++ b/include/linux/pipe_fs_i.h
@@ -41,7 +41,6 @@ struct pipe_buffer {
* @w_counter: writer counter
* @fasync_readers: reader side fasync
* @fasync_writers: writer side fasync
- * @inode: inode this pipe is attached to
* @bufs: the circular array of pipe buffers
**/
struct pipe_inode_info {
@@ -57,7 +56,6 @@ struct pipe_inode_info {
struct page *tmp_page;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
- struct inode *inode;
struct pipe_buffer *bufs;
};
]
|
| 23/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 17877
output: 17
thoughts: 548
Thoughts: **Analyzing the `pipe->files` Field in the Context of a Potential Race Condition**
Okay, so I'm digging into this `pipe->files` field and how it's used. It seems like it acts as a flag, differentiating between normal pipes and those internal ones created for file-to-file splicing. That makes sense. Let me clarify, is the internal per-task pipe still used in file-to-file splice? Time to check `fs/splice.c` to confirm the details around the use of `pipe->files`.
Right, if `pipe->files` is being used to tell normal pipes apart from internal ones, then for the internal pipes, `pipe->files` is likely zero. Normal pipes, on the other hand, should have a value greater than zero. Now, there's this potential data race. It's between `put_pipe_info` decrementing `pipe->files` from, say, two to one, and `pipe_lock` reading `pipe->files`.
If `pipe->files` is read as one or two, itβs non-zero. That means `pipe_lock` *will* lock the mutex. But, is this a problem?
No, hold on, I think I'm seeing it. For a *normal* pipe, and we're using a normal one here via `vmsplice` since we are pulling from a file descriptor, `pipe->files` is always going to be greater than zero as long as there's an open file descriptor. The `pipe_lock`'s read is just `if (pipe->files)`. Since itβs a normal pipe, and we have a reference to the file descriptor through the pipe, `pipe->files` is guaranteed to be greater than zero, no matter what, so this condition will always evaluate to true, whether it reads one or two.
Wait, what if `pipe->files` *is* decremented to zero? Ah, that would mean the last file descriptor is closed. However, `vmsplice` holds a reference, so it *can't* be decremented to zero while we're in the critical section! So, `pipe->files` will *always* be greater than zero during `vmsplice`. Therefore, `pipe_lock` will *always* lock the mutex when it should.
So, the data race appears benign. It's a read of `pipe->files` without a lock, but the value is irrelevant as long as it's non-zero, and that's guaranteed by the file reference count.
Now, I should confirm that `pipe->files` isn't used anywhere else that could cause a problem. Let's trace all of the uses of `pipe->files` and make sure it's all consistent and there are no other places for a similar issue.
|
| 24/2 |
2026/05/26 16:23 |
tool |
grepper |
0m
Args: map[Expression:pipe->files]
Results: map[Output:fs/pipe.c=88=void pipe_lock(struct pipe_inode_info *pipe)
fs/pipe.c-89-{
fs/pipe.c:90: if (pipe->files)
fs/pipe.c-91- mutex_lock(&pipe->mutex);
--
fs/pipe.c=95=void pipe_unlock(struct pipe_inode_info *pipe)
fs/pipe.c-96-{
fs/pipe.c:97: if (pipe->files)
fs/pipe.c-98- mutex_unlock(&pipe->mutex);
--
fs/pipe.c=709=static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
--
fs/pipe.c-713- spin_lock(&inode->i_lock);
fs/pipe.c:714: if (!--pipe->files) {
fs/pipe.c-715- inode->i_pipe = NULL;
--
fs/pipe.c=886=static struct inode * get_pipe_inode(void)
--
fs/pipe.c-900- inode->i_pipe = pipe;
fs/pipe.c:901: pipe->files = 2;
fs/pipe.c-902- pipe->readers = pipe->writers = 1;
--
fs/pipe.c=1121=static int fifo_open(struct inode *inode, struct file *filp)
--
fs/pipe.c-1131- pipe = inode->i_pipe;
fs/pipe.c:1132: pipe->files++;
fs/pipe.c-1133- spin_unlock(&inode->i_lock);
--
fs/pipe.c-1138- return -ENOMEM;
fs/pipe.c:1139: pipe->files = 1;
fs/pipe.c-1140- spin_lock(&inode->i_lock);
fs/pipe.c-1141- if (unlikely(inode->i_pipe)) {
fs/pipe.c:1142: inode->i_pipe->files++;
fs/pipe.c-1143- spin_unlock(&inode->i_lock);
--
fs/splice.c=442=static int splice_from_pipe_feed(struct pipe_inode_info *pipe, struct splice_desc *sd,
--
fs/splice.c-478- pipe->tail = tail;
fs/splice.c:479: if (pipe->files)
fs/splice.c-480- sd->need_wakeup = true;
--
fs/splice.c=662=iter_file_splice_write(struct pipe_inode_info *pipe, struct file *out,
--
fs/splice.c-757- pipe->tail = tail;
fs/splice.c:758: if (pipe->files)
fs/splice.c-759- sd.need_wakeup = true;
--
fs/splice.c=795=ssize_t splice_to_socket(struct pipe_inode_info *pipe, struct file *out,
--
fs/splice.c-906- pipe->tail = tail;
fs/splice.c:907: if (pipe->files)
fs/splice.c-908- need_wakeup = true;
]
|
| 25/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 19905
output: 19
thoughts: 338
Thoughts: **Analyzing the Race Condition in Pipe File Descriptor Handling**
Okay, so I'm looking at how `pipe->files` is used across several functions: `pipe_lock`, `pipe_unlock`, `splice_from_pipe_feed`, `iter_file_splice_write`, and `splice_to_socket`. I see that it's read in these functions without any kind of lock. But where is it written? Ah, in `put_pipe_info`, `get_pipe_inode`, and `fifo_open`. The writes *are* protected by `inode->i_lock`, except in that `get_pipe_inode` case where the pipe is freshly allocated β interesting.
The thing is, `pipe->files` seems to be acting as a boolean flag. It's indicating whether itβs a standard pipe or an internal pipe. And for a standard pipe, as long as the pipe exists it will *always* be greater than 0, so the exact value doesnβt matter. But, I need to consider a potential race: what if `pipe->files` is decremented from 1 to 0?
If it *does* go from 1 to 0, that signals the last file descriptor is closing. Now I have to ask myself: can any of those functions I mentioned ( `pipe_lock`, `splice_from_pipe_feed`, etc.) *possibly* be running concurrently when that happens? For `pipe_lock`, it's called by `vmsplice`, which holds a reference to the file descriptor, so `pipe->files` can't be 0. So what about internal pipes, those are created through `splice` or something similar?
Wait, do we even *still* have internal pipes in the current implementation? I'll need to check `fs/splice.c` to be sure. That's the best way to get a full view of this part of the code.
|
| 26/2 |
2026/05/26 16:23 |
tool |
grepper |
0m
Args: map[Expression:alloc_pipe_info]
Results: map[Output:fs/pipe.c=787=bool pipe_is_unprivileged_user(void)
--
fs/pipe.c-791-
fs/pipe.c:792:struct pipe_inode_info *alloc_pipe_info(void)
fs/pipe.c-793-{
--
fs/pipe.c=886=static struct inode * get_pipe_inode(void)
--
fs/pipe.c-895-
fs/pipe.c:896: pipe = alloc_pipe_info();
fs/pipe.c-897- if (!pipe)
--
fs/pipe.c=1121=static int fifo_open(struct inode *inode, struct file *filp)
--
fs/pipe.c-1135- spin_unlock(&inode->i_lock);
fs/pipe.c:1136: pipe = alloc_pipe_info();
fs/pipe.c-1137- if (!pipe)
--
fs/splice.c=1025=ssize_t splice_direct_to_actor(struct file *in, struct splice_desc *sd,
--
fs/splice.c-1046- if (unlikely(!pipe)) {
fs/splice.c:1047: pipe = alloc_pipe_info();
fs/splice.c-1048- if (!pipe)
--
include/linux/pipe_fs_i.h=313=void pipe_wait_writable(struct pipe_inode_info *);
include/linux/pipe_fs_i.h-314-
include/linux/pipe_fs_i.h:315:struct pipe_inode_info *alloc_pipe_info(void);
include/linux/pipe_fs_i.h-316-void free_pipe_info(struct pipe_inode_info *);
]
|
| 27/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 21141
output: 34
thoughts: 35
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm deep in the weeds of kernel-level I/O:
**Digging into `splice_direct_to_actor`**
Right, *that* call⦠`splice_direct_to_actor` is the one. I remember seeing that name. Now, let me see⦠ah, yes! It's calling `alloc_pipe_info()`. That rings a bell. Okay, so we're talking about direct I/O with pipes, and the pipe information structure itself must be critical to that. Makes sense. I need to take a closer look at `splice_direct_to_actor` now to understand exactly how those two functions interact and what the implications are. This could be where the performance bottleneck is for that thing I've been working on.
|
| 28/2 |
2026/05/26 16:23 |
tool |
read-file |
0m
Args: map[File:fs/splice.c FirstLine:1020 LineCount:50]
Results: map[Contents:1020: * points, without requiring an explicit pipe. Internally an allocated
1021: * pipe is cached in the process, and reused during the lifetime of
1022: * that process.
1023: *
1024: */
1025: ssize_t splice_direct_to_actor(struct file *in, struct splice_desc *sd,
1026: splice_direct_actor *actor)
1027: {
1028: struct pipe_inode_info *pipe;
1029: ssize_t ret, bytes;
1030: size_t len;
1031: int i, flags, more;
1032:
1033: /*
1034: * We require the input to be seekable, as we don't want to randomly
1035: * drop data for eg socket -> socket splicing. Use the piped splicing
1036: * for that!
1037: */
1038: if (unlikely(!(in->f_mode & FMODE_LSEEK)))
1039: return -EINVAL;
1040:
1041: /*
1042: * neither in nor out is a pipe, setup an internal pipe attached to
1043: * 'out' and transfer the wanted data from 'in' to 'out' through that
1044: */
1045: pipe = current->splice_pipe;
1046: if (unlikely(!pipe)) {
1047: pipe = alloc_pipe_info();
1048: if (!pipe)
1049: return -ENOMEM;
1050:
1051: /*
1052: * We don't have an immediate reader, but we'll read the stuff
1053: * out of the pipe right after the splice_to_pipe(). So set
1054: * PIPE_READERS appropriately.
1055: */
1056: pipe->readers = 1;
1057:
1058: current->splice_pipe = pipe;
1059: }
1060:
1061: /*
1062: * Do the splice.
1063: */
1064: bytes = 0;
1065: len = sd->total_len;
1066:
1067: /* Don't block on output, we have to drain the direct pipe. */
1068: flags = sd->flags;
1069: sd->flags &= ~SPLICE_F_NONBLOCK;
]
|
| 29/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 22095
output: 36
thoughts: 981
Thoughts: **Benign Data Race in Pipe Implementation**
Okay, let me think this through. The KCSAN report flagged a potential data race between `put_pipe_info`, which decrements `pipe->files`, and the `pipe_lock` function, which reads `pipe->files`. I need to understand if this is a real problem. First, I know `pipe->files` is 0 for internal pipes because it's initialized that way. For normal pipes, it should be greater than zero, representing the number of file descriptors referencing the pipe.
The issue is that `pipe->files` is read without a lock in `pipe_lock`. If the read happens concurrently with a decrement in `put_pipe_info`, there *could* be a problem. My initial thought is that the check `pipe->files > 0` might incorrectly evaluate to false if it reads a zero. But wait, is this even possible?
For a *normal* pipe, the caller of `pipe_lock` holds a reference to a file descriptor. That means `pipe->files` *must* be at least 1. So, if the read happens concurrently, it has to read either the original value or the decremented value, both of which are > 0. Since it's an `unsigned int` (32-bit), reads and writes should be atomic on x86-64, right? So, there's no way it'll return a value less than 0. Okay, I think this should be fine. Even if there *is* a data race, it looks like it is benign.
But hold on, is there a use-after-free issue? If `pipe->files` gets decremented to 0, `put_pipe_info` could free the pipe! But as I mentioned, `vmsplice` holds a reference. But, since `pipe->files` is always >= 1, the `pipe` will not be freed. However, could there be another thread closing the file descriptor, causing the refcount to go to zero?
I know `fdget` protects against that. It increments the file's `f_count` if the file table is shared (or uses the lock/RCU if it is not). That `fdget` call prevents the file from being freed before `vmsplice` is done. `fput` won't call `file->f_op->release`, which is `pipe_release`, for this file descriptor. `pipe_release` decrements `pipe->files`. But for *this* file descriptor, `pipe_release` will never be called if `fdget` holds a reference, because the file descriptor is still alive. The refcount is never 0 until `vmsplice` is complete.
Okay, let me confirm. `pipe->files` represents the number of file descriptors that reference the pipe, that is, it is the number of times `fifo_open` or `get_pipe_inode` was called, minus the number of times `pipe_release` was called. `vmsplice` holds a reference to a file descriptor, so `pipe_release` cannot be called for that specific file descriptor. So, `pipe->files` must be at least 1. Got it. So this particular `pipe`'s `pipe_release` will never run before it finishes using the `pipe`. This also means that `pipe_lock` will *always* see a value > 0.
Just to be thorough, could `pipe->files` *ever* be zero, even with `vmsplice` in the picture? If `vmsplice` is called on an internal pipe, then there is no file descriptor, so it wouldn't even happen. And I know only `fifo_open`, `get_pipe_inode`, and `put_pipe_info` modify the counter, all related to open and close.
So, in conclusion, this data race is completely benign. I should also check if there have been any recent patches addressing this.
|
| 30/2 |
2026/05/26 16:23 |
tool |
git-log |
0m
Args: map[CodeRegexp:pipe->files Count:10 PathPrefix:fs/pipe.c]
Results: map[Output:85f273a6a1e4 fs/pipe: Convert to lockdep_cmp_fn
b0d8d2292160 vfs: fix subtle use-after-free of pipe_inode_info
6447a3cf19da get rid of pipe->inode
ba5bb147330a pipe: take allocation and freeing of pipe_inode_info out of ->i_mutex
]
|
| 31/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 24098
output: 27
thoughts: 19
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm deep in my domain:
**Diving into Commit b0d8d2292160**
Right, let's get down to brass tacks. I need to take a look at commit `b0d8d2292160`. No fluff, no hesitation. I'm focusing in, zeroing in on this specific point in the codebase's history. This particular snapshot holds something I need to understand, maybe troubleshoot, or perhaps just verify. It could be a bug fix, a new feature implementation, or a refactoring effort; the specifics don't matter just yet. What *does* matter is that this is the focal point of my current investigation. I need to understand what changed, *why* it changed, and how those alterations impact the larger system. Time to crack open the diff tool. Let's see what secrets this commit holds.
|
| 32/2 |
2026/05/26 16:23 |
tool |
git-show |
0m
Args:
Results: map[Output:commit b0d8d2292160bb63de1972361ebed100c64b5b37
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Mon Dec 2 09:44:51 2013 -0800
vfs: fix subtle use-after-free of pipe_inode_info
The pipe code was trying (and failing) to be very careful about freeing
the pipe info only after the last access, with a pattern like:
spin_lock(&inode->i_lock);
if (!--pipe->files) {
inode->i_pipe = NULL;
kill = 1;
}
spin_unlock(&inode->i_lock);
__pipe_unlock(pipe);
if (kill)
free_pipe_info(pipe);
where the final freeing is done last.
HOWEVER. The above is actually broken, because while the freeing is
done at the end, if we have two racing processes releasing the pipe
inode info, the one that *doesn't* free it will decrement the ->files
count, and unlock the inode i_lock, but then still use the
"pipe_inode_info" afterwards when it does the "__pipe_unlock(pipe)".
This is *very* hard to trigger in practice, since the race window is
very small, and adding debug options seems to just hide it by slowing
things down.
Simon originally reported this way back in July as an Oops in
kmem_cache_allocate due to a single bit corruption (due to the final
"spin_unlock(pipe->mutex.wait_lock)" incrementing a field in a different
allocation that had re-used the free'd pipe-info), it's taken this long
to figure out.
Since the 'pipe->files' accesses aren't even protected by the pipe lock
(we very much use the inode lock for that), the simple solution is to
just drop the pipe lock early. And since there were two users of this
pattern, create a helper function for it.
Introduced commit ba5bb147330a ("pipe: take allocation and freeing of
pipe_inode_info out of ->i_mutex").
Reported-by: Simon Kirby <sim@hostway.ca>
Reported-by: Ian Applegate <ia@cloudflare.com>
Acked-by: Al Viro <viro@zeniv.linux.org.uk>
Cc: stable@kernel.org # v3.10+
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
diff --git a/fs/pipe.c b/fs/pipe.c
index d2c45e14e6d8..0e0752ef2715 100644
--- a/fs/pipe.c
+++ b/fs/pipe.c
@@ -726,11 +726,25 @@ pipe_poll(struct file *filp, poll_table *wait)
return mask;
}
+static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
+{
+ int kill = 0;
+
+ spin_lock(&inode->i_lock);
+ if (!--pipe->files) {
+ inode->i_pipe = NULL;
+ kill = 1;
+ }
+ spin_unlock(&inode->i_lock);
+
+ if (kill)
+ free_pipe_info(pipe);
+}
+
static int
pipe_release(struct inode *inode, struct file *file)
{
- struct pipe_inode_info *pipe = inode->i_pipe;
- int kill = 0;
+ struct pipe_inode_info *pipe = file->private_data;
__pipe_lock(pipe);
if (file->f_mode & FMODE_READ)
@@ -743,17 +757,9 @@ pipe_release(struct inode *inode, struct file *file)
kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
}
- spin_lock(&inode->i_lock);
- if (!--pipe->files) {
- inode->i_pipe = NULL;
- kill = 1;
- }
- spin_unlock(&inode->i_lock);
__pipe_unlock(pipe);
- if (kill)
- free_pipe_info(pipe);
-
+ put_pipe_info(inode, pipe);
return 0;
}
@@ -1014,7 +1020,6 @@ static int fifo_open(struct inode *inode, struct file *filp)
{
struct pipe_inode_info *pipe;
bool is_pipe = inode->i_sb->s_magic == PIPEFS_MAGIC;
- int kill = 0;
int ret;
filp->f_version = 0;
@@ -1130,15 +1135,9 @@ static int fifo_open(struct inode *inode, struct file *filp)
goto err;
err:
- spin_lock(&inode->i_lock);
- if (!--pipe->files) {
- inode->i_pipe = NULL;
- kill = 1;
- }
- spin_unlock(&inode->i_lock);
__pipe_unlock(pipe);
- if (kill)
- free_pipe_info(pipe);
+
+ put_pipe_info(inode, pipe);
return ret;
}
]
|
| 33/2 |
2026/05/26 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 25797
output: 37
thoughts: 3067
Thoughts: **Benign Data Race in Pipe File Descriptor Accounting**
Okay, so the initial issue was a use-after-free scenario, which `put_pipe_info` was moved to address. This ensured `pipe` wasn't freed prematurely. However, the existing `pipe_lock` function, which checks `pipe->files` before acquiring a mutex, piqued my interest. My thought process quickly zeroed in on the potential data race: concurrent calls to `pipe_lock` and `put_pipe_info` could lead to inconsistent reads of `pipe->files`.
The logic seemed sound initially. `pipe_lock` is only called when a file descriptor reference is held, implying `pipe->files` can't be zero in that context. But `pipe_wait_writable`, called from `vmsplice_to_pipe`, raised a red flag. Digging into the call chain, `vmsplice_to_pipe` *does* hold a file descriptor, negating the zero `pipe->files` possibility.
I realized the race boils down to a decrementing of `pipe->files` by `put_pipe_info` (say, from 2 to 1) and a concurrent read by `pipe_lock`. Since both values are non-zero, the mutex is consistently locked, making the race *benign*. This read could only be between a non-zero to a non-zero, never to zero.
The next concern was related to internal pipes where `pipe->files` is 0. However, those pipes are per-task. The per-task internal pipe doesn't need to be locked because the operations are not shared across processes. When `pipe->files` is 0, it means it's per-task, and it skips the lock. When `pipe->files` is non-zero, it means a normal pipe that has locking applied.
Furthermore, I explored `pipe_release`, noting `put_pipe_info` is *after* the mutex unlock. This confirmed that even if the last file descriptor is being closed, and `pipe->files` becomes 0, there isn't a race as the mutex has been already released.
I considered compiler optimizations, but the assembly code confirms a simple load and branch, making a torn value where zero is returned, impossible. Memory ordering also doesn't apply because `pipe->files` functions as a boolean flag. Even if the load is reordered, the value will still be non-zero.
I reviewed other uses of `pipe->files` in `splice_from_pipe_feed`, `iter_file_splice_write`, and `splice_to_socket`, where it's treated as a boolean. This re-confirmed the data race is benign.
Then, I looked at a KCSAN report detailing the race conditions. I confirmed the report mirrors the scenario I've described: a file descriptor being closed (decrementing `pipe->files`) and a concurrent call to `pipe_wait_writable` (reading `pipe->files`). The report is accurate and describes a safe data race.
I explored the extreme case of `pipe->files` being 1 and `pipe_release` and `pipe_lock` being called concurrently. Since `vmsplice` and `pipe_release` can only call these operations when the file is available, they will not result in a race and it is completely normal.
The data race is not exploitable and cannot lead to memory corruption or denial of service because a non-zero is always read.
Finally, I considered an `unsigned int` overflow in `pipe->files`, but reaching the max value is practically impossible due to file descriptor limits. Even if it happens, it cannot lead to a security problem, since the `pipe->files` value will not be interpreted as zero, but 0xffffffff which is non-zero. The same idea goes for the increment operations in `fifo_open`. The potential for a zero read from a compiler optimization is also ruled out by the simple load instruction in the assembly.
I plan to search for recent patches related to this data race with a search on git, but overall, it appears the KCSAN report is flagging a benign data race.
|
| 34/2 |
2026/05/26 16:24 |
tool |
git-log |
0m
Args: map[MessageRegexps:[data-race pipe_release] PathPrefix:fs/pipe.c]
Results:
|
| 35/2 |
2026/05/26 16:24 |
llm |
expert |
0m
Model:
Tokens:
input: 29809
output: 26
thoughts: 18
Thoughts: Here's a summary of my thought process:
**Searching for a Missing Commit**
Okay, so I'm looking at `fs/pipe.c` and I'm pretty certain there's no commit I'm expecting to find there. Time to cast a wider net. No point in assuming I know where the change happened without verification. Let's fire up a global search. I need to understand what's been happening around that area.
|
| 36/2 |
2026/05/26 16:24 |
tool |
git-log |
0m
Args: map[MessageRegexps:[data-race pipe_lock]]
Results:
|
| 37/2 |
2026/05/26 16:24 |
llm |
expert |
0m
Model:
Tokens:
input: 29962
output: 20
thoughts: 16
Thoughts: Okay, here's my interpretation of that thought process, tailored for an expert audience:
**Investigating Potential Concurrency Issues with Pipe and File Interactions**
Alright, my internal thought process starts with a specific technical focus. I'm immediately drawn to the combination of `pipe->files` and the potential for `data-race` conditions. This suggests an investigation into a very specific area of concurrency challenges within a system, likely involving inter-process communication (IPC) through pipes, and the subsequent interaction with file system operations. I'm thinking about scenarios where multiple threads or processes might be accessing and modifying shared data related to files, leading to unpredictable and incorrect results due to unsynchronized access. The use of a pipe implies some sort of stream-based data transfer. I'm thinking about potential issues where data might be partially written, overwritten, or read incorrectly if the access isn't properly guarded, perhaps with mutexes, semaphores, or other synchronization primitives. I need to delve deeper into the system's architecture to really understand where these potential vulnerabilities might lie, the specific file operations involved, and the threading/process models at play. This search likely represents the initial step in a detailed debugging or performance analysis effort.
|
| 38/2 |
2026/05/26 16:24 |
tool |
git-log |
0m
Args: map[MessageRegexps:[pipe->files]]
Results: map[Output:b10a4a9f7695 create_pipe_files(): use fput() if allocation of the second file fails
b0d8d2292160 vfs: fix subtle use-after-free of pipe_inode_info
6447a3cf19da get rid of pipe->inode
ba5bb147330a pipe: take allocation and freeing of pipe_inode_info out of ->i_mutex
]
|
| 39/2 |
2026/05/26 16:25 |
llm |
expert |
0m
Model:
Tokens:
input: 30369
output: 66
thoughts: 1518
Thoughts: **Analysis of the KCSAN Warning: A Benign Data Race**
Okay, so there's no immediate fix for this KCSAN warning, which isn't surprising. It's a known pattern that these data race reports pop up when there's no explicit annotation with `READ_ONCE`, `WRITE_ONCE`, or `data_race()`. To properly address it, I'd probably wrap the read of `pipe->files` in `data_race()` or `READ_ONCE()`, and the write with `WRITE_ONCE()`. But, the key takeaway here is that, from a security standpoint, this isn't a vulnerability.
Let me lay out the reasoning step-by-step:
1. The KCSAN report flagged a data race on `pipe->files` between the `put_pipe_info` function (which writes to the field) and the `pipe_lock` function (which reads from it).
2. `put_pipe_info` decrements `pipe->files` whenever a file descriptor associated with the pipe is closed.
3. `pipe_lock` reads `pipe->files` to determine whether the pipe is a normal pipe (with `files > 0`) or an internal pipe per-task (`files == 0`).
4. In the reported scenario, `vmsplice` is active and holds a reference to a file descriptor. This guarantees that `pipe->files` is at least 1.
5. A concurrent close of another file descriptor decrements `pipe->files` from 2 to 1.
6. `pipe_lock` reads `pipe->files` concurrently. The value transitions from 2 to 1, but this doesn't change the outcome. Both are non-zero, and `pipe_lock` will always evaluate the condition `if (pipe->files)` as true and lock the mutex.
7. This data race is completely benign. It doesn't affect any logic or memory safety in the kernel.
8. There's no way to exploit this. No denial-of-service, no memory corruption, and no privilege escalation.
Now, let's nail down the analysis results:
* Exploitable: **False**
* DenialOfService: **False**
* Unprivileged: **True** (An unprivileged user can trigger this by racing `vmsplice` and `close`)
* UserNamespace: **True** (It's accessible from unprivileged code; therefore, it's also reachable from user namespaces.)
* VMGuestTrigger: **False**
* VMHostTrigger: **False**
* NetworkTrigger: **False**
* RemoteTrigger: **False**
* PeripheralTrigger: **False**
* FilesystemTrigger: **False**
I'm double-checking, and `vmsplice` and pipes are standard and available to all users. No restrictions. This is definitely accessible to unprivileged processes.
Let's summarize clearly:
The KCSAN report concerns a data race on the `pipe->files` field of the `struct pipe_inode_info`.
* The race occurs between:
1. A write in `put_pipe_info` (triggered by closing a file descriptor, thus calling `pipe_release`), which decrements `pipe->files`.
2. A read in `pipe_lock` (called by `pipe_wait_writable` in the `vmsplice` function), which checks the value of `pipe->files`.
* The field `pipe->files` tracks the number of `struct file` instances referencing the pipe. Its value is used as a boolean to distinguish between normal pipes (`pipe->files > 0`) and internal pipes (`pipe->files == 0`) used by `splice_direct_to_actor`. Normal pipes require locking.
* In this specific scenario, a user-space program concurrently calls `vmsplice` and `close` on file descriptors pointing to the same pipe.
* The `vmsplice` system call guarantees that `pipe->files` is at least 1.
* The concurrent `close` decrements `pipe->files` from 2 to 1.
* `pipe_lock` reads `pipe->files`. Since the value can only transition from 2 to 1, the result is not affected.
* Since the read value is always non-zero, `pipe_lock` correctly proceeds to lock the mutex (`mutex_lock(&pipe->mutex)`).
* Since the only condition is whether or not the value is non-zero, this is a completely benign data race. The
|
| 40/2 |
2026/05/26 16:25 |
tool |
set-results |
0m
Args: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
|
| 41/2 |
2026/05/26 16:25 |
llm |
expert |
0m
Model:
Tokens:
input: 33042
output: 663
thoughts: 14
Thoughts: Okay, here's the summary, rendered as a first-person thought process, keeping in mind the user is an expert:
**Finalizing the Response**
Alright, the data is in, the analysis is complete, and the results are definitive. Time to synthesize all of this and formulate the final, polished response. My goal here isn't just to *present* the findings, but to strategically frame them for the user. I need to make sure the key insights are immediately apparent, that the methodology is transparent (though I don't need to over-explain; this is an expert audience, after all), and that the conclusions are both robust and actionable. I'll need to consider how to best position these results within the broader context of their work, identifying implications and potential avenues for further exploration. It's about distilling the complexity into something elegant and impactful. Let's get this done.
|
| 42/1 |
2026/05/26 16:25 |
action |
format-explanation |
0m
Results: map[Explanation:**Analysis of the Bug Report**
The KCSAN report highlights a data race on the `pipe->files` field within a
`struct pipe_inode_info`. The race occurs between:
1. **Write:** `put_pipe_info()` (called via `pipe_release()` when a file
descriptor is closed), which decrements `pipe->files`.
2. **Read:** `pipe_lock()` (called via `pipe_wait_writable()` during
`vmsplice()`), which checks `if (pipe->files)`.
**Root Cause & Impact Analysis:**
The `pipe->files` field tracks the number of `struct file` instances referencing
the pipe. It is used in `pipe_lock()` and `pipe_unlock()` as a boolean flag to
distinguish between normal pipes (`pipe->files > 0`) and internal per-task pipes
used by `splice_direct_to_actor()` (`pipe->files == 0`). Internal pipes do not
require locking, while normal pipes do.
In the reported scenario, a user-space program is concurrently calling
`vmsplice()` on one file descriptor and `close()` on another file descriptor
pointing to the same pipe.
- The `vmsplice()` system call holds a reference to its file descriptor (via
`fdget()`), ensuring that the file cannot be freed and `pipe_release()` cannot
be called for it. Thus, `pipe->files` is guaranteed to be at least `1` for the
duration of the `vmsplice()` call.
- The concurrent `close()` of the other file descriptor causes `put_pipe_info()`
to decrement `pipe->files` from `2` to `1`.
- `pipe_lock()` reads `pipe->files` without a lock. However, since the value is
transitioning from `2` to `1`, the read will always observe a non-zero value.
- Because the observed value is always non-zero, `pipe_lock()` will correctly
evaluate the condition as true and proceed to lock the pipe's mutex
(`mutex_lock(&pipe->mutex)`).
**Security Implications:**
Since the logic only depends on `pipe->files` being non-zero, and the file
reference held by `vmsplice()` guarantees it remains non-zero, this data race is
completely benign. The compiler will emit a single load instruction for the
read, which cannot produce a torn value of `0` on supported architectures.
This data race does not lead to any memory corruption, use-after-free, logic
errors, or denial of service. It is a known pattern in the kernel where a benign
data race is flagged by KCSAN because it lacks explicit
`READ_ONCE()`/`WRITE_ONCE()` or `data_race()` annotations.
**Conclusion:**
This is a benign data race and a false positive from a security perspective. It
is not exploitable and does not cause a denial of service. Unprivileged users
can trigger the KCSAN warning by racing `vmsplice()` and `close()`, but it has
no adverse effects on production kernels where KCSAN is disabled.]
|