| 0/0 |
2026/08/09 02:30 |
flow |
assessment-security |
1h03m
Results: map[DenialOfService:false Explanation:**Root Cause Analysis**
The bug is a race condition between hole punching (via `madvise(MADV_REMOVE)` or
`fallocate()`) and asynchronous compressed writeback in Btrfs.
When a hole is punched, `btrfs_punch_hole()` calls `btrfs_wait_ordered_range()`
to flush any pending delayed allocations (delalloc) and wait for all ordered
extents in the range to complete.
1. `btrfs_wait_ordered_range()` triggers writeback by calling
`filemap_fdatawrite_range()`.
2. For compressed extents, Btrfs does not create the ordered extent
synchronously. Instead, `run_delalloc_compressed()` queues the compression work
to a background workqueue (`delalloc_workers`) and leaves the page locked but
**without** the `PG_writeback` flag set.
3. `filemap_fdatawait_range()` is then called to wait for writeback to finish,
but since the `PG_writeback` flag is not yet set on these pages, it returns
immediately.
4. `btrfs_wait_ordered_range()` then checks for ordered extents, finds none
(because the workqueue hasn't created them yet), and returns.
5. `btrfs_punch_hole()` proceeds, locks the file range, and calls
`btrfs_assert_inode_range_clean()`.
6. Meanwhile, the background workqueue executes `submit_one_async_extent()`,
which allocates the data extent, creates the ordered extent, and finally unlocks
the page and sets `PG_writeback`.
7. `btrfs_assert_inode_range_clean()` checks for ordered extents in the locked
range and finds the newly created ordered extent, triggering the `ASSERT()` and
crashing the kernel.
**Security Impact**
This bug is **not exploitable** and does not pose a security risk in production
environments:
1. **No Memory Corruption:** The crash is strictly caused by a developer
`ASSERT()` (`CONFIG_BTRFS_ASSERT`). In standard production kernels (e.g.,
Ubuntu, Debian, Fedora), this config is disabled. Without the assert, the hole
punch and the delayed ordered extent completion will safely race to update the
b-tree. Both operations take the `io_tree` lock, ensuring they are properly
serialized. There are no use-after-free, double-free, or out-of-bounds memory
accesses.
2. **No Denial of Service:** Since the `ASSERT()` is compiled out in production
builds, the kernel will not crash or panic.
3. **Data Inconsistency:** The only consequence of this race in production is a
functional bug where a punched hole might be partially overwritten by the
delayed compressed write. Because an unprivileged user can only punch holes in
files they already have write access to, this data inconsistency is restricted
to their own files and cannot be used for privilege escalation or cross-user
data corruption.
**Trigger**
The bug can be triggered by any **unprivileged** local user by creating a Btrfs
file, enabling compression, writing data to it, and concurrently calling
`madvise(MADV_REMOVE)` or `fallocate(FALLOC_FL_PUNCH_HOLE)` on the file's
memory-mapped range. Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/08/09 02:30 |
action |
syz-repro-to-c-repro |
0m
Results:
|
| 2/1 |
2026/08/09 02:30 |
action |
kernel-checkouter |
0m
Results: map[KernelSrc:/app/workdir/cache/src/2c647d04ba64204b0de04a5458fe5db9d3049774]
|
| 3/1 |
2026/08/09 02:30 |
action |
kernel-builder |
24m
Results: map[KernelObj:/app/workdir/cache/build/6480dbf24284b37512c7db063815e531e01624d2]
|
| 4/1 |
2026/08/09 02:54 |
action |
codesearch-prepare |
10m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/08/09 03:04 |
agent |
expert |
28m
Model:
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true 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:
loop0: detected capacity change from 0 to 32768
BTRFS: device fsid 14d642db-7b15-43e4-81e6-4b8fac6a25f8 devid 1 transid 8 /dev/loop0 (7:0) scanned by syz.0.0 (5319)
BTRFS info (device loop0): first mount of filesystem 14d642db-7b15-43e4-81e6-4b8fac6a25f8
BTRFS info (device loop0): using blake2b checksum algorithm
BTRFS info (device loop0): enabling ssd optimizations
BTRFS info (device loop0): turning on async discard
BTRFS info (device loop0): enabling free space tree
BTRFS info (device loop0): use zstd compression, level 3
BTRFS error (device loop0): found unexpected ordered extent in file range [0, 6295551] for inode 263 root 5 (ordered range [131072, 262143])
assertion failed: ordered == NULL, in fs/btrfs/inode.c:10662
------------[ cut here ]------------
kernel BUG at fs/btrfs/inode.c:10662!
Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
CPU: 0 UID: 0 PID: 5319 Comm: syz.0.0 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:btrfs_assert_inode_range_clean+0x2cf/0x320 fs/btrfs/inode.c:10662
Code: fd 48 8b 3c 24 e8 11 8b 04 00 48 c7 c7 c0 27 38 8c 48 c7 c6 a0 40 38 8c 48 c7 c2 a0 28 38 8c b9 a6 29 00 00 e8 62 9c 13 fd 90 <0f> 0b 89 d9 80 e1 07 38 c1 0f 8c b8 fe ff ff be 08 00 00 00 48 89
RSP: 0018:ffffc9000f186f28 EFLAGS: 00010246
RAX: 000000000000003c RBX: 0000000000000001 RCX: 67b6815a688fe000
RDX: ffffc9000e8ca000 RSI: 00000000000057ab RDI: 00000000000057ac
RBP: 0000000000020000 R08: ffffc9000f186ca7 R09: 1ffff92001e30d94
R10: dffffc0000000000 R11: fffff52001e30d95 R12: 0000000000000000
R13: 0000000000000005 R14: dffffc0000000000 R15: 0000000000600fff
FS: 00007f0a019ce6c0(0000) GS:ffff88808c54a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f817db41000 CR3: 0000000043099000 CR4: 0000000000352ef0
Call Trace:
<TASK>
btrfs_punch_hole_lock_range+0x521/0x5b0 fs/btrfs/file.c:2291
btrfs_punch_hole fs/btrfs/file.c:2756 [inline]
btrfs_fallocate+0x17a8/0x1c30 fs/btrfs/file.c:3128
vfs_fallocate+0x65a/0x7e0 fs/open.c:338
madvise_remove mm/madvise.c:1038 [inline]
madvise_vma_behavior+0x2c07/0x4340 mm/madvise.c:1351
madvise_walk_vmas+0x576/0xb00 mm/madvise.c:1712
madvise_do_behavior+0x385/0x540 mm/madvise.c:1907
do_madvise+0x327/0x3a0 mm/madvise.c:2005
__do_sys_madvise mm/madvise.c:2014 [inline]
__se_sys_madvise mm/madvise.c:2012 [inline]
__x64_sys_madvise+0xa6/0xc0 mm/madvise.c:2012
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x174/0x580 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f0a00b9e0d9
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f0a019cdfe8 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
RAX: ffffffffffffffda RBX: 00007f0a00e25fa0 RCX: 00007f0a00b9e0d9
RDX: 0000000000000009 RSI: 0000000000600003 RDI: 0000200000000000
RBP: 00007f0a00c35024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f0a00e26038 R14: 00007f0a00e25fa0 R15: 00007ffe34d18798
</TASK>
Modules linked in:
---[ end trace 0000000000000000 ]---
RIP: 0010:btrfs_assert_inode_range_clean+0x2cf/0x320 fs/btrfs/inode.c:10662
Code: fd 48 8b 3c 24 e8 11 8b 04 00 48 c7 c7 c0 27 38 8c 48 c7 c6 a0 40 38 8c 48 c7 c2 a0 28 38 8c b9 a6 29 00 00 e8 62 9c 13 fd 90 <0f> 0b 89 d9 80 e1 07 38 c1 0f 8c b8 fe ff ff be 08 00 00 00 48 89
RSP: 0018:ffffc9000f186f28 EFLAGS: 00010246
RAX: 000000000000003c RBX: 0000000000000001 RCX: 67b6815a688fe000
RDX: ffffc9000e8ca000 RSI: 00000000000057ab RDI: 00000000000057ac
RBP: 0000000000020000 R08: ffffc9000f186ca7 R09: 1ffff92001e30d94
R10: dffffc0000000000 R11: fffff52001e30d95 R12: 0000000000000000
R13: 0000000000000005 R14: dffffc0000000000 R15: 0000000000600fff
FS: 00007f0a019ce6c0(0000) GS:ffff88808c54a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f0a019ad9a0 CR3: 0000000043099000 CR4: 0000000000352ef0
Reply: **Root Cause Analysis**
The bug is a race condition between hole punching (via `madvise(MADV_REMOVE)` or `fallocate()`) and asynchronous compressed writeback in Btrfs.
When a hole is punched, `btrfs_punch_hole()` calls `btrfs_wait_ordered_range()` to flush any pending delayed allocations (delalloc) and wait for all ordered extents in the range to complete.
1. `btrfs_wait_ordered_range()` triggers writeback by calling `filemap_fdatawrite_range()`.
2. For compressed extents, Btrfs does not create the ordered extent synchronously. Instead, `run_delalloc_compressed()` queues the compression work to a background workqueue (`delalloc_workers`) and leaves the page locked but **without** the `PG_writeback` flag set.
3. `filemap_fdatawait_range()` is then called to wait for writeback to finish, but since the `PG_writeback` flag is not yet set on these pages, it returns immediately.
4. `btrfs_wait_ordered_range()` then checks for ordered extents, finds none (because the workqueue hasn't created them yet), and returns.
5. `btrfs_punch_hole()` proceeds, locks the file range, and calls `btrfs_assert_inode_range_clean()`.
6. Meanwhile, the background workqueue executes `submit_one_async_extent()`, which allocates the data extent, creates the ordered extent, and finally unlocks the page and sets `PG_writeback`.
7. `btrfs_assert_inode_range_clean()` checks for ordered extents in the locked range and finds the newly created ordered extent, triggering the `ASSERT()` and crashing the kernel.
**Security Impact**
This bug is **not exploitable** and does not pose a security risk in production environments:
1. **No Memory Corruption:** The crash is strictly caused by a developer `ASSERT()` (`CONFIG_BTRFS_ASSERT`). In standard production kernels (e.g., Ubuntu, Debian, Fedora), this config is disabled. Without the assert, the hole punch and the delayed ordered extent completion will safely race to update the b-tree. Both operations take the `io_tree` lock, ensuring they are properly serialized. There are no use-after-free, double-free, or out-of-bounds memory accesses.
2. **No Denial of Service:** Since the `ASSERT()` is compiled out in production builds, the kernel will not crash or panic.
3. **Data Inconsistency:** The only consequence of this race in production is a functional bug where a punched hole might be partially overwritten by the delayed compressed write. Because an unprivileged user can only punch holes in files they already have write access to, this data inconsistency is restricted to their own files and cannot be used for privilege escalation or cross-user data corruption.
**Trigger**
The bug can be triggered by any **unprivileged** local user by creating a Btrfs file, enabling compression, writing data to it, and concurrently calling `madvise(MADV_REMOVE)` or `fallocate(FALLOC_FL_PUNCH_HOLE)` on the file's memory-mapped range.
|
| 6/2 |
2026/08/09 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 8650
output: 44
thoughts: 111
Thoughts: **Btrfs Kernel Bug Investigation**
Okay, so I've got a problem here β a serious one. It looks like we've got a kernel BUG, specifically in `fs/btrfs/inode.c` at line 10662. The culprit is `btrfs_assert_inode_range_clean`, and it's being triggered by a chain of function calls: `btrfs_punch_hole_lock_range` from `btrfs_punch_hole`, all initiated from `btrfs_fallocate`. The originating system call appears to be `madvise(MADV_REMOVE)`. Time to dive into `fs/btrfs/inode.c` and figure out what's going wrong with this file system.
|
| 7/2 |
2026/08/09 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_assert_inode_range_clean]
Results: map[SourceCode:10630: /*
10631: * Verify that there are no ordered extents for a given file range.
10632: *
10633: * @inode: The target inode.
10634: * @start: Start offset of the file range, should be sector size aligned.
10635: * @end: End offset (inclusive) of the file range, its value +1 should be
10636: * sector size aligned.
10637: *
10638: * This should typically be used for cases where we locked an inode's VFS lock in
10639: * exclusive mode, we have also locked the inode's i_mmap_lock in exclusive mode,
10640: * we have flushed all delalloc in the range, we have waited for all ordered
10641: * extents in the range to complete and finally we have locked the file range in
10642: * the inode's io_tree.
10643: */
10644: void btrfs_assert_inode_range_clean(struct btrfs_inode *inode, u64 start, u64 end)
10645: {
10646: struct btrfs_root *root = inode->root;
10647: struct btrfs_ordered_extent *ordered;
10648:
10649: if (!IS_ENABLED(CONFIG_BTRFS_ASSERT))
10650: return;
10651:
10652: ordered = btrfs_lookup_first_ordered_range(inode, start, end + 1 - start);
10653: if (ordered) {
10654: btrfs_err(root->fs_info,
10655: "found unexpected ordered extent in file range [%llu, %llu] for inode %llu root %llu (ordered range [%llu, %llu])",
10656: start, end, btrfs_ino(inode), btrfs_root_id(root),
10657: ordered->file_offset,
10658: ordered->file_offset + ordered->num_bytes - 1);
10659: btrfs_put_ordered_extent(ordered);
10660: }
10661:
10662: ASSERT(ordered == NULL);
10663: }
SourceFile:fs/btrfs/inode.c]
|
| 8/2 |
2026/08/09 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 9651
output: 35
thoughts: 82
Thoughts: **Debugging a Btrfs Assertion**
Okay, so I've hit an `ASSERT` in Btrfs, specifically `ASSERT(ordered == NULL)`. The trace tells me this is only enabled when `CONFIG_BTRFS_ASSERT` is set. My gut feeling is that `CONFIG_BTRFS_ASSERT` is a debugging option and probably shouldn't be enabled in production builds. I need to confirm that. Let me quickly double-check the Kconfig file for Btrfs (`fs/btrfs/Kconfig`) to confirm if my assumption is correct. That should give me the context I need to figure out how serious this is and where to start digging.
|
| 9/2 |
2026/08/09 03:04 |
tool |
read-file |
0m
Args: map[File:fs/btrfs/Kconfig FirstLine:1 LineCount:100]
Results: map[Contents: 1: # SPDX-License-Identifier: GPL-2.0
2:
3: config BTRFS_FS
4: tristate "Btrfs filesystem support"
5: select BLK_CGROUP_PUNT_BIO
6: select CRC32
7: select CRYPTO_LIB_BLAKE2B
8: select CRYPTO_LIB_SHA256
9: select ZLIB_INFLATE
10: select ZLIB_DEFLATE
11: select LZO_COMPRESS
12: select LZO_DECOMPRESS
13: select ZSTD_COMPRESS
14: select ZSTD_DECOMPRESS
15: select FS_IOMAP
16: select RAID6_PQ
17: select XOR_BLOCKS
18: select XXHASH
19: depends on PAGE_SIZE_LESS_THAN_256KB
20:
21: help
22: Btrfs is a general purpose copy-on-write filesystem with extents,
23: writable snapshotting, support for multiple devices and many more
24: features focused on fault tolerance, repair and easy administration.
25:
26: The filesystem disk format is no longer unstable, and it's not
27: expected to change unless there are strong reasons to do so. If there
28: is a format change, file systems with a unchanged format will
29: continue to be mountable and usable by newer kernels.
30:
31: For more information, please see the web pages at
32: https://btrfs.readthedocs.io
33:
34: To compile this file system support as a module, choose M here. The
35: module will be called btrfs.
36:
37: If unsure, say N.
38:
39: config BTRFS_FS_POSIX_ACL
40: bool "Btrfs POSIX Access Control Lists"
41: depends on BTRFS_FS
42: select FS_POSIX_ACL
43: help
44: POSIX Access Control Lists (ACLs) support permissions for users and
45: groups beyond the owner/group/world scheme.
46:
47: If you don't know what Access Control Lists are, say N
48:
49: config BTRFS_FS_RUN_SANITY_TESTS
50: bool "Btrfs will run sanity tests upon loading"
51: depends on BTRFS_FS
52: help
53: This will run sanity tests for core functionality like free space,
54: extent maps, extent io, extent buffers, inodes, qgroups and others,
55: at module load time. These are mostly regression tests and are only
56: interesting to developers.
57:
58: If unsure, say N.
59:
60: config BTRFS_DEBUG
61: bool "Btrfs debugging support"
62: depends on BTRFS_FS
63: select REF_TRACKER if STACKTRACE_SUPPORT
64: help
65: Enable run-time debugging support for the btrfs filesystem.
66:
67: Additional potentially expensive checks, debugging functionality or
68: sysfs exported information is enabled, like leak checks of internal
69: objects, optional forced space fragmentation and /sys/fs/btrfs/debug .
70: This has negative impact on performance.
71:
72: If unsure, say N.
73:
74: config BTRFS_ASSERT
75: bool "Btrfs assert support"
76: depends on BTRFS_FS
77: help
78: Enable run-time assertion checking. Additional safety checks are
79: done, simple enough not to affect performance but verify invariants
80: and assumptions of code to run properly. This may result in panics,
81: and is meant for developers but can be enabled in general.
82:
83: If unsure, say N.
84:
85: config BTRFS_EXPERIMENTAL
86: bool "Btrfs experimental features"
87: depends on BTRFS_FS
88: default n
89: help
90: Enable experimental features. These features may not be stable enough
91: for end users. This is meant for btrfs developers or users who wish
92: to test the functionality and report problems.
93:
94: Current list:
95:
96: - RAID mirror read policy - additional read policies for balancing
97: reading from redundant block group
98: profiles (currently: pid, round-robin,
99: fixed devid)
100:
]
|
| 10/2 |
2026/08/09 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 11258
output: 44
thoughts: 103
Thoughts: **Analyzing the Implications of Disabling `CONFIG_BTRFS_ASSERT`**
Okay, so I'm looking at this Btrfs code, specifically around `CONFIG_BTRFS_ASSERT`. It's a debugging option, not something you'd typically leave enabled in production unless you're specifically troubleshooting. The description even advises saying "N" if you're unsure, which is generally a good practice for features like this.
My immediate question is: what's the consequence of having `CONFIG_BTRFS_ASSERT` disabled? The code tells me that if it's not enabled, `btrfs_assert_inode_range_clean` simply returns. Basically, it's a no-op when this assert is disabled. It looks like it's a safety net, a sanity check that's bypassed in a standard build.
Now, the interesting bit. The call stack reveals that `btrfs_assert_inode_range_clean` gets called from `btrfs_punch_hole_lock_range`. So, when the assert is disabled, whatever logic that depends on `btrfs_assert_inode_range_clean` becomes unmonitored. That's where I need to focus: I need to consider what the assertion might be checking, and what impact might occur if the check is skipped. This requires a deeper dive into the context of `btrfs_punch_hole_lock_range`. It looks like I need to assess how that affects data integrity, performance, or potential corruption scenarios. It's a standard pattern: debug code adds overhead, and you want to be certain you are removing it cleanly.
|
| 11/2 |
2026/08/09 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/file.c Name:btrfs_punch_hole_lock_range]
Results: map[SourceCode:2265: static void btrfs_punch_hole_lock_range(struct inode *inode,
2266: const u64 lockstart, const u64 lockend,
2267: struct extent_state **cached_state)
2268: {
2269: while (1) {
2270: truncate_pagecache_range(inode, lockstart, lockend);
2271:
2272: btrfs_lock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
2273: cached_state);
2274: /*
2275: * We can't have ordered extents in the range, nor dirty/writeback
2276: * pages, because we have locked the inode's VFS lock in exclusive
2277: * mode, we have locked the inode's i_mmap_lock in exclusive mode,
2278: * we have flushed all delalloc in the range and we have waited
2279: * for any ordered extents in the range to complete.
2280: * We can race with anyone reading pages from this range, so after
2281: * locking the range check if we have pages in the range, and if
2282: * we do, unlock the range and retry.
2283: */
2284: if (!check_range_has_page(inode, lockstart, lockend))
2285: break;
2286:
2287: btrfs_unlock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
2288: cached_state);
2289: }
2290:
2291: btrfs_assert_inode_range_clean(BTRFS_I(inode), lockstart, lockend);
2292: }
SourceFile:fs/btrfs/file.c]
|
| 12/2 |
2026/08/09 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 12279
output: 40
thoughts: 272
Thoughts: **My Analysis of Punching Holes in Btrfs**
Okay, so this comment highlights a crucial safety invariant when punching holes in a Btrfs filesystem. It boils down to this: we *cannot* proceed with dropping extents in a given range if certain conditions exist, specifically ordered extents, dirty pages requiring writeback, or the presence of these pages within our target range. The reasoning is clear: we've already secured exclusive locks on the inode's VFS lock and `i_mmap_lock`, flushed delalloc, and waited for ordered extents to complete, aiming for a consistent state. However, we're still vulnerable to a race condition with concurrent reads.
The core of the issue is data integrity. If an ordered extent is still being written when we drop the associated extents during a `btrfs_punch_hole` operation, we risk dropping data that's still in flight. This could manifest in several ways: dropping in-flight extents prematurely, corrupting the extent tree itself, or even causing a use-after-free scenario if the ordered extent attempts to modify the extent tree *after* we've deleted what it was trying to write.
Essentially, `btrfs_punch_hole` drops extents. If an ordered extent is concurrently being written to the range, we could end up with extents inserted into the tree after the `punch_hole` function call, or modification of the tree after our modifications, leading to corruption of data.
My immediate next step is to carefully examine the `btrfs_punch_hole` implementation itself. I need to understand *precisely* how it handles this scenario. It's vital to see how it either avoids this race or mitigates the risk. Specifically, I'll be looking for the steps taken to ensure that no such races exist.
|
| 13/2 |
2026/08/09 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/file.c Name:btrfs_punch_hole]
Results: map[SourceCode:2644: static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len)
2645: {
2646: struct inode *inode = file_inode(file);
2647: struct btrfs_fs_info *fs_info = inode_to_fs_info(inode);
2648: struct btrfs_root *root = BTRFS_I(inode)->root;
2649: struct extent_state *cached_state = NULL;
2650: struct btrfs_path *path;
2651: struct btrfs_trans_handle *trans = NULL;
2652: u64 lockstart;
2653: u64 lockend;
2654: u64 tail_start;
2655: u64 tail_len;
2656: const u64 orig_start = offset;
2657: const u64 orig_end = offset + len - 1;
2658: int ret = 0;
2659: bool same_block;
2660: u64 ino_size;
2661: bool truncated_block = false;
2662: bool updated_inode = false;
2663:
2664: btrfs_inode_lock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2665:
2666: ret = btrfs_wait_ordered_range(BTRFS_I(inode), offset, len);
2667: if (ret)
2668: goto out_only_mutex;
2669:
2670: ino_size = round_up(inode->i_size, fs_info->sectorsize);
2671: ret = find_first_non_hole(BTRFS_I(inode), &offset, &len);
2672: if (ret < 0)
2673: goto out_only_mutex;
2674: if (ret && !len) {
2675: /* Already in a large hole */
2676: ret = 0;
2677: goto out_only_mutex;
2678: }
2679:
2680: ret = file_modified(file);
2681: if (ret)
2682: goto out_only_mutex;
2683:
2684: lockstart = round_up(offset, fs_info->sectorsize);
2685: lockend = round_down(offset + len, fs_info->sectorsize) - 1;
2686: same_block = (BTRFS_BYTES_TO_BLKS(fs_info, offset))
2687: == (BTRFS_BYTES_TO_BLKS(fs_info, offset + len - 1));
2688: /*
2689: * Only do this if we are in the same block and we aren't doing the
2690: * entire block.
2691: */
2692: if (same_block && len < fs_info->sectorsize) {
2693: if (offset < ino_size) {
2694: truncated_block = true;
2695: ret = btrfs_truncate_block(BTRFS_I(inode), offset + len - 1,
2696: orig_start, orig_end);
2697: } else {
2698: ret = 0;
2699: }
2700: goto out_only_mutex;
2701: }
2702:
2703: /* zero back part of the first block */
2704: if (offset < ino_size) {
2705: truncated_block = true;
2706: ret = btrfs_truncate_block(BTRFS_I(inode), offset, orig_start, orig_end);
2707: if (ret) {
2708: btrfs_inode_unlock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2709: return ret;
2710: }
2711: }
2712:
2713: /* Check the aligned pages after the first unaligned page,
2714: * if offset != orig_start, which means the first unaligned page
2715: * including several following pages are already in holes,
2716: * the extra check can be skipped */
2717: if (offset == orig_start) {
2718: /* after truncate page, check hole again */
2719: len = offset + len - lockstart;
2720: offset = lockstart;
2721: ret = find_first_non_hole(BTRFS_I(inode), &offset, &len);
2722: if (ret < 0)
2723: goto out_only_mutex;
2724: if (ret && !len) {
2725: ret = 0;
2726: goto out_only_mutex;
2727: }
2728: lockstart = offset;
2729: }
2730:
2731: /* Check the tail unaligned part is in a hole */
2732: tail_start = lockend + 1;
2733: tail_len = offset + len - tail_start;
2734: if (tail_len) {
2735: ret = find_first_non_hole(BTRFS_I(inode), &tail_start, &tail_len);
2736: if (unlikely(ret < 0))
2737: goto out_only_mutex;
2738: if (!ret) {
2739: /* zero the front end of the last page */
2740: if (tail_start + tail_len < ino_size) {
2741: truncated_block = true;
2742: ret = btrfs_truncate_block(BTRFS_I(inode),
2743: tail_start + tail_len - 1,
2744: orig_start, orig_end);
2745: if (ret)
2746: goto out_only_mutex;
2747: }
2748: }
2749: }
2750:
2751: if (lockend < lockstart) {
2752: ret = 0;
2753: goto out_only_mutex;
2754: }
2755:
2756: btrfs_punch_hole_lock_range(inode, lockstart, lockend, &cached_state);
2757:
2758: path = btrfs_alloc_path();
2759: if (!path) {
2760: ret = -ENOMEM;
2761: goto out;
2762: }
2763:
2764: ret = btrfs_replace_file_extents(BTRFS_I(inode), path, lockstart,
2765: lockend, NULL, &trans);
2766: btrfs_free_path(path);
2767: if (ret)
2768: goto out;
2769:
2770: ASSERT(trans != NULL);
2771: inode_inc_iversion(inode);
2772: inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
2773: ret = btrfs_update_inode(trans, BTRFS_I(inode));
2774: updated_inode = true;
2775: btrfs_end_transaction(trans);
2776: btrfs_btree_balance_dirty(fs_info);
2777: out:
2778: btrfs_unlock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
2779: &cached_state);
2780: out_only_mutex:
2781: if (!updated_inode && truncated_block && !ret) {
2782: /*
2783: * If we only end up zeroing part of a page, we still need to
2784: * update the inode item, so that all the time fields are
2785: * updated as well as the necessary btrfs inode in memory fields
2786: * for detecting, at fsync time, if the inode isn't yet in the
2787: * log tree or it's there but not up to date.
2788: */
2789: struct timespec64 now = inode_set_ctime_current(inode);
2790:
2791: inode_inc_iversion(inode);
2792: inode_set_mtime_to_ts(inode, now);
2793: trans = btrfs_start_transaction(root, 1);
2794: if (IS_ERR(trans)) {
2795: ret = PTR_ERR(trans);
2796: } else {
2797: int ret2;
2798:
2799: ret = btrfs_update_inode(trans, BTRFS_I(inode));
2800: ret2 = btrfs_end_transaction(trans);
2801: if (!ret)
2802: ret = ret2;
2803: }
2804: }
2805: btrfs_inode_unlock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2806: return ret;
2807: }
SourceFile:fs/btrfs/file.c]
|
| 14/2 |
2026/08/09 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 15611
output: 33
thoughts: 322
Thoughts: **Investigating a Potential Ordering Issue in `btrfs_punch_hole`**
Okay, so I'm digging into `btrfs_punch_hole` and something doesn't quite sit right with me. I see that after locking the inode, the function immediately calls `btrfs_wait_ordered_range` for the given `offset` and `len`. Seems straightforward enough: it's waiting for any ordered extents within that range to complete before proceeding. Then, it moves on to some checks, and eventually calls `btrfs_punch_hole_lock_range` with `lockstart` and `lockend`.
But here's the kicker: I'm noticing that the `lockstart` and `lockend` variables are calculated *after* the call to `btrfs_wait_ordered_range`. And the calculation is: `lockstart` is the offset rounded up to the sector size, and `lockend` is `offset + len - 1` rounded down to the sector size. If `offset` isn't sector aligned, which is often the case, `lockstart` gets rounded up. But that means `btrfs_wait_ordered_range` is waiting for a range that extends from the provided `offset` to `offset + len - 1`. And then `lockstart` and `lockend` are within that very range.
So, logically, `btrfs_wait_ordered_range` *should* be covering the ordered extents in the sub-range that we are going to operate on via `btrfs_punch_hole_lock_range`. Why, then, would there be any ordered extents *left* that need to be dealt with? There seems to be a potential race or incorrect ordering here. I'm wondering if a subsequent commit modified this behavior.
Time to check the commit logs.
|
| 15/2 |
2026/08/09 03:05 |
tool |
git-log |
10m
Error:
Args: map[CodeRegexp:btrfs_assert_inode_range_clean Count:10]
Results:
|
| 16/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 16402
output: 48
thoughts: 20
Thoughts: Okay, here's my interpretation of that thought process, tailored for an expert audience, and presented as if it were my own internal monologue:
**Diving into the Btrfs Inode Code**
Alright, time to get to the core of the matter. I need to focus on a very specific area within the Btrfs filesystem code. The goal here is to zero in on the inode implementation. Given the context of what I'm working on, I'm going to start my investigation with the `fs/btrfs/inode.c` file. The use of `PathPrefix` is a common and quick way to navigate to the precise file. This will give me a base to understand its behavior and how its interacting with the rest of the file system. Let's see what's in there.
|
| 17/2 |
2026/08/09 03:15 |
tool |
git-log |
0m
Args: map[CodeRegexp:btrfs_assert_inode_range_clean Count:10 PathPrefix:fs/btrfs/inode.c]
Results: map[Output:63c34cb4c6dd btrfs: add and use helper to assert an inode range is clean
]
|
| 18/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 16680
output: 25
thoughts: 18
Thoughts: Okay, let's dive into this. Here's my perspective on that commit:
**Analyzing Commit 63c34cb4c6dd**
Alright, time to get my head around this commit, `63c34cb4c6dd`. Let's see what's changed and what's motivating this. I need to understand the intent behind this, the potential impact, and whether it aligns with the overall project goals. First, I'll need to use my usual tools to diff the changes, really dig into the code, and see if there are any subtle modifications I should be looking for. I am also going to think about what the relevant unit tests say, and think about edge cases. I hope that the commit message provides useful context to get me started. From there, I'll be assessing the approach; is this an elegant solution, or a quick fix? I'll probably also need to consider potential areas for refactoring or further optimization, depending on what I find. After that, I will also consider the upstream branch, and whether this changes anything at the merge level. It's time to put on my detective hat and start investigating!
|
| 19/2 |
2026/08/09 03:15 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 63c34cb4c6dddd7899a14ed0a11b208a41e9c85c
Author: Filipe Manana <fdmanana@suse.com>
Date: Tue Mar 15 15:22:41 2022 +0000
btrfs: add and use helper to assert an inode range is clean
We have four different scenarios where we don't expect to find ordered
extents after locking a file range:
1) During plain fallocate;
2) During hole punching;
3) During zero range;
4) During reflinks (both cloning and deduplication).
This is because in all these cases we follow the pattern:
1) Lock the inode's VFS lock in exclusive mode;
2) Lock the inode's i_mmap_lock in exclusive node, to serialize with
mmap writes;
3) Flush delalloc in a file range and wait for all ordered extents
to complete - both done through btrfs_wait_ordered_range();
4) Lock the file range in the inode's io_tree.
So add a helper that asserts that we don't have ordered extents for a
given range. Make the four scenarios listed above use this helper after
locking the respective file range.
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h
index 9addfd5cdf4e..e43e7853f389 100644
--- a/fs/btrfs/ctree.h
+++ b/fs/btrfs/ctree.h
@@ -3375,6 +3375,7 @@ void btrfs_inode_unlock(struct inode *inode, unsigned int ilock_flags);
void btrfs_update_inode_bytes(struct btrfs_inode *inode,
const u64 add_bytes,
const u64 del_bytes);
+void btrfs_assert_inode_range_clean(struct btrfs_inode *inode, u64 start, u64 end);
/* ioctl.c */
long btrfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c
index fd90e25382b2..bd329316945f 100644
--- a/fs/btrfs/file.c
+++ b/fs/btrfs/file.c
@@ -2608,6 +2608,8 @@ static void btrfs_punch_hole_lock_range(struct inode *inode,
unlock_extent_cached(&BTRFS_I(inode)->io_tree, lockstart,
lockend, cached_state);
}
+
+ btrfs_assert_inode_range_clean(BTRFS_I(inode), lockstart, lockend);
}
static int btrfs_insert_replace_extent(struct btrfs_trans_handle *trans,
@@ -3479,6 +3481,8 @@ static long btrfs_fallocate(struct file *file, int mode,
lock_extent_bits(&BTRFS_I(inode)->io_tree, alloc_start, locked_end,
&cached_state);
+ btrfs_assert_inode_range_clean(BTRFS_I(inode), alloc_start, locked_end);
+
/* First, check if we exceed the qgroup limit */
INIT_LIST_HEAD(&reserve_list);
while (cur_offset < alloc_end) {
diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c
index 31ccb82157c2..c4b68be7edd7 100644
--- a/fs/btrfs/inode.c
+++ b/fs/btrfs/inode.c
@@ -11261,6 +11261,41 @@ void btrfs_update_inode_bytes(struct btrfs_inode *inode,
spin_unlock(&inode->lock);
}
+/**
+ * Verify that there are no ordered extents for a given file range.
+ *
+ * @inode: The target inode.
+ * @start: Start offset of the file range, should be sector size aligned.
+ * @end: End offset (inclusive) of the file range, its value +1 should be
+ * sector size aligned.
+ *
+ * This should typically be used for cases where we locked an inode's VFS lock in
+ * exclusive mode, we have also locked the inode's i_mmap_lock in exclusive mode,
+ * we have flushed all delalloc in the range, we have waited for all ordered
+ * extents in the range to complete and finally we have locked the file range in
+ * the inode's io_tree.
+ */
+void btrfs_assert_inode_range_clean(struct btrfs_inode *inode, u64 start, u64 end)
+{
+ struct btrfs_root *root = inode->root;
+ struct btrfs_ordered_extent *ordered;
+
+ if (!IS_ENABLED(CONFIG_BTRFS_ASSERT))
+ return;
+
+ ordered = btrfs_lookup_first_ordered_range(inode, start, end + 1 - start);
+ if (ordered) {
+ btrfs_err(root->fs_info,
+"found unexpected ordered extent in file range [%llu, %llu] for inode %llu root %llu (ordered range [%llu, %llu])",
+ start, end, btrfs_ino(inode), root->root_key.objectid,
+ ordered->file_offset,
+ ordered->file_offset + ordered->num_bytes - 1);
+ btrfs_put_ordered_extent(ordered);
+ }
+
+ ASSERT(ordered == NULL);
+}
+
static const struct inode_operations btrfs_dir_inode_operations = {
.getattr = btrfs_getattr,
.lookup = btrfs_lookup,
diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c
index c257bf037cf2..c39f8b3a5a4a 100644
--- a/fs/btrfs/reflink.c
+++ b/fs/btrfs/reflink.c
@@ -614,14 +614,23 @@ static void btrfs_double_extent_unlock(struct inode *inode1, u64 loff1,
static void btrfs_double_extent_lock(struct inode *inode1, u64 loff1,
struct inode *inode2, u64 loff2, u64 len)
{
+ u64 range1_end = loff1 + len - 1;
+ u64 range2_end = loff2 + len - 1;
+
if (inode1 < inode2) {
swap(inode1, inode2);
swap(loff1, loff2);
+ swap(range1_end, range2_end);
} else if (inode1 == inode2 && loff2 < loff1) {
swap(loff1, loff2);
+ swap(range1_end, range2_end);
}
- lock_extent(&BTRFS_I(inode1)->io_tree, loff1, loff1 + len - 1);
- lock_extent(&BTRFS_I(inode2)->io_tree, loff2, loff2 + len - 1);
+
+ lock_extent(&BTRFS_I(inode1)->io_tree, loff1, range1_end);
+ lock_extent(&BTRFS_I(inode2)->io_tree, loff2, range2_end);
+
+ btrfs_assert_inode_range_clean(BTRFS_I(inode1), loff1, range1_end);
+ btrfs_assert_inode_range_clean(BTRFS_I(inode2), loff2, range2_end);
}
static void btrfs_double_mmap_lock(struct inode *inode1, struct inode *inode2)
]
|
| 20/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 19143
output: 44
thoughts: 113
Thoughts: **Analyzing the Invariant and Hole Punching**
Okay, so I'm looking at this assertion. Essentially, it's just a check to ensure the invariant is valid. If this invariant fails, then that implies we have an ordered extent lurking within the specified range. That's the crux of the issue. Now, the scenario is, we're considering punching a hole when this situation occurs. I need to understand the implications of this.
If the invariant doesn't hold, and we attempt to punch a hole, my current understanding is the hole-punching code will effectively be dropping extents. This will trigger the completion of the ordered extent, which is the key here. Now, when an ordered extent completes, that should invoke `btrfs_finish_ordered_io`. Okay, so `btrfs_finish_ordered_io` is where the core logic will reside. Time to delve into the code for that function and understand what happens when it's called. This function is where the consequences of the hole punch with an un-validated invariant are realized.
|
| 21/2 |
2026/08/09 03:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:btrfs_finish_ordered_io]
Results: map[SourceCode:3435: int btrfs_finish_ordered_io(struct btrfs_ordered_extent *ordered)
3436: {
3437: if (btrfs_is_zoned(ordered->inode->root->fs_info) &&
3438: !test_bit(BTRFS_ORDERED_IOERR, &ordered->flags) &&
3439: list_empty(&ordered->bioc_list))
3440: btrfs_finish_ordered_zoned(ordered);
3441: return btrfs_finish_one_ordered(ordered);
3442: }
SourceFile:fs/btrfs/inode.c]
|
| 22/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 19717
output: 42
thoughts: 15
Thoughts: Okay, let's dive into this. Here's my thought process on this:
**Analyzing `btrfs_finish_one_ordered`**
Alright, time to get to it. Let's start with `btrfs_finish_one_ordered`. That's the function we're focusing on right now. I need to refresh my understanding of how this specific piece of the Btrfs puzzle fits into the bigger picture. I know it's related to ordered extent processing, but I need to make sure I recall the details correctly to get a good grip on its function. Let's see how well I remember the intricacies of Btrfs's ordered data flushing. Is it handling the finalization steps or more granular work within the flush process? It should be, in principle, concerned with finalizing a single ordered extent operation. I'm wondering what contexts call this function, are they synchronous or asynchronous? Okay, let's get down to the details.
|
| 23/2 |
2026/08/09 03:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_finish_one_ordered]
Results: map[SourceCode:3177: /*
3178: * As ordered data IO finishes, this gets called so we can finish
3179: * an ordered extent if the range of bytes in the file it covers are
3180: * fully written.
3181: */
3182: int btrfs_finish_one_ordered(struct btrfs_ordered_extent *ordered_extent)
3183: {
3184: struct btrfs_inode *inode = ordered_extent->inode;
3185: struct btrfs_root *root = inode->root;
3186: struct btrfs_fs_info *fs_info = root->fs_info;
3187: struct btrfs_trans_handle *trans = NULL;
3188: struct extent_io_tree *io_tree = &inode->io_tree;
3189: struct extent_state *cached_state = NULL;
3190: u64 start, end;
3191: int compress_type = 0;
3192: int ret = 0;
3193: u64 logical_len = ordered_extent->num_bytes;
3194: bool freespace_inode;
3195: bool truncated = false;
3196: bool clear_reserved_extent = true;
3197: unsigned int clear_bits = 0;
3198:
3199: start = ordered_extent->file_offset;
3200: end = start + ordered_extent->num_bytes - 1;
3201:
3202: if (!test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags) &&
3203: !test_bit(BTRFS_ORDERED_PREALLOC, &ordered_extent->flags) &&
3204: !test_bit(BTRFS_ORDERED_DIRECT, &ordered_extent->flags) &&
3205: !test_bit(BTRFS_ORDERED_ENCODED, &ordered_extent->flags))
3206: clear_bits |= EXTENT_DELALLOC_NEW;
3207:
3208: if (!test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags))
3209: clear_bits |= EXTENT_DEFRAG;
3210:
3211: freespace_inode = btrfs_is_free_space_inode(inode);
3212: if (!freespace_inode)
3213: btrfs_lockdep_acquire(fs_info, btrfs_ordered_extent);
3214:
3215: if (unlikely(test_bit(BTRFS_ORDERED_IOERR, &ordered_extent->flags))) {
3216: ret = -EIO;
3217: goto out;
3218: }
3219:
3220: ret = btrfs_zone_finish_endio(fs_info, ordered_extent->disk_bytenr,
3221: ordered_extent->disk_num_bytes);
3222: if (ret)
3223: goto out;
3224:
3225: if (test_bit(BTRFS_ORDERED_TRUNCATED, &ordered_extent->flags)) {
3226: truncated = true;
3227: logical_len = ordered_extent->truncated_len;
3228: /* Truncated the entire extent, don't bother adding */
3229: if (!logical_len)
3230: goto out;
3231: }
3232:
3233: /*
3234: * If it's a COW write we need to lock the extent range as we will be
3235: * inserting/replacing file extent items and unpinning an extent map.
3236: * This must be taken before joining a transaction, as it's a higher
3237: * level lock (like the inode's VFS lock), otherwise we can run into an
3238: * ABBA deadlock with other tasks (transactions work like a lock,
3239: * depending on their current state).
3240: */
3241: if (!test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags)) {
3242: clear_bits |= EXTENT_LOCKED | EXTENT_FINISHING_ORDERED;
3243: btrfs_lock_extent_bits(io_tree, start, end,
3244: EXTENT_LOCKED | EXTENT_FINISHING_ORDERED,
3245: &cached_state);
3246: }
3247:
3248: if (freespace_inode)
3249: trans = btrfs_join_transaction_spacecache(root);
3250: else
3251: trans = btrfs_join_transaction(root);
3252: if (IS_ERR(trans)) {
3253: ret = PTR_ERR(trans);
3254: trans = NULL;
3255: goto out;
3256: }
3257:
3258: trans->block_rsv = &inode->block_rsv;
3259:
3260: ret = btrfs_insert_raid_extent(trans, ordered_extent);
3261: if (unlikely(ret)) {
3262: btrfs_abort_transaction(trans, ret);
3263: goto out;
3264: }
3265:
3266: if (test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags)) {
3267: /* Logic error */
3268: ASSERT(list_empty(&ordered_extent->csum_list));
3269: if (unlikely(!list_empty(&ordered_extent->csum_list))) {
3270: ret = -EINVAL;
3271: btrfs_abort_transaction(trans, ret);
3272: goto out;
3273: }
3274:
3275: btrfs_inode_safe_disk_i_size_write(inode, 0);
3276: ret = btrfs_update_inode_fallback(trans, inode);
3277: if (unlikely(ret)) {
3278: /* -ENOMEM or corruption */
3279: btrfs_abort_transaction(trans, ret);
3280: }
3281: goto out;
3282: }
3283:
3284: if (test_bit(BTRFS_ORDERED_COMPRESSED, &ordered_extent->flags))
3285: compress_type = ordered_extent->compress_type;
3286: if (test_bit(BTRFS_ORDERED_PREALLOC, &ordered_extent->flags)) {
3287: BUG_ON(compress_type);
3288: ret = btrfs_mark_extent_written(trans, inode,
3289: ordered_extent->file_offset,
3290: ordered_extent->file_offset +
3291: logical_len);
3292: btrfs_zoned_release_data_reloc_bg(fs_info, ordered_extent->disk_bytenr,
3293: ordered_extent->disk_num_bytes);
3294: if (unlikely(ret < 0)) {
3295: btrfs_abort_transaction(trans, ret);
3296: goto out;
3297: }
3298: } else {
3299: BUG_ON(root == fs_info->tree_root);
3300: ret = insert_ordered_extent_file_extent(trans, ordered_extent);
3301: if (unlikely(ret < 0)) {
3302: btrfs_abort_transaction(trans, ret);
3303: goto out;
3304: }
3305: clear_reserved_extent = false;
3306: btrfs_release_delalloc_bytes(fs_info,
3307: ordered_extent->disk_bytenr,
3308: ordered_extent->disk_num_bytes);
3309: }
3310:
3311: ret = btrfs_unpin_extent_cache(inode, ordered_extent->file_offset,
3312: ordered_extent->num_bytes, trans->transid);
3313: if (unlikely(ret < 0)) {
3314: btrfs_abort_transaction(trans, ret);
3315: goto out;
3316: }
3317:
3318: ret = add_pending_csums(trans, ordered_extent);
3319: if (unlikely(ret)) {
3320: btrfs_abort_transaction(trans, ret);
3321: goto out;
3322: }
3323:
3324: /*
3325: * If this is a new delalloc range, clear its new delalloc flag to
3326: * update the inode's number of bytes. This needs to be done first
3327: * before updating the inode item.
3328: */
3329: if ((clear_bits & EXTENT_DELALLOC_NEW) &&
3330: !test_bit(BTRFS_ORDERED_TRUNCATED, &ordered_extent->flags))
3331: btrfs_clear_extent_bit(&inode->io_tree, start, end,
3332: EXTENT_DELALLOC_NEW | EXTENT_ADD_INODE_BYTES,
3333: &cached_state);
3334:
3335: btrfs_inode_safe_disk_i_size_write(inode, 0);
3336: ret = btrfs_update_inode_fallback(trans, inode);
3337: if (unlikely(ret)) { /* -ENOMEM or corruption */
3338: btrfs_abort_transaction(trans, ret);
3339: goto out;
3340: }
3341: out:
3342: if (clear_bits)
3343: btrfs_clear_extent_bit(&inode->io_tree, start, end, clear_bits,
3344: &cached_state);
3345:
3346: if (trans)
3347: btrfs_end_transaction(trans);
3348:
3349: if (ret || truncated) {
3350: /*
3351: * If we failed to finish this ordered extent for any reason we
3352: * need to make sure BTRFS_ORDERED_IOERR is set on the ordered
3353: * extent, and mark the inode with the error if it wasn't
3354: * already set. Any error during writeback would have already
3355: * set the mapping error, so we need to set it if we're the ones
3356: * marking this ordered extent as failed.
3357: */
3358: if (ret)
3359: btrfs_mark_ordered_extent_error(ordered_extent);
3360:
3361: /*
3362: * Drop extent maps for the part of the extent we didn't write.
3363: *
3364: * We have an exception here for the free_space_inode, this is
3365: * because when we do btrfs_get_extent() on the free space inode
3366: * we will search the commit root. If this is a new block group
3367: * we won't find anything, and we will trip over the assert in
3368: * writepage where we do ASSERT(em->block_start !=
3369: * EXTENT_MAP_HOLE).
3370: *
3371: * Theoretically we could also skip this for any NOCOW extent as
3372: * we don't mess with the extent map tree in the NOCOW case, but
3373: * for now simply skip this if we are the free space inode.
3374: */
3375: if (!btrfs_is_free_space_inode(inode)) {
3376: u64 unwritten_start = start;
3377:
3378: if (truncated)
3379: unwritten_start += logical_len;
3380:
3381: btrfs_drop_extent_map_range(inode, unwritten_start,
3382: end, false);
3383: }
3384:
3385: /*
3386: * If the ordered extent had an IOERR or something else went
3387: * wrong we need to return the space for this ordered extent
3388: * back to the allocator. We only free the extent in the
3389: * truncated case if we didn't write out the extent at all.
3390: *
3391: * If we made it past insert_reserved_file_extent before we
3392: * errored out then we don't need to do this as the accounting
3393: * has already been done.
3394: */
3395: if ((ret || !logical_len) &&
3396: clear_reserved_extent &&
3397: !test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags) &&
3398: !test_bit(BTRFS_ORDERED_PREALLOC, &ordered_extent->flags)) {
3399: /*
3400: * Discard the range before returning it back to the
3401: * free space pool
3402: */
3403: if (ret && btrfs_test_opt(fs_info, DISCARD_SYNC))
3404: btrfs_discard_extent(fs_info,
3405: ordered_extent->disk_bytenr,
3406: ordered_extent->disk_num_bytes,
3407: NULL, true);
3408: btrfs_free_reserved_extent(fs_info,
3409: ordered_extent->disk_bytenr,
3410: ordered_extent->disk_num_bytes, true);
3411: /*
3412: * Actually free the qgroup rsv which was released when
3413: * the ordered extent was created.
3414: */
3415: btrfs_qgroup_free_refroot(fs_info, btrfs_root_id(inode->root),
3416: ordered_extent->qgroup_rsv,
3417: BTRFS_QGROUP_RSV_DATA);
3418: }
3419: }
3420:
3421: /*
3422: * This needs to be done to make sure anybody waiting knows we are done
3423: * updating everything for this ordered extent.
3424: */
3425: btrfs_remove_ordered_extent(ordered_extent);
3426:
3427: /* once for us */
3428: btrfs_put_ordered_extent(ordered_extent);
3429: /* once for the tree */
3430: btrfs_put_ordered_extent(ordered_extent);
3431:
3432: return ret;
3433: }
SourceFile:fs/btrfs/inode.c]
|
| 24/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 24227
output: 24
thoughts: 751
Thoughts: **Analyzing a Potential Data Corruption Scenario in Btrfs**
Okay, so here's what's running through my head. If `btrfs_finish_one_ordered` finishes its work and inserts an extent into the file extent tree, but `btrfs_punch_hole` has *already* removed that range, we've got a problem. `btrfs_punch_hole` removes extents, and if an ordered extent completes *after* that, it could essentially undelete the data or create an overlap, or at the very least, leave a "hole" that isn't really a hole. That's data corruption right there.
I was initially concerned this could lead to memory corruption, like a race condition on the B-tree modifications, if `btrfs_finish_one_ordered` and `btrfs_punch_hole` hit the B-tree at the same time. I know `btrfs_punch_hole` holds the inode's VFS lock and `i_mmap_lock`. But `btrfs_finish_one_ordered` runs in an endio worker thread, which doesn't hold those locks.
Ah, but then I remember the `io_tree`. `btrfs_finish_one_ordered` locks the extent range in the `io_tree` before inserting:
```c
if (!test_bit(BTRFS_ORDERED_NOCOW, &ordered_extent->flags)) {
clear_bits |= EXTENT_LOCKED | EXTENT_FINISHING_ORDERED;
btrfs_lock_extent_bits(io_tree, start, end,
EXTENT_LOCKED | EXTENT_FINISHING_ORDERED,
&cached_state);
}
```
And `btrfs_punch_hole` also does the same:
```c
btrfs_lock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
cached_state);
```
So, they *are* serialized by the `io_tree` lock. If `btrfs_punch_hole` has the lock, `btrfs_finish_one_ordered` will block. So the extent insertion always *happens after* the hole punch.
So, I think it's more of a logical bug (data corruption) rather than memory corruption directly, but it *is* an issue.
Now, wait, if `btrfs_punch_hole` drops the extent map and then `btrfs_finish_one_ordered` inserts the extent, we could have an extent in the B-tree but *not* an extent map. Or vice versa. Then, I remember `btrfs_finish_one_ordered` calls `insert_ordered_extent_file_extent`, which adds a *file extent item*, not an extent map. If the file extent item is added where a hole is, well, it's just a file extent.
Okay, let me review my thoughts, does that make it no memory corruption? I think so.
Time to see if there are any CVEs or exploits on this. And time to start looking for the commit that fixes this.
|
| 25/2 |
2026/08/09 03:15 |
tool |
git-log |
0m
Args: map[MessageRegexps:[found unexpected ordered extent in file range]]
Results:
|
| 26/2 |
2026/08/09 03:15 |
llm |
expert |
0m
Model:
Tokens:
input: 25724
output: 33
thoughts: 22
Thoughts: Here's my thought process, summarized as you requested:
**Investigating a Potential Btrfs Issue**
Okay, so I've got a hunch, or maybe just a vague recollection, about a possible issue within the Btrfs filesystem code. The specific function name that's come to mind is "btrfs_assert_inode_range_clean". I need to find out more about where this function is used and when, so I'm going to head over to the Git log. My goal is to see its history. I want to search the Git log for any instances of that function name. This should help me pinpoint recent commits or changes related to it. This will provide some context and potential clues about the bug or behavior I'm interested in investigating.
|
| 27/2 |
2026/08/09 03:16 |
tool |
git-log |
10m
Error:
Args: map[CodeRegexp:btrfs_assert_inode_range_clean Count:50]
Results:
|
| 28/2 |
2026/08/09 03:26 |
llm |
expert |
0m
Model:
Tokens:
input: 25976
output: 48
thoughts: 16
Thoughts: Okay, here's my attempt at summarizing the thought process, as if it were my own, assuming I'm an expert in file systems and kernel internals.
**Diving into the Btrfs File Implementation**
Alright, I need to get to the heart of how Btrfs handles files. The core file operations are always a good place to start digging when I'm chasing a bug or just trying to understand something better. So, my initial thought is, let's drill down into the likely implementation details. The file handling code is usually located in a dedicated area. Considering I am diving into the guts of the kernel, `fs/btrfs/` feels like the likely place for the Btrfs specific code, and given that I am looking for something file-oriented, `file.c` seems like the most obvious candidate for the file-handling aspects. Let's see what's in there. I'm expecting to find everything from open and close implementations, read/write handling, inode operations, and maybe even some of the more complex caching and allocation strategies related to file data. This is where the magic happens, or where the problems often hide!
|
| 29/2 |
2026/08/09 03:26 |
tool |
git-log |
0m
Args: map[CodeRegexp:btrfs_assert_inode_range_clean Count:50 PathPrefix:fs/btrfs/file.c]
Results: map[Output:63c34cb4c6dd btrfs: add and use helper to assert an inode range is clean
]
|
| 30/2 |
2026/08/09 03:26 |
llm |
expert |
1m
Model:
Tokens:
input: 26324
output: 21
thoughts: 34
Thoughts: Okay, here's the summary of those thoughts, tailored for an expert audience, presented in the first person:
**Digging Deeper: The Elusive Bug Hunt**
Alright, I need to get back on track here. I'm hitting a wall on this bug, and it's time to shake things up. I can't just keep staring at the same line of code; thereβs got to be more. Maybe this fix isn't where I *think* it is. Perhaps a seemingly unrelated commit in another file actually addresses the root cause indirectly, or the commit message itself holds a hidden clue. This requires a broader approach. Instead of focusing solely on the immediate code, I should look at the commit history and the related files.
The error message, "found unexpected ordered extent in file range," needs to guide me. Let me go back and do a global search across the entire project for that specific string. Hopefully this will reveal other instances, related code, and possibly a more comprehensive context than the single file I'm currently focused on. I'll start with the search to uncover the full scope of where this specific string is used, and then broaden the search to include other likely keywords and surrounding code.
|
| 31/2 |
2026/08/09 03:28 |
tool |
grepper |
0m
Args: map[Expression:found unexpected ordered extent in file range]
Results: map[Output:fs/btrfs/inode.c=10644=void btrfs_assert_inode_range_clean(struct btrfs_inode *inode, u64 start, u64 end)
--
fs/btrfs/inode.c-10654- btrfs_err(root->fs_info,
fs/btrfs/inode.c:10655:"found unexpected ordered extent in file range [%llu, %llu] for inode %llu root %llu (ordered range [%llu, %llu])",
fs/btrfs/inode.c-10656- start, end, btrfs_ino(inode), btrfs_root_id(root),
]
|
| 32/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 26821
output: 46
thoughts: 21
Thoughts: Here's my thought process, as if I were considering this search:
**Diving into Btrfs and Ordered Extents**
Okay, so I need to understand the interaction between `btrfs_punch_hole` and "ordered extent" behavior. My first instinct, as always, is to go straight to the source: the Git log. It's the most reliable record of changes, bug fixes, and the reasoning behind them. I'm going to search the Git log for these specific keywords, `"btrfs_punch_hole"` and `"ordered extent"`. That should give me a chronological view of where these concepts converge, any commits that directly link them, and any discussions that might shed light on how they interact within the Btrfs filesystem. The goal is to see if there have been any regressions, improvements or clarifications on the interaction between the two concepts. I'm expecting to find information about data integrity, performance considerations, and how they relate to the underlying physical layout on disk. Let's see what the log reveals.
|
| 33/2 |
2026/08/09 03:28 |
tool |
git-log |
0m
Args: map[Count:20 MessageRegexps:[btrfs_punch_hole ordered extent] PathPrefix:fs/btrfs/]
Results: map[Output:0528476b6ac7 btrfs: fix the filemap_range_has_page() call in btrfs_punch_hole_lock_range()
a7f8b1c2ac21 btrfs: file: reserve qgroup space after the hole punch range is locked
6126e3caf746 Btrfs: fix ordered extent check in btrfs_punch_hole
]
|
| 34/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 27217
output: 26
thoughts: 17
Thoughts: Okay, let's dive into this. Here's my thought process regarding that string:
**Analyzing the String: `0528476b6ac7`**
My first instinct is to treat this like a potential hash value. The format immediately screams hexadecimal β the presence of digits 0-9 and letters a-f is a dead giveaway. The length, too, is a clue. It's 12 characters, which could represent a variety of things depending on the hashing algorithm. A 48-bit value, perhaps? Or a truncated version of something longer?
Given my expertise, I'm already mentally running through potential hash functions: MD5, SHA-1, SHA-256 (though that's less likely with a length like this), maybe even something less common, depending on the context. If itβs from a specific system, I'd immediately consult my knowledge of its typical hash usage. Is there known output length?
My next step would be to determine the context. Where did this string come from? What system or application is it associated with? Knowing the source is crucial. Did it come from a database, a log file, a network packet, a source code dump, or something else entirely? The origin provides valuable clues, potentially even the type of data that was hashed (password, file content, a key, or random ID).
I would then consider: Is this a unique identifier, or could multiple inputs map to this same value? Is it likely that I'm supposed to 'reverse' it? This would depend on whether this looks like some kind of obfuscation or something with intent to hide.
Without any additional context, I'd start with a basic online hash cracker or analyzer. I'd input the string and see if any tools can identify the hashing algorithm used. I'd also check if it's already in any known databases of cracked hashes, especially if it looks like a common password. Even if that fails, Iβd move to trying to brute force known hashing outputs in a variety of permutations. If that proves fruitless, I'd likely then need to consult other sources, based on the origin of the string.
|
| 35/2 |
2026/08/09 03:28 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 0528476b6ac7832f31e2ed740a57ae31316b124e
Author: Qu Wenruo <wqu@suse.com>
Date: Mon May 31 16:50:54 2021 +0800
btrfs: fix the filemap_range_has_page() call in btrfs_punch_hole_lock_range()
[BUG]
With current subpage RW support, the following script can hang the fs
with 64K page size.
# mkfs.btrfs -f -s 4k $dev
# mount $dev -o nospace_cache $mnt
# fsstress -w -n 50 -p 1 -s 1607749395 -d $mnt
The kernel will do an infinite loop in btrfs_punch_hole_lock_range().
[CAUSE]
In btrfs_punch_hole_lock_range() we:
- Truncate page cache range
- Lock extent io tree
- Wait any ordered extents in the range.
We exit the loop until we meet all the following conditions:
- No ordered extent in the lock range
- No page is in the lock range
The latter condition has a pitfall, it only works for sector size ==
PAGE_SIZE case.
While can't handle the following subpage case:
0 32K 64K 96K 128K
| |///////||//////| ||
lockstart=32K
lockend=96K - 1
In this case, although the range crosses 2 pages,
truncate_pagecache_range() will invalidate no page at all, but only zero
the [32K, 96K) range of the two pages.
Thus filemap_range_has_page(32K, 96K-1) will always return true, thus we
will never meet the loop exit condition.
[FIX]
Fix the problem by doing page alignment for the lock range.
Function filemap_range_has_page() has already handled lend < lstart
case, we only need to round up @lockstart, and round_down @lockend for
truncate_pagecache_range().
This modification should not change any thing for sector size ==
PAGE_SIZE case, as in that case our range is already page aligned.
Tested-by: Ritesh Harjani <riteshh@linux.ibm.com> # [ppc64]
Tested-by: Anand Jain <anand.jain@oracle.com> # [aarch64]
Signed-off-by: Qu Wenruo <wqu@suse.com>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c
index 617af76e876e..d0081b2d47ab 100644
--- a/fs/btrfs/file.c
+++ b/fs/btrfs/file.c
@@ -2486,6 +2486,17 @@ static int btrfs_punch_hole_lock_range(struct inode *inode,
const u64 lockend,
struct extent_state **cached_state)
{
+ /*
+ * For subpage case, if the range is not at page boundary, we could
+ * have pages at the leading/tailing part of the range.
+ * This could lead to dead loop since filemap_range_has_page()
+ * will always return true.
+ * So here we need to do extra page alignment for
+ * filemap_range_has_page().
+ */
+ const u64 page_lockstart = round_up(lockstart, PAGE_SIZE);
+ const u64 page_lockend = round_down(lockend + 1, PAGE_SIZE) - 1;
+
while (1) {
struct btrfs_ordered_extent *ordered;
int ret;
@@ -2506,7 +2517,7 @@ static int btrfs_punch_hole_lock_range(struct inode *inode,
(ordered->file_offset + ordered->num_bytes <= lockstart ||
ordered->file_offset > lockend)) &&
!filemap_range_has_page(inode->i_mapping,
- lockstart, lockend)) {
+ page_lockstart, page_lockend)) {
if (ordered)
btrfs_put_ordered_extent(ordered);
break;
]
|
| 36/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 28953
output: 33
thoughts: 99
Thoughts: **Analyzing the Timeline and Identifying the Key Elements**
Okay, so the initial code snippet, `0528476b6ac7`, that's from 2021, I need to keep that in mind as a point of reference. I need to get back to the bug report. I'm going to re-examine that report to confirm the kernel version it references, specifically, because it mentions `btrfs_assert_inode_range_clean`, which I know was introduced in 2022. That helps narrow the scope of the problem. Now, let's switch gears: I'm going to search through the commit history for relevant interactions. I need to identify commits where both `btrfs_assert_inode_range_clean` *and* `btrfs_punch_hole` are mentioned. The overlap of these two functions is the key. Those will probably be critical to understanding what's really happening here.
|
| 37/2 |
2026/08/09 03:28 |
tool |
git-log |
0m
Args: map[Count:20 MessageRegexps:[btrfs_assert_inode_range_clean]]
Results:
|
| 38/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 29302
output: 50
thoughts: 52
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience:
**Debugging a Potential Btrfs Inconsistency**
Alright, hang on a second. I just used `btrfs_assert_inode_range_clean` in `MessageRegexps`, but now I'm thinking... that function's assertion might not *always* be present in the log messages I'm parsing. The logs aren't always consistent, and that could lead to a false negative. I need to widen my net.
Let me rethink this. Instead of relying on a potentially absent assertion, I should look for the *key indicators* that would trigger that assertion. Specifically, I should search for events that are *likely* to involve holes being punched, and that are often associated with the issue I'm tracking. That means a targeted search for `btrfs_punch_hole` calls, which directly indicate hole punching on disk, and also search for logs related to 'ordered extent' as hole punching often affects the ordered extent functionality. If I can find these, I can be more confident in identifying the relevant events.
|
| 39/2 |
2026/08/09 03:28 |
tool |
git-log |
0m
Args: map[CodeRegexp:btrfs_punch_hole Count:20 MessageRegexps:[ordered extent] PathPrefix:fs/btrfs/]
Results: map[Output:55961c8abfdc btrfs: remove ordered extent check and wait during hole punching and zero range
bcd77455d590 btrfs: don't clear page extent mapped if we're not invalidating the full page
]
|
| 40/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 29711
output: 25
thoughts: 37
Thoughts: Here's a summary of my understanding of that commit, presented as if it were my thought process:
**Diving into the Btrfs Hole Punching Optimization**
Okay, so I'm looking at commit `55961c8abfdc` related to Btrfs, specifically concerning "remove ordered extent check and wait during hole punching and zero range." Right, so the title implies we're streamlining some operations related to how Btrfs handles file fragmentation and writing zeroes. My immediate thought is, "Ah, probably related to improving the speed of sparse file operations, or maybe even dedup."
The "ordered extent check" phrase immediately flags something. I'm imagining there was a check in the past where some kind of ordering of extents was *required* for either hole punching or writing zeroes efficiently. That ordering adds overhead. The fact that the commit is removing it indicates there's now a different method in place, hopefully one that's faster and more flexible. Or perhaps the ordering assumption was unnecessary in the first place, maybe leading to performance stalls or redundant checks.
And the "wait" component... that's always interesting. Whenever a system needs to *wait*, that's a potential bottleneck. I'm assuming that this waiting was related to this "ordered extent check," in that maybe the process would stall until a given condition was met on the extent data. If the commit removes both of these, it could be a significant performance gain.
I'll have to dig deeper into the commit itself, especially the code diffs, to understand *how* they managed to remove this check and wait. I suspect that there's a new locking strategy, or a more efficient use of the block allocator, or perhaps even a change in the metadata layout that allows for the safe removal of this redundant step. It's also possible that there was a subtle flaw in the prior logic that this commit fixes, allowing for the removal of the guard. Regardless, I'll need to see the actual code to see what the specific change is and assess the potential impact on performance. I'm guessing that this is targeted towards the file system level and not the block device, so the block device logic likely doesn't have to change. This is interesting.
|
| 41/2 |
2026/08/09 03:28 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 55961c8abfdcb1f0c8420c5c6bdff86170cf43fc
Author: Filipe Manana <fdmanana@suse.com>
Date: Tue Mar 15 15:22:40 2022 +0000
btrfs: remove ordered extent check and wait during hole punching and zero range
For hole punching and zero range we have this loop that checks if we have
ordered extents after locking the file range, and if so unlock the range,
wait for ordered extents, and retry until we don't find more ordered
extents.
This logic was needed in the past because:
1) Direct IO writes within the i_size boundary did not take the inode's
VFS lock. This was because that lock used to be a mutex, then some
years ago it was switched to a rw semaphore (commit 9902af79c01a8e
("parallel lookups: actual switch to rwsem")), and then btrfs was
changed to take the VFS inode's lock in shared mode for writes that
don't cross the i_size boundary (commit e9adabb9712ef9 ("btrfs: use
shared lock for direct writes within EOF"));
2) We could race with memory mapped writes, because memory mapped writes
don't acquire the inode's VFS lock. We don't have that race anymore,
as we have a rw semaphore to synchronize memory mapped writes with
fallocate (and reflinking too). That change happened with commit
8d9b4a162a37ce ("btrfs: exclude mmap from happening during all
fallocate operations").
So stop looking for ordered extents after locking the file range when
doing hole punching and zero range operations.
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c
index f8eaa184f7ab..fd90e25382b2 100644
--- a/fs/btrfs/file.c
+++ b/fs/btrfs/file.c
@@ -2570,10 +2570,10 @@ static int find_first_non_hole(struct btrfs_inode *inode, u64 *start, u64 *len)
return ret;
}
-static int btrfs_punch_hole_lock_range(struct inode *inode,
- const u64 lockstart,
- const u64 lockend,
- struct extent_state **cached_state)
+static void btrfs_punch_hole_lock_range(struct inode *inode,
+ const u64 lockstart,
+ const u64 lockend,
+ struct extent_state **cached_state)
{
/*
* For subpage case, if the range is not at page boundary, we could
@@ -2587,40 +2587,27 @@ static int btrfs_punch_hole_lock_range(struct inode *inode,
const u64 page_lockend = round_down(lockend + 1, PAGE_SIZE) - 1;
while (1) {
- struct btrfs_ordered_extent *ordered;
- int ret;
-
truncate_pagecache_range(inode, lockstart, lockend);
lock_extent_bits(&BTRFS_I(inode)->io_tree, lockstart, lockend,
cached_state);
- ordered = btrfs_lookup_first_ordered_extent(BTRFS_I(inode),
- lockend);
-
/*
- * We need to make sure we have no ordered extents in this range
- * and nobody raced in and read a page in this range, if we did
- * we need to try again.
+ * We can't have ordered extents in the range, nor dirty/writeback
+ * pages, because we have locked the inode's VFS lock in exclusive
+ * mode, we have locked the inode's i_mmap_lock in exclusive mode,
+ * we have flushed all delalloc in the range and we have waited
+ * for any ordered extents in the range to complete.
+ * We can race with anyone reading pages from this range, so after
+ * locking the range check if we have pages in the range, and if
+ * we do, unlock the range and retry.
*/
- if ((!ordered ||
- (ordered->file_offset + ordered->num_bytes <= lockstart ||
- ordered->file_offset > lockend)) &&
- !filemap_range_has_page(inode->i_mapping,
- page_lockstart, page_lockend)) {
- if (ordered)
- btrfs_put_ordered_extent(ordered);
+ if (!filemap_range_has_page(inode->i_mapping, page_lockstart,
+ page_lockend))
break;
- }
- if (ordered)
- btrfs_put_ordered_extent(ordered);
+
unlock_extent_cached(&BTRFS_I(inode)->io_tree, lockstart,
lockend, cached_state);
- ret = btrfs_wait_ordered_range(inode, lockstart,
- lockend - lockstart + 1);
- if (ret)
- return ret;
}
- return 0;
}
static int btrfs_insert_replace_extent(struct btrfs_trans_handle *trans,
@@ -3073,10 +3060,7 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len)
goto out_only_mutex;
}
- ret = btrfs_punch_hole_lock_range(inode, lockstart, lockend,
- &cached_state);
- if (ret)
- goto out_only_mutex;
+ btrfs_punch_hole_lock_range(inode, lockstart, lockend, &cached_state);
path = btrfs_alloc_path();
if (!path) {
@@ -3367,10 +3351,8 @@ static int btrfs_zero_range(struct inode *inode,
if (ret < 0)
goto out;
space_reserved = true;
- ret = btrfs_punch_hole_lock_range(inode, lockstart, lockend,
- &cached_state);
- if (ret)
- goto out;
+ btrfs_punch_hole_lock_range(inode, lockstart, lockend,
+ &cached_state);
ret = btrfs_qgroup_reserve_data(BTRFS_I(inode), &data_reserved,
alloc_start, bytes_to_reserve);
if (ret) {
]
|
| 42/2 |
2026/08/09 03:28 |
llm |
expert |
0m
Model:
Tokens:
input: 32049
output: 36
thoughts: 246
Thoughts: **Analyzing a Btrfs Bug Report**
Okay, so I'm looking at this commit, specifically the part where they removed the loop in `btrfs_punch_hole_lock_range` that waits for ordered extents. The reasoning was that ordered extents shouldn't *exist* there. But the syzbot report is throwing a wrench in the works: it's showing an *actual* ordered extent! This is concerning because the report clearly states, "found unexpected ordered extent in file range [0, 6295551] for inode 263 root 5 (ordered range [131072, 262143])". It points the finger at commit `55961c8abfdc`.
This contradicts the commit's assumption. Why is this ordered extent showing up? I need to understand the root cause. The syzbot report is also mentioning `madvise(MADV_REMOVE)`. That's interesting, but how is it related? Aha, `madvise(MADV_REMOVE)` leads to a call to `vfs_fallocate(file, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, start, len)`. But `madvise(MADV_REMOVE)` *doesn't* work on a file descriptor directly; it operates on memory mapped regions, which is slightly unusual. Okay, I need to dig deeper into the `madvise_remove` implementation to connect the dots. I need to understand how we got from `madvise` to the ordered extent.
|
| 43/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_remove]
Results: map[SourceCode: 996: /*
997: * Application wants to free up the pages and associated backing store.
998: * This is effectively punching a hole into the middle of a file.
999: */
1000: static long madvise_remove(struct madvise_behavior *madv_behavior)
1001: {
1002: loff_t offset;
1003: int error;
1004: struct file *f;
1005: struct mm_struct *mm = madv_behavior->mm;
1006: struct vm_area_struct *vma = madv_behavior->vma;
1007: unsigned long start = madv_behavior->range.start;
1008: unsigned long end = madv_behavior->range.end;
1009:
1010: mark_mmap_lock_dropped(madv_behavior);
1011:
1012: if (vma->vm_flags & VM_LOCKED)
1013: return -EINVAL;
1014:
1015: f = vma->vm_file;
1016:
1017: if (!f || !f->f_mapping || !f->f_mapping->host) {
1018: return -EINVAL;
1019: }
1020:
1021: if (!vma_is_shared_maywrite(vma))
1022: return -EACCES;
1023:
1024: offset = (loff_t)(start - vma->vm_start)
1025: + ((loff_t)vma->vm_pgoff << PAGE_SHIFT);
1026:
1027: /*
1028: * Filesystem's fallocate may need to take i_rwsem. We need to
1029: * explicitly grab a reference because the vma (and hence the
1030: * vma's reference to the file) can go away as soon as we drop
1031: * mmap_lock.
1032: */
1033: get_file(f);
1034: if (userfaultfd_remove(vma, start, end)) {
1035: /* mmap_lock was not released by userfaultfd_remove() */
1036: mmap_read_unlock(mm);
1037: }
1038: error = vfs_fallocate(f,
1039: FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
1040: offset, end - start);
1041: fput(f);
1042: mmap_read_lock(mm);
1043: return error;
1044: }
SourceFile:mm/madvise.c]
|
| 44/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 33457
output: 40
thoughts: 166
Thoughts: **My Analysis of the `madvise_remove` Path**
Okay, let's break this down. From what I'm seeing, `madvise_remove` is hitting `vfs_fallocate`, which in turn calls into `btrfs_fallocate`. My focus is now on understanding why this particular code path is behaving the way it is. Specifically, why do we not have any ordered extents here?
I see that `btrfs_fallocate` ultimately leads to `btrfs_punch_hole`. I need to figure out the conditions which explain the lack of ordered extents.
The commit `55961c8abfdc` immediately seems relevant. It seems to explain the reasoning, stating we *can't* have ordered extents in the range. It says we've got the VFS lock in exclusive mode, the `i_mmap_lock` in exclusive mode, we've flushed all delalloc within the region, and we've waited for any existing ordered extents to complete.
Okay, let me go back to `btrfs_punch_hole`.
|
| 45/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/file.c Name:btrfs_punch_hole]
Results: map[SourceCode:2644: static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len)
2645: {
2646: struct inode *inode = file_inode(file);
2647: struct btrfs_fs_info *fs_info = inode_to_fs_info(inode);
2648: struct btrfs_root *root = BTRFS_I(inode)->root;
2649: struct extent_state *cached_state = NULL;
2650: struct btrfs_path *path;
2651: struct btrfs_trans_handle *trans = NULL;
2652: u64 lockstart;
2653: u64 lockend;
2654: u64 tail_start;
2655: u64 tail_len;
2656: const u64 orig_start = offset;
2657: const u64 orig_end = offset + len - 1;
2658: int ret = 0;
2659: bool same_block;
2660: u64 ino_size;
2661: bool truncated_block = false;
2662: bool updated_inode = false;
2663:
2664: btrfs_inode_lock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2665:
2666: ret = btrfs_wait_ordered_range(BTRFS_I(inode), offset, len);
2667: if (ret)
2668: goto out_only_mutex;
2669:
2670: ino_size = round_up(inode->i_size, fs_info->sectorsize);
2671: ret = find_first_non_hole(BTRFS_I(inode), &offset, &len);
2672: if (ret < 0)
2673: goto out_only_mutex;
2674: if (ret && !len) {
2675: /* Already in a large hole */
2676: ret = 0;
2677: goto out_only_mutex;
2678: }
2679:
2680: ret = file_modified(file);
2681: if (ret)
2682: goto out_only_mutex;
2683:
2684: lockstart = round_up(offset, fs_info->sectorsize);
2685: lockend = round_down(offset + len, fs_info->sectorsize) - 1;
2686: same_block = (BTRFS_BYTES_TO_BLKS(fs_info, offset))
2687: == (BTRFS_BYTES_TO_BLKS(fs_info, offset + len - 1));
2688: /*
2689: * Only do this if we are in the same block and we aren't doing the
2690: * entire block.
2691: */
2692: if (same_block && len < fs_info->sectorsize) {
2693: if (offset < ino_size) {
2694: truncated_block = true;
2695: ret = btrfs_truncate_block(BTRFS_I(inode), offset + len - 1,
2696: orig_start, orig_end);
2697: } else {
2698: ret = 0;
2699: }
2700: goto out_only_mutex;
2701: }
2702:
2703: /* zero back part of the first block */
2704: if (offset < ino_size) {
2705: truncated_block = true;
2706: ret = btrfs_truncate_block(BTRFS_I(inode), offset, orig_start, orig_end);
2707: if (ret) {
2708: btrfs_inode_unlock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2709: return ret;
2710: }
2711: }
2712:
2713: /* Check the aligned pages after the first unaligned page,
2714: * if offset != orig_start, which means the first unaligned page
2715: * including several following pages are already in holes,
2716: * the extra check can be skipped */
2717: if (offset == orig_start) {
2718: /* after truncate page, check hole again */
2719: len = offset + len - lockstart;
2720: offset = lockstart;
2721: ret = find_first_non_hole(BTRFS_I(inode), &offset, &len);
2722: if (ret < 0)
2723: goto out_only_mutex;
2724: if (ret && !len) {
2725: ret = 0;
2726: goto out_only_mutex;
2727: }
2728: lockstart = offset;
2729: }
2730:
2731: /* Check the tail unaligned part is in a hole */
2732: tail_start = lockend + 1;
2733: tail_len = offset + len - tail_start;
2734: if (tail_len) {
2735: ret = find_first_non_hole(BTRFS_I(inode), &tail_start, &tail_len);
2736: if (unlikely(ret < 0))
2737: goto out_only_mutex;
2738: if (!ret) {
2739: /* zero the front end of the last page */
2740: if (tail_start + tail_len < ino_size) {
2741: truncated_block = true;
2742: ret = btrfs_truncate_block(BTRFS_I(inode),
2743: tail_start + tail_len - 1,
2744: orig_start, orig_end);
2745: if (ret)
2746: goto out_only_mutex;
2747: }
2748: }
2749: }
2750:
2751: if (lockend < lockstart) {
2752: ret = 0;
2753: goto out_only_mutex;
2754: }
2755:
2756: btrfs_punch_hole_lock_range(inode, lockstart, lockend, &cached_state);
2757:
2758: path = btrfs_alloc_path();
2759: if (!path) {
2760: ret = -ENOMEM;
2761: goto out;
2762: }
2763:
2764: ret = btrfs_replace_file_extents(BTRFS_I(inode), path, lockstart,
2765: lockend, NULL, &trans);
2766: btrfs_free_path(path);
2767: if (ret)
2768: goto out;
2769:
2770: ASSERT(trans != NULL);
2771: inode_inc_iversion(inode);
2772: inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
2773: ret = btrfs_update_inode(trans, BTRFS_I(inode));
2774: updated_inode = true;
2775: btrfs_end_transaction(trans);
2776: btrfs_btree_balance_dirty(fs_info);
2777: out:
2778: btrfs_unlock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
2779: &cached_state);
2780: out_only_mutex:
2781: if (!updated_inode && truncated_block && !ret) {
2782: /*
2783: * If we only end up zeroing part of a page, we still need to
2784: * update the inode item, so that all the time fields are
2785: * updated as well as the necessary btrfs inode in memory fields
2786: * for detecting, at fsync time, if the inode isn't yet in the
2787: * log tree or it's there but not up to date.
2788: */
2789: struct timespec64 now = inode_set_ctime_current(inode);
2790:
2791: inode_inc_iversion(inode);
2792: inode_set_mtime_to_ts(inode, now);
2793: trans = btrfs_start_transaction(root, 1);
2794: if (IS_ERR(trans)) {
2795: ret = PTR_ERR(trans);
2796: } else {
2797: int ret2;
2798:
2799: ret = btrfs_update_inode(trans, BTRFS_I(inode));
2800: ret2 = btrfs_end_transaction(trans);
2801: if (!ret)
2802: ret = ret2;
2803: }
2804: }
2805: btrfs_inode_unlock(BTRFS_I(inode), BTRFS_ILOCK_MMAP);
2806: return ret;
2807: }
SourceFile:fs/btrfs/file.c]
|
| 46/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 36540
output: 40
thoughts: 125
Thoughts: **My Analysis of `btrfs_punch_hole`**
Okay, so I'm looking at `btrfs_punch_hole` and the first thing that jumps out is this call to `btrfs_wait_ordered_range`. I know that function; it's waiting for ordered extents in the range specified by `offset` and `len`. Specifically, it's concerned with the range `[offset, offset + len - 1]`. That makes sense, ensuring the data in that region is consistent before we do anything destructive.
But then... `btrfs_truncate_block` is called. Ah, right, that's the function that's going to actually modify the file to "punch" the hole. And I'm suddenly realizing, *hang on*, `btrfs_truncate_block` itself might *create* new ordered extents! We need to consider how that interacts with the prior wait. Does `btrfs_wait_ordered_range` account for extents that are *created* within the targeted range? It's a key interaction. I'm going to jump into `btrfs_truncate_block` and see how it behaves exactly and determine if the ordered extent behavior is as I expect.
|
| 47/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_truncate_block]
Results: map[SourceCode:5031: /*
5032: * Handle the truncation of a fs block.
5033: *
5034: * @inode - inode that we're zeroing
5035: * @offset - the file offset of the block to truncate
5036: * The value must be inside [@start, @end], and the function will do
5037: * extra checks if the block that covers @offset needs to be zeroed.
5038: * @start - the start file offset of the range we want to zero
5039: * @end - the end (inclusive) file offset of the range we want to zero.
5040: *
5041: * If the range is not block aligned, read out the folio that covers @offset,
5042: * and if needed zero blocks that are inside the folio and covered by [@start, @end).
5043: * If @start or @end + 1 lands inside a block, that block will be marked dirty
5044: * for writeback.
5045: *
5046: * This is utilized by hole punch, zero range, file expansion.
5047: */
5048: int btrfs_truncate_block(struct btrfs_inode *inode, u64 offset, u64 start, u64 end)
5049: {
5050: struct btrfs_fs_info *fs_info = inode->root->fs_info;
5051: struct address_space *mapping = inode->vfs_inode.i_mapping;
5052: struct extent_io_tree *io_tree = &inode->io_tree;
5053: struct btrfs_ordered_extent *ordered;
5054: struct extent_state *cached_state = NULL;
5055: struct extent_changeset *data_reserved = NULL;
5056: bool only_release_metadata = false;
5057: u32 blocksize = fs_info->sectorsize;
5058: pgoff_t index = (offset >> PAGE_SHIFT);
5059: struct folio *folio;
5060: gfp_t mask = btrfs_alloc_write_mask(mapping);
5061: int ret = 0;
5062: const bool in_head_block = is_inside_block(offset, round_down(start, blocksize),
5063: blocksize);
5064: const bool in_tail_block = is_inside_block(offset, round_down(end, blocksize),
5065: blocksize);
5066: bool need_truncate_head = false;
5067: bool need_truncate_tail = false;
5068: u64 zero_start;
5069: u64 zero_end;
5070: u64 block_start;
5071: u64 block_end;
5072:
5073: /* @offset should be inside the range. */
5074: ASSERT(start <= offset && offset <= end, "offset=%llu start=%llu end=%llu",
5075: offset, start, end);
5076:
5077: /* The range is aligned at both ends. */
5078: if (IS_ALIGNED(start, blocksize) && IS_ALIGNED(end + 1, blocksize)) {
5079: /*
5080: * For block size < page size case, we may have polluted blocks
5081: * beyond EOF. So we also need to zero them out.
5082: */
5083: if (end == (u64)-1 && blocksize < PAGE_SIZE)
5084: ret = truncate_block_zero_beyond_eof(inode, start);
5085: goto out;
5086: }
5087:
5088: /*
5089: * @offset may not be inside the head nor tail block. In that case we
5090: * don't need to do anything.
5091: */
5092: if (!in_head_block && !in_tail_block)
5093: goto out;
5094:
5095: /*
5096: * Skip the truncation if the range in the target block is already aligned.
5097: * The seemingly complex check will also handle the same block case.
5098: */
5099: if (in_head_block && !IS_ALIGNED(start, blocksize))
5100: need_truncate_head = true;
5101: if (in_tail_block && !IS_ALIGNED(end + 1, blocksize))
5102: need_truncate_tail = true;
5103: if (!need_truncate_head && !need_truncate_tail)
5104: goto out;
5105:
5106: block_start = round_down(offset, blocksize);
5107: block_end = block_start + blocksize - 1;
5108:
5109: ret = btrfs_check_data_free_space(inode, &data_reserved, block_start,
5110: blocksize, false);
5111: if (ret < 0) {
5112: size_t write_bytes = blocksize;
5113:
5114: if (btrfs_check_nocow_lock(inode, block_start, &write_bytes, false) > 0) {
5115: /* For nocow case, no need to reserve data space. */
5116: ASSERT(write_bytes == blocksize, "write_bytes=%zu blocksize=%u",
5117: write_bytes, blocksize);
5118: only_release_metadata = true;
5119: } else {
5120: goto out;
5121: }
5122: }
5123: ret = btrfs_delalloc_reserve_metadata(inode, blocksize, blocksize, false);
5124: if (ret < 0) {
5125: if (!only_release_metadata)
5126: btrfs_free_reserved_data_space(inode, data_reserved,
5127: block_start, blocksize);
5128: goto out;
5129: }
5130: again:
5131: folio = __filemap_get_folio(mapping, index,
5132: FGP_LOCK | FGP_ACCESSED | FGP_CREAT, mask);
5133: if (IS_ERR(folio)) {
5134: if (only_release_metadata)
5135: btrfs_delalloc_release_metadata(inode, blocksize, true);
5136: else
5137: btrfs_delalloc_release_space(inode, data_reserved,
5138: block_start, blocksize, true);
5139: btrfs_delalloc_release_extents(inode, blocksize);
5140: ret = PTR_ERR(folio);
5141: goto out;
5142: }
5143:
5144: if (!folio_test_uptodate(folio)) {
5145: ret = btrfs_read_folio(NULL, folio);
5146: folio_lock(folio);
5147: if (folio->mapping != mapping) {
5148: folio_unlock(folio);
5149: folio_put(folio);
5150: goto again;
5151: }
5152: if (unlikely(!folio_test_uptodate(folio))) {
5153: ret = -EIO;
5154: goto out_unlock;
5155: }
5156: }
5157:
5158: /*
5159: * We unlock the page after the io is completed and then re-lock it
5160: * above. release_folio() could have come in between that and cleared
5161: * folio private, but left the page in the mapping. Set the page mapped
5162: * here to make sure it's properly set for the subpage stuff.
5163: */
5164: ret = set_folio_extent_mapped(folio);
5165: if (ret < 0)
5166: goto out_unlock;
5167:
5168: folio_wait_writeback(folio);
5169:
5170: btrfs_lock_extent(io_tree, block_start, block_end, &cached_state);
5171:
5172: ordered = btrfs_lookup_ordered_extent(inode, block_start);
5173: if (ordered) {
5174: btrfs_unlock_extent(io_tree, block_start, block_end, &cached_state);
5175: folio_unlock(folio);
5176: folio_put(folio);
5177: btrfs_start_ordered_extent(ordered);
5178: btrfs_put_ordered_extent(ordered);
5179: goto again;
5180: }
5181:
5182: ret = btrfs_reset_extent_delalloc(inode, block_start, block_end, 0, &cached_state);
5183: if (ret) {
5184: btrfs_unlock_extent(io_tree, block_start, block_end, &cached_state);
5185: goto out_unlock;
5186: }
5187:
5188: if (end == (u64)-1) {
5189: /*
5190: * We're truncating beyond EOF, the remaining blocks normally are
5191: * already holes thus no need to zero again, but it's possible for
5192: * fs block size < page size cases to have memory mapped writes
5193: * to pollute ranges beyond EOF.
5194: *
5195: * In that case although such polluted blocks beyond EOF will
5196: * not reach disk, it still affects our page caches.
5197: */
5198: zero_start = max_t(u64, folio_pos(folio), start);
5199: zero_end = min_t(u64, folio_next_pos(folio) - 1, end);
5200: } else {
5201: zero_start = max_t(u64, block_start, start);
5202: zero_end = min_t(u64, block_end, end);
5203: }
5204: folio_zero_range(folio, zero_start - folio_pos(folio),
5205: zero_end - zero_start + 1);
5206:
5207: btrfs_folio_set_dirty(fs_info, folio, block_start,
5208: block_end + 1 - block_start);
5209:
5210: if (only_release_metadata)
5211: btrfs_set_extent_bit(&inode->io_tree, block_start, block_end,
5212: EXTENT_NORESERVE, &cached_state);
5213:
5214: btrfs_unlock_extent(io_tree, block_start, block_end, &cached_state);
5215:
5216: out_unlock:
5217: if (ret) {
5218: if (only_release_metadata)
5219: btrfs_delalloc_release_metadata(inode, blocksize, true);
5220: else
5221: btrfs_delalloc_release_space(inode, data_reserved,
5222: block_start, blocksize, true);
5223: }
5224: btrfs_delalloc_release_extents(inode, blocksize);
5225: folio_unlock(folio);
5226: folio_put(folio);
5227: out:
5228: if (only_release_metadata)
5229: btrfs_check_nocow_unlock(inode);
5230: extent_changeset_free(data_reserved);
5231: return ret;
5232: }
SourceFile:fs/btrfs/inode.c]
|
| 48/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 40363
output: 44
thoughts: 864
Thoughts: **Analyzing Btrfs Punch Hole and Delalloc Interactions**
Okay, let's break this down. `btrfs_truncate_block` is zeroing a block and marking it dirty. This is where it gets interesting, since marking a folio dirty triggers delalloc. And when delalloc is flushed, it yields ordered extents. The key question is timing: when does delalloc get flushed relative to `btrfs_truncate_block`?
Looking at the `btrfs_punch_hole` function, `btrfs_truncate_block` is called at two points: once for `offset` and again for `tail_start + tail_len - 1`. Crucially, these offsets are outside of the `[lockstart, lockend]` range that is subsequently handled by `btrfs_punch_hole_lock_range`. `lockstart` is the start of a sector boundary after the truncate, and `lockend` is a sector boundary before.
The dirty pages created by the truncation, therefore, are *outside* of the locked range. So why would we see an ordered extent inside `[lockstart, lockend]`? I'm missing something here...Unless... `btrfs_truncate_block` is triggering writeback for a larger range than what it explicitly dirties, or external writeback is happening.
Ah, but wait! The code calls `btrfs_wait_ordered_range` at the beginning. This waits for ordered extents within the `[offset, offset + len - 1]` range. But I need to confirm: does `btrfs_wait_ordered_range` flush delalloc? Yes, it does!
So, the process is: `btrfs_wait_ordered_range` (flushes delalloc) -> `btrfs_truncate_block` (zeroes blocks, creating more delalloc outside the range) -> `btrfs_punch_hole_lock_range` (deals with the locked range). That makes more sense. The initial `btrfs_wait_ordered_range` is crucial to ensure that any prior operations are completed before we modify the file. The subsequent `btrfs_truncate_block` calls are not within the exclusive range.
|
| 49/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:btrfs_wait_ordered_range]
Results: map[SourceCode: 907: /*
908: * Used to wait on ordered extents across a large range of bytes.
909: */
910: int btrfs_wait_ordered_range(struct btrfs_inode *inode, u64 start, u64 len)
911: {
912: int ret = 0;
913: int ret_wb = 0;
914: u64 end;
915: u64 orig_end;
916: struct btrfs_ordered_extent *ordered;
917:
918: if (start + len < start) {
919: orig_end = OFFSET_MAX;
920: } else {
921: orig_end = start + len - 1;
922: if (orig_end > OFFSET_MAX)
923: orig_end = OFFSET_MAX;
924: }
925:
926: /* start IO across the range first to instantiate any delalloc
927: * extents
928: */
929: ret = btrfs_fdatawrite_range(inode, start, orig_end);
930: if (ret)
931: return ret;
932:
933: /*
934: * If we have a writeback error don't return immediately. Wait first
935: * for any ordered extents that haven't completed yet. This is to make
936: * sure no one can dirty the same page ranges and call writepages()
937: * before the ordered extents complete - to avoid failures (-EEXIST)
938: * when adding the new ordered extents to the ordered tree.
939: */
940: ret_wb = filemap_fdatawait_range(inode->vfs_inode.i_mapping, start, orig_end);
941:
942: end = orig_end;
943: while (1) {
944: ordered = btrfs_lookup_first_ordered_extent(inode, end);
945: if (!ordered)
946: break;
947: if (ordered->file_offset > orig_end) {
948: btrfs_put_ordered_extent(ordered);
949: break;
950: }
951: if (ordered->file_offset + ordered->num_bytes <= start) {
952: btrfs_put_ordered_extent(ordered);
953: break;
954: }
955: btrfs_start_ordered_extent(ordered);
956: end = ordered->file_offset;
957: /*
958: * If the ordered extent had an error save the error but don't
959: * exit without waiting first for all other ordered extents in
960: * the range to complete.
961: */
962: if (test_bit(BTRFS_ORDERED_IOERR, &ordered->flags))
963: ret = -EIO;
964: btrfs_put_ordered_extent(ordered);
965: if (end == 0 || end == start)
966: break;
967: end--;
968: }
969: return ret_wb ? ret_wb : ret;
970: }
SourceFile:fs/btrfs/ordered-data.c]
|
| 50/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 42736
output: 40
thoughts: 864
Thoughts: **Analyzing a Btrfs Ordered Extent Issue**
Okay, so I'm looking at this syzbot report about an unexpected ordered extent in Btrfs, and I need to break this down. The core problem seems to be an ordered extent appearing *inside* a range that was supposed to be protected by `btrfs_wait_ordered_range`. This function is critical for flushing delalloc and ensuring ordered writes, like those used in `btrfs_punch_hole` which calls it.
The initial chain is straightforward: `btrfs_wait_ordered_range` flushes delalloc and waits, which makes sense. `btrfs_punch_hole`, in turn, calls `btrfs_truncate_block` which *can* create new delalloc. The catch is that this delalloc is created *outside* the protected range. So far, so good, but it is not what the report shows.
The syzbot report shows an ordered extent *within* the expected range. The reported file range is `[0, 6295551]`, which is equivalent to `[lockstart, lockend]`. The ordered range is `[131072, 262143]`, which is firmly *within* `[lockstart, lockend]`. This makes no sense, given that the delalloc is *outside* the range!
My immediate question is: how could an ordered extent be created *after* `btrfs_wait_ordered_range` has supposedly done its job? I need to consider all possible sources of ordered extent creation. Since `btrfs_wait_ordered_range` is supposed to flush delalloc, I need to check if there's a race condition with other write operations. I need to make sure `btrfs_punch_hole` does not race. I did check recent commits and the commit `55961c8abfdc` explicitly addresses races with `mmap` writes by adding a semaphore. This should have excluded the possibility of `mmap` writes creating the ordered extent. Looking at the code for `btrfs_punch_hole`, it *does* lock the mmap lock.
So, I started methodically eliminating potential races. Direct IO writes are excluded because `btrfs_fallocate` takes the VFS lock in exclusive mode. Even buffered writes via io_uring take the VFS lock. `madvise(MADV_REMOVE)` calls `vfs_fallocate` which also takes the VFS lock. All of these should be locked out by `btrfs_punch_hole`. So, where else could this be coming from?
Then it hit me. What about a direct IO write that has already passed the lock, but hasn't created the ordered extent yet? If a direct IO write has been scheduled, the ordered extent is created *after* the lock. This seems like the most plausible explanation. Direct IO writes *do* create ordered extents. I'll need to dig into the `btrfs_direct_write` code to confirm this, but this seems like the most promising avenue of investigation.
|
| 51/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/file.c Name:btrfs_direct_write]
Results: map[SourceCode: 842: ssize_t btrfs_direct_write(struct kiocb *iocb, struct iov_iter *from)
843: {
844: struct file *file = iocb->ki_filp;
845: struct inode *inode = file_inode(file);
846: struct btrfs_fs_info *fs_info = inode_to_fs_info(inode);
847: loff_t pos;
848: ssize_t written = 0;
849: ssize_t written_buffered;
850: size_t prev_left = 0;
851: loff_t endbyte;
852: ssize_t ret;
853: unsigned int ilock_flags = 0;
854: struct iomap_dio *dio;
855: const u64 data_profile = btrfs_data_alloc_profile(fs_info) &
856: BTRFS_BLOCK_GROUP_PROFILE_MASK;
857:
858: if (iocb->ki_flags & IOCB_NOWAIT)
859: ilock_flags |= BTRFS_ILOCK_TRY;
860:
861: /*
862: * If the write DIO is within EOF, use a shared lock and also only if
863: * security bits will likely not be dropped by file_remove_privs() called
864: * from btrfs_write_check(). Either will need to be rechecked after the
865: * lock was acquired.
866: */
867: if (iocb->ki_pos + iov_iter_count(from) <= i_size_read(inode) && IS_NOSEC(inode))
868: ilock_flags |= BTRFS_ILOCK_SHARED;
869:
870: /*
871: * If our data profile has duplication (either extra mirrors or RAID56),
872: * we can not trust the direct IO buffer, the content may change during
873: * writeback and cause different contents written to different mirrors.
874: *
875: * Thus only RAID0 and SINGLE can go true zero-copy direct IO.
876: */
877: if (data_profile != BTRFS_BLOCK_GROUP_RAID0 && data_profile != 0)
878: goto buffered;
879:
880: relock:
881: ret = btrfs_inode_lock(BTRFS_I(inode), ilock_flags);
882: if (ret < 0)
883: return ret;
884:
885: /* Shared lock cannot be used with security bits set. */
886: if ((ilock_flags & BTRFS_ILOCK_SHARED) && !IS_NOSEC(inode)) {
887: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
888: ilock_flags &= ~BTRFS_ILOCK_SHARED;
889: goto relock;
890: }
891:
892: ret = generic_write_checks(iocb, from);
893: if (ret <= 0) {
894: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
895: return ret;
896: }
897:
898: ret = btrfs_write_check(iocb, ret);
899: if (ret < 0) {
900: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
901: goto out;
902: }
903:
904: pos = iocb->ki_pos;
905: /*
906: * Re-check since file size may have changed just before taking the
907: * lock or pos may have changed because of O_APPEND in generic_write_check()
908: */
909: if ((ilock_flags & BTRFS_ILOCK_SHARED) &&
910: pos + iov_iter_count(from) > i_size_read(inode)) {
911: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
912: ilock_flags &= ~BTRFS_ILOCK_SHARED;
913: goto relock;
914: }
915:
916: if (check_direct_IO(fs_info, from, pos)) {
917: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
918: goto buffered;
919: }
920: /*
921: * We can't control the folios being passed in, applications can write
922: * to them while a direct IO write is in progress. This means the
923: * content might change after we calculated the data checksum.
924: * Therefore we can end up storing a checksum that doesn't match the
925: * persisted data.
926: *
927: * To be extra safe and avoid false data checksum mismatch, if the
928: * inode requires data checksum, just fallback to buffered IO.
929: * For buffered IO we have full control of page cache and can ensure
930: * no one is modifying the content during writeback.
931: */
932: if (!(BTRFS_I(inode)->flags & BTRFS_INODE_NODATASUM)) {
933: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
934: goto buffered;
935: }
936:
937: /*
938: * The iov_iter can be mapped to the same file range we are writing to.
939: * If that's the case, then we will deadlock in the iomap code, because
940: * it first calls our callback btrfs_dio_iomap_begin(), which will create
941: * an ordered extent, and after that it will fault in the pages that the
942: * iov_iter refers to. During the fault in we end up in the readahead
943: * pages code (starting at btrfs_readahead()), which will lock the range,
944: * find that ordered extent and then wait for it to complete (at
945: * btrfs_lock_and_flush_ordered_range()), resulting in a deadlock since
946: * obviously the ordered extent can never complete as we didn't submit
947: * yet the respective bio(s). This always happens when the buffer is
948: * memory mapped to the same file range, since the iomap DIO code always
949: * invalidates pages in the target file range (after starting and waiting
950: * for any writeback).
951: *
952: * So here we disable page faults in the iov_iter and then retry if we
953: * got -EFAULT, faulting in the pages before the retry.
954: */
955: again:
956: from->nofault = true;
957: dio = btrfs_dio_write(iocb, from, written);
958: from->nofault = false;
959:
960: if (IS_ERR_OR_NULL(dio)) {
961: ret = PTR_ERR_OR_ZERO(dio);
962: } else {
963: /*
964: * If we have a synchronous write, we must make sure the fsync
965: * triggered by the iomap_dio_complete() call below doesn't
966: * deadlock on the inode lock - we are already holding it and we
967: * can't call it after unlocking because we may need to complete
968: * partial writes due to the input buffer (or parts of it) not
969: * being already faulted in.
970: */
971: ASSERT(current->journal_info == NULL);
972: current->journal_info = BTRFS_TRANS_DIO_WRITE_STUB;
973: ret = iomap_dio_complete(dio);
974: current->journal_info = NULL;
975: }
976:
977: /* No increment (+=) because iomap returns a cumulative value. */
978: if (ret > 0)
979: written = ret;
980:
981: if (iov_iter_count(from) > 0 && (ret == -EFAULT || ret >= 0)) {
982: const size_t left = iov_iter_count(from);
983: /*
984: * We have more data left to write. Try to fault in as many as
985: * possible of the remainder pages and retry. We do this without
986: * releasing and locking again the inode, to prevent races with
987: * truncate.
988: *
989: * Also, in case the iov refers to pages in the file range of the
990: * file we want to write to (due to a mmap), we could enter an
991: * infinite loop if we retry after faulting the pages in, since
992: * iomap will invalidate any pages in the range early on, before
993: * it tries to fault in the pages of the iov. So we keep track of
994: * how much was left of iov in the previous EFAULT and fallback
995: * to buffered IO in case we haven't made any progress.
996: */
997: if (left == prev_left) {
998: ret = -ENOTBLK;
999: } else {
1000: fault_in_iov_iter_readable(from, left);
1001: prev_left = left;
1002: goto again;
1003: }
1004: }
1005:
1006: btrfs_inode_unlock(BTRFS_I(inode), ilock_flags);
1007:
1008: /*
1009: * If 'ret' is -ENOTBLK or we have not written all data, then it means
1010: * we must fallback to buffered IO.
1011: */
1012: if ((ret < 0 && ret != -ENOTBLK) || !iov_iter_count(from))
1013: goto out;
1014:
1015: buffered:
1016: /*
1017: * If we are in a NOWAIT context, then return -EAGAIN to signal the caller
1018: * it must retry the operation in a context where blocking is acceptable,
1019: * because even if we end up not blocking during the buffered IO attempt
1020: * below, we will block when flushing and waiting for the IO.
1021: */
1022: if (iocb->ki_flags & IOCB_NOWAIT) {
1023: ret = -EAGAIN;
1024: goto out;
1025: }
1026:
1027: pos = iocb->ki_pos;
1028: written_buffered = btrfs_buffered_write(iocb, from);
1029: if (written_buffered < 0) {
1030: ret = written_buffered;
1031: goto out;
1032: }
1033: /*
1034: * Ensure all data is persisted. We want the next direct IO read to be
1035: * able to read what was just written.
1036: */
1037: endbyte = pos + written_buffered - 1;
1038: ret = btrfs_fdatawrite_range(BTRFS_I(inode), pos, endbyte);
1039: if (ret)
1040: goto out;
1041: ret = filemap_fdatawait_range(inode->i_mapping, pos, endbyte);
1042: if (ret)
1043: goto out;
1044: written += written_buffered;
1045: iocb->ki_pos = pos + written_buffered;
1046: invalidate_mapping_pages(file->f_mapping, pos >> PAGE_SHIFT,
1047: endbyte >> PAGE_SHIFT);
1048: out:
1049: return ret < 0 ? ret : written;
1050: }
SourceFile:fs/btrfs/direct-io.c]
|
| 52/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 47850
output: 42
thoughts: 1191
Thoughts: **Analyzing Concurrency in Btrfs Punch Hole Operations**
Okay, let's break this down systematically. I'm looking at how `btrfs_punch_hole` interacts with direct IO and ordered extents, especially regarding potential race conditions. First, I note that direct IO uses `btrfs_inode_lock` which *can* conflict with `inode_lock` used by operations like `btrfs_fallocate`, meaning they can't run concurrently. This is the VFS lock, and `btrfs_inode_lock` can take that lock exclusively unless `BTRFS_ILOCK_SHARED` is set. Then it uses `inode_lock_shared`.
The core of the problem seems to be `btrfs_wait_ordered_range`. I'm concerned that an ordered extent might be created *after* this call returns, thus bypassing the protection intended. My initial hunch is that it's related to how `btrfs_wait_ordered_range` interacts with the range it's supposed to wait on. Let's dig in.
The function `btrfs_punch_hole` calls `btrfs_wait_ordered_range` with an `offset` and `len`. From those, I derive `lockstart` and `lockend`, which define the range I *actually* care about. Critically, I see that `[lockstart, lockend]` is a *subset* of the range `[offset, offset + len - 1]` passed to `btrfs_wait_ordered_range`. This is good; `btrfs_wait_ordered_range` is waiting on a *larger* range than I'm ultimately concerned with, so any ordered extent within my target range should be accounted for.
The question then becomes: can an ordered extent be created *after* `btrfs_wait_ordered_range` returns but *within* my `[lockstart, lockend]` range? I need to understand what else might create ordered extents. I'm zeroing in on `btrfs_truncate_block`. It appears that this *could* generate ordered extents. The arguments to that function is before `offset` and after `offset + len -1`. It turns out that any dirty pages created by `btrfs_truncate_block` will *not* overlap with `[lockstart, lockend]`. I'm confident in that.
Now, what if, because I round up and down, I end up with `lockstart > lockend`? The code handles this: it skips the punch hole function entirely if that is the case.
Finally, what about a folio race? `btrfs_truncate_block` dirties a folio, and then another thread flushes the folio to disk, creating the extent. Since I use `fs_info->sectorsize` for my blocksize and the dirtied folio range is either before or after `[lockstart, lockend]`, this *shouldn't* be an issue because the ordered extents will be outside my area of concern. Therefore I should be safe.
|
| 53/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_folio_set_dirty]
Results: map[SourceCode: 780: IMPLEMENT_BTRFS_PAGE_OPS(dirty, btrfs_folio_mark_dirty_reserved,
SourceFile:fs/btrfs/subpage.c]
|
| 54/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 49813
output: 43
thoughts: 28
Thoughts: Here's my summary, as though I'm thinking through this process:
**Diving into btrfs_folio_set_dirty**
Okay, time to dig into this. I'm going to start by taking a look at `btrfs_folio_set_dirty` within `fs/btrfs/subpage.c`. This is where the core logic for marking a folio as dirty in the Btrfs filesystem should reside. I need to refresh my understanding of how Btrfs handles dirty page tracking, especially concerning folios, given the complexities of its copy-on-write nature. I'll need to understand how this function interacts with the higher-level dirty page management, and how it ensures data integrity and eventual writeback to the underlying storage. Let's see what interesting tidbits are hidden in there.
|
| 55/2 |
2026/08/09 03:29 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/subpage.c Name:btrfs_folio_set_dirty]
Results: map[SourceCode: 780: IMPLEMENT_BTRFS_PAGE_OPS(dirty, btrfs_folio_mark_dirty_reserved,
SourceFile:fs/btrfs/subpage.c]
|
| 56/2 |
2026/08/09 03:29 |
llm |
expert |
0m
Model:
Tokens:
input: 50125
output: 43
thoughts: 17
Thoughts: Here's my take on those thoughts:
**Btrfs Page Operations β A Macro Moment**
Okay, so I'm looking at this code, trying to get a handle on how Btrfs is handling page operations. And then it hits me: `IMPLEMENT_BTRFS_PAGE_OPS`. Wait a minute... that's a macro, isn't it? A macro! My brain immediately starts running through the implications. I need to remember what's happening in there.
So, instead of dealing with it directly, the core page operation implementation details are probably hidden away behind this macro. Makes sense, in the grand scheme of code readability. Gotta go look at what the macro expands to... this definitely calls for a deeper dive. It's time to dig into the macro definition and figure out exactly what it's abstracting. I'll need to check the include files. Okay, let's see what we can find. It might be good to expand the macro in-place just to see what kind of boilerplate we're dealing with.
|
| 57/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:fs/btrfs/subpage.c Name:IMPLEMENT_BTRFS_PAGE_OPS]
Results: map[SourceCode: SourceFile:]
|
| 58/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 50429
output: 38
thoughts: 23
Thoughts: Here's a summary of my thought process, as requested:
**Diving into `fs/btrfs/subpage.c`**
Okay, time to dig in. My current focus is on understanding something related to btrfs, and the code around `fs/btrfs/subpage.c` line 780 seems like a likely area to start. I'm not going to approach this blindly; I need to identify the purpose of this section of code. Given my expertise, I'll be looking for clues immediately. I'll be searching for data structures or functions that deal with subpages. I'll mentally map out the surrounding code to grasp the broader context. Is this related to allocation, deallocation, metadata management, or something else entirely? A quick scan for comments or variable names should help steer me in the right direction. Once I get a handle on what's going on, I'll be able to really break it down and understand the nuances. I'll also be watching out for any potential interactions with other parts of the btrfs filesystem. This could be where the magic happens, or it could be a small component of a larger picture. It's time to see what we're working with here.
|
| 59/2 |
2026/08/09 03:30 |
tool |
read-file |
0m
Args: map[File:fs/btrfs/subpage.c FirstLine:750 LineCount:50]
Results: map[Contents: 750: if (unlikely(!fs_info) || \
751: !btrfs_is_subpage(fs_info, folio)) \
752: return folio_test_func(folio); \
753: btrfs_subpage_clamp_range(folio, &start, &len); \
754: return btrfs_subpage_test_##name(fs_info, folio, start, len); \
755: } \
756: void btrfs_meta_folio_set_##name(struct folio *folio, const struct extent_buffer *eb) \
757: { \
758: if (!btrfs_meta_is_subpage(eb->fs_info)) { \
759: folio_set_func(folio); \
760: return; \
761: } \
762: btrfs_subpage_set_##name(eb->fs_info, folio, eb->start, eb->len); \
763: } \
764: void btrfs_meta_folio_clear_##name(struct folio *folio, const struct extent_buffer *eb) \
765: { \
766: if (!btrfs_meta_is_subpage(eb->fs_info)) { \
767: folio_clear_func(folio); \
768: return; \
769: } \
770: btrfs_subpage_clear_##name(eb->fs_info, folio, eb->start, eb->len); \
771: } \
772: bool btrfs_meta_folio_test_##name(struct folio *folio, const struct extent_buffer *eb) \
773: { \
774: if (!btrfs_meta_is_subpage(eb->fs_info)) \
775: return folio_test_func(folio); \
776: return btrfs_subpage_test_##name(eb->fs_info, folio, eb->start, eb->len); \
777: }
778: IMPLEMENT_BTRFS_PAGE_OPS(uptodate, folio_mark_uptodate, folio_clear_uptodate,
779: folio_test_uptodate);
780: IMPLEMENT_BTRFS_PAGE_OPS(dirty, btrfs_folio_mark_dirty_reserved,
781: folio_clear_dirty_for_io, folio_test_dirty);
782: IMPLEMENT_BTRFS_PAGE_OPS(writeback, folio_start_writeback, folio_end_writeback,
783: folio_test_writeback);
784:
785: #define DEFINE_GET_SUBPAGE_BITMAP(name) \
786: static inline unsigned long get_bitmap_value_##name( \
787: const struct btrfs_fs_info *fs_info, \
788: struct folio *folio) \
789: { \
790: const unsigned int __bpf = btrfs_blocks_per_folio(fs_info, folio); \
791: const struct btrfs_folio_state *__bfs = folio_get_private(folio); \
792: unsigned long value; \
793: \
794: ASSERT(__bpf <= BITS_PER_LONG); \
795: value = bitmap_read(__bfs->bitmaps, __bpf * btrfs_bitmap_nr_##name, \
796: __bpf); \
797: return value; \
798: } \
799: static inline const unsigned long *get_bitmap_pointer_##name( \
]
|
| 60/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 51804
output: 44
thoughts: 543
Thoughts: **Analyzing Btrfs Dirty Page Handling and Ordered Extents**
Okay, let's break this down. If the block size matches the page size, `btrfs_folio_set_dirty` simplifies down to `folio_mark_dirty`, and `btrfs_truncate_block` is dealing with whole pages. Since we're page aligned, this operation is occurring outside the range we care about. No problem there.
But, if the block size is smaller, we're in subpage mode. `btrfs_folio_set_dirty` calls `btrfs_subpage_set_dirty`, which sets dirty bits in a bitmap for that subpage, and then calls `folio_mark_dirty`. The `writepages` function will then only write the dirty blocks based on this bitmap. This is good; it avoids writing unneeded data. So we're not creating ordered extents for non-dirty blocks. Excellent.
Now, that concurrent `madvise(MADV_REMOVE)` scenario... Both `madvise` operations serialize through `inode_lock(inode)` in `vfs_fallocate`, so that's handled. We're safe there.
The question of ordered extents created by `btrfs_wait_ordered_range` itself is more interesting. This function calls `btrfs_fdatawrite_range` which flushes delalloc and creates ordered extents. Then it calls `filemap_fdatawait_range` and waits. But does that wait function *actually* wait for ordered extents? I need to know!
Ah, the code has a loop calling `btrfs_lookup_first_ordered_extent` and then `btrfs_start_ordered_extent`. *That* function is what waits for the ordered extent to complete! So `btrfs_wait_ordered_range` does properly wait. I should double check what that `btrfs_start_ordered_extent` call does in detail. This ensures that when the function returns, all of the ordered writes are done.
|
| 61/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:btrfs_start_ordered_extent]
Results: map[SourceCode: 201: static inline void btrfs_start_ordered_extent(struct btrfs_ordered_extent *entry)
202: {
203: return btrfs_start_ordered_extent_nowriteback(entry, 0, 0);
204: }
SourceFile:fs/btrfs/ordered-data.h]
|
| 62/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 52943
output: 48
thoughts: 19
Thoughts: Here's a summary of my thought process, as requested:
**Diving into `btrfs_start_ordered_extent_nowriteback`**
Okay, time to dig into this `btrfs_start_ordered_extent_nowriteback` function. I see it mentioned and, given my experience, I immediately start running through its potential roles in my head. I'm assuming this is related to Btrfs, obviously, and my initial guesses are around handling data ordering and extent management, specifically in situations where writeback might be undesirable or problematic. The "no writeback" aspect is particularly interesting, as it immediately highlights potential performance implications or perhaps a focus on data integrity in a specific context. I need to understand its relationship to other core Btrfs functions like the metadata tree, transaction management, and how it interacts with the underlying block device. I'll need to look at its call sites to determine precisely where and why it's invoked. The overall goal is likely to optimize operations or ensure data consistency during certain Btrfs file system processes. I expect that I need to investigate the functionβs arguments and internal logic to fully grasp its operational flow and its significance in ensuring Btrfsβs operational consistency and efficiency.
|
| 63/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:btrfs_start_ordered_extent_nowriteback]
Results: map[SourceCode: 860: /*
861: * Start IO and wait for a given ordered extent to finish.
862: *
863: * Wait on page writeback for all the pages in the extent but not in
864: * [@nowriteback_start, @nowriteback_start + @nowriteback_len) and the
865: * IO completion code to insert metadata into the btree corresponding to the extent.
866: */
867: void btrfs_start_ordered_extent_nowriteback(struct btrfs_ordered_extent *entry,
868: u64 nowriteback_start, u32 nowriteback_len)
869: {
870: u64 start = entry->file_offset;
871: u64 end = start + entry->num_bytes - 1;
872: struct btrfs_inode *inode = entry->inode;
873: bool freespace_inode;
874:
875: trace_btrfs_ordered_extent_start(inode, entry);
876:
877: /*
878: * If this is a free space inode do not take the ordered extents lockdep
879: * map.
880: */
881: freespace_inode = btrfs_is_free_space_inode(inode);
882:
883: /*
884: * pages in the range can be dirty, clean or writeback. We
885: * start IO on any dirty ones so the wait doesn't stall waiting
886: * for the flusher thread to find them
887: */
888: if (!test_bit(BTRFS_ORDERED_DIRECT, &entry->flags)) {
889: if (!nowriteback_len) {
890: filemap_fdatawrite_range(inode->vfs_inode.i_mapping, start, end);
891: } else {
892: if (start < nowriteback_start)
893: filemap_fdatawrite_range(inode->vfs_inode.i_mapping, start,
894: nowriteback_start - 1);
895: if (nowriteback_start + nowriteback_len < end)
896: filemap_fdatawrite_range(inode->vfs_inode.i_mapping,
897: nowriteback_start + nowriteback_len,
898: end);
899: }
900: }
901:
902: if (!freespace_inode)
903: btrfs_might_wait_for_event(inode->root->fs_info, btrfs_ordered_extent);
904: wait_event(entry->wait, test_bit(BTRFS_ORDERED_COMPLETE, &entry->flags));
905: }
SourceFile:fs/btrfs/ordered-data.c]
|
| 64/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 54098
output: 46
thoughts: 329
Thoughts: **Understanding Btrfs Ordered Extent Processing**
Okay, let me break this down. My initial understanding was that `btrfs_start_ordered_extent` blocks until `BTRFS_ORDERED_COMPLETE` is signaled, so it waits for a specific ordered extent to finish its processing. Similarly, `btrfs_wait_ordered_range` seemed to wait for all ordered extents within a specified range to complete. The inconsistency started to appear here. If that were the case, how could an ordered extent *exist* within a range that's supposedly being waited upon?
Ah, right, now I see it. It's a loop. `btrfs_wait_ordered_range` doesn't just wait passively; it actively *iterates* over the ordered extents within the given range. The provided C code snippet is the key:
```c
end = orig_end;
while (1) {
ordered = btrfs_lookup_first_ordered_extent(inode, end);
if (!ordered)
break;
if (ordered->file_offset > orig_end) {
btrfs_put_ordered_extent(ordered);
break;
}
if (ordered->file_offset + ordered->num_bytes <= start) {
btrfs_put_ordered_extent(ordered);
break;
}
btrfs_start_ordered_extent(ordered);
end = ordered->file_offset;
...
if (end == 0 || end == start)
break;
end--;
}
```
The crucial part is the call to `btrfs_lookup_first_ordered_extent(inode, end)`. That's where I need to look closer. Okay, no, wait... let me re-think what `btrfs_lookup_first_ordered_extent` does. It's looking for the first ordered extent that overlaps with the range `[0, end]`. Now, am I making a mistake on how `btrfs_lookup_first_ordered_extent` works? I need to clarify what it's really doing.
|
| 65/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:btrfs_lookup_first_ordered_extent]
Results: map[SourceCode:1067: /*
1068: * lookup and return any extent before 'file_offset'. NULL is returned
1069: * if none is found
1070: */
1071: struct btrfs_ordered_extent *
1072: btrfs_lookup_first_ordered_extent(struct btrfs_inode *inode, u64 file_offset)
1073: {
1074: struct rb_node *node;
1075: struct btrfs_ordered_extent *entry = NULL;
1076:
1077: spin_lock(&inode->ordered_tree_lock);
1078: node = ordered_tree_search(inode, file_offset);
1079: if (!node)
1080: goto out;
1081:
1082: entry = rb_entry(node, struct btrfs_ordered_extent, rb_node);
1083: refcount_inc(&entry->refs);
1084: trace_btrfs_ordered_extent_lookup_first(inode, entry);
1085: out:
1086: spin_unlock(&inode->ordered_tree_lock);
1087: return entry;
1088: }
SourceFile:fs/btrfs/ordered-data.c]
|
| 66/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 55320
output: 40
thoughts: 11
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm an expert in this field, presented as a first-person summary:
**Checking `ordered_tree_search`**
Alright, time to dive in. My mind immediately goes to the `ordered_tree_search` implementation. I need to refresh my understanding of how that particular algorithm functions and what assumptions it makes. I want to quickly evaluate its core logic: the ordering criteria, the branching strategy, and the pruning techniques employed. Are there any potential bottlenecks I should anticipate, given the scale or nature of the problem I'm working on? What about its space complexity? I need to ensure its still optimal. I will check for any known limitations or edge cases, maybe I made a note of something the last time I considered it. Let's see if anything stands out immediately, and then I'll delve deeper if necessary.
|
| 67/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/ordered-data.c Name:ordered_tree_search]
Results: map[SourceCode: 123: /*
124: * look find the first ordered struct that has this offset, otherwise
125: * the first one less than this offset
126: */
127: static inline struct rb_node *ordered_tree_search(struct btrfs_inode *inode,
128: u64 file_offset)
129: {
130: struct rb_node *prev = NULL;
131: struct rb_node *ret;
132: struct btrfs_ordered_extent *entry;
133:
134: if (inode->ordered_tree_last) {
135: entry = rb_entry(inode->ordered_tree_last, struct btrfs_ordered_extent,
136: rb_node);
137: if (in_range(file_offset, entry->file_offset, entry->num_bytes))
138: return inode->ordered_tree_last;
139: }
140: ret = __tree_search(&inode->ordered_tree, file_offset, &prev);
141: if (!ret)
142: ret = prev;
143: if (ret)
144: inode->ordered_tree_last = ret;
145: return ret;
146: }
SourceFile:fs/btrfs/ordered-data.c]
|
| 68/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 55958
output: 41
thoughts: 697
Thoughts: **Analyzing the Ordered Extent Search**
Okay, let's break this down. The core of this is how `ordered_tree_search` functions. It's supposed to return the first ordered extent that *matches* `file_offset`, but if it doesn't find a direct match, it'll return the one *less* than `file_offset`. That little detail about "less than" β that's the key. That means it can search *backwards*.
Now, looking at `btrfs_wait_ordered_range`, I can see how that backwards search works in practice. We initialize `end` to `orig_end` and then loop. In each iteration, we look for an ordered extent at or before `end`. If we find one (`ordered`), we check a couple of things:
1. If `ordered->file_offset` is greater than `orig_end`, we put it and break. This handles the case where there's an extent *after* the range we're interested in.
2. If `ordered` is strictly *before* `start`, we put it and break.
3. Otherwise, we process the extent by starting it and then decrementing `end` to `ordered->file_offset`.
So, it's pretty clear that it *is* iterating backwards from `orig_end` down toward `start`. We're effectively walking down the tree.
My immediate thought was: What if there's an ordered extent created *after* `orig_end`? But `ordered_tree_search` won't return it because of the "less than" behavior. It will give us the one at or before `orig_end`, as expected. So, it *should* correctly iterate backwards.
Now, here's where it gets interesting: what if an ordered extent gets created *during* our wait? Specifically, imagine that we're waiting for an extent at `end = 1000`. And then, someone creates one at `2000`. We won't see it because we're moving *backwards* by decrementing `end` in each iteration! That would be a race condition.
But who *can* create an ordered extent? I was thinking, we're holding the VFS lock, so *nobody* should be able to. However... then I remembered `btrfs_fdatawrite_range`. This is where the file system writes data back for the range. We start the writeback, then `filemap_fdatawait_range` waits for it to complete.
Now, `filemap_fdatawait_range` isn't waiting for the *writeback* itself. It's waiting for the *pages* to be written back! And, when a page is written back, that's what creates an ordered extent, right? So, logically, all the ordered extents for a given range should be created *before* `filemap_fdatawait_range` returns. Therefore, our backward search *should* find them, because no ordered extents should be created *during* our search.
Is that correct? I think I need to check `btrfs_writepage` now to be sure of that.
|
| 69/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:fs/btrfs/extent_io.c Name:btrfs_writepage]
Results: map[SourceCode: SourceFile:]
|
| 70/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 57410
output: 43
thoughts: 16
Thoughts: Okay, let's dive into this. Here's my thought process on checking `btrfs_run_delalloc_range`, tailored for an expert like yourself:
**Diving into `btrfs_run_delalloc_range`**
Alright, time to refresh my memory and scrutinize `btrfs_run_delalloc_range`. I know this function is critical for handling delayed allocation and ensuring data integrity and performance within a Btrfs filesystem. My immediate focus is going to be on:
* **Understanding the Context:** Where does this function fit within the larger data path? Is it called directly after writeback, during transaction commit, or somewhere else? I need to quickly recall the surrounding functions and data structures.
* **Delalloc Management:** What exactly is the function doing with the delalloc ranges? Is it merging ranges, flushing them to disk, updating metadata, or some combination of these? I need to analyze how itβs interacting with the block groups, extent trees, and superblocks.
* **Error Handling:** What's the error handling like in this function? Are there checks for insufficient space, corrupted extents, or other potential issues? How are errors signaled, and what's the recovery mechanism? I'll want to check for potential race conditions or atomicity issues too.
* **Performance Implications:** Are there any obvious performance bottlenecks in this function? Does it scale well under heavy write loads? I'll be looking for inefficient algorithms or unnecessary locking. Considering how the function is invoked is important.
* **Recent Changes:** I should also check if there have been any recent modifications to this function or the surrounding code. Are there any outstanding patches or bug reports I should be aware of? That will inform my line of inquiry.
* **Data Structures:** Of course, a deep dive into the underlying data structures, particularly the extent trees and block group management structures, is vital. I'll need to understand how the delayed allocation data is tracked and how `btrfs_run_delalloc_range` manipulates them.
My goal is to quickly ascertain its correctness, performance characteristics, and potential areas for optimization or improvement. This initial investigation should quickly point me to areas that need further scrutiny, whether itβs a specific algorithm, a locking strategy, or a specific interaction with other Btrfs components.
|
| 71/2 |
2026/08/09 03:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_run_delalloc_range]
Results: map[SourceCode:2387: /*
2388: * Function to process delayed allocation (create CoW) for ranges which are
2389: * being touched for the first time.
2390: */
2391: int btrfs_run_delalloc_range(struct btrfs_inode *inode, struct folio *locked_folio,
2392: u64 start, u64 end, struct writeback_control *wbc)
2393: {
2394: const bool zoned = btrfs_is_zoned(inode->root->fs_info);
2395:
2396: /*
2397: * The range must cover part of the @locked_folio, or a return of 1
2398: * can confuse the caller.
2399: */
2400: ASSERT(!(end <= folio_pos(locked_folio) ||
2401: start >= folio_next_pos(locked_folio)));
2402:
2403: if (start == 0 && end + 1 <= inode->root->fs_info->sectorsize &&
2404: end + 1 >= inode->disk_i_size) {
2405: int ret;
2406:
2407: ret = run_delalloc_inline(inode, locked_folio);
2408: if (ret < 0)
2409: return ret;
2410: if (ret == 0)
2411: return 1;
2412: /*
2413: * Continue regular handling if we can not create an
2414: * inlined extent.
2415: */
2416: }
2417:
2418: if (should_nocow(inode, start, end))
2419: return run_delalloc_nocow(inode, locked_folio, start, end);
2420:
2421: if (btrfs_inode_can_compress(inode) &&
2422: inode_need_compress(inode, start, end, false) &&
2423: run_delalloc_compressed(inode, locked_folio, start, end, wbc))
2424: return 1;
2425:
2426: if (zoned)
2427: return run_delalloc_cow(inode, locked_folio, start, end, wbc, true);
2428: else
2429: return cow_file_range(inode, locked_folio, start, end, NULL, 0);
2430: }
SourceFile:fs/btrfs/inode.c]
|
| 72/2 |
2026/08/09 03:30 |
llm |
expert |
0m
Model:
Tokens:
input: 58685
output: 39
thoughts: 362
Thoughts: **Delving into Btrfs Writeback and Ordered Extents**
Okay, let's break down this Btrfs writeback process. First, `btrfs_run_delalloc_range` is responsible for creating ordered extents. But wait, digging deeper, I see the path: `run_delalloc_cow` leads to `cow_file_range` and then to `btrfs_add_ordered_extent`. So, it's during the writeback process itself that these ordered extents are actually generated, interesting. The whole thing starts with `filemap_fdatawrite_range`, right? That initiates the writeback, which then calls `writepages`. This, in turn, cascades down to `btrfs_writepages`, which goes to `extent_write_cache_pages`, and finally, we hit `btrfs_run_delalloc_range`.
So, `filemap_fdatawrite_range` is the trigger, submitting the pages for writeback, and in the midst of this, ordered extents get created. But, a crucial question, does `filemap_fdatawrite_range` wait for the completion of these ordered extent creations? No, it just *starts* the writeback, it seems. Then, we have `filemap_fdatawait_range`, which waits for the `PG_writeback` bit to clear. Now, when does this bit get cleared? Initially, I thought it was `btrfs_finish_ordered_io`, but no, the `PG_writeback` flag is cleared when the bio operation completes.
Aha! `btrfs_run_delalloc_range` creates the ordered extent, submits the bio, and the bio completion event is what clears the `PG_writeback` bit. So, by the time `filemap_fdatawait_range` returns, the bio is already done, and the `PG_writeback` bit is cleared. That makes sense.
But wait, hold on a second! What about the case of compressed extents? We're missing something here. In the case of compressed extents, `run_delalloc_compressed` submits the compression work to a workqueue. Okay, I better drill down into that `run_delalloc_compressed` function to understand the implications of this approach.
|
| 73/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:run_delalloc_compressed]
Results: map[SourceCode:1560: static bool run_delalloc_compressed(struct btrfs_inode *inode,
1561: struct folio *locked_folio, u64 start,
1562: u64 end, struct writeback_control *wbc)
1563: {
1564: struct btrfs_fs_info *fs_info = inode->root->fs_info;
1565: struct cgroup_subsys_state *blkcg_css = wbc_blkcg_css(wbc);
1566: struct async_cow *ctx;
1567: struct async_chunk *async_chunk;
1568: unsigned long nr_pages;
1569: u64 num_chunks = DIV_ROUND_UP(end - start, BTRFS_COMPRESSION_CHUNK_SIZE);
1570: int i;
1571: unsigned nofs_flag;
1572: const blk_opf_t write_flags = wbc_to_write_flags(wbc);
1573:
1574: nofs_flag = memalloc_nofs_save();
1575: ctx = kvmalloc_flex(*ctx, chunks, num_chunks);
1576: memalloc_nofs_restore(nofs_flag);
1577: if (!ctx)
1578: return false;
1579:
1580: set_bit(BTRFS_INODE_HAS_ASYNC_EXTENT, &inode->runtime_flags);
1581:
1582: async_chunk = ctx->chunks;
1583: atomic_set(&ctx->num_chunks, num_chunks);
1584:
1585: for (i = 0; i < num_chunks; i++) {
1586: u64 cur_end = min(end, start + BTRFS_COMPRESSION_CHUNK_SIZE - 1);
1587:
1588: /*
1589: * igrab is called higher up in the call chain, take only the
1590: * lightweight reference for the callback lifetime
1591: */
1592: ihold(&inode->vfs_inode);
1593: async_chunk[i].async_cow = ctx;
1594: async_chunk[i].inode = inode;
1595: async_chunk[i].start = start;
1596: async_chunk[i].end = cur_end;
1597: async_chunk[i].write_flags = write_flags;
1598: INIT_LIST_HEAD(&async_chunk[i].extents);
1599:
1600: /*
1601: * The locked_folio comes all the way from writepage and its
1602: * the original folio we were actually given. As we spread
1603: * this large delalloc region across multiple async_chunk
1604: * structs, only the first struct needs a pointer to
1605: * locked_folio.
1606: *
1607: * This way we don't need racey decisions about who is supposed
1608: * to unlock it.
1609: */
1610: if (locked_folio) {
1611: /*
1612: * Depending on the compressibility, the pages might or
1613: * might not go through async. We want all of them to
1614: * be accounted against wbc once. Let's do it here
1615: * before the paths diverge. wbc accounting is used
1616: * only for foreign writeback detection and doesn't
1617: * need full accuracy. Just account the whole thing
1618: * against the first page.
1619: */
1620: wbc_account_cgroup_owner(wbc, locked_folio,
1621: cur_end - start);
1622: async_chunk[i].locked_folio = locked_folio;
1623: locked_folio = NULL;
1624: } else {
1625: async_chunk[i].locked_folio = NULL;
1626: }
1627:
1628: if (blkcg_css != blkcg_root_css) {
1629: css_get(blkcg_css);
1630: async_chunk[i].blkcg_css = blkcg_css;
1631: async_chunk[i].write_flags |= REQ_BTRFS_CGROUP_PUNT;
1632: } else {
1633: async_chunk[i].blkcg_css = NULL;
1634: }
1635:
1636: btrfs_init_work(&async_chunk[i].work, compress_file_range,
1637: submit_compressed_extents);
1638:
1639: nr_pages = DIV_ROUND_UP(cur_end - start, PAGE_SIZE);
1640: atomic_add(nr_pages, &fs_info->async_delalloc_pages);
1641:
1642: btrfs_queue_work(fs_info->delalloc_workers, &async_chunk[i].work);
1643:
1644: start = cur_end + 1;
1645: }
1646: return true;
1647: }
SourceFile:fs/btrfs/inode.c]
|
| 74/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 61065
output: 38
thoughts: 305
Thoughts: **Delalloc Flow and `PG_writeback` Synchronization**
Okay, so `run_delalloc_compressed` queues the compressed write work to `fs_info->delalloc_workers` and returns, that much is clear. The key is in how it interacts with `PG_writeback`. I need to be sure the cache pages are correctly handled and synchronized, especially in light of the later `filemap_fdatawait_range` call.
Right, `run_delalloc_compressed` *doesn't* clear `PG_writeback`. And I see `extent_write_cache_pages` *does* set `PG_writeback` before calling `btrfs_run_delalloc_range`. That makes sense. So, the process is: write page, set `PG_writeback`, queue delalloc, the worker thread will eventually create the ordered extent, then submit the bio, *then* clear `PG_writeback`.
Ah, that means the critical synchronization point is `filemap_fdatawait_range`, which will block waiting for `PG_writeback` to clear. When that happens, the ordered extent is effectively created! The bio isn't completed when `PG_writeback` is cleared.
But wait. If `PG_writeback` is cleared *after* the bio completes, then the ordered extent would be created before the bio is even submitted, and that is *before* `PG_writeback` is cleared, and this sequence can't work!
So, by the time `filemap_fdatawait_range` returns, the ordered extent *must* have been created. This is vital, since this call to `filemap_fdatawait_range` ensures data integrity and that must come first.
I need to confirm if there are any subtle race conditions here where `PG_writeback` might be cleared *before* extent creation. I will need to check `compress_file_range` to make sure it doesn't have any odd behaviors.
|
| 75/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:compress_file_range]
Results: map[SourceCode: 843: /*
844: * Work queue call back to started compression on a file and pages.
845: *
846: * This is done inside an ordered work queue, and the compression is spread
847: * across many cpus. The actual IO submission is step two, and the ordered work
848: * queue takes care of making sure that happens in the same order things were
849: * put onto the queue by writepages and friends.
850: *
851: * If this code finds it can't get good compression, it puts an entry onto the
852: * work queue to write the uncompressed bytes. This makes sure that both
853: * compressed inodes and uncompressed inodes are written in the same order that
854: * the flusher thread sent them down.
855: */
856: static void compress_file_range(struct btrfs_work *work)
857: {
858: struct async_chunk *async_chunk =
859: container_of(work, struct async_chunk, work);
860: struct btrfs_inode *inode = async_chunk->inode;
861: struct btrfs_fs_info *fs_info = inode->root->fs_info;
862: struct compressed_bio *cb = NULL;
863: u64 blocksize = fs_info->sectorsize;
864: u64 start = async_chunk->start;
865: u64 end = async_chunk->end;
866: u64 actual_end;
867: u64 i_size;
868: u32 cur_len;
869: int ret = 0;
870: unsigned long total_compressed = 0;
871: unsigned long total_in = 0;
872: int compress_type = fs_info->compress_type;
873: int compress_level = fs_info->compress_level;
874:
875: if (btrfs_is_shutdown(fs_info))
876: goto cleanup_and_bail_uncompressed;
877:
878: inode_should_defrag(inode, start, end, end - start + 1, SZ_16K);
879:
880: /*
881: * We need to call clear_page_dirty_for_io on each page in the range.
882: * Otherwise applications with the file mmap'd can wander in and change
883: * the page contents while we are compressing them.
884: */
885: ret = extent_range_clear_dirty_for_io(inode, start, end);
886:
887: /*
888: * All the folios should have been locked thus no failure.
889: *
890: * And even if some folios are missing, btrfs_compress_bio()
891: * would handle them correctly, so here just do an ASSERT() check for
892: * early logic errors.
893: */
894: ASSERT(ret == 0);
895:
896: /*
897: * We need to save i_size before now because it could change in between
898: * us evaluating the size and assigning it. This is because we lock and
899: * unlock the page in truncate and fallocate, and then modify the i_size
900: * later on.
901: *
902: * The barriers are to emulate READ_ONCE, remove that once i_size_read
903: * does that for us.
904: */
905: barrier();
906: i_size = i_size_read(&inode->vfs_inode);
907: barrier();
908: actual_end = min_t(u64, i_size, end + 1);
909: again:
910: total_in = 0;
911: cur_len = min(end + 1 - start, BTRFS_MAX_UNCOMPRESSED);
912: ret = 0;
913: cb = NULL;
914:
915: /*
916: * we don't want to send crud past the end of i_size through
917: * compression, that's just a waste of CPU time. So, if the
918: * end of the file is before the start of our current
919: * requested range of bytes, we bail out to the uncompressed
920: * cleanup code that can deal with all of this.
921: *
922: * It isn't really the fastest way to fix things, but this is a
923: * very uncommon corner.
924: */
925: if (actual_end <= start)
926: goto cleanup_and_bail_uncompressed;
927:
928: /*
929: * We do compression for mount -o compress and when the inode has not
930: * been flagged as NOCOMPRESS. This flag can change at any time if we
931: * discover bad compression ratios.
932: */
933: if (!inode_need_compress(inode, start, end, false))
934: goto cleanup_and_bail_uncompressed;
935:
936: if (0 < inode->defrag_compress && inode->defrag_compress < BTRFS_NR_COMPRESS_TYPES) {
937: compress_type = inode->defrag_compress;
938: compress_level = inode->defrag_compress_level;
939: } else if (inode->prop_compress) {
940: compress_type = inode->prop_compress;
941: }
942:
943: /* Compression level is applied here. */
944: cb = btrfs_compress_bio(inode, start, cur_len, compress_type,
945: compress_level, async_chunk->write_flags);
946: if (IS_ERR(cb)) {
947: cb = NULL;
948: goto mark_incompressible;
949: }
950:
951: total_compressed = cb->bbio.bio.bi_iter.bi_size;
952: total_in = cur_len;
953:
954: /*
955: * We aren't doing an inline extent. Round the compressed size up to a
956: * block size boundary so the allocator does sane things.
957: */
958: round_up_last_block(cb, blocksize);
959: total_compressed = cb->bbio.bio.bi_iter.bi_size;
960: ASSERT(IS_ALIGNED(total_compressed, blocksize));
961:
962: /*
963: * One last check to make sure the compression is really a win, compare
964: * the page count read with the blocks on disk, compression must free at
965: * least one sector.
966: */
967: total_in = round_up(total_in, fs_info->sectorsize);
968: if (total_compressed + blocksize > total_in)
969: goto mark_incompressible;
970:
971:
972: /*
973: * The async work queues will take care of doing actual allocation on
974: * disk for these compressed pages, and will submit the bios.
975: */
976: ret = add_async_extent(async_chunk, start, total_in, cb);
977: BUG_ON(ret);
978: if (start + total_in < end) {
979: start += total_in;
980: cond_resched();
981: goto again;
982: }
983: return;
984:
985: mark_incompressible:
986: if (!btrfs_test_opt(fs_info, FORCE_COMPRESS) && !inode->prop_compress)
987: inode->flags |= BTRFS_INODE_NOCOMPRESS;
988: cleanup_and_bail_uncompressed:
989: ret = add_async_extent(async_chunk, start, end - start + 1, NULL);
990: BUG_ON(ret);
991: if (cb)
992: cleanup_compressed_bio(cb);
993: }
SourceFile:fs/btrfs/inode.c]
|
| 76/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 64271
output: 39
thoughts: 62
Thoughts: **Analyzing the File Compression Workflow**
Okay, so I'm following the file compression process here. It looks like `compress_file_range` is the starting point. My initial thought is that it's the core function responsible for, well, compressing a specific range within the file. It's calling `add_async_extent`, which makes sense; this is likely where we gather details about the data extent to be compressed. Crucially, that `add_async_extent` function seems to be appending the extent's details to `async_chunk->extents`. This implies we're batching up the extents somehow, probably for efficiency.
Now, as the second part of the workqueue, `submit_compressed_extents` is invoked. This is where the actual compression and perhaps writing to storage takes place. Given my understanding of how I'd approach this, I need to check `submit_compressed_extents` next. I need to understand how the extents are processed from `async_chunk->extents`, and how the workqueue is interacting with the underlying I/O system, and whether there are any potential bottlenecks there. That's the key to understanding the performance characteristics of this entire process.
|
| 77/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:submit_compressed_extents]
Results: map[SourceCode:1514: /*
1515: * Phase two of compressed writeback. This is the ordered portion of the code,
1516: * which only gets called in the order the work was queued. We walk all the
1517: * async extents created by compress_file_range and send them down to the disk.
1518: *
1519: * If called with @do_free == true then it'll try to finish the work and free
1520: * the work struct eventually.
1521: */
1522: static noinline void submit_compressed_extents(struct btrfs_work *work, bool do_free)
1523: {
1524: struct async_chunk *async_chunk = container_of(work, struct async_chunk,
1525: work);
1526: struct btrfs_fs_info *fs_info = btrfs_work_owner(work);
1527: struct async_extent *async_extent;
1528: unsigned long nr_pages;
1529: u64 alloc_hint = 0;
1530:
1531: if (do_free) {
1532: struct async_cow *async_cow;
1533:
1534: btrfs_add_delayed_iput(async_chunk->inode);
1535: if (async_chunk->blkcg_css)
1536: css_put(async_chunk->blkcg_css);
1537:
1538: async_cow = async_chunk->async_cow;
1539: if (atomic_dec_and_test(&async_cow->num_chunks))
1540: kvfree(async_cow);
1541: return;
1542: }
1543:
1544: nr_pages = (async_chunk->end - async_chunk->start + PAGE_SIZE) >>
1545: PAGE_SHIFT;
1546:
1547: while (!list_empty(&async_chunk->extents)) {
1548: async_extent = list_first_entry(&async_chunk->extents,
1549: struct async_extent, list);
1550: list_del(&async_extent->list);
1551: submit_one_async_extent(async_chunk, async_extent, &alloc_hint);
1552: }
1553:
1554: /* atomic_sub_return implies a barrier */
1555: if (atomic_sub_return(nr_pages, &fs_info->async_delalloc_pages) <
1556: 5 * SZ_1M)
1557: cond_wake_up_nomb(&fs_info->async_submit_wait);
1558: }
SourceFile:fs/btrfs/inode.c]
|
| 78/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 65415
output: 40
thoughts: 18
Thoughts: Here's a summary of my thinking, expressed in the first person:
**My Internal Processing of Extent Submissions**
Okay, so I'm looking at how we're actually submitting extents. The core of this operation seems to be built around a specific function. I can see that `submit_compressed_extents` is essentially a wrapper, a higher-level function, for handling the submission of our compressed data extents. It's the point of entry for this whole process. Now, within `submit_compressed_extents`, the actual, low-level work of queuing up the extent submission is delegated to `submit_one_async_extent`. So the first function's role is probably to orchestrate the process and apply some logic (compression perhaps) before the real submission happens. I'll need to look closely at `submit_one_async_extent` to understand exactly how the extent is built and handled asynchronously. Understanding the relationship here - the delegation of work - will be crucial to optimizing the overall extent submission pipeline.
|
| 79/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:submit_one_async_extent]
Results: map[SourceCode:1024: static void submit_one_async_extent(struct async_chunk *async_chunk,
1025: struct async_extent *async_extent,
1026: u64 *alloc_hint)
1027: {
1028: struct btrfs_inode *inode = async_chunk->inode;
1029: struct extent_io_tree *io_tree = &inode->io_tree;
1030: struct btrfs_root *root = inode->root;
1031: struct btrfs_fs_info *fs_info = root->fs_info;
1032: struct btrfs_ordered_extent *ordered;
1033: struct btrfs_file_extent file_extent;
1034: struct btrfs_key ins;
1035: struct folio *locked_folio = NULL;
1036: struct extent_state *cached = NULL;
1037: struct extent_map *em;
1038: int ret = 0;
1039: u32 compressed_size;
1040: u64 start = async_extent->start;
1041: u64 end = async_extent->start + async_extent->ram_size - 1;
1042:
1043: if (async_chunk->blkcg_css)
1044: kthread_associate_blkcg(async_chunk->blkcg_css);
1045:
1046: /*
1047: * If async_chunk->locked_folio is in the async_extent range, we need to
1048: * handle it.
1049: */
1050: if (async_chunk->locked_folio) {
1051: u64 locked_folio_start = folio_pos(async_chunk->locked_folio);
1052: u64 locked_folio_end = locked_folio_start +
1053: folio_size(async_chunk->locked_folio) - 1;
1054:
1055: if (!(start >= locked_folio_end || end <= locked_folio_start))
1056: locked_folio = async_chunk->locked_folio;
1057: }
1058:
1059: if (!async_extent->cb) {
1060: submit_uncompressed_range(inode, async_extent, locked_folio);
1061: goto done;
1062: }
1063:
1064: compressed_size = async_extent->cb->bbio.bio.bi_iter.bi_size;
1065: ret = btrfs_reserve_extent(root, async_extent->ram_size,
1066: compressed_size, compressed_size,
1067: 0, *alloc_hint, &ins, true, true);
1068: if (ret) {
1069: /*
1070: * We can't reserve contiguous space for the compressed size.
1071: * Unlikely, but it's possible that we could have enough
1072: * non-contiguous space for the uncompressed size instead. So
1073: * fall back to uncompressed.
1074: */
1075: submit_uncompressed_range(inode, async_extent, locked_folio);
1076: cleanup_compressed_bio(async_extent->cb);
1077: async_extent->cb = NULL;
1078: goto done;
1079: }
1080:
1081: btrfs_lock_extent(io_tree, start, end, &cached);
1082:
1083: /* Here we're doing allocation and writeback of the compressed pages */
1084: file_extent.disk_bytenr = ins.objectid;
1085: file_extent.disk_num_bytes = ins.offset;
1086: file_extent.ram_bytes = async_extent->ram_size;
1087: file_extent.num_bytes = async_extent->ram_size;
1088: file_extent.offset = 0;
1089: file_extent.compression = async_extent->cb->compress_type;
1090:
1091: async_extent->cb->bbio.bio.bi_iter.bi_sector = ins.objectid >> SECTOR_SHIFT;
1092:
1093: em = btrfs_create_io_em(inode, start, &file_extent, BTRFS_ORDERED_COMPRESSED);
1094: if (IS_ERR(em)) {
1095: ret = PTR_ERR(em);
1096: goto out_free_reserve;
1097: }
1098: btrfs_free_extent_map(em);
1099:
1100: ordered = btrfs_alloc_ordered_extent(inode, start, &file_extent,
1101: 1U << BTRFS_ORDERED_COMPRESSED);
1102: if (IS_ERR(ordered)) {
1103: btrfs_drop_extent_map_range(inode, start, end, false);
1104: ret = PTR_ERR(ordered);
1105: goto out_free_reserve;
1106: }
1107: async_extent->cb->bbio.ordered = ordered;
1108: btrfs_dec_block_group_reservations(fs_info, ins.objectid);
1109:
1110: /* Clear dirty, set writeback and unlock the pages. */
1111: extent_clear_unlock_delalloc(inode, start, end,
1112: NULL, &cached, EXTENT_LOCKED | EXTENT_DELALLOC,
1113: PAGE_UNLOCK | PAGE_START_WRITEBACK);
1114: btrfs_submit_bbio(&async_extent->cb->bbio, 0);
1115: async_extent->cb = NULL;
1116:
1117: *alloc_hint = ins.objectid + ins.offset;
1118: done:
1119: if (async_chunk->blkcg_css)
1120: kthread_associate_blkcg(NULL);
1121: kfree(async_extent);
1122: return;
1123:
1124: out_free_reserve:
1125: btrfs_dec_block_group_reservations(fs_info, ins.objectid);
1126: btrfs_free_reserved_extent(fs_info, ins.objectid, ins.offset, true);
1127: mapping_set_error(inode->vfs_inode.i_mapping, -EIO);
1128: extent_clear_unlock_delalloc(inode, start, end,
1129: NULL, &cached,
1130: EXTENT_LOCKED | EXTENT_DELALLOC |
1131: EXTENT_DELALLOC_NEW |
1132: EXTENT_DEFRAG | EXTENT_CLEAR_META_RESV,
1133: PAGE_UNLOCK | PAGE_START_WRITEBACK |
1134: PAGE_END_WRITEBACK);
1135: if (async_extent->cb)
1136: cleanup_compressed_bio(async_extent->cb);
1137: if (async_chunk->blkcg_css)
1138: kthread_associate_blkcg(NULL);
1139: btrfs_debug(fs_info,
1140: "async extent submission failed root=%lld inode=%llu start=%llu len=%llu ret=%d",
1141: btrfs_root_id(root), btrfs_ino(inode), start,
1142: async_extent->ram_size, ret);
1143: kfree(async_extent);
1144: }
SourceFile:fs/btrfs/inode.c]
|
| 80/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 67853
output: 42
thoughts: 202
Thoughts: **Delving into the Btrfs Page Writeback Process**
Okay, so I'm tracing the writeback path here, and it looks like `submit_one_async_extent` is the function that ultimately kicks off the ordered extent creation using `btrfs_alloc_ordered_extent`. That makes sense. The key is in the sequencing: it creates the ordered extent and *then* the code sets `PAGE_START_WRITEBACK`.
But that placement of `PAGE_START_WRITEBACK` is making me pause. If it's being set *here*, right after the extent allocation, what state were the pages in *before* that point? My immediate thought is, the page must have been considered "dirty," but not actively in a writeback operation yet. This means it was waiting.
Now, wait a minute... I remember seeing `extent_write_cache_pages` set `PG_writeback` earlier in the process *before* calling `btrfs_run_delalloc_range`. That would be where this state originates from. I should check that code path again. I need to be certain about the flow, and what functions are responsible for what state transition. It seems like the state transition is not as straightforward as I initially thought.
|
| 81/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/extent_io.c Name:extent_write_cache_pages]
Results: map[SourceCode:2566: /*
2567: * Walk the list of dirty pages of the given address space and write all of them.
2568: *
2569: * @mapping: address space structure to write
2570: * @wbc: subtract the number of written pages from *@wbc->nr_to_write
2571: * @bio_ctrl: holds context for the write, namely the bio
2572: *
2573: * If a page is already under I/O, write_cache_pages() skips it, even
2574: * if it's dirty. This is desirable behaviour for memory-cleaning writeback,
2575: * but it is INCORRECT for data-integrity system calls such as fsync(). fsync()
2576: * and msync() need to guarantee that all the data which was dirty at the time
2577: * the call was made get new I/O started against them. If wbc->sync_mode is
2578: * WB_SYNC_ALL then we were called for data integrity and we must wait for
2579: * existing IO to complete.
2580: */
2581: static int extent_write_cache_pages(struct address_space *mapping,
2582: struct btrfs_bio_ctrl *bio_ctrl)
2583: {
2584: struct writeback_control *wbc = bio_ctrl->wbc;
2585: struct inode *inode = mapping->host;
2586: int ret = 0;
2587: bool done = false;
2588: int nr_to_write_done = 0;
2589: struct folio_batch fbatch;
2590: unsigned int nr_folios;
2591: pgoff_t index;
2592: pgoff_t end; /* Inclusive */
2593: pgoff_t done_index;
2594: bool range_whole = false;
2595: bool scanned = false;
2596: xa_mark_t tag;
2597:
2598: /*
2599: * We have to hold onto the inode so that ordered extents can do their
2600: * work when the IO finishes. The alternative to this is failing to add
2601: * an ordered extent if the igrab() fails there and that is a huge pain
2602: * to deal with, so instead just hold onto the inode throughout the
2603: * writepages operation. If it fails here we are freeing up the inode
2604: * anyway and we'd rather not waste our time writing out stuff that is
2605: * going to be truncated anyway.
2606: */
2607: if (!igrab(inode))
2608: return 0;
2609:
2610: folio_batch_init(&fbatch);
2611: if (wbc->range_cyclic) {
2612: index = mapping->writeback_index; /* Start from prev offset */
2613: end = -1;
2614: /*
2615: * Start from the beginning does not need to cycle over the
2616: * range, mark it as scanned.
2617: */
2618: scanned = (index == 0);
2619: } else {
2620: index = wbc->range_start >> PAGE_SHIFT;
2621: end = wbc->range_end >> PAGE_SHIFT;
2622: if (wbc->range_start == 0 && wbc->range_end == LLONG_MAX)
2623: range_whole = true;
2624: scanned = true;
2625: }
2626:
2627: /*
2628: * We do the tagged writepage as long as the snapshot flush bit is set
2629: * and we are the first one who do the filemap_flush() on this inode.
2630: *
2631: * The nr_to_write == LONG_MAX is needed to make sure other flushers do
2632: * not race in and drop the bit.
2633: */
2634: if (range_whole && wbc->nr_to_write == LONG_MAX &&
2635: test_and_clear_bit(BTRFS_INODE_SNAPSHOT_FLUSH,
2636: &BTRFS_I(inode)->runtime_flags))
2637: wbc->tagged_writepages = 1;
2638:
2639: tag = wbc_to_tag(wbc);
2640: retry:
2641: if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)
2642: tag_pages_for_writeback(mapping, index, end);
2643: done_index = index;
2644: while (!done && !nr_to_write_done && (index <= end) &&
2645: (nr_folios = filemap_get_folios_tag(mapping, &index,
2646: end, tag, &fbatch))) {
2647: unsigned i;
2648:
2649: for (i = 0; i < nr_folios; i++) {
2650: struct folio *folio = fbatch.folios[i];
2651:
2652: done_index = folio_next_index(folio);
2653: /*
2654: * At this point we hold neither the i_pages lock nor
2655: * the folio lock: the folio may be truncated or
2656: * invalidated (changing folio->mapping to NULL).
2657: */
2658: if (!folio_trylock(folio)) {
2659: submit_write_bio(bio_ctrl, 0);
2660: folio_lock(folio);
2661: }
2662:
2663: if (unlikely(folio->mapping != mapping)) {
2664: folio_unlock(folio);
2665: continue;
2666: }
2667:
2668: if (!folio_test_dirty(folio)) {
2669: /* Someone wrote it for us. */
2670: folio_unlock(folio);
2671: continue;
2672: }
2673:
2674: /*
2675: * For subpage case, compression can lead to mixed
2676: * writeback and dirty flags, e.g:
2677: * 0 32K 64K 96K 128K
2678: * | |//////||/////| |//|
2679: *
2680: * In above case, [32K, 96K) is asynchronously submitted
2681: * for compression, and [124K, 128K) needs to be written back.
2682: *
2683: * If we didn't wait writeback for page 64K, [128K, 128K)
2684: * won't be submitted as the page still has writeback flag
2685: * and will be skipped in the next check.
2686: *
2687: * This mixed writeback and dirty case is only possible for
2688: * subpage case.
2689: *
2690: * TODO: Remove this check after migrating compression to
2691: * regular submission.
2692: */
2693: if (wbc->sync_mode != WB_SYNC_NONE ||
2694: btrfs_is_subpage(inode_to_fs_info(inode), folio)) {
2695: if (folio_test_writeback(folio))
2696: submit_write_bio(bio_ctrl, 0);
2697: folio_wait_writeback(folio);
2698: }
2699:
2700: if (folio_test_writeback(folio) ||
2701: !folio_test_dirty(folio)) {
2702: folio_unlock(folio);
2703: continue;
2704: }
2705:
2706: ret = extent_writepage(folio, bio_ctrl);
2707: if (ret < 0) {
2708: done = true;
2709: break;
2710: }
2711:
2712: /*
2713: * The filesystem may choose to bump up nr_to_write.
2714: * We have to make sure to honor the new nr_to_write
2715: * at any time.
2716: */
2717: nr_to_write_done = (wbc->sync_mode == WB_SYNC_NONE &&
2718: wbc->nr_to_write <= 0);
2719: }
2720: folio_batch_release(&fbatch);
2721: cond_resched();
2722: }
2723: if (!scanned && !done) {
2724: /*
2725: * We hit the last page and there is more work to be done: wrap
2726: * back to the start of the file
2727: */
2728: scanned = true;
2729: index = 0;
2730:
2731: /*
2732: * If we're looping we could run into a page that is locked by a
2733: * writer and that writer could be waiting on writeback for a
2734: * page in our current bio, and thus deadlock, so flush the
2735: * write bio here.
2736: */
2737: submit_write_bio(bio_ctrl, 0);
2738: goto retry;
2739: }
2740:
2741: if (wbc->range_cyclic || (wbc->nr_to_write > 0 && range_whole))
2742: mapping->writeback_index = done_index;
2743:
2744: btrfs_add_delayed_iput(BTRFS_I(inode));
2745: return ret;
2746: }
SourceFile:fs/btrfs/extent_io.c]
|
| 82/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 71222
output: 39
thoughts: 27
Thoughts: Here's a summary of my thought process:
**Diving into the Write Cache**
Okay, so `extent_write_cache_pages` is kicking things off. My initial thought is that it's calling `extent_writepage`, and I need to understand what that function is doing. Let's dig in and examine the inner workings of `extent_writepage`. Let's see how the write cache pages are being handled.
|
| 83/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/extent_io.c Name:extent_writepage]
Results: map[SourceCode:1985: /*
1986: * the writepage semantics are similar to regular writepage. extent
1987: * records are inserted to lock ranges in the tree, and as dirty areas
1988: * are found, they are marked writeback. Then the lock bits are removed
1989: * and the end_io handler clears the writeback ranges
1990: *
1991: * Return 0 if everything goes well.
1992: * Return <0 for error.
1993: */
1994: static int extent_writepage(struct folio *folio, struct btrfs_bio_ctrl *bio_ctrl)
1995: {
1996: struct btrfs_inode *inode = BTRFS_I(folio->mapping->host);
1997: struct btrfs_fs_info *fs_info = inode->root->fs_info;
1998: int ret;
1999: size_t pg_offset;
2000: loff_t i_size = i_size_read(&inode->vfs_inode);
2001: const pgoff_t end_index = i_size >> PAGE_SHIFT;
2002: const unsigned int blocks_per_folio = btrfs_blocks_per_folio(fs_info, folio);
2003:
2004: trace_extent_writepage(folio, &inode->vfs_inode, bio_ctrl->wbc);
2005:
2006: WARN_ON(!folio_test_locked(folio));
2007:
2008: pg_offset = offset_in_folio(folio, i_size);
2009: if (folio->index > end_index ||
2010: (folio->index == end_index && !pg_offset)) {
2011: folio_invalidate(folio, 0, folio_size(folio));
2012: folio_unlock(folio);
2013: return 0;
2014: }
2015:
2016: if (folio_contains(folio, end_index))
2017: folio_zero_range(folio, pg_offset, folio_size(folio) - pg_offset);
2018:
2019: bio_ctrl_init_submit_bitmap(fs_info, folio, bio_ctrl);
2020: /*
2021: * If the page is dirty but without private set, it's marked dirty
2022: * without informing the fs.
2023: * Nowadays that is a bug, since the introduction of
2024: * pin_user_pages*().
2025: *
2026: * So here we check if the page has private set to rule out such
2027: * case.
2028: */
2029: if (unlikely(!folio_test_private(folio))) {
2030: DEBUG_WARN();
2031: btrfs_err_rl(fs_info,
2032: "root %lld ino %llu folio %llu is marked dirty without notifying the fs",
2033: btrfs_root_id(inode->root),
2034: btrfs_ino(inode), folio_pos(folio));
2035: ret = -EUCLEAN;
2036: goto done;
2037: }
2038:
2039: ret = set_folio_extent_mapped(folio);
2040: if (ret < 0)
2041: goto done;
2042:
2043: ret = writepage_delalloc(inode, folio, bio_ctrl);
2044: if (ret == 1)
2045: return 0;
2046: if (ret)
2047: goto done;
2048:
2049: ret = extent_writepage_io(inode, folio, folio_pos(folio),
2050: folio_size(folio), bio_ctrl, i_size);
2051: if (ret == 1)
2052: return 0;
2053: if (unlikely(ret < 0))
2054: btrfs_err_rl(fs_info,
2055: "failed to submit blocks, root=%lld inode=%llu folio=%llu submit_bitmap=%*pbl: %d",
2056: btrfs_root_id(inode->root), btrfs_ino(inode),
2057: folio_pos(folio), blocks_per_folio,
2058: bio_ctrl->submit_bitmap, ret);
2059:
2060: bio_ctrl->wbc->nr_to_write--;
2061:
2062: done:
2063: if (ret < 0)
2064: mapping_set_error(folio->mapping, ret);
2065: /*
2066: * Only unlock ranges that are submitted. As there can be some async
2067: * submitted ranges inside the folio.
2068: */
2069: btrfs_folio_end_lock_bitmap(fs_info, folio, bio_ctrl->submit_bitmap);
2070: ASSERT(ret <= 0);
2071: return ret;
2072: }
SourceFile:fs/btrfs/extent_io.c]
|
| 84/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 72850
output: 40
thoughts: 26
Thoughts: Here's a summary of my thought process, as if I were analyzing that code snippet:
**Diving into the Writepage Delalloc Function**
Okay, so `extent_writepage` is calling `writepage_delalloc`. Right, let's drill down into `writepage_delalloc` and see what's happening there. I need to understand the specifics of how this function handles the write-back process, especially concerning the de-allocation aspects hinted at by the "delalloc" naming. I'm assuming it has a specific role related to handling allocated blocks during the write process; I'm curious to find out its specific interaction with the page cache, block device, and any potential metadata updates. This looks to be the key to understanding this function and its specific role.
|
| 85/2 |
2026/08/09 03:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/extent_io.c Name:writepage_delalloc]
Results: map[SourceCode:1552: /*
1553: * Do all of the delayed allocation setup.
1554: *
1555: * Return >0 if all the dirty blocks are submitted async (compression) or inlined.
1556: * The @folio should no longer be touched (treat it as already unlocked).
1557: *
1558: * Return 0 if there is still dirty block that needs to be submitted through
1559: * extent_writepage_io().
1560: * bio_ctrl->submit_bitmap will indicate which blocks of the folio should be
1561: * submitted, and @folio is still kept locked.
1562: *
1563: * Return <0 if there is any error hit.
1564: * Any allocated ordered extent range covering this folio will be marked
1565: * finished (IOERR), and @folio is still kept locked.
1566: */
1567: static noinline_for_stack int writepage_delalloc(struct btrfs_inode *inode,
1568: struct folio *folio,
1569: struct btrfs_bio_ctrl *bio_ctrl)
1570: {
1571: struct btrfs_fs_info *fs_info = inode_to_fs_info(&inode->vfs_inode);
1572: struct writeback_control *wbc = bio_ctrl->wbc;
1573: const bool is_subpage = btrfs_is_subpage(fs_info, folio);
1574: const u64 page_start = folio_pos(folio);
1575: const u64 page_end = page_start + folio_size(folio) - 1;
1576: const unsigned int blocks_per_folio = btrfs_blocks_per_folio(fs_info, folio);
1577: unsigned long delalloc_bitmap[BITS_TO_LONGS(BTRFS_MAX_BLOCKS_PER_FOLIO)] = { 0 };
1578: /*
1579: * Save the last found delalloc end. As the delalloc end can go beyond
1580: * page boundary, thus we cannot rely on subpage bitmap to locate the
1581: * last delalloc end.
1582: */
1583: u64 last_delalloc_end = 0;
1584: /*
1585: * The range end (exclusive) of the last successfully finished delalloc
1586: * range.
1587: * Any range covered by ordered extent must either be manually marked
1588: * finished (error handling), or has IO submitted (and finish the
1589: * ordered extent normally).
1590: *
1591: * This records the end of ordered extent cleanup if we hit an error.
1592: */
1593: u64 last_finished_delalloc_end = page_start;
1594: u64 delalloc_start = page_start;
1595: u64 delalloc_end = page_end;
1596: u64 delalloc_to_write = 0;
1597: unsigned int start_bit;
1598: unsigned int end_bit;
1599: int ret = 0;
1600:
1601: /* Save the dirty bitmap as our submission bitmap will be a subset of it. */
1602: btrfs_copy_subpage_dirty_bitmap(fs_info, folio, bio_ctrl->submit_bitmap);
1603:
1604: ret = writepage_fixup(inode, folio, bio_ctrl);
1605: if (ret)
1606: return ret;
1607:
1608: for_each_set_bitrange(start_bit, end_bit, bio_ctrl->submit_bitmap,
1609: blocks_per_folio) {
1610: u64 start = page_start + (start_bit << fs_info->sectorsize_bits);
1611: u32 len = (end_bit - start_bit) << fs_info->sectorsize_bits;
1612:
1613: btrfs_folio_set_lock(fs_info, folio, start, len);
1614: }
1615:
1616: /* Lock all (subpage) delalloc ranges inside the folio first. */
1617: while (delalloc_start < page_end) {
1618: delalloc_end = page_end;
1619: if (!find_lock_delalloc_range(&inode->vfs_inode, folio,
1620: &delalloc_start, &delalloc_end)) {
1621: delalloc_start = delalloc_end + 1;
1622: continue;
1623: }
1624: set_delalloc_bitmap(folio, delalloc_bitmap, delalloc_start,
1625: min(delalloc_end, page_end) + 1 - delalloc_start);
1626: last_delalloc_end = delalloc_end;
1627: delalloc_start = delalloc_end + 1;
1628: }
1629: delalloc_start = page_start;
1630:
1631: if (!last_delalloc_end)
1632: goto out;
1633:
1634: /* Run the delalloc ranges for the above locked ranges. */
1635: while (delalloc_start < page_end) {
1636: u64 found_start;
1637: u32 found_len;
1638: bool found;
1639:
1640: if (!is_subpage) {
1641: /*
1642: * For non-subpage case, the found delalloc range must
1643: * cover this folio and there must be only one locked
1644: * delalloc range.
1645: */
1646: found_start = page_start;
1647: found_len = last_delalloc_end + 1 - found_start;
1648: found = true;
1649: } else {
1650: found = find_next_delalloc_bitmap(folio, delalloc_bitmap,
1651: delalloc_start, &found_start, &found_len);
1652: }
1653: if (!found)
1654: break;
1655: /*
1656: * The subpage range covers the last sector, the delalloc range may
1657: * end beyond the folio boundary, use the saved delalloc_end
1658: * instead.
1659: */
1660: if (found_start + found_len >= page_end)
1661: found_len = last_delalloc_end + 1 - found_start;
1662:
1663: if (ret >= 0) {
1664: /*
1665: * Some delalloc range may be created by previous folios.
1666: * Thus we still need to clean up this range during error
1667: * handling.
1668: */
1669: last_finished_delalloc_end = found_start;
1670: /* No errors hit so far, run the current delalloc range. */
1671: ret = btrfs_run_delalloc_range(inode, folio,
1672: found_start,
1673: found_start + found_len - 1,
1674: wbc);
1675: if (ret >= 0)
1676: last_finished_delalloc_end = found_start + found_len;
1677: if (unlikely(ret < 0))
1678: btrfs_err_rl(fs_info,
1679: "failed to run delalloc range, root=%lld ino=%llu folio=%llu submit_bitmap=%*pbl start=%llu len=%u: %d",
1680: btrfs_root_id(inode->root),
1681: btrfs_ino(inode),
1682: folio_pos(folio),
1683: blocks_per_folio,
1684: bio_ctrl->submit_bitmap,
1685: found_start, found_len, ret);
1686: } else {
1687: /*
1688: * We've hit an error during previous delalloc range,
1689: * have to cleanup the remaining locked ranges.
1690: */
1691: btrfs_unlock_extent(&inode->io_tree, found_start,
1692: found_start + found_len - 1, NULL);
1693: unlock_delalloc_folio(&inode->vfs_inode, folio,
1694: found_start,
1695: found_start + found_len - 1);
1696: }
1697:
1698: /*
1699: * We have some ranges that's going to be submitted asynchronously
1700: * (compression or inline). These range have their own control
1701: * on when to unlock the pages. We should not touch them
1702: * anymore, so clear the range from the submission bitmap.
1703: */
1704: if (ret > 0) {
1705: unsigned int start_bit = (found_start - page_start) >>
1706: fs_info->sectorsize_bits;
1707: unsigned int end_bit = (min(page_end + 1, found_start + found_len) -
1708: page_start) >> fs_info->sectorsize_bits;
1709: bitmap_clear(bio_ctrl->submit_bitmap, start_bit, end_bit - start_bit);
1710: }
1711: /*
1712: * Above btrfs_run_delalloc_range() may have unlocked the folio,
1713: * thus for the last range, we cannot touch the folio anymore.
1714: */
1715: if (found_start + found_len >= last_delalloc_end + 1)
1716: break;
1717:
1718: delalloc_start = found_start + found_len;
1719: }
1720: /*
1721: * It's possible we had some ordered extents created before we hit
1722: * an error, cleanup non-async successfully created delalloc ranges.
1723: */
1724: if (unlikely(ret < 0)) {
1725: unsigned int bitmap_size = min(
1726: (last_finished_delalloc_end - page_start) >>
1727: fs_info->sectorsize_bits,
1728: blocks_per_folio);
1729:
1730: for_each_set_bitrange(start_bit, end_bit, bio_ctrl->submit_bitmap,
1731: bitmap_size) {
1732: u64 start = page_start + (start_bit << fs_info->sectorsize_bits);
1733: u32 len = (end_bit - start_bit) << fs_info->sectorsize_bits;
1734:
1735: btrfs_mark_ordered_io_finished(inode, start, len, false);
1736: }
1737: return ret;
1738: }
1739: out:
1740: if (last_delalloc_end)
1741: delalloc_end = last_delalloc_end;
1742: else
1743: delalloc_end = page_end;
1744: /*
1745: * delalloc_end is already one less than the total length, so
1746: * we don't subtract one from PAGE_SIZE.
1747: */
1748: delalloc_to_write +=
1749: DIV_ROUND_UP(delalloc_end + 1 - page_start, PAGE_SIZE);
1750:
1751: /*
1752: * If all ranges are submitted asynchronously, we just need to account
1753: * for them here.
1754: */
1755: if (bitmap_empty(bio_ctrl->submit_bitmap, blocks_per_folio)) {
1756: wbc->nr_to_write -= delalloc_to_write;
1757: return 1;
1758: }
1759:
1760: if (wbc->nr_to_write < delalloc_to_write) {
1761: int thresh = 8192;
1762:
1763: if (delalloc_to_write < thresh * 2)
1764: thresh = delalloc_to_write;
1765: wbc->nr_to_write = min_t(u64, delalloc_to_write,
1766: thresh);
1767: }
1768:
1769: return 0;
1770: }
SourceFile:fs/btrfs/extent_io.c]
|
| 86/2 |
2026/08/09 03:31 |
llm |
expert |
0m
Model:
Tokens:
input: 76719
output: 43
thoughts: 4911
Thoughts: **Analyzing the Btrfs Async Delalloc and Hole Punching Race Condition**
Okay, so I've been tracing through this Btrfs code, and it seems like there's a nasty race condition when dealing with compressed extents and punching holes in a file. Here's what I've figured out:
`writepage_delalloc` kicks off by calling `btrfs_run_delalloc_range`. If the range is compressed, which means a positive return, it tells `writepage_delalloc` to clear the bitmap. And if the return from `writepage_delalloc` returns 1, that the extent_writepage returns 0. If it returns 0, it *doesn't* call `extent_writepage_io`, so it doesn't set `PAGE_START_WRITEBACK`. That's where things get interesting.
Now, because of the code flow here, `extent_writepage` returns, and the page is left *dirty* and *not* marked as being in *writeback*. This means when we get to `filemap_fdatawait_range`, itβs going to skip this page because it's not tagged with `PG_writeback`. `btrfs_wait_ordered_range` gets called, it calls `filemap_fdatawait_range`, and the tag check returns immediately. Then `btrfs_wait_ordered_range` is going to look for an ordered extent and find *nothing* because the worker thread hasn't created it yet.
This is where the race happens. `btrfs_punch_hole` proceeds assuming this range is clean, locks the range, and calls `btrfs_assert_inode_range_clean`. Meanwhile, the `delalloc_workers` workqueue is running `submit_one_async_extent`, which is *now* creating the ordered extent! The ordered extent is created *after* `btrfs_wait_ordered_range` has finished, *while* `btrfs_punch_hole` is holding the locks! This explains the bug perfectly!
To double-check this:
1. `btrfs_wait_ordered_range` -> `btrfs_fdatawrite_range` -> `filemap_fdatawrite_range` -> `extent_write_cache_pages` -> `extent_writepage` -> `writepage_delalloc` -> `btrfs_run_delalloc_range` -> queues to `delalloc_workers`
2. `writepage_delalloc` clears the bitmap, and returns 1
3. `extent_writepage` returns 0. Page is left locked.
4. Then `btrfs_punch_hole` is called
5. `run_delalloc_compressed` passes the folio to the async chunk
6. And `submit_one_async_extent` unlocks it
7. `filemap_fdatawait_range` doesn't wait (no writeback).
8. `btrfs_wait_ordered_range` doesn't find the ordered extent, so it doesn't wait
9. `btrfs_punch_hole` locks the folio, by calling `truncate_inode_pages_range`, which calls `folio_lock(folio)` and blocks!
10. The folio is locked
11. `submit_one_async_extent` creates the ordered extent and *unlocks* the page.
12. `truncate_inode_pages_range` locks the folio and returns
13. And now it asserts clean!
Here's the problem: The *page* is locked by `extent_write_cache_pages` while `submit_one_async_extent` runs asynchronously. Because of the async nature of the delalloc worker, `filemap_fdatawait_range` doesn't wait for the page to be written back since it isn't tagged and `btrfs_wait_ordered_range` also doesn't wait because there's no ordered extent *yet*. `btrfs_punch_hole` then tries to truncate the pages, which is going to try to lock the page again, and now *blocks* until the delalloc worker thread finishes. That thread then creates the ordered extent and *unlocks* the page.
So, in summary:
* **Thread 1 (madvise/hole punch):** Reads and Locks a page, enqueues an item to the `delalloc_workers` workqueue to eventually write it. The page is left dirty and locked. Calls `filemap_fdatawait_range` but doesn't wait. Calls `btrfs_wait_ordered_range` but
|
| 87/2 |
2026/08/09 03:32 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/file.c Name:btrfs_replace_file_extents]
Results: map[SourceCode:2380: /*
2381: * The respective range must have been previously locked, as well as the inode.
2382: * The end offset is inclusive (last byte of the range).
2383: * @extent_info is NULL for fallocate's hole punching and non-NULL when replacing
2384: * the file range with an extent.
2385: * When not punching a hole, we don't want to end up in a state where we dropped
2386: * extents without inserting a new one, so we must abort the transaction to avoid
2387: * a corruption.
2388: */
2389: int btrfs_replace_file_extents(struct btrfs_inode *inode,
2390: struct btrfs_path *path, const u64 start,
2391: const u64 end,
2392: struct btrfs_replace_extent_info *extent_info,
2393: struct btrfs_trans_handle **trans_out)
2394: {
2395: struct btrfs_drop_extents_args drop_args = { 0 };
2396: struct btrfs_root *root = inode->root;
2397: struct btrfs_fs_info *fs_info = root->fs_info;
2398: const u64 min_size = btrfs_calc_insert_metadata_size(fs_info, 1);
2399: u64 ino_size = round_up(inode->vfs_inode.i_size, fs_info->sectorsize);
2400: struct btrfs_trans_handle *trans = NULL;
2401: struct btrfs_block_rsv rsv;
2402: unsigned int rsv_count;
2403: u64 cur_offset;
2404: u64 len = end - start;
2405: int ret = 0;
2406:
2407: if (end <= start)
2408: return -EINVAL;
2409:
2410: btrfs_init_metadata_block_rsv(fs_info, &rsv, BTRFS_BLOCK_RSV_TEMP);
2411: rsv.size = min_size;
2412: rsv.failfast = true;
2413:
2414: /*
2415: * 1 - update the inode
2416: * 1 - removing the extents in the range
2417: * 1 - adding the hole extent if no_holes isn't set or if we are
2418: * replacing the range with a new extent
2419: */
2420: if (!btrfs_fs_incompat(fs_info, NO_HOLES) || extent_info)
2421: rsv_count = 3;
2422: else
2423: rsv_count = 2;
2424:
2425: trans = btrfs_start_transaction(root, rsv_count);
2426: if (IS_ERR(trans)) {
2427: ret = PTR_ERR(trans);
2428: trans = NULL;
2429: goto out_release;
2430: }
2431:
2432: ret = btrfs_block_rsv_migrate(&fs_info->trans_block_rsv, &rsv,
2433: min_size, false);
2434: if (WARN_ON(ret))
2435: goto out_trans;
2436: trans->block_rsv = &rsv;
2437:
2438: cur_offset = start;
2439: drop_args.path = path;
2440: drop_args.end = end + 1;
2441: drop_args.drop_cache = true;
2442: while (cur_offset < end) {
2443: drop_args.start = cur_offset;
2444: ret = btrfs_drop_extents(trans, root, inode, &drop_args);
2445: /* If we are punching a hole decrement the inode's byte count */
2446: if (!extent_info)
2447: btrfs_update_inode_bytes(inode, 0,
2448: drop_args.bytes_found);
2449: if (ret != -ENOSPC) {
2450: /*
2451: * The only time we don't want to abort is if we are
2452: * attempting to clone a partial inline extent, in which
2453: * case we'll get EOPNOTSUPP. However if we aren't
2454: * clone we need to abort no matter what, because if we
2455: * got EOPNOTSUPP via prealloc then we messed up and
2456: * need to abort.
2457: */
2458: if (unlikely(ret &&
2459: (ret != -EOPNOTSUPP ||
2460: (extent_info && extent_info->is_new_extent))))
2461: btrfs_abort_transaction(trans, ret);
2462: break;
2463: }
2464:
2465: trans->block_rsv = &fs_info->trans_block_rsv;
2466:
2467: if (!extent_info && cur_offset < drop_args.drop_end &&
2468: cur_offset < ino_size) {
2469: ret = fill_holes(trans, inode, path, cur_offset,
2470: drop_args.drop_end);
2471: if (unlikely(ret)) {
2472: /*
2473: * If we failed then we didn't insert our hole
2474: * entries for the area we dropped, so now the
2475: * fs is corrupted, so we must abort the
2476: * transaction.
2477: */
2478: btrfs_abort_transaction(trans, ret);
2479: break;
2480: }
2481: } else if (!extent_info && cur_offset < drop_args.drop_end) {
2482: /*
2483: * We are past the i_size here, but since we didn't
2484: * insert holes we need to clear the mapped area so we
2485: * know to not set disk_i_size in this area until a new
2486: * file extent is inserted here.
2487: */
2488: ret = btrfs_inode_clear_file_extent_range(inode,
2489: cur_offset,
2490: drop_args.drop_end - cur_offset);
2491: if (unlikely(ret)) {
2492: /*
2493: * We couldn't clear our area, so we could
2494: * presumably adjust up and corrupt the fs, so
2495: * we need to abort.
2496: */
2497: btrfs_abort_transaction(trans, ret);
2498: break;
2499: }
2500: }
2501:
2502: if (extent_info &&
2503: drop_args.drop_end > extent_info->file_offset) {
2504: u64 replace_len = drop_args.drop_end -
2505: extent_info->file_offset;
2506:
2507: ret = btrfs_insert_replace_extent(trans, inode, path,
2508: extent_info, replace_len,
2509: drop_args.bytes_found);
2510: if (unlikely(ret)) {
2511: btrfs_abort_transaction(trans, ret);
2512: break;
2513: }
2514: extent_info->data_len -= replace_len;
2515: extent_info->data_offset += replace_len;
2516: extent_info->file_offset += replace_len;
2517: }
2518:
2519: /*
2520: * We are releasing our handle on the transaction, balance the
2521: * dirty pages of the btree inode and flush delayed items, and
2522: * then get a new transaction handle, which may now point to a
2523: * new transaction in case someone else may have committed the
2524: * transaction we used to replace/drop file extent items. So
2525: * bump the inode's iversion and update mtime and ctime except
2526: * if we are called from a dedupe context. This is because a
2527: * power failure/crash may happen after the transaction is
2528: * committed and before we finish replacing/dropping all the
2529: * file extent items we need.
2530: */
2531: inode_inc_iversion(&inode->vfs_inode);
2532:
2533: if (!extent_info || extent_info->update_times)
2534: inode_set_mtime_to_ts(&inode->vfs_inode,
2535: inode_set_ctime_current(&inode->vfs_inode));
2536:
2537: ret = btrfs_update_inode(trans, inode);
2538: if (ret)
2539: break;
2540:
2541: btrfs_end_transaction(trans);
2542: btrfs_btree_balance_dirty(fs_info);
2543:
2544: trans = btrfs_start_transaction(root, rsv_count);
2545: if (IS_ERR(trans)) {
2546: ret = PTR_ERR(trans);
2547: trans = NULL;
2548: break;
2549: }
2550:
2551: ret = btrfs_block_rsv_migrate(&fs_info->trans_block_rsv,
2552: &rsv, min_size, false);
2553: if (WARN_ON(ret))
2554: break;
2555: trans->block_rsv = &rsv;
2556:
2557: cur_offset = drop_args.drop_end;
2558: len = end - cur_offset;
2559: if (!extent_info && len) {
2560: ret = find_first_non_hole(inode, &cur_offset, &len);
2561: if (unlikely(ret < 0))
2562: break;
2563: if (ret && !len) {
2564: ret = 0;
2565: break;
2566: }
2567: }
2568: }
2569:
2570: /*
2571: * If we were cloning, force the next fsync to be a full one since we
2572: * we replaced (or just dropped in the case of cloning holes when
2573: * NO_HOLES is enabled) file extent items and did not setup new extent
2574: * maps for the replacement extents (or holes).
2575: */
2576: if (extent_info && !extent_info->is_new_extent)
2577: btrfs_set_inode_full_sync(inode);
2578:
2579: if (ret)
2580: goto out_trans;
2581:
2582: trans->block_rsv = &fs_info->trans_block_rsv;
2583: /*
2584: * If we are using the NO_HOLES feature we might have had already an
2585: * hole that overlaps a part of the region [lockstart, lockend] and
2586: * ends at (or beyond) lockend. Since we have no file extent items to
2587: * represent holes, drop_end can be less than lockend and so we must
2588: * make sure we have an extent map representing the existing hole (the
2589: * call to __btrfs_drop_extents() might have dropped the existing extent
2590: * map representing the existing hole), otherwise the fast fsync path
2591: * will not record the existence of the hole region
2592: * [existing_hole_start, lockend].
2593: */
2594: if (drop_args.drop_end <= end)
2595: drop_args.drop_end = end + 1;
2596: /*
2597: * Don't insert file hole extent item if it's for a range beyond eof
2598: * (because it's useless) or if it represents a 0 bytes range (when
2599: * cur_offset == drop_end).
2600: */
2601: if (!extent_info && cur_offset < ino_size &&
2602: cur_offset < drop_args.drop_end) {
2603: ret = fill_holes(trans, inode, path, cur_offset,
2604: drop_args.drop_end);
2605: if (unlikely(ret)) {
2606: /* Same comment as above. */
2607: btrfs_abort_transaction(trans, ret);
2608: goto out_trans;
2609: }
2610: } else if (!extent_info && cur_offset < drop_args.drop_end) {
2611: /* See the comment in the loop above for the reasoning here. */
2612: ret = btrfs_inode_clear_file_extent_range(inode, cur_offset,
2613: drop_args.drop_end - cur_offset);
2614: if (unlikely(ret)) {
2615: btrfs_abort_transaction(trans, ret);
2616: goto out_trans;
2617: }
2618:
2619: }
2620: if (extent_info) {
2621: ret = btrfs_insert_replace_extent(trans, inode, path,
2622: extent_info, extent_info->data_len,
2623: drop_args.bytes_found);
2624: if (unlikely(ret)) {
2625: btrfs_abort_transaction(trans, ret);
2626: goto out_trans;
2627: }
2628: }
2629:
2630: out_trans:
2631: if (!trans)
2632: goto out_release;
2633:
2634: trans->block_rsv = &fs_info->trans_block_rsv;
2635: if (ret)
2636: btrfs_end_transaction(trans);
2637: else
2638: *trans_out = trans;
2639: out_release:
2640: btrfs_block_rsv_release(fs_info, &rsv, (u64)-1, NULL);
2641: return ret;
2642: }
SourceFile:fs/btrfs/file.c]
|
| 88/2 |
2026/08/09 03:32 |
llm |
expert |
0m
Model:
Tokens:
input: 86943
output: 66
thoughts: 1251
Thoughts: **Analysis of a Btrfs Data Corruption Bug**
Okay, let's break this down. `btrfs_replace_file_extents` eventually calls `btrfs_drop_extents`, right? And `btrfs_drop_extents` is the function that actually removes extents from the btree. Good. And, importantly, if there's an ordered extent, it doesn't crash β at least, not in a typical production scenario. That's key. So, the initial concern about a kernel crash, that is, something that would stop the system, can be dismissed.
Now, can an unprivileged process trigger this? Looks like it. `madvise(MADV_REMOVE)` is available to anyone. A user can create a btrfs file, write to it, and then call `madvise(MADV_REMOVE)`. That confirms unprivileged access.
Denial of Service (DoS)? No, not in production. `CONFIG_BTRFS_ASSERT` is disabled, so there is no BUG, which would give us an instant DoS. What about other forms of DoS? A deadlock? No, we've analyzed it and found no deadlock. A resource leak? Seems the ordered extent is correctly finished and inserted, which means nothing will be leaked. It boils down to data corruption; the hole punch doesn't actually punch the hole. But data corruption *within a user's own file* isn't a security vulnerability, it's just a bug. If a user screws up their own file, that's their problem, not a system problem. No DoS, and not exploitable in the traditional sense.
Wait, what if a user tries to hole-punch a file they *don't* own? `madvise_remove` has a check on `vma_is_shared_maywrite(vma)`. They need write access. If they already have write access, then they can already modify the file. No privilege escalation.
What about a swap file? Swap files don't support hole punching. And executable files? If they can write, they can modify it anyway. This seems straightforward.
The security impact, in short: *none*. It's a straight functional bug.
Okay, let's summarize the root cause, now that we know we're not talking about a major security flaw.
Here's the race condition: it is between `madvise(MADV_REMOVE)` (which calls `btrfs_punch_hole`) and asynchronous compressed writeback.
`btrfs_wait_ordered_range` flushes delalloc and waits for ordered extents, which is what we want.
However, for *compressed* writes, the ordered extent creation is deferred to a workqueue (`delalloc_workers`).
`filemap_fdatawait_range` doesn't wait for these deferred ordered extents because the pages are not marked as `PG_writeback` yet. That is the issue.
Consequently, `btrfs_wait_ordered_range` returns *before* the ordered extent is created.
`btrfs_punch_hole` then locks the range and asserts that there are no ordered extents.
Meanwhile, the workqueue is happily creating the ordered extent, triggering that `ASSERT` in `btrfs_assert_inode_range_clean`.
However, the `ASSERT` is only enabled in debug configurations (`CONFIG_BTRFS_ASSERT`), which are disabled in production kernels, which means we avoid the crash.
Without the `ASSERT`, the hole punch and the ordered extent completion race to update the btree.
Since both operations lock the `io_tree` for the range, they're serialized, so we're safe, but at the very least, you have inconsistent data!
The only consequence: the punched hole might get overwritten by the delayed compressed write, leading to data inconsistency in the user's file.
This does not cause memory corruption, kernel crashes (in production), or privilege escalation.
It is not exploitable and does not cause a denial of service.
So I'll set the results as:
Exploitable: false
DenialOfService: false (because crashes are only enabled in a debug setting)
Unprivileged: true
UserNamespace: false
VMGuestTrigger: false
VMHostTrigger: false
NetworkTrigger: false
RemoteTrigger: false
PeripheralTrigger: false
FilesystemTrigger: false
Wait... is there *any* possibility that `btrfs_assert_inode_range_clean` is enabled in some production kernels? Unlikely, but let's consider it. Usually, `CONFIG_BTRFS_ASSERT` is not enabled, right? Even if it is,
|
| 89/2 |
2026/08/09 03:32 |
tool |
set-results |
0m
Args: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 90/2 |
2026/08/09 03:32 |
llm |
expert |
0m
Model:
Tokens:
input: 89349
output: 705
thoughts: 163
Thoughts: **Finalizing the Analysis**
Okay, the data's in, and it's time to craft the definitive response. Here's how I see this playing out: I need to clearly lay out the sequence of events and the core issue.
First, it's that race condition: `btrfs_punch_hole` going head-to-head with async compressed writeback. That's the root of the problem. Second, I need to articulate precisely *why* `btrfs_wait_ordered_range` falls down on the job. It fails to wait properly for the ordered extent because `PG_writeback` isn't set on the page until the workqueue finally processes the async extent. That's a crucial piece of the puzzle.
Now, why does this trigger the `ASSERT`? I need to connect those dots, explaining the specific conditions that expose this bug. It's not just a theoretical issue; there's a definite pathway here.
Then, the critical point for our audience β the practical implications. Itβs imperative to stress that this isnβt a security catastrophe in a production environment. I need to make sure the user understands why β the `ASSERT` is disabled in production kernels, and we're *not* looking at memory corruption, nor are we dealing with a Use-After-Free scenario. At most, we're talking about a potential data inconsistency, but it's contained within the user's own file.
Finally, the interesting bit: I need to underscore that this *is* reachable by unprivileged users. I should specifically mention that a user can trigger this through `madvise(MADV_REMOVE)` or `fallocate()`. This makes it a potential DoS vulnerability, though relatively minor.
|
| 91/1 |
2026/08/09 03:33 |
action |
format-explanation |
0m
Results: map[Explanation:**Root Cause Analysis**
The bug is a race condition between hole punching (via `madvise(MADV_REMOVE)` or
`fallocate()`) and asynchronous compressed writeback in Btrfs.
When a hole is punched, `btrfs_punch_hole()` calls `btrfs_wait_ordered_range()`
to flush any pending delayed allocations (delalloc) and wait for all ordered
extents in the range to complete.
1. `btrfs_wait_ordered_range()` triggers writeback by calling
`filemap_fdatawrite_range()`.
2. For compressed extents, Btrfs does not create the ordered extent
synchronously. Instead, `run_delalloc_compressed()` queues the compression work
to a background workqueue (`delalloc_workers`) and leaves the page locked but
**without** the `PG_writeback` flag set.
3. `filemap_fdatawait_range()` is then called to wait for writeback to finish,
but since the `PG_writeback` flag is not yet set on these pages, it returns
immediately.
4. `btrfs_wait_ordered_range()` then checks for ordered extents, finds none
(because the workqueue hasn't created them yet), and returns.
5. `btrfs_punch_hole()` proceeds, locks the file range, and calls
`btrfs_assert_inode_range_clean()`.
6. Meanwhile, the background workqueue executes `submit_one_async_extent()`,
which allocates the data extent, creates the ordered extent, and finally unlocks
the page and sets `PG_writeback`.
7. `btrfs_assert_inode_range_clean()` checks for ordered extents in the locked
range and finds the newly created ordered extent, triggering the `ASSERT()` and
crashing the kernel.
**Security Impact**
This bug is **not exploitable** and does not pose a security risk in production
environments:
1. **No Memory Corruption:** The crash is strictly caused by a developer
`ASSERT()` (`CONFIG_BTRFS_ASSERT`). In standard production kernels (e.g.,
Ubuntu, Debian, Fedora), this config is disabled. Without the assert, the hole
punch and the delayed ordered extent completion will safely race to update the
b-tree. Both operations take the `io_tree` lock, ensuring they are properly
serialized. There are no use-after-free, double-free, or out-of-bounds memory
accesses.
2. **No Denial of Service:** Since the `ASSERT()` is compiled out in production
builds, the kernel will not crash or panic.
3. **Data Inconsistency:** The only consequence of this race in production is a
functional bug where a punched hole might be partially overwritten by the
delayed compressed write. Because an unprivileged user can only punch holes in
files they already have write access to, this data inconsistency is restricted
to their own files and cannot be used for privilege escalation or cross-user
data corruption.
**Trigger**
The bug can be triggered by any **unprivileged** local user by creating a Btrfs
file, enabling compression, writing data to it, and concurrently calling
`madvise(MADV_REMOVE)` or `fallocate(FALLOC_FL_PUNCH_HOLE)` on the file's
memory-mapped range.]
|