mirror of https://lore.kernel.org/lkml/
 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 08/18] beamfs: add inode and directory operations
Date: Mon, 19 Oct 2026 06:17:18 +0200	[thread overview]
Message-ID: <20261019041728.327785-9-aurelien@hackers.camp> (raw)
In-Reply-To: <20261019041728.327785-1-aurelien@hackers.camp>

inode.c reads inodes from the inode table into the VFS.

dir.c implements readdir and dirent.c the directory block format.
Directory blocks carry inline Reed-Solomon parity, sixteen codewords
per block as on the data path, and hold variable-length entries
delimited by d_rec_len.

Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
 fs/beamfs/dir.c    | 279 ++++++++++++++++++++++++++++++++
 fs/beamfs/dirent.c | 276 ++++++++++++++++++++++++++++++++
 fs/beamfs/inode.c  | 388 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 943 insertions(+)
 create mode 100644 fs/beamfs/dir.c
 create mode 100644 fs/beamfs/dirent.c
 create mode 100644 fs/beamfs/inode.c

diff --git a/fs/beamfs/dir.c b/fs/beamfs/dir.c
new file mode 100644
index 000000000000..dcb595b16b4e
--- /dev/null
+++ b/fs/beamfs/dir.c
@@ -0,0 +1,279 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Directory operations
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/fs_dirent.h>
+#include "beamfs.h"
+
+/*
+ * beamfs_readdir - iterate directory entries
+ *
+ * ctx->pos encoding:
+ *   0, 1       : '.' and '..' (emitted by dir_emit_dots)
+ *   INT_MAX    : EOF
+ *   other      : 2 + block_idx * BEAMFS_DATA_INLINE_BYTES + offset
+ *
+ * block_idx is the 0-based logical block, direct then single indirect;
+ * offset is a byte offset into that block's 3824-byte payload. The
+ * cookie names where reading resumes, and any value is accepted: one
+ * that falls inside a record resumes at the next record boundary.
+ *
+ * At most BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS = 524 blocks, so
+ * pos is at most 2 + 524 * 3824, well within the 32-bit pos space.
+ */
+static int beamfs_readdir(struct file *file, struct dir_context *ctx)
+{
+	struct inode            *inode = file_inode(file);
+	struct super_block      *sb    = inode->i_sb;
+	struct buffer_head      *bh;
+	struct beamfs_dir_entry *de;
+	u8            *payload;
+	unsigned long  block_no;
+	u32            offset;
+	int            block_idx;
+	int            start_block;
+	u32            start_off;
+	int            ret = 0;
+
+	/*
+	 * ctx->pos is a byte offset into the directory, and any value is
+	 * legal.
+	 *
+	 * It used to be (block + 1) << 16 | offset, which cannot express
+	 * a position below 65536 as anything but block -1: seekdir to a
+	 * cookie this filesystem never issued -- which is exactly what
+	 * t_readdir_3 does -- started the walk outside every block and
+	 * returned nothing. "Unexpected EOF while reading dir",
+	 * reproducible at thirty files and above.
+	 *
+	 * ext2 treats pos as the offset it is: block = pos / blocksize,
+	 * offset = pos % blocksize, and a position past the end simply
+	 * finds no entries. Same here, with 3824-byte blocks. Positions
+	 * 0 and 1 stay reserved for dot and dotdot, so the first real
+	 * entry sits at 2 and the arithmetic starts one step in.
+	 */
+	if (ctx->pos == INT_MAX)
+		return 0;
+
+	if (ctx->pos < 2) {
+		if (!dir_emit_dots(file, ctx))
+			return 0;
+	}
+
+	if (ctx->pos <= 2) {
+		start_block = 0;
+		start_off   = 0;
+	} else {
+		u64 p = (u64)ctx->pos - 2;
+
+		/*
+		 * A directory block is not a capsule.
+		 *
+		 * dirent.c encodes and decodes one in the alternating
+		 * layout, so its payload is 3824 whatever the volume's
+		 * data blocks do. Following beamfs_block_payload here
+		 * would have the reader seek by 3808 into blocks laid
+		 * out in 3824 -- every entry past the first block off by
+		 * sixteen bytes.
+		 */
+		start_block = (int)div_u64_rem(p, BEAMFS_DATA_INLINE_BYTES, &start_off);
+	}
+
+	payload = beamfs_scratch_get(sb);
+	if (!payload)
+		return -ENOMEM;
+
+	for (block_idx = start_block;
+	     block_idx < (int)(BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS);
+	     block_idx++) {
+		u64 _bno;
+		int _ret = beamfs_dir_get_block(inode, block_idx, false, &_bno);
+
+		if (_ret) {
+			ret = _ret;
+			goto out;
+		}
+		block_no = (unsigned long)_bno;
+		if (!block_no)
+			break;
+
+		bh = beamfs_bread(sb, block_no, "directory");
+		if (!bh)
+			continue;
+
+		_ret = beamfs_dirent_decode(sb, bh, payload);
+		brelse(bh);
+		if (_ret < 0) {
+			pr_err_ratelimited("beamfs: directory block %lu uncorrectable\n",
+					   block_no);
+			ret = _ret;
+			goto out;
+		}
+
+		offset = (block_idx == start_block) ? start_off : 0;
+
+		/*
+		 * Slide to a record boundary.
+		 *
+		 * seekdir can land mid-record, and reading a header from
+		 * the middle of a name yields nonsense. Walk from the top
+		 * of the block to the first record at or after the
+		 * requested offset -- ext2_validate_entry does the same,
+		 * for the same reason.
+		 */
+		if (offset != 0 && offset < BEAMFS_DATA_INLINE_BYTES) {
+			u32 scan = 0;
+
+			while (scan < offset) {
+				u32 nxt;
+
+				de = (struct beamfs_dir_entry *)(payload + scan);
+				if (!beamfs_dirent_valid(de, scan))
+					break;
+				nxt = beamfs_dirent_next(de, scan);
+				if (nxt == BEAMFS_DIRENT_NOSPACE || nxt <= scan)
+					break;
+				scan = nxt;
+			}
+			offset = scan;
+		}
+
+		while (offset < BEAMFS_DATA_INLINE_BYTES) {
+			u32 next;
+
+			de = (struct beamfs_dir_entry *)(payload + offset);
+			if (!beamfs_dirent_valid(de, offset))
+				break;
+
+			if (de->d_ino &&
+			    !(de->d_name_len == 1 && de->d_name[0] == '.') &&
+			    !(de->d_name_len == 2 && de->d_name[0] == '.' &&
+			      de->d_name[1] == '.')) {
+				u8 dt;
+
+				switch (de->d_file_type) {
+				case 1:
+					dt = DT_REG;
+					break;
+				case 2:
+					dt = DT_DIR;
+					break;
+				default:
+					dt = de->d_file_type;
+					break;
+				}
+
+				if (!dir_emit(ctx, de->d_name, de->d_name_len,
+					      le64_to_cpu(de->d_ino), dt))
+					goto out;
+			}
+
+			next = beamfs_dirent_next(de, offset);
+			if (next == BEAMFS_DIRENT_NOSPACE)
+				break;
+			offset = next;
+
+			/*
+			 * Advance after emitting, and for every record, so
+			 * the cookie names where reading resumes rather
+			 * than the entry just returned.
+			 */
+			ctx->pos = 2 + (loff_t)block_idx *
+					BEAMFS_DATA_INLINE_BYTES + offset;
+		}
+	}
+
+	ctx->pos = INT_MAX;
+out:
+	beamfs_scratch_put(sb, payload);
+	return ret;
+}
+
+/*
+ * beamfs_lookup - find dentry in directory
+ */
+struct dentry *beamfs_lookup(struct inode *dir,
+			     struct dentry *dentry,
+			     unsigned int flags)
+{
+	struct super_block      *sb = dir->i_sb;
+	struct buffer_head      *bh;
+	struct beamfs_dir_entry *de;
+	struct inode            *inode = NULL;
+	u8            *payload;
+	unsigned long  block_no;
+	u32            offset;
+	unsigned int   i;
+	int            ret = 0;
+
+	if (dentry->d_name.len > BEAMFS_MAX_FILENAME)
+		return ERR_PTR(-ENAMETOOLONG);
+
+	payload = beamfs_scratch_get(sb);
+	if (!payload)
+		return ERR_PTR(-ENOMEM);
+
+	for (i = 0; i < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS; i++) {
+		u64 _bno;
+		int _ret = beamfs_dir_get_block(dir, i, false, &_bno);
+
+		if (_ret) {
+			ret = _ret;
+			goto out;
+		}
+		block_no = (unsigned long)_bno;
+		if (!block_no)
+			break;
+
+		bh = beamfs_bread(sb, block_no, "directory");
+		if (!bh)
+			continue;
+
+		_ret = beamfs_dirent_decode(sb, bh, payload);
+		brelse(bh);
+		if (_ret < 0) {
+			ret = _ret;
+			goto out;
+		}
+
+		offset = 0;
+		while (offset < BEAMFS_DATA_INLINE_BYTES) {
+			u32 next;
+
+			de = (struct beamfs_dir_entry *)(payload + offset);
+			if (!beamfs_dirent_valid(de, offset))
+				break;
+
+			if (de->d_ino &&
+			    de->d_name_len == dentry->d_name.len &&
+			    !memcmp(de->d_name, dentry->d_name.name,
+				    de->d_name_len)) {
+				inode = beamfs_iget(sb,
+						    (unsigned long)le64_to_cpu(de->d_ino));
+				goto out;
+			}
+
+			next = beamfs_dirent_next(de, offset);
+			if (next == BEAMFS_DIRENT_NOSPACE)
+				break;
+			offset = next;
+		}
+	}
+
+out:
+	beamfs_scratch_put(sb, payload);
+	if (ret)
+		return ERR_PTR(ret);
+	if (IS_ERR(inode))
+		return ERR_CAST(inode);
+	return d_splice_alias(inode, dentry);
+}
+
+const struct file_operations beamfs_dir_operations = {
+	.llseek         = generic_file_llseek,
+	.read           = generic_read_dir,
+	.iterate_shared = beamfs_readdir,
+};
diff --git a/fs/beamfs/dirent.c b/fs/beamfs/dirent.c
new file mode 100644
index 000000000000..fcefff321c39
--- /dev/null
+++ b/fs/beamfs/dirent.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Directory blocks: variable-length entries, and parity over them
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Every other structure on this volume is covered. A directory block
+ * without parity would be 4096 raw bytes, no checksum, no correction,
+ * and an upset there silent and permanent: a flipped inode number
+ * points a name at the wrong file, a flipped length walks the parser
+ * off the end of an entry, and neither is detectable let alone
+ * correctable.
+ *
+ * Parity is inline, sixteen RS codewords in the block, matching the
+ * data path. The alternative -- a separate parity region, which is what
+ * the indirect blocks use -- keeps 4096 usable bytes but makes that
+ * region a single point of failure for every directory on the volume,
+ * and leaves the filesystem with three different parity models instead
+ * of two. ext4 puts its directory checksum inline for the same reason.
+ *
+ * Entries are variable length, and every walk steps by d_rec_len.
+ * Stepping by sizeof(struct beamfs_dir_entry) instead -- a flat 268
+ * bytes, whatever the name's length -- would give fourteen entries per
+ * 3824-byte block and a hard ceiling of 7336 per directory.
+ *
+ * An eleven-character name takes 24 bytes: 144 entries per block and a
+ * ceiling above 75000. Roughly ten times the density, ten times fewer
+ * blocks read per lookup, and ten times less metadata exposed for the
+ * same directory.
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include "beamfs.h"
+
+/*
+ * Which subblock an offset falls in, and where that subblock ends.
+ *
+ * Each of the sixteen subblocks is an independent codeword: one is
+ * correctable, or not, on its own. An entry spanning two turns a single
+ * uncorrectable subblock into a parse failure for everything after it
+ * in the block, so entries are kept inside one whenever they fit.
+ */
+static inline u32 beamfs_dirent_sub_end(u32 off)
+{
+	return (off / BEAMFS_SUBBLOCK_DATA + 1) * BEAMFS_SUBBLOCK_DATA;
+}
+
+/*
+ * Where the next entry of @len bytes goes, given a cursor at @off.
+ *
+ * Returns the offset to place it at, which is @off unless the entry
+ * would straddle a subblock boundary and does not have to. Returns
+ * BEAMFS_DIRENT_NOSPACE when the block cannot hold it.
+ *
+ * An entry longer than a subblock -- a name of 225 characters or more, which
+ * NAME_MAX allows and xfstests exercises -- is placed where it falls.
+ * Refusing it would mean rejecting a filename every other Linux
+ * filesystem accepts, and the property is kept everywhere it can be.
+ */
+u32 beamfs_dirent_place(u32 off, u16 len)
+{
+	u32 end;
+
+	if (off + len > BEAMFS_DATA_INLINE_BYTES)
+		return BEAMFS_DIRENT_NOSPACE;
+
+	if (len > BEAMFS_SUBBLOCK_DATA)
+		return off;			/* cannot fit either way */
+
+	end = beamfs_dirent_sub_end(off);
+	if (off + len <= end)
+		return off;			/* fits where it is */
+
+	if (end + len > BEAMFS_DATA_INLINE_BYTES)
+		return BEAMFS_DIRENT_NOSPACE;
+
+	return end;				/* start of the next subblock */
+}
+
+/*
+ * Step to the entry after the one at @off.
+ *
+ * A record length of zero, or one that would leave the block, ends the
+ * walk: on a damaged block that is the difference between stopping and
+ * reading whatever follows as a directory entry.
+ */
+u32 beamfs_dirent_next(const struct beamfs_dir_entry *de, u32 off)
+{
+	u16 rec = le16_to_cpu(de->d_rec_len);
+
+	if (rec < BEAMFS_DIRENT_MIN_LEN)
+		return BEAMFS_DIRENT_NOSPACE;
+	if (off + rec > BEAMFS_DATA_INLINE_BYTES)
+		return BEAMFS_DIRENT_NOSPACE;
+	return off + rec;
+}
+
+/*
+ * Is the entry at @off structurally sound?
+ *
+ * Checked before the name is used, because a flipped length is exactly
+ * the failure this parity was added to catch, and a name length that
+ * runs past the record is how a directory read walks off into the next
+ * entry.
+ */
+bool beamfs_dirent_valid(const struct beamfs_dir_entry *de, u32 off)
+{
+	u16 rec = le16_to_cpu(de->d_rec_len);
+
+	if (rec < BEAMFS_DIRENT_MIN_LEN)
+		return false;
+	if (off + rec > BEAMFS_DATA_INLINE_BYTES)
+		return false;
+	/* d_name_len is a u8: every length it can hold is allowed. */
+	BUILD_BUG_ON(BEAMFS_MAX_FILENAME < U8_MAX);
+	if (BEAMFS_DIRENT_HDR_LEN + de->d_name_len > rec)
+		return false;
+	return true;
+}
+
+/*
+ * Re-encode a directory block's parity. The buffer lock must be held.
+ *
+ * Same sixteen codewords as a data block, over the same 239-byte
+ * subblocks, so a corrupted directory is correctable to the same depth
+ * as corrupted file contents.
+ */
+void beamfs_dirent_encode(struct buffer_head *bh)
+{
+	u8 *b = (u8 *)bh->b_data;
+	unsigned int i;
+
+	/*
+	 * The payload is laid out contiguously in the first 3824 bytes
+	 * and scattered into the codeword slots here, the way the data
+	 * path does it, so the walk above can treat the block as flat.
+	 */
+	for (i = BEAMFS_DATA_INLINE_SUBBLOCKS; i-- > 0; )
+		memmove(b + (size_t)i * BEAMFS_SUBBLOCK_TOTAL,
+			b + (size_t)i * BEAMFS_SUBBLOCK_DATA,
+			BEAMFS_SUBBLOCK_DATA);
+
+	{
+		int _e = beamfs_rs_encode_region(b, BEAMFS_SUBBLOCK_TOTAL,
+				b + BEAMFS_SUBBLOCK_DATA,
+				BEAMFS_SUBBLOCK_TOTAL,
+				BEAMFS_SUBBLOCK_DATA,
+				BEAMFS_DATA_INLINE_SUBBLOCKS);
+
+		/*
+		 * A directory block with no valid parity is one a
+		 * verify will later call beyond correction.
+		 */
+		if (_e < 0)
+			pr_err_ratelimited("beamfs/dirent: encode failed: %d\n",
+					   _e);
+	}
+}
+
+/*
+ * Decode a directory block into @dst, correcting what RS can.
+ *
+ * @dst holds the 3824 payload bytes contiguously, which is what the
+ * entry walk expects. Returns the number of subblocks that needed
+ * correcting, or -EUCLEAN if any could not be.
+ */
+int beamfs_dirent_decode(struct super_block *sb, struct buffer_head *bh,
+			 u8 *dst)
+{
+	/*
+	 * On the stack, not from the allocator.
+	 *
+	 * These are 64 and 512 bytes, and this runs once per directory
+	 * block on every lookup, readdir, create and unlink -- so a
+	 * ten-block directory meant twenty allocations to resolve one
+	 * name. Under memory pressure that turns a path that should never
+	 * block into one that does: generic/558 had twelve tasks in D and
+	 * one of them was sitting in kfree underneath this function,
+	 * called from beamfs_lookup.
+	 *
+	 * 576 bytes against the kernel's 2 KiB frame budget, on a
+	 * function whose callers are shallow.
+	 */
+	int results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+	u8 positions[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+	unsigned int i, corrected = 0;
+	int ret = 0;
+
+	memset(results, 0, sizeof(results));
+
+	/*
+	 * Under the buffer lock, like the sweep.
+	 *
+	 * beamfs_dirent_encode holds lock_buffer across the memmove that
+	 * spreads the records into codeword slots and the RS encode that
+	 * follows. Copying without the lock catches that halfway: payload
+	 * from after the write, parity from before, uncorrectable by
+	 * construction and reported as damage on a block nothing has
+	 * damaged.
+	 *
+	 * Five callers reach here -- readdir, lookup, add_dirent,
+	 * del_dirent, dir_is_empty -- and any of them can run while
+	 * another writes the same directory.
+	 */
+	lock_buffer(bh);
+	/*
+	 * The whole block, parity included.
+	 *
+	 * decode_rs8 corrects data and parity in place -- it writes to
+	 * both buffers it is given. Copying only the payload and pointing
+	 * the decoder at bh->b_data for the parity means every read
+	 * rewrites the shared buffer's parity bytes. A block read often
+	 * enough degrades until it will not decode at all, which is what
+	 * the sweep kept reporting on blocks fsck called clean, and why
+	 * the count grew as the volume got busier.
+	 *
+	 * The data path has done this correctly all along: "Decode
+	 * RS(255,239) subblocks into a private scratch buffer, never into
+	 * bh->b_data."
+	 */
+	memcpy(dst, (u8 *)bh->b_data, BEAMFS_BLOCK_SIZE);
+	unlock_buffer(bh);
+
+	ret = beamfs_rs_decode_region(dst, BEAMFS_SUBBLOCK_TOTAL,
+				      dst + BEAMFS_SUBBLOCK_DATA,
+				      BEAMFS_SUBBLOCK_TOTAL,
+				      BEAMFS_SUBBLOCK_DATA,
+				      BEAMFS_DATA_INLINE_SUBBLOCKS,
+				      results, positions,
+				      BEAMFS_RS_PARITY / 2,
+				"directory");
+
+	for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+		if (results[i] > 0) {
+			corrected++;
+			beamfs_log_rs_event_flagged(sb,
+						    bh->b_blocknr,
+				positions + (size_t)i * (BEAMFS_RS_PARITY / 2),
+				(unsigned int)results[i],
+				BEAMFS_SUBBLOCK_DATA,
+				beamfs_rs_event_subblock_bits(i));
+			beamfs_budget_record(sb, bh->b_blocknr,
+					     (unsigned int)results[i]);
+		} else if (results[i] < 0) {
+			/*
+			 * Name the block and the codeword. The buffer knows
+			 * where it came from; the decoder does not, and a
+			 * line that says only "uncorrectable" leaves the
+			 * reader with nowhere to look.
+			 */
+			pr_err_ratelimited("beamfs: directory block %llu subblock %u/%u uncorrectable\n",
+					   (unsigned long long)bh->b_blocknr,
+					   i, BEAMFS_DATA_INLINE_SUBBLOCKS);
+			ret = -EUCLEAN;
+		}
+	}
+
+	/*
+	 * De-interleave what the decoder corrected.
+	 *
+	 * dst held the block in its on-disk layout for the decode --
+	 * decode_rs8 writes to both the data and the parity it is given,
+	 * so both had to be private. Callers want the 3824 payload bytes
+	 * contiguous, which is what the walk over records expects.
+	 */
+	if (ret >= 0) {
+		unsigned int k;
+
+		for (k = 0; k < BEAMFS_DATA_INLINE_SUBBLOCKS; k++)
+			memmove(dst + (size_t)k * BEAMFS_SUBBLOCK_DATA,
+				dst + (size_t)k * BEAMFS_SUBBLOCK_TOTAL,
+				BEAMFS_SUBBLOCK_DATA);
+	}
+
+	return ret < 0 ? ret : (int)corrected;
+}
diff --git a/fs/beamfs/inode.c b/fs/beamfs/inode.c
new file mode 100644
index 000000000000..fb7a8aead44a
--- /dev/null
+++ b/fs/beamfs/inode.c
@@ -0,0 +1,388 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Inode operations
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/pagemap.h>
+#include <linux/slab.h>
+#include <linux/time.h>
+#include "beamfs.h"
+
+/*
+ * Split a nanosecond count into the seconds and nanoseconds a timespec64
+ * wants, with the nanosecond part never negative.
+ */
+static void beamfs_split_ns(s64 ns, s64 *sec, long *nsec)
+{
+	s32 rem;
+
+	*sec = div_s64_rem(ns, NSEC_PER_SEC, &rem);
+	if (rem < 0) {
+		*sec -= 1;
+		rem += NSEC_PER_SEC;
+	}
+	*nsec = rem;
+}
+
+#define beamfs_set_time_from_ns(inode, setter, ns)			\
+	do {								\
+		s64  _sec;						\
+		long _nsec;						\
+									\
+		beamfs_split_ns((ns), &_sec, &_nsec);			\
+		setter((inode), _sec, _nsec);				\
+	} while (0)
+
+/*
+ * beamfs_iget - read inode from disk into VFS
+ * @sb:  superblock
+ * @ino: inode number (1-based)
+ *
+ * Inode table starts at s_inode_table_blk.
+ * Each block holds BEAMFS_BLOCK_SIZE / sizeof(beamfs_inode) inodes.
+ */
+struct inode *beamfs_iget(struct super_block *sb, unsigned long ino)
+{
+	struct beamfs_sb_info    *sbi = BEAMFS_SB(sb);
+	struct beamfs_inode_info *fi;
+	struct beamfs_inode      *raw;
+	struct buffer_head      *bh;
+	struct inode            *inode;
+	unsigned long            inodes_per_block;
+	unsigned long            block, offset;
+	u64                      ninodes;
+	__u32                    crc;
+
+	/*
+	 * An inode number is on-disk data, so it is attacker- and
+	 * flip-controlled. beamfs_lookup passes de->d_ino straight here
+	 * and beamfs_dirent_valid checks d_rec_len and d_name_len only,
+	 * never d_ino. Without this bound the block computed just below,
+	 * s_inode_table_blk + (ino - 1) / inodes_per_block, walks out of
+	 * the inode table and an arbitrary block is read as inodes; any
+	 * write to the inode that comes back then allocates blocks under
+	 * a number no volume of this size can hold. Observed as ino=27192
+	 * on a 16384-inode volume, 241 blocks.
+	 *
+	 * The bound comes from the superblock, not from sbi->s_ninodes:
+	 * beamfs_setup_bitmap runs after the root iget in fill_super, so
+	 * s_ninodes is still zero on that first call and every mount
+	 * would be refused here.
+	 */
+	ninodes = le64_to_cpu(sbi->s_beamfs_sb->s_inode_count);
+	if (ino < BEAMFS_RESERVED_INO_ROOT || (u64)ino > ninodes) {
+		pr_err_ratelimited("beamfs: inode number %lu out of range (1..%llu)\n",
+				   ino, ninodes);
+		return ERR_PTR(-EUCLEAN);
+	}
+
+	inode = iget_locked(sb, ino);
+	if (!inode)
+		return ERR_PTR(-ENOMEM);
+
+	/* Already in cache */
+	if (!beamfs_inode_is_new(inode))
+		return inode;
+
+	inodes_per_block = BEAMFS_BLOCK_SIZE / sizeof(struct beamfs_inode);
+	block  = le64_to_cpu(sbi->s_beamfs_sb->s_inode_table_blk)
+		 + (ino - 1) / inodes_per_block;
+	offset = (ino - 1) % inodes_per_block;
+
+	bh = beamfs_bread(sb, block, "inode table");
+	if (!bh) {
+		pr_err("beamfs: unable to read inode block %lu\n", block);
+		iget_failed(inode);
+		return ERR_PTR(-EIO);
+	}
+
+	raw = (struct beamfs_inode *)bh->b_data + offset;
+
+	/*
+	 * Integrity check, two stages.
+	 *
+	 * Stage A: CRC32. The nominal path. >99% of inode reads are
+	 * expected to match here at low cost; CRC32 is hardware-accelerated
+	 * by crc32_le on architectures that support it.
+	 *
+	 * Stage B: RS FEC, only invoked if CRC32 fails AND the format
+	 * declares RS protection on inodes. Two activation paths:
+	 *   - s_data_protection_scheme == INODE_UNIVERSAL (legacy v4)
+	 *   - s_feat_incompat & BEAMFS_FEATURE_INCOMPAT_PER_INODE_RS (v5)
+	 * Both paths share the same on-disk parity layout in i_reserved
+	 * [0..15] (16 bytes covering BEAMFS_INODE_RS_DATA = 172 data
+	 * bytes). The kernel write path always emits this parity
+	 * (namei.c::beamfs_write_inode_raw) and mkfs.beamfs always seeds
+	 * it at format time (rs_encode_inode), so the decoder is safe to
+	 * invoke whenever either path is active.
+	 *
+	 * After a successful RS correction we re-verify CRC32 on the
+	 * corrected buffer before accepting the inode. This guards
+	 * against the rare case where an SEU on the parity field itself
+	 * makes decode_rs8 return success while the data has actually
+	 * been altered toward a wrong codeword.
+	 */
+	/*
+	 * A free inode is not a corrupt one.
+	 *
+	 * Free slots are zeroed, i_crc32 included, and beamfs_inode_crc of
+	 * a zeroed inode is 0x0ee93c94 rather than zero -- so every read of
+	 * a free inode fails the check below by construction. Dozens of them
+	 * appeared in twenty milliseconds during one test run, reported as
+	 * "CRC32 mismatch", which is both the wrong error and the wrong
+	 * diagnosis: nothing was corrupt, something had followed a stale
+	 * reference.
+	 *
+	 * ESTALE is what the VFS expects here, and it is what ext2 returns
+	 * for an inode with no links.
+	 */
+	if (le16_to_cpu(raw->i_mode) == 0) {
+		brelse(bh);
+		iget_failed(inode);
+		return ERR_PTR(-ESTALE);
+	}
+
+	crc = beamfs_inode_crc(raw);
+	if (crc != le32_to_cpu(raw->i_crc32)) {
+		struct beamfs_super_block *bsb = BEAMFS_SB(sb)->s_beamfs_sb;
+		u32 scheme = le32_to_cpu(bsb->s_data_protection_scheme);
+		int nerr;
+
+		if (scheme != BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL &&
+		    !(BEAMFS_SB(sb)->s_feat_incompat &
+		      BEAMFS_FEATURE_INCOMPAT_PER_INODE_RS)) {
+			pr_err("beamfs: inode %lu CRC32 mismatch (no RS available, scheme=%u feat_incompat=0x%016llx)\n",
+			       ino, scheme,
+			       (unsigned long long)BEAMFS_SB(sb)->s_feat_incompat);
+			brelse(bh);
+			iget_failed(inode);
+			return ERR_PTR(-EIO);
+		}
+
+		{
+			u8 positions[BEAMFS_RS_PARITY / 2];
+			struct beamfs_inode *fixed;
+
+			/*
+			 * Decode a copy, not the shared buffer.
+			 *
+			 * decode_rs8 corrects in place, and raw points into
+			 * the inode table block, which holds several inodes.
+			 * Correcting there rewrites a buffer other inodes are
+			 * read from, and the mark_buffer_dirty below then
+			 * carries the result to disk -- a speculative fix
+			 * written before the CRC has confirmed it, on a block
+			 * that may simply have been mid-write.
+			 *
+			 * The same shape as beamfs_ind_parity_verify had, and
+			 * as the data read path avoids: "never into
+			 * bh->b_data. decode_rs8 mutates both data and parity."
+			 *
+			 * Decode into a private copy, confirm with the CRC,
+			 * and only then write the corrected inode back under
+			 * the buffer lock. A correction that does not pass the
+			 * CRC touches nothing.
+			 */
+			/*
+			 * The copy lives on the heap: with it on the stack
+			 * the frame of beamfs_iget was 560 bytes, past the
+			 * 512 submit-checklist allows, and iget_locked has
+			 * just allocated the in-core inode anyway.
+			 */
+			fixed = kmemdup(raw, sizeof(*fixed), GFP_NOFS);
+			if (!fixed) {
+				brelse(bh);
+				iget_failed(inode);
+				return ERR_PTR(-ENOMEM);
+			}
+			nerr = beamfs_rs_decode((u8 *)fixed,
+						BEAMFS_INODE_RS_DATA,
+					       fixed->i_reserved,
+					       positions,
+					       BEAMFS_RS_PARITY / 2,
+				"inode");
+			if (nerr < 0) {
+				pr_err("beamfs: inode %lu uncorrectable\n", ino);
+				kfree(fixed);
+				brelse(bh);
+				iget_failed(inode);
+				return ERR_PTR(-EIO);
+			}
+
+			crc = beamfs_inode_crc(fixed);
+			if (crc != le32_to_cpu(fixed->i_crc32)) {
+				pr_err("beamfs: inode %lu CRC32 mismatch after RS correction\n",
+				       ino);
+				kfree(fixed);
+				brelse(bh);
+				iget_failed(inode);
+				return ERR_PTR(-EIO);
+			}
+
+			/* Confirmed: publish the correction. */
+			lock_buffer(bh);
+			memcpy(raw, fixed, sizeof(*fixed));
+			unlock_buffer(bh);
+			kfree(fixed);
+
+			/* Log the RS event with its positions: nerr is the number
+			 * of data bytes corrected, the entries positions[] holds.
+			 */
+			if (nerr > 0) {
+				unsigned int np = (unsigned int)nerr;
+
+				if (np > BEAMFS_RS_PARITY / 2)
+					np = BEAMFS_RS_PARITY / 2;
+				/*
+				 * The table block, not the inode number: a
+				 * clustering pass asks where on the medium the
+				 * damage landed, and an inode number is not a
+				 * position.
+				 */
+				beamfs_log_rs_event_flagged(sb, bh->b_blocknr,
+							    positions, np,
+						   BEAMFS_INODE_RS_DATA,
+						   BEAMFS_RS_EVENT_FLAG_INODE);
+			}
+			mark_buffer_dirty(bh);
+			pr_warn("beamfs: inode %lu corrected by RS FEC (%d symbols)\n",
+				ino, nerr);
+		}
+	}
+
+	fi = BEAMFS_I(inode);
+
+	/* Populate VFS inode */
+	inode->i_mode  = le16_to_cpu(raw->i_mode);
+	inode->i_uid   = make_kuid(sb->s_user_ns, le32_to_cpu(raw->i_uid));
+	inode->i_gid   = make_kgid(sb->s_user_ns, le32_to_cpu(raw->i_gid));
+	set_nlink(inode, le16_to_cpu(raw->i_nlink));
+	inode->i_size  = le64_to_cpu(raw->i_size);
+
+	/*
+	 * Times before 1970 are negative, and there is nothing exotic
+	 * about them: tar preserves them, build systems set them, and a
+	 * file dated 1960 is a file. le64_to_cpu returns unsigned, so
+	 * dividing it turned -315M seconds into 1.8e19 -- generic/258
+	 * reports it as "Timestamp wrapped".
+	 *
+	 * div_s64_rem rather than / and %, because C truncates toward
+	 * zero: -1.5e9 ns would give -1 second and -5e8 nanoseconds, and
+	 * a negative nanosecond field is not a time the VFS can use. The
+	 * floor form gives -2 seconds and +5e8 nanoseconds, which is the
+	 * same instant expressed the way timespec64 requires.
+	 */
+	beamfs_set_time_from_ns(inode, inode_set_atime,
+				(s64)le64_to_cpu(raw->i_atime));
+	beamfs_set_time_from_ns(inode, inode_set_mtime,
+				(s64)le64_to_cpu(raw->i_mtime));
+	beamfs_set_time_from_ns(inode, inode_set_ctime,
+				(s64)le64_to_cpu(raw->i_ctime));
+
+	/* Copy block pointers to in-memory inode */
+	memcpy(fi->i_direct, raw->i_direct, sizeof(fi->i_direct));
+	fi->i_indirect  = raw->i_indirect;
+	fi->i_dindirect = raw->i_dindirect;
+	fi->i_tindirect = raw->i_tindirect;
+	fi->i_flags     = le32_to_cpu(raw->i_flags);
+
+	/* Layer 1 defense: mark reserved inodes (canary) as immutable.
+	 * VFS rejects open(O_TRUNC), setattr(ATTR_SIZE), unlink, chmod
+	 * with EPERM before reaching beamfs hooks. Layer 2 in alloc.c
+	 * provides the safety net if VFS check is bypassed (corruption).
+	 */
+	if (beamfs_ino_is_reserved((u64)ino) && ino != BEAMFS_RESERVED_INO_ROOT)
+		inode->i_flags |= S_IMMUTABLE;
+
+	/* Set ops based on file type */
+	if (S_ISDIR(inode->i_mode)) {
+		inode->i_op  = &beamfs_dir_inode_operations;
+		inode->i_fop = &beamfs_dir_operations;
+	} else if (S_ISREG(inode->i_mode)) {
+		if (sbi->s_scheme == BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE) {
+			/*
+			 * v2 INLINE: per-block RS FEC requires gather/scatter
+			 * across 16 subblocks per disk block. Single-page
+			 * folios only; large folios deferred to v2.1.
+			 * Dedicated i_op carries setattr/truncate (sub-step 6).
+			 */
+			inode->i_op = &beamfs_inline_inode_operations;
+			inode->i_fop = &beamfs_inline_file_operations;
+			inode->i_mapping->a_ops = &beamfs_inline_aops;
+			mapping_set_folio_order_range(inode->i_mapping, 0, 0);
+		} else {
+			/* legacy iomap path (scheme=5 INODE_UNIVERSAL) */
+			inode->i_op = &beamfs_file_inode_operations;
+			inode->i_fop = &beamfs_file_operations;
+			inode->i_mapping->a_ops = &beamfs_aops;
+		}
+	} else if (S_ISLNK(inode->i_mode)) {
+		/*
+		 * Symlink: a target under 96 bytes is inline in i_direct[],
+		 * a longer one in the data block i_direct[0] names.
+		 */
+		inode->i_op = &beamfs_symlink_inode_operations;
+	} else {
+		/* Special files: use generic */
+		init_special_inode(inode, inode->i_mode, 0);
+	}
+
+	brelse(bh);
+	unlock_new_inode(inode);
+	return inode;
+}
+
+/*
+ * beamfs_getattr -- stat for every beamfs inode.
+ *
+ * st_blocks is the count of blocks the inode holds on the medium, data
+ * and indirect alike. The inode does not store it, so it is taken from
+ * the tree the first time it is asked for, and beamfs_alloc_block and
+ * beamfs_free_block keep it from there. Regular files and symlinks used
+ * simple_getattr, which counts the pages in the page cache: 0 for any
+ * file after a mount.
+ *
+ * The count is taken under the allocation mutex alone, which every path
+ * that installs or frees a block of the inode holds, a directory's
+ * included. Not the inode lock: ->getattr is called without it, and a
+ * caller of vfs_getattr that holds it would wait on itself here.
+ *
+ * A count stopped by a block of pointers that failed its check is not
+ * kept, and is taken again on the next call; the block was reported
+ * where it failed. Until a count is kept, the mask statx returns leaves
+ * STATX_BLOCKS out rather than vouch for the number.
+ */
+int beamfs_getattr(const struct mnt_idmap *idmap, const struct path *path,
+		   struct kstat *stat, u32 request_mask,
+		   unsigned int query_flags)
+{
+	struct inode *inode = d_inode(path->dentry);
+	struct beamfs_inode_info *fi = BEAMFS_I(inode);
+	bool counted;
+
+	/* Pairs with the release below: the count is in before the flag. */
+	counted = smp_load_acquire(&fi->i_blocks_counted);
+	if ((request_mask & STATX_BLOCKS) && !counted) {
+		mutex_lock(&fi->i_alloc_mutex);
+		counted = fi->i_blocks_counted;
+		if (!counted) {
+			u64 n = 0;
+			int err = beamfs_count_blocks(inode, &n);
+
+			inode_set_bytes(inode, (loff_t)n * BEAMFS_BLOCK_SIZE);
+			if (!err) {
+				/* Pairs with the acquire above. */
+				smp_store_release(&fi->i_blocks_counted, true);
+				counted = true;
+			}
+		}
+		mutex_unlock(&fi->i_alloc_mutex);
+	}
+	generic_fillattr(idmap, request_mask, inode, stat);
+	if (!counted)
+		stat->result_mask &= ~STATX_BLOCKS;
+	return 0;
+}
-- 
2.55.0


  parent reply	other threads:[~2026-10-19  4:18 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 ` [RFC PATCH 07/18] beamfs: add superblock operations Aurelien DESBRIERES
2026-10-19  4:17 ` Aurelien DESBRIERES [this message]
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-9-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

all inboxes | Powered by JetHome®