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 06/18] beamfs: add block and inode allocator
Date: Mon, 19 Oct 2026 06:17:16 +0200 [thread overview]
Message-ID: <20261019041728.327785-7-aurelien@hackers.camp> (raw)
In-Reply-To: <20261019041728.327785-1-aurelien@hackers.camp>
Block and inode allocation over in-memory bitmaps loaded at mount
time. No I/O is done under the allocator's spinlock.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/alloc.c | 1099 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1099 insertions(+)
create mode 100644 fs/beamfs/alloc.c
diff --git a/fs/beamfs/alloc.c b/fs/beamfs/alloc.c
new file mode 100644
index 000000000000..96e7ce8e1b4a
--- /dev/null
+++ b/fs/beamfs/alloc.c
@@ -0,0 +1,1099 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Block and inode allocator
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Both block and inode allocators use in-memory bitmaps built at mount
+ * time. No I/O is performed under the spinlock.
+ *
+ * Layout: the superblock is block 0 and records where each region
+ * starts -- the inode table (s_inode_table_blk), the block bitmap
+ * (s_bitmap_blk), the indirect-parity and error-budget regions when the
+ * volume has them, and the data area (s_data_start_blk). The block
+ * bitmap covers the data area only, one bit per block from
+ * s_data_start; blocks below it are never allocated or freed here.
+ *
+ * Bitmap convention: bit set (1) = free, bit clear (0) = used.
+ *
+ * The block bitmap is kept on disk: s_bitmap_blocks_count blocks from
+ * s_bitmap_blk, each holding BEAMFS_BITMAP_SUBBLOCKS (16) RS(255,239)
+ * codewords of 239 data bytes, 30592 bits per block. It is decoded at
+ * mount, a block that needed correction is written back at once, and
+ * every later change re-encodes the one codeword holding the changed
+ * bit. The inode bitmap has no on-disk form: it is rebuilt at mount
+ * from the inode table, where a zero i_mode marks a free slot.
+ *
+ * The free counts, s_free_blocks and s_free_inodes, are taken from the
+ * superblock at mount rather than recounted from the bitmaps, so a
+ * power loss between a bitmap write and the superblock write can leave
+ * them out of step with the bitmaps.
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/bitmap.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+#include "beamfs.h"
+#include "beamfs_trace.h"
+
+/* ------------------------------------------------------------------ */
+/* Block bitmap */
+/* ------------------------------------------------------------------ */
+
+/*
+ * beamfs_setup_bitmap - allocate and initialize in-memory bitmaps
+ *
+ * Called from beamfs_fill_super() after the superblock is read.
+ *
+ * Block bitmap: loaded from the s_bitmap_blocks_count on-disk bitmap
+ * blocks starting at s_bitmap_blk. Each 239-byte subblock is
+ * RS(255,239) FEC-protected. If a subblock is corrected, the event is
+ * logged to the RS journal and the corrected bitmap block is written
+ * back immediately.
+ *
+ * Inode bitmap: reconstructed by scanning the inode table for free slots
+ * (i_mode == 0). This is O(total_inodes) but only at mount.
+ */
+int beamfs_setup_bitmap(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned long total_blocks;
+ unsigned long data_start;
+ unsigned long total_inodes;
+ unsigned long inode_table_blk;
+ unsigned long inodes_per_block;
+ unsigned long block, i;
+ struct buffer_head *bh;
+ struct beamfs_inode *raw;
+ u64 bitmap_blk;
+ u8 *bdata;
+ bool corrected = false;
+
+ /* --- Block bitmap --- */
+ total_blocks = le64_to_cpu(sbi->s_beamfs_sb->s_block_count);
+ data_start = le64_to_cpu(sbi->s_beamfs_sb->s_data_start_blk);
+ bitmap_blk = le64_to_cpu(sbi->s_beamfs_sb->s_bitmap_blk);
+
+ if (total_blocks <= data_start) {
+ pr_err("beamfs: invalid block layout (total=%lu data_start=%lu)\n",
+ total_blocks, data_start);
+ return -EINVAL;
+ }
+
+ if (bitmap_blk == 0 || bitmap_blk >= data_start) {
+ pr_err("beamfs: invalid bitmap block %llu\n", bitmap_blk);
+ return -EINVAL;
+ }
+
+ sbi->s_nblocks = total_blocks - data_start;
+ sbi->s_data_start = data_start;
+
+ /*
+ * kvmalloc rather than bitmap_zalloc: one bit per data block means
+ * a 931 GiB volume asks for 29 MiB in one piece, and the page
+ * allocator has no run of contiguous pages that long once the
+ * machine has been up a while. The mount failed with ENOMEM on a
+ * VM with 1.7 GiB free -- not for want of memory, but for want of
+ * it in one stretch.
+ *
+ * vmalloc backing costs an extra page-table walk per access, which
+ * for a bitmap consulted under a spinlock is not measurable
+ * against the block I/O it guards. ext4 makes the same trade for
+ * its larger structures.
+ */
+ sbi->s_block_bitmap = kvzalloc_objs(*sbi->s_block_bitmap,
+ BITS_TO_LONGS(sbi->s_nblocks),
+ GFP_KERNEL);
+ if (!sbi->s_block_bitmap)
+ return -ENOMEM;
+
+ /*
+ * Multi-block bitmap layout: K = bitmap_blocks_count contiguous
+ * blocks starting at bitmap_blk. Each block protects 30592 data
+ * block bits via RS over BEAMFS_BITMAP_SUBBLOCKS (16) sub-blocks.
+ * K is decoded from s_flags bits 0..15; 0 means legacy 1.
+ */
+ sbi->s_bitmap_blocks_count =
+ beamfs_bitmap_blocks_count_from_flags(sbi->s_beamfs_sb->s_flags);
+
+ sbi->s_bitmap_needs_encode =
+ bitmap_zalloc(sbi->s_bitmap_blocks_count, GFP_KERNEL);
+ if (!sbi->s_bitmap_needs_encode)
+ return -ENOMEM;
+
+ sbi->s_bitmap_blkhs = kcalloc(sbi->s_bitmap_blocks_count,
+ sizeof(*sbi->s_bitmap_blkhs), GFP_KERNEL);
+ if (!sbi->s_bitmap_blkhs) {
+ kvfree(sbi->s_block_bitmap);
+ sbi->s_block_bitmap = NULL;
+ return -ENOMEM;
+ }
+
+ {
+ u32 k;
+ unsigned long bit_global = 0;
+ unsigned long max_bit = sbi->s_nblocks;
+
+ for (k = 0;
+ k < sbi->s_bitmap_blocks_count && bit_global < max_bit;
+ k++) {
+ u64 disk_blk = bitmap_blk + (u64)k;
+ int rs_results[BEAMFS_BITMAP_SUBBLOCKS];
+ u8 rs_positions[BEAMFS_BITMAP_SUBBLOCKS *
+ (BEAMFS_RS_PARITY / 2)];
+ unsigned long bit_local;
+
+ bh = beamfs_bread(sb, disk_blk, "bitmap");
+ if (!bh) {
+ pr_err("beamfs: cannot read bitmap blk %llu (k=%u)\n",
+ disk_blk, k);
+ while (k > 0) {
+ k--;
+ brelse(sbi->s_bitmap_blkhs[k]);
+ sbi->s_bitmap_blkhs[k] = NULL;
+ }
+ kfree(sbi->s_bitmap_blkhs);
+ sbi->s_bitmap_blkhs = NULL;
+ bitmap_free(sbi->s_bitmap_needs_encode);
+ sbi->s_bitmap_needs_encode = NULL;
+ kvfree(sbi->s_block_bitmap);
+ sbi->s_block_bitmap = NULL;
+ return -EIO;
+ }
+ sbi->s_bitmap_blkhs[k] = bh;
+ bdata = (u8 *)bh->b_data;
+
+ beamfs_rs_decode_region(bdata, BEAMFS_SUBBLOCK_TOTAL,
+ bdata + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_BITMAP_SUBBLOCKS,
+ rs_results, rs_positions,
+ BEAMFS_RS_PARITY / 2,
+ "bitmap");
+
+ for (i = 0; i < BEAMFS_BITMAP_SUBBLOCKS; i++) {
+ int rc = rs_results[i];
+
+ if (rc < 0) {
+ pr_err("beamfs: bmap blk %u sub %lu uncor\n",
+ k, i);
+ } else if (rc > 0) {
+ unsigned int np = (unsigned int)rc;
+ u8 *pos = rs_positions +
+ i * (BEAMFS_RS_PARITY / 2);
+
+ if (np > BEAMFS_RS_PARITY / 2)
+ np = BEAMFS_RS_PARITY / 2;
+ pr_warn("beamfs: bmap blk %u sub %lu: %d corrected\n",
+ k, i, rc);
+ beamfs_log_rs_event_flagged(sb,
+ disk_blk,
+ pos, np,
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ corrected = true;
+ }
+ }
+
+ bit_local = 0;
+ for (i = 0;
+ i < BEAMFS_BITMAP_SUBBLOCKS &&
+ bit_local < BEAMFS_BITS_PER_BITMAP_BLOCK &&
+ bit_global < max_bit; i++) {
+ u8 *subdata = bdata + i * BEAMFS_SUBBLOCK_TOTAL;
+ unsigned long b;
+
+ for (b = 0;
+ b < BEAMFS_SUBBLOCK_DATA * 8 &&
+ bit_global < max_bit;
+ b++, bit_local++, bit_global++) {
+ if (subdata[b / 8] & (1u << (b % 8)))
+ set_bit(bit_global,
+ sbi->s_block_bitmap);
+ else
+ clear_bit(bit_global,
+ sbi->s_block_bitmap);
+ }
+ }
+
+ if (corrected) {
+ mark_buffer_dirty(bh);
+ /*
+ * A repair that does not land is a bitmap
+ * that decodes the same way on the next
+ * mount, and the same correction again.
+ */
+ if (sync_dirty_buffer(bh))
+ pr_err("beamfs: bitmap block %u repair did not reach the medium\n",
+ k);
+ corrected = false;
+ }
+ }
+ }
+
+ /* --- Inode bitmap --- */
+ total_inodes = le64_to_cpu(sbi->s_beamfs_sb->s_inode_count);
+ inode_table_blk = le64_to_cpu(sbi->s_beamfs_sb->s_inode_table_blk);
+ inodes_per_block = BEAMFS_BLOCK_SIZE / sizeof(struct beamfs_inode);
+
+ /*
+ * Start at the beginning: at mount the layout is whatever the
+ * previous session left, and there is no better guess.
+ */
+ sbi->s_alloc_goal = 0;
+
+ sbi->s_ninodes = total_inodes;
+
+ sbi->s_inode_bitmap = kvzalloc_objs(*sbi->s_inode_bitmap,
+ BITS_TO_LONGS(total_inodes + 1),
+ GFP_KERNEL);
+ if (!sbi->s_inode_bitmap) {
+ u32 k;
+
+ for (k = 0; k < sbi->s_bitmap_blocks_count; k++) {
+ if (sbi->s_bitmap_blkhs[k])
+ brelse(sbi->s_bitmap_blkhs[k]);
+ }
+ kfree(sbi->s_bitmap_blkhs);
+ sbi->s_bitmap_blkhs = NULL;
+ bitmap_free(sbi->s_bitmap_needs_encode);
+ sbi->s_bitmap_needs_encode = NULL;
+ kvfree(sbi->s_block_bitmap);
+ sbi->s_block_bitmap = NULL;
+ return -ENOMEM;
+ }
+
+ for (block = 0; block * inodes_per_block < total_inodes; block++) {
+ bh = beamfs_bread(sb, inode_table_blk + block, "bitmap");
+ if (!bh) {
+ pr_warn("beamfs: cannot read inode table block %lu at mount\n",
+ inode_table_blk + block);
+ continue;
+ }
+
+ raw = (struct beamfs_inode *)bh->b_data;
+
+ for (i = 0; i < inodes_per_block; i++) {
+ unsigned long ino = block * inodes_per_block + i + 1;
+
+ if (ino > total_inodes)
+ break;
+ if (ino == 1)
+ continue;
+ if (le16_to_cpu(raw[i].i_mode) == 0)
+ set_bit(ino, sbi->s_inode_bitmap);
+ }
+ brelse(bh);
+ }
+
+ pr_info("beamfs: bitmaps initialized (%lu data blocks, %lu free; "
+ "%lu inodes, %lu free)\n",
+ sbi->s_nblocks, sbi->s_free_blocks,
+ total_inodes, sbi->s_free_inodes);
+
+ return 0;
+}
+
+/*
+ * Rebuild one subblock of a bitmap block, or all of them.
+ *
+ * @sub: which codeword to rebuild, or BEAMFS_BITMAP_SUBBLOCKS for all.
+ * The buffer lock must be held.
+ *
+ * The whole-block version cost 30592 test_bit calls and sixteen RS
+ * encodes for one bit changed. Filling 256 MiB means 70197 allocations,
+ * so generic/015 spent two billion test_bit calls and 1.1 million RS
+ * encodes in the allocator -- twenty minutes on a test that should take
+ * one, with the flusher pinned at 92% of a core.
+ *
+ * Two things were wasteful and neither had to be. A bit belongs to
+ * exactly one of the sixteen codewords, so changing it invalidates one
+ * parity block, not sixteen. And the in-memory bitmap is already a bit
+ * array in the same order as the on-disk one: memcpy moves it, where
+ * the old loop asked about every bit individually.
+ *
+ * Per allocation this becomes one memcpy of 239 bytes and one RS
+ * encode. The full-block path stays for beamfs_bitmap_encode_pending,
+ * which rebuilds a whole block.
+ */
+static void beamfs_bitmap_encode_sub_locked(struct beamfs_sb_info *sbi,
+ u32 k, u32 sub,
+ struct buffer_head *bh)
+{
+ unsigned long first, last;
+ u8 *bdata = (u8 *)bh->b_data;
+ u32 i, lo, hi;
+
+ first = (unsigned long)k * BEAMFS_BITS_PER_BITMAP_BLOCK;
+ last = min(first + BEAMFS_BITS_PER_BITMAP_BLOCK,
+ (unsigned long)sbi->s_nblocks);
+
+ if (sub >= BEAMFS_BITMAP_SUBBLOCKS) {
+ lo = 0;
+ hi = BEAMFS_BITMAP_SUBBLOCKS;
+ memset(bdata, 0, BEAMFS_BLOCK_SIZE);
+ } else {
+ lo = sub;
+ hi = sub + 1;
+ }
+
+ for (i = lo; i < hi; i++) {
+ u8 *subdata = bdata + (size_t)i * BEAMFS_SUBBLOCK_TOTAL;
+ unsigned long bit0 = first +
+ (unsigned long)i * BEAMFS_SUBBLOCK_DATA * 8;
+ size_t nbytes;
+
+ if (bit0 >= last) {
+ memset(subdata, 0, BEAMFS_SUBBLOCK_DATA);
+ continue;
+ }
+
+ /*
+ * Bit b of the volume lives at bit b - bit0 of this
+ * codeword, and bit0 is a multiple of 1912 -- which is a
+ * multiple of 8, so the copy is byte-aligned and memcpy
+ * does it.
+ */
+ nbytes = min_t(size_t, BEAMFS_SUBBLOCK_DATA,
+ DIV_ROUND_UP(last - bit0, 8));
+ memcpy(subdata, (const u8 *)sbi->s_block_bitmap + bit0 / 8,
+ nbytes);
+ if (nbytes < BEAMFS_SUBBLOCK_DATA)
+ memset(subdata + nbytes, 0,
+ BEAMFS_SUBBLOCK_DATA - nbytes);
+
+ {
+ int _e = beamfs_rs_encode_region(subdata,
+ BEAMFS_SUBBLOCK_TOTAL,
+ subdata + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA, 1);
+
+ /*
+ * An encode that fails leaves a block whose
+ * parity describes nothing, written with every
+ * check passing.
+ */
+ if (_e < 0)
+ pr_err_ratelimited("beamfs: bitmap subblock encode failed: %d\n",
+ _e);
+ }
+ }
+}
+
+/*
+ * Rebuild one bitmap block's on-disk image, all sixteen codewords. The
+ * buffer lock must be held.
+ */
+static void beamfs_bitmap_encode_one_locked(struct beamfs_sb_info *sbi, u32 k,
+ struct buffer_head *bh)
+{
+ beamfs_bitmap_encode_sub_locked(sbi, k, BEAMFS_BITMAP_SUBBLOCKS, bh);
+}
+
+/*
+ * Rebuild the on-disk image of every bitmap block flagged in
+ * s_bitmap_needs_encode, each under its buffer lock.
+ *
+ * Called from sync_fs and put_super, before the bitmap buffers are
+ * written. beamfs_write_bitmap_block rebuilds the codeword it changes
+ * as it dirties the buffer, and clears the flag; see there for why that
+ * rebuild cannot wait for sync. s_block_bitmap is the authority and is
+ * copied without s_lock; the buffer lock serialises against a
+ * concurrent rebuild of the same block.
+ */
+void beamfs_bitmap_encode_pending(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u32 k;
+
+ if (!sbi || !sbi->s_bitmap_blkhs || !sbi->s_bitmap_needs_encode ||
+ !sbi->s_block_bitmap)
+ return;
+
+ for (k = 0; k < sbi->s_bitmap_blocks_count; k++) {
+ struct buffer_head *bh;
+
+ if (!test_and_clear_bit(k, sbi->s_bitmap_needs_encode))
+ continue;
+
+ bh = sbi->s_bitmap_blkhs[k];
+ if (!bh)
+ continue;
+
+ lock_buffer(bh);
+ beamfs_bitmap_encode_one_locked(sbi, k, bh);
+ unlock_buffer(bh);
+ }
+}
+
+/*
+ * beamfs_write_bitmap_block - bring the on-disk image of the bitmap
+ * block holding @bit_global up to date,
+ * and mark it dirty.
+ *
+ * @bit_global is the bit, counted from s_data_start, that the caller
+ * has just changed in sbi->s_block_bitmap. Each subblock of a bitmap
+ * block is an RS codeword of its own, so only the one holding that bit
+ * is re-encoded. With @owner, the buffer also goes on the owner's
+ * metadata list (see below).
+ *
+ * Called without s_lock: the buffer lock taken here can sleep.
+ * Returns 0, -EIO when the volume has failed or the buffer's last
+ * write failed, or -EINVAL when there is no bitmap block for
+ * @bit_global.
+ */
+int beamfs_write_bitmap_block(struct super_block *sb,
+ unsigned long bit_global,
+ struct inode *owner)
+{
+ if (beamfs_failed(sb))
+ return -EIO;
+
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct buffer_head *bh;
+ u32 k;
+
+ if (!sbi->s_bitmap_blkhs || !sbi->s_block_bitmap)
+ return -EINVAL;
+ if (bit_global >= sbi->s_nblocks)
+ return -EINVAL;
+
+ k = (u32)(bit_global / BEAMFS_BITS_PER_BITMAP_BLOCK);
+ if (k >= sbi->s_bitmap_blocks_count)
+ return -EINVAL;
+
+ bh = sbi->s_bitmap_blkhs[k];
+ if (!bh)
+ return -EINVAL;
+
+ /*
+ * Rebuild the block's on-disk image now, under the lock.
+ *
+ * A deferred version of this lived here for a day: mark the block
+ * as needing a rebuild, do it in sync_fs, one rebuild per block
+ * touched instead of one per bit. It measured 25% faster on
+ * writes and a factor of four on deletes, and it was wrong.
+ *
+ * The needs_encode flag and the buffer's dirty bit are two states
+ * and the kernel knows only the second. Between mark_buffer_dirty
+ * below and sync_fs getting round to the rebuild, the flusher can
+ * write the buffer with its old contents and clear dirty; sync_fs
+ * then rebuilds an image nobody will ever write. fsck found
+ * 122368 blocks still marked used after 400 MiB was written and
+ * deleted, doubling with every cycle, while df reported the
+ * volume empty. The bitmap in memory was right and the one on
+ * disk was a lie.
+ *
+ * One codeword rebuilt -- a 239-byte copy and one RS encode --
+ * on every allocation and every free. That is what correctness
+ * costs here. Bringing it down means rebuilding less per change,
+ * not letting the buffer reach the disk stale.
+ */
+ lock_buffer(bh);
+ /*
+ * Not over a write that failed.
+ *
+ * The bitmap blocks are read at mount into buffers held until
+ * unmount, and the module never reads them again. A write of one
+ * that fails leaves its buffer not uptodate (end_buffer_write_sync
+ * clears the bit on an error): the medium holds an older bitmap
+ * than memory does. Dirtying the buffer anyway is what
+ * mark_buffer_dirty warns about. The superblock, the other buffer
+ * held for the mount, is treated this way in
+ * beamfs_dirty_super_now: the volume is failed and refuses further
+ * writes.
+ *
+ * generic/361 on aarch64, kernel 7.3-rc5, beamfs 0.1.23, on
+ * 2026-10-01: writes to loop0, the device under the volume,
+ * failed ("lost sync page write"), and within a millisecond came
+ * the WARNING at fs/buffer.c:991 from here, called by
+ * beamfs_alloc_block.
+ *
+ * Tested under the buffer lock: a write in flight holds it until
+ * its completion has set or cleared uptodate.
+ */
+ if (!buffer_uptodate(bh)) {
+ unlock_buffer(bh);
+ beamfs_fail(sb, "write_bitmap_block", -EIO);
+ return -EIO;
+ }
+ /*
+ * Only the codeword this bit belongs to. The other fifteen are
+ * untouched and their parity still holds.
+ */
+ beamfs_bitmap_encode_sub_locked(sbi, k,
+ (u32)((bit_global % BEAMFS_BITS_PER_BITMAP_BLOCK) /
+ (BEAMFS_SUBBLOCK_DATA * 8)),
+ bh);
+ clear_bit(k, sbi->s_bitmap_needs_encode);
+ mark_buffer_dirty(bh);
+
+ /*
+ * On the volume's shared metadata list, never the owner's.
+ *
+ * The bitmap block is shared by every inode allocating from it.
+ * Attached to the owner's i_metadata_bhs it moved from one inode's
+ * list to the next, and mmb_sync has a window -- b_mmb NULL under
+ * one inode's lock -- where mmb_mark_buffer_dirty under another
+ * inode's lock relinks the same b_assoc_buffers: list_del
+ * corruption, then a soft lockup holding mmb->lock (dbench,
+ * generic/241, x86-01, 2026-10-15). One list, one lock, no move.
+ *
+ * The bitmap-before-inode ordering that prevents the boot-time
+ * double-free -- boot N persisting an inode pointer without the
+ * matching bitmap clear, boot N+1 mounting a bitmap that declares
+ * a block free while an inode still references it -- is kept by
+ * beamfs_write_inode, which syncs s_shared_bhs before writing the
+ * inode. owner is NULL on the mount path and on the free path from
+ * beamfs_free_data_blocks; the block is on the list either way.
+ */
+ mmb_mark_buffer_dirty(bh, &sbi->s_shared_bhs);
+ if (owner && !(inode_state_read_once(owner) & I_FREEING)) {
+ /*
+ * And the owner, or the list is never walked.
+ *
+ * The ordering above holds only if __writeback_single_inode
+ * runs, and the VFS runs it for an inode it believes is
+ * dirty. Attaching the bitmap block to an inode nothing
+ * marked leaves it in memory, which is the case this
+ * attachment exists to prevent.
+ */
+ mark_inode_dirty(owner);
+ }
+
+ unlock_buffer(bh);
+ return 0;
+}
+
+/*
+ * beamfs_destroy_bitmap - free in-memory bitmaps at umount
+ */
+void beamfs_destroy_bitmap(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (sbi->s_bitmap_blkhs) {
+ u32 k;
+
+ for (k = 0; k < sbi->s_bitmap_blocks_count; k++) {
+ if (sbi->s_bitmap_blkhs[k]) {
+ brelse(sbi->s_bitmap_blkhs[k]);
+ sbi->s_bitmap_blkhs[k] = NULL;
+ }
+ }
+ kfree(sbi->s_bitmap_blkhs);
+ sbi->s_bitmap_blkhs = NULL;
+ bitmap_free(sbi->s_bitmap_needs_encode);
+ sbi->s_bitmap_needs_encode = NULL;
+ }
+ if (sbi->s_block_bitmap) {
+ kvfree(sbi->s_block_bitmap);
+ sbi->s_block_bitmap = NULL;
+ }
+ if (sbi->s_inode_bitmap) {
+ kvfree(sbi->s_inode_bitmap);
+ sbi->s_inode_bitmap = NULL;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Block allocation */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Reservation windows, after ext2.
+ *
+ * One cursor for the whole volume, advanced by whoever allocates and
+ * pulled back by whoever frees, lays three files written at once down
+ * as one interleaving: block n to the first, n+1 to the second, n+2
+ * to the third. Measured under generic/074 on 2026-09-23, after the
+ * writeback learned to gather contiguous blocks into one bio:
+ * 1 054 631 bios of 1 069 059 still carried a single block, because no
+ * two consecutive blocks of any file were adjacent on the medium, and
+ * the device served those 4 KiB writes at 570 a second with the queue
+ * seventeen deep. ext2 ran the same test in 17 s on the same device.
+ *
+ * ext2's answer (fs/ext2/balloc.c, rsv_window): a writer's goal is the
+ * block after the last one it got, and each writer holds a window of
+ * blocks, in memory only, where nobody else allocates. The window
+ * starts small and doubles each time it is used up in order, so a
+ * sequential writer's window grows to match it. Nothing about it is
+ * on the disk; a window is discarded when the inode goes away, and
+ * ignored when nothing free is left outside the windows.
+ */
+#define BEAMFS_RSV_MIN 16
+#define BEAMFS_RSV_MAX 512
+
+void beamfs_rsv_init(struct beamfs_inode_info *fi)
+{
+ INIT_LIST_HEAD(&fi->i_rsv_list);
+ fi->i_rsv_start = 0;
+ fi->i_rsv_end = 0;
+ fi->i_rsv_next = 0;
+ fi->i_rsv_size = BEAMFS_RSV_MIN;
+ fi->i_last_alloc = 0;
+}
+
+static void beamfs_rsv_drop_locked(struct beamfs_inode_info *fi)
+{
+ if (!list_empty(&fi->i_rsv_list))
+ list_del_init(&fi->i_rsv_list);
+ fi->i_rsv_start = 0;
+ fi->i_rsv_end = 0;
+ fi->i_rsv_next = 0;
+}
+
+void beamfs_rsv_discard(struct inode *inode)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(inode->i_sb);
+
+ spin_lock(&sbi->s_lock);
+ beamfs_rsv_drop_locked(BEAMFS_I(inode));
+ spin_unlock(&sbi->s_lock);
+}
+
+/* The end of another inode's window that covers @bit, or 0. */
+static unsigned long beamfs_rsv_covering(struct beamfs_sb_info *sbi,
+ const struct beamfs_inode_info *me,
+ unsigned long bit)
+{
+ struct beamfs_inode_info *o;
+
+ list_for_each_entry(o, &sbi->s_rsv_windows, i_rsv_list) {
+ if (o == me)
+ continue;
+ if (bit >= o->i_rsv_start && bit < o->i_rsv_end)
+ return o->i_rsv_end;
+ }
+ return 0;
+}
+
+/* The start of the nearest other window at or after @bit, or nblocks. */
+static unsigned long beamfs_rsv_next_start(struct beamfs_sb_info *sbi,
+ const struct beamfs_inode_info *me,
+ unsigned long bit)
+{
+ struct beamfs_inode_info *o;
+ unsigned long lim = sbi->s_nblocks;
+
+ list_for_each_entry(o, &sbi->s_rsv_windows, i_rsv_list) {
+ if (o == me)
+ continue;
+ if (o->i_rsv_start >= bit && o->i_rsv_start < lim)
+ lim = o->i_rsv_start;
+ }
+ return lim;
+}
+
+/*
+ * The first free block from @goal, wrapping once, that lies in no
+ * other inode's window when @respect is set. s_nblocks when there is
+ * none.
+ */
+static unsigned long beamfs_rsv_find(struct beamfs_sb_info *sbi,
+ const struct beamfs_inode_info *me,
+ unsigned long goal, bool respect)
+{
+ unsigned long n = sbi->s_nblocks;
+ unsigned long bit = goal < n ? goal : 0;
+ bool wrapped = (bit == 0);
+
+ for (;;) {
+ unsigned long end;
+
+ bit = find_next_bit(sbi->s_block_bitmap, n, bit);
+ if (bit >= n) {
+ if (wrapped)
+ return n;
+ wrapped = true;
+ bit = 0;
+ continue;
+ }
+ end = respect ? beamfs_rsv_covering(sbi, me, bit) : 0;
+ if (!end)
+ return bit;
+ /* Inside somebody's window: continue past it. */
+ bit = end;
+ if (bit >= n) {
+ if (wrapped)
+ return n;
+ wrapped = true;
+ bit = 0;
+ }
+ }
+}
+
+/*
+ * A block for @fi: from its window while the window has one, else a
+ * new window from its goal. Under s_lock. s_nblocks when none is free.
+ */
+static unsigned long beamfs_rsv_alloc_locked(struct beamfs_sb_info *sbi,
+ struct beamfs_inode_info *fi)
+{
+ unsigned long n = sbi->s_nblocks;
+ unsigned long bit = n;
+ unsigned long goal;
+ bool exhausted;
+
+ if (fi->i_rsv_end > fi->i_rsv_next) {
+ bit = find_next_bit(sbi->s_block_bitmap, fi->i_rsv_end,
+ fi->i_rsv_next);
+ if (bit >= fi->i_rsv_end)
+ bit = n;
+ }
+ if (bit < n) {
+ fi->i_rsv_next = bit + 1;
+ fi->i_last_alloc = bit;
+ return bit;
+ }
+
+ /*
+ * A window used up in order earns a larger one; a window given
+ * up before that keeps its size.
+ */
+ exhausted = fi->i_rsv_end != 0 && fi->i_rsv_next >= fi->i_rsv_end;
+ beamfs_rsv_drop_locked(fi);
+
+ goal = fi->i_last_alloc ? fi->i_last_alloc + 1 : sbi->s_alloc_goal;
+ bit = beamfs_rsv_find(sbi, fi, goal, true);
+ if (bit >= n)
+ bit = beamfs_rsv_find(sbi, fi, goal, false);
+ if (bit >= n)
+ return n;
+
+ if (exhausted && fi->i_rsv_size < BEAMFS_RSV_MAX)
+ fi->i_rsv_size *= 2;
+ fi->i_rsv_start = bit;
+ fi->i_rsv_end = min3(bit + fi->i_rsv_size,
+ beamfs_rsv_next_start(sbi, fi, bit + 1), n);
+ fi->i_rsv_next = bit + 1;
+ fi->i_last_alloc = bit;
+ list_add(&fi->i_rsv_list, &sbi->s_rsv_windows);
+ return bit;
+}
+
+/*
+ * beamfs_alloc_block - allocate a free data block
+ *
+ * Returns absolute block number (>= s_data_start) on success,
+ * or 0 on failure (block 0 is the superblock, never a valid data block).
+ * No I/O performed; bitmap is in memory.
+ */
+u64 beamfs_alloc_block(struct super_block *sb, struct inode *owner)
+{
+ /*
+ * A failed volume takes no more writes. See beamfs_fail: the
+ * device is gone, its buffers cannot be read back, and marking
+ * them dirty is what generic/338 catches with dm-error.
+ */
+ if (beamfs_failed(sb))
+ return 0;
+
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned long bit;
+
+ if (!sbi->s_block_bitmap) {
+ pr_err("beamfs: block bitmap not initialized\n");
+ return 0;
+ }
+
+ spin_lock(&sbi->s_lock);
+
+ if (sbi->s_free_blocks == 0) {
+ spin_unlock(&sbi->s_lock);
+ return 0;
+ }
+
+ /*
+ * From where the last one landed, then from the start.
+ *
+ * Two passes rather than one scan from zero: the first covers the
+ * region a sequential writer is actually using, the second the
+ * holes earlier writers left behind. Together they are exhaustive,
+ * so free_blocks > 0 still guarantees a hit.
+ */
+ /*
+ * With an owner, from its reservation window (above). Without
+ * one -- the mount path, metadata with no inode -- from the
+ * volume's cursor, outside every window while that is possible.
+ */
+ if (owner) {
+ bit = beamfs_rsv_alloc_locked(sbi, BEAMFS_I(owner));
+ } else {
+ bit = beamfs_rsv_find(sbi, NULL, sbi->s_alloc_goal, true);
+ if (bit >= sbi->s_nblocks)
+ bit = beamfs_rsv_find(sbi, NULL, sbi->s_alloc_goal, false);
+ }
+
+ if (bit >= sbi->s_nblocks) {
+ spin_unlock(&sbi->s_lock);
+ pr_err("beamfs: bitmap inconsistency: free_blocks=%lu but no free bit\n",
+ sbi->s_free_blocks);
+ return 0;
+ }
+
+ /*
+ * Next search starts after this one. Past the end it wraps, which
+ * the second pass above then handles.
+ */
+ sbi->s_alloc_goal = bit + 1;
+ if (sbi->s_alloc_goal >= sbi->s_nblocks)
+ sbi->s_alloc_goal = 0;
+
+ clear_bit(bit, sbi->s_block_bitmap);
+ sbi->s_free_blocks--;
+ sbi->s_beamfs_sb->s_free_blocks = cpu_to_le64(sbi->s_free_blocks);
+ beamfs_dirty_super(sbi);
+
+ spin_unlock(&sbi->s_lock);
+
+ /*
+ * st_blocks, once beamfs_getattr has counted the tree. Before
+ * that the count is wholly the tree's, and a free could take it
+ * below zero.
+ */
+ if (owner && BEAMFS_I(owner)->i_blocks_counted)
+ inode_add_bytes(owner, BEAMFS_BLOCK_SIZE);
+
+ /*
+ * Rebuild the on-disk bitmap block from s_block_bitmap. Outside
+ * s_lock, because beamfs_write_bitmap_block takes the buffer lock,
+ * which can sleep.
+ */
+ beamfs_write_bitmap_block(sb, bit, owner);
+
+ trace_beamfs_block_alloc(sb->s_dev, owner ? owner->i_ino : 0,
+ (u64)(sbi->s_data_start + bit), 0);
+ return (u64)(sbi->s_data_start + bit);
+}
+
+/*
+ * beamfs_block_is_allocated -- is @block currently in use?
+ *
+ * Indirect blocks hold 512 pointers in exactly 4096 bytes, leaving no room
+ * for a checksum, so unlike data blocks, inodes and the superblock they carry
+ * no integrity field of their own. A flip in one corrupts a pointer, and the
+ * bounds check at the read sites only rejects values outside the data area.
+ * A corrupted pointer that lands inside it reaches an unrelated block and the
+ * read proceeds. DATA_SELFID catches the case where that block belongs to
+ * another file; this catches the case where it belongs to no file at all,
+ * which is what a random flip on a 64-bit pointer usually produces.
+ *
+ * The in-memory bitmap uses 1 for free, so an allocated block has its bit
+ * clear. It is held for the lifetime of the mount and is RS-protected on
+ * disk, so consulting it costs a bit test and no I/O.
+ */
+bool beamfs_block_is_allocated(struct super_block *sb, u64 block)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned long bit;
+
+ /*
+ * Blocks below s_data_start -- the superblock, inode table,
+ * bitmap, root directory and canary among them -- are reserved
+ * and lie outside the bitmap, so it has no answer for them.
+ * Calling them unallocated made every legitimate pointer into
+ * that zone fail, starting with i_direct[0] of the root inode:
+ * the very first block resolved at mount was rejected, and no
+ * image built by mkfs.beamfs --from-dir could boot. They are
+ * answered as allocated, and beamfs_free_block skips them the
+ * same way. Whether a pointer may lead into that zone at all is
+ * for the caller to decide.
+ */
+ if (block < sbi->s_data_start)
+ return true;
+ if (!sbi->s_block_bitmap)
+ return true; /* bitmap unavailable: do not reject */
+ bit = (unsigned long)(block - sbi->s_data_start);
+ if (bit >= sbi->s_nblocks)
+ return false;
+
+ return !test_bit(bit, sbi->s_block_bitmap);
+}
+
+/*
+ * beamfs_free_block - return a data block to the free pool
+ */
+void beamfs_free_block(struct super_block *sb, u64 block, struct inode *owner)
+{
+ /*
+ * A failed volume takes no more writes. See beamfs_fail: the
+ * device is gone, its buffers cannot be read back, and marking
+ * them dirty is what generic/338 catches with dm-error.
+ */
+ if (beamfs_failed(sb))
+ return;
+
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned long bit;
+
+ /* Layer 2 defense: silently skip reserved blocks. Any caller
+ * reaching this path with block < s_data_start is either a
+ * legitimate truncate of a reserved-pointing inode (canary, only
+ * possible if Layer 1 S_IMMUTABLE was bypassed by corruption) or
+ * a stale pointer from a corrupted indirect block. In both cases,
+ * silently rejecting preserves bitmap integrity without log spam.
+ */
+ if (block < sbi->s_data_start)
+ return;
+
+ bit = (unsigned long)(block - sbi->s_data_start);
+ if (bit >= sbi->s_nblocks) {
+ pr_err("beamfs: block %llu out of range\n", block);
+ return;
+ }
+
+ spin_lock(&sbi->s_lock);
+
+ if (test_bit(bit, sbi->s_block_bitmap)) {
+ /*
+ * Rate-limited, and no stack.
+ *
+ * A double free is worth knowing about, but a stack dump
+ * for one is a debugging aid that becomes a liability the
+ * moment the condition is reachable in normal operation.
+ * Under dm-error -- generic/338 -- writes fail by design,
+ * frees retry, and the trace lands in dmesg where
+ * _check_dmesg finds it and fails a test that was
+ * measuring something else entirely.
+ *
+ * The message still says which block, which is what a
+ * reader needs; whoever wants the caller can set a
+ * kprobe.
+ */
+ pr_warn_ratelimited("beamfs: double free of block %llu\n",
+ block);
+ spin_unlock(&sbi->s_lock);
+ return;
+ }
+ trace_beamfs_block_free(sb->s_dev, owner ? owner->i_ino : 0,
+ block, _RET_IP_);
+
+ /*
+ * Pull the goal back to what was just freed, if it is behind.
+ *
+ * Without this a block freed before the cursor is only found on
+ * the next wrap, so a workload that fills, deletes and refills --
+ * generic/015 and generic/027 both do exactly that -- walks the
+ * whole bitmap again on every cycle instead of reusing what it
+ * just released.
+ */
+ if (bit < sbi->s_alloc_goal)
+ sbi->s_alloc_goal = bit;
+
+ set_bit(bit, sbi->s_block_bitmap);
+ sbi->s_free_blocks++;
+ sbi->s_beamfs_sb->s_free_blocks = cpu_to_le64(sbi->s_free_blocks);
+ beamfs_dirty_super(sbi);
+
+ spin_unlock(&sbi->s_lock);
+
+ /* st_blocks: see beamfs_alloc_block. */
+ if (owner && BEAMFS_I(owner)->i_blocks_counted)
+ inode_sub_bytes(owner, BEAMFS_BLOCK_SIZE);
+
+ /*
+ * A freed block's pointer may legitimately vanish, and a freed
+ * indirect block's slots go with it. Tell the checker before the
+ * block is handed to anybody else, or every ordinary truncate
+ * reads as a violation.
+ */
+ beamfs_tc_forget_child(sb, block);
+ beamfs_tc_forget_parent(sb, block);
+ /* See alloc_block: rebuild the on-disk bitmap outside s_lock. */
+ beamfs_write_bitmap_block(sb, bit, owner);
+}
+
+/* ------------------------------------------------------------------ */
+/* Inode number allocation */
+/* ------------------------------------------------------------------ */
+
+/*
+ * beamfs_alloc_inode_num - allocate a free inode number
+ *
+ * Uses the in-memory inode bitmap. No I/O performed, no sb_bread under
+ * spinlock.
+ *
+ * Returns inode number >= 2 on success (1 = root, always reserved),
+ * or 0 on failure.
+ */
+u64 beamfs_alloc_inode_num(struct super_block *sb)
+{
+ /*
+ * A failed volume takes no more writes. See beamfs_fail: the
+ * device is gone, its buffers cannot be read back, and marking
+ * them dirty is what generic/338 catches with dm-error.
+ */
+ if (beamfs_failed(sb))
+ return 0;
+
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned long bit;
+
+ if (!sbi->s_inode_bitmap) {
+ pr_err("beamfs: inode bitmap not initialized\n");
+ return 0;
+ }
+
+ spin_lock(&sbi->s_lock);
+
+ if (sbi->s_free_inodes == 0) {
+ spin_unlock(&sbi->s_lock);
+ return 0;
+ }
+
+ /*
+ * Bits 0 and 1 are never set (inode 0 invalid, inode 1 = root reserved).
+ * find_next_bit starting at 2 skips both.
+ */
+ bit = find_next_bit(sbi->s_inode_bitmap, sbi->s_ninodes + 1, 2);
+ if (bit > sbi->s_ninodes) {
+ spin_unlock(&sbi->s_lock);
+ pr_err("beamfs: inode bitmap inconsistency: free_inodes=%lu but no free bit\n",
+ sbi->s_free_inodes);
+ return 0;
+ }
+
+ clear_bit(bit, sbi->s_inode_bitmap);
+ sbi->s_free_inodes--;
+ sbi->s_beamfs_sb->s_free_inodes = cpu_to_le64(sbi->s_free_inodes);
+ beamfs_dirty_super(sbi);
+
+ spin_unlock(&sbi->s_lock);
+
+ return (u64)bit;
+}
+
+/*
+ * beamfs_free_inode_num - return an inode number to the free pool
+ *
+ * Called from evict_inode path when nlink drops to 0.
+ */
+void beamfs_free_inode_num(struct super_block *sb, u64 ino)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ /* Layer 2 defense: silently skip reserved inodes (root, canary).
+ * Range check covers both reserved range and bitmap bounds.
+ */
+ if (beamfs_ino_is_reserved(ino) || ino > sbi->s_ninodes)
+ return;
+
+ spin_lock(&sbi->s_lock);
+
+ if (test_bit((unsigned long)ino, sbi->s_inode_bitmap)) {
+ pr_warn("beamfs: double free of inode %llu\n", ino);
+ spin_unlock(&sbi->s_lock);
+ return;
+ }
+
+ set_bit((unsigned long)ino, sbi->s_inode_bitmap);
+ sbi->s_free_inodes++;
+ sbi->s_beamfs_sb->s_free_inodes = cpu_to_le64(sbi->s_free_inodes);
+ beamfs_dirty_super(sbi);
+
+ spin_unlock(&sbi->s_lock);
+}
--
2.55.0
next prev 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 ` Aurelien DESBRIERES [this message]
2026-10-19 4:17 ` [RFC PATCH 07/18] beamfs: add superblock operations Aurelien DESBRIERES
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-7-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