| 0/0 |
2026/05/30 21:38 |
flow |
assessment-security |
57m
Results: map[DenialOfService:false Explanation:The bug is an uninitialized value in `xlog_verify_head` in
`fs/xfs/xfs_log_recover.c`.
**Root Cause:**
During XFS log recovery, `xlog_verify_head` calls `xlog_rseek_logrec_hdr` to
search backwards for log records and initialize `tmp_rhead_blk`. If the log is
corrupted or maliciously crafted such that no blocks contain the
`XLOG_HEADER_MAGIC_NUM`, `xlog_rseek_logrec_hdr` returns `0` (meaning 0 records
found) without initializing `tmp_rhead_blk`.
However, `xlog_verify_head` only checks if the return value is negative (`if
(error < 0) return error;`). Since `0` is not negative, it proceeds to call
`xlog_do_recovery_pass` with the uninitialized `tmp_rhead_blk` as the `tail_blk`
argument.
**Security Impact:**
In `xlog_do_recovery_pass`, the uninitialized value is used as a disk block
number (`blk_no`) to read from the log via `xlog_bread` -> `xlog_do_io`.
- If the uninitialized value is out of bounds (which is highly likely for a
random stack value), `xlog_verify_bno` rejects it, and `xlog_do_io` prints a
warning to the kernel log (`dmesg`) containing the invalid block number. This
results in a **kernel stack information leak**, which could potentially be used
to bypass KASLR. The mount operation then safely aborts with `-EFSCORRUPTED`.
- If the uninitialized value happens to be within the valid log block range, the
kernel will read and process a random block from the log. Since the attacker
already controls the entire malicious disk image, reading a random block from it
does not grant any new capabilities or lead to memory corruption.
**Exploitability:**
This bug is **not exploitable** for memory corruption or privilege escalation.
It only results in a stack infoleak to the kernel log and a failed mount
operation. It does not cause a kernel crash, hang, or resource leak, so it is
not a Denial of Service (DoS).
**Accessibility:**
Triggering this bug requires mounting a maliciously crafted XFS filesystem
image.
- **Unprivileged / User Namespaces:** XFS does not set the `FS_USERNS_MOUNT`
flag, meaning it cannot be mounted by unprivileged users, even within a user
namespace. It requires `CAP_SYS_ADMIN` in the initial namespace.
- **Peripheral / Filesystem Trigger:** The bug can be triggered if a user
physically plugs in a malicious USB drive and the desktop environment
auto-mounts it, or if a privileged user is tricked into mounting a downloaded
malicious image. Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/05/30 21:38 |
action |
syz-repro-to-c-repro |
0m
Results:
|
| 2/1 |
2026/05/30 21:38 |
action |
kernel-checkouter |
1m
Results: map[KernelSrc:/app/workdir/cache/src/5954b25b6018a27d36c43293783ddc10f52d5131]
|
| 3/1 |
2026/05/30 21:40 |
action |
kernel-builder |
28m
Results: map[KernelObj:/app/workdir/cache/build/09d1caa061cb53c83a74b4ab1f16c58752477a4b]
|
| 4/1 |
2026/05/30 22:09 |
action |
codesearch-prepare |
18m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/05/30 22:27 |
agent |
expert |
8m
Model:
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption or elevated privileges.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a 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.
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:
XFS (loop4): Mounting V5 Filesystem d7dc424e-7990-42cb-9f91-9cb7200a101d
=====================================================
BUG: KMSAN: uninit-value in xlog_verify_head+0x6c3/0x910 fs/xfs/xfs_log_recover.c:1058
xlog_verify_head+0x6c3/0x910 fs/xfs/xfs_log_recover.c:1058
xlog_find_tail+0xc2e/0x1a50 fs/xfs/xfs_log_recover.c:1315
xlog_recover+0x6d/0x800 fs/xfs/xfs_log_recover.c:3426
xfs_log_mount+0x4da/0x880 fs/xfs/xfs_log.c:617
xfs_mountfs+0x1599/0x2d00 fs/xfs/xfs_mount.c:1034
xfs_fs_fill_super+0x2603/0x2be0 fs/xfs/xfs_super.c:1938
get_tree_bdev_flags+0x6e6/0x920 fs/super.c:1694
get_tree_bdev+0x38/0x50 fs/super.c:1717
xfs_fs_get_tree+0x35/0x40 fs/xfs/xfs_super.c:1985
vfs_get_tree+0xb3/0x5d0 fs/super.c:1754
fc_mount fs/namespace.c:1193 [inline]
do_new_mount_fc fs/namespace.c:3763 [inline]
do_new_mount+0x885/0x1dd0 fs/namespace.c:3839
path_mount+0x7a2/0x20b0 fs/namespace.c:4159
do_mount fs/namespace.c:4172 [inline]
__do_sys_mount fs/namespace.c:4361 [inline]
__se_sys_mount+0x704/0x7f0 fs/namespace.c:4338
__ia32_sys_mount+0xe2/0x150 fs/namespace.c:4338
ia32_sys_call+0x27fe/0x4360 arch/x86/include/generated/asm/syscalls_32.h:22
do_syscall_32_irqs_on arch/x86/entry/syscall_32.c:83 [inline]
__do_fast_syscall_32+0x17f/0x3f0 arch/x86/entry/syscall_32.c:307
do_fast_syscall_32+0x37/0x80 arch/x86/entry/syscall_32.c:332
do_SYSENTER_32+0x1f/0x30 arch/x86/entry/syscall_32.c:370
entry_SYSENTER_compat_after_hwframe+0x84/0x8e
Uninit was stored to memory at:
xlog_verify_head+0x6bc/0x910 fs/xfs/xfs_log_recover.c:1058
xlog_find_tail+0xc2e/0x1a50 fs/xfs/xfs_log_recover.c:1315
xlog_recover+0x6d/0x800 fs/xfs/xfs_log_recover.c:3426
xfs_log_mount+0x4da/0x880 fs/xfs/xfs_log.c:617
xfs_mountfs+0x1599/0x2d00 fs/xfs/xfs_mount.c:1034
xfs_fs_fill_super+0x2603/0x2be0 fs/xfs/xfs_super.c:1938
get_tree_bdev_flags+0x6e6/0x920 fs/super.c:1694
get_tree_bdev+0x38/0x50 fs/super.c:1717
xfs_fs_get_tree+0x35/0x40 fs/xfs/xfs_super.c:1985
vfs_get_tree+0xb3/0x5d0 fs/super.c:1754
fc_mount fs/namespace.c:1193 [inline]
do_new_mount_fc fs/namespace.c:3763 [inline]
do_new_mount+0x885/0x1dd0 fs/namespace.c:3839
path_mount+0x7a2/0x20b0 fs/namespace.c:4159
do_mount fs/namespace.c:4172 [inline]
__do_sys_mount fs/namespace.c:4361 [inline]
__se_sys_mount+0x704/0x7f0 fs/namespace.c:4338
__ia32_sys_mount+0xe2/0x150 fs/namespace.c:4338
ia32_sys_call+0x27fe/0x4360 arch/x86/include/generated/asm/syscalls_32.h:22
do_syscall_32_irqs_on arch/x86/entry/syscall_32.c:83 [inline]
__do_fast_syscall_32+0x17f/0x3f0 arch/x86/entry/syscall_32.c:307
do_fast_syscall_32+0x37/0x80 arch/x86/entry/syscall_32.c:332
do_SYSENTER_32+0x1f/0x30 arch/x86/entry/syscall_32.c:370
entry_SYSENTER_compat_after_hwframe+0x84/0x8e
Local variable tmp_rhead_blk created at:
xlog_verify_head+0x81/0x910 fs/xfs/xfs_log_recover.c:1032
xlog_find_tail+0xc2e/0x1a50 fs/xfs/xfs_log_recover.c:1315
CPU: 1 UID: 0 PID: 7664 Comm: syz.4.285 Tainted: G L syzkaller #0 PREEMPT(full)
Tainted: [L]=SOFTLOCKUP
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
=====================================================
Reply: The bug is an uninitialized value in `xlog_verify_head` in `fs/xfs/xfs_log_recover.c`.
**Root Cause:**
During XFS log recovery, `xlog_verify_head` calls `xlog_rseek_logrec_hdr` to search backwards for log records and initialize `tmp_rhead_blk`. If the log is corrupted or maliciously crafted such that no blocks contain the `XLOG_HEADER_MAGIC_NUM`, `xlog_rseek_logrec_hdr` returns `0` (meaning 0 records found) without initializing `tmp_rhead_blk`.
However, `xlog_verify_head` only checks if the return value is negative (`if (error < 0) return error;`). Since `0` is not negative, it proceeds to call `xlog_do_recovery_pass` with the uninitialized `tmp_rhead_blk` as the `tail_blk` argument.
**Security Impact:**
In `xlog_do_recovery_pass`, the uninitialized value is used as a disk block number (`blk_no`) to read from the log via `xlog_bread` -> `xlog_do_io`.
- If the uninitialized value is out of bounds (which is highly likely for a random stack value), `xlog_verify_bno` rejects it, and `xlog_do_io` prints a warning to the kernel log (`dmesg`) containing the invalid block number. This results in a **kernel stack information leak**, which could potentially be used to bypass KASLR. The mount operation then safely aborts with `-EFSCORRUPTED`.
- If the uninitialized value happens to be within the valid log block range, the kernel will read and process a random block from the log. Since the attacker already controls the entire malicious disk image, reading a random block from it does not grant any new capabilities or lead to memory corruption.
**Exploitability:**
This bug is **not exploitable** for memory corruption or privilege escalation. It only results in a stack infoleak to the kernel log and a failed mount operation. It does not cause a kernel crash, hang, or resource leak, so it is not a Denial of Service (DoS).
**Accessibility:**
Triggering this bug requires mounting a maliciously crafted XFS filesystem image.
- **Unprivileged / User Namespaces:** XFS does not set the `FS_USERNS_MOUNT` flag, meaning it cannot be mounted by unprivileged users, even within a user namespace. It requires `CAP_SYS_ADMIN` in the initial namespace.
- **Peripheral / Filesystem Trigger:** The bug can be triggered if a user physically plugs in a malicious USB drive and the desktop environment auto-mounts it, or if a privileged user is tricked into mounting a downloaded malicious image.
|
| 6/2 |
2026/05/30 22:27 |
llm |
expert |
0m
Model:
Tokens:
input: 7189
output: 43
thoughts: 142
Thoughts: Okay, here's my breakdown of this issue, keeping in mind my expertise:
**Diving into the XFS Log Recovery**
Alright, so we've got a KMSAN uninit-value bug. That's always a fun one. The report flags `xlog_verify_head` within `fs/xfs/xfs_log_recover.c:1058` as the culprit. My immediate concern is the context β it's triggered during an XFS mount, specifically through the sequence: `xfs_fs_fill_super` -> `xfs_mountfs` -> `xfs_log_mount` -> `xlog_recover` -> `xlog_find_tail` -> `xlog_verify_head`. This narrows the focus significantly. Itβs definitely a filesystem mount bug, which means weβre dealing with potential corruption or initialization issues at the most fundamental level of XFS operation. The crucial part now is pinpointing *why* `xlog_verify_head` is hitting this uninitialized value during the mount process. I need to get into the code for `xlog_verify_head` and understand exactly what it's expecting to be initialized and how it's being used. This isn't a runtime issue, it's a *startup* issue, so we're talking about initial data structures or variable states. Letβs get into the source and see what's going on.
|
| 7/2 |
2026/05/30 22:27 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_verify_head]
Results: map[SourceCode:1006: /*
1007: * Detect and trim torn writes from the head of the log.
1008: *
1009: * Storage without sector atomicity guarantees can result in torn writes in the
1010: * log in the event of a crash. Our only means to detect this scenario is via
1011: * CRC verification. While we can't always be certain that CRC verification
1012: * failure is due to a torn write vs. an unrelated corruption, we do know that
1013: * only a certain number (XLOG_MAX_ICLOGS) of log records can be written out at
1014: * one time. Therefore, CRC verify up to XLOG_MAX_ICLOGS records at the head of
1015: * the log and treat failures in this range as torn writes as a matter of
1016: * policy. In the event of CRC failure, the head is walked back to the last good
1017: * record in the log and the tail is updated from that record and verified.
1018: */
1019: STATIC int
1020: xlog_verify_head(
1021: struct xlog *log,
1022: xfs_daddr_t *head_blk, /* in/out: unverified head */
1023: xfs_daddr_t *tail_blk, /* out: tail block */
1024: char *buffer,
1025: xfs_daddr_t *rhead_blk, /* start blk of last record */
1026: struct xlog_rec_header **rhead, /* ptr to last record */
1027: bool *wrapped) /* last rec. wraps phys. log */
1028: {
1029: struct xlog_rec_header *tmp_rhead;
1030: char *tmp_buffer;
1031: xfs_daddr_t first_bad;
1032: xfs_daddr_t tmp_rhead_blk;
1033: int found;
1034: int error;
1035: bool tmp_wrapped;
1036:
1037: /*
1038: * Check the head of the log for torn writes. Search backwards from the
1039: * head until we hit the tail or the maximum number of log record I/Os
1040: * that could have been in flight at one time. Use a temporary buffer so
1041: * we don't trash the rhead/buffer pointers from the caller.
1042: */
1043: tmp_buffer = xlog_alloc_buffer(log, 1);
1044: if (!tmp_buffer)
1045: return -ENOMEM;
1046: error = xlog_rseek_logrec_hdr(log, *head_blk, *tail_blk,
1047: XLOG_MAX_ICLOGS, tmp_buffer,
1048: &tmp_rhead_blk, &tmp_rhead, &tmp_wrapped);
1049: kvfree(tmp_buffer);
1050: if (error < 0)
1051: return error;
1052:
1053: /*
1054: * Now run a CRC verification pass over the records starting at the
1055: * block found above to the current head. If a CRC failure occurs, the
1056: * log block of the first bad record is saved in first_bad.
1057: */
1058: error = xlog_do_recovery_pass(log, *head_blk, tmp_rhead_blk,
1059: XLOG_RECOVER_CRCPASS, &first_bad);
1060: if ((error == -EFSBADCRC || error == -EFSCORRUPTED) && first_bad) {
1061: /*
1062: * We've hit a potential torn write. Reset the error and warn
1063: * about it.
1064: */
1065: error = 0;
1066: xfs_warn(log->l_mp,
1067: "Torn write (CRC failure) detected at log block 0x%llx. Truncating head block from 0x%llx.",
1068: first_bad, *head_blk);
1069:
1070: /*
1071: * Get the header block and buffer pointer for the last good
1072: * record before the bad record.
1073: *
1074: * Note that xlog_find_tail() clears the blocks at the new head
1075: * (i.e., the records with invalid CRC) if the cycle number
1076: * matches the current cycle.
1077: */
1078: found = xlog_rseek_logrec_hdr(log, first_bad, *tail_blk, 1,
1079: buffer, rhead_blk, rhead, wrapped);
1080: if (found < 0)
1081: return found;
1082: if (found == 0) /* XXX: right thing to do here? */
1083: return -EIO;
1084:
1085: /*
1086: * Reset the head block to the starting block of the first bad
1087: * log record and set the tail block based on the last good
1088: * record.
1089: *
1090: * Bail out if the updated head/tail match as this indicates
1091: * possible corruption outside of the acceptable
1092: * (XLOG_MAX_ICLOGS) range. This is a job for xfs_repair...
1093: */
1094: *head_blk = first_bad;
1095: *tail_blk = BLOCK_LSN(be64_to_cpu((*rhead)->h_tail_lsn));
1096: if (*head_blk == *tail_blk) {
1097: ASSERT(0);
1098: return 0;
1099: }
1100: }
1101: if (error)
1102: return error;
1103:
1104: return xlog_verify_tail(log, *head_blk, tail_blk,
1105: be32_to_cpu((*rhead)->h_size));
1106: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 8/2 |
2026/05/30 22:27 |
llm |
expert |
0m
Model:
Tokens:
input: 9458
output: 47
thoughts: 84
Thoughts: **Analyzing the Recovery Process**
Okay, so I'm looking at how the system recovers from a crash, specifically within the XLOG subsystem. I see `tmp_rhead_blk` being the initial value. This is crucial; it's the starting point for the recovery pass. Line 1058 shows `tmp_rhead_blk` being passed to `xlog_do_recovery_pass`. The critical bit is where `tmp_rhead_blk` comes from - it looks like `xlog_rseek_logrec_hdr` on line 1046 is responsible for initializing this variable. That's the function I need to understand. Let's delve into what `xlog_rseek_logrec_hdr` actually does.
|
| 9/2 |
2026/05/30 22:27 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_rseek_logrec_hdr]
Results: map[SourceCode: 754: /*
755: * Seek backwards in the log for log record headers.
756: *
757: * Given a starting log block, walk backwards until we find the provided number
758: * of records or hit the provided tail block. The return value is the number of
759: * records encountered or a negative error code. The log block and buffer
760: * pointer of the last record seen are returned in rblk and rhead respectively.
761: */
762: STATIC int
763: xlog_rseek_logrec_hdr(
764: struct xlog *log,
765: xfs_daddr_t head_blk,
766: xfs_daddr_t tail_blk,
767: int count,
768: char *buffer,
769: xfs_daddr_t *rblk,
770: struct xlog_rec_header **rhead,
771: bool *wrapped)
772: {
773: int i;
774: int error;
775: int found = 0;
776: char *offset = NULL;
777: xfs_daddr_t end_blk;
778:
779: *wrapped = false;
780:
781: /*
782: * Walk backwards from the head block until we hit the tail or the first
783: * block in the log.
784: */
785: end_blk = head_blk > tail_blk ? tail_blk : 0;
786: for (i = (int) head_blk - 1; i >= end_blk; i--) {
787: error = xlog_bread(log, i, 1, buffer, &offset);
788: if (error)
789: goto out_error;
790:
791: if (*(__be32 *) offset == cpu_to_be32(XLOG_HEADER_MAGIC_NUM)) {
792: *rblk = i;
793: *rhead = (struct xlog_rec_header *) offset;
794: if (++found == count)
795: break;
796: }
797: }
798:
799: /*
800: * If we haven't hit the tail block or the log record header count,
801: * start looking again from the end of the physical log. Note that
802: * callers can pass head == tail if the tail is not yet known.
803: */
804: if (tail_blk >= head_blk && found != count) {
805: for (i = log->l_logBBsize - 1; i >= (int) tail_blk; i--) {
806: error = xlog_bread(log, i, 1, buffer, &offset);
807: if (error)
808: goto out_error;
809:
810: if (*(__be32 *)offset ==
811: cpu_to_be32(XLOG_HEADER_MAGIC_NUM)) {
812: *wrapped = true;
813: *rblk = i;
814: *rhead = (struct xlog_rec_header *) offset;
815: if (++found == count)
816: break;
817: }
818: }
819: }
820:
821: return found;
822:
823: out_error:
824: return error;
825: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 10/2 |
2026/05/30 22:27 |
llm |
expert |
0m
Model:
Tokens:
input: 10838
output: 47
thoughts: 353
Thoughts: **Analyzing a Potential Initialization Bug in XLOG Recovery**
Okay, so I'm looking at this `xlog_verify_head` function and I see something that's making me a bit uneasy. Specifically, how it handles the return value of `xlog_rseek_logrec_hdr`. This function, as I understand it, is responsible for seeking the log record header, and it returns a count of records found. If it returns 0, it means no records were found.
The crucial part is this: if `xlog_rseek_logrec_hdr` returns 0 β meaning, *no* records are found β the code *doesn't* initialize `tmp_rhead_blk` before passing it to `xlog_do_recovery_pass`. It seems like the intention is that `tmp_rhead_blk` is meant to store the block number of the *first* valid log record. However, if there are no valid log records, that value might be completely undefined.
So, if `found == 0`, meaning that `xlog_rseek_logrec_hdr` found no records, then `error` will be assigned zero and `tmp_rhead_blk` will be uninitialized. Later on, `xlog_do_recovery_pass` is called with this potentially uninitialized value. The question becomes, what happens when an uninitialized value is used in this subsequent call? Does this behavior result in undefined behavior?
I need to confirm if this is a known issue or if some later patch handles the zero record case correctly. If this wasn't addressed, it could lead to unpredictable behavior, potentially crashes or data corruption, during recovery scenarios. I will check for related fixes now.
|
| 11/2 |
2026/05/30 22:28 |
tool |
git-log |
0m
Args: map[CodeRegexp:tmp_rhead_blk Count:10 PathPrefix:fs/xfs/xfs_log_recover.c]
Results: map[Output:6e9b3dd80f9c xfs: stop using bp naming for log recovery buffers
7088c4136fa1 xfs: detect and trim torn writes during log recovery
]
|
| 12/2 |
2026/05/30 22:28 |
llm |
expert |
0m
Model:
Tokens:
input: 11665
output: 47
thoughts: 17
Thoughts: Okay, here's my attempt at summarizing that thought process, keeping in mind the expert user:
**Addressing the Uninitialized Value**
Alright, before I dive too deep into this problem, I'm going to quickly check if there's a well-known fix, a common workaround, or perhaps a patch already available for this uninitialized value issue. I've seen this happen before, and I'd like to leverage any existing community knowledge or documented solutions to save myself some time and effort. There's probably a known cause, maybe a subtle corner-case I'm missing, or perhaps a specific configuration nuance. Checking for a straightforward solution now could prevent a lot of wasted time later, which is something I always strive for. Let's see what the documentation and the forums have to say.
|
| 13/2 |
2026/05/30 22:28 |
tool |
git-log |
0m
Args: map[CodeRegexp:tmp_rhead_blk Count:10 PathPrefix:fs/xfs/xfs_log_recover.c]
Results: map[Output:6e9b3dd80f9c xfs: stop using bp naming for log recovery buffers
7088c4136fa1 xfs: detect and trim torn writes during log recovery
]
|
| 14/2 |
2026/05/30 22:28 |
llm |
expert |
0m
Model:
Tokens:
input: 11956
output: 47
thoughts: 29
Thoughts: Okay, here's my interpretation of those thoughts, summarized in the first person, assuming the context is database internals and I'm an expert in this area:
**Digging into Potential Corruption Issues**
Right, time to get my hands dirty. I need to figure out what's been going on with these potential corruption issues. My instinct is to focus on a targeted search. I'm going to look specifically for commits that involve `xlog_verify_head`. That function is critical for ensuring the integrity of the transaction log's header, so any changes there warrant serious investigation. And I'm going to couple that with looking for mentions of `uninit`, because that screams potential memory errors, which can lead to corruption, or `tmp_rhead_blk`. This block, given its naming, I bet, is related to a temporary read head block which, if mishandled, could also be a source of the issues reported. My thought process is to use these as keywords that would narrow the focus of the search and highlight possibly related commits.
|
| 15/2 |
2026/05/30 22:28 |
tool |
git-log |
0m
Args: map[Count:10 MessageRegexps:[xlog_verify_head] PathPrefix:fs/xfs/xfs_log_recover.c]
Results:
|
| 16/2 |
2026/05/30 22:28 |
llm |
expert |
1m
Model:
Tokens:
input: 12264
output: 28
thoughts: 29
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Investigating XFS Log Recovery**
Alright, time to dig into this. I'm thinking, let's target the heart of the matter - specifically, where the XFS log recovery process is handled. My instincts say the best place to start is within the core file system code, and likely focusing on the log-related recovery functions. So, let's zero in on `fs/xfs/xfs_log_recover.c`. I have a feeling that function name I remember, `xlog_verify_head` is directly relevant. That name sounds like a routine that confirms the validity of the log header, which seems like a critical first step. I suspect it'll be a key component in understanding how XFS validates and processes the log during recovery. Let's see what's in there.
|
| 17/2 |
2026/05/30 22:29 |
tool |
git-log |
0m
Args: map[Count:10 MessageRegexps:[xlog_verify_head]]
Results:
|
| 18/2 |
2026/05/30 22:29 |
llm |
expert |
0m
Model:
Tokens:
input: 12527
output: 46
thoughts: 50
Thoughts: Okay, let me think this through.
**Investigating the `xlog_verify_head` Presence**
Hmm, I have a feeling that function, `xlog_verify_head`, might not be explicitly called out in the commit message. Let me do a quick sanity check to confirm that. My next step will be to dive into the history of how `xlog_verify_head` is handled, within the `fs/xfs/xfs_log_recover.c` source file. A review of the commit history should illuminate the context. Hopefully it's mentioned there, but if not, I'll need to dig deeper!
|
| 19/2 |
2026/05/30 22:29 |
tool |
git-log |
0m
Args: map[Count:10 SourcePath:fs/xfs/xfs_log_recover.c SymbolName:xlog_verify_head]
Results: map[Output:49292576136f xfs: convert kmem_free() for kvmalloc users to kvfree()
b63da6c8dfa9 xfs: delete duplicated words + other fixes
6e9b3dd80f9c xfs: stop using bp naming for log recovery buffers
6ad5b3255b9e xfs: use bios directly to read and write the log recovery buffers
0703a8e1c17e xfs: replace do_mod with native operations
a4c9b34d6a17 xfs: handle -EFSCORRUPTED during head/tail verification
4a4f66eac468 xfs: fix log recovery corruption error due to tail overwrite
5297ac1f6d7c xfs: always verify the log tail during recovery
82ff6cc26e98 xfs: separate log head record discovery from verification
7088c4136fa1 xfs: detect and trim torn writes during log recovery
]
|
| 20/2 |
2026/05/30 22:30 |
llm |
expert |
0m
Model:
Tokens:
input: 13013
output: 43
thoughts: 274
Thoughts: **Analyzing a Potential Kernel Bug**
Okay, let's get down to brass tacks. I need to check if this bug is still lurking in the current kernel version. My initial concern is with this `tmp_rhead_blk` variable. It looks like, under certain circumstances, it might be left uninitialized.
Specifically, if `found == 0` (or, in this code's parlance, `error == 0`), `tmp_rhead_blk` is never given a value. Now, looking at the call in `xlog_verify_head`:
```c
error = xlog_rseek_logrec_hdr(log, *head_blk, *tail_blk,
XLOG_MAX_ICLOGS, tmp_buffer,
&tmp_rhead_blk, &tmp_rhead, &tmp_wrapped);
kvfree(tmp_buffer);
if (error < 0)
return error;
```
It seems `xlog_rseek_logrec_hdr` could return the number of records found. If it returns less than 0, then the error is returned. However, if the function returns zero, then the code proceeds past the initial if condition. This means we'll hit the subsequent code block, potentially using an uninitialized `tmp_rhead_blk`.
If `error == 0` it *doesn't* return and moves on. That's a problem. The code carries on as if `tmp_rhead_blk` has a valid value, but it doesn't!
I need to confirm if there's a fix implemented in the latest tree to address this. Let's dig into the current `xlog_verify_head` implementation and see what's changed.
|
| 21/2 |
2026/05/30 22:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_verify_head]
Results: map[SourceCode:1006: /*
1007: * Detect and trim torn writes from the head of the log.
1008: *
1009: * Storage without sector atomicity guarantees can result in torn writes in the
1010: * log in the event of a crash. Our only means to detect this scenario is via
1011: * CRC verification. While we can't always be certain that CRC verification
1012: * failure is due to a torn write vs. an unrelated corruption, we do know that
1013: * only a certain number (XLOG_MAX_ICLOGS) of log records can be written out at
1014: * one time. Therefore, CRC verify up to XLOG_MAX_ICLOGS records at the head of
1015: * the log and treat failures in this range as torn writes as a matter of
1016: * policy. In the event of CRC failure, the head is walked back to the last good
1017: * record in the log and the tail is updated from that record and verified.
1018: */
1019: STATIC int
1020: xlog_verify_head(
1021: struct xlog *log,
1022: xfs_daddr_t *head_blk, /* in/out: unverified head */
1023: xfs_daddr_t *tail_blk, /* out: tail block */
1024: char *buffer,
1025: xfs_daddr_t *rhead_blk, /* start blk of last record */
1026: struct xlog_rec_header **rhead, /* ptr to last record */
1027: bool *wrapped) /* last rec. wraps phys. log */
1028: {
1029: struct xlog_rec_header *tmp_rhead;
1030: char *tmp_buffer;
1031: xfs_daddr_t first_bad;
1032: xfs_daddr_t tmp_rhead_blk;
1033: int found;
1034: int error;
1035: bool tmp_wrapped;
1036:
1037: /*
1038: * Check the head of the log for torn writes. Search backwards from the
1039: * head until we hit the tail or the maximum number of log record I/Os
1040: * that could have been in flight at one time. Use a temporary buffer so
1041: * we don't trash the rhead/buffer pointers from the caller.
1042: */
1043: tmp_buffer = xlog_alloc_buffer(log, 1);
1044: if (!tmp_buffer)
1045: return -ENOMEM;
1046: error = xlog_rseek_logrec_hdr(log, *head_blk, *tail_blk,
1047: XLOG_MAX_ICLOGS, tmp_buffer,
1048: &tmp_rhead_blk, &tmp_rhead, &tmp_wrapped);
1049: kvfree(tmp_buffer);
1050: if (error < 0)
1051: return error;
1052:
1053: /*
1054: * Now run a CRC verification pass over the records starting at the
1055: * block found above to the current head. If a CRC failure occurs, the
1056: * log block of the first bad record is saved in first_bad.
1057: */
1058: error = xlog_do_recovery_pass(log, *head_blk, tmp_rhead_blk,
1059: XLOG_RECOVER_CRCPASS, &first_bad);
1060: if ((error == -EFSBADCRC || error == -EFSCORRUPTED) && first_bad) {
1061: /*
1062: * We've hit a potential torn write. Reset the error and warn
1063: * about it.
1064: */
1065: error = 0;
1066: xfs_warn(log->l_mp,
1067: "Torn write (CRC failure) detected at log block 0x%llx. Truncating head block from 0x%llx.",
1068: first_bad, *head_blk);
1069:
1070: /*
1071: * Get the header block and buffer pointer for the last good
1072: * record before the bad record.
1073: *
1074: * Note that xlog_find_tail() clears the blocks at the new head
1075: * (i.e., the records with invalid CRC) if the cycle number
1076: * matches the current cycle.
1077: */
1078: found = xlog_rseek_logrec_hdr(log, first_bad, *tail_blk, 1,
1079: buffer, rhead_blk, rhead, wrapped);
1080: if (found < 0)
1081: return found;
1082: if (found == 0) /* XXX: right thing to do here? */
1083: return -EIO;
1084:
1085: /*
1086: * Reset the head block to the starting block of the first bad
1087: * log record and set the tail block based on the last good
1088: * record.
1089: *
1090: * Bail out if the updated head/tail match as this indicates
1091: * possible corruption outside of the acceptable
1092: * (XLOG_MAX_ICLOGS) range. This is a job for xfs_repair...
1093: */
1094: *head_blk = first_bad;
1095: *tail_blk = BLOCK_LSN(be64_to_cpu((*rhead)->h_tail_lsn));
1096: if (*head_blk == *tail_blk) {
1097: ASSERT(0);
1098: return 0;
1099: }
1100: }
1101: if (error)
1102: return error;
1103:
1104: return xlog_verify_tail(log, *head_blk, tail_blk,
1105: be32_to_cpu((*rhead)->h_size));
1106: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 22/2 |
2026/05/30 22:30 |
llm |
expert |
0m
Model:
Tokens:
input: 15495
output: 47
thoughts: 72
Thoughts: **Debugging a Potential XLOG Recovery Issue**
Okay, hold on a second. My eyes just landed on this line: `error = xlog_rseek_logrec_hdr(...)`. Hmm, if `error` is zero, as in, no records were found by that function... then `xlog_do_recovery_pass` is called. The issue here is that `tmp_rhead_blk` is passed to that function, but it looks like `tmp_rhead_blk` hasn't been initialized in this scenario. That's... a potential problem. This could lead to undefined behavior or, at best, incorrect recovery. I need to check if there's a fix for this in the commit history or in any related discussions. It's a classic uninitialized variable issue, but critical in the recovery path. I need to understand if the caller is supposed to handle the zero error or if this is a genuine bug.
|
| 23/2 |
2026/05/30 22:30 |
tool |
git-log |
0m
Args: map[CodeRegexp:tmp_rhead_blk Count:10 PathPrefix:fs/xfs/xfs_log_recover.c]
Results: map[Output:6e9b3dd80f9c xfs: stop using bp naming for log recovery buffers
7088c4136fa1 xfs: detect and trim torn writes during log recovery
]
|
| 24/2 |
2026/05/30 22:30 |
llm |
expert |
0m
Model:
Tokens:
input: 15871
output: 45
thoughts: 33
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience:
**Current State Assessment**
Alright, so the immediate situation is clear: there's *no* commit in the repository that addresses this issue. The bug, whatever it is, remains active within the main codebase. We're still staring at the problem. My next step, based on this, is to dive straight into the relevant recovery path. I'm going to start by examining `xlog_do_recovery_pass`. That's where the failure needs to be addressed.
|
| 25/2 |
2026/05/30 22:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_do_recovery_pass]
Results: map[SourceCode:2988: /*
2989: * Read the log from tail to head and process the log records found.
2990: * Handle the two cases where the tail and head are in the same cycle
2991: * and where the active portion of the log wraps around the end of
2992: * the physical log separately. The pass parameter is passed through
2993: * to the routines called to process the data and is not looked at
2994: * here.
2995: */
2996: STATIC int
2997: xlog_do_recovery_pass(
2998: struct xlog *log,
2999: xfs_daddr_t head_blk,
3000: xfs_daddr_t tail_blk,
3001: int pass,
3002: xfs_daddr_t *first_bad) /* out: first bad log rec */
3003: {
3004: struct xlog_rec_header *rhead;
3005: xfs_daddr_t blk_no, rblk_no;
3006: xfs_daddr_t rhead_blk;
3007: char *offset;
3008: char *hbp, *dbp;
3009: int error = 0, h_size, h_len;
3010: int error2 = 0;
3011: int bblks, split_bblks;
3012: int hblks = 1, split_hblks, wrapped_hblks;
3013: int i;
3014: struct hlist_head rhash[XLOG_RHASH_SIZE];
3015: LIST_HEAD (buffer_list);
3016:
3017: ASSERT(head_blk != tail_blk);
3018: blk_no = rhead_blk = tail_blk;
3019:
3020: for (i = 0; i < XLOG_RHASH_SIZE; i++)
3021: INIT_HLIST_HEAD(&rhash[i]);
3022:
3023: hbp = xlog_alloc_buffer(log, hblks);
3024: if (!hbp)
3025: return -ENOMEM;
3026:
3027: /*
3028: * Read the header of the tail block and get the iclog buffer size from
3029: * h_size. Use this to tell how many sectors make up the log header.
3030: */
3031: if (xfs_has_logv2(log->l_mp)) {
3032: /*
3033: * When using variable length iclogs, read first sector of
3034: * iclog header and extract the header size from it. Get a
3035: * new hbp that is the correct size.
3036: */
3037: error = xlog_bread(log, tail_blk, 1, hbp, &offset);
3038: if (error)
3039: goto bread_err1;
3040:
3041: rhead = (struct xlog_rec_header *)offset;
3042:
3043: /*
3044: * xfsprogs has a bug where record length is based on lsunit but
3045: * h_size (iclog size) is hardcoded to 32k. Now that we
3046: * unconditionally CRC verify the unmount record, this means the
3047: * log buffer can be too small for the record and cause an
3048: * overrun.
3049: *
3050: * Detect this condition here. Use lsunit for the buffer size as
3051: * long as this looks like the mkfs case. Otherwise, return an
3052: * error to avoid a buffer overrun.
3053: */
3054: h_size = be32_to_cpu(rhead->h_size);
3055: h_len = be32_to_cpu(rhead->h_len);
3056: if (h_len > h_size && h_len <= log->l_mp->m_logbsize &&
3057: rhead->h_num_logops == cpu_to_be32(1)) {
3058: xfs_warn(log->l_mp,
3059: "invalid iclog size (%d bytes), using lsunit (%d bytes)",
3060: h_size, log->l_mp->m_logbsize);
3061: h_size = log->l_mp->m_logbsize;
3062: }
3063:
3064: error = xlog_valid_rec_header(log, rhead, tail_blk, h_size);
3065: if (error)
3066: goto bread_err1;
3067:
3068: /*
3069: * This open codes xlog_logrec_hblks so that we can reuse the
3070: * fixed up h_size value calculated above. Without that we'd
3071: * still allocate the buffer based on the incorrect on-disk
3072: * size.
3073: */
3074: if (h_size > XLOG_HEADER_CYCLE_SIZE &&
3075: (rhead->h_version & cpu_to_be32(XLOG_VERSION_2))) {
3076: hblks = DIV_ROUND_UP(h_size, XLOG_HEADER_CYCLE_SIZE);
3077: if (hblks > 1) {
3078: kvfree(hbp);
3079: hbp = xlog_alloc_buffer(log, hblks);
3080: if (!hbp)
3081: return -ENOMEM;
3082: }
3083: }
3084: } else {
3085: ASSERT(log->l_sectBBsize == 1);
3086: h_size = XLOG_BIG_RECORD_BSIZE;
3087: }
3088:
3089: dbp = xlog_alloc_buffer(log, BTOBB(h_size));
3090: if (!dbp) {
3091: kvfree(hbp);
3092: return -ENOMEM;
3093: }
3094:
3095: memset(rhash, 0, sizeof(rhash));
3096: if (tail_blk > head_blk) {
3097: /*
3098: * Perform recovery around the end of the physical log.
3099: * When the head is not on the same cycle number as the tail,
3100: * we can't do a sequential recovery.
3101: */
3102: while (blk_no < log->l_logBBsize) {
3103: /*
3104: * Check for header wrapping around physical end-of-log
3105: */
3106: offset = hbp;
3107: split_hblks = 0;
3108: wrapped_hblks = 0;
3109: if (blk_no + hblks <= log->l_logBBsize) {
3110: /* Read header in one read */
3111: error = xlog_bread(log, blk_no, hblks, hbp,
3112: &offset);
3113: if (error)
3114: goto bread_err2;
3115: } else {
3116: /* This LR is split across physical log end */
3117: if (blk_no != log->l_logBBsize) {
3118: /* some data before physical log end */
3119: ASSERT(blk_no <= INT_MAX);
3120: split_hblks = log->l_logBBsize - (int)blk_no;
3121: ASSERT(split_hblks > 0);
3122: error = xlog_bread(log, blk_no,
3123: split_hblks, hbp,
3124: &offset);
3125: if (error)
3126: goto bread_err2;
3127: }
3128:
3129: /*
3130: * Note: this black magic still works with
3131: * large sector sizes (non-512) only because:
3132: * - we increased the buffer size originally
3133: * by 1 sector giving us enough extra space
3134: * for the second read;
3135: * - the log start is guaranteed to be sector
3136: * aligned;
3137: * - we read the log end (LR header start)
3138: * _first_, then the log start (LR header end)
3139: * - order is important.
3140: */
3141: wrapped_hblks = hblks - split_hblks;
3142: error = xlog_bread_noalign(log, 0,
3143: wrapped_hblks,
3144: offset + BBTOB(split_hblks));
3145: if (error)
3146: goto bread_err2;
3147: }
3148: rhead = (struct xlog_rec_header *)offset;
3149: error = xlog_valid_rec_header(log, rhead,
3150: split_hblks ? blk_no : 0, h_size);
3151: if (error)
3152: goto bread_err2;
3153:
3154: bblks = (int)BTOBB(be32_to_cpu(rhead->h_len));
3155: blk_no += hblks;
3156:
3157: /*
3158: * Read the log record data in multiple reads if it
3159: * wraps around the end of the log. Note that if the
3160: * header already wrapped, blk_no could point past the
3161: * end of the log. The record data is contiguous in
3162: * that case.
3163: */
3164: if (blk_no + bblks <= log->l_logBBsize ||
3165: blk_no >= log->l_logBBsize) {
3166: rblk_no = xlog_wrap_logbno(log, blk_no);
3167: error = xlog_bread(log, rblk_no, bblks, dbp,
3168: &offset);
3169: if (error)
3170: goto bread_err2;
3171: } else {
3172: /* This log record is split across the
3173: * physical end of log */
3174: offset = dbp;
3175: split_bblks = 0;
3176: if (blk_no != log->l_logBBsize) {
3177: /* some data is before the physical
3178: * end of log */
3179: ASSERT(!wrapped_hblks);
3180: ASSERT(blk_no <= INT_MAX);
3181: split_bblks =
3182: log->l_logBBsize - (int)blk_no;
3183: ASSERT(split_bblks > 0);
3184: error = xlog_bread(log, blk_no,
3185: split_bblks, dbp,
3186: &offset);
3187: if (error)
3188: goto bread_err2;
3189: }
3190:
3191: /*
3192: * Note: this black magic still works with
3193: * large sector sizes (non-512) only because:
3194: * - we increased the buffer size originally
3195: * by 1 sector giving us enough extra space
3196: * for the second read;
3197: * - the log start is guaranteed to be sector
3198: * aligned;
3199: * - we read the log end (LR header start)
3200: * _first_, then the log start (LR header end)
3201: * - order is important.
3202: */
3203: error = xlog_bread_noalign(log, 0,
3204: bblks - split_bblks,
3205: offset + BBTOB(split_bblks));
3206: if (error)
3207: goto bread_err2;
3208: }
3209:
3210: error = xlog_recover_process(log, rhash, rhead, offset,
3211: pass, &buffer_list);
3212: if (error)
3213: goto bread_err2;
3214:
3215: blk_no += bblks;
3216: rhead_blk = blk_no;
3217: }
3218:
3219: ASSERT(blk_no >= log->l_logBBsize);
3220: blk_no -= log->l_logBBsize;
3221: rhead_blk = blk_no;
3222: }
3223:
3224: /* read first part of physical log */
3225: while (blk_no < head_blk) {
3226: error = xlog_bread(log, blk_no, hblks, hbp, &offset);
3227: if (error)
3228: goto bread_err2;
3229:
3230: rhead = (struct xlog_rec_header *)offset;
3231: error = xlog_valid_rec_header(log, rhead, blk_no, h_size);
3232: if (error)
3233: goto bread_err2;
3234:
3235: /* blocks in data section */
3236: bblks = (int)BTOBB(be32_to_cpu(rhead->h_len));
3237: error = xlog_bread(log, blk_no+hblks, bblks, dbp,
3238: &offset);
3239: if (error)
3240: goto bread_err2;
3241:
3242: error = xlog_recover_process(log, rhash, rhead, offset, pass,
3243: &buffer_list);
3244: if (error)
3245: goto bread_err2;
3246:
3247: blk_no += bblks + hblks;
3248: rhead_blk = blk_no;
3249: }
3250:
3251: bread_err2:
3252: kvfree(dbp);
3253: bread_err1:
3254: kvfree(hbp);
3255:
3256: /*
3257: * Submit buffers that have been dirtied by the last record recovered.
3258: */
3259: if (!list_empty(&buffer_list)) {
3260: if (error) {
3261: /*
3262: * If there has been an item recovery error then we
3263: * cannot allow partial checkpoint writeback to
3264: * occur. We might have multiple checkpoints with the
3265: * same start LSN in this buffer list, and partial
3266: * writeback of a checkpoint in this situation can
3267: * prevent future recovery of all the changes in the
3268: * checkpoints at this start LSN.
3269: *
3270: * Note: Shutting down the filesystem will result in the
3271: * delwri submission marking all the buffers stale,
3272: * completing them and cleaning up _XBF_LOGRECOVERY
3273: * state without doing any IO.
3274: */
3275: xlog_force_shutdown(log, SHUTDOWN_LOG_IO_ERROR);
3276: }
3277: error2 = xfs_buf_delwri_submit(&buffer_list);
3278: }
3279:
3280: if (error && first_bad)
3281: *first_bad = rhead_blk;
3282:
3283: /*
3284: * Transactions are freed at commit time but transactions without commit
3285: * records on disk are never committed. Free any that may be left in the
3286: * hash table.
3287: */
3288: for (i = 0; i < XLOG_RHASH_SIZE; i++) {
3289: struct hlist_node *tmp;
3290: struct xlog_recover *trans;
3291:
3292: hlist_for_each_entry_safe(trans, tmp, &rhash[i], r_list)
3293: xlog_recover_free_trans(trans);
3294: }
3295:
3296: return error ? error : error2;
3297: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 26/2 |
2026/05/30 22:30 |
llm |
expert |
0m
Model:
Tokens:
input: 20941
output: 41
thoughts: 1430
Thoughts: **Vulnerability Analysis: Uninitialized Variable in XFS Log Recovery**
Okay, here's what's bothering me. I've been tracing through the XFS log recovery code, and I've spotted a critical vulnerability related to an uninitialized variable, `tail_blk`.
It starts in `xlog_do_recovery_pass`. The critical thing is that `tail_blk` can be uninitialized.
```c
STATIC int
xlog_do_recovery_pass(
struct xlog *log,
xfs_daddr_t head_blk,
xfs_daddr_t tail_blk,
int pass,
xfs_daddr_t *first_bad)
```
Initially, `tail_blk` is assigned its value from the input.
```c
blk_no = rhead_blk = tail_blk;
```
Then, the code uses `tail_blk` immediately in `xlog_bread`:
```c
if (xfs_has_logv2(log->l_mp)) {
error = xlog_bread(log, tail_blk, 1, hbp, &offset);
```
The problem is, if `tail_blk` is uninitialized, the call to `xlog_bread` will read from a random block number. That's bad. I immediately see the potential for a read out-of-bounds or an invalid block, returning an error, or, even worse, reading garbage data that could corrupt the log recovery process.
Let's look at how `tail_blk` gets its value. It's set, through `tmp_rhead_blk` in `xlog_verify_head`.
```c
error = xlog_rseek_logrec_hdr(log, *head_blk, *tail_blk,
XLOG_MAX_ICLOGS, tmp_buffer,
&tmp_rhead_blk, &tmp_rhead, &tmp_wrapped);
kvfree(tmp_buffer);
if (error < 0)
return error;
```
Now, `xlog_rseek_logrec_hdr` returns an integer. What happens if `error` is zero? Then, `tmp_rhead_blk` is uninitialized!
I needed to check if error can be zero.
Let's trace into `xlog_rseek_logrec_hdr`.
```c
STATIC int
xlog_rseek_logrec_hdr(
struct xlog *log,
xfs_daddr_t head_blk,
xfs_daddr_t tail_blk,
int count,
char *buffer,
xfs_daddr_t *rblk,
struct xlog_rec_header **rhead,
bool *wrapped)
```
The key here is how the algorithm determines `tmp_rhead_blk`.
```c
end_blk = head_blk > tail_blk ? tail_blk : 0;
for (i = (int) head_blk - 1; i >= end_blk; i--) {
```
If `head_blk` is 0, then `end_blk` is also 0. So the loop condition is `i >= 0`. The loop is then not entered when `head_blk == 0`.
However, if `tail_blk >= head_blk`, then the second loop executes.
If `tail_blk` is 0, the second loop executes from `log->l_logBBsize -1` backwards to 0, if `head_blk == 0`.
If none of the blocks in the log have the expected `XLOG_HEADER_MAGIC_NUM` signature, then the loop never finds it.
If no block contains the `XLOG_HEADER_MAGIC_NUM`, `found` remains at zero, and `xlog_rseek_logrec_hdr` returns zero! So that's the case where `tmp_rhead_blk` is *not* initialized.
Going back to `xlog_do_recovery_pass`, if `error` from `xlog_rseek_logrec_hdr` is zero, `tmp_rhead_blk` is uninitialized. `tail_blk` becomes `tmp_rhead_blk`.
This `tail_blk` is then used directly in `xlog_bread`, and the same error occurs.
So, in summary, if the log search in `xlog_rseek
|
| 27/2 |
2026/05/30 22:30 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_bread]
Results: map[SourceCode: 161: STATIC int
162: xlog_bread(
163: struct xlog *log,
164: xfs_daddr_t blk_no,
165: int nbblks,
166: char *data,
167: char **offset)
168: {
169: int error;
170:
171: error = xlog_do_io(log, blk_no, nbblks, data, REQ_OP_READ);
172: if (!error)
173: *offset = data + xlog_align(log, blk_no);
174: return error;
175: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 28/2 |
2026/05/30 22:30 |
llm |
expert |
1m
Model:
Tokens:
input: 23679
output: 43
thoughts: 12
Thoughts: Okay, let's dive into this. Here's my thought process summarized, assuming I'm already pretty familiar with the subject matter:
**Analyzing `xlog_do_io`**
Right, let's take a look at `xlog_do_io`. I need to get a clear picture of how this function is functioning, what its purpose is, and its relationship to the broader system. I'm going to want to understand its inputs, outputs, any side effects, and of course, how it interacts with other modules or routines. I will likely focus on the critical parts for performance and the areas that may cause contention or bottlenecks. Time to trace the function and see what's going on...
|
| 29/2 |
2026/05/30 22:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_do_io]
Results: map[SourceCode: 119: static int
120: xlog_do_io(
121: struct xlog *log,
122: xfs_daddr_t blk_no,
123: unsigned int nbblks,
124: char *data,
125: enum req_op op)
126: {
127: int error;
128:
129: if (XFS_IS_CORRUPT(log->l_mp, !xlog_verify_bno(log, blk_no, nbblks))) {
130: xfs_warn(log->l_mp,
131: "Invalid log block/length (0x%llx, 0x%x) for buffer",
132: blk_no, nbblks);
133: return -EFSCORRUPTED;
134: }
135:
136: blk_no = round_down(blk_no, log->l_sectBBsize);
137: nbblks = round_up(nbblks, log->l_sectBBsize);
138: ASSERT(nbblks > 0);
139:
140: error = xfs_rw_bdev(log->l_targ->bt_bdev, log->l_logBBstart + blk_no,
141: BBTOB(nbblks), data, op);
142: if (error && !xlog_is_shutdown(log)) {
143: xfs_alert(log->l_mp,
144: "log recovery %s I/O error at daddr 0x%llx len %d error %d",
145: op == REQ_OP_WRITE ? "write" : "read",
146: blk_no, nbblks, error);
147: }
148: return error;
149: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 30/2 |
2026/05/30 22:31 |
llm |
expert |
0m
Model:
Tokens:
input: 24459
output: 44
thoughts: 129
Thoughts: **Analyzing Potential Log Corruption**
Okay, so I'm looking at `xlog_do_io` in the XFS code. The first thing that jumps out is this check: `XFS_IS_CORRUPT`. It's pretty clear that if `xlog_verify_bno` returns something invalid, it's flagged as potentially corrupted data. My job here is to ensure that block numbers and lengths are valid before proceeding.
We've got a problem if the block number (`blk_no`) and the number of blocks (`nbblks`) are inconsistent with what we'd expect based on the XFS log structure. The `xfs_warn` call is crucial; it's logging a warning, including the problematic block number and length, which is vital for debugging any FS corruption issues. I'll need to double check the error code `-EFSCORRUPTED`, to ensure it is handled correctly within the file system code. This will be the key to triggering a failure of the I/O if the log data looks suspicious.
So, the next logical step is to dive into `xlog_verify_bno` itself to understand precisely what criteria are used to determine if a block number is valid and the related block length, as this is the linchpin to the error detection. This looks to be a common pattern; it is likely an important place to look for subtle bugs or potential vulnerabilities.
|
| 31/2 |
2026/05/30 22:31 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_verify_bno]
Results: map[SourceCode: 50: /*
51: * Verify the log-relative block number and length in basic blocks are valid for
52: * an operation involving the given XFS log buffer. Returns true if the fields
53: * are valid, false otherwise.
54: */
55: static inline bool
56: xlog_verify_bno(
57: struct xlog *log,
58: xfs_daddr_t blk_no,
59: int bbcount)
60: {
61: if (blk_no < 0 || blk_no >= log->l_logBBsize)
62: return false;
63: if (bbcount <= 0 || (blk_no + bbcount) > log->l_logBBsize)
64: return false;
65: return true;
66: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 32/2 |
2026/05/30 22:31 |
llm |
expert |
0m
Model:
Tokens:
input: 25205
output: 45
thoughts: 1503
Thoughts: **My Analysis of the Uninitialized Value Bug**
Okay, so I'm digging into this code and trying to figure out what's going on with this `blk_no` variable. It looks like `blk_no` is actually derived from several other variables like `tail_blk` and `tmp_rhead_blk`, but the crucial point is that it can be uninitialized. If this value, which represents a block number, is out of bounds, the function gracefully returns an error, specifically `-EFSCORRUPTED`. If the value happens to fall within the valid range for the log, then the code *reads a block from disk based on this potentially random, uninitialized value*. This seems really dodgy.
My initial thought was that this uninitialized value could lead to an information leak, potentially revealing sensitive kernel information, and that it could cause a memory corruption. But it appears it doesn't cause a direct memory corruption because the value is just used as a block number to read a disk block. Then, if the log record that is read is somehow valid, but malicious, the mount *might* fail, or the file system *might* get corrupted.
The vulnerability stems from the fact that an attacker can craft a malicious filesystem image that, when mounted, triggers this uninitialized read. The scenario is that the function `xlog_verify_head` is called during the mount process. An attacker can create a filesystem with a log that essentially contains zero valid records, which can happen if there are no blocks containing `XLOG_HEADER_MAGIC_NUM`. When the system tries to read the log header, it encounters this undefined state and the uninitialized `blk_no` is used.
I did some digging, and it turns out the uninitialized value is from the kernel stack, which means we might be able to leak the content of the kernel stack if the uninitialized value is used in a specific way. Checking the code of `xlog_do_io` revealed that if `blk_no` is out of bounds, the *value of `blk_no` gets printed to the kernel log*! Eureka! This is a kernel stack information leak. An attacker can now mount a malicious image, read the kernel log (dmesg), and discover this uninitialized value, defeating KASLR or potentially revealing kernel pointers.
However, after careful review, I don't see how this information leak directly translates to a *memory corruption* or *privilege escalation*. The uninitialized value isn't used to create or dereference pointers in memory; it is only used to compute an offset on the disk, and then the content of the block is read to a buffer.
So, while KMSAN would flag this, in a production environment, the worst-case scenario is a warning in the logs and a failed mount. It's not a Denial of Service because the mount fails and doesn't crash or hang, which makes the situation less severe.
The key question is: is this "exploitable?" The prompt says that uninitialized values might be exploitable. It further says that it depends on the use of the uninitialized value. Since I've determined the uninitialized value leads to a stack information leak, is it exploitable? Based on the prompt, it needs to result in memory corruption or privilege escalation, and I don't see that here. It's an infoleak. The infoleak itself doesn't cause memory corruption or privilege escalation, but could be a stepping stone. However, with this specific example, the attacker controls the filesystem image, so controlling `blk_no` to read a random block doesn't give the attacker additional capabilities.
I'm leaning towards *Exploitable = False*, since the information leak itself doesn't lead to memory corruption or privilege escalation.
|
| 33/2 |
2026/05/30 22:32 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_do_recovery_pass]
Results: map[SourceCode:2988: /*
2989: * Read the log from tail to head and process the log records found.
2990: * Handle the two cases where the tail and head are in the same cycle
2991: * and where the active portion of the log wraps around the end of
2992: * the physical log separately. The pass parameter is passed through
2993: * to the routines called to process the data and is not looked at
2994: * here.
2995: */
2996: STATIC int
2997: xlog_do_recovery_pass(
2998: struct xlog *log,
2999: xfs_daddr_t head_blk,
3000: xfs_daddr_t tail_blk,
3001: int pass,
3002: xfs_daddr_t *first_bad) /* out: first bad log rec */
3003: {
3004: struct xlog_rec_header *rhead;
3005: xfs_daddr_t blk_no, rblk_no;
3006: xfs_daddr_t rhead_blk;
3007: char *offset;
3008: char *hbp, *dbp;
3009: int error = 0, h_size, h_len;
3010: int error2 = 0;
3011: int bblks, split_bblks;
3012: int hblks = 1, split_hblks, wrapped_hblks;
3013: int i;
3014: struct hlist_head rhash[XLOG_RHASH_SIZE];
3015: LIST_HEAD (buffer_list);
3016:
3017: ASSERT(head_blk != tail_blk);
3018: blk_no = rhead_blk = tail_blk;
3019:
3020: for (i = 0; i < XLOG_RHASH_SIZE; i++)
3021: INIT_HLIST_HEAD(&rhash[i]);
3022:
3023: hbp = xlog_alloc_buffer(log, hblks);
3024: if (!hbp)
3025: return -ENOMEM;
3026:
3027: /*
3028: * Read the header of the tail block and get the iclog buffer size from
3029: * h_size. Use this to tell how many sectors make up the log header.
3030: */
3031: if (xfs_has_logv2(log->l_mp)) {
3032: /*
3033: * When using variable length iclogs, read first sector of
3034: * iclog header and extract the header size from it. Get a
3035: * new hbp that is the correct size.
3036: */
3037: error = xlog_bread(log, tail_blk, 1, hbp, &offset);
3038: if (error)
3039: goto bread_err1;
3040:
3041: rhead = (struct xlog_rec_header *)offset;
3042:
3043: /*
3044: * xfsprogs has a bug where record length is based on lsunit but
3045: * h_size (iclog size) is hardcoded to 32k. Now that we
3046: * unconditionally CRC verify the unmount record, this means the
3047: * log buffer can be too small for the record and cause an
3048: * overrun.
3049: *
3050: * Detect this condition here. Use lsunit for the buffer size as
3051: * long as this looks like the mkfs case. Otherwise, return an
3052: * error to avoid a buffer overrun.
3053: */
3054: h_size = be32_to_cpu(rhead->h_size);
3055: h_len = be32_to_cpu(rhead->h_len);
3056: if (h_len > h_size && h_len <= log->l_mp->m_logbsize &&
3057: rhead->h_num_logops == cpu_to_be32(1)) {
3058: xfs_warn(log->l_mp,
3059: "invalid iclog size (%d bytes), using lsunit (%d bytes)",
3060: h_size, log->l_mp->m_logbsize);
3061: h_size = log->l_mp->m_logbsize;
3062: }
3063:
3064: error = xlog_valid_rec_header(log, rhead, tail_blk, h_size);
3065: if (error)
3066: goto bread_err1;
3067:
3068: /*
3069: * This open codes xlog_logrec_hblks so that we can reuse the
3070: * fixed up h_size value calculated above. Without that we'd
3071: * still allocate the buffer based on the incorrect on-disk
3072: * size.
3073: */
3074: if (h_size > XLOG_HEADER_CYCLE_SIZE &&
3075: (rhead->h_version & cpu_to_be32(XLOG_VERSION_2))) {
3076: hblks = DIV_ROUND_UP(h_size, XLOG_HEADER_CYCLE_SIZE);
3077: if (hblks > 1) {
3078: kvfree(hbp);
3079: hbp = xlog_alloc_buffer(log, hblks);
3080: if (!hbp)
3081: return -ENOMEM;
3082: }
3083: }
3084: } else {
3085: ASSERT(log->l_sectBBsize == 1);
3086: h_size = XLOG_BIG_RECORD_BSIZE;
3087: }
3088:
3089: dbp = xlog_alloc_buffer(log, BTOBB(h_size));
3090: if (!dbp) {
3091: kvfree(hbp);
3092: return -ENOMEM;
3093: }
3094:
3095: memset(rhash, 0, sizeof(rhash));
3096: if (tail_blk > head_blk) {
3097: /*
3098: * Perform recovery around the end of the physical log.
3099: * When the head is not on the same cycle number as the tail,
3100: * we can't do a sequential recovery.
3101: */
3102: while (blk_no < log->l_logBBsize) {
3103: /*
3104: * Check for header wrapping around physical end-of-log
3105: */
3106: offset = hbp;
3107: split_hblks = 0;
3108: wrapped_hblks = 0;
3109: if (blk_no + hblks <= log->l_logBBsize) {
3110: /* Read header in one read */
3111: error = xlog_bread(log, blk_no, hblks, hbp,
3112: &offset);
3113: if (error)
3114: goto bread_err2;
3115: } else {
3116: /* This LR is split across physical log end */
3117: if (blk_no != log->l_logBBsize) {
3118: /* some data before physical log end */
3119: ASSERT(blk_no <= INT_MAX);
3120: split_hblks = log->l_logBBsize - (int)blk_no;
3121: ASSERT(split_hblks > 0);
3122: error = xlog_bread(log, blk_no,
3123: split_hblks, hbp,
3124: &offset);
3125: if (error)
3126: goto bread_err2;
3127: }
3128:
3129: /*
3130: * Note: this black magic still works with
3131: * large sector sizes (non-512) only because:
3132: * - we increased the buffer size originally
3133: * by 1 sector giving us enough extra space
3134: * for the second read;
3135: * - the log start is guaranteed to be sector
3136: * aligned;
3137: * - we read the log end (LR header start)
3138: * _first_, then the log start (LR header end)
3139: * - order is important.
3140: */
3141: wrapped_hblks = hblks - split_hblks;
3142: error = xlog_bread_noalign(log, 0,
3143: wrapped_hblks,
3144: offset + BBTOB(split_hblks));
3145: if (error)
3146: goto bread_err2;
3147: }
3148: rhead = (struct xlog_rec_header *)offset;
3149: error = xlog_valid_rec_header(log, rhead,
3150: split_hblks ? blk_no : 0, h_size);
3151: if (error)
3152: goto bread_err2;
3153:
3154: bblks = (int)BTOBB(be32_to_cpu(rhead->h_len));
3155: blk_no += hblks;
3156:
3157: /*
3158: * Read the log record data in multiple reads if it
3159: * wraps around the end of the log. Note that if the
3160: * header already wrapped, blk_no could point past the
3161: * end of the log. The record data is contiguous in
3162: * that case.
3163: */
3164: if (blk_no + bblks <= log->l_logBBsize ||
3165: blk_no >= log->l_logBBsize) {
3166: rblk_no = xlog_wrap_logbno(log, blk_no);
3167: error = xlog_bread(log, rblk_no, bblks, dbp,
3168: &offset);
3169: if (error)
3170: goto bread_err2;
3171: } else {
3172: /* This log record is split across the
3173: * physical end of log */
3174: offset = dbp;
3175: split_bblks = 0;
3176: if (blk_no != log->l_logBBsize) {
3177: /* some data is before the physical
3178: * end of log */
3179: ASSERT(!wrapped_hblks);
3180: ASSERT(blk_no <= INT_MAX);
3181: split_bblks =
3182: log->l_logBBsize - (int)blk_no;
3183: ASSERT(split_bblks > 0);
3184: error = xlog_bread(log, blk_no,
3185: split_bblks, dbp,
3186: &offset);
3187: if (error)
3188: goto bread_err2;
3189: }
3190:
3191: /*
3192: * Note: this black magic still works with
3193: * large sector sizes (non-512) only because:
3194: * - we increased the buffer size originally
3195: * by 1 sector giving us enough extra space
3196: * for the second read;
3197: * - the log start is guaranteed to be sector
3198: * aligned;
3199: * - we read the log end (LR header start)
3200: * _first_, then the log start (LR header end)
3201: * - order is important.
3202: */
3203: error = xlog_bread_noalign(log, 0,
3204: bblks - split_bblks,
3205: offset + BBTOB(split_bblks));
3206: if (error)
3207: goto bread_err2;
3208: }
3209:
3210: error = xlog_recover_process(log, rhash, rhead, offset,
3211: pass, &buffer_list);
3212: if (error)
3213: goto bread_err2;
3214:
3215: blk_no += bblks;
3216: rhead_blk = blk_no;
3217: }
3218:
3219: ASSERT(blk_no >= log->l_logBBsize);
3220: blk_no -= log->l_logBBsize;
3221: rhead_blk = blk_no;
3222: }
3223:
3224: /* read first part of physical log */
3225: while (blk_no < head_blk) {
3226: error = xlog_bread(log, blk_no, hblks, hbp, &offset);
3227: if (error)
3228: goto bread_err2;
3229:
3230: rhead = (struct xlog_rec_header *)offset;
3231: error = xlog_valid_rec_header(log, rhead, blk_no, h_size);
3232: if (error)
3233: goto bread_err2;
3234:
3235: /* blocks in data section */
3236: bblks = (int)BTOBB(be32_to_cpu(rhead->h_len));
3237: error = xlog_bread(log, blk_no+hblks, bblks, dbp,
3238: &offset);
3239: if (error)
3240: goto bread_err2;
3241:
3242: error = xlog_recover_process(log, rhash, rhead, offset, pass,
3243: &buffer_list);
3244: if (error)
3245: goto bread_err2;
3246:
3247: blk_no += bblks + hblks;
3248: rhead_blk = blk_no;
3249: }
3250:
3251: bread_err2:
3252: kvfree(dbp);
3253: bread_err1:
3254: kvfree(hbp);
3255:
3256: /*
3257: * Submit buffers that have been dirtied by the last record recovered.
3258: */
3259: if (!list_empty(&buffer_list)) {
3260: if (error) {
3261: /*
3262: * If there has been an item recovery error then we
3263: * cannot allow partial checkpoint writeback to
3264: * occur. We might have multiple checkpoints with the
3265: * same start LSN in this buffer list, and partial
3266: * writeback of a checkpoint in this situation can
3267: * prevent future recovery of all the changes in the
3268: * checkpoints at this start LSN.
3269: *
3270: * Note: Shutting down the filesystem will result in the
3271: * delwri submission marking all the buffers stale,
3272: * completing them and cleaning up _XBF_LOGRECOVERY
3273: * state without doing any IO.
3274: */
3275: xlog_force_shutdown(log, SHUTDOWN_LOG_IO_ERROR);
3276: }
3277: error2 = xfs_buf_delwri_submit(&buffer_list);
3278: }
3279:
3280: if (error && first_bad)
3281: *first_bad = rhead_blk;
3282:
3283: /*
3284: * Transactions are freed at commit time but transactions without commit
3285: * records on disk are never committed. Free any that may be left in the
3286: * hash table.
3287: */
3288: for (i = 0; i < XLOG_RHASH_SIZE; i++) {
3289: struct hlist_node *tmp;
3290: struct xlog_recover *trans;
3291:
3292: hlist_for_each_entry_safe(trans, tmp, &rhash[i], r_list)
3293: xlog_recover_free_trans(trans);
3294: }
3295:
3296: return error ? error : error2;
3297: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 34/2 |
2026/05/30 22:32 |
llm |
expert |
0m
Model:
Tokens:
input: 32422
output: 32
thoughts: 655
Thoughts: **Analyzing Potential Issues in Uninitialized Block Number Handling**
Okay, so here's the deal: I've been looking at a section of code where `blk_no` might be uninitialized. It seems like if `tail_blk` is uninitialized, the code *also* assumes `blk_no` is uninitialized, which is a bit of a red flag. Specifically, I'm thinking about this loop:
```c
if (tail_blk > head_blk) {
while (blk_no < log->l_logBBsize) {
```
If `tail_blk` *is* uninitialized and happens to be greater than `head_blk`, the loop condition will be met and the loop will execute with the unitialized `blk_no`. The thing is, the code then uses `blk_no` in `xlog_bread` as a block number. `xlog_bread` calls `xlog_verify_bno` to check it.
My initial thought was that a potential out-of-bounds read is possible. But, `xlog_bread` should check `blk_no`'s bounds using `xlog_verify_bno`. If it's out of bounds, I should see `-EFSCORRUPTED` being returned. That triggers an error path, cleans things up, and reports the error. So I'm not seeing an immediate buffer overflow here. Even if `blk_no` *is* within bounds, `xlog_valid_rec_header` should then kick in and validate the header of the block. If that fails, it also leads to an error return. Even if a block *happens* to have a valid header and is read, the loop would process the block out of order and this might cause confusion or inconsistencies, but since the attacker controls the underlying disk anyway, it's not a security vulnerability. It's unlikely to be exploitable, since anything the attacker does this way they could achieve more easily by writing the log record at the expected place.
But wait, what if `blk_no` is *negative*? `xfs_daddr_t` is `int64_t`. If it's negative, then `xlog_verify_bno` will immediately return `false`, which would mean `xlog_bread` and thus `xlog_do_io` returns `-EFSCORRUPTED`. This should be safe; it's handled.
Now, I need to consider Denial of Service. Does a return of `-EFSCORRUPTED` cause a denial of service? In this situation, the mount fails. Well, failing to mount a malicious filesystem isn't a DoS, it's the *expected* behavior. It's working as designed to protect the system. So no, this isn't a DoS.
So, let's look at the triggers and see if it is something that can be initiated by an unprivileged user. Mounting requires `CAP_SYS_ADMIN` in the initial user namespace or, potentially, in a user namespace. The critical question: Can an unprivileged user mount XFS *within* a user namespace? If so, this could be exploited. Let's see if XFS sets `FS_USERNS_MOUNT`!
|
| 35/2 |
2026/05/30 22:32 |
tool |
grepper |
0m
Error:
Args: map[Expression:FS_USERNS_MOUNT PathPrefix:fs/xfs/]
Results:
|
| 36/2 |
2026/05/30 22:32 |
llm |
expert |
0m
Model:
Tokens:
input: 33831
output: 41
thoughts: 370
Thoughts: **My Assessment of the XFS Vulnerability**
Okay, let's break this down systematically. The core question is whether this XFS bug can be exploited in various scenarios. First, it appears that XFS, by default, doesn't set `FS_USERNS_MOUNT`, so it can't be mounted in a user namespace by an unprivileged user. That directly answers the initial query: the prompt specifies that we should assume user namespaces aren't accessible.
The prompt breaks down the questions for the vulnerability assessment into "Accessible From Unprivileged Processes" and "Accessible From User Namespaces".
Since XFS *can't* be mounted within a user namespace by an unprivileged user, the user namespace trigger is automatically a "False". No dice there.
But wait a minute, the prompt also says, "Assume that user namespaces are not accessible". So, since XFS *also* can't be mounted by an unprivileged user, period, then "Unprivileged = False" as well. I'm noting that a loop device setup would also require `CAP_SYS_ADMIN`, and so is also not an option for unprivileged users.
Next, the prompt asks about a "malicious filesystem trigger". Ah, that's straightforward. If a malicious XFS filesystem image is mounted, it would trigger the bug. So "FilesystemTrigger = True".
The question about a "PeripheralTrigger" is an important one. If a user connects a USB drive containing a compromised XFS filesystem, and the desktop environment automatically mounts it, that's a direct route to exploitation. So, "PeripheralTrigger = True".
"VMGuestTrigger"? No, this is a filesystem bug, not related to virtualization directly. So, "VMGuestTrigger = False." The same logic applies to "VMHostTrigger" and "NetworkTrigger" and "RemoteTrigger". All of those are "False".
Let me just quickly double-check that first point about XFS and user namespaces... I'll check `fs/xfs/xfs_super.c`.
|
| 37/2 |
2026/05/30 22:32 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_super.c Name:xfs_fs_type]
Results: map[SourceCode:2288: static struct file_system_type xfs_fs_type = {
2289: .owner = THIS_MODULE,
2290: .name = "xfs",
2291: .init_fs_context = xfs_init_fs_context,
2292: .parameters = xfs_fs_parameters,
2293: .kill_sb = xfs_kill_sb,
2294: .fs_flags = FS_REQUIRES_DEV | FS_ALLOW_IDMAP | FS_MGTIME |
2295: FS_LBS,
2296: };
SourceFile:fs/xfs/xfs_super.c]
|
| 38/2 |
2026/05/30 22:32 |
llm |
expert |
1m
Model:
Tokens:
input: 34876
output: 43
thoughts: 1842
Thoughts: **Analysis of the XFS Log Verification Vulnerability**
Okay, so the initial issue is the uninitialized `tmp_rhead_blk` in `xlog_verify_head`, and it's being passed around as `tail_blk` in `xlog_do_recovery_pass`. Given `FS_USERNS_MOUNT` is absent, the user namespace aspect is irrelevant, so this is just about standard mounting. My initial thought was that this looks like an infoleak, since the attacker already has disk access, reading a random block *should* be relatively benign, just leaking uninitialized memory. KMSAN does flag uses of unitialized memory, but doesn't mean that it's always exploitable, it's just a starting point. So, I flagged it as not exploitable, because it is only reading a random block, and the attacker already has full control of the disk. I can discount elevation of privilege, or memory corruption. Denial of Service is also initially discounted as the mount fails as is expected if the filesystem is corrupted.
However, the more I dug, the more concerning it gets. I noted an alternative code path in `xlog_verify_head` that is hit if `xlog_do_recovery_pass` returns `-EFSCORRUPTED`. If `tmp_rhead_blk` is out of bounds, `first_bad` might not be set in `xlog_do_recovery_pass`. The critical path here is in `xlog_verify_head`, specifically around the error handling. I see that `first_bad` is set to `tmp_rhead_blk`. This value is then printed to the kernel log as a result of a CRC failure. Then `xlog_rseek_logrec_hdr` is called.
Here's where it gets interesting. `xlog_rseek_logrec_hdr`'s behavior depends on the sign of `first_bad`. If `first_bad` is a huge positive number, the search loop will effectively be skipped and the mount will still fail, as expected, but this may cause a lot of CPU load. BUT, if `first_bad` is a huge negative number, things change. `xfs_daddr_t` being an `int64_t` is key here. Because `end_blk` will be zero, if `first_bad` is negative, the search loop in `xlog_rseek_logrec_hdr` doesn't execute in a way that we expect. Instead, it seems to iterate from the tail to `tail_blk`. This is a valid search range.
The problem is that the loop in `xlog_rseek_logrec_hdr` *can* succeed in finding a valid record! This means that `found` becomes `1`. Then `xlog_verify_head` updates `*head_blk` to `first_bad` and returns 0. *This means we now have a negative `*head_blk` and a successful mount*. The mount then continues with the negative value. That seems like an exploitable vulnerability. I think I have a bug!
I need to investigate `xlog_find_tail` to see what is likely to happen now that we have a negative value in `*head_blk`!
|
| 39/2 |
2026/05/30 22:33 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_find_tail]
Results: map[SourceCode:1218: /*
1219: * Find the sync block number or the tail of the log.
1220: *
1221: * This will be the block number of the last record to have its
1222: * associated buffers synced to disk. Every log record header has
1223: * a sync lsn embedded in it. LSNs hold block numbers, so it is easy
1224: * to get a sync block number. The only concern is to figure out which
1225: * log record header to believe.
1226: *
1227: * The following algorithm uses the log record header with the largest
1228: * lsn. The entire log record does not need to be valid. We only care
1229: * that the header is valid.
1230: *
1231: * We could speed up search by using current head_blk buffer, but it is not
1232: * available.
1233: */
1234: STATIC int
1235: xlog_find_tail(
1236: struct xlog *log,
1237: xfs_daddr_t *head_blk,
1238: xfs_daddr_t *tail_blk)
1239: {
1240: struct xlog_rec_header *rhead;
1241: char *offset = NULL;
1242: char *buffer;
1243: int error;
1244: xfs_daddr_t rhead_blk;
1245: xfs_lsn_t tail_lsn;
1246: bool wrapped = false;
1247: bool clean = false;
1248:
1249: /*
1250: * Find previous log record
1251: */
1252: if ((error = xlog_find_head(log, head_blk)))
1253: return error;
1254: ASSERT(*head_blk < INT_MAX);
1255:
1256: buffer = xlog_alloc_buffer(log, 1);
1257: if (!buffer)
1258: return -ENOMEM;
1259: if (*head_blk == 0) { /* special case */
1260: error = xlog_bread(log, 0, 1, buffer, &offset);
1261: if (error)
1262: goto done;
1263:
1264: if (xlog_get_cycle(offset) == 0) {
1265: *tail_blk = 0;
1266: /* leave all other log inited values alone */
1267: goto done;
1268: }
1269: }
1270:
1271: /*
1272: * Search backwards through the log looking for the log record header
1273: * block. This wraps all the way back around to the head so something is
1274: * seriously wrong if we can't find it.
1275: */
1276: error = xlog_rseek_logrec_hdr(log, *head_blk, *head_blk, 1, buffer,
1277: &rhead_blk, &rhead, &wrapped);
1278: if (error < 0)
1279: goto done;
1280: if (!error) {
1281: xfs_warn(log->l_mp, "%s: couldn't find sync record", __func__);
1282: error = -EFSCORRUPTED;
1283: goto done;
1284: }
1285: *tail_blk = BLOCK_LSN(be64_to_cpu(rhead->h_tail_lsn));
1286:
1287: /*
1288: * Set the log state based on the current head record.
1289: */
1290: xlog_set_state(log, *head_blk, rhead, rhead_blk, wrapped);
1291: tail_lsn = atomic64_read(&log->l_tail_lsn);
1292:
1293: /*
1294: * Look for an unmount record at the head of the log. This sets the log
1295: * state to determine whether recovery is necessary.
1296: */
1297: error = xlog_check_unmount_rec(log, head_blk, tail_blk, rhead,
1298: rhead_blk, buffer, &clean);
1299: if (error)
1300: goto done;
1301:
1302: /*
1303: * Verify the log head if the log is not clean (e.g., we have anything
1304: * but an unmount record at the head). This uses CRC verification to
1305: * detect and trim torn writes. If discovered, CRC failures are
1306: * considered torn writes and the log head is trimmed accordingly.
1307: *
1308: * Note that we can only run CRC verification when the log is dirty
1309: * because there's no guarantee that the log data behind an unmount
1310: * record is compatible with the current architecture.
1311: */
1312: if (!clean) {
1313: xfs_daddr_t orig_head = *head_blk;
1314:
1315: error = xlog_verify_head(log, head_blk, tail_blk, buffer,
1316: &rhead_blk, &rhead, &wrapped);
1317: if (error)
1318: goto done;
1319:
1320: /* update in-core state again if the head changed */
1321: if (*head_blk != orig_head) {
1322: xlog_set_state(log, *head_blk, rhead, rhead_blk,
1323: wrapped);
1324: tail_lsn = atomic64_read(&log->l_tail_lsn);
1325: error = xlog_check_unmount_rec(log, head_blk, tail_blk,
1326: rhead, rhead_blk, buffer,
1327: &clean);
1328: if (error)
1329: goto done;
1330: }
1331: }
1332:
1333: /*
1334: * Note that the unmount was clean. If the unmount was not clean, we
1335: * need to know this to rebuild the superblock counters from the perag
1336: * headers if we have a filesystem using non-persistent counters.
1337: */
1338: if (clean)
1339: xfs_set_clean(log->l_mp);
1340:
1341: /*
1342: * Make sure that there are no blocks in front of the head
1343: * with the same cycle number as the head. This can happen
1344: * because we allow multiple outstanding log writes concurrently,
1345: * and the later writes might make it out before earlier ones.
1346: *
1347: * We use the lsn from before modifying it so that we'll never
1348: * overwrite the unmount record after a clean unmount.
1349: *
1350: * Do this only if we are going to recover the filesystem
1351: *
1352: * NOTE: This used to say "if (!readonly)"
1353: * However on Linux, we can & do recover a read-only filesystem.
1354: * We only skip recovery if NORECOVERY is specified on mount,
1355: * in which case we would not be here.
1356: *
1357: * But... if the -device- itself is readonly, just skip this.
1358: * We can't recover this device anyway, so it won't matter.
1359: */
1360: if (!xfs_readonly_buftarg(log->l_targ))
1361: error = xlog_clear_stale_blocks(log, tail_lsn);
1362:
1363: done:
1364: kvfree(buffer);
1365:
1366: if (error)
1367: xfs_warn(log->l_mp, "failed to locate log tail");
1368: return error;
1369: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 40/2 |
2026/05/30 22:33 |
llm |
expert |
0m
Model:
Tokens:
input: 39853
output: 43
thoughts: 99
Thoughts: **Analyzing the Potential Issue in the XLOG Processing**
Okay, so `xlog_verify_head` just returned 0, meaning something *might* be off, but we're still moving forward to find the tail. Now, the code has this situation where `*head_blk` is negative. That's immediately a red flag. Negative block numbers⦠that's not supposed to happen in our block management scheme. Let's see, what happens next? Ah, right, the code then checks if `*head_blk` has changed from its original value, stored as `orig_head`. If it has, we go into `xlog_set_state`. Given that `*head_blk` *is* negative, this condition is almost certainly going to be met. So, the key here is to dive into `xlog_set_state` and understand what it's doing with this potentially problematic `*head_blk`. I need to figure out how this negative value got set, and what implications it has. This could be a critical point for determining the source of a corrupted XLOG. Let's see how `xlog_set_state` interacts with the remaining parameters: `log`, `rhead`, `rhead_blk`, and `wrapped`.
|
| 41/2 |
2026/05/30 22:33 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_set_state]
Results: map[SourceCode:1191: static void
1192: xlog_set_state(
1193: struct xlog *log,
1194: xfs_daddr_t head_blk,
1195: struct xlog_rec_header *rhead,
1196: xfs_daddr_t rhead_blk,
1197: bool bump_cycle)
1198: {
1199: /*
1200: * Reset log values according to the state of the log when we
1201: * crashed. In the case where head_blk == 0, we bump curr_cycle
1202: * one because the next write starts a new cycle rather than
1203: * continuing the cycle of the last good log record. At this
1204: * point we have guaranteed that all partial log records have been
1205: * accounted for. Therefore, we know that the last good log record
1206: * written was complete and ended exactly on the end boundary
1207: * of the physical log.
1208: */
1209: log->l_prev_block = rhead_blk;
1210: log->l_curr_block = (int)head_blk;
1211: log->l_curr_cycle = be32_to_cpu(rhead->h_cycle);
1212: if (bump_cycle)
1213: log->l_curr_cycle++;
1214: atomic64_set(&log->l_tail_lsn, be64_to_cpu(rhead->h_tail_lsn));
1215: log->l_ailp->ail_head_lsn = be64_to_cpu(rhead->h_lsn);
1216: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 42/2 |
2026/05/30 22:33 |
llm |
expert |
0m
Model:
Tokens:
input: 40782
output: 46
thoughts: 317
Thoughts: **Analyzing a Potential Log Corruption Vulnerability**
Okay, so I'm looking at this code and immediately something jumps out at me: `log->l_curr_block = (int)head_blk;`. That's where it happens, and it looks like `log->l_curr_block` is going to become negative. My expert intuition is immediately piqued - I'm thinking, is this exploitable?
It looks like `log->l_curr_block` is used during log writes, and a negative block number? That's definitely bad. We could be writing to memory we shouldn't. But wait, let me think this through. `xlog_verify_head` is involved, and it only returns 0 if `xlog_rseek_logrec_hdr` finds a *valid* log record.
Alright, if `first_bad` is negative, `xlog_rseek_logrec_hdr` searches backwards from the log's end down to `tail_blk`. It finds a valid record at block `rblk`, okay. But here's the crucial point: then `xlog_verify_head` does something unexpected. It seems to update `head_blk` with the negative `first_bad` value instead of `rblk`.
So, it's setting `*head_blk` to this uninitialized negative value. Exactly! This means that `*head_blk` is indeed negative. And since `log->l_curr_block` is assigned to `(int)head_blk`, it's negative too.
The flow continues, and `xlog_find_tail` will call `xlog_clear_stale_blocks(log, tail_lsn)`. I think this needs a closer look, especially with that negative `log->l_curr_block` variable being used. I need to dive into `xlog_clear_stale_blocks` next. This could be a very serious problem.
|
| 43/2 |
2026/05/30 22:34 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_clear_stale_blocks]
Results: map[SourceCode:1586: /*
1587: * This routine is called to blow away any incomplete log writes out
1588: * in front of the log head. We do this so that we won't become confused
1589: * if we come up, write only a little bit more, and then crash again.
1590: * If we leave the partial log records out there, this situation could
1591: * cause us to think those partial writes are valid blocks since they
1592: * have the current cycle number. We get rid of them by overwriting them
1593: * with empty log records with the old cycle number rather than the
1594: * current one.
1595: *
1596: * The tail lsn is passed in rather than taken from
1597: * the log so that we will not write over the unmount record after a
1598: * clean unmount in a 512 block log. Doing so would leave the log without
1599: * any valid log records in it until a new one was written. If we crashed
1600: * during that time we would not be able to recover.
1601: */
1602: STATIC int
1603: xlog_clear_stale_blocks(
1604: struct xlog *log,
1605: xfs_lsn_t tail_lsn)
1606: {
1607: int tail_cycle, head_cycle;
1608: int tail_block, head_block;
1609: int tail_distance, max_distance;
1610: int distance;
1611: int error;
1612:
1613: tail_cycle = CYCLE_LSN(tail_lsn);
1614: tail_block = BLOCK_LSN(tail_lsn);
1615: head_cycle = log->l_curr_cycle;
1616: head_block = log->l_curr_block;
1617:
1618: /*
1619: * Figure out the distance between the new head of the log
1620: * and the tail. We want to write over any blocks beyond the
1621: * head that we may have written just before the crash, but
1622: * we don't want to overwrite the tail of the log.
1623: */
1624: if (head_cycle == tail_cycle) {
1625: /*
1626: * The tail is behind the head in the physical log,
1627: * so the distance from the head to the tail is the
1628: * distance from the head to the end of the log plus
1629: * the distance from the beginning of the log to the
1630: * tail.
1631: */
1632: if (XFS_IS_CORRUPT(log->l_mp,
1633: head_block < tail_block ||
1634: head_block >= log->l_logBBsize))
1635: return -EFSCORRUPTED;
1636: tail_distance = tail_block + (log->l_logBBsize - head_block);
1637: } else {
1638: /*
1639: * The head is behind the tail in the physical log,
1640: * so the distance from the head to the tail is just
1641: * the tail block minus the head block.
1642: */
1643: if (XFS_IS_CORRUPT(log->l_mp,
1644: head_block >= tail_block ||
1645: head_cycle != tail_cycle + 1))
1646: return -EFSCORRUPTED;
1647: tail_distance = tail_block - head_block;
1648: }
1649:
1650: /*
1651: * If the head is right up against the tail, we can't clear
1652: * anything.
1653: */
1654: if (tail_distance <= 0) {
1655: ASSERT(tail_distance == 0);
1656: return 0;
1657: }
1658:
1659: max_distance = XLOG_TOTAL_REC_SHIFT(log);
1660: /*
1661: * Take the smaller of the maximum amount of outstanding I/O
1662: * we could have and the distance to the tail to clear out.
1663: * We take the smaller so that we don't overwrite the tail and
1664: * we don't waste all day writing from the head to the tail
1665: * for no reason.
1666: */
1667: max_distance = min(max_distance, tail_distance);
1668:
1669: if ((head_block + max_distance) <= log->l_logBBsize) {
1670: /*
1671: * We can stomp all the blocks we need to without
1672: * wrapping around the end of the log. Just do it
1673: * in a single write. Use the cycle number of the
1674: * current cycle minus one so that the log will look like:
1675: * n ... | n - 1 ...
1676: */
1677: error = xlog_write_log_records(log, (head_cycle - 1),
1678: head_block, max_distance, tail_cycle,
1679: tail_block);
1680: if (error)
1681: return error;
1682: } else {
1683: /*
1684: * We need to wrap around the end of the physical log in
1685: * order to clear all the blocks. Do it in two separate
1686: * I/Os. The first write should be from the head to the
1687: * end of the physical log, and it should use the current
1688: * cycle number minus one just like above.
1689: */
1690: distance = log->l_logBBsize - head_block;
1691: error = xlog_write_log_records(log, (head_cycle - 1),
1692: head_block, distance, tail_cycle,
1693: tail_block);
1694:
1695: if (error)
1696: return error;
1697:
1698: /*
1699: * Now write the blocks at the start of the physical log.
1700: * This writes the remainder of the blocks we want to clear.
1701: * It uses the current cycle number since we're now on the
1702: * same cycle as the head so that we get:
1703: * n ... n ... | n - 1 ...
1704: * ^^^^^ blocks we're writing
1705: */
1706: distance = max_distance - (log->l_logBBsize - head_block);
1707: error = xlog_write_log_records(log, head_cycle, 0, distance,
1708: tail_cycle, tail_block);
1709: if (error)
1710: return error;
1711: }
1712:
1713: return 0;
1714: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 44/2 |
2026/05/30 22:34 |
llm |
expert |
0m
Model:
Tokens:
input: 43636
output: 45
thoughts: 435
Thoughts: **Analyzing the Corruption Check in XFS Logging**
Okay, so I'm looking at this corruption check in the XFS logging code, specifically when `head_block` is negative. This immediately raises a red flag. If `head_block` is negative, we have two distinct scenarios to consider.
First, if `head_cycle` and `tail_cycle` are the same, the code checks if `head_block` is less than `tail_block` *or* if `head_block` is greater than or equal to the log's size (`log->l_logBBsize`). With `head_block` negative and `tail_block` potentially zero, the first part of this OR condition becomes true since a negative number is always less than zero. This leads to an immediate `-EFSCORRUPTED` error. That makes sense, because it is comparing an uninitialized block to another.
Now, if `head_cycle` and `tail_cycle` are different. The code then checks if `head_block` is greater than or equal to `tail_block` *or* if `head_cycle` is not `tail_cycle + 1`. With `head_block` negative, the first condition becomes false since the tail block is not going to be smaller. I'm assuming that the head cycle is one greater than the tail.
Here's where it gets interesting. If that check passes, the code calculates `tail_distance` as `tail_block - head_block`. Given a negative `head_block`, this simplifies to `tail_block + |head_block|`. That is logical because you are calculating the distance from the head of the log to its tail. Then, it checks if `(head_block + max_distance)` is less than or equal to the log size. And here's the kicker: `xlog_write_log_records` is called with `head_block`. Since `head_block` is negative, the function is going to try to write to a negative block number. That's a definite no-no and suggests a pretty serious flaw or logic error.
My next step is definitely to dive into the implementation of `xlog_write_log_records` to understand precisely what it does with this negative block number. I need to figure out how this situation could have occurred in the first place, or if the corruption check itself needs some tweaking.
|
| 45/2 |
2026/05/30 22:34 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_write_log_records]
Results: map[SourceCode:1503: STATIC int
1504: xlog_write_log_records(
1505: struct xlog *log,
1506: int cycle,
1507: int start_block,
1508: int blocks,
1509: int tail_cycle,
1510: int tail_block)
1511: {
1512: char *offset;
1513: char *buffer;
1514: int balign, ealign;
1515: int sectbb = log->l_sectBBsize;
1516: int end_block = start_block + blocks;
1517: int bufblks;
1518: int error = 0;
1519: int i, j = 0;
1520:
1521: /*
1522: * Greedily allocate a buffer big enough to handle the full
1523: * range of basic blocks to be written. If that fails, try
1524: * a smaller size. We need to be able to write at least a
1525: * log sector, or we're out of luck.
1526: */
1527: bufblks = roundup_pow_of_two(blocks);
1528: while (bufblks > log->l_logBBsize)
1529: bufblks >>= 1;
1530: while (!(buffer = xlog_alloc_buffer(log, bufblks))) {
1531: bufblks >>= 1;
1532: if (bufblks < sectbb)
1533: return -ENOMEM;
1534: }
1535:
1536: /* We may need to do a read at the start to fill in part of
1537: * the buffer in the starting sector not covered by the first
1538: * write below.
1539: */
1540: balign = round_down(start_block, sectbb);
1541: if (balign != start_block) {
1542: error = xlog_bread_noalign(log, start_block, 1, buffer);
1543: if (error)
1544: goto out_free_buffer;
1545:
1546: j = start_block - balign;
1547: }
1548:
1549: for (i = start_block; i < end_block; i += bufblks) {
1550: int bcount, endcount;
1551:
1552: bcount = min(bufblks, end_block - start_block);
1553: endcount = bcount - j;
1554:
1555: /* We may need to do a read at the end to fill in part of
1556: * the buffer in the final sector not covered by the write.
1557: * If this is the same sector as the above read, skip it.
1558: */
1559: ealign = round_down(end_block, sectbb);
1560: if (j == 0 && (start_block + endcount > ealign)) {
1561: error = xlog_bread_noalign(log, ealign, sectbb,
1562: buffer + BBTOB(ealign - start_block));
1563: if (error)
1564: break;
1565:
1566: }
1567:
1568: offset = buffer + xlog_align(log, start_block);
1569: for (; j < endcount; j++) {
1570: xlog_add_record(log, offset, cycle, i+j,
1571: tail_cycle, tail_block);
1572: offset += BBSIZE;
1573: }
1574: error = xlog_bwrite(log, start_block, endcount, buffer);
1575: if (error)
1576: break;
1577: start_block += endcount;
1578: j = 0;
1579: }
1580:
1581: out_free_buffer:
1582: kvfree(buffer);
1583: return error;
1584: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 46/2 |
2026/05/30 22:34 |
llm |
expert |
0m
Model:
Tokens:
input: 45866
output: 42
thoughts: 37
Thoughts: **Analyzing a Potential Bug in `xlog_bwrite`**
Okay, so I'm thinking about this code and something's poking at me. It looks like if `start_block` ends up being a negative number, `xlog_bwrite` is going to get called with a negative block number. That's... not ideal. Block numbers are generally supposed to be positive integers, so a negative one is definitely a red flag. Let's dig into `xlog_bwrite` and see what it does with a potentially invalid input like that. I need to understand the implications of this. It could range from a simple error check triggering to something a lot more serious like corruption. Time to investigate.
|
| 47/2 |
2026/05/30 22:34 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/xfs/xfs_log_recover.c Name:xlog_bwrite]
Results: map[SourceCode: 177: STATIC int
178: xlog_bwrite(
179: struct xlog *log,
180: xfs_daddr_t blk_no,
181: int nbblks,
182: char *data)
183: {
184: return xlog_do_io(log, blk_no, nbblks, data, REQ_OP_WRITE);
185: }
SourceFile:fs/xfs/xfs_log_recover.c]
|
| 48/2 |
2026/05/30 22:34 |
llm |
expert |
0m
Model:
Tokens:
input: 46263
output: 27
thoughts: 2082
Thoughts: **Analysis of XFS Log Verification Vulnerability**
Okay, let's break this down systematically. My initial thought process is to trace how these functions interact and identify potential weaknesses. The code snippet, specifically the `xlog_do_io` and its interaction with `xlog_verify_bno`, flags any invalid block number, and rightfully so returns an error and *prevents* the write. Good, thatβs safe. It's a key defense.
My mind then jumps to edge cases β what if the invalid block *isn't* negative, but still out-of-bounds? `xlog_verify_bno` has to catch that too. And it does. It will return false, the write doesn't happen, also safe. But what happens with a *valid* block number? If itβs valid, the code might still attempt a write. If it's a *valid* log block, the attacker already *controls* that area of the disk. Writing to a valid block in the log, even if chosen at random doesn't give them new capabilities, so no privilege escalation here. It *could* corrupt the log but the attacker is already in a position to corrupt the log in any way they want, so it's not a new attack vector.
The only real issue appears to be an information leak through `xfs_warn` β that's what's getting logged. Is an information leak considered *exploitable*? Generally, no. "Exploitable" means memory corruption or privilege escalation, which arenβt happening here. It's not leading to a crash and I don't see any immediate path to escalate privileges or bypass security boundaries. I'm focusing on "memory corruption or elevated privileges." This is all contained in the disk context.
So, exploitability is False, and there's no Denial of Service. The kernel just reports a corruption and fails the mount. It's not unprivileged because XFS is not accessible to unprivileged processes. It's also not triggered by user namespaces and not a VM guest or host trigger, neither network nor remote. It can be triggered by a peripheral, like a malicious USB drive or filesystem image, or a malicious filesystem image.
Okay, let's step back. Can an *unprivileged* user mount a filesystem with the help of something like FUSE? No, this is XFS. And the syscall is performed by root, via `udisks2` and similar, even though the trigger is initiated by the unprivileged user, the bug is still reached by a root process, not an unprivileged one. So Unprivileged is False.
Let's do a complete review of `xlog_verify_head` again to see if I missed any paths. Alright, `xlog_rseek_logrec_hdr` could return an error, in which case `tmp_rhead_blk` is uninitialized. And if there is no error, it is still uninitialized.
Then `xlog_do_recovery_pass` gets called with this uninitialized value. This now seems very interesting! In this function, `blk_no` gets set to `tail_blk`. Then, `xlog_bread` gets called. If `blk_no` is bad, it'll return a -EFSCORRUPTED error. Then, that error, combined with a potential `first_bad` set, might result in a warning being logged with the uninitialized value of `rhead_blk`. This is still just an info leak.
Then, there is this call to `xlog_rseek_logrec_hdr`. If the bad value is out of bounds, that will call `xlog_bread` and return `-EFSCORRUPTED`, the mount fails. If it is *within* bounds, the search could find a log record, or it might not. If not, it returns 0. If it finds one, it sets head_blk. Writing to that valid block is not exploitable because the attacker already controls the log.
Wait, the interesting case comes if `tail_blk` is uninitialized and happens to be equal to `head_blk`. The code has this `ASSERT(head_blk != tail_blk)`. This could fail! But an `ASSERT` is probably just a warning. This doesn't crash the kernel in production builds.
So, there are no memory corruption, privilege escalation, or DoS vulnerabilities here. Just the potential for an information leak and maybe writing to a valid log block.
|
| 49/2 |
2026/05/30 22:34 |
tool |
grepper |
0m
Args: map[Expression:define ASSERT PathPrefix:fs/xfs/]
Results: map[Output:fs/xfs/xfs_platform.h=224=int xfs_rw_bdev(struct block_device *bdev, sector_t sector, unsigned int count,
--
fs/xfs/xfs_platform.h-226-
fs/xfs/xfs_platform.h:227:#define ASSERT_ALWAYS(expr) \
fs/xfs/xfs_platform.h-228- (likely(expr) ? (void)0 : assfail(NULL, #expr, __FILE__, __LINE__))
--
fs/xfs/xfs_platform.h-230-#ifdef DEBUG
fs/xfs/xfs_platform.h:231:#define ASSERT(expr) \
fs/xfs/xfs_platform.h-232- (likely(expr) ? (void)0 : assfail(NULL, #expr, __FILE__, __LINE__))
--
fs/xfs/xfs_platform.h-241- */
fs/xfs/xfs_platform.h:242:#define ASSERT(expr) \
fs/xfs/xfs_platform.h-243- (likely(expr) ? (void)0 : asswarn(NULL, #expr, __FILE__, __LINE__))
--
fs/xfs/xfs_platform.h-246-
fs/xfs/xfs_platform.h:247:#define ASSERT(expr) ((void)0)
fs/xfs/xfs_platform.h-248-
]
|
| 50/2 |
2026/05/30 22:34 |
llm |
expert |
1m
Model:
Tokens:
input: 49714
output: 66
thoughts: 1104
Thoughts: **Analyzing a Potential Denial-of-Service Vulnerability in XFS Logging**
Okay, let's break this down. In production builds, that `ASSERT` macro is a no-op, which means it won't crash the system if the condition it checks fails, so that's good. The scenario we're looking at involves the condition `head_blk == tail_blk`, and what happens in the `xlog_do_recovery_pass` function when that's true. If they are equal, the code will just return 0, `xlog_verify_head` will return 0, and `xlog_find_tail` will continue. This won't cause any real issues.
Now, I need to double-check `xlog_rseek_logrec_hdr` to see if it can return 0, which could indicate a problem if the tail wasn't correctly identified. Looking at the code for that function, if `head_blk` is 0, the first loop won't execute, since `i` is initialized to -1. If `tail_blk` is also 0, then the second loop will execute, from the last block down to 0, if there aren't any blocks in the log that are flagged as valid records, it will return 0. So yes, it can return 0 in that scenario.
Is there any other way `found` can be 0? Yep, if `head_blk` is for instance 10, and `tail_blk` is 0. The first loop will execute from 9 down to 0, which is good. If no valid header is found within that range, the second loop will not execute because `tail_blk >= head_blk` will be false. So, it can return 0. This is also possible, as it could be caused by either a malicious, or corrupted log.
Therefore, it looks like there's a real bug here, and it's easily triggered by a malicious filesystem. I need to get the "set-results" tool running right away.
|
| 51/2 |
2026/05/30 22:35 |
tool |
set-results |
0m
Args: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 52/2 |
2026/05/30 22:35 |
llm |
expert |
0m
Model:
Tokens:
input: 51396
output: 616
thoughts: 358
Thoughts: **My Analysis of the XFS Log Recovery Bug**
Alright, the data's in, and it's time to craft the final response. My immediate assessment is that we're dealing with an uninitialized variable vulnerability within the `xlog_verify_head` function, specifically in `fs/xfs/xfs_log_recover.c`. I see that the root cause is stemming from a scenario where `xlog_rseek_logrec_hdr` returns 0, indicating the absence of any log records. This omission leaves `tmp_rhead_blk` hanging, uninitialized.
This uninitialized `tmp_rhead_blk` then gets passed downstream as `tail_blk` to `xlog_do_recovery_pass`. Here, it's used as a block number, and this is where it gets interesting. When this `tail_blk` value is utilized in `xlog_bread` to read from the log, we have a problem if it's not a valid block number.
If `tail_blk` is out of bounds, `xlog_verify_bno` thankfully rejects it, and `xlog_do_io` will log a warning, containing the uninitialized value to the kernel log (dmesg). This constitutes an information leak, revealing kernel stack memory and causing the mount to fail. A mount failure is expected behaviour for corrupted filesystems, so this does not constitute a denial of service attack.
However, if, by some unlucky circumstance, the uninitialized `tail_blk` value happens to fall within valid bounds, `xlog_bread` reads and processes the block at that location. But since the attacker has control over the disk image, and by definition, the log contents, this doesn't lead to any new capabilities. This, in itself, is harmless.
There's no memory corruption, out-of-bounds memory access beyond the log, or privilege escalation here. It's a straight-forward filesystem corruption scenario, resulting in a predictable mount failure. The attack vector is clear: mounting a crafted malicious XFS filesystem image. It's important to note, though, that standard XFS configurations, in general, prevent unprivileged users or processes within user namespaces from triggering this bug.
|
| 53/1 |
2026/05/30 22:36 |
action |
format-explanation |
0m
Results: map[Explanation:The bug is an uninitialized value in `xlog_verify_head` in
`fs/xfs/xfs_log_recover.c`.
**Root Cause:**
During XFS log recovery, `xlog_verify_head` calls `xlog_rseek_logrec_hdr` to
search backwards for log records and initialize `tmp_rhead_blk`. If the log is
corrupted or maliciously crafted such that no blocks contain the
`XLOG_HEADER_MAGIC_NUM`, `xlog_rseek_logrec_hdr` returns `0` (meaning 0 records
found) without initializing `tmp_rhead_blk`.
However, `xlog_verify_head` only checks if the return value is negative (`if
(error < 0) return error;`). Since `0` is not negative, it proceeds to call
`xlog_do_recovery_pass` with the uninitialized `tmp_rhead_blk` as the `tail_blk`
argument.
**Security Impact:**
In `xlog_do_recovery_pass`, the uninitialized value is used as a disk block
number (`blk_no`) to read from the log via `xlog_bread` -> `xlog_do_io`.
- If the uninitialized value is out of bounds (which is highly likely for a
random stack value), `xlog_verify_bno` rejects it, and `xlog_do_io` prints a
warning to the kernel log (`dmesg`) containing the invalid block number. This
results in a **kernel stack information leak**, which could potentially be used
to bypass KASLR. The mount operation then safely aborts with `-EFSCORRUPTED`.
- If the uninitialized value happens to be within the valid log block range, the
kernel will read and process a random block from the log. Since the attacker
already controls the entire malicious disk image, reading a random block from it
does not grant any new capabilities or lead to memory corruption.
**Exploitability:**
This bug is **not exploitable** for memory corruption or privilege escalation.
It only results in a stack infoleak to the kernel log and a failed mount
operation. It does not cause a kernel crash, hang, or resource leak, so it is
not a Denial of Service (DoS).
**Accessibility:**
Triggering this bug requires mounting a maliciously crafted XFS filesystem
image.
- **Unprivileged / User Namespaces:** XFS does not set the `FS_USERNS_MOUNT`
flag, meaning it cannot be mounted by unprivileged users, even within a user
namespace. It requires `CAP_SYS_ADMIN` in the initial namespace.
- **Peripheral / Filesystem Trigger:** The bug can be triggered if a user
physically plugs in a malicious USB drive and the desktop environment
auto-mounts it, or if a privileged user is tricked into mounting a downloaded
malicious image.]
|