Linux userland API discussions
 help / color / mirror / Atom feed
From: Aurelien DESBRIERES <aurelien@hackers.camp>
To: Aurelien DESBRIERES <aurelien@hackers.camp>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Randy Dunlap <rdunlap@infradead.org>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>
Cc: linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-doc@vger.kernel.org, aurelien.desbrieres@gmail.com,
	linux-api@vger.kernel.org
Subject: [RFC PATCH 07/18] beamfs: add superblock operations
Date: Mon, 19 Oct 2026 06:17:17 +0200	[thread overview]
Message-ID: <20261019041728.327785-8-aurelien@hackers.camp> (raw)
In-Reply-To: <20261019041728.327785-1-aurelien@hackers.camp>

Mount and unmount, statfs, sync, the inode cache, and registration of
the "beamfs" filesystem type, which requires a block device. The
tracepoints are instantiated in this file.

Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
 fs/beamfs/super.c | 2012 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 2012 insertions(+)
 create mode 100644 fs/beamfs/super.c

diff --git a/fs/beamfs/super.c b/fs/beamfs/super.c
new file mode 100644
index 000000000000..fcf255408877
--- /dev/null
+++ b/fs/beamfs/super.c
@@ -0,0 +1,2012 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Superblock operations
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/slab.h>
+#include <linux/buffer_head.h>
+#include <linux/ktime.h>
+#include <linux/statfs.h>
+#include "beamfs.h"
+
+/*
+ * One translation unit defines the tracepoints; every other file just
+ * includes the header. This one carries the definition because it is
+ * always built.
+ */
+#define CREATE_TRACE_POINTS
+#include "beamfs_trace.h"
+
+/* Inode cache (slab allocator) */
+static struct kmem_cache *beamfs_inode_cachep;
+
+/*
+ * alloc_inode - allocate a new inode with beamfs_inode_info embedded
+ */
+static struct inode *beamfs_alloc_inode(struct super_block *sb)
+{
+	struct beamfs_inode_info *fi;
+
+	fi = kmem_cache_alloc(beamfs_inode_cachep, GFP_KERNEL);
+	if (!fi)
+		return NULL;
+
+	memset(fi->i_direct, 0, sizeof(fi->i_direct));
+	fi->i_indirect  = 0;
+	fi->i_dindirect = 0;
+	fi->i_tindirect = 0;
+	fi->i_flags     = 0;
+	fi->i_blocks_counted = false;
+	mmb_init(&fi->i_metadata_bhs, &fi->vfs_inode.i_data);
+	mutex_init(&fi->i_alloc_mutex);
+	beamfs_rsv_init(fi);
+
+	return &fi->vfs_inode;
+}
+
+/*
+ * free_inode - return inode to slab cache
+ */
+static void beamfs_free_inode(struct inode *inode)
+{
+	mutex_destroy(&BEAMFS_I(inode)->i_alloc_mutex);
+	kmem_cache_free(beamfs_inode_cachep, BEAMFS_I(inode));
+}
+
+/*
+ * statfs - filesystem statistics
+ */
+static int beamfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+	struct super_block   *sb  = dentry->d_sb;
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+	buf->f_type    = BEAMFS_MAGIC;
+	buf->f_bsize   = sb->s_blocksize;
+	buf->f_blocks  = le64_to_cpu(sbi->s_beamfs_sb->s_block_count);
+	buf->f_bfree   = sbi->s_free_blocks;
+	buf->f_bavail  = sbi->s_free_blocks;
+	buf->f_files   = le64_to_cpu(sbi->s_beamfs_sb->s_inode_count);
+	buf->f_ffree   = sbi->s_free_inodes;
+	buf->f_namelen = BEAMFS_MAX_FILENAME;
+
+	return 0;
+}
+
+/*
+ * put_super - release superblock resources
+ */
+static void beamfs_put_super(struct super_block *sb)
+{
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+	/*
+	 * The shared metadata list is written and emptied before the
+	 * buffers on it are released: one left there keeps b_mmb pointing
+	 * into sbi after kfree, for drop_buffers to follow later.
+	 */
+	if (sbi) {
+		if (mmb_sync(&sbi->s_shared_bhs))
+			pr_err("beamfs: put_super: shared metadata not written\n");
+		mmb_invalidate(&sbi->s_shared_bhs);
+	}
+
+	if (sbi) {
+		/* Stop the sweep before the structures it reads go away. */
+		beamfs_scrub_exit(sb);
+		beamfs_tc_exit(sbi);
+
+		/*
+		 * Rebuild, then write, then release -- in that order.
+		 *
+		 * The VFS calls sync_fs before put_super, so this is usually
+		 * one pass over buffers that are already clean.
+		 *
+		 * The rebuild was here and the write was not, so
+		 * beamfs_destroy_bitmap released the buffers with brelse
+		 * and the images died in memory. Every block freed since
+		 * the last sync stayed marked used on disk: fsck reported
+		 * 122368 used-but-unreferenced blocks after one write of
+		 * 400 MiB and its deletion, and the count doubled with each
+		 * cycle. df was right and the disk was not.
+		 *
+		 * The superblock was written here from the start, which is
+		 * why the free count looked correct while the bitmap
+		 * underneath it did not.
+		 */
+		beamfs_bitmap_encode_pending(sb);
+		beamfs_super_encode_pending(sbi);
+
+		{
+			unsigned int dirty = 0, written = 0;
+			int first_err = 0;
+			u32 k;
+
+			for (k = 0; sbi->s_bitmap_blkhs &&
+			     k < sbi->s_bitmap_blocks_count; k++) {
+				struct buffer_head *bh = sbi->s_bitmap_blkhs[k];
+				int rc;
+
+				if (!bh || !buffer_dirty(bh))
+					continue;
+				dirty++;
+				rc = sync_dirty_buffer(bh);
+				if (rc) {
+					if (!first_err)
+						first_err = rc;
+					pr_err("beamfs: umount: bitmap block %u did not reach the medium\n",
+					       k);
+				} else {
+					written++;
+				}
+			}
+			/*
+			 * The last chance a freed block has to be recorded
+			 * as free on the medium: nearly half the updates to
+			 * the bitmap are attached to no inode and nothing
+			 * else carries them here.
+			 */
+			trace_beamfs_bitmap_flush(sb->s_dev, "put_super",
+						  dirty, written, first_err);
+		}
+		/*
+		 * The superblock last, and loudly: a volume whose
+		 * superblock did not land is one the next mount reads as
+		 * it was before everything above.
+		 */
+		/* A clean unmount: the mounted flag comes off before the superblock goes. */
+		if (!sb_rdonly(sb) && !READ_ONCE(sbi->s_failed) && sbi->s_beamfs_sb) {
+			u32 fl = le32_to_cpu(sbi->s_beamfs_sb->s_flags);
+
+			sbi->s_beamfs_sb->s_flags = cpu_to_le32(fl & ~BEAMFS_SB_FLAG_MOUNTED);
+			beamfs_dirty_super_now(sbi);
+		}
+		if (sbi->s_sbh && buffer_dirty(sbi->s_sbh) &&
+		    sync_dirty_buffer(sbi->s_sbh))
+			pr_err("beamfs: umount: the superblock did not reach the medium\n");
+
+		beamfs_destroy_bitmap(sb);
+		mempool_destroy(sbi->s_wb_pages);
+		sbi->s_wb_pages = NULL;
+		mempool_destroy(sbi->s_scratch_pool);
+		sbi->s_scratch_pool = NULL;
+		kvfree(sbi->s_sb_rs_staging);
+		sbi->s_sb_rs_staging = NULL;
+		brelse(sbi->s_sbh);
+		kfree(sbi->s_beamfs_sb);
+		kfree(sbi);
+		sb->s_fs_info = NULL;
+	}
+}
+
+/*
+ * seen_block -- linear search for a block id in the seen[] array.
+ * Used by beamfs_free_data_blocks to free each direct block once when
+ * the inode is corrupted -- a bit flip in a pointer can leave two
+ * slots naming the same physical block.
+ */
+static bool seen_block(const u64 *seen, unsigned int n, u64 blk)
+{
+	unsigned int i;
+
+	for (i = 0; i < n; i++) {
+		if (seen[i] == blk)
+			return true;
+	}
+	return false;
+}
+
+/*
+ * beamfs_free_one_block -- free a data block, refusing a second free.
+ *
+ * A block that is already clear in the bitmap when we come to free it
+ * means two pointers reached it: either a genuine aliasing bug or, more
+ * likely here, a pointer flipped by a particle so that two slots name
+ * the same physical block. Freeing it twice would clear a bit that a
+ * live file now owns, turning a read error into silent cross-file
+ * corruption -- which is the failure mode this filesystem exists to
+ * prevent.
+ *
+ * The event is journalled rather than merely logged: an aliased pointer
+ * discovered during teardown is exactly the forensic record the RS
+ * journal is for, and dmesg alone leaves the run reading as an
+ * unexplained space discrepancy.
+ *
+ * Returns true if the block was freed.
+ */
+static bool beamfs_free_one_block(struct super_block *sb, u64 blk)
+{
+	if (!blk)
+		return false;
+
+	if (!beamfs_block_is_allocated(sb, blk)) {
+		pr_err_ratelimited("beamfs: refusing double free of block %llu\n",
+				   (unsigned long long)blk);
+		beamfs_log_rs_event_flagged(sb, blk, NULL, 0,
+					    BEAMFS_SUBBLOCK_DATA, 0);
+		return false;
+	}
+
+	beamfs_free_block(sb, blk, NULL);
+	return true;
+}
+
+/*
+ * beamfs_free_ind_range -- free part or all of an indirection subtree.
+ *
+ * @blk:    the indirect block itself
+ * @depth:  1 = its pointers are data blocks
+ *          2 = its pointers are single-indirect blocks
+ *          3 = its pointers are double-indirect blocks
+ * @base:   logical index of the first data block this subtree covers
+ * @first:  logical index from which to free; everything below survives
+ *
+ * Returns true when the subtree is now empty and @blk itself has been
+ * freed, false when something in it survived and @blk was kept.
+ *
+ * Absolute indices rather than a running countdown. The countdown
+ * version shared one mutable counter across the whole recursion, mixing
+ * "how much to preserve" with "how far we have got", so no call could
+ * reason about its own range without knowing what its siblings had
+ * consumed. It freed indirect blocks that still held live pointers;
+ * the allocator handed them out as data blocks; a later lookup read
+ * file contents as pointers. xfstests generic/013 found it as
+ * phys=6148914691236517205 -- 0x5555... , fsstress payload.
+ *
+ * Here every subtree knows its own range by construction: child j of a
+ * block at @base covers [base + j*span, base + (j+1)*span). Whether it
+ * is wholly above @first, wholly below, or straddling follows from
+ * arithmetic, with no state to get out of step.
+ *
+ * Holes are why survivors counts pointers rather than blocks: beamfs
+ * allocates on demand, so a subtree can be sparsely populated and a
+ * count derived from logical indices would not match what is actually
+ * there.
+ */
+bool beamfs_free_ind_range(struct super_block *sb, u64 blk,
+			   unsigned int depth, u64 base, u64 first,
+			   struct inode *inode)
+{
+	u64 nptrs = BEAMFS_BLOCK_SIZE / sizeof(__le64);
+	u64 child_span = 1;
+	struct buffer_head *ibh;
+	unsigned int j;
+	u64 survivors = 0;
+	bool dirtied = false;
+
+	if (!blk)
+		return true;
+
+	for (j = 1; j < depth; j++)
+		child_span *= nptrs;
+
+	/* Wholly below the cut: nothing here is going away. */
+	if (base + child_span * nptrs <= first)
+		return false;
+
+	ibh = beamfs_bread(sb, blk, "superblock");
+	if (!ibh) {
+		pr_err_ratelimited("beamfs: cannot read indirect block %llu, subtree left allocated\n",
+				   (unsigned long long)blk);
+		return false;
+	}
+
+	/*
+	 * Clear the slots under the buffer lock, the way the install
+	 * sites do.
+	 *
+	 * Zeroing a pointer is a store into a shared indirect block: the
+	 * flusher can be submitting that buffer while the loop runs. But
+	 * the work between reads sleeps -- the recursion, and
+	 * free_one_block through the bitmap -- so the lock cannot be held
+	 * across it. Decide first, then take the lock only to write the
+	 * slots that are going away.
+	 */
+	for (j = 0; j < nptrs; j++) {
+		__le64 *ptrs = (__le64 *)ibh->b_data;
+		u64 child = le64_to_cpu(ptrs[j]);
+		u64 child_base = base + (u64)j * child_span;
+
+		if (!child)
+			continue;
+
+		if (depth > 1) {
+			if (beamfs_free_ind_range(sb, child, depth - 1,
+						  child_base, first, inode)) {
+				lock_buffer(ibh);
+				beamfs_tc_clear(sb, ibh->b_blocknr, (u32)j);
+				ptrs[j] = 0;
+				unlock_buffer(ibh);
+				dirtied = true;
+			} else {
+				survivors++;
+			}
+			continue;
+		}
+
+		if (child_base < first) {
+			survivors++;
+			continue;
+		}
+
+		beamfs_free_one_block(sb, child);
+		lock_buffer(ibh);
+		beamfs_tc_clear(sb, ibh->b_blocknr, (u32)j);
+		ptrs[j] = 0;
+		unlock_buffer(ibh);
+		dirtied = true;
+	}
+
+	if (dirtied) {
+		/*
+		 * The parity has to follow the block down.
+		 *
+		 * This is the only path that takes pointers out of an
+		 * indirect block, and it left the parity describing the
+		 * state before. A verify then "corrects" the block toward
+		 * that older state and hands back a pointer that was
+		 * cleared -- to a block the allocator has since given to
+		 * another file.
+		 *
+		 * A frozen generic/083 volume shows it exactly: block
+		 * 48682 holds one pointer on disk and its parity
+		 * describes two, so reading it corrected produces 48683,
+		 * which belongs to inode 1317. The checker reported a
+		 * block shared between two inodes that share nothing.
+		 *
+		 * On the inode's metadata list for the same reason every
+		 * other site is: a buffer dirtied and attached to nothing
+		 * is never flushed by __writeback_single_inode.
+		 */
+		beamfs_ind_parity_update(sb, ibh, inode);
+		if (inode) {
+			mmb_mark_buffer_dirty(ibh,
+					      &BEAMFS_I(inode)->i_metadata_bhs);
+			/*
+			 * And the inode: a list nothing marks is a list
+			 * write_inode never empties, and the comment
+			 * above is exactly about a buffer that reaches
+			 * the medium late.
+			 */
+			mark_inode_dirty(inode);
+		} else {
+			mark_buffer_dirty(ibh);
+		}
+	}
+	/*
+	 * Free the block only once nothing points out of it. A block with
+	 * one surviving pointer is still load-bearing: freeing it hands
+	 * the allocator a block whose live pointers a later lookup still
+	 * follows.
+	 */
+	if (survivors == 0) {
+		/*
+		 * bforget, not brelse, for a block that is going away.
+		 *
+		 * ext2_free_branches ends exactly this way -- bforget(bh)
+		 * and then ext2_free_blocks -- and beamfs, which carries
+		 * the same i_metadata_bhs field and the same mmb_*
+		 * helpers, called brelse. __bforget is clear_buffer_dirty,
+		 * remove_assoc_queue and __brelse: the middle one is what
+		 * was missing.
+		 *
+		 * Twenty lines above, this buffer was attached to the
+		 * inode's metadata list. brelse leaves it there while the
+		 * block goes back to the allocator, so the next owner gets
+		 * that same buffer from sb_bread, writes its pointers into
+		 * it -- and when the OLD inode is evicted, mmb_invalidate
+		 * empties its list without writing anything, taking the
+		 * new owner's pointers with it.
+		 *
+		 * generic/083 on 2026-09-18, three captures with the same
+		 * shape. In the clearest, blocks 8890 and 8892 were freed
+		 * from inode 20 at 34.563 and handed to inode 102 at
+		 * 34.656; 8890 became an indirect block, 8892 its first
+		 * child, and fsck found 8892 marked used and referenced by
+		 * nothing -- with 92 of 4096 bytes set, so it had been
+		 * written. No lost pointer was reported, because none was
+		 * lost: the pointers were right and the block holding them
+		 * never reached the medium.
+		 *
+		 * Only on this branch. A block with survivors keeps its
+		 * buffer legitimately dirty and must still be written;
+		 * clearing that is how an earlier attempt at this fix, one
+		 * placed in beamfs_free_block where it caught all twenty-two
+		 * callers, turned 58 leaked blocks into 345 and thousands
+		 * of out-of-range pointers.
+		 */
+		bforget(ibh);
+		beamfs_free_one_block(sb, blk);
+		return true;
+	}
+	brelse(ibh);
+	return false;
+}
+
+/*
+ * beamfs_free_data_blocks -- release all data blocks of a deleted inode.
+ *
+ * Frees the direct blocks and the three indirection trees, or the data
+ * block of a long symlink. Called from evict_inode, under
+ * i_alloc_mutex, when nlink has dropped to 0.
+ *
+ * A bit flip landing on i_direct[] can leave two slots naming the same
+ * physical block, and freeing it once per slot is a double free. A
+ * direct block is freed only if no earlier slot named it, tracked in
+ * seen[]; the indirection trees go through beamfs_free_one_block, which
+ * refuses a block the bitmap already calls free.
+ *
+ * seen[] is allocated with GFP_NOFS, to avoid recursion into the
+ * filesystem from this path. On OOM the direct blocks are freed without
+ * deduplication, and beamfs_free_block then refuses, with a
+ * rate-limited warning, a second free of a block still free.
+ */
+static void beamfs_free_data_blocks(struct inode *inode)
+{
+	struct beamfs_inode_info *fi = BEAMFS_I(inode);
+	struct super_block      *sb = inode->i_sb;
+	u64                     *seen;
+	unsigned int             n_seen = 0;
+	unsigned int             cap;
+	u64                      nptrs = BEAMFS_BLOCK_SIZE / sizeof(__le64);
+	int                      i;
+
+	/*
+	 * Symlinks come in two shapes and i_size tells them apart.
+	 *
+	 * Short ones keep the target in i_direct[] as raw bytes. Those
+	 * bytes are not block pointers: freeing them would hand the
+	 * allocator the ASCII of the path read as little-endian u64, which
+	 * is how that bug presents. They own nothing, so there is nothing
+	 * to free.
+	 *
+	 * Long ones keep the target in a data block with i_direct[0]
+	 * holding its number, and that block has to go back. Returning
+	 * early for every symlink -- correct while the short form was the
+	 * only form -- would leak one block per long symlink, forever.
+	 */
+	if (S_ISLNK(inode->i_mode)) {
+		if (i_size_read(inode) >= (loff_t)sizeof(fi->i_direct)) {
+			u64 blk = le64_to_cpu(fi->i_direct[0]);
+
+			if (blk)
+				beamfs_free_block(sb, blk, NULL);
+		}
+		memset(fi->i_direct, 0, sizeof(fi->i_direct));
+		return;
+	}
+
+	cap = BEAMFS_DIRECT_BLOCKS + (unsigned int)nptrs + 1;
+	seen = kmalloc_array(cap, sizeof(*seen), GFP_NOFS);
+
+	/* Free direct blocks */
+	for (i = 0; i < BEAMFS_DIRECT_BLOCKS; i++) {
+		u64 blk = le64_to_cpu(fi->i_direct[i]);
+
+		if (blk && (!seen || !seen_block(seen, n_seen, blk))) {
+			beamfs_free_block(sb, blk, NULL);
+			if (seen)
+				seen[n_seen++] = blk;
+		}
+		fi->i_direct[i] = 0;
+	}
+
+	/*
+	 * Indirection levels. All three are walked: the allocator reaches
+	 * triple indirect, so anything shallower here leaks every block a
+	 * file held past 2 MiB -- permanently, since nothing else ever
+	 * clears those bits.
+	 */
+	/*
+	 * first = 0: the inode is going away, so nothing survives.
+	 * base is the logical index the level starts at, which is what
+	 * lets each subtree place itself without a shared counter.
+	 */
+	if (fi->i_indirect) {
+		beamfs_free_ind_range(sb, le64_to_cpu(fi->i_indirect), 1,
+				      BEAMFS_MAX_IBLOCK_DIRECT, 0, inode);
+		fi->i_indirect = 0;
+	}
+	if (fi->i_dindirect) {
+		beamfs_free_ind_range(sb, le64_to_cpu(fi->i_dindirect), 2,
+				      BEAMFS_MAX_IBLOCK_INDIRECT, 0, inode);
+		fi->i_dindirect = 0;
+	}
+	if (fi->i_tindirect) {
+		beamfs_free_ind_range(sb, le64_to_cpu(fi->i_tindirect), 3,
+				      BEAMFS_MAX_IBLOCK_DINDIRECT, 0, inode);
+		fi->i_tindirect = 0;
+	}
+
+	kfree(seen);
+}
+
+/*
+ * evict_inode - called when the last reference to an inode is dropped.
+ * An inode with no links left has its blocks freed, its mode zeroed on
+ * disk and its number returned to the bitmap. Every inode's metadata
+ * buffers are written before its list is dropped.
+ */
+static void beamfs_evict_inode(struct inode *inode)
+{
+	/* Its reservation window, before its blocks. */
+	beamfs_rsv_discard(inode);
+	truncate_inode_pages_final(&inode->i_data);
+	/*
+	 * If the file is truly deleted (nlink == 0), free all data blocks,
+	 * zero i_mode on disk so the inode table scan at next mount
+	 * correctly identifies this slot as free, then release the inode
+	 * number back to the bitmap.
+	 */
+	if (!inode->i_nlink) {
+		/*
+		 * Under the allocator's mutex, like every other walk of
+		 * the indirection tree.
+		 *
+		 * Eviction is not obviously concurrent with anything --
+		 * the inode has no references left -- but the tree it
+		 * walks is the same one the allocator writes, and the
+		 * blocks it returns go back to a bitmap other inodes are
+		 * drawing from. Taking the lock costs nothing on a path
+		 * that runs once per inode and removes the question.
+		 */
+		mutex_lock(&BEAMFS_I(inode)->i_alloc_mutex);
+		beamfs_free_data_blocks(inode);
+		mutex_unlock(&BEAMFS_I(inode)->i_alloc_mutex);
+		inode->i_mode = 0;
+		{
+			int _w = beamfs_write_inode_raw(inode);
+
+			/*
+			 * The blocks are already free. An inode that
+			 * does not reach the medium leaves the next
+			 * mount reading a mode that is not zero, over
+			 * blocks the bitmap has given away.
+			 */
+			if (_w)
+				pr_err_ratelimited("beamfs: evict: inode %llu not written: %d\n",
+						   (unsigned long long)inode->i_ino,
+						   _w);
+		}
+	}
+	/*
+	 * Written before the list is thrown away.
+	 *
+	 * mmb_invalidate empties the list without writing anything --
+	 * fs/buffer.c takes each buffer off the queue and the contents go
+	 * with whatever they held. For an inode that still has links,
+	 * that is metadata its pointers still reach.
+	 *
+	 * Without the sync, generic/269 left 2230 indirect blocks whose
+	 * pointer was installed and whose contents never reached the
+	 * medium -- fsck read each as never described, the subtree under
+	 * it unreachable. A probe counted 23510 evictions and 23510
+	 * invalidations, 16381 of them from umount: every dirty metadata
+	 * buffer an inode still owned at unmount was dropped.
+	 *
+	 * write_inode syncs the same list; eviction has to as well.
+	 */
+	{
+		int had = mmb_has_buffers(&BEAMFS_I(inode)->i_metadata_bhs);
+		int err = 0;
+
+		/*
+		 * Said whichever way it goes.
+		 *
+		 * write_inode traces its own sync and evict traced
+		 * nothing, so a run leaving 22 indirect blocks that
+		 * nothing ever wrote gave no way to tell an inode whose
+		 * list was empty from one whose list was thrown away. @n
+		 * is what the list held when the call started.
+		 */
+		/*
+		 * Written whether or not the inode survives.
+		 *
+		 * A deleted inode used to drop its list on the grounds that
+		 * its blocks are freed above and what they hold no longer
+		 * matters. That holds for its own indirect blocks. It does
+		 * not hold for the two kinds of buffer the volume shares: the
+		 * bitmap block, and the indirect parity region, which carries
+		 * the parity of fourteen indirect blocks belonging to other,
+		 * living files. Those live on s_shared_bhs (one list, never an
+		 * inode's) and are synced here first, before the inode's own.
+		 *
+		 * The bitmap has a net -- sync_fs and put_super walk
+		 * s_bitmap_blkhs and write every block still dirty. The
+		 * parity region has none: dropped here, it is gone, and the
+		 * fourteen indirect blocks it described read as never
+		 * described, each taking its subtree with it. That is what
+		 * fsck reports as "no parity was ever written for it, so
+		 * nothing ever wrote the block" -- a deduction from an
+		 * empty slot, on blocks that were written.
+		 *
+		 * Forcing pointee-before-pointer ordering
+		 * (CONFIG_BEAMFS_ORDERED_META) did not change the count,
+		 * which is what ruled the block itself out and left its
+		 * parity as the thing that never landed.
+		 */
+		err = mmb_sync(&BEAMFS_SB(inode->i_sb)->s_shared_bhs);
+		if (!err)
+			err = mmb_sync(&BEAMFS_I(inode)->i_metadata_bhs);
+		if (err)
+			pr_err_ratelimited("beamfs: inode %llu: metadata not written before evict: %d\n",
+					   (unsigned long long)inode->i_ino,
+					   err);
+		trace_beamfs_mmb(inode->i_sb->s_dev, inode->i_ino,
+				 inode->i_nlink ? "evict sync" : "evict sync unlinked",
+				 had, err);
+
+		mmb_invalidate(&BEAMFS_I(inode)->i_metadata_bhs);
+	}
+	clear_inode(inode);
+	/*
+	 * Ordering constraint: beamfs_free_inode_num() must run AFTER
+	 * clear_inode(), which asserts the inode owns no metadata
+	 * buffer_heads. beamfs_free_data_blocks() above calls
+	 * beamfs_free_block(sb, blk, NULL) -- the NULL owner keeps
+	 * mmb_mark_buffer_dirty() from re-attaching bitmap buffer_heads
+	 * to a dying inode after its page cache has been torn down.
+	 * Deferring the inode-number free until after clear_inode() also
+	 * closes a window where a concurrent beamfs_alloc_inode_num()
+	 * could hand out the same ino before the VFS had finished.
+	 *
+	 * Root cause: beamfs_free_block() used to pass the evicted inode
+	 * as owner, appending bitmap bh's to its metadata list after
+	 * truncate_inode_pages_final() had emptied it, which tripped
+	 * clear_inode()'s assertion. Confirmed by ftrace/kprobe on
+	 * beamfs_evict_inode and clear_inode during `depmod -a` on the
+	 * rootfs (renameat2 path).
+	 *
+	 * The mechanism was i_data.i_private_list with
+	 * mark_buffer_dirty_inode() and invalidate_inode_buffers() until
+	 * the kernel replaced it with the per-inode mapping_metadata_bhs
+	 * list and the mmb_* helpers. The ordering requirement is
+	 * unchanged; only the names are.
+	 */
+	if (!inode->i_nlink)
+		beamfs_free_inode_num(inode->i_sb, (u64)inode->i_ino);
+}
+
+/*
+ * beamfs_sync_fs - establish bitmap-before-inode ordering at sync time.
+ *
+ * Without this hook, sync_filesystem() submits
+ * the inode-table buffer (containing fresh inode->i_direct[] pointers)
+ * through sync_blockdev_nowait() while the bitmap buffer (carrying the
+ * matching bit clear) is left to the bdi writeback queue without
+ * deterministic ordering. On reboot()/kernel_restart shortly after,
+ * the inode may reach disk while the bitmap clear does not, persisting
+ * a state where boot N+1 mounts a bitmap that declares the block free
+ * while an inode still references it. First unlink at t~10s on boot N+1
+ * trips beamfs_free_block canary "double free of block N".
+ *
+ * Kernel sync_filesystem() sequence (fs/sync.c):
+ *   writeback_inodes_sb(sb)         // start I/O on dirty inodes, no wait
+ *   sb->s_op->sync_fs(sb, 0)        // <-- our hook, wait=0
+ *   sync_blockdev_nowait(s_bdev)    // submit other dirty bhs, no wait
+ *   sync_inodes_sb(sb)              // wait inodes flushed
+ *   sb->s_op->sync_fs(sb, 1)        // <-- our hook, wait=1
+ *   sync_blockdev(s_bdev)           // wait all bhs
+ *
+ * The wait=0 call lands BEFORE sync_blockdev_nowait, so calling
+ * sync_dirty_buffer (= submit + wait) on every bitmap buffer here
+ * forces bitmap to disk BEFORE the inode-table buffer is even
+ * submitted. This is the exact ordering required.
+ *
+ * Instrumentation: one pr_debug per call reports the number of dirty
+ * bitmap buffers found, how many were flushed, and the time spent.
+ * It is a pr_debug rather than a pr_info because sync_fs runs on every
+ * sync: under load that is a log line per second for data that is only
+ * of interest when investigating. Enable with dyndbg when needed.
+ */
+static int beamfs_sync_fs(struct super_block *sb, int wait)
+{
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+	u32 k;
+	unsigned int n_bitmap_dirty = 0;
+	unsigned int n_bitmap_synced = 0;
+	unsigned int n_sb_synced = 0;
+	int last_err = 0;
+	u64 t0_ns, t1_ns;
+
+	if (!sbi)
+		return 0;
+
+	t0_ns = ktime_get_ns();
+
+	/*
+	 * Rebuild anything still flagged as stale before any of it is
+	 * written. beamfs_write_bitmap_block and beamfs_dirty_super
+	 * rebuild as they mark the buffer dirty, so this normally finds
+	 * nothing to do; see beamfs_write_bitmap_block for why the bitmap
+	 * cannot wait for sync.
+	 */
+	beamfs_bitmap_encode_pending(sb);
+	beamfs_super_encode_pending(sbi);
+
+	/* Flush all dirty bitmap buffers synchronously. */
+	if (sbi->s_bitmap_blkhs) {
+		for (k = 0; k < sbi->s_bitmap_blocks_count; k++) {
+			struct buffer_head *bh = sbi->s_bitmap_blkhs[k];
+			int rc;
+
+			if (!bh)
+				continue;
+			if (!buffer_dirty(bh))
+				continue;
+			n_bitmap_dirty++;
+			rc = sync_dirty_buffer(bh);
+			if (rc) {
+				if (!last_err)
+					last_err = rc;
+			} else {
+				n_bitmap_synced++;
+			}
+		}
+	}
+
+	/* Flush superblock buffer (free_blocks counter + RS journal). */
+	if (sbi->s_sbh && buffer_dirty(sbi->s_sbh)) {
+		int rc = sync_dirty_buffer(sbi->s_sbh);
+
+		if (rc) {
+			if (!last_err)
+				last_err = rc;
+		} else {
+			n_sb_synced = 1;
+		}
+	}
+
+	t1_ns = ktime_get_ns();
+	pr_debug("beamfs/sync310: wait=%d bitmap_dirty=%u bitmap_synced=%u sb_synced=%u err=%d dt_ns=%llu\n",
+		 wait, n_bitmap_dirty, n_bitmap_synced, n_sb_synced,
+		last_err, (unsigned long long)(t1_ns - t0_ns));
+	trace_beamfs_bitmap_flush(sb->s_dev, "sync_fs", n_bitmap_dirty,
+				  n_bitmap_synced, last_err);
+
+	return last_err;
+}
+
+/*
+ * Persist everything this inode owns, not just the inode.
+ *
+ * Called by __writeback_single_inode during any WB_SYNC_ALL writeback,
+ * which is what 7.3 added: before it, a filesystem had its own fsync
+ * and several racing fsyncs could each see the dirty bits already clear
+ * and return before the metadata was on the medium. I_SYNC serialises
+ * it properly now, and the metadata buffers attached to the inode are
+ * written here rather than from a path the VFS does not know about.
+ *
+ * ext2 has the same shape. beamfs descends from its design and this
+ * follows it.
+ */
+static int beamfs_sync_inode_metadata(struct inode *inode,
+				      struct writeback_control *wbc)
+{
+	int err = mmb_sync(&BEAMFS_SB(inode->i_sb)->s_shared_bhs);
+
+	if (!err)
+		err = mmb_sync(&BEAMFS_I(inode)->i_metadata_bhs);
+	if (err)
+		pr_err_ratelimited("beamfs: cannot sync metadata of inode %lu\n",
+				   (unsigned long)inode->i_ino);
+	return err;
+}
+
+static const struct super_operations beamfs_super_ops = {
+	.alloc_inode    = beamfs_alloc_inode,
+	.free_inode     = beamfs_free_inode,
+	.evict_inode    = beamfs_evict_inode,
+	.put_super      = beamfs_put_super,
+	.write_inode    = beamfs_write_inode,
+	.sync_inode_metadata = beamfs_sync_inode_metadata,
+	.sync_fs        = beamfs_sync_fs,
+	.statfs         = beamfs_statfs,
+};
+
+/*
+ * beamfs_sb_to_rs_staging -- serialize the CRC32-covered region of a
+ * superblock into a contiguous staging buffer for RS encode/decode.
+ *
+ * Output buffer layout (BEAMFS_SB_RS_STAGING_BYTES bytes):
+ *   [0, off_crc32)                              region A:
+ *                                                 sb_bytes[0..off_crc32)
+ *   [off_crc32, BEAMFS_SB_RS_COVERAGE_BYTES)     region B:
+ *                                                 sb_bytes[off_uuid..off_pad)
+ *   [BEAMFS_SB_RS_COVERAGE_BYTES,
+ *    BEAMFS_SB_RS_STAGING_BYTES)                 zero pad to round up
+ *                                                 to whole RS subblocks
+ *
+ * Field offsets are derived from struct layout via offsetof, so the
+ * helper is invariant under future format extensions provided
+ * BEAMFS_SB_RS_COVERAGE_BYTES is updated in lockstep. BUILD_BUG_ON
+ * below enforces that consistency at compile time.
+ *
+ * The s_crc32 field [off_crc32, off_uuid) is excluded, exactly as
+ * beamfs_crc32_sb() does. Same coverage on both protection layers.
+ *
+ * Must match mkfs.beamfs.c::sb_to_rs_staging() byte-for-byte.
+ */
+static void beamfs_sb_to_rs_staging(const struct beamfs_super_block *sb,
+				    u8 staging[BEAMFS_SB_RS_STAGING_BYTES])
+{
+	const u8 *base = (const u8 *)sb;
+	const size_t off_crc32 = offsetof(struct beamfs_super_block, s_crc32);
+	const size_t off_uuid  = offsetof(struct beamfs_super_block, s_uuid);
+	const size_t off_pad   = offsetof(struct beamfs_super_block, s_pad);
+
+	BUILD_BUG_ON(off_crc32 + (off_pad - off_uuid) !=
+		     BEAMFS_SB_RS_COVERAGE_BYTES);
+	BUILD_BUG_ON(BEAMFS_SB_RS_STAGING_BYTES <
+		     BEAMFS_SB_RS_COVERAGE_BYTES);
+
+	memcpy(staging, base, off_crc32);
+	memcpy(staging + off_crc32, base + off_uuid, off_pad - off_uuid);
+	memset(staging + BEAMFS_SB_RS_COVERAGE_BYTES, 0,
+	       BEAMFS_SB_RS_STAGING_BYTES - BEAMFS_SB_RS_COVERAGE_BYTES);
+}
+
+/*
+ * beamfs_sb_from_rs_staging -- inverse of beamfs_sb_to_rs_staging.
+ * Restores the (possibly RS-corrected) bytes from staging back onto
+ * the superblock, leaving s_crc32 (bytes [off_crc32, off_uuid))
+ * untouched. The trailing zero-pad bytes of staging
+ * [BEAMFS_SB_RS_COVERAGE_BYTES, BEAMFS_SB_RS_STAGING_BYTES) are not
+ * copied back.
+ *
+ * Used on the mount-time RS recovery path in beamfs_fill_super.
+ */
+static void beamfs_sb_from_rs_staging(const u8 staging[BEAMFS_SB_RS_STAGING_BYTES],
+				      struct beamfs_super_block *sb)
+{
+	u8 *base = (u8 *)sb;
+	const size_t off_crc32 = offsetof(struct beamfs_super_block, s_crc32);
+	const size_t off_uuid  = offsetof(struct beamfs_super_block, s_uuid);
+	const size_t off_pad   = offsetof(struct beamfs_super_block, s_pad);
+
+	BUILD_BUG_ON(off_crc32 + (off_pad - off_uuid) !=
+		     BEAMFS_SB_RS_COVERAGE_BYTES);
+
+	memcpy(base, staging, off_crc32);
+	memcpy(base + off_uuid, staging + off_crc32, off_pad - off_uuid);
+}
+
+/*
+ * Rebuild the superblock's on-disk image if it is flagged as stale.
+ *
+ * beamfs_dirty_super rebuilds on every change and clears
+ * s_super_needs_encode -- see there for why the superblock cannot wait
+ * for sync -- so from sync_fs and put_super this normally finds nothing
+ * to do.
+ */
+void beamfs_super_encode_pending(struct beamfs_sb_info *sbi)
+{
+	if (!sbi || !sbi->s_super_needs_encode)
+		return;
+	sbi->s_super_needs_encode = false;
+	beamfs_dirty_super_now(sbi);
+}
+
+/*
+ * The volume has failed. Say so once, and stop writing.
+ *
+ * ext4 calls this ext4_error and XFS calls it a shutdown; both do the
+ * same two things -- record the failure so nothing else tries, and
+ * take the mount read-only so the VFS stops sending work. Neither
+ * unwinds what was in flight: the point is to stop, not to repair.
+ *
+ * Not cleared on any path. A volume that failed once has to be
+ * unmounted and checked; pretending otherwise is how a bad device
+ * becomes a corrupt filesystem.
+ */
+void beamfs_fail(struct super_block *sb, const char *where, int err)
+{
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+	if (!sbi)
+		return;
+	if (cmpxchg(&sbi->s_failed, false, true))
+		return;			/* someone else was first */
+
+	pr_err("beamfs: volume failed in %s (%d), refusing further writes\n",
+	       where, err);
+
+	/*
+	 * s_failed is enough, and SB_RDONLY is not ours to set.
+	 *
+	 * ext4 says so in as many words -- "We don't set SB_RDONLY because
+	 * that requires sb->s_umount" -- and this path holds no such lock.
+	 * Worse, get_tree_bdev returns -EBUSY when the flags of an
+	 * existing superblock differ from the ones asked for, so a volume
+	 * marked read-only here could not be mounted rw again while its
+	 * superblock lived. generic/338 failed one volume with dm-error
+	 * and every test after it got "No space left on device" from a
+	 * mount that had quietly refused: nine failures from one.
+	 *
+	 * What refuses a failed volume, with EIO: a write through
+	 * beamfs_inline_iomap_begin, the allocator and its
+	 * callers, an inode write, the superblock and the bitmap. Data
+	 * already accepted into the page cache is still written back. The
+	 * next mount starts from a zeroed sb_info.
+	 */
+}
+
+/*
+ * A scratch page, or NULL.
+ *
+ * __GFP_NORETRY, and every caller handles NULL: this is reached from
+ * beamfs_ind_parity_verify, which runs under i_alloc_mutex, while a
+ * reader on the same inode holds a locked folio and waits for that
+ * mutex. A wait here is a wait the folio waiter cannot outlast --
+ * mempool_alloc with GFP_NOFS sleeps until somebody returns a page,
+ * and under generic/464 the somebody was itself queued behind the
+ * folio.
+ *
+ * generic/464 wedged the node twice that way. The blocked-state dump
+ * shows one xfs_io in __mutex_lock under beamfs_inline_read_folio_range
+ * and twenty tasks in folio_wait_bit_common behind it, the machine
+ * spinning at 140% with nothing moving for half an hour.
+ *
+ * Giving up costs a verify that does not happen. Waiting costs the
+ * mount.
+ */
+void *beamfs_scratch_get(struct super_block *sb)
+{
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+	if (!sbi || !sbi->s_scratch_pool)
+		return kmalloc(BEAMFS_BLOCK_SIZE, GFP_NOFS | __GFP_NORETRY);
+	return mempool_alloc(sbi->s_scratch_pool,
+			     GFP_NOFS | __GFP_NORETRY | __GFP_NOWARN);
+}
+
+void beamfs_scratch_put(struct super_block *sb, void *p)
+{
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+	if (!p)
+		return;
+	if (!sbi || !sbi->s_scratch_pool) {
+		kfree(p);
+		return;
+	}
+	mempool_free(p, sbi->s_scratch_pool);
+}
+
+/*
+ * beamfs_dirty_super - propagate the authoritative in-memory superblock
+ * (sbi->s_beamfs_sb) onto the buffer head, recompute its RS parity and
+ * s_crc32, and mark the buffer dirty for writeback.
+ *
+ * Every site that mutates the on-disk superblock (free_blocks,
+ * free_inodes, RS journal, ...) must call this helper instead of
+ * mark_buffer_dirty(sbi->s_sbh) directly. Without the CRC refresh,
+ * the on-disk superblock keeps a stale checksum that fails verification
+ * at the next mount.
+ *
+ * Caller MUST hold sbi->s_lock so that the snapshot copied to the
+ * buffer head is taken atomically with respect to other writers.
+ */
+void beamfs_dirty_super(struct beamfs_sb_info *sbi)
+{
+	if (!sbi)
+		return;
+
+	/*
+	 * Rebuilt now, not at sync.
+	 *
+	 * The superblock buffer comes from the block device's cache, so
+	 * the periodic flusher can write it at any time without going
+	 * through sync_fs. A rebuild left for later would let it reach
+	 * the disk with the bits of one state and the parity of another
+	 * -- and a superblock whose parity does not match is a volume
+	 * that may not mount. The cost is thirteen shortened RS codewords
+	 * per change.
+	 *
+	 * The flag is cleared here, so beamfs_super_encode_pending, called
+	 * from sync_fs and put_super, finds nothing left to do.
+	 */
+	sbi->s_super_needs_encode = false;
+	beamfs_dirty_super_now(sbi);
+}
+
+void beamfs_dirty_super_now(struct beamfs_sb_info *sbi)
+{
+	struct beamfs_super_block *fsb;
+	u32 crc;
+
+	if (!sbi || !sbi->s_sbh || !sbi->s_beamfs_sb)
+		return;
+
+	/*
+	 * Nothing more goes to a volume that has failed.
+	 *
+	 * The superblock buffer could not be read back on a device
+	 * answering EIO, so it is not uptodate, and mark_buffer_dirty
+	 * below opens with WARN_ON_ONCE(!buffer_uptodate) -- which is the
+	 * kernel saying that a buffer nobody could read holds nothing
+	 * worth writing. generic/338 puts dm-error under the mount and
+	 * watches for it.
+	 */
+	if (READ_ONCE(sbi->s_failed))
+		return;
+	if (!buffer_uptodate(sbi->s_sbh)) {
+		beamfs_fail(sbi->s_sb, "dirty_super", -EIO);
+		return;
+	}
+
+	fsb = (struct beamfs_super_block *)sbi->s_sbh->b_data;
+
+	/*
+	 * Copy the authoritative in-memory image onto the buffer head.
+	 * Several callers update sbi->s_beamfs_sb in place without touching
+	 * fsb; this memcpy serializes them onto disk.
+	 */
+	memcpy(fsb, sbi->s_beamfs_sb, sizeof(*fsb));
+
+	/*
+	 * Encode RS parity over the CRC32-covered region (skipping
+	 * s_crc32). The staging buffer, BEAMFS_SB_RS_STAGING_BYTES (2769
+	 * bytes), is allocated once per mount by beamfs_fill_super rather
+	 * than placed on the stack, which it would push past the 2 KiB
+	 * frame budget. Without it the RS encode is skipped and only the
+	 * CRC32 below is refreshed.
+	 */
+	{
+		u8 *staging = sbi->s_sb_rs_staging;
+		u8 *parity_dst = (u8 *)fsb + BEAMFS_SB_RS_PARITY_OFFSET;
+
+		/*
+		 * The buffer is the mount's, not this call's.
+		 *
+		 * This allocated 2769 bytes on every call, and
+		 * beamfs_free_block reaches it under s_lock -- a
+		 * spinlock. kvmalloc sleeps, and an allocation that
+		 * sleeps under a spinlock is a deadlock waiting for a
+		 * machine busy enough to take the slow path.
+		 *
+		 * DEBUG_ATOMIC_SLEEP found it on the first generic/076
+		 * after the check was turned on: "BUG: sleeping function
+		 * called from invalid context at sched/mm.h:322", rm
+		 * holding sb_writers, i_alloc_mutex and s_lock, in
+		 * __kvmalloc_node_noprof under beamfs_dirty_super_now
+		 * under beamfs_free_block under beamfs_evict_inode.
+		 *
+		 * Writing it is serialised by s_lock, which every path
+		 * into this function holds or takes.
+		 */
+		if (!staging) {
+			pr_warn_ratelimited("beamfs: dirty_super: no staging buffer; skipping RS re-encode\n");
+		} else {
+			beamfs_sb_to_rs_staging(fsb, staging);
+			{
+				int _e = beamfs_rs_encode_region(staging,
+						BEAMFS_SB_RS_DATA_LEN,
+						parity_dst, BEAMFS_RS_PARITY,
+						BEAMFS_SB_RS_DATA_LEN,
+						BEAMFS_SB_RS_SUBBLOCKS);
+
+				/*
+				 * The superblock: a failure here costs
+				 * the whole volume on the next mount.
+				 */
+				if (_e < 0)
+					pr_err("beamfs: superblock encode failed: %d\n",
+					       _e);
+			}
+		}
+	}
+
+	crc = beamfs_crc32_sb(fsb);
+	fsb->s_crc32 = cpu_to_le32(crc);
+	sbi->s_beamfs_sb->s_crc32 = fsb->s_crc32;
+
+	mark_buffer_dirty(sbi->s_sbh);
+}
+
+/*
+ * beamfs_log_rs_event_flagged -- record an RS correction event in the
+ *                       superblock persistent journal (40-byte
+ *                       entry), with caller-supplied extra flags
+ *                       OR-ed into re_flags.
+ *
+ * See beamfs.h for full parameter contract. Forensic policy summary:
+ *   n_positions >= 2 -> Shannon entropy computed, ENTROPY_VALID set
+ *   n_positions == 1 -> entropy zeroed, ENTROPY_VALID cleared
+ *   n_positions == 0 -> UNCORRECTABLE set; extra_flags MAY still
+ *                       carry RMW_NEUTRALISED if the uncorrectable
+ *                       event was detected during an RMW transit
+ *                       (in which case the encode that follows
+ *                       cannot recover the data either; the flip
+ *                       persists in RAM and on disk after this
+ *                       cycle, but the RMW context is preserved
+ *                       for forensic analysis).
+ *
+ * extra_flags MUST NOT set BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID or
+ * BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE; those bits are policy-owned
+ * and reserved for the implementation.
+ *
+ * Safe to call from any context; spinlock-protected internally.
+ */
+void beamfs_log_rs_event_flagged(struct super_block *sb,
+				 u64 block_no,
+			const u8 *positions,
+			unsigned int n_positions,
+			size_t code_len_bytes,
+			u32 extra_flags)
+{
+	struct beamfs_sb_info  *sbi = BEAMFS_SB(sb);
+	struct beamfs_rs_event *ev;
+	u8 head;
+	u32 entropy_q16 = 0;
+	u32 flags = 0;
+	const u32 reserved_mask = BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID |
+				  BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE;
+
+	if (!sbi || !sbi->s_sbh)
+		return;
+
+	/*
+	 * Invariant guards: surface bug at WARN_ON_ONCE without panicking.
+	 * The journal entry is skipped if an invariant is violated;
+	 * reserved bits in extra_flags are cleared instead.
+	 *
+	 * Two valid call shapes:
+	 *   (a) correctable: positions != NULL, 1 <= n_positions <= RS/2
+	 *   (b) uncorrectable: positions == NULL, n_positions == 0
+	 */
+	if (WARN_ON_ONCE(n_positions > BEAMFS_RS_PARITY / 2))
+		return;
+	if (WARN_ON_ONCE(!n_positions != !positions))
+		return;
+	if (WARN_ON_ONCE(code_len_bytes == 0 ||
+			 code_len_bytes > BEAMFS_SUBBLOCK_DATA))
+		return;
+	if (WARN_ON_ONCE(extra_flags & reserved_mask))
+		extra_flags &= ~reserved_mask;
+
+	/*
+	 * Forensic policy:
+	 *   n_positions >= 2  -> entropy computed, ENTROPY_VALID set
+	 *   n_positions == 1  -> entropy zero, ENTROPY_VALID cleared
+	 *                       (single-sample, not forensically significant)
+	 *   n_positions == 0  -> UNCORRECTABLE set, entropy zero,
+	 *                       symbol_count zero (codeword exceeded RS
+	 *                       correction radius; data unrecoverable)
+	 */
+	if (n_positions == 0) {
+		flags = BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE;
+	} else if (n_positions >= 2) {
+		entropy_q16 = beamfs_rs_compute_entropy_q16_16(positions,
+							       n_positions,
+							      code_len_bytes);
+		flags = BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID;
+	}
+
+	flags |= extra_flags;
+
+	spin_lock(&sbi->s_lock);
+
+	head = sbi->s_beamfs_sb->s_rs_journal_head % BEAMFS_RS_JOURNAL_SIZE;
+	ev   = &sbi->s_beamfs_sb->s_rs_journal[head];
+
+	ev->re_block_no       = cpu_to_le64(block_no);
+	ev->re_timestamp      = cpu_to_le64(ktime_get_ns());
+	ev->re_symbol_count   = cpu_to_le32(n_positions);
+	ev->re_entropy_q16_16 = cpu_to_le32(entropy_q16);
+	ev->re_flags          = cpu_to_le32(flags);
+	ev->re_reserved       = 0; /* structural sentinel, MUST stay zero */
+	ev->re_pad            = 0; /* structural sentinel, MUST stay zero */
+	{
+		u32 ev_crc = beamfs_crc32(ev,
+					 offsetof(struct beamfs_rs_event,
+						  re_crc32));
+		ev->re_crc32 = cpu_to_le32(ev_crc);
+	}
+
+	sbi->s_beamfs_sb->s_rs_journal_head = (head + 1) % BEAMFS_RS_JOURNAL_SIZE;
+	/* What ext2 calls EXT2_ERROR_FS: the kernel saw damage it could not undo. */
+	if (flags & BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE)
+		sbi->s_beamfs_sb->s_flags |= cpu_to_le32(BEAMFS_SB_FLAG_ERRORS);
+
+	beamfs_dirty_super(sbi);
+
+	spin_unlock(&sbi->s_lock);
+
+	trace_beamfs_rs_event(sb->s_dev, block_no,
+			      beamfs_rs_event_subblock(flags),
+			      n_positions, flags);
+	pr_debug("beamfs: RS event block=%llu symbols=%u entropy_valid=%u entropy_q16=%u rmw_neutralised=%u uncorrectable=%u\n",
+		 block_no, n_positions,
+		 (flags & BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID) ? 1 : 0,
+		 entropy_q16,
+		 (flags & BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED) ? 1 : 0,
+		 (flags & BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE) ? 1 : 0);
+}
+
+/*
+ * beamfs_log_rs_event -- record an RS event with no extra context flag.
+ * Equivalent to beamfs_log_rs_event_flagged(..., 0); used by the
+ * replay of superblock recovery events in beamfs_fill_super.
+ */
+void beamfs_log_rs_event(struct super_block *sb,
+			 u64 block_no,
+			const u8 *positions,
+			unsigned int n_positions,
+			size_t code_len_bytes)
+{
+	beamfs_log_rs_event_flagged(sb, block_no, positions, n_positions,
+				    code_len_bytes, 0);
+}
+
+/*
+ * Pending RS recovery event captured during the SB CRC32-failure path,
+ * to be replayed into the journal once sbi is fully initialized.
+ *
+ * Capturing first and replaying later decouples the recovery detection
+ * from the journal write: it preserves the fail-secure invariant
+ * (validate the SB image before allocating sbi) while keeping the
+ * forensic record of SB recovery, unless the mount fails between
+ * capture and replay.
+ */
+struct beamfs_pending_rs_event {
+	u64 block_no;
+	unsigned int n_positions;
+	size_t code_len_bytes;
+	u8 positions[BEAMFS_RS_PARITY / 2];
+};
+
+/*
+ * beamfs_capture_sb_rs_events -- record which superblock subblocks RS
+ * repaired, for replay into the journal once sbi exists.
+ *
+ * The journal cannot be written yet: the superblock is being read, so
+ * sbi is not initialised. The corrections themselves are already in
+ * the superblock buffer, which carries them to disk with its next
+ * write; what is deferred is only their forensic record, which is why
+ * a failed allocation here returns 0 rather than failing the mount.
+ *
+ * @rs_results:   per-subblock symbol counts from beamfs_rs_decode_region,
+ *                positive where that subblock was corrected
+ * @rs_positions: flat array of corrected symbol positions,
+ *                BEAMFS_RS_PARITY / 2 slots per subblock
+ * @out:          receives the allocated event buffer, NULL if none
+ *
+ * Returns the number of events stored in *out.
+ */
+static unsigned int
+beamfs_capture_sb_rs_events(const int *rs_results, const u8 *rs_positions,
+			    struct beamfs_pending_rs_event **out)
+{
+	struct beamfs_pending_rs_event *pending;
+	unsigned int i, k = 0, n_events = 0;
+
+	*out = NULL;
+
+	for (i = 0; i < BEAMFS_SB_RS_SUBBLOCKS; i++) {
+		if (rs_results[i] > 0)
+			n_events++;
+	}
+	if (!n_events)
+		return 0;
+
+	pending = kmalloc_array(n_events, sizeof(*pending), GFP_KERNEL);
+	if (!pending) {
+		/*
+		 * The kernel already reports the allocation failure itself,
+		 * so this says only what it does not: how many recovery
+		 * events go unrecorded as a result. Debug level because the
+		 * superblock is corrected either way -- what is lost is the
+		 * forensic trace, not the data.
+		 */
+		pr_debug("beamfs: %u SB recovery events not journalled\n",
+			 n_events);
+		return 0;
+	}
+
+	for (i = 0; i < BEAMFS_SB_RS_SUBBLOCKS; i++) {
+		unsigned int np;
+		const u8 *src;
+
+		if (rs_results[i] <= 0)
+			continue;
+
+		np = (unsigned int)rs_results[i];
+		if (np > BEAMFS_RS_PARITY / 2)
+			np = BEAMFS_RS_PARITY / 2;
+
+		pending[k].block_no = BEAMFS_RS_BLOCK_NO_SB_MARKER | (u64)i;
+		pending[k].n_positions = np;
+		pending[k].code_len_bytes = BEAMFS_SB_RS_DATA_LEN;
+		src = rs_positions + i * (BEAMFS_RS_PARITY / 2);
+		memcpy(pending[k].positions, src, np * sizeof(*src));
+		k++;
+	}
+
+	*out = pending;
+	return k;
+}
+
+/*
+ * beamfs_fill_super - read superblock from disk and initialize VFS sb
+ */
+int beamfs_fill_super(struct super_block *sb, struct fs_context *fc)
+{
+	struct beamfs_sb_info     *sbi;
+	struct beamfs_super_block *fsb;
+	struct buffer_head       *bh;
+	struct inode             *root_inode;
+	__u32                     crc;
+	struct beamfs_pending_rs_event *pending = NULL;
+	unsigned int              n_pending = 0;
+	int                       ret = -EINVAL;
+
+	/* Set block size */
+	if (!sb_set_blocksize(sb, BEAMFS_BLOCK_SIZE)) {
+		errorf(fc, "beamfs: unable to set block size %d", BEAMFS_BLOCK_SIZE);
+		return -EINVAL;
+	}
+
+	/* Read block 0 - superblock */
+	bh = beamfs_bread(sb, 0, "superblock");
+	if (!bh) {
+		errorf(fc, "beamfs: unable to read superblock");
+		return -EIO;
+	}
+
+	fsb = (struct beamfs_super_block *)bh->b_data;
+
+	/* Verify magic */
+	if (le32_to_cpu(fsb->s_magic) != BEAMFS_MAGIC) {
+		errorf(fc, "beamfs: bad magic 0x%08x (expected 0x%08x)",
+		       le32_to_cpu(fsb->s_magic), BEAMFS_MAGIC);
+		goto out_brelse;
+	}
+
+	/* Strict version check: this kernel mounts only BEAMFS_VERSION_CURRENT
+	 * images. Older v2/v3 images require offline migration via mkfs.beamfs
+	 * --migrate. Rationale: dual-format in-kernel parsing doubles the audit
+	 * surface (KASAN, syzkaller) for no operational benefit on a niche FS.
+	 */
+	if (le32_to_cpu(fsb->s_version) != BEAMFS_VERSION_CURRENT) {
+		errorf(fc, "beamfs: unsupported on-disk version %u (this kernel requires v%u)",
+		       le32_to_cpu(fsb->s_version), BEAMFS_VERSION_CURRENT);
+		goto out_brelse;
+	}
+
+	/* Verify CRC32 of superblock (excluding the crc32 field itself) */
+	crc = beamfs_crc32_sb(fsb);
+	if (crc != le32_to_cpu(fsb->s_crc32)) {
+		u8 *staging = NULL;
+		u8 *parity_src = (u8 *)fsb + BEAMFS_SB_RS_PARITY_OFFSET;
+		int *rs_results = NULL;
+		u8 *rs_positions = NULL;
+		int rc;
+
+		/*
+		 * Heap-allocate the RS scratch buffers (2769 + 52 + 416 bytes
+		 * total) to keep beamfs_fill_super under the 2 KB stack budget.
+		 */
+		staging      = kvmalloc(BEAMFS_SB_RS_STAGING_BYTES, GFP_NOFS);
+		rs_results   = kvmalloc_array(BEAMFS_SB_RS_SUBBLOCKS,
+					      sizeof(*rs_results), GFP_NOFS);
+		rs_positions = kvmalloc_array(BEAMFS_SB_RS_SUBBLOCKS *
+					      (BEAMFS_RS_PARITY / 2),
+					      sizeof(*rs_positions), GFP_NOFS);
+		if (!staging || !rs_results || !rs_positions) {
+			errorf(fc, "beamfs: superblock RS recovery: out of memory");
+			ret = -ENOMEM;
+			kvfree(staging);
+			kvfree(rs_results);
+			kvfree(rs_positions);
+			goto out_brelse;
+		}
+
+		pr_warn("beamfs: superblock CRC32 mismatch (got 0x%08x, expected 0x%08x), attempting RS recovery\n",
+			crc, le32_to_cpu(fsb->s_crc32));
+
+		beamfs_sb_to_rs_staging(fsb, staging);
+		rc = beamfs_rs_decode_region(staging, BEAMFS_SB_RS_DATA_LEN,
+					     parity_src, BEAMFS_RS_PARITY,
+					    BEAMFS_SB_RS_DATA_LEN,
+					    BEAMFS_SB_RS_SUBBLOCKS,
+					    rs_results,
+					    rs_positions,
+					    BEAMFS_RS_PARITY / 2,
+				"superblock");
+		if (rc < 0) {
+			errorf(fc, "beamfs: superblock CRC32 mismatch and RS uncorrectable");
+			kvfree(staging);
+			kvfree(rs_results);
+			kvfree(rs_positions);
+			goto out_brelse;
+		}
+
+		beamfs_sb_from_rs_staging(staging, fsb);
+		kvfree(staging);
+		staging = NULL;
+
+		crc = beamfs_crc32_sb(fsb);
+		if (crc != le32_to_cpu(fsb->s_crc32)) {
+			errorf(fc, "beamfs: superblock CRC32 still mismatch after RS recovery");
+			kvfree(rs_results);
+			kvfree(rs_positions);
+			goto out_brelse;
+		}
+
+		pr_warn("beamfs: superblock corrected by RS FEC\n");
+
+		n_pending = beamfs_capture_sb_rs_events(rs_results,
+							rs_positions,
+							&pending);
+
+		/* Heap buffers consumed; free before continuing. */
+		kvfree(rs_results);
+		kvfree(rs_positions);
+	}
+
+	/*
+	 * Validate the feature fields.
+	 *
+	 * s_data_protection_scheme: range-check against the enum maximum.
+	 *   The three high-order bytes of the __le32 act as a structural
+	 *   sentinel; any value above BEAMFS_DATA_PROTECTION_MAX is rejected.
+	 * s_feat_incompat:  unknown bits are a hard refusal (any read).
+	 * s_feat_ro_compat: unknown bits force SB_RDONLY but allow mount.
+	 * s_feat_compat:    informational, never gates mount.
+	 */
+	{
+		u32 scheme = le32_to_cpu(fsb->s_data_protection_scheme);
+		u64 unknown_incompat  = le64_to_cpu(fsb->s_feat_incompat) &
+					~BEAMFS_FEAT_INCOMPAT_SUPP;
+		u64 unknown_ro_compat = le64_to_cpu(fsb->s_feat_ro_compat) &
+					~BEAMFS_FEAT_RO_COMPAT_SUPP;
+		u64 unknown_compat    = le64_to_cpu(fsb->s_feat_compat) &
+					~BEAMFS_FEAT_COMPAT_SUPP;
+
+		if (scheme > BEAMFS_DATA_PROTECTION_MAX) {
+			errorf(fc, "beamfs: invalid data_protection_scheme %u (max %u)",
+			       scheme, BEAMFS_DATA_PROTECTION_MAX);
+			goto out_brelse;
+		}
+
+		/*
+		 * A volume written before the inode checksum covered the
+		 * block pointers.
+		 *
+		 * Its i_crc32 spans the head of the inode alone, so this
+		 * kernel would compute a different value for every inode
+		 * on it and read the whole table as damaged -- thousands
+		 * of RS decodes against parity that is correct, and a
+		 * mount that appears to work while reporting corruption
+		 * everywhere.
+		 *
+		 * Refusing with the reason is the kinder failure. The
+		 * volume is not damaged and its data is intact; it needs
+		 * a rewrite, not a repair, and saying so is more use
+		 * than a page of decoder errors.
+		 */
+		if (!(le64_to_cpu(fsb->s_feat_incompat) &
+		      BEAMFS_FEATURE_INCOMPAT_INODE_CRC_FULL)) {
+			errorf(fc,
+			       "beamfs: this volume predates INODE_CRC_FULL -- its inode checksums do not cover the block pointers, so a corrupted pointer would go uncorrected. Copy the data off and remake the volume.");
+			goto out_brelse;
+		}
+
+		/*
+		 * The parity region carries its own FEC now, and that
+		 * changed how many slots a region block holds: fourteen
+		 * under RS instead of sixteen, because only
+		 * BEAMFS_DATA_INLINE_BYTES of the block is payload.
+		 *
+		 * Reading an older volume with this arithmetic addresses
+		 * the wrong slot and hands back a neighbour's parity,
+		 * which reads as damage on a healthy block. The volume is
+		 * fine; it needs a rewrite, not a repair.
+		 *
+		 * Only checked when a region exists -- with parity off
+		 * there is nothing to be incompatible about.
+		 */
+		if (le32_to_cpu(fsb->s_ind_parity_mode) != BEAMFS_IND_PARITY_NONE &&
+		    !(le64_to_cpu(fsb->s_feat_incompat) &
+		      BEAMFS_FEATURE_INCOMPAT_IND_PARITY_FEC)) {
+			errorf(fc,
+			       "beamfs: this volume predates IND_PARITY_FEC -- its parity region has no protection of its own and a different slot geometry, so reading it here would return the wrong block's parity. Copy the data off and remake the volume.");
+			goto out_brelse;
+		}
+
+		if (unknown_incompat) {
+			errorf(fc, "beamfs: unsupported incompat features 0x%016llx",
+			       unknown_incompat);
+			goto out_brelse;
+		}
+
+		if (unknown_ro_compat && !sb_rdonly(sb)) {
+			pr_warn("beamfs: unsupported ro_compat features 0x%016llx, forcing read-only mount\n",
+				unknown_ro_compat);
+			sb->s_flags |= SB_RDONLY;
+		}
+
+		if (unknown_compat)
+			pr_info("beamfs: unknown compat features 0x%016llx (informational)\n",
+				unknown_compat);
+	}
+
+	/* Allocate in-memory sb info */
+	sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
+	if (!sbi) {
+		ret = -ENOMEM;
+		goto out_brelse;
+	}
+
+	sbi->s_beamfs_sb = kzalloc(sizeof(*sbi->s_beamfs_sb), GFP_KERNEL);
+	if (!sbi->s_beamfs_sb) {
+		ret = -ENOMEM;
+		goto out_free_sbi;
+	}
+
+	memcpy(sbi->s_beamfs_sb, fsb, sizeof(*fsb));
+	sbi->s_sbh         = bh;
+	sbi->s_free_blocks = le64_to_cpu(fsb->s_free_blocks);
+	sbi->s_free_inodes = le64_to_cpu(fsb->s_free_inodes);
+	spin_lock_init(&sbi->s_lock);
+	INIT_LIST_HEAD(&sbi->s_rsv_windows);
+	mmb_init(&sbi->s_shared_bhs, sb->s_bdev->bd_mapping);
+
+	/*
+	 * The paths that can hold a scratch page at the same time:
+	 * readdir, lookup, add_dirent, del_dirent, dir_is_empty, the
+	 * sweep, the block decoder, two in writeback -- and three more
+	 * in beamfs_ind_parity_verify, which decodes a region block into
+	 * scratch before it can read one slot out of it.
+	 *
+	 * Twelve was not enough under read pressure. mempool_alloc with
+	 * GFP_NOFS waits when the pool is empty, and a reader that holds
+	 * two while waiting for a third is waiting on the readers that
+	 * hold the rest. generic/464 walked into it: three sshd-session
+	 * tasks stalled 191 seconds in __bread_gfp under
+	 * beamfs_ind_parity_verify, the machine at 330% CPU with 16 MiB
+	 * free and 7.6 GiB of page cache GFP_NOFS was not allowed to
+	 * reclaim, and it never came back.
+	 *
+	 * Thirty-two, which is 128 KiB a mount: enough that every path
+	 * can hold its three and still find one, and small enough not to
+	 * matter on the embedded profile this filesystem targets.
+	 */
+	sbi->s_scratch_pool = mempool_create_kmalloc_pool(32,
+							  BEAMFS_BLOCK_SIZE);
+	if (!sbi->s_scratch_pool) {
+		ret = -ENOMEM;
+		goto out_free_fsb;
+	}
+
+	/*
+	 * Staging for the superblock's RS encode, taken once.
+	 *
+	 * beamfs_dirty_super_now needs 2769 bytes to lay the superblock
+	 * out for the encoder, and beamfs_free_block reaches it under
+	 * s_lock -- a spinlock. Allocating there is what
+	 * DEBUG_ATOMIC_SLEEP reports as "sleeping function called from
+	 * invalid context"; allocating here costs 2769 bytes a mount and
+	 * nothing else.
+	 */
+	sbi->s_sb_rs_staging = kvmalloc(BEAMFS_SB_RS_STAGING_BYTES,
+					GFP_KERNEL);
+	if (!sbi->s_sb_rs_staging) {
+		ret = -ENOMEM;
+		goto out_free_fsb;
+	}
+	sbi->s_wb_pages = mempool_create_page_pool(64, 0);
+	if (!sbi->s_wb_pages) {
+		ret = -ENOMEM;
+		goto out_free_fsb;
+	}
+
+	sb->s_fs_info  = sbi;
+	sb->s_magic    = BEAMFS_MAGIC;
+	sb->s_op       = &beamfs_super_ops;
+	/*
+	 * What the indirection can actually address, not what the type
+	 * could hold. See BEAMFS_MAX_FILE_SIZE: overstating this makes
+	 * the VFS accept sizes the filesystem cannot reach and defers the
+	 * failure to a read that has no good way to explain itself.
+	 */
+	sb->s_maxbytes = (loff_t)BEAMFS_MAX_FILE_SIZE;
+
+	/*
+	 * i_nlink is 16 bits in struct beamfs_inode. Without this the VFS
+	 * never refuses a link and the count is cut when the inode is
+	 * written; with it link(2), mkdir(2) and rename(2) fail with
+	 * EMLINK first.
+	 */
+	sb->s_max_links = U16_MAX;
+
+	/* Read root inode (inode 1) */
+	root_inode = beamfs_iget(sb, 1);
+	if (IS_ERR(root_inode)) {
+		ret = PTR_ERR(root_inode);
+		pr_err("beamfs: failed to read root inode: %d\n", ret);
+		goto out_free_fsb;
+	}
+
+	sb->s_root = d_make_root(root_inode);
+	if (!sb->s_root) {
+		ret = -ENOMEM;
+		goto out_free_fsb;
+	}
+
+	/* Replay any SB RS recovery events captured before sbi was ready.
+	 * These are journalled before bitmap setup so the forensic order
+	 * (SB events first, then bitmap events) reflects mount sequence.
+	 */
+	if (pending) {
+		unsigned int i;
+
+		for (i = 0; i < n_pending; i++) {
+			beamfs_log_rs_event(sb,
+					    pending[i].block_no,
+					   pending[i].positions,
+					   pending[i].n_positions,
+					   pending[i].code_len_bytes);
+		}
+		kfree(pending);
+		pending = NULL;
+	}
+
+	if (beamfs_setup_bitmap(sb)) {
+		ret = -ENOMEM;
+		goto out_put_root;
+	}
+
+	sbi->s_scheme = le32_to_cpu(fsb->s_data_protection_scheme);
+	/*
+	 * The legacy iomap path (file.c) installs indirect pointers
+	 * without maintaining their parity slot and reads them without
+	 * verifying it, while mkfs lays out the parity region for every
+	 * scheme. A volume formatted with it carries indirect blocks no
+	 * parity describes, by construction. Refused rather than mounted
+	 * unprotected; mkfs.beamfs refuses to format it since the same day.
+	 */
+	if (sbi->s_scheme == BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL) {
+		errorf(fc,
+		       "beamfs: scheme INODE_UNIVERSAL (5) keeps no parity for its indirect blocks; not mountable");
+		ret = -EINVAL;
+		goto out_free_fsb;
+	}
+
+	/*
+	 * Indirection parity. Zero on volumes formatted without it, which
+	 * is what turns every call in indparity.c into a no-op -- the
+	 * feature costs nothing on a volume that did not ask for it.
+	 */
+	sbi->s_ind_parity_blk  = le64_to_cpu(fsb->s_ind_parity_blk);
+	sbi->s_ind_parity_len  = le32_to_cpu(fsb->s_ind_parity_len);
+	sbi->s_ind_parity_mode = le32_to_cpu(fsb->s_ind_parity_mode);
+	sbi->s_sb = sb;
+	sbi->s_budget_blk = le64_to_cpu(fsb->s_budget_blk);
+	sbi->s_budget_len = le32_to_cpu(fsb->s_budget_len);
+	if (sbi->s_ind_parity_mode >= BEAMFS_IND_PARITY__MAX) {
+		errorf(fc, "beamfs: unknown indirect parity mode %u",
+		       sbi->s_ind_parity_mode);
+		ret = -EINVAL;
+		goto out_free_fsb;
+	}
+	sbi->s_feat_incompat = le64_to_cpu(fsb->s_feat_incompat);
+	sbi->s_data_csum = !!(le64_to_cpu(fsb->s_feat_ro_compat) &
+			      BEAMFS_FEATURE_RO_COMPAT_DATA_CSUM);
+	sbi->s_data_selfid = !!(le64_to_cpu(fsb->s_feat_ro_compat) &
+				BEAMFS_FEATURE_RO_COMPAT_DATA_SELFID);
+
+	pr_info("beamfs: mounted v%u (blocks=%llu free=%lu inodes=%llu scheme=%u feat=0x%016llx/0x%016llx/0x%016llx)\n",
+		le32_to_cpu(fsb->s_version),
+		le64_to_cpu(fsb->s_block_count),
+		sbi->s_free_blocks,
+		le64_to_cpu(fsb->s_inode_count),
+		le32_to_cpu(fsb->s_data_protection_scheme),
+		le64_to_cpu(fsb->s_feat_compat),
+		le64_to_cpu(fsb->s_feat_incompat),
+		le64_to_cpu(fsb->s_feat_ro_compat));
+
+	/*
+	 * Anchor before the scrubber starts: the first sweep may journal
+	 * an event, and an entry with no anchor to convert against is
+	 * ordering information wearing a timestamp's clothes.
+	 */
+	beamfs_clock_anchor(sb);
+
+	/*
+	 * The state the last mount left, and ours.
+	 *
+	 * ext2 has done this since the beginning: a flag set while mounted
+	 * and cleared by a clean unmount tells the next mount, and fsck,
+	 * whether the volume was ever put down properly; a second flag
+	 * says the kernel saw damage. beamfs keeps the same two,
+	 * BEAMFS_SB_FLAG_MOUNTED and BEAMFS_SB_FLAG_ERRORS; without them
+	 * nothing could tell a volume that needed checking from one that
+	 * did not.
+	 */
+	{
+		u32 fl = le32_to_cpu(sbi->s_beamfs_sb->s_flags);
+
+		if (fl & BEAMFS_SB_FLAG_MOUNTED)
+			pr_warn("beamfs: %s was not cleanly unmounted; running fsck.beamfs is recommended\n",
+				sb->s_id);
+		if (fl & BEAMFS_SB_FLAG_ERRORS)
+			pr_warn("beamfs: %s recorded errors during a previous mount; running fsck.beamfs is recommended\n",
+				sb->s_id);
+		sbi->s_unclean = !!(fl & (BEAMFS_SB_FLAG_MOUNTED | BEAMFS_SB_FLAG_ERRORS));
+		if (!sb_rdonly(sb)) {
+			sbi->s_beamfs_sb->s_flags = cpu_to_le32(fl | BEAMFS_SB_FLAG_MOUNTED);
+			beamfs_dirty_super_now(sbi);
+			if (sbi->s_sbh && sync_dirty_buffer(sbi->s_sbh))
+				pr_warn("beamfs: %s: the mounted flag did not reach the medium\n",
+					sb->s_id);
+		}
+	}
+
+	beamfs_tc_init(sbi);
+
+	/*
+	 * Start the scrubber last, once the volume is fully usable: it
+	 * reads blocks and journals what it finds, so it has no business
+	 * running against a superblock still being assembled.
+	 *
+	 * A failure to start is not a failure to mount. The filesystem
+	 * still corrects on read; what is lost is the sweep that would
+	 * have found the drift before a reader did. Refusing the mount
+	 * over it would trade a degraded guarantee for no filesystem at
+	 * all, which is the worse outcome on a device that has no second
+	 * copy.
+	 */
+	ret = beamfs_scrub_init(sb);
+	if (ret) {
+		pr_warn("beamfs: scrubber did not start (%d); correction on read is unaffected\n",
+			ret);
+	}
+
+	return 0;
+
+out_put_root:
+	dput(sb->s_root);
+	sb->s_root = NULL;
+out_free_fsb:
+	kfree(sbi->s_beamfs_sb);
+out_free_sbi:
+	/*
+	 * Everything fill_super allocated before it could fail here, the
+	 * scratch pool and the RS staging included: a mount that fails
+	 * past them must not leave them behind.
+	 */
+	mempool_destroy(sbi->s_wb_pages);
+	mempool_destroy(sbi->s_scratch_pool);
+	kvfree(sbi->s_sb_rs_staging);
+	kfree(sbi);
+	sb->s_fs_info = NULL;
+out_brelse:
+	/* Free any pending events buffer that survived to here. The replay
+	 * block sets pending = NULL after consumption, so kfree(NULL) is the
+	 * nominal no-op. Reaching here with pending != NULL means a failure
+	 * occurred between capture and replay; the events are dropped.
+	 */
+	kfree(pending);
+	brelse(bh);
+	return ret;
+}
+
+/*
+ * fs_context ops
+ */
+static int beamfs_get_tree(struct fs_context *fc)
+{
+	return get_tree_bdev(fc, beamfs_fill_super);
+}
+
+/*
+ * beamfs_reconfigure - handle mount -o remount
+ *
+ * The flush is the whole job. Accepting the request and doing nothing
+ * left every dirty inode in memory while the blocks they name were
+ * already marked used on disk, so a remount,ro produced a filesystem
+ * holding allocations nothing pointed at.
+ *
+ * generic/452 is the smallest possible demonstration: copy ls onto the
+ * scratch volume, remount read-only, and the checker finds block 18443
+ * -- data_start itself, the first block of the file -- marked used and
+ * unreferenced, with the inode still reading size=0 and no direct
+ * pointer. The pointer was written; it never reached the medium.
+ *
+ * xfstests remounts read-only after most tests before running its
+ * check, so this reached far past the one test that isolates it.
+ *
+ * sync_filesystem is what every other filesystem does here, ext2
+ * included, and the VFS does not do it for us: the ro/rw transition is
+ * the VFS's, the flush before it is ours.
+ *
+ * Then the state the next mount reads. A read-write mount sets
+ * BEAMFS_SB_FLAG_MOUNTED and a clean unmount clears it, but put_super
+ * writes nothing to a volume that is read-only by then: the way to
+ * read-only clears it here, after the flush, as ext2 marks its s_state
+ * valid there, or the next mount calls unclean a volume that was put
+ * down properly (generic/452). The way back sets it again, and is
+ * refused on a volume that has failed, or that carries ro_compat
+ * features this kernel does not know, which fill_super mounts
+ * read-only.
+ */
+static int beamfs_reconfigure(struct fs_context *fc)
+{
+	struct super_block *sb = fc->root->d_sb;
+	struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+	bool to_ro = fc->sb_flags & SB_RDONLY;
+	u32 fl;
+	int ret;
+
+	ret = sync_filesystem(sb);
+	if (ret)
+		return ret;
+	if (!(fc->sb_flags_mask & SB_RDONLY) || to_ro == sb_rdonly(sb))
+		return 0;
+
+	if (!to_ro) {
+		u64 unknown = le64_to_cpu(sbi->s_beamfs_sb->s_feat_ro_compat) &
+			      ~BEAMFS_FEAT_RO_COMPAT_SUPP;
+
+		if (unknown) {
+			errorf(fc,
+			       "beamfs: unsupported ro_compat features 0x%016llx, the volume stays read-only",
+			       unknown);
+			return -EROFS;
+		}
+		if (beamfs_failed(sb)) {
+			errorf(fc, "beamfs: the volume has failed and stays read-only");
+			return -EROFS;
+		}
+	} else if (beamfs_failed(sb)) {
+		/* Left marked in use: a failed volume wants fsck.beamfs. */
+		return 0;
+	}
+
+	spin_lock(&sbi->s_lock);
+	fl = le32_to_cpu(sbi->s_beamfs_sb->s_flags);
+	if (to_ro)
+		fl &= ~BEAMFS_SB_FLAG_MOUNTED;
+	else
+		fl |= BEAMFS_SB_FLAG_MOUNTED;
+	sbi->s_beamfs_sb->s_flags = cpu_to_le32(fl);
+	beamfs_dirty_super(sbi);
+	spin_unlock(&sbi->s_lock);
+
+	if (sbi->s_sbh && sync_dirty_buffer(sbi->s_sbh))
+		pr_warn("beamfs: %s: the mounted flag did not reach the medium\n",
+			sb->s_id);
+	return 0;
+}
+
+static const struct fs_context_operations beamfs_context_ops = {
+	.get_tree   = beamfs_get_tree,
+	.reconfigure = beamfs_reconfigure,
+};
+
+static int beamfs_init_fs_context(struct fs_context *fc)
+{
+	fc->ops = &beamfs_context_ops;
+	return 0;
+}
+
+/*
+ * Stop the scrubber before the rest of the unmount.
+ *
+ * The sweep holds a reference on the inode it walks, and put_super,
+ * where it used to be stopped, runs after generic_shutdown_super has
+ * evicted the volume's inodes: an inode held then is busy after
+ * unmount, and its iput later touches a superblock that is gone. Only
+ * a volume whose fill_super got as far as the root has a scrubber.
+ */
+static void beamfs_kill_sb(struct super_block *sb)
+{
+	if (sb->s_root)
+		beamfs_scrub_stop(sb);
+	kill_block_super(sb);
+}
+
+static struct file_system_type beamfs_fs_type = {
+	.owner            = THIS_MODULE,
+	.name             = "beamfs",
+	.init_fs_context  = beamfs_init_fs_context,
+	.kill_sb          = beamfs_kill_sb,
+	.fs_flags         = FS_REQUIRES_DEV,
+};
+
+/*
+ * Inode cache constructor
+ */
+static void beamfs_inode_init_once(void *obj)
+{
+	struct beamfs_inode_info *fi = obj;
+
+	inode_init_once(&fi->vfs_inode);
+}
+
+/*
+ * Module init / exit
+ */
+static int __init beamfs_init(void)
+{
+	int ret;
+
+	/* Verify on-disk structure sizes at compile time */
+	BUILD_BUG_ON(sizeof(struct beamfs_super_block) != BEAMFS_BLOCK_SIZE);
+	BUILD_BUG_ON(sizeof(struct beamfs_inode) != 256);
+	BUILD_BUG_ON(sizeof(struct beamfs_rs_event) != 40);
+	BUILD_BUG_ON(sizeof(struct beamfs_dir_entry) != 268);
+
+	/* Initialize GF(2^8) tables for RS FEC - once, before any mount */
+	ret = beamfs_rs_init_tables();
+	if (ret)
+		return ret;
+
+	beamfs_inode_cachep =
+		kmem_cache_create("beamfs_inode_cache",
+				  sizeof(struct beamfs_inode_info),
+				  0,
+				  SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
+				  beamfs_inode_init_once);
+
+	if (!beamfs_inode_cachep) {
+		pr_err("beamfs: failed to create inode cache\n");
+		ret = -ENOMEM;
+		goto out_rs;
+	}
+
+	ret = register_filesystem(&beamfs_fs_type);
+	if (ret) {
+		pr_err("beamfs: failed to register filesystem: %d\n", ret);
+		goto out_cache;
+	}
+
+	/*
+	 * Last, and its failure is not fatal: debugfs may be absent from
+	 * the build or the mount, and a filesystem that refuses to load
+	 * because a benchmark has nowhere to publish itself would be
+	 * trading a working module for a measurement.
+	 */
+	beamfs_debugfs_init();
+
+	pr_info("beamfs: module loaded (beamfs - resilient filesystem)\n");
+	return 0;
+
+out_cache:
+	kmem_cache_destroy(beamfs_inode_cachep);
+out_rs:
+	beamfs_rs_exit_tables();
+	return ret;
+}
+
+static void __exit beamfs_exit(void)
+{
+	/* One page per cpu, held for the life of the module. */
+	beamfs_ind_parity_cache_free();
+
+	/* Before the codec tables go, since a reader could be in a run. */
+	beamfs_debugfs_exit();
+	unregister_filesystem(&beamfs_fs_type);
+	rcu_barrier();
+	kmem_cache_destroy(beamfs_inode_cachep);
+	beamfs_rs_exit_tables();
+	pr_info("beamfs: module unloaded\n");
+}
+
+module_init(beamfs_init);
+module_exit(beamfs_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Aurelien DESBRIERES <aurelien@hackers.camp>");
+MODULE_DESCRIPTION("beamfs - resilient filesystem");
+MODULE_VERSION("0.1.26");
+MODULE_ALIAS_FS("beamfs");
+MODULE_SOFTDEP("pre: reed_solomon");
-- 
2.55.0


  parent reply	other threads:[~2026-10-19  4:17 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19  4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 01/18] beamfs: add on-disk format definitions Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 02/18] beamfs: add in-core header and tracepoints Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 03/18] beamfs: add CRC32 and Reed-Solomon layer Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 04/18] beamfs: add error budget, alerts and clock Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 05/18] beamfs: add indirect parity, tree checker Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 06/18] beamfs: add block and inode allocator Aurelien DESBRIERES
2026-10-19  4:17 ` Aurelien DESBRIERES [this message]
2026-10-19  4:17 ` [RFC PATCH 08/18] beamfs: add inode and directory operations Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 09/18] beamfs: add namespace operations Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 10/18] beamfs: add iomap file operations Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 11/18] beamfs: inline data path 1/4, block map Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 12/18] beamfs: inline data path 2/4, allocation Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 13/18] beamfs: inline data path 3/4, read side Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 14/18] beamfs: inline data path 4/4, write side Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 15/18] beamfs: add scrubber and RS benchmark Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 16/18] Documentation: filesystems: add beamfs Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 17/18] beamfs: add Kconfig and Makefile Aurelien DESBRIERES
2026-10-19  4:17 ` [RFC PATCH 18/18] MAINTAINERS: add beamfs Aurelien DESBRIERES

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261019041728.327785-8-aurelien@hackers.camp \
    --to=aurelien@hackers.camp \
    --cc=aurelien.desbrieres@gmail.com \
    --cc=brauner@kernel.org \
    --cc=corbet@lwn.net \
    --cc=jack@suse.cz \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=rdunlap@infradead.org \
    --cc=skhan@linuxfoundation.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox