Skip to content

flake.lock: Update - #2

Open
github-actions[bot] wants to merge 494 commits into
privatefrom
update_flake_lock_action
Open

github-actions[bot] wants to merge 494 commits into
privatefrom
update_flake_lock_action

Conversation

@github-actions

Copy link
Copy Markdown

Automated changes by the update-flake-lock GitHub Action.

Flake lock file updates:

• Updated input 'crane':
    'github:ipetkov/crane/0532eb17955225173906d671fb36306bdeb1e2dc?narHash=sha256-EVZd2RsbpreRUDSi9rBwPY%2BZxoyMaiEBbZxxhljbaS4%3D' (2026-05-30)
  → 'github:ipetkov/crane/1316b7d278ad77a16aec024b71d971366e123bec?narHash=sha256-gcFMcRjw0ZSn380Rx2QLlU1goUQeSrKX/DF12omI6%2Bo%3D' (2026-07-18)
• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb?narHash=sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4%3D' (2026-05-13)
  → 'github:hercules-ci/flake-parts/17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e?narHash=sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w%2B6NWK9yA%3D' (2026-07-01)
• Updated input 'flake-parts/nixpkgs-lib':
    'github:nix-community/nixpkgs.lib/f5901329dade4a6ea039af1433fb087bd9c1fe14?narHash=sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ%3D' (2026-04-26)
  → 'github:nix-community/nixpkgs.lib/db3f255737b94216eb71cce308e2912cf6bc2d7c?narHash=sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC%2BeBu2zKlp8%3D' (2026-06-28)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/e73de5be04e0eff4190a1432b946d469c794e7b4?narHash=sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE%3D' (2026-06-26)
  → 'github:nixos/nixpkgs/61b7c44c4073f0b827768aff0049561b5110ea5a?narHash=sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84%3D' (2026-07-18)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/85570ef134d92a8702de6afd1f6f0209c863fa91?narHash=sha256-6QBThUi7SuK%2BdgA%2BDCaEkQGZN4kYx6DpXmK45%2BMG9zI%3D' (2026-05-30)
  → 'github:oxalica/rust-overlay/afacd6819d3765a05814ee8e3de74c77d42ac799?narHash=sha256-NYF7ZM5ip0u%2Bw1pBFDpIGEbrbgN/wpnLFAmBkWkYMXw%3D' (2026-07-19)
• Updated input 'treefmt-nix':
    'github:numtide/treefmt-nix/790751ff7fd3801feeaf96d7dc416a8d581265ba?narHash=sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0%3D' (2026-04-08)
  → 'github:numtide/treefmt-nix/df3c0640565d04a0261253cdd89fce78ec50168a?narHash=sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0%3D' (2026-07-18)

Running GitHub Actions on this PR

GitHub Actions will not run workflows on pull requests which are opened by a GitHub Action.

To run GitHub Actions workflows on this PR, run:

git branch -D update_flake_lock_action
git fetch origin
git checkout update_flake_lock_action
git commit --amend --no-edit
git push origin update_flake_lock_action --force

@github-actions
github-actions Bot force-pushed the update_flake_lock_action branch from a782057 to e49144d Compare July 19, 2026 13:04
Kent Overstreet and others added 29 commits July 29, 2026 17:25
bchfs_read() truncates the readahead bio's bi_iter.bi_size to the
current fragment with a swap, and the old error path skipped the
restore - the landmine the comment warned about, armed when
bch2_read_extent() started returning transaction restarts from
promote_alloc(): the retry saw the truncated bi_size, flagged its
fragment BCH_READ_last_fragment, and completed the read after one
fragment - with the remaining folios marked uptodate but never
submitted to any device, returning recycled pages as file data.

O_DIRECT was immune (stack iterator, restored before the error check),
no checksum was ever computed over the never-read region, and a
completed promote heals it (nopromote_already_promoted short-circuits
the restartable lookup) - hence unreproducible-looking wrong buffered
reads that eventually fix themselves.

Restructure so every path restores the iterator except submitted-final-
fragment, where it isn't ours to touch - and clear the stale
BCH_READ_last_fragment on the restart retry (readpage_bio_extend() can
grow the bio between attempts, so the retried fragment is no longer
necessarily the last). must_clone stays sticky: once a fragment split
off, error attribution requires cloning (see __bch2_read_extent()).

Reported-by: jrt37 (IRC), with an exceptional analysis pinning the
mechanism from the field
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Same class as the bchfs_read() fix: flags accumulate across the retry
loop, so a failed attempt's BCH_READ_last_fragment survives into the
retry. bvec_iter is a stack iterator and stable across attempts, but
the extent isn't - the retry re-looks it up, and if a concurrent write
split it, the retried fragment is no longer the last: the stale flag
submits it as final and the loop breaks, returning success with the
rest of the O_DIRECT buffer never read.

Narrower window than the buffered bug (needs the extent to shrink
between attempts), same corruption class. Clear the flag on the retry
path; must_clone stays sticky, splits demand cloning for error
attribution.

Reported-by: jrt37's field analysis (the "sticky-last_fragment variant
that can bite O_DIRECT" claim, verified)
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Field report (CRCinAU): copying the kernel git tree's fs/bcachefs/ over
a dkms source tree loses version.h and module-version.c - they're staged
by bcachefs-tools 'make install_dkms', not carried in the kernel tree.
The result was a bare "no rule to make target module-version.o" with no
hint, and the prebuilt-module fetch silently never ran (no version, no
lookup key).

Now both degrade loudly: no version.h warns that the prebuilt lookup is
being skipped and why, and a missing module-version.c builds the module
without MODULE_VERSION instead of failing.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Field report: unclean-shutdown recovery died in check_dirents() with
ENOMEM_trans_kmalloc (+ the WARN_ON_ONCE in __bch2_trans_kmalloc),
failing the recovery pass and leaving the filesystem unmountable.

That error has exactly one source: a single transaction's bump
allocations exceeded BTREE_TRANS_MEM_MAX (64k). check_dirents processes
each dirent in one commit_do - trans mem resets per key - but within
one check_dirent call, three repair loops iterate over every snapshot
version of the target inode and batch all their updates into a single
commit; each version costs ~100-550 bytes of trans mem (bkey_inode_buf,
dirent copies, fsck_err log entries), so ~120+ versions needing repair
blows the cap. All the errors involved are AUTOFIX: a plain mount does
this, and the repair itself is what wedges the filesystem.

Gate each loop with bch2_trans_commit_lazy_if_full(). The re-drive then
runs against a partially committed batch, so each loop must converge on
committed state rather than repeat it:

 - the check_dirent_target() loop already converges:
   get_visible_inodes() rereads the inode versions, and committed
   backpointer/d_type repairs no longer fire

 - the dirent_to_inode_in_descendant_snapshot copy loop fires off
   inode-side state the dirent copies don't change: probe for an
   identical copy already committed at each snapshot and skip it

 - the dirent_to_overwritten_inode whiteout loop likewise never sees
   its snapshots_seen-derived predicate change: check whether the
   dirent is still visible in that snapshot before repairing - which
   also stops us reporting and re-whiteouting a dirent that is already
   invisible there

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
kfree_rcu(), kfree_rcu_mightsleep(), kvfree_rcu() and
kvfree_rcu_mightsleep() were all #defined to a plain kfree(), marked
/* XXX */. No grace period: the object is freed while readers may still
hold it.

fs/ is shared kernel/userspace source, so every one of the 19 call sites
is correct in the kernel and a use-after-free in the tools whenever a
reader is concurrent. Two that matter most:

 - fs/snapshots/snapshot.c frees the snapshot table this way, and every
   bch2_snapshot_parent() / _is_ancestor() / _redundant_interior() /
   _will_delete() reads it under guard(rcu)() via
   rcu_dereference(c->snapshots.table). fsck does this constantly.

 - fs/btree/interior.c carries a comment reading "kfree_rcu(), or a
   concurrent lookup can memcmp freed memory" - a description of the bug
   the shim introduced, sitting directly above a call to it.

Route them through liburcu's call_rcu(). The rcu_head embedded in the
object can't be used directly: call_rcu() hands the callback
&obj->rcu_field, and free() needs the base of the allocation, which isn't
recoverable from that without the field's offset - so carry the pointer
in a small wrapper. If the wrapper can't be allocated, block for a grace
period instead; slow, but the alternative is the bug being fixed.

Found while reading the core from a random `bcachefs image update`
segfault, which crashes inside liburcu's call_rcu callback dispatch loop
walking a queue that contains a pointer into unmapped memory. This is not
proven to be that crash - I haven't traced a specific pair - but it is
the right shape for it: rare, timing-dependent, and it moves under
unrelated commits.

Tested: builds clean, format + fsck work (which exercises the snapshot
table path). The image commands need a real block device, so the actual
crashing path isn't reachable outside a VM.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Same class as check_dirent's per-snapshot loops: one inode write per
visible snapshot version, batched into a single transaction, bounded
only by snapshot count. Gate with commit_lazy_if_full; convergence is
already built in - repairs are attempt-local and the inode walker
revalidates on commit via commit_count, so committed flag repairs
don't refire.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Field report (piotrszegda's sda2, on master with the check_dirents
bound): check_xattrs() died with ENOMEM_trans_kmalloc - the str_hash
repair for an xattr at the wrong offset fans whiteouts across every
snapshot that overwrote the position, one batched update per snapshot,
unbounded on deep-history filesystems.

Gate the loop with commit_lazy_if_full in fsck: the re-drive converges
since committed whiteouts fail the existing KEY_TYPE_deleted check and
are skipped. Runtime callers (extent splits) are excluded so their
whiteouts stay atomic with the extent update.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
…ipe width

"There is no configuration to limit stripe width" predates both. Spell
out that the cap excludes parity: a cap of 9 with 2 parity blocks gives
stripes 11 wide. Device selection within the bounds remains automatic.

Reported-by: doot (IRC)
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
On 32-bit architectures, `struct inode.i_ino` is 32-bit (`unsigned long`),
which truncates bcachefs's native 64-bit inode numbers (`ei_inum.inum`).
This causes:
 - Spurious BUG_ON panics in bch2_inode_update_after_write() and bch2_rename2()
 - Corrupted btree key positioning during Direct/buffered I/O, truncate, fallocate, and seek
 - Duplicate inode collision errors due to truncated stat->ino values

Fix these by using `inode->ei_inum.inum` instead of `inode->v.i_ino` across
the VFS layer.

(cherry picked from commit 5555322)
Zero-initialize `c::timespec` padding fields (`..unsafe { std::mem::zeroed() }`)
to safely handle Bindgen-generated struct padding (e.g. `_TIME_BITS=64`).

Additionally, extract duplicated `atime` and `mtime` conversion logic in
`setattr()` into a local `parse_time` closure.

(cherry picked from commit fcee95f)
Zero-initialize `c::timespec64` padding fields (`..unsafe { std::mem::zeroed() }`)
for Bindgen FFI safety on 32-bit `_TIME_BITS=64` targets.

Additionally, cast `Stat` timestamps explicitly to `i64` and consolidate the
`timespec64` conversion pipeline into a local `to_bch_time` closure.

(cherry picked from commit b46f867)
32-bit kernel builds can't emit u64 division libcalls: the compiler
lowers them to __udivdi3/__aeabi_uldivmod, which the kernel doesn't
export to modules.

(cherry picked from commit ece63a9)
[dropped the BITS_PER_LONG -> __SIZEOF_LONG__ hunk: the userspace
 build's kernel-compat shim defines BITS_PER_LONG from glibc's
 __WORDSIZE (include/linux/bitops.h), so it's correct in both builds]
BLKGETSIZE64 is _IOR(0x12,114,size_t): the number encodes sizeof(the
argument type), and the direction bits differ on powerpc/mips/sparc, so
it can't be a constant in our source. We had it three times - a pair of
cfg'd literals in bdev.rs, and two rustix opcode::read::<u64>() in
util.rs and qcow2.rs which encode 8 unconditionally and so are wrong on
32 bit, the same bug the literals were just patched for.

Let bindgen compute them from linux/fs.h instead. It needs
clang_macro_fallback (these expand to expressions, not literals) and it
needs stddef.h included first: the uapi header uses bare size_t and
leaves defining it to the includer, and without it bindgen's probe
fails to compile and drops the constant silently. Verified it computes
0x80081272 for x86_64 and 0x80041272 for i686/armhf.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
bch2_key_trigger() - the gc and mem-apply dispatch - ran
bch2_trigger_snapshot_nr_keys() unconditionally, while the trans-commit
dispatch gates it on btree_type_has_snapshots(). Snapshot-field btrees
carry a snapshot in their bpos without being snapshotted, so gc counted
their keys into per-snapshot accounting that the runtime side never
maintains: device_remove_background failed fsck -n with

  accounting mismatch for snapshot id=4294967295 btree=reconcile_pending:
        got 0 0 0
    should be 28973 1158920 0

- 28973 evacuation work items counted by gc only. Latent until snapshot
counters became mem entries (gc previously dropped its mods to them at
bch2_accounting_mem_mod_locked, so gc_done never saw the discrepancy).

Add the same btree_type_has_snapshots() gate to bch2_key_trigger().
Filesystems that ran the ungated gc may carry a spurious counter for
these btrees; the next gc counts zero there and corrects it back down.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Field report (1.38->1.40 upgrade, wedged in emergency-RO on every
mount): a snapshot node spliced out by a legacy flags-only
bch2_snapshot_node_delete() after the state field had already been
synced carries the tombstone wipe (parent/tree/depth/skiplist zeroed,
child retained as the live-descendant breadcrumb) and the legacy DELETED
bit - but a state field stale at its pre-splice value, no_keys.
state_compat trusts the nonzero state field, so the interior-deletion
collector queues the tombstone as a one-child no_keys node, and the
check_snapshots it forces edge-validates the breadcrumb as a live edge:
the child was reparented at splice time and doesn't point back ->
EINVAL_snapshot_child_bad_parent, emergency RO, and the same wedge on
the next mount.

Current writers dual-write state and flags atomically via
bch2_snapshot_state_set(), so a valid state that contradicts the flags
proves a legacy flags-only writer wrote last - the flags are the fresher
truth. The flags are single unprotected bits, though, so require
structural corroboration before overriding a codeword: tree == 0 is the
tombstone wipe's signature, unreachable for any live, will_delete or
no_keys node - a stray DELETED bitflip on a live-shaped node stays with
the state field. The repair (snapshot_state_stale_tombstone) rewrites
the state to deleted through the normal dual-write, making the node the
inert tombstone it already is on disk.

bch2_delete_dead_interior_snapshots() also re-collects its delete list
after the forced check_snapshots: the check pass is licensed to change
node states (this repair does exactly that), and a stale pre-check list
would send the now-deleted tombstone to bch2_snapshot_node_delete(),
which refuses already-deleted nodes.

Reproduced byte-for-byte and repair-verified by
snapshot-inject/stale_state_tombstone.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
The damage entries recorded by the snapshot sweeps
(bkey_in_deleted/missing_snapshot -> keys_deleted) carry the dead
snapshot's id - by report time the inode version at that snapshot is
gone and path resolution fails, so the report printed a bare
'inum N:S', useless to the user. On resolution failure, retry through
the inode's surviving snapshot versions: the path in a live view still
names the file. The bare-inum fallback remains for inodes with no
surviving version anywhere.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
…g dirent

When check_directory_structure walks an inode's bi_dir backpointer and the
naming dirent is gone (ENOENT), we can't recover the filename - but bi_dir
still identifies the directory the inode lived in. Print that path, so a user
recovering a damaged filesystem sees which directory lost a file instead of an
opaque inode number.

Gives a usable "what was lost" list where a full filename list isn't possible
(a missing dirent leaves no name to report).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
The hash_table_key_duplicate repair deletes the losing entry - a
user-visible dirent removal the damage report never mentioned - and in
the both-valid case renames one entry to .fsck_renamed-N, which the
user needs to know to go look for. Record the deletion as
dir_entries_removed on the directory, and the rename with a new
dir_entries_renamed damage type.

Only for dirents: str_hash also covers xattrs, which have no damage
type yet.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
…tent

The childless-no_keys fsck_err in check_should_delete_leaf() fires from
delete_dead_snapshots' collection loop, whose fix is out-of-band: the
node is only pushed to delete_leaves, deleted by later per-node commits.
__bch2_fsck_err() queues the repair log entry on the collection trans -
which never committed, so the entry was dropped at the next trans_begin,
tripping the debug-build dropped-updates WARN at iter.c:3774 (the known
no_keys_with_data / state_zeroed_pending_deletion CI failures, plus
their dmesg-window bleed victims).

Convert the collection loop to for_each_btree_key_commit so the log
entry rides out in the per-key commit; empty commits are a no-op for
the keys that queue nothing.

The commit adds a restart point after the collection pushes, which were
previously replay-safe only by ordering (all restart points above them):
make every push idempotent instead - nodup add for delete_leaves,
has_id guards on delete_interior and no_keys (deleting_from_trees
already used nodup).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
The collection-loop fix (c797ea3) silenced the first instance of
the iter.c dropped-updates WARN, unmasking the next (WARN_ON_ONCE, one
site per boot): check_subvol's unlinked branch calls
bch2_subvolume_set_deleted() with earlier repairs' fsck_err journal
log entries still queued, and bch2_subvolumes_reparent()'s opening
lockrestart_do drops them at trans_begin. In CI this failed
state_zeroed_pending_deletion / will_delete_dead_leaf_rewind directly
and stale_state_tombstone by dmesg bleed.

Same medicine as the collection loop: commit_lazy first - free when
nothing is queued (the pagecache-worker caller), and its
restart-on-success re-drives check_subvol, whose committed fixes don't
refire.

The class keeps producing instances: every path where an fsck_err
precedes a trans_begin-bearing helper on the same trans. The
framework-level question (fsck log entries as transactional updates at
all) is still open - each unmasking will surface the next site one CI
push at a time until it's answered.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
…mption

check_inode() exempts a subvol root with a missing subvolume from the
bi_subvol repair when its backpointer is zeroed - the expected state of
an unlinked subvolume pending sweep. But the exemption never verified
its premise: orderly deletion tombstones the subvolume and marks the
snapshot will_delete together, so subvolume-gone-but-snapshot-live
means the chain broke. On a damaged filesystem (field report,
piotrszegda's sda2) such an inode kept bi_subvol pointing at a missing
subvolume, and check_unreachable_inodes() then fail-stopped: the
reattach path's bch2_bkey_get_mut_typed() on the missing subvolume
returned ENOENT_bkey_type_mismatch, aborting recovery.

Require corroboration: the unlinked flag (which the delete path now
sets - see the following patch - but pre-fix filesystems lack), or the
snapshot actually being in a deletion state. Broken-chain inodes take
the existing repair and reattach as ordinary directories.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
BCH_INODE_unlinked means "we definitively know that there isn't, and
shouldn't be, a dirent pointing to this inode" - record that at the flag
definition, since every consumer's correctness hangs off it:
check_unreachable_inodes() reads it as "unreachable by design, don't
reattach", and the inode trigger enrolls flagged inodes in the
deleted_inodes btree.

Under that meaning an unlinked subvolume's root inode should carry the
flag - no dirent points at it, ever again - but its deletion still
belongs to the subvolume deletion path (subvolume state unlinked -> VFS
eviction -> snapshot sweep), never to the inode reaper. Add the helpers
the reaper guards will be built on:

- bch2_inode_is_subvolume_root(): the ownership discriminator.
  Deliberately mode-agnostic (bi_subvol, not S_ISDIR) so single-file
  subvolumes - which the deletion machinery already handles - stay
  correct.

- bch2_subvolume_deletion_pending(): is this subvolume anywhere in the
  deletion sequence? Covers the unlinked window, the deleted-tombstone
  window (bch2_subvolume_get() reports tombstones as ENOENT, so it
  arrives via the snapshot check), and mid-sweep. Subvolume gone with a
  live snapshot is damage, not pending deletion.

No behavioral change.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Subvolume roots are deleted by the subvolume deletion path - subvolume
state unlinked, VFS eviction, then the snapshot sweep - never by the
inode reaper. Make both reaper arms enforce that ownership rule, ahead
of the delete path setting BCH_INODE_unlinked on unlinked subvolume
roots (which makes them read i_nlink == 0):

- bch2_evict_inode(): don't route subvolume roots to bch2_inode_rm().
  This eviction completing is precisely what
  bch2_subvolume_wait_for_pagecache_and_delete() blocks on before
  tombstoning - deleting here would race the sweep it hands off to.
  (That race losing benignly was the "VFS incorrectly tried to delete
  inode" regression in subvol.snapshot_file_delete_2.)

- may_delete_deleted_inode(): drop deleted_inodes entries for subvolume
  roots - the trigger enrolls any inode transitioning to unlinked, so
  the entry is expected, but crash recovery for subvolume deletion
  belongs to check_subvols(), keyed off SUBVOLUME_STATE_unlinked. On
  the direct bch2_inode_rm() path, refuse with a traceable error: with
  the eviction guard that's unreachable, so reaching it means a bug or
  corruption, and "VFS incorrectly tried to delete inode" will print
  the errcode.

This also protects against a subvolume root that wrongly acquires the
unlinked flag (corruption): previously the reaper would consume it
without complaint, deleting keys out from under a pending snapshot
sweep whenever its probes happened to pass.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
BCH_INODE_unlinked means "we definitively know that there isn't, and
shouldn't be, a dirent pointing to this inode" - which is exactly the
state of a subvolume root once its subvolume is unlinked. Making the
flag the norm there keeps fsck's invariant local and uniform: a
directory without a backpointer is either flagged (pending deletion) or
damaged (reattach) - no subvolume-chain lookup needed to classify it.

Teach check_inode the three places that assumed the flag was
file-lifecycle only:

- The unlinked-dir strip: an unlinked subvolume root legitimately
  carries the flag and is legitimately non-empty (the snapshot sweep
  deletes the contents). Only strip when the subvolume isn't being
  deleted - then it's corruption, since the subvolume key is what links
  a subvolume root.

- The unlinked-inode reap: skip subvolume roots; deletion belongs to the
  subvolume path and check_subvols() resumes it after a crash.

- The missing-bi_subvol exemption: rebuilt on
  bch2_subvolume_deletion_pending(). When the chain corroborates and the
  root is flag-less (deleted before the delete path set the flag),
  repair by setting the flag, converging old filesystems to the
  invariant (subvol_root_unlinked_flag_missing, autofix).

The last one also fixes a leak in the previous exemption: a flagged root
whose chain did NOT corroborate (subvolume gone, snapshot live) was
exempted on the flag alone - and then nothing would ever delete it: the
sweep never comes (snapshot live) and check_unreachable_inodes() skips
unlinked inodes. Now it takes the bi_subvol-zeroing repair and becomes
an ordinary unlinked inode the reaper handles.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
With every consumer of BCH_INODE_unlinked now handling subvolume roots -
the eviction and deleted_inodes reaper arms guarded, check_inode's
unlinked handling subvolume-aware - the delete path can record what is
now definitively true: no dirent will ever point at this inode again.

fsck's invariant becomes local and uniform (a backpointer-less directory
is either flagged or damaged), check_unreachable_inodes() gets its
"don't reattach" witness straight from the inode, and a held-open
destroyed subvolume root now correctly reports st_nlink == 0 - the VFS
nlink update flows from the same inode write.

Previously applied without the consumer guards as eef1af1 and
reverted: VFS eviction routed the flagged root to bch2_inode_rm(), which
raced the snapshot sweep and logged "VFS incorrectly tried to delete
inode" (subvol.snapshot_file_delete_2).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
BCH_RENAME_OVERWRITE called bch2_inode_nlink_dec() on the victim
unconditionally - including when the victim is a subvolume root, which
the cross-subvolume EXDEV logic explicitly permits and nothing else
refuses: POSIX allows overwriting an empty directory, so
"mv somedir emptysubvol" reaches this today. The root got the unlinked
flag via nlink_dec while the subvolume stayed live - never unlinked, its
deletion machinery never engaged. Before the reaper guards that let VFS
eviction delete a live subvolume's root keys out from under it; with
them it merely left a dirent-less live subvolume for fsck to puzzle
over.

Overwriting a subvolume root is a subvolume deletion, same as unlink:
refuse if it has child subvolumes, hand it to the subvolume deletion
path, and mark the root unlinked. This also covers prospective
single-file subvolumes, where overwrite needs no empty-victim
precondition at all.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
…repair

The dirent_to_missing_parent_subvol / dirent_not_visible_in_parent_subvol
repair rewrites the dirent in place with bch2_bkey_make_mut_typed(...,
0, ...) - no BTREE_UPDATE_internal_snapshot_node. The dirent's own
position is routinely an interior snapshot node on any snapshotted
filesystem (dirents are created at the then-active snapshot, which
later becomes interior when the next snapshot splits it), so the commit
trips the interior-snapshot assertion in btree_insert_entry_checks()
(btree/commit.c:388) - BUG during offline fsck.

This is the flaky panic previously fingerprinted in the
missing_main_subvol repair cascade: whether the not-visible branch fired
with a resolvable replacement subvol depended on preceding repairs,
hence the flakiness. Reproduced deterministically by the check_root
injection tests (root subvolume deleted -> every DT_SUBVOL dirent's
parent_subvol dangles).

The neighboring subvolume rewrite in the same function needs no flag -
the subvolumes btree has no snapshot field.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Both repair branches wrote the inode with bch2_fsck_write_inode() - the
self-committing, restart-eating commit_do() wrapper - followed by
bch2_trans_commit_lazy(). When the inner write ate a restart and the
lazy commit then had nothing to commit, the sequence returned 0 with
trans->restart_count advanced, and the enclosing commit_do()'s
verify_not_restarted panics:

  trans->restart_count 3, should be 1, last restarted by bch2_fsck_write_inode

Hit deterministically by the root_inode_not_dir injection test: every
dirent key under the file-mode root fires key_in_wrong_inode_type, and
one internal restart during the repair storm trips the verify.
(Decoded from the module: the faulting verify is the per-key
commit_do()'s, immediately after the inlined bch2_trans_commit at
update.h:328.)

Use __bch2_fsck_write_inode(): the update queues in the caller's
transaction, the lazy commit (or the caller's commit) writes it, and
restarts propagate to the restart handler that owns them - which also
restores the batching the lazy commit was there for.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
@github-actions
github-actions Bot force-pushed the update_flake_lock_action branch from e49144d to d1a42e4 Compare August 7, 2026 09:51
@github-actions
github-actions Bot force-pushed the update_flake_lock_action branch from d1a42e4 to ea1dcfe Compare August 7, 2026 12:32
Flake lock file updates:

• Updated input 'crane':
    'github:ipetkov/crane/0532eb17955225173906d671fb36306bdeb1e2dc?narHash=sha256-EVZd2RsbpreRUDSi9rBwPY%2BZxoyMaiEBbZxxhljbaS4%3D' (2026-05-30)
  → 'github:ipetkov/crane/2c71e194474d13de031d729b729c968ddbe3507f?narHash=sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw%3D' (2026-08-03)
• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb?narHash=sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4%3D' (2026-05-13)
  → 'github:hercules-ci/flake-parts/427bf4bd9435fdf21321c8cc628c24efc14c0f7a?narHash=sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw%3D' (2026-08-01)
• Updated input 'flake-parts/nixpkgs-lib':
    'github:nix-community/nixpkgs.lib/f5901329dade4a6ea039af1433fb087bd9c1fe14?narHash=sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ%3D' (2026-04-26)
  → 'github:nix-community/nixpkgs.lib/0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c?narHash=sha256-OmshNvn2vupOFpYinLUu%2B1Dnpu4n7Q5N3ggGVNHpkUI%3D' (2026-07-26)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/e73de5be04e0eff4190a1432b946d469c794e7b4?narHash=sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE%3D' (2026-06-26)
  → 'github:nixos/nixpkgs/b7c2ada94fe99c15b0dbcf4d11fd7850b957a436?narHash=sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM%3D' (2026-08-05)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/85570ef134d92a8702de6afd1f6f0209c863fa91?narHash=sha256-6QBThUi7SuK%2BdgA%2BDCaEkQGZN4kYx6DpXmK45%2BMG9zI%3D' (2026-05-30)
  → 'github:oxalica/rust-overlay/57a23bfaf4f7017267294b161175db1e32eb1c85?narHash=sha256-jfR6OhwurCKn1tREyfOcK/Omxf1Q/DzDDFbnEr1mBLs%3D' (2026-08-07)
• Updated input 'treefmt-nix':
    'github:numtide/treefmt-nix/790751ff7fd3801feeaf96d7dc416a8d581265ba?narHash=sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0%3D' (2026-04-08)
  → 'github:numtide/treefmt-nix/ae7910970dddc408fe6ab1c8e4b277bb21d72dc0?narHash=sha256-NLSyTCW4K4ofhNBllt3omPasm6QpralXH1DBZOc91Dw%3D' (2026-08-05)
@github-actions
github-actions Bot force-pushed the update_flake_lock_action branch from ea1dcfe to 5ba9f57 Compare August 7, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants