XFS (nvme0n1p2): Internal error at line 721 of file fs/xfs/libxfs/xfs_defer.c
862 GiB free. A 2 TB NVMe drive at 53% capacity. The filesystem shut itself down because it could not allocate one block.
TL;DR
xfs_parent_da_args_init() never sets args->total. The field stays zero,
xfs_da_grow_inode_int() subtracts from it, and it underflows to
0xFFFFFFFF. Every downstream guard that should catch an impossible
allocation is defeated by a single (int) cast, so maxlen gets clamped to
zero and the allocator rejects its own arguments with a bare -ENOSPC.
xfs_defer_finish_noroll() treats that as fatal and takes the filesystem
down.
It needs parent pointers (parent=1) plus an inode with many hard links,
which is why a Yocto do_package run reproduces it and nothing else on this
machine does.
The fix is one line. It reproduces deterministically in about 0.2 seconds on a 512 MiB scratch filesystem, there is an xfstests case that fails without the fix and passes with it, and the series is on linux-xfs awaiting review.
The same thing without the jargon
If you do not carry XFS internals around in your head, here is the shape of it. The rest of the post assumes you have read this much and no more.
First, the error message is a red herring. The kernel log says “Corruption of in-memory data”, and nothing was corrupt. That string is a generic label attached to one exit path, not a diagnosis, and it cost me half a day of chasing memory faults that were never there.
What actually happens: there is a counter meaning “how many blocks may this operation still use.” One path forgot to set it before using it, so it started at zero. The code then subtracted from it.
The path in question writes a parent pointer, which is the record that a file belongs to a directory. One gets written on every hardlink, which is why a packaging step that hardlinks the same file into dozens of staging directories is what found this.
Subtracting one from zero in that counter does not give minus one. It is unsigned, so it rolls over to the largest value it can hold, about 4.3 billion, the way an odometer wound backwards past zero reads 999999 rather than -1.
Everything after that behaved correctly and arrived somewhere absurd. A safety check read 4.3 billion, concluded there was effectively unlimited room, and waved the request through. The next layer did its own arithmetic on the same poisoned value and decided the operation could use zero blocks. Then a third check compared “needs at least one block” against “may use zero blocks”, found that impossible, and returned “out of disk space.”
Nothing was out of disk space. The disk was half empty. The filesystem could not distinguish “this request is impossible because the disk is full” from “this request is impossible because I computed nonsense,” so it assumed its own in-memory state was corrupt and shut down to avoid writing damage to disk. That is the correct thing to do when your bookkeeping contradicts itself. It was just reasoning from one bad number.
Two assertions exist in the source specifically to catch this, and both are compiled out of every kernel anyone actually runs.
The machine
| Board | ASUS ROG Crosshair X870E Hero |
| CPU | AMD Ryzen 9 9950X, 16 cores, 32 threads |
| Memory | 96 GB |
| Root | XFS on NVMe, crc=1 reflink=1 rmapbt=1 parent=1 |
| Kernel | 7.1.5, CachyOS |
| Workload | BitBake building a full image with Chromium |
Two weeks of crashes. The first week went into BIOS settings and hardware swaps, because a machine that hard-reboots under load looks like a hardware problem and I treated it as one. Fourteen crashes produced a saved dump; the earliest ones happened before I had kdump armed at all. Six carried the XFS shutdown signature.
Always under a heavy build. Never at idle, never under synthetic I/O load,
never under fio.
Phase 1: everything that was not the problem
I want to lead with the dead ends, because they took most of the five days and because the shape of the error sent me somewhere wrong.
-ENOSPC with 862 GiB free reads like an accounting bug. So I went looking
for one.
Reservation sizing. I worked through four candidate paths where an
attribute-fork operation could under-reserve. All four compute their
reservations correctly. More decisively, an over-run reservation does not
produce -ENOSPC at all: it trips xfs_trans_mod_sb() at
fs/xfs/xfs_trans.c:350-351 with a different failure entirely. The errno
was telling me it was not this, and I did not listen for two days.
Reflink and extent churn. The filesystem has reflink=1, and BitBake’s
do_package hardlinks aggressively. A plausible story about extent-count
explosion. Falsified: the reproducer triggers the crash with reflink traffic
absent.
Pre-existing extended attributes. My best hypothesis for a while was
that files arriving with user xattrs pushed the attribute fork over a
threshold. I built a --no-xattr control arm into the reproducer and it
crashed anyway. I read the argv back out of the vmcore to be certain the
flag was actually in effect. It was. Hypothesis dead.
Memory pressure. The shutdown reason is SHUTDOWN_CORRUPT_INCORE,
which prints as “Corruption of in-memory data”. That phrasing cost me half
a day. It is the generic out_shutdown: label at xfs_defer.c:721 and it
implies nothing whatsoever about memory. PSI counters showed mem_full at
0.00% and zero pswpout across the entire build.
Allocation-group locking order. XFS refuses allocations that would
violate AG lock ordering, and it reports that refusal as -ENOSPC too. A
real candidate. Ruled out by tracing: the terminal event was never
xfs_alloc_vextent_skip_deadlock.
Rogue DMA. This one I tested rather than argued. The box boots with
iommu=pt, which means no DMA translation for most devices, which means a
misbehaving device could scribble on kernel memory with no fault and no log
line. I rebooted with iommu.strict=1 to force full translation and ran the
reproducer again. IO_PAGE_FAULT count: 0. The crash reproduced identically.
Not DMA.
Six hypotheses, six refutations, and I still had no idea what was happening.
Phase 2: the crash destroys its own evidence
The obvious move is to trace the allocator and read the trace after the crash. That does not work here, and the reasons are worth writing down.
The shutdown is configured to panic (fs.xfs.panic_mask=16) so kdump fires
and writes a vmcore. But a vmcore is a snapshot of memory at the moment of
death, and what I needed was the sequence of allocator decisions leading up
to it. That lives in the ftrace ring buffer.
ftrace_dump_on_oops is the standard answer: on an oops, flush the trace
ring into the kernel log, where vmcore-dmesg will capture it. It failed
twice, in two different ways.
Mode 2 dumped the wrong CPU. ftrace_dump_on_oops=2 dumps only the
crashing CPU’s buffer. I reasoned that the failing allocation runs in the
same task, therefore the same CPU. That conflates task with CPU. Under
PREEMPT_DYNAMIC=full with 16 workers and a build across 32 threads, the
task migrates mid-syscall. The capture held CPU 10, and the crashing task
had exactly one event in it: the shutdown itself. 1,133 trace lines, none
of them the ones I needed.
Mode 1 blocked kdump entirely. Dumping all 32 CPUs produces roughly
2.1 MB of text. ftrace_dump_on_oops runs from the DIE_OOPS notifier,
which fires before crash_kexec(). Every line goes through printk, and
printk paints the framebuffer console. At this resolution that is a few
hundred lines per second. The machine sat there painting a trace dump
instead of writing a vmcore, and I lost the crash.
I also learned that the trace volume itself was the problem. I had enabled
the entire xfs_alloc_* family; the cursor-walk events accounted for 95% of
the output. And xfs_buf_ioerror, despite the name, is not a failure-path
tracepoint: XFS calls it with error 0 from xfs_buf_get_map() on
essentially every buffer lookup. I measured 402,082 ring overruns on one CPU
in 15 seconds. Unfiltered, it evicted the window I cared about before the
failure ever happened.
Phase 3: a ring buffer that survives the reboot
The fix for all of this is a kernel feature I had not used before: persistent, boot-mapped trace buffers.
reserve_mem=64M:4096:trace trace_instance=boot_map@trace
This reserves 64 MB at a fixed physical address and backs a named trace
instance with it. The contents survive kexec and a full reboot. Nothing
needs to be flushed through printk, nothing competes with the panic path,
and kdump is untouched.
One sharp edge: the contents survive, the configuration does not. After every boot the instance exists and holds the previous run’s data, but no events are enabled. The arming script has to run again, and it must not clear the buffer before I have read it. I put a guard in mine after nearly wiping the only good capture I ever got.
That guard had its own bug worth mentioning, because it is a classic:
retained=$(grep -cve '^#' "$I/trace" || echo 0)
grep -c exits 1 when the count is zero, so on an empty buffer the command
substitution produced "0\n0" and the numeric comparison broke. The guard
protecting my evidence was itself unable to count. || true plus an
explicit default fixed it.
With the persistent instance armed and the event list trimmed to terminal events only, I ran the build and the reproducer together and waited.
Phase 4: four lines
531.418881: xfs_alloc_vextent_loopfailed: agno=0x1 minlen=0x1 maxlen=0x1
531.435265: xfs_alloc_size_nominleft: agno=0x2 minlen=0x1 maxlen=0x0 total=0xffffffff (-1)
531.435266: xfs_alloc_vextent_badargs: agno=-1 minlen=0x1 maxlen=0x0 total=0x1
531.440550: xfs_force_shutdown: ptag=0x10 flags=0x8 fname=fs/xfs/libxfs/xfs_defer.c line_num=721
total=0xffffffff, 5.3 milliseconds before the shutdown, in the crashing
task. maxlen goes from 1 to 0 between the first and second line.
That is the whole bug, and the rest is reading source.
The mechanism
xfs_parent_da_args_init() at fs/xfs/libxfs/xfs_parent.c:151-171 fills in
every field of the xfs_da_args it builds. Every field except total. The
struct is zeroed, so total starts at 0.
xfs_da_grow_inode_int() then treats it as a running remainder at
fs/xfs/libxfs/xfs_da_btree.c:2357:
args->total -= dp->i_nblocks - nblks;
args->total is xfs_extlen_t, which is unsigned. Subtracting from zero
underflows it to 0xFFFFFFFF.
Now the interesting part, in xfs_alloc_space_available() at
fs/xfs/libxfs/xfs_alloc.c:2523-2535:
available = (int)(pagf_freeblks + agflcount - reservation
- min_free - args->minleft);
if (available < (int)max(args->total, alloc_len))
return false;
if (available < (int)args->maxlen && !(flags & XFS_ALLOC_FLAG_CHECK)) {
args->maxlen = available;
ASSERT(args->maxlen > 0);
ASSERT(args->maxlen >= args->minlen);
}
max(args->total, alloc_len) is 0xFFFFFFFF. Cast to int, that is -1.
The guard becomes available < -1, which cannot be true for any
non-negative available. A gate designed to reject allocations that lack
free space can no longer reject anything.
Execution falls into the clamp, args->maxlen is assigned available, and
when available is 0 the result is maxlen = 0. The two ASSERT calls
that exist precisely to catch this are compiled out: CONFIG_XFS_DEBUG is
not set on any distribution kernel.
xfs_alloc_vextent_check_args() then does its job correctly. It sees
minlen=1 > maxlen=0, an impossible request, and returns -ENOSPC with a
tracepoint and no message. xfs_defer_finish_noroll() treats any non-EAGAIN
error as fatal, calls xfs_force_shutdown(SHUTDOWN_CORRUPT_INCORE), and the
filesystem is gone.
Every layer behaved as written. The only defect is the missing assignment.
Why this needs parent pointers and hard links
parent=1 stores one parent-pointer xattr per directory entry, so an inode
with N hard links carries N of them. I confirmed this directly: 31 links
produced 31 parent pointers.
BitBake’s do_package hardlinks the same file into many package staging
directories. Each link() writes another parent pointer, the attribute fork
grows past the point where it needs a leaf-to-node conversion, and
xfs_da_grow_inode_int() runs on a xfs_da_args that never had total
set. Without parent pointers there is no such xattr traffic. Without many
links there is no fork growth. That is why this box crashes and my other
machines do not.
The fix
args->total = xfs_attr_calc_size(args, &local);
One line, in xfs_parent_da_args_init().
The reason to write it exactly that way, rather than clamping the subtraction
or picking a safe constant, is that XFS already does this correctly somewhere
else. When the filesystem replays a parent-pointer insert from the log after a
crash, xfs_attri_recover_work() sets the same field from the same function
(xfs_attr_item.c:706). So replaying the operation from the log was always
right, and performing it live was always wrong. The fix makes the normal path
behave like the recovery path instead of contradicting it, which is a much
easier thing to defend than “I added a missing line.”
I argued to myself that this is both necessary and sufficient before
building anything, because “the patch makes the symptom go away” is a weak
claim. maxlen = 0 requires the clamp to execute with available == 0,
which requires the gate above it to pass. With a correct total of roughly
25 blocks and alloc_len of 1, the gate demands available >= 25, so any
available small enough to clamp maxlen to zero would have failed the
gate first. The failing state is unreachable. With total = 0xFFFFFFFF the
gate compares against -1 and always passes.
I carry four more patches alongside it, and only one of them is a fix: a
genuinely separate bug where xfs_defer_finish_one() returns an
uninitialised error on the item-less barrier path, reachable through
xfs_defer_add_barrier() on any kernel built with
CONFIG_XFS_ONLINE_REPAIR. The other three are diagnostics, including one
that prints the actual errno at the shutdown site so the next person does
not have to do any of this.
Validation: making the bug fire on demand
For most of this investigation the only way to see the failure was to run a Yocto build and a hardlink stressor next to three desktop applications and wait five to seven minutes for the machine to die. That is a terrible experiment. It takes a reboot per data point, it destroys the evidence that explains it, and the moment you ask “how many failures did the patched kernel avoid?” you find you never measured a baseline rate to compare against.
The way out was to stop treating the failure as stochastic. Reading the source, it needs exactly two things to happen at once:
N1, two xfs_da_grow_inode() calls sharing one xfs_da_args. That is the
XFS_DAS_LEAF_ADD -> xfs_attr3_leaf_to_node() -> XFS_DAS_NODE_ADD ->
xfs_da3_split() path, and enough long-named hardlinks on one inode produce it
reliably.
N2, at the second of those calls, an allocation group where
available = pagf_freeblks + agflcount - reservation - min_free - args->minleft
is exactly zero. Not negative, not one: at available == -1 the same code
assigns args->maxlen = 0xFFFFFFFF and takes a different path entirely, and at
available >= 1 the allocation just succeeds.
My stressor supplied N1 all day long and never once supplied N2. That is the whole reason it could not crash anything on its own. On a 1.9 TB filesystem with 862 GiB free, waiting for one of 32 allocation groups to sit at exactly zero is a coincidence you wait hours for, and the BitBake build and the browsers were only ever churning allocation until it happened.
So construct N2 instead of waiting for it. Build a 512 MiB filesystem with two allocation groups and parent pointers, fill it to within a few hundred blocks, then walk that margin down one block at a time, hardlinking at every step:
mkfs.xfs -f -m crc=1 -n parent=1 -d agcount=2 /var/tmp/pptr.img
fallocate -l 411M /mnt/scratch/ballast # free blocks now 464
# punch one 4 KiB hole per step, 64 hardlinks per step
The whole free pool is now a few hundred blocks, so the sweep passes through zero within a step or two rather than never.
It fires on the second step. Roughly 0.2 seconds of link activity:
xfs_alloc_cur_lookup minlen 1 maxlen 0 ... total 4294967295
xfs_alloc_cur_right minlen 1 maxlen 0 ... total 4294967295
xfs_alloc_size_neither minlen 1 maxlen 0 ... total 4294967295
xfs_alloc_vextent_first_ag minlen 1 maxlen 0 ... total 1
xfs_alloc_vextent_badargs minlen 1 maxlen 0 ... total 1
XFS (loop0): Corruption of in-memory data (0x8) detected at
xfs_defer_finish_noroll+0x29a/0x4b0 (fs/xfs/libxfs/xfs_defer.c:721)
Read those five lines in order and the whole bug is there. maxlen is already
clamped to zero while total still reads 0xFFFFFFFF. Then total becomes 1,
because xfs_bmap_btalloc_low_space() sets args->total = ap->minlen for a
last-ditch attempt across every allocation group. But nothing resets
args->maxlen, so the poisoned zero survives into
xfs_alloc_vextent_check_args(), which rejects minlen(1) > maxlen(0) and
returns a bare -ENOSPC. The fallback that exists to rescue a too-large
reservation cannot rescue a clamped maxlen.
There is also a detail worth pausing on. Earlier in the same capture, several
hundred milliseconds before the shutdown, another ln carried
total 4294967295 with maxlen 1 and allocated successfully. The underflow
is not rare and is usually harmless. It only kills you when it lands on an
allocation group sitting at exactly zero.
The A/B
Same machine, same script, same geometry, only the kernel differs:
| Unpatched | Patched | |
|---|---|---|
| Outcome | shutdown at step 1 | 400 steps, 25,024 links, clean |
total 4294967295 | 8 | 0 |
| Attr fork | 0 -> 4294967295 | 25 -> 24 |
| Data fork (control) | 78 -> 77 | 78 -> 77 |
The attr-fork row is the fix. xfs_attr_calc_size() returns
XFS_DAENTER_SPACE_RES(mp, XFS_ATTR_FORK), which for a parent pointer is a
constant 25 on a 4 KiB-block filesystem, and the 24 is that same
xfs_da_args at its second growth after one block was subtracted. Every one of
those events is a trip through the exact code that used to wrap, and none of
them wrapped.
The data-fork row is why I believe the comparison. 78 -> 77 is the identical
subtraction happening correctly, and it is unchanged across both kernels. The
workload was the same; the patch moved only the population it aimed at.
I also ran the same test on a second, headless machine on its unpatched kernel.
It produced a byte-identical event distribution: 52 events at total 0, 30 at
78, 8 at 77, 8 at 4294967295, 2 at 1. Two different CPUs, two different
boards, the same counts. Whatever this is, it is not timing-dependent.
That headless result also killed a hypothesis I had held for a week. I thought the desktop applications contributed something causal, because the stressor alone never reproduced anything. They did not. They were one way of producing N2, and a nearly-full 512 MiB image is a better one.
A regression test that fails without the fix
The reproducer above is a shell script that lives on my machine. The version that belongs upstream is an xfstests case, and writing one turned out to be mostly a matter of stating the two conditions in the harness’s own vocabulary:
_scratch_mkfs_sized $((512 * 1024 * 1024)) # two AGs, parent=1
# ballast to leave ~460 free blocks
# per step: punch one 4 KiB hole, then 64 long-named hardlinks
It needs no explicit assertion. xfstests runs _check_dmesg and
_check_xfs_filesystem after every test, so a shutdown fails it automatically.
The part that matters is that I ran it on both kernels:
| kernel | result |
|---|---|
| unpatched | fails - dirty log, filesystem inconsistent, xfs_defer.c:721 in dmesg, dead by step 2 |
| patched | passes - 400 steps, 25,024 links, 25 seconds |
Until that first row existed the test was worthless. A regression test that has only ever been observed passing tells you nothing, and I nearly shipped one: my first version left 128 free blocks and swept up to 192, entirely below the band where the bug fires. It passed on a patched kernel for the wrong reason.
One trap worth repeating, because it will cost someone a machine. If
fs.xfs.panic_mask is non-zero, the shutdown becomes a BUG(), kdump fires,
and the host reboots instead of reporting a failure. XFS defaults to 0. Mine
was set to 16 for crash capture, which is exactly the configuration a person
debugging this bug would have.
Regression coverage
./check -g parent -g attr on the patched kernel: 53 of 55 passing. The two
failures are environmental, a setfattr deprecation warning newer than the
golden output and an O_TMPFILE EOVERFLOW inside the test harness. Neither
mentions parent pointers, allocation or ENOSPC, and no XFS shutdown appeared in
dmesg for the duration. I have not run those two on an unpatched kernel, so
“environmental” is an inference from what they say, not a demonstration.
Eight days
The patched kernel has since carried ordinary Yocto build load for eight days with no filesystem shutdown and no crash dump. The longest single uptime in that window was three days. Before the patch the same machine died within five to seven minutes once the conditions coincided.
I am wary of that number carrying more weight than it deserves, which is why it comes last. Eight days of not-crashing is consistent with a fix and also consistent with luck; the argument that does the work is still the one in the table above, where the underflow is present 8 times on one kernel and 0 times on the other under an identical script.
Nineteen tests did not run. Several skipped for a reason that is its own small
lesson: xfs/433 and xfs/532 report [not run] requires CONFIG_XFS_DEBUG.
The error-injection machinery that would have been the obvious tool for this
whole investigation is compiled out of every distribution kernel, for the same
reason the two ASSERTs guarding this bug are.
The series went to linux-xfs on 2026-08-08 as
[PATCH 0/5] xfs: fix filesystem shutdown from parent pointer reservation underflow.
Five patches: the fix, an unrelated bug found while reading the same code, one
change that makes this class of failure diagnosable in seconds rather than
days, and two corrections.
It is submitted, not merged. Upstream review takes as long as it takes and the maintainers may well want it done differently; I will update this post if the shape of the fix changes.
A variant I have not proven
While reading the gate I noticed a second path through it that is worse than the one I hit.
If available evaluates to -1 rather than 0, the gate still passes, and
the clamp assigns args->maxlen = (xfs_extlen_t)-1, which is 0xFFFFFFFF.
That does not trip xfs_alloc_vextent_check_args(), because minlen is
no longer greater than maxlen. Instead of a loud shutdown, a request for a
colossal extent proceeds.
I have not reproduced this and I am not claiming it happens. If it does, it would move the missing assignment from “causes a shutdown” to “can corrupt data”, which is a materially different bug.
Still unexplained
The root cause accounts for the shutdowns. It does not account for everything I saw.
Three of the six crashes had systemd-journal segfaulting with error 15
(a reserved-bit page-table fault), the faulting address equal to the
instruction pointer, and a zeroed code page at the same 0x7e0 offset each
time. Those segfaults precede the filesystem shutdown, so they are not
downstream of it. One crash showed an AIL inconsistency. Two showed
xfs_ialloc_read_agi() returning -5 with zero block-layer errors. On one
occasion the initramfs was corrupt at rest and I had to rebuild it from a
rescue USB.
The crash that produced the trace above had the clean XFS bug and none of these. So the XFS bug happens without the corruption. Whether the corruption happens without the XFS bug was the open question, and eight days on the patched kernel have now answered it as far as I can:
Jul 20 - Jul 30, unpatched journald error 15 / RSVD faults: 2 (a floor;
several crashes destroyed their own logs)
Jul 30 - Aug 7, patched journald error 15 / RSVD faults: 0
Twelve segfaults did occur in that window, all of them ld-linux probe
failures from the build and one virsh. None carried error 15, a
reserved-bit fault, or journald.
So the likeliest reading is now that those faults were downstream of the filesystem dying rather than a second, independent problem. I am not calling it settled. The pre-patch rate was low enough that eight clean days is suggestive rather than conclusive, and it does not explain the thing that made me suspect independence in the first place: in dmesg the segfaults preceded the shutdown. Either the ordering is misleading or there is still something here I have not understood.
Why you never see the panic
A smaller finding, but it wasted real time.
Every crash froze the display on the compositor’s last frame and rebooted
minutes later with nothing on screen. CONFIG_DRM_PANIC=y is set,
DRM_PANIC_SCREEN is kmsg, and amdgpu wires up get_scanout_buffer. It
should draw.
It does not, because drm_panic registers as a kmsg dumper at
drivers/gpu/drm/drm_panic.c:1041, and kmsg dumpers run from inside
panic(). For a BUG() with kdump armed, oops_end() calls
crash_kexec() as its first action at arch/x86/kernel/dumpstack.c:377 and
never returns. panic() is never reached, so the panic screen never draws.
Wayland holds DRM master, so the framebuffer console is not visible either.
crash_kexec_post_notifiers=1 makes kexec_should_crash() return 0 at
kernel/crash_core.c:97-98, so oops_end() skips the early kexec, falls
through to panic(), draws the screen, and still writes the vmcore from
__crash_kexec() at kernel/panic.c:695. The cost is that notifiers now
run before the dump, so a hung notifier loses it. That is why it is not the
default.
Open claims
- The missing
args->totalassignment is necessary and sufficient for this shutdown. Falsified by reproducing anxfs_alloc_vextent_badargswithminlen > maxlenon a kernel carrying patch 5, or by a shutdown atxfs_defer.c:721with-ENOSPCwhere the trace shows a correctly initialisedtotal. - The trigger requires
parent=1plus a high link count. Falsified by reproducing the shutdown on a filesystem formatted without parent pointers, or with a workload that never exceeds a few links per inode. - The
available == -1variant can drive a0xFFFFFFFFallocation request. Unproven. Confirmed by a trace showingmaxlen 4294967295passingcheck_args; refuted by a source argument showingavailablecannot reach-1on this path. - The journald reserved-bit faults were downstream of the filesystem
shutdown, not independent of it. I claimed the opposite for a fortnight.
Revised on eight days of patched-kernel build load producing zero of them
against a pre-patch floor of two. Falsified by a single
error 15reserved-bit fault on a patched kernel, which would put independence back on the table; also weakened by anyone explaining why, in dmesg, the segfaults appear before the shutdown they are supposedly caused by. available == 0is a necessary condition, and the value is exactly zero rather than a range. Supported by the reproducer firing within one or two single-block steps of the sweep and not before. Falsified by a trace showingmaxlenclamped to zero whileavailablewas some other value.xfs_attr_calc_size()returns a constant 25 for every parent pointer on a 4 KiB-block filesystem, so the 24 in the patched traces is the second growth rather than a second reservation size. Falsified by a parent-pointer allocation carrying anytotalother than 25 or 24 on a patched kernel, or bym_bm_maxlevels[XFS_ATTR_FORK] != 5here.- The desktop applications contributed nothing causal. They were one
stochastic route to
available == 0, and a nearly-full image is a deterministic one. I believed the opposite for a week. Falsified by a workload that reproduces the shutdown with an idle allocator, or by showing a browser or camera path that reaches XFS attribute forks. - The two fstests failures are environmental rather than regressions.
Falsified if
generic/062andgeneric/633pass on an unpatched kernel of the same version, since that would make the patch the only difference. I have not run that comparison; the argument so far is only that neither failure mentions parent pointers, allocation or ENOSPC. tests/xfs/842reproduces this bug and only this bug. Falsified by the test failing on a kernel carrying patch 5, or by it passing on an unpatched kernel in a configuration where the standalone reproducer still fires.
What I would do differently
Trust the errno. -ENOSPC on a half-empty filesystem is not an accounting
bug, it is an argument-validation bug, and the codebase says so: five of the
conditions in xfs_alloc_vextent_check_args() are impossible-argument
checks that report as -ENOSPC with nothing but a tracepoint. I spent two
days on space accounting because the errno had a familiar meaning.
Reach for persistent tracing sooner. I burned two crashes on
ftrace_dump_on_oops before switching to a boot-mapped instance that was
always going to be the right tool for a failure that destroys its own logs.
Distrust asserts in shipped kernels. Two ASSERT calls sit directly on this
bug and neither has ever executed on a distribution kernel, because
CONFIG_XFS_DEBUG is off everywhere it matters. An assert that only runs on
developer builds is documentation, not a guard.
References
- The series on lore
[PATCH 0/5] xfs: fix filesystem shutdown from parent pointer reservation underflow, posted 2026-08-08
fs/xfs/libxfs/xfs_parent.c-xfs_parent_da_args_init()fs/xfs/libxfs/xfs_da_btree.c:2357- the underflowing subtractionfs/xfs/libxfs/xfs_alloc.c:2523-2535-xfs_alloc_space_available()fs/xfs/libxfs/xfs_defer.c:721- the shutdown sitearch/x86/kernel/dumpstack.c:377-oops_end()callingcrash_kexec()kernel/crash_core.c:97-98-kexec_should_crash()drivers/gpu/drm/drm_panic.c:1041-drm_panickmsg dumper registrationDocumentation/admin-guide/kernel-parameters.txt-reserve_mem,trace_instance,crash_kexec_post_notifiers