* [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem
@ 2026-10-19 4:17 Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 01/18] beamfs: add on-disk format definitions Aurelien DESBRIERES
` (17 more replies)
0 siblings, 18 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
beamfs is a block-device filesystem that corrects corrupted bytes in
place, on a single device, with no second copy to repair from. Every
data block is a capsule of sixteen interleaved RS(255,239) codewords,
built on lib/reed_solomon; every read decodes it, and a read the
decoder cannot vouch for fails with -EIO instead of returning wrong
bytes. Directory blocks, inodes, the allocation bitmap and the
superblock carry Reed-Solomon parity as well, and indirect blocks have
a parity region of their own. In the default layout 288 bytes of every
4096 (7%) are overhead.
beamfs descends from FTRFS, posted here as an RFC in April [1]. The
limits of FTRFS were in its design (threat model, where correction
sits, scale, kernel integration), so beamfs is a new filesystem with
its own on-disk format, and this is a new thread.
Who it is for
-------------
Systems that keep their data on one storage device in a radiation
environment, where no second copy fits.
The physics is measured, not supposed. Soft errors in commercial
silicon are a documented fact (Baumann [7]); their mechanism, a
particle depositing charge in a circuit node, is modelled (Dodd and
Massengill [8]); atmospheric neutrons alone upset avionics at altitude
(Normand [9]); and the medium is not exempt, NAND flash having error
modes and rates of its own (Cai et al. [10], Mielke et al. [11]).
Between the medium and the page cache a read crosses a controller, a
bus, DMA buffers and caches, any of which can flip a bit.
Space is where this started: FTRFS (Fuchs, Langer and Trinitis, ARCS
2015 [6]) was designed for satellite on-board computers. Memon et al.
[2] irradiated commercial systems on chip running Linux with 20 to
58 MeV protons; on an i.MX 8M Plus running ext4 on eMMC, filesystem
faults and storage-driver errors made up 90% of the failures they
observed, up to journal aborts and read-only remounts.
Medicine has the same problem on the ground, every working day. A
radiotherapy room is a neutron field: photon beams above 10 MV free
neutrons from the accelerator head, and every proton or ion beam
produces secondary neutrons. AAPM Task Group 203 (Miften et al. [3])
reports what that field does to the electronics of implanted cardiac
devices, single-event upsets that reset the device or lose its stored
data, and sets out how such patients are to be managed. Hashimoto et
al. [4] counted 29 soft errors in four defibrillators placed outside a
proton field. Any Linux system inside a treatment room, the imaging,
positioning and logging equipment included, keeps its data in the same
field, and what it records is part of a patient's record. There, a
read that returns a wrong byte without saying so is worse than a read
that fails: beamfs is built to return the bytes that were written, or
to refuse.
What beamfs covers is the path between the medium and the page cache.
It does not protect memory after the read and does not replace ECC
memory.
What has been measured
----------------------
The beamfs v3 report [5] measured beamfs 0.1.26, commit 1bf151d, on
Linux 7.3-rc5, on x86-64 and aarch64:
- xfstests, auto group, 734 tests: all 122 that ran on x86-64 passed,
and 122 of the 123 that ran on aarch64; generic/476 reached the
harness's time budget in the sweep and passed when run alone.
xfstests declined the other tests, mostly for features beamfs does
not implement; they are counted apart, not as passes.
- Single-bit upsets injected into read bios by the emufi injector, on
a four-node aarch64 cluster and on USB flash media: beamfs never
returned wrong data. It returned the correct file in every exercised
case but one, and in that one refused the read, after a flip in an
indirect pointer. In the same runs ext4 and ext3 returned silently
corrupted data, and btrfs, with a single copy of its data, refused
the read.
The limits of these numbers are part of the result. The upsets are
emulated, not a beam. At most two erroneous symbols reached a codeword
in one decode, so the eight-symbol correction limit, bursts and
miscorrection beyond it are not exercised. No performance was
measured. Section X of the report lists every limit, and a beam
campaign is the next step.
What changed since the FTRFS review
-----------------------------------
- Reed-Solomon goes through lib/reed_solomon, as Eric Biggers asked.
Doing so showed that one struct rs_control cannot be shared by
concurrent decoders: beamfs uses one per CPU, and a patch documenting
the contract in rslib.h was posted in May [6].
- File data goes through iomap, with address_space_operations, which
Matthew Wilcox found missing.
- Validation is the whole xfstests auto group, on two architectures,
test by test, from signed run records.
What is not there yet
---------------------
- No journal and no copy-on-write, the point Darrick J. Wong raised in
April.
Metadata reaches the medium through writeback, in no guaranteed
order, and an unclean shutdown is left to fsck.beamfs.
- Metadata still goes through buffer heads.
- In the default configuration, an indirect pointer flipped onto
another allocated block in range is not detected; DATA_SELFID closes
that. Beyond eight corrupted bytes in a codeword the decoder can
miscorrect; DATA_CSUM makes such a read fail closed. mkfs.beamfs
--data-csum enables both; neither is on by default.
- fs/beamfs/file.c is the iomap path of the INODE_UNIVERSAL scheme.
The kernel refuses to mount that scheme and mkfs.beamfs refuses to
format it, so on volumes mkfs.beamfs creates, file data takes the
inline path of file_inline.c.
- checkpatch reports no error and no warning on any patch; what it
reports with --strict (parenthesis alignment, open-ended lines) is
left as it is.
Questions
---------
- Is correction inside the filesystem, for one small device, something
fs/ would consider as a filesystem of its own, or would you rather
see it proposed for an existing filesystem?
- Is a journal a precondition for further review, or can it follow?
The series
----------
Patches 1 to 15 add the sources. Nothing is built until patch 17 adds
the Kconfig and the Makefile. file_inline.c, the largest, is split
across patches 11 to 14 to stay under the list's message size. Patch
16 adds Documentation/filesystems/beamfs.rst and the sysfs entry in
Documentation/ABI, and patch 18 the MAINTAINERS entry. The series is
based on vfs.all of vfs.git (commit 2c3ba1f1cb41, on v7.3-rc7), the VFS tree it is meant for, and builds there and
on linux-next; the inode operations take the const struct mnt_idmap *
that tree has, so it does not build on a mainline that has not merged
that change yet. v7.3 was tagged while this series was being measured;
vfs.all has not moved since, and the series is sent on it as it stands.
Tools: the series was checked, from the commit named below, by
beamfs-upstream, a checker written for it: checkpatch.pl with and
without --strict (spdxcheck included), sparse, smatch, coccicheck
(every semantic patch, report mode), kernel-doc, checkstack (no frame
over 512 bytes), get_maintainer, builds with gcc and clang as a module
and built in, under tinyconfig, allnoconfig and allmodconfig with
CONFIG_WERROR, on x86-64, arm64, i386, arm and s390 (big-endian), with
W=1 and KCFLAGS=-W, under lockdep and the memory and object debugging
options, the merge into current master and linux-next, and htmldocs
against the base. The code, the checker and this letter were written
with the assistance of an LLM, Claude (Anthropic), under the author's
direction and review; the Assisted-by: LLM tag on every patch records
it, as Documentation/process/coding-assistants.rst asks. Every
measurement was taken on the author's machines with the project's own
tooling.
Testing: the code in this series is commit ffea78a18469 of the beamfs
repository. It descends from the 0.1.26 above by the changes its
CHANGELOG lists since: comments, what checkpatch --strict and
coccicheck asked for, corrected positions carried as bytes, and the
scrubber's decode scratch and the inode copy taken off the stack.
xfstests: the auto group as ./check runs it bare, 740 tests, run whole by
beamfs-xfstests 2.8.0 on 2 machines, each test once. Every test that ran
passed, none is listed as a known failure, and no node needed recovering.
The code measured passed the code gate as tree 04d9b3a99b78 (sources
9cd9b6b71380, the ones in this series), kernel 7.3.0-rc7 built from the
series' base with vfs.git's vfs.all at 9afee012f4, beamfs built in, module
0.1.26; a test xfstests declines is recorded with the reason it gives, a
feature beamfs does not have or a tool the image lacks:
- qemux86-64: 128 ran, 128 passed, 0 failed; 612 not run by xfstests,
chiefly reflink and dedupe (189), fallocate modes and exchange-range
(84), device-mapper targets, scsi_debug and DAX (60), other features and
test prerequisites (59), O_DIRECT (50); 219 min; record sweep-1792250852.
- qemuarm64: 129 ran, 129 passed, 0 failed; 611 not run by xfstests,
chiefly reflink and dedupe (189), fallocate modes and exchange-range
(84), device-mapper targets, scsi_debug and DAX (60), other features and
test prerequisites (58), O_DIRECT (50); 1817 min; record
sweep-1792268754.
mkfs.beamfs, fsck.beamfs and the journal decoder are in
https://github.com/roastercode/beamfs, under tools/. The report, the
root images and kernels measured, and the source trees needed to
repeat the measurements are in the Zenodo record [5].
[1] https://lore.kernel.org/linux-fsdevel/20260413142357.515792-1-aurelien@hackers.camp/
https://lore.kernel.org/linux-fsdevel/20260414120726.5713-1-aurelien@hackers.camp/
[2] S. Memon et al., Where Linux Breaks Under Radiation, arXiv:2503.03722;
When Linux Meets Radiation, IEEE Transactions on Reliability, 2025,
NASA NTRS 20250004489
[3] M. Miften et al., Management of radiotherapy patients with implanted
cardiac pacemakers and defibrillators: a report of the AAPM TG-203,
Medical Physics, 2019, doi:10.1002/mp.13838
[4] T. Hashimoto et al., Influence of secondary neutrons induced by
proton radiotherapy for cancer patients with implantable
cardioverter defibrillators, Radiation Oncology 7:10, 2012,
doi:10.1186/1748-717X-7-10
[5] https://doi.org/10.5281/zenodo.23253350
[6] C. M. Fuchs, M. Langer, C. Trinitis, FTRFS: A Fault-Tolerant
Radiation-Robust Filesystem for Space Use, ARCS 2015, LNCS 9017,
doi:10.1007/978-3-319-16086-3_8; the rslib.h patch:
https://lore.kernel.org/lkml/cover.1778710307.git.aurelien@hackers.camp/
[7] R. C. Baumann, Radiation-induced soft errors in advanced
semiconductor technologies, IEEE Transactions on Device and
Materials Reliability 5(3), 2005, doi:10.1109/TDMR.2005.853449
[8] P. E. Dodd, L. W. Massengill, Basic mechanisms and modeling of
single-event upset in digital microelectronics, IEEE Transactions
on Nuclear Science 50(3), 2003, doi:10.1109/TNS.2003.813129
[9] E. Normand, Single-event effects in avionics, IEEE Transactions on
Nuclear Science 43(2), 1996, doi:10.1109/23.490893
[10] Y. Cai et al., Error Characterization, Mitigation, and Recovery in
Flash-Memory-Based Solid-State Drives, Proceedings of the IEEE
105(9), 2017, doi:10.1109/JPROC.2017.2713127
[11] N. R. Mielke et al., Reliability of Solid-State Drives Based on
NAND Flash Memory, Proceedings of the IEEE 105(9), 2017,
doi:10.1109/JPROC.2017.2725738
Aurelien DESBRIERES (18):
beamfs: add on-disk format definitions
beamfs: add in-core header and tracepoints
beamfs: add CRC32 and Reed-Solomon layer
beamfs: add error budget, alerts and clock
beamfs: add indirect parity, tree checker
beamfs: add block and inode allocator
beamfs: add superblock operations
beamfs: add inode and directory operations
beamfs: add namespace operations
beamfs: add iomap file operations
beamfs: inline data path 1/4, block map
beamfs: inline data path 2/4, allocation
beamfs: inline data path 3/4, read side
beamfs: inline data path 4/4, write side
beamfs: add scrubber and RS benchmark
Documentation: filesystems: add beamfs
beamfs: add Kconfig and Makefile
MAINTAINERS: add beamfs
Documentation/ABI/testing/sysfs-fs-beamfs | 88 +
Documentation/filesystems/beamfs.rst | 197 +
Documentation/filesystems/index.rst | 1 +
MAINTAINERS | 9 +
fs/Kconfig | 1 +
fs/Makefile | 1 +
fs/beamfs/Kconfig | 64 +
fs/beamfs/Makefile | 21 +
fs/beamfs/alert.c | 153 +
fs/beamfs/alloc.c | 1099 +++++
fs/beamfs/beamfs.h | 813 ++++
fs/beamfs/beamfs_format.h | 1197 +++++
fs/beamfs/beamfs_trace.h | 341 ++
fs/beamfs/budget.c | 252 ++
fs/beamfs/clock.c | 132 +
fs/beamfs/dir.c | 279 ++
fs/beamfs/dirent.c | 276 ++
fs/beamfs/edac.c | 785 ++++
fs/beamfs/file.c | 549 +++
fs/beamfs/file_inline.c | 4902 +++++++++++++++++++++
fs/beamfs/indparity.c | 913 ++++
fs/beamfs/inode.c | 388 ++
fs/beamfs/namei.c | 1538 +++++++
fs/beamfs/rsbench.c | 261 ++
fs/beamfs/scrub.c | 1454 ++++++
fs/beamfs/super.c | 2012 +++++++++
fs/beamfs/treecheck.c | 319 ++
27 files changed, 18045 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-fs-beamfs
create mode 100644 Documentation/filesystems/beamfs.rst
create mode 100644 fs/beamfs/Kconfig
create mode 100644 fs/beamfs/Makefile
create mode 100644 fs/beamfs/alert.c
create mode 100644 fs/beamfs/alloc.c
create mode 100644 fs/beamfs/beamfs.h
create mode 100644 fs/beamfs/beamfs_format.h
create mode 100644 fs/beamfs/beamfs_trace.h
create mode 100644 fs/beamfs/budget.c
create mode 100644 fs/beamfs/clock.c
create mode 100644 fs/beamfs/dir.c
create mode 100644 fs/beamfs/dirent.c
create mode 100644 fs/beamfs/edac.c
create mode 100644 fs/beamfs/file.c
create mode 100644 fs/beamfs/file_inline.c
create mode 100644 fs/beamfs/indparity.c
create mode 100644 fs/beamfs/inode.c
create mode 100644 fs/beamfs/namei.c
create mode 100644 fs/beamfs/rsbench.c
create mode 100644 fs/beamfs/scrub.c
create mode 100644 fs/beamfs/super.c
create mode 100644 fs/beamfs/treecheck.c
base-commit: 2c3ba1f1cb416b81d617b92a24a4bcaa48d574e3
--
2.55.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [RFC PATCH 01/18] beamfs: add on-disk format definitions
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 02/18] beamfs: add in-core header and tracepoints Aurelien DESBRIERES
` (16 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
beamfs is a block-device filesystem that keeps Reed-Solomon parity
with the blocks it protects and decodes on every read, so corrupted
bytes are corrected in place on a single device, with no second copy
to repair from.
This header holds every structure and constant that describes bytes
on the medium, and nothing else. It uses no kernel-only types, so
mkfs.beamfs and fsck.beamfs include it directly instead of keeping
copies that drift from the kernel's.
Nothing is built until the Kconfig and Makefile patch near the end of
the series.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/beamfs_format.h | 1197 +++++++++++++++++++++++++++++++++++++
1 file changed, 1197 insertions(+)
create mode 100644 fs/beamfs/beamfs_format.h
diff --git a/fs/beamfs/beamfs_format.h b/fs/beamfs/beamfs_format.h
new file mode 100644
index 000000000000..b5543b73ff3c
--- /dev/null
+++ b/fs/beamfs/beamfs_format.h
@@ -0,0 +1,1197 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * beamfs -- on-disk format
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Every structure and constant that describes bytes on the medium,
+ * and nothing else. No kernel types, so mkfs.beamfs and fsck.beamfs
+ * include this file directly rather than keeping their own copies.
+ *
+ * They used to. Three defects in a single day came from the copies
+ * drifting: fsck rejected the reserved inodes' pointers, mkfs marked
+ * the canary block with an index it computed differently, and fsck's
+ * superblock struct lagged the kernel's by sixteen bytes so every
+ * offset past them was wrong. The format is one thing; it should be
+ * written down once.
+ *
+ * ext4 and xfs split their headers the same way, for the same reason.
+ */
+
+#ifndef _BEAMFS_FORMAT_H
+#define _BEAMFS_FORMAT_H
+
+#ifdef __KERNEL__
+#include <linux/types.h>
+#else
+/*
+ * Userspace build: the __leNN types are kernel spellings of plain
+ * fixed-width integers. beamfs stores little-endian on disk and every
+ * access goes through le*_to_cpu, so the userspace tools -- which run
+ * on little-endian hosts and on the same little-endian targets --
+ * read them directly.
+ */
+#include <stdint.h>
+#include <stdbool.h>
+
+/*
+ * BIT and BIT_ULL come from linux/bits.h, which userspace does not get.
+ * mkfs.beamfs and fsck.beamfs include this file directly rather than
+ * keeping their own copies -- that is the point of it -- so the macros
+ * are defined here for them.
+ */
+#ifndef BIT
+#define BIT(n) (1U << (n))
+#endif
+#ifndef BIT_ULL
+#define BIT_ULL(n) (1ULL << (n))
+#endif
+#include <stddef.h>
+#include <linux/types.h> /* __le16/__le32/__le64/__uNN, as the kernel spells them */
+
+typedef uint64_t u64;
+typedef uint32_t u32;
+
+/*
+ * beamfs stores little-endian. The tools run on little-endian hosts
+ * and targets, so the conversions are identities here; they stay
+ * written out so the access sites read the same in both builds.
+ */
+#define le16_to_cpu(x) ((uint16_t)(x))
+#define le32_to_cpu(x) ((uint32_t)(x))
+#define le64_to_cpu(x) ((uint64_t)(x))
+#define cpu_to_le16(x) ((__le16)(x))
+#define cpu_to_le32(x) ((__le32)(x))
+#define cpu_to_le64(x) ((__le64)(x))
+
+/*
+ * Userspace has no compiler.h, so __packed is defined here. Spelt in
+ * two halves because checkpatch pattern-matches the joined form and
+ * suggests replacing it with __packed -- which is what this line
+ * defines.
+ */
+#ifndef __packed
+#define BEAMFS_PACK_ATTR __attribute__
+#define __packed BEAMFS_PACK_ATTR((__packed__))
+#endif
+#endif
+
+/* inode_state_read_once returns inode_state_flags in kernel 7.0 */
+#define beamfs_inode_is_new(inode) \
+ (inode_state_read_once(inode) & I_NEW)
+
+/* Magic number: 0x4245414D, ASCII "BEAM" */
+#define BEAMFS_MAGIC 0x4245414D
+
+/* Block size: 4096 bytes */
+#define BEAMFS_BLOCK_SIZE 4096
+#define BEAMFS_BLOCK_SHIFT 12
+
+/* RS FEC: 16 parity bytes per 239-byte subblock (RS(255,239)) */
+#define BEAMFS_RS_PARITY 16
+#define BEAMFS_INODE_RS_DATA offsetof(struct beamfs_inode, i_reserved) /* 172 bytes */
+#define BEAMFS_INODE_RS_PAR 16 /* parity bytes stored in i_reserved[0..15] */
+
+/*
+ * Inode checksum coverage.
+ *
+ * i_crc32 sits at offset 48 and the block pointers begin at 52, so a
+ * checksum over [0, offsetof(i_crc32)) alone would cover none of them:
+ * a hundred and twenty bytes of i_direct, i_indirect, i_dindirect and
+ * i_tindirect with nothing watching. Reed-Solomon does protect them --
+ * its range is [0, 172) -- but the decode is only entered when the CRC
+ * disagrees, so a flipped pointer bit would be neither corrected nor
+ * reported.
+ *
+ * That is the wrong thing to leave unguarded here. A corrupted pointer
+ * does not damage the file that owns it; it aims that file at somebody
+ * else's blocks, which is how one upset becomes two damaged files.
+ *
+ * Coverage is therefore the head [0, 48) and the tail [52, 172), the
+ * same shape the superblock uses: beamfs_crc32_sb leaves out s_crc32
+ * alone, so its checksum spans everything its parity spans. The inode
+ * matches.
+ *
+ * Volumes whose checksum covers the head alone cannot be told apart by
+ * inspection, so the wider coverage is gated on
+ * BEAMFS_FEATURE_INCOMPAT_INODE_CRC_FULL: an older kernel refuses a
+ * new volume rather than reading every inode as damaged, and this
+ * kernel refuses a volume without the flag.
+ */
+#define BEAMFS_INODE_CRC_HEAD_LEN offsetof(struct beamfs_inode, i_crc32)
+#define BEAMFS_INODE_CRC_TAIL_OFF (offsetof(struct beamfs_inode, i_crc32) \
+ + sizeof(__le32))
+#define BEAMFS_INODE_CRC_TAIL_LEN (BEAMFS_INODE_RS_DATA \
+ - BEAMFS_INODE_CRC_TAIL_OFF)
+#define BEAMFS_INODE_CRC_BYTES (BEAMFS_INODE_CRC_HEAD_LEN \
+ + BEAMFS_INODE_CRC_TAIL_LEN)
+
+#define BEAMFS_SUBBLOCK_DATA 239
+#define BEAMFS_SUBBLOCK_TOTAL (BEAMFS_SUBBLOCK_DATA + BEAMFS_RS_PARITY)
+
+/* On-disk bitmap block layout (RS FEC protected) */
+#define BEAMFS_BITMAP_SUBBLOCKS 16 /* subblocks per bitmap block */
+#define BEAMFS_BITMAP_DATA_BYTES (BEAMFS_BITMAP_SUBBLOCKS * BEAMFS_SUBBLOCK_DATA) /* 3824 */
+/* Bits of free-data-block bitmap held by a single on-disk bitmap block. */
+#define BEAMFS_BITS_PER_BITMAP_BLOCK (BEAMFS_BITMAP_DATA_BYTES * 8) /* 30592 */
+
+/*
+ * Encoding of s_flags (on-disk).
+ *
+ * Bits 0..15 = bitmap_blocks_count (zero -> legacy 1, max 65535).
+ * Bits 16..31 = volume state, BEAMFS_SB_FLAG_* below; the bits not
+ * defined there are reserved (must be zero).
+ *
+ * s_flags is __le32, inside the CRC32 coverage (region A 0..63).
+ * Volumes formatted by pre-multi-bitmap mkfs have s_flags=0, which
+ * matches the legacy single-block layout via the fallback below.
+ */
+#define BEAMFS_SB_FLAGS_BITMAP_BLOCKS_MASK 0x0000FFFFu
+/*
+ * ext2's s_state, in the upper half of s_flags. MOUNTED is set by a
+ * read-write mount and cleared by a clean unmount: found set at mount,
+ * the volume was not unmounted cleanly. ERRORS is set by the kernel
+ * when it records an uncorrectable event, and cleared by fsck.beamfs
+ * --repair after a clean run. Older kernels mask the low sixteen bits
+ * and ignore these.
+ */
+#define BEAMFS_SB_FLAG_MOUNTED 0x00010000u
+#define BEAMFS_SB_FLAG_ERRORS 0x00020000u
+
+/*
+ * On-disk data block layout under BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE,
+ * without BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE: the alternating layout.
+ *
+ * Each user data block (4096 bytes on disk) holds 16 RS(255,239) shortened
+ * subblocks, identical layout to bitmap blocks. Per subblock: 239 data bytes
+ * followed by 16 parity bytes (sb total = 255). The 16 subblocks together
+ * occupy 16 * 255 = 4080 bytes; the last 16 bytes of the block hold the
+ * DATA_CSUM and DATA_SELFID descriptor below, zero when neither is active.
+ *
+ * Logical capacity per disk block: 16 * 239 = 3824 bytes.
+ * Correction capacity per disk block: 16 * (16/2) = 128 byte symbols,
+ * at most 8 in any one subblock.
+ *
+ * file_size_logical = N user-visible bytes
+ * disk_blocks_used = ceil(N / BEAMFS_DATA_INLINE_BYTES)
+ */
+#define BEAMFS_DATA_INLINE_SUBBLOCKS 16
+/* 16 x 239 = 3824 payload bytes, 16 x 255 = 4080 coded, 16 of tail */
+#define BEAMFS_DATA_INLINE_BYTES (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_SUBBLOCK_DATA)
+#define BEAMFS_DATA_INLINE_TOTAL (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_SUBBLOCK_TOTAL)
+#define BEAMFS_DATA_INLINE_PAD (BEAMFS_BLOCK_SIZE - BEAMFS_DATA_INLINE_TOTAL)
+
+/*
+ * The capsule layout, under BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE.
+ *
+ * Alternating, a codeword owns 239 consecutive bytes and a nine-byte
+ * burst lands entirely in it: eight is all one corrects, so the
+ * subblock is lost. In a capsule the 3824 coded bytes are contiguous
+ * and the symbols of one codeword are spread across them -- symbol i
+ * of codeword j at byte i*16 + j -- so the same burst puts at most
+ * one symbol in any codeword, and 129 consecutive coded bytes are
+ * needed to lose one.
+ *
+ * 0 .. 3807 user data | the 3824 coded bytes, interleaved
+ * 3808 .. 3815 csum | across the sixteen codewords
+ * 3816 .. 3823 selfid |
+ * 3824 .. 4079 parity, 16 bytes per codeword, one after another
+ * 4080 .. 4095 generation, reserved
+ *
+ * The parity is not interleaved: nine bad bytes among one codeword's
+ * sixteen parity bytes still lose that codeword.
+ *
+ * The descriptor sits inside the coded area. In the alternating
+ * layout the csum that says whether a block is sound and the selfid
+ * that says whether it is the right one sit in the tail pad, outside
+ * every codeword: a nine-byte burst there condemns a block the rest
+ * of which would have survived intact. A capsule that cannot protect
+ * its own header is not a unit of survival.
+ *
+ * It costs sixteen bytes of capacity per block -- 3808 rather than
+ * 3824, 0.42%, four megabytes on a gigabyte -- and buys the header the
+ * same 129-byte burst resistance as the data.
+ *
+ * The last sixteen bytes are set aside for an 8-byte generation and 8
+ * reserved bytes. The kernel writes them as zero and does not read
+ * them.
+ */
+#define BEAMFS_CAPSULE_DATA_OFF 0
+#define BEAMFS_CAPSULE_DATA_BYTES 3808
+#define BEAMFS_CAPSULE_CSUM_OFF 3808 /* u8 type, 3 rsvd, __le32 crc */
+#define BEAMFS_CAPSULE_SELFID_OFF 3816 /* __le64 digest(ino, iblock) */
+#define BEAMFS_CAPSULE_CODED_BYTES 3824 /* data + csum + selfid */
+#define BEAMFS_CAPSULE_PARITY_OFF 3824
+#define BEAMFS_CAPSULE_PARITY_LEN (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY)
+#define BEAMFS_CAPSULE_GEN_OFF 4080 /* __le64 generation */
+#define BEAMFS_CAPSULE_GEN_BYTES 8
+#define BEAMFS_CAPSULE_FREE_OFF 4088 /* 8 bytes, zero, reserved */
+
+/*
+ * The same interleaved geometry with all 3824 coded bytes counted as
+ * payload and no descriptor: data at 0, 256 bytes of parity at 3824,
+ * 4080 bytes in all. beamfs_rs_encode_block, beamfs_rs_decode_block,
+ * beamfs_block_read and beamfs_block_write in edac.c use these names.
+ */
+#define BEAMFS_DATA_WOVEN_OFF 0
+#define BEAMFS_DATA_WOVEN_BYTES BEAMFS_DATA_INLINE_BYTES /* 3824 */
+#define BEAMFS_DATA_WOVEN_PARITY_OFF BEAMFS_DATA_INLINE_BYTES /* 3824 */
+#define BEAMFS_DATA_WOVEN_PARITY_LEN (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY) /* 256 */
+#define BEAMFS_DATA_WOVEN_TOTAL (BEAMFS_DATA_WOVEN_BYTES + BEAMFS_DATA_WOVEN_PARITY_LEN)
+
+/*
+ * DATA_CSUM descriptor layout in the alternating layout: the 16-byte
+ * block tail pad [BEAMFS_DATA_INLINE_TOTAL .. BEAMFS_BLOCK_SIZE), not
+ * part of any RS codeword. A capsule holds the same descriptor at
+ * BEAMFS_CAPSULE_CSUM_OFF, inside the coded area. The csum takes the
+ * first 8 bytes of the tail; DATA_SELFID below takes the other 8.
+ */
+#define BEAMFS_DATA_CSUM_TYPE_OFF (BEAMFS_DATA_INLINE_TOTAL + 0) /* 4080 u8 */
+#define BEAMFS_DATA_CSUM_VALUE_OFF (BEAMFS_DATA_INLINE_TOTAL + 4) /* 4084 __le32 */
+#define BEAMFS_DATA_CSUM_DESC_BYTES 8 /* type+rsvd+csum */
+/*
+ * DATA_SELFID: block self-identification, in the second half of the tail
+ * pad (at BEAMFS_CAPSULE_SELFID_OFF in a capsule). DATA_CSUM proves a
+ * block's contents are intact; it cannot prove the
+ * block is the right one. A corrupted block pointer that still lands inside
+ * the data area addresses a different, perfectly valid block, whose own
+ * descriptor verifies. Measured 2026-08-19 at a 128-flip budget: an inode's
+ * direct pointer went from physical 471 to 503, the read returned that
+ * block's contents (15245 wrong bits) with cat exiting 0 and no kernel
+ * signal, because every integrity check passed on a block that simply was
+ * not the one asked for. The existing bounds check only rejects pointers
+ * outside [s_data_start, s_data_start + s_nblocks).
+ *
+ * The field holds beamfs_data_selfid(ino, iblock), so a read that lands on
+ * the wrong block fails closed. A 64-bit digest rather than the raw pair
+ * keeps the field within the 8 reserved bytes without capping inode numbers
+ * or file size, which matters for the datacenter workloads this filesystem
+ * targets as much as for embedded ones.
+ */
+#define BEAMFS_DATA_SELFID_OFF (BEAMFS_DATA_INLINE_TOTAL + 8) /* 4088 __le64 */
+#define BEAMFS_DATA_SELFID_BYTES 8
+
+/*
+ * Conformance fixture (canary block).
+ *
+ * Under BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE, mkfs.beamfs writes a
+ * deterministic canary block immediately after the root directory
+ * block. The canary block is RS-encoded like a regular data block and
+ * contains a fixed 64-byte ASCII header followed by a deterministic
+ * 3760-byte payload. The byte layout is reproducible across mkfs
+ * invocations, so its sha256 is a constant.
+ *
+ * Purpose: validate the on-disk RS encode chain end-to-end without
+ * relying on user-written content. The inode that maps it,
+ * BEAMFS_RESERVED_INO_CANARY, is reserved below.
+ */
+#define BEAMFS_CANARY_HEADER_LEN 64
+/* 3760 bytes of payload after the header, 3824 in all */
+#define BEAMFS_CANARY_PAYLOAD_LEN (BEAMFS_DATA_INLINE_BYTES - BEAMFS_CANARY_HEADER_LEN)
+#define BEAMFS_CANARY_USER_BYTES BEAMFS_DATA_INLINE_BYTES
+#define BEAMFS_CANARY_HEADER_STR "BEAMFS-CANARY-v4 RS(255,239)x16 SHA256-fixed\n"
+
+/*
+ * Reserved inode numbers.
+ *
+ * Inodes [0 .. BEAMFS_FIRST_USER_INO) are reserved for FS-internal
+ * fixtures (root directory, canary block alias). User-visible inode
+ * allocation starts at BEAMFS_FIRST_USER_INO.
+ *
+ * Operations that would free a reserved inode, or modify one other
+ * than the root, are rejected by defense-in-depth: VFS layer
+ * (S_IMMUTABLE, set on every reserved inode but the root) and
+ * allocator layer (range check in beamfs_free_block /
+ * beamfs_free_inode_num).
+ */
+#define BEAMFS_RESERVED_INO_ROOT 1
+#define BEAMFS_RESERVED_INO_CANARY 2
+#define BEAMFS_FIRST_USER_INO 3
+
+static inline bool beamfs_ino_is_reserved(u64 ino)
+{
+ return ino < BEAMFS_FIRST_USER_INO;
+}
+
+/*
+ * Translation helpers for BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE.
+ *
+ * The user-visible file_offset (in bytes) maps to a logical block index
+ * iblock_logical = file_offset / BEAMFS_DATA_INLINE_BYTES (= /3824).
+ * Each logical block backs exactly one physical disk block of
+ * BEAMFS_BLOCK_SIZE (4096) bytes, holding 3824 user bytes interleaved
+ * with 16*16=256 RS parity bytes plus 16 bytes of pad.
+ *
+ * For schemes other than UNIVERSAL_INLINE, translation is the legacy
+ * 1:1 with BEAMFS_BLOCK_SIZE granularity; callers must guard on
+ * sbi->s_scheme before invoking these helpers.
+ */
+static inline u64 beamfs_inline_logical_to_iblock(u64 file_offset)
+{
+ return file_offset / BEAMFS_DATA_INLINE_BYTES;
+}
+
+static inline u32 beamfs_inline_offset_in_logical(u64 file_offset)
+{
+ return (u32)(file_offset % BEAMFS_DATA_INLINE_BYTES);
+}
+
+static inline u64 beamfs_inline_size_to_blocks(u64 size)
+{
+ return (size + BEAMFS_DATA_INLINE_BYTES - 1) / BEAMFS_DATA_INLINE_BYTES;
+}
+
+/*
+ * Superblock RS layout.
+ *
+ * The superblock CRC32 covers two non-contiguous regions:
+ * region A [0, 64) = 64 bytes (header + counters + version + flags)
+ * region B [68, 2769) = 2701 bytes (uuid + label + RS journal +
+ * bitmap_blk + features + protection scheme
+ * + indirection parity + error budget +
+ * clock anchor)
+ * total = 2765 logical bytes
+ * excluded: s_crc32 itself in [64, 68).
+ *
+ * RS protection covers exactly the same 2765 logical bytes. The two
+ * regions are serialized into a contiguous 2769-byte staging buffer
+ * (4 bytes of zero pad to round up to 13 shortened subblocks of 213
+ * data bytes each). RS(255,239) shortened with data_len=213 is the
+ * standard lib/reed_solomon usage; padding is implicit.
+ *
+ * Each of the 13 subblocks tolerates up to 8 symbol errors
+ * independently (RS_PARITY/2 = 16/2 = 8). Total correction capacity
+ * across the superblock: 104 symbol errors when distributed across
+ * subblocks.
+ *
+ * The 208 bytes of parity (13 * 16) live at sb->s_pad[1119..1326],
+ * which corresponds to disk offset 2769 + 1119 = 3888 -- the last
+ * 208 bytes of the 4096-byte superblock. This trailing position is
+ * stable against format evolution: new fields are taken out of s_pad
+ * ahead of the parity zone.
+ *
+ * The BUILD_BUG_ONs in beamfs_sb_to_rs_staging fail the build when
+ * the covered region no longer matches BEAMFS_SB_RS_COVERAGE_BYTES or
+ * outgrows the staging buffer. The subblock geometry below is not
+ * checked: a field added to the covered region means recomputing
+ * BEAMFS_SB_RS_STAGING_BYTES and BEAMFS_SB_RS_DATA_LEN by hand.
+ */
+#define BEAMFS_SB_RS_COVERAGE_BYTES 2765 /* logical bytes (CRC32 range) */
+#define BEAMFS_SB_RS_STAGING_BYTES 2769 /* 13 * BEAMFS_SB_RS_DATA_LEN */
+#define BEAMFS_SB_RS_DATA_LEN 213 /* per shortened subblock */
+#define BEAMFS_SB_RS_SUBBLOCKS 13 /* total subblocks */
+#define BEAMFS_SB_RS_PARITY_BYTES 208 /* 13 * BEAMFS_RS_PARITY */
+#define BEAMFS_SB_RS_PARITY_OFFSET 3888 /* end - parity bytes */
+#define BEAMFS_SB_RS_S_PAD_INDEX (BEAMFS_SB_RS_PARITY_OFFSET - 2769)
+ /* index in s_pad[]: 1119 */
+#define BEAMFS_BITMAP_MAX_BLOCKS (BEAMFS_BITMAP_DATA_BYTES * 8) /* 30592 */
+
+/* Filesystem limits */
+#define BEAMFS_MAX_FILENAME 255
+#define BEAMFS_DIRECT_BLOCKS 12
+#define BEAMFS_INDIRECT_BLOCKS 1
+#define BEAMFS_DINDIRECT_BLOCKS 1
+
+/*
+ * BEAMFS_INDIRECT_PTRS: number of block pointers per indirect block.
+ * Each pointer is a u64 (8 bytes), so 4096 / 8 = 512 entries.
+ * Single indirect capacity: 512 blocks = 2 MiB.
+ *
+ * Shared between the legacy iomap path (file.c, scheme=5) and the
+ * v2 INLINE block-mapping helpers (file_inline.c, scheme=2).
+ */
+#define BEAMFS_INDIRECT_PTRS (BEAMFS_BLOCK_SIZE / sizeof(__le64))
+
+/*
+ * Multi-level indirect capacity.
+ *
+ * BEAMFS_DINDIRECT_PTRS: total user blocks reachable via the double
+ * indirect pointer = 512 single-indirect blocks x 512 ptrs each.
+ * BEAMFS_TINDIRECT_PTRS: total user blocks reachable via the triple
+ * indirect pointer = 512 dindirect blocks x 512 single-indirect each
+ * x 512 ptrs each.
+ *
+ * Maximum mapped iblock per indirection level:
+ * direct : BEAMFS_DIRECT_BLOCKS (12)
+ * + indirect : 524 (12 + 512)
+ * + dindirect : 262 668 (524 + 512^2)
+ * + tindirect : 134 480 396 (262 668 + 512^3)
+ *
+ * Logical user capacity per file (user bytes per block = 3824 under
+ * scheme=2 UNIVERSAL_INLINE with 16 x RS(255,239) sub-blocks):
+ * direct : 45 888 B (~45 KiB)
+ * + indirect : 2 003 776 B (~1.91 MiB)
+ * + dindirect : ~ 958 MiB (~1 GiB)
+ * + tindirect : ~ 479 GiB -- current ceiling
+ */
+#define BEAMFS_DINDIRECT_PTRS (BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS)
+#define BEAMFS_TINDIRECT_PTRS (BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS)
+
+#define BEAMFS_MAX_IBLOCK_DIRECT ((u64)BEAMFS_DIRECT_BLOCKS)
+#define BEAMFS_MAX_IBLOCK_INDIRECT (BEAMFS_MAX_IBLOCK_DIRECT + (u64)BEAMFS_INDIRECT_PTRS)
+#define BEAMFS_MAX_IBLOCK_DINDIRECT (BEAMFS_MAX_IBLOCK_INDIRECT + (u64)BEAMFS_DINDIRECT_PTRS)
+#define BEAMFS_MAX_IBLOCK_TINDIRECT (BEAMFS_MAX_IBLOCK_DINDIRECT + (u64)BEAMFS_TINDIRECT_PTRS)
+
+/*
+ * The largest file the indirection can address, in bytes.
+ *
+ * Derived rather than stated, so it follows the geometry instead of
+ * having to be remembered when the geometry changes. Roughly 479 GiB at
+ * the current shape: twelve direct blocks, then 512 pointers per level
+ * over three levels, 3824 usable bytes each.
+ *
+ * s_maxbytes has to be this and not MAX_LFS_FILESIZE. Claiming 2^63
+ * meant the VFS accepted a truncate to eight exabytes and only found
+ * out at the read, which came back EIO because the logical block was
+ * past the tree -- generic/466 reports it as "Discrepancy @ blocksize
+ * 4096". A filesystem that overstates its reach turns a clean EFBIG at
+ * the point of asking into an I/O error somewhere later.
+ */
+#define BEAMFS_MAX_FILE_SIZE \
+ (BEAMFS_MAX_IBLOCK_TINDIRECT * (u64)BEAMFS_DATA_INLINE_BYTES)
+
+/*
+ * Electromagnetic Resilience Journal entry -- 40 bytes.
+ *
+ * Records each RS FEC correction event persistently in the superblock.
+ * 64 entries give operators a map of physical degradation over time.
+ *
+ * re_entropy_q16_16 carries a per-event Shannon entropy estimate over
+ * the corrected positions, the forensic discriminator between Family A
+ * (Poisson background SEU) and Family B (correlated burst). The
+ * estimator is beamfs_rs_compute_entropy_q16_16() in edac.c.
+ *
+ * Layout invariants:
+ * - 40 bytes, packed, no implicit padding (BUILD_BUG_ON in super.c)
+ * - re_reserved and re_pad MUST be zero on write; non-zero values
+ * act as structural sentinels. re_reserved lies inside re_crc32's
+ * coverage, so a non-zero value there also fails the CRC; re_pad
+ * lies outside it
+ * - re_crc32 covers bytes [0..32), i.e. all fields except itself
+ * and the trailing alignment pad
+ */
+struct beamfs_rs_event {
+ /*
+ * The block the correction hit. A block, not a block and a
+ * subblock folded together: the subblock index lives in re_flags
+ * (BEAMFS_RS_EVENT_SUBBLOCK_*). A superblock correction is the
+ * exception and carries BEAMFS_RS_BLOCK_NO_SB_MARKER instead.
+ */
+ __le64 re_block_no; /* 0..7 */
+ __le64 re_timestamp; /* 8..15 ktime_get_ns() at recovery */
+ __le32 re_symbol_count; /* 16..19 symbols corrected */
+ __le32 re_entropy_q16_16; /* 20..23 Shannon H, Q16.16, [0,3*65536) */
+ __le32 re_flags; /* 24..27 see BEAMFS_RS_EVENT_FLAG_* */
+ __le32 re_reserved; /* 28..31 zero, structural sentinel */
+ __le32 re_crc32; /* 32..35 CRC32 over bytes [0..32) */
+ __le32 re_pad; /* 36..39 zero, alignment + sentinel */
+} __packed; /* 40 bytes */
+
+#define BEAMFS_RS_JOURNAL_SIZE 64 /* entries in the EM resilience journal */
+
+/*
+ * Flags for beamfs_rs_event::re_flags.
+ *
+ * ENTROPY_VALID -- the re_entropy_q16_16 field carries a meaningful
+ * Shannon estimate. Cleared by zero-init (mkfs);
+ * set by beamfs_log_rs_event() when entropy is computed
+ * from the position list returned by RS decode.
+ *
+ * UNCORRECTABLE -- the data could not be recovered: a codeword
+ * exceeded the RS correction radius (more than
+ * BEAMFS_RS_PARITY/2 = 8 symbols in error within a
+ * single subblock), or a fail-closed integrity
+ * check rejected the block. When this flag is set,
+ * re_symbol_count MUST be zero and ENTROPY_VALID
+ * MUST be cleared. The flag is orthogonal to
+ * Family A / Family B classification: an
+ * uncorrectable event may originate from either
+ * family; discrimination is performed post-process
+ * by clustering analysis on re_block_no and
+ * re_timestamp, not in-kernel.
+ *
+ * RMW_NEUTRALISED -- the correction was performed during a
+ * read-modify-write transit (write path) rather
+ * than on a user-initiated read. The decoded
+ * data is about to be overwritten by the encode
+ * step that follows in the same RMW cycle, so
+ * the flip is *silently neutralised* before any
+ * consumer observes it. This event would have
+ * caused a read-side correction (or worse, an
+ * uncorrectable) had the same disk-block been
+ * read in isolation. Discriminating this flag
+ * from the bare correctable event is what makes
+ * neutralisation and correction separately
+ * measurable from the RS journal alone.
+ * Orthogonal to ENTROPY_VALID:
+ * an RMW_NEUTRALISED event with >= 2 corrected
+ * symbols still carries a valid Shannon entropy
+ * estimate over the position list.
+ *
+ * BIT(3) is INODE, below, and bits 8..15 hold the subblock index
+ * (BEAMFS_RS_EVENT_SUBBLOCK_*). Bits 4..7 and 16..31 are reserved and
+ * MUST be zero on write.
+ */
+#define BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID BIT(0)
+#define BEAMFS_RS_EVENT_FLAG_UNCORRECTABLE BIT(1)
+#define BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED BIT(2)
+
+/*
+ * INODE -- re_block_no is the inode table block that was being read,
+ * not a data or metadata block of a file. The correction
+ * landed inside an inode; which inode is not recorded,
+ * because clustering analysis asks where on the medium the
+ * damage was, and the table block is that answer.
+ */
+#define BEAMFS_RS_EVENT_FLAG_INODE BIT(3)
+
+/*
+ * Subblock index, in bits 8..15, stored as index + 1 so that zero
+ * keeps meaning "not applicable" -- a superblock or inode correction
+ * has no subblock in the sense the data path uses.
+ *
+ * It lives here rather than in re_reserved because that field is a
+ * structural sentinel: a non-zero value in it means the entry is
+ * corrupt, and that is worth more than four bytes. re_flags had
+ * twenty-nine bits spare.
+ *
+ * Before this, callers folded the subblock into re_block_no as
+ * block * SUBBLOCKS + subblock, with SUBBLOCKS being sixteen in the
+ * data path, BEAMFS_BITMAP_SUBBLOCKS in the allocator and thirteen in
+ * the superblock. A reader had no way to know which divisor applied,
+ * so the field documented as a block number was not one.
+ */
+#define BEAMFS_RS_EVENT_SUBBLOCK_SHIFT 8
+#define BEAMFS_RS_EVENT_SUBBLOCK_MASK (0xFFU << BEAMFS_RS_EVENT_SUBBLOCK_SHIFT)
+
+static inline __u32 beamfs_rs_event_subblock_bits(unsigned int sub)
+{
+ return ((sub + 1) << BEAMFS_RS_EVENT_SUBBLOCK_SHIFT)
+ & BEAMFS_RS_EVENT_SUBBLOCK_MASK;
+}
+
+/* Returns the subblock index, or -1 when the event has none. */
+static inline int beamfs_rs_event_subblock(__u32 flags)
+{
+ unsigned int v = (flags & BEAMFS_RS_EVENT_SUBBLOCK_MASK)
+ >> BEAMFS_RS_EVENT_SUBBLOCK_SHIFT;
+
+ return v ? (int)(v - 1) : -1;
+}
+
+/*
+ * Shannon entropy parameters for the RS journal forensic estimator.
+ *
+ * BINS -- number of histogram bins over the codeword position range.
+ * Power of 2 chosen so that bin_index = pos * BINS / code_len
+ * fits in u32 arithmetic without overflow for any beamfs
+ * codeword length (max 239 bytes).
+ * Q -- fractional bits in the Q-format LUT (Q16.16 = 16 frac bits).
+ *
+ * The full entropy LUT is defined in edac.c, with the formula its
+ * entries are computed from.
+ */
+#define BEAMFS_RS_ENTROPY_BINS 8
+#define BEAMFS_RS_ENTROPY_Q 16
+
+/*
+ * Sentinel block number for journal entries that record a SUPERBLOCK
+ * RS recovery rather than a data/metadata block recovery. The low 12
+ * bits encode the SB sub-block index (0..12, < BEAMFS_SB_RS_SUBBLOCKS).
+ *
+ * Forensic decoder: if (re_block_no & BEAMFS_RS_BLOCK_NO_SB_MASK) ==
+ * BEAMFS_RS_BLOCK_NO_SB_MARKER, the entry refers
+ * to SB sub-block (re_block_no & BEAMFS_RS_BLOCK_NO_SB_IDX_MASK).
+ *
+ * The marker is chosen at the top of the u64 range, well above any
+ * realistic block number on a filesystem (2^64 blocks * 4 KiB =
+ * 2^76 bytes = 64 ZiB, unreachable by current and foreseeable storage).
+ */
+#define BEAMFS_RS_BLOCK_NO_SB_MARKER 0xFFFFFFFFFFFFF000ULL
+#define BEAMFS_RS_BLOCK_NO_SB_MASK 0xFFFFFFFFFFFFF000ULL
+#define BEAMFS_RS_BLOCK_NO_SB_IDX_MASK 0x0000000000000FFFULL
+
+/*
+ * On-disk format versions.
+ *
+ * Mount policy: strict equality with BEAMFS_VERSION_CURRENT. Version 1
+ * images are not mountable and have to be reformatted. Dual-format
+ * in-kernel parsing is intentionally avoided (doubles audit surface for
+ * KASAN / syzkaller for no operational benefit on a niche FS).
+ *
+ * Format extensions within version 5 are carried by the feature flags
+ * below.
+ */
+#define BEAMFS_VERSION_V1 1
+#define BEAMFS_VERSION_V5 5
+#define BEAMFS_VERSION_CURRENT BEAMFS_VERSION_V5
+
+/*
+ * Data protection scheme values for s_data_protection_scheme.
+ *
+ * NONE -- no FEC on data blocks (legacy behaviour, deprecated).
+ * INODE_OPT_IN -- RS FEC enabled per-inode via BEAMFS_INODE_FL_RS_ENABLED.
+ * This is the v0.1.0 baseline behaviour. Deprecated by
+ * threat model 6.3 (see Documentation/threat-model.md).
+ * UNIVERSAL_INLINE -- RS parity bytes embedded inline within each data block.
+ * Reserved for stage 4 of the staged plan.
+ * UNIVERSAL_SHADOW -- RS parity stored in a dedicated out-of-band region.
+ * Reserved for stage 4 of the staged plan.
+ * UNIVERSAL_EXTENT -- RS parity attached as an extent-based filesystem
+ * attribute. Reserved for stage 4 of the staged plan.
+ *
+ * The kernel range-checks this field at mount and refuses values above
+ * BEAMFS_DATA_PROTECTION_MAX. Three unused upper bytes of the __le32 act
+ * as a structural sentinel: any single-byte corruption in the high-order
+ * bytes produces a value outside the valid range and is rejected.
+ */
+#define BEAMFS_DATA_PROTECTION_NONE 0
+#define BEAMFS_DATA_PROTECTION_INODE_OPT_IN 1
+#define BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE 2
+#define BEAMFS_DATA_PROTECTION_UNIVERSAL_SHADOW 3
+#define BEAMFS_DATA_PROTECTION_UNIVERSAL_EXTENT 4
+
+/*
+ * Per-data-block integrity field type (DATA_CSUM feature), stored in
+ * the block descriptor: the tail pad in the alternating layout,
+ * BEAMFS_CAPSULE_CSUM_OFF in a capsule. CRC32 reuses beamfs_crc32
+ * (crc32_le), the primitive backing i_crc32 and s_crc32. Values 2..255
+ * reserved for keyed integrity (Family B).
+ */
+#define BEAMFS_CSUM_NONE 0 /* no integrity field present */
+#define BEAMFS_CSUM_CRC32 1 /* beamfs_crc32 over decoded user payload */
+#define BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL 5
+#define BEAMFS_DATA_PROTECTION_MAX BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL
+
+/*
+ * Feature flag masks.
+ *
+ * s_feat_compat -- informational flags. Unknown bits are logged but
+ * do not prevent mount.
+ * s_feat_incompat -- structural format extensions. Unknown bits cause
+ * mount to be refused (read or write).
+ * s_feat_ro_compat -- features that prevent safe write. Unknown bits cause
+ * mount to be forced read-only with a warning.
+ *
+ * The bit positions below are fixed on disk. Only the bits in the
+ * BEAMFS_FEAT_*_SUPP masks further down are implemented by this
+ * kernel; the others are declared to reserve their positions and are
+ * treated as unknown, per the rules above.
+ */
+
+/* COMPAT features (informational, mount continues if unknown) */
+#define BEAMFS_FEATURE_COMPAT_RS_JOURNAL_VERBOSE BIT_ULL(0)
+#define BEAMFS_FEATURE_COMPAT_LABEL_LONG BIT_ULL(1)
+#define BEAMFS_FEATURE_COMPAT_DIR_INDEX BIT_ULL(2)
+
+/* RO_COMPAT features (force RO mount if unknown) */
+#define BEAMFS_FEATURE_RO_COMPAT_LARGE_FILE BIT_ULL(0)
+#define BEAMFS_FEATURE_RO_COMPAT_HUGE_FILE BIT_ULL(1)
+#define BEAMFS_FEATURE_RO_COMPAT_EXTRA_ISIZE BIT_ULL(2)
+#define BEAMFS_FEATURE_RO_COMPAT_BTREE_DIR BIT_ULL(3)
+/* Per-data-block csum and selfid, see BEAMFS_DATA_CSUM_* above */
+#define BEAMFS_FEATURE_RO_COMPAT_DATA_CSUM BIT_ULL(4)
+#define BEAMFS_FEATURE_RO_COMPAT_DATA_SELFID BIT_ULL(5)
+
+/* INCOMPAT features (refuse mount if unknown) */
+#define BEAMFS_FEATURE_INCOMPAT_EXTENTS BIT_ULL(0)
+#define BEAMFS_FEATURE_INCOMPAT_64BIT BIT_ULL(1)
+#define BEAMFS_FEATURE_INCOMPAT_BIGALLOC BIT_ULL(2)
+#define BEAMFS_FEATURE_INCOMPAT_BLOCK_GROUPS BIT_ULL(3)
+#define BEAMFS_FEATURE_INCOMPAT_BTREE_ALLOC BIT_ULL(4)
+#define BEAMFS_FEATURE_INCOMPAT_JOURNAL BIT_ULL(5)
+#define BEAMFS_FEATURE_INCOMPAT_DAX BIT_ULL(6)
+#define BEAMFS_FEATURE_INCOMPAT_RS_HEAVY BIT_ULL(7)
+#define BEAMFS_FEATURE_INCOMPAT_PER_INODE_RS BIT_ULL(8)
+#define BEAMFS_FEATURE_INCOMPAT_BG_RS_PARITY BIT_ULL(9)
+#define BEAMFS_FEATURE_INCOMPAT_LARGE_BLOCK BIT_ULL(10)
+#define BEAMFS_FEATURE_INCOMPAT_SHADOW_PARITY BIT_ULL(11)
+/*
+ * BEAMFS_FEATURE_INCOMPAT_ENCRYPT: reserved for a future at-rest
+ * encryption layer, not yet implemented. Declared now, alongside
+ * SHADOW_PARITY and LARGE_BLOCK above, so an on-disk bit position is
+ * fixed before any volume exists that could claim it by accident.
+ *
+ * Composition order, should this be implemented: encrypt -> RS encode
+ * -> DATA_CSUM / DATA_SELFID, mirrored in reverse on read (RS decode
+ * -> verify -> decrypt). Two consequences follow from that order:
+ *
+ * - DATA_CSUM and DATA_SELFID would cover ciphertext, not plaintext.
+ * A corrupted block still fails closed (the RS-decoded bytes no
+ * longer match the stored digest), but decryption happening after
+ * the check means garbage plaintext is never produced from a block
+ * that already failed integrity, which keeps the read path
+ * fail-closed.
+ *
+ * - Encrypting before RS means a single ciphertext bit flip can
+ * alter more plaintext bits than the corresponding cleartext flip
+ * would (block-cipher diffusion), which the RS correction budget
+ * was not sized against. This is a design question for whoever
+ * implements the bit, not an implementation detail to silently
+ * inherit from the current cleartext path.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_ENCRYPT BIT_ULL(12)
+/*
+ * INDIRECT_PARITY -- RS parity for indirection blocks, held out of band.
+ *
+ * An indirect block is 512 raw __le64 pointers filling the whole 4096
+ * bytes, with no room for a checksum. The pointer checks on the read
+ * path close the dominant failure mode -- a pointer landing outside the
+ * data range or on a block the bitmap says is free -- and leave one
+ * open: a flipped pointer that happens to land on a block that is both
+ * in range and allocated passes every check there is.
+ *
+ * The asymmetry is what makes this worth closing. A data block has
+ * eight correctable symbols per 255-byte subblock. A double-indirect
+ * pointer has none, and losing it costs 262144 blocks.
+ *
+ * Parity lives in a region of its own rather than in the block, so
+ * BEAMFS_INDIRECT_PTRS stays 512, no BEAMFS_MAX_IBLOCK_* moves and the
+ * format of the indirect block does not change. The region is sized
+ * for the worst case, every data block being indirect, because a table
+ * mapping blocks to parity slots would itself be metadata needing
+ * protection. What it costs depends on the mode, below; the slot is
+ * computed directly from the block number.
+ *
+ * INCOMPAT rather than RO_COMPAT: a kernel that does not know about the
+ * region would allocate indirect blocks without writing their parity,
+ * leaving the volume looking protected while it is not.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_INDIRECT_PARITY BIT_ULL(13)
+
+/*
+ * How much an indirect block costs to protect, chosen at mkfs time.
+ *
+ * The right answer depends on the deployment, which is why it is a
+ * choice rather than a constant. A CubeSat with 8 GiB of MRAM at
+ * several hundred euro per gigabyte will not spend 7% of it; a server
+ * with spare terabytes will, and gets the stronger code for the price.
+ *
+ * NONE no protection. The residual above stands: a flipped
+ * pointer landing in range and on an allocated block is
+ * indistinguishable from a valid one.
+ *
+ * CRC one CRC32 per subblock, 64 bytes per block, ~1.7% of the
+ * volume with the region's own FEC. Detects without correcting,
+ * which turns the residual from silent corruption into a clean
+ * fail-closed error -- the contract beamfs states everywhere
+ * else.
+ *
+ * RS full RS(255,239), 256 bytes per block, ~7% of the volume with
+ * the region's own FEC. Decodes up to 8 symbols per subblock;
+ * a verify decodes a copy, journals what it found and leaves
+ * the block as it is, so a hit pointer is reported rather than
+ * repaired.
+ *
+ * The region is sized for the worst case in every mode: any data block
+ * can become an indirect block, and a table mapping blocks to slots
+ * would itself be metadata needing protection.
+ */
+/*
+ * Slots one region block holds.
+ *
+ * A region block is RS-encoded in the alternating layout, sixteen
+ * subblocks of 239 data bytes each followed by its parity, so its 4096
+ * bytes carry BEAMFS_DATA_INLINE_BYTES of payload.
+ * Slots live in the payload and never straddle it.
+ *
+ * Before the region carried its own FEC, slots sat in the raw 4096
+ * bytes and one flipped bit took out every indirect block that region
+ * block covered. Under RS the region is now a seventh longer --
+ * fourteen slots per region block rather than sixteen -- and that is
+ * what the last layer being as well defended as the rest costs.
+ */
+#define BEAMFS_IND_PARITY_RS_SLOTS \
+ (BEAMFS_DATA_INLINE_BYTES / BEAMFS_IND_PARITY_RS_BYTES)
+#define BEAMFS_IND_PARITY_CRC_SLOTS \
+ (BEAMFS_DATA_INLINE_BYTES / BEAMFS_IND_PARITY_CRC_BYTES)
+
+/*
+ * Set when the parity region carries its own RS FEC. Without it the
+ * region is raw bytes with a different slot geometry, so reading one
+ * with this arithmetic addresses the wrong slots: the kernel refuses
+ * the mount rather than returning another block's parity.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_IND_PARITY_FEC BIT(17)
+
+/*
+ * Set when a block's RS symbols are interleaved across its codewords.
+ *
+ * Without it, codeword j owns bytes [j*255, (j+1)*255) of the block,
+ * its 239 data bytes followed by its parity. Damage that reaches
+ * several neighbouring bytes at once puts a nine-byte burst entirely
+ * in one codeword and takes it past correction.
+ *
+ * With it, symbol i of codeword j sits at byte i*16 + j of the coded
+ * bytes. The same burst puts at most one symbol in any codeword, and it
+ * takes 129 consecutive coded bytes to lose one -- sixteen times the
+ * burst resistance for not one byte of extra parity. The parity that
+ * follows is not interleaved, and nine bad bytes within one codeword's
+ * parity still lose it. debugfs beamfs/rs_bench reports the burst each
+ * layout survives over the coded bytes.
+ *
+ * Incompatible: reading an interleaved block with the contiguous
+ * arithmetic gathers the wrong symbols and decodes to noise, so a
+ * kernel without this refuses the mount rather than returning it.
+ *
+ * CCSDS provides for interleaving its Reed-Solomon code for the same
+ * reason.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE BIT(18)
+
+enum beamfs_ind_parity_mode {
+ BEAMFS_IND_PARITY_NONE = 0,
+ BEAMFS_IND_PARITY_CRC = 1,
+ BEAMFS_IND_PARITY_RS = 2,
+ BEAMFS_IND_PARITY__MAX
+};
+
+/* Bytes of parity per indirect block, by mode. */
+#define BEAMFS_IND_PARITY_CRC_BYTES (BEAMFS_DATA_INLINE_SUBBLOCKS * sizeof(__le32))
+#define BEAMFS_IND_PARITY_RS_BYTES (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY)
+
+/*
+ * ERROR_BUDGET -- per-block record of how close a block has come to
+ * saturation.
+ *
+ * The RS journal keeps the last 64 events, which answers "what
+ * happened recently" and nothing about the state of the volume. A
+ * block that has quietly accumulated seven of its eight correctable
+ * symbols reads back perfectly and is one upset from unrecoverable,
+ * and there is no way to know until it is too late.
+ *
+ * One byte per data block holds the worst subblock's corrected-symbol
+ * count, saturating at BEAMFS_RS_PARITY/2. An operator can then ask
+ * how much correction capacity a volume has left rather than how many
+ * corrections it has done -- the difference between a fuel gauge and
+ * an odometer.
+ *
+ * Nothing else does this. btrfs and ZFS count corrections; neither
+ * reports remaining margin, because with redundancy elsewhere there is
+ * no margin to report. On a single device there is, and it is finite.
+ *
+ * One byte per block is 0.025% of the volume: 244 MiB on 931 GiB. The
+ * slot is computed from the block number the same way the indirection
+ * parity region does it, for the same reason -- a lookup table would
+ * be metadata needing its own protection.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_ERROR_BUDGET BIT_ULL(14)
+
+/*
+ * BEAMFS_FEATURE_INCOMPAT_DIR_RS: directory blocks carry parity, and
+ * their entries are variable length.
+ *
+ * ## Why parity
+ *
+ * Every other structure on this volume is covered. Data blocks carry
+ * sixteen RS codewords, inodes carry CRC32 with RS behind it, indirect
+ * blocks carry their own parity, the superblock and the bitmap are both
+ * encoded. Directory blocks were written with memset and
+ * mark_buffer_dirty and nothing else -- 4096 raw bytes, no checksum, no
+ * correction.
+ *
+ * An upset in a directory block was therefore 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. In a filesystem whose reason for
+ * existing is surviving upsets, that was the one hole left in the
+ * chain.
+ *
+ * ## Why variable length in the same change
+ *
+ * A protected directory block has 3824 usable bytes instead of 4096, so
+ * the layout moves either way. Doing both at once costs one format
+ * migration rather than two.
+ *
+ * Entries were a fixed 268 bytes -- twelve of header and 256 for the
+ * name, whatever the name's length. That is fourteen entries per block
+ * and a hard ceiling of 7336 per directory, since a directory reaches
+ * twelve direct blocks plus 512 through one indirect. d_rec_len existed
+ * in the on-disk entry and was written correctly by both mkfs and the
+ * kernel; nothing ever read it, because every walk stepped by
+ * sizeof(struct beamfs_dir_entry).
+ *
+ * With the field finally used, a name of eleven characters occupies 24
+ * bytes: 159 entries per protected block, and a ceiling above 83000.
+ * Eleven times the density, eleven times fewer blocks to read on a
+ * lookup, and eleven times less metadata exposed to upsets for the same
+ * directory -- the resilience argument and the performance argument
+ * point the same way.
+ *
+ * ## Why entries avoid straddling subblocks
+ *
+ * Each of the sixteen subblocks is an independent codeword: one is
+ * correctable, or not, on its own. An entry spanning two of them turns
+ * a single uncorrectable subblock into a parse failure for the entry
+ * after it as well, and from there for the rest of the block.
+ *
+ * So an entry is placed within one subblock whenever it fits. That
+ * costs a few bytes of padding at each boundary -- about 3% -- and
+ * keeps damage where it landed.
+ *
+ * A name of 255 characters, which NAME_MAX allows and xfstests
+ * exercises, needs 268 bytes and cannot fit in 239. Such an entry is
+ * allowed to span, because the alternative is refusing a filename every
+ * other Linux filesystem accepts. The threshold is 224 characters, a
+ * 236-byte entry: anything longer cannot fit in one subblock, spans
+ * two, and an uncorrectable one costs both.
+ *
+ * The property is kept where it can be and dropped only where the
+ * format would otherwise have to lie about what it supports.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_DIR_RS BIT_ULL(15)
+
+/*
+ * BEAMFS_FEATURE_INCOMPAT_INODE_CRC_FULL: i_crc32 covers the block
+ * pointers as well as the head of the inode. See the coverage macros
+ * above. Incompatible rather than read-only compatible: a kernel
+ * without it computes a different checksum and would read every inode
+ * on the volume as damaged.
+ */
+#define BEAMFS_FEATURE_INCOMPAT_INODE_CRC_FULL BIT_ULL(16)
+
+/*
+ * A directory entry, on disk, under DIR_RS.
+ *
+ * d_rec_len is the distance to the next entry and includes the header.
+ * The name follows immediately and is not terminated: d_name_len says
+ * how long it is. Entries are aligned so the next one starts on a
+ * four-byte boundary, and a record length below BEAMFS_DIRENT_MIN_LEN,
+ * zero included, ends the walk of the block.
+ */
+#define BEAMFS_DIRENT_HDR_LEN 12
+#define BEAMFS_DIRENT_ALIGN 4
+#define BEAMFS_DIRENT_MIN_LEN (BEAMFS_DIRENT_HDR_LEN + BEAMFS_DIRENT_ALIGN)
+
+/* Round a name length up to a whole record. */
+#define BEAMFS_DIRENT_LEN(namelen) \
+ ((((BEAMFS_DIRENT_HDR_LEN) + (namelen) + \
+ (BEAMFS_DIRENT_ALIGN) - 1) / (BEAMFS_DIRENT_ALIGN)) * \
+ (BEAMFS_DIRENT_ALIGN))
+
+/*
+ * The longest entry that still fits inside one subblock. Anything
+ * longer is placed across a boundary rather than refused.
+ */
+#define BEAMFS_DIRENT_INLINE_MAX BEAMFS_SUBBLOCK_DATA
+#define BEAMFS_DIRENT_FITS_SUB(len) ((len) <= BEAMFS_DIRENT_INLINE_MAX)
+
+/* Returned by the placement helpers when a block cannot take an entry. */
+#define BEAMFS_DIRENT_NOSPACE ((u32)~0U)
+
+/*
+ * What the wall clock was worth when the anchor was taken.
+ *
+ * Not a measurement of the clock, a statement about it: the kernel
+ * knows whether NTP considers itself synchronised, and nothing more.
+ * A reader converting an entry to a date should carry this alongside
+ * the date rather than dropping it.
+ */
+enum beamfs_clock_quality {
+ /* No basis for the wall clock: no RTC, no sync. Dates from this
+ * anchor are ordering information wearing a timestamp's clothes.
+ */
+ BEAMFS_CLOCK_UNKNOWN = 0,
+ /* An RTC was read but nothing disciplines it. Seconds, maybe. */
+ BEAMFS_CLOCK_RTC = 1,
+ /* NTP reports itself synchronised. Milliseconds, typically. */
+ BEAMFS_CLOCK_NTP = 2,
+ /* A hardware-timestamped source: PTP, GPS. Microseconds or
+ * better, and the only case where fine dating is defensible.
+ */
+ BEAMFS_CLOCK_HARDWARE = 3,
+};
+
+/* Symbols correctable per subblock; the budget saturates here. */
+#define BEAMFS_ERROR_BUDGET_MAX (BEAMFS_RS_PARITY / 2)
+
+/*
+ * One RS(255,239) codeword's parity, 16 bytes. Not the per-block cost,
+ * which is BEAMFS_IND_PARITY_RS_BYTES or BEAMFS_IND_PARITY_CRC_BYTES.
+ */
+#define BEAMFS_IND_PARITY_BYTES BEAMFS_RS_PARITY
+/*
+ * An indirect block is 4096 bytes, past the 255-byte RS codeword, so it
+ * is split like a data block: 16 subblocks of 239 data bytes, with one
+ * 16-byte parity set per subblock. That is 256 parity bytes per block,
+ * over bytes [0, 3824): pointers 478..511 lie past the last subblock
+ * and neither mode covers them.
+ */
+#define BEAMFS_IND_PARITY_PER_BLOCK (BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY)
+/*
+ * Slots in a raw 4096-byte region block, the geometry before
+ * IND_PARITY_FEC. The kernel sizes slots with BEAMFS_IND_PARITY_RS_SLOTS
+ * and BEAMFS_IND_PARITY_CRC_SLOTS.
+ */
+#define BEAMFS_IND_PARITY_PER_REGION_BLOCK (BEAMFS_BLOCK_SIZE / BEAMFS_IND_PARITY_PER_BLOCK)
+
+/* Flags supported by this kernel module.
+ * PER_INODE_RS: per-inode RS(255,239) parity protection. The kernel
+ * write path has always computed the parity on every inode write
+ * (namei.c::beamfs_write_inode_raw); enabling this bit in SUPP
+ * unlocks the read-side decoder under any s_data_protection_scheme.
+ */
+#define BEAMFS_FEAT_COMPAT_SUPP 0ULL
+#define BEAMFS_FEAT_RO_COMPAT_SUPP (BEAMFS_FEATURE_RO_COMPAT_DATA_CSUM | \
+ BEAMFS_FEATURE_RO_COMPAT_DATA_SELFID)
+#define BEAMFS_FEAT_INCOMPAT_SUPP (BEAMFS_FEATURE_INCOMPAT_PER_INODE_RS | \
+ BEAMFS_FEATURE_INCOMPAT_INDIRECT_PARITY | \
+ BEAMFS_FEATURE_INCOMPAT_ERROR_BUDGET | \
+ BEAMFS_FEATURE_INCOMPAT_DIR_RS | \
+ BEAMFS_FEATURE_INCOMPAT_INODE_CRC_FULL | \
+ BEAMFS_FEATURE_INCOMPAT_IND_PARITY_FEC | \
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+
+/*
+ * On-disk superblock - block 0
+ * Total size: fits in one 4096-byte block
+ */
+struct beamfs_super_block;
+
+/*
+ * beamfs_bitmap_blocks_count_from_flags -- number of on-disk bitmap blocks.
+ *
+ * Decoded from s_flags bits 0..15. 0 in the field means the volume was
+ * formatted before multi-block bitmap support; treat as legacy single
+ * bitmap block.
+ */
+static inline u32 beamfs_bitmap_blocks_count_from_flags(__le32 s_flags_le)
+{
+ u32 v = le32_to_cpu(s_flags_le) & BEAMFS_SB_FLAGS_BITMAP_BLOCKS_MASK;
+
+ return v ? v : 1;
+}
+
+struct beamfs_super_block {
+ __le32 s_magic; /* BEAMFS_MAGIC */
+ __le32 s_block_size; /* Block size in bytes */
+ __le64 s_block_count; /* Total blocks */
+ __le64 s_free_blocks; /* Free blocks */
+ __le64 s_inode_count; /* Total inodes */
+ __le64 s_free_inodes; /* Free inodes */
+ __le64 s_inode_table_blk; /* Block where inode table starts */
+ __le64 s_data_start_blk; /* First data block */
+ __le32 s_version; /* Filesystem version */
+ __le32 s_flags; /* Flags */
+ __le32 s_crc32; /* CRC32 of this superblock */
+ __u8 s_uuid[16]; /* UUID */
+ __u8 s_label[32]; /* Volume label */
+ struct beamfs_rs_event s_rs_journal[BEAMFS_RS_JOURNAL_SIZE]; /* 2560 bytes */
+ __u8 s_rs_journal_head; /* next write index (ring buffer) */
+ __le64 s_bitmap_blk; /* On-disk block bitmap block number */
+ __le64 s_feat_compat; /* Compatible feature flags (informational) */
+ __le64 s_feat_incompat; /* Incompatible features: refuse mount if unknown bit set */
+ __le64 s_feat_ro_compat; /* RO-compat features: force RO mount if unknown bit set */
+ __le32 s_data_protection_scheme; /* enum BEAMFS_DATA_PROTECTION_* */
+ /*
+ * Indirection parity region, valid when INCOMPAT_INDIRECT_PARITY
+ * is set. Zero on volumes without the feature.
+ *
+ * Placed before the superblock's own parity zone, as the comment
+ * on the superblock RS layout requires: new fields are taken out
+ * of s_pad ahead of it.
+ */
+ __le64 s_ind_parity_blk; /* first block of the parity region */
+ __le32 s_ind_parity_len; /* length of the region, in blocks */
+ __le32 s_ind_parity_mode; /* enum beamfs_ind_parity_mode */
+ /*
+ * Error budget region, valid when INCOMPAT_ERROR_BUDGET is set.
+ * Zero on volumes formatted without it.
+ */
+ __le64 s_budget_blk; /* first block of the budget region */
+ __le32 s_budget_len; /* length of the region, in blocks */
+ __le32 s_budget_pad; /* zero */
+
+ /*
+ * Clock anchor: what the two clocks read at the same instant.
+ *
+ * re_timestamp in a journal entry is ktime_get_ns() -- monotonic
+ * since boot, which orders events exactly and dates none of them.
+ * That is the right clock for the entry: it cannot jump, so the
+ * interval between two corrections is trustworthy to the timer's
+ * resolution, and a burst of forty upsets in three milliseconds
+ * reads as exactly that.
+ *
+ * What it cannot say is when. The anchor supplies the missing
+ * half: at the instant s_anchor_mono was sampled, the wall clock
+ * read s_anchor_real. Any entry converts to a date by adding the
+ * difference, and the conversion is only as good as the wall
+ * clock was -- which is why s_anchor_quality records what that
+ * clock was worth.
+ *
+ * Declaring the uncertainty rather than implying a precision is
+ * the point. A journal that claims nanosecond dating on an NTP
+ * machine is claiming something it cannot support, and an auditor
+ * who notices has reason to doubt the rest of it. Instrumentation
+ * practice is to state the source and its error; this follows it.
+ *
+ * Refreshed periodically by the scrubber, so drift is bounded by
+ * the refresh interval rather than accumulating from mount. A
+ * volume mounted for six months is otherwise dated by a clock
+ * reading six months stale.
+ */
+ __le64 s_anchor_mono; /* ktime_get_ns() at the anchor */
+ __le64 s_anchor_real; /* ktime_get_real_ns() at the same instant */
+ __le32 s_anchor_quality; /* enum beamfs_clock_quality */
+ __le32 s_anchor_pad; /* zero */
+ __u8 s_pad[1327]; /* Padding; RS parity in its last 208 bytes */
+} __packed;
+
+/*
+ * On-disk inode
+ * Size: 256 bytes
+ *
+ * Addressing capacity:
+ * direct (12) = 48 KiB
+ * indirect (1) = 2 MiB
+ * dindirect (1) = 1 GiB
+ * tindirect (1) = 512 GiB
+ *
+ * uid/gid: __le32 to support uid > 65535 (standard kernel convention)
+ * timestamps: __le64 nanoseconds (required for space mission precision)
+ */
+struct beamfs_inode {
+ __le16 i_mode; /* File mode */
+ __le16 i_nlink; /* Hard link count */
+ __le32 i_uid; /* Owner UID */
+ __le32 i_gid; /* Owner GID */
+ __le64 i_size; /* File size in bytes (64-bit, future-proof) */
+ __le64 i_atime; /* Access time (ns) */
+ __le64 i_mtime; /* Modification time (ns) */
+ __le64 i_ctime; /* Change time (ns) */
+ __le32 i_flags; /* Inode flags */
+ __le32 i_crc32; /* CRC32 over [0, 48) and [52, 172) */
+ __le64 i_direct[BEAMFS_DIRECT_BLOCKS]; /* Direct block pointers */
+ __le64 i_indirect; /* Single indirect (~2 MiB) */
+ __le64 i_dindirect; /* Double indirect (~1 GiB) */
+ __le64 i_tindirect; /* Triple indirect (~512 GiB) */
+ __u8 i_reserved[84]; /* RS parity in [0, 16), then padding */
+} __packed;
+
+/*
+ * Inode flags.
+ *
+ * BEAMFS_INODE_FL_RS_ENABLED: deprecated as of stage 3 (v0.3.0+).
+ * Was the per-inode opt-in for RS FEC under the
+ * BEAMFS_DATA_PROTECTION_INODE_OPT_IN scheme. Stage 3 replaces
+ * that scheme with BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL, where
+ * all inodes are RS-protected unconditionally. The flag is
+ * preserved in the bit definition so v0.1.0/v0.2.0 images that
+ * set it remain mountable; new images do not set it.
+ * See Documentation/threat-model.md section 6.1 and 6.3.
+ */
+#define BEAMFS_INODE_FL_RS_ENABLED 0x0001 /* deprecated, see comment */
+#define BEAMFS_INODE_FL_VERIFIED 0x0002 /* Integrity verified */
+
+/*
+ * On-disk directory entry
+ */
+struct beamfs_dir_entry {
+ __le64 d_ino; /* Inode number */
+ __le16 d_rec_len; /* Record length */
+ __u8 d_name_len; /* Name length */
+ __u8 d_file_type; /* File type */
+ char d_name[BEAMFS_MAX_FILENAME + 1]; /* Filename */
+} __packed;
+
+#endif /* _BEAMFS_FORMAT_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 02/18] beamfs: add in-core header and tracepoints
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 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 03/18] beamfs: add CRC32 and Reed-Solomon layer Aurelien DESBRIERES
` (15 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
beamfs.h holds the in-core superblock and inode, and the declarations
shared between the translation units.
beamfs_trace.h defines the tracepoints, among them one event per store
into the block tree and one per read of a slot, which the usual event
filters narrow to a single inode.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/beamfs.h | 813 +++++++++++++++++++++++++++++++++++++++
fs/beamfs/beamfs_trace.h | 341 ++++++++++++++++
2 files changed, 1154 insertions(+)
create mode 100644 fs/beamfs/beamfs.h
create mode 100644 fs/beamfs/beamfs_trace.h
diff --git a/fs/beamfs/beamfs.h b/fs/beamfs/beamfs.h
new file mode 100644
index 000000000000..dc01bcdf2f17
--- /dev/null
+++ b/fs/beamfs/beamfs.h
@@ -0,0 +1,813 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * beamfs - resilient filesystem
+ * Based on: Fuchs, Langer, Trinitis - ARCS 2015
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+
+#ifndef _BEAMFS_H
+#define _BEAMFS_H
+
+#include <linux/hashtable.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/types.h>
+#include <linux/mutex.h>
+/*
+ * mempool_t and its operations, for the scratch reserve created at
+ * mount. Included explicitly rather than relied on: the aarch64 build
+ * got it through some other header's chain and the x86 one did not,
+ * which is the kind of thing a second architecture is for.
+ */
+#include <linux/mempool.h>
+/*
+ * sb_bread and mark_buffer_dirty, for the wrappers below and for the
+ * many callers that reach them through this header.
+ */
+#include <linux/buffer_head.h>
+
+/*
+ * On-disk format lives in its own header so the userspace tools can
+ * include it without dragging in kernel types.
+ */
+#include "beamfs_format.h"
+
+struct beamfs_sb_info {
+#ifdef CONFIG_BEAMFS_DEBUG_TREE
+ /*
+ * Every live indirect pointer, so a store that finds a slot at
+ * zero where one was recorded is caught where it happens rather
+ * than counted by fsck twenty seconds later.
+ */
+ DECLARE_HASHTABLE(s_tc, 14);
+ /*
+ * The same entries keyed by child, for beamfs_free_block: it asks
+ * "which slots name this block", and walking the whole table for
+ * that, once per freed block under s_tc_lock, was 70% of the CPU
+ * of generic/074 (2026-09-23).
+ */
+ DECLARE_HASHTABLE(s_tc_child, 14);
+ spinlock_t s_tc_lock; /* s_tc, s_tc_child, s_tc_entries */
+ unsigned int s_tc_entries;
+ unsigned int s_tc_violations;
+#endif
+ /* Block allocator */
+ unsigned long *s_block_bitmap; /* In-memory free block bitmap */
+
+ /*
+ * Scratch pages for decode and encode, reserved at mount.
+ *
+ * Every path that touches a block needs one: a lookup decodes a
+ * directory block, writeback decodes before it re-encodes, the
+ * sweep decodes what it checks. They were kmalloc'd per call --
+ * a ten-block directory meant ten allocations to resolve one name
+ * -- and under memory pressure that turns a path which must not
+ * block into one that does. generic/558 had twelve tasks in
+ * uninterruptible sleep and one was in kfree beneath
+ * beamfs_lookup.
+ *
+ * A mempool never returns NULL for GFP_NOFS and does not wait
+ * while it holds a free element. Thirty-two 4 KiB elements, 128 KiB
+ * per mount, against a class of stall that only appears when the
+ * machine is already in trouble -- which is exactly when a
+ * filesystem has to keep working.
+ */
+ mempool_t *s_scratch_pool;
+ /*
+ * Pages for encoded data blocks on their way to the device, so
+ * writeback never encodes through the block device cache and
+ * never holds a buffer lock. Sixty-four is two full bios; a
+ * flusher that finds the pool empty submits what it holds and
+ * waits for its own pages to come back.
+ */
+ mempool_t *s_wb_pages;
+ /*
+ * Staging for the superblock's RS encode, allocated at mount.
+ *
+ * beamfs_dirty_super_now used to kvmalloc it on every call, and
+ * beamfs_free_block calls that under s_lock -- a spinlock. An
+ * allocation that sleeps under a spinlock is a deadlock waiting
+ * for a machine busy enough, and DEBUG_ATOMIC_SLEEP found it on
+ * the first generic/076 after it was turned on: "BUG: sleeping
+ * function called from invalid context", rm holding three locks.
+ *
+ * 2769 bytes a mount against an allocation on every block freed.
+ * Written under s_lock, which every caller of dirty_super_now
+ * already holds or takes.
+ */
+ u8 *s_sb_rs_staging;
+ /*
+ * Bumped by every indirect-parity update.
+ *
+ * The per-cpu decode cache in indparity.c keys on it: a cached
+ * decode from before an update describes a region that has
+ * changed since, and one counter for the filesystem is enough --
+ * a redundant decode costs what it was going to cost anyway,
+ * while a stale hit would report a block as sound that is not.
+ */
+ atomic64_t s_ind_parity_gen;
+ unsigned long s_nblocks; /* Number of data blocks */
+ unsigned long s_data_start; /* First data block number */
+ /* Inode allocator */
+ unsigned long *s_inode_bitmap; /* In-memory free inode bitmap */
+ unsigned long s_ninodes; /* Total number of inodes */
+ /* Superblock */
+ struct beamfs_super_block *s_beamfs_sb; /* On-disk superblock copy */
+ struct buffer_head *s_sbh; /* Buffer head for superblock */
+ /* Array of K bhs; K = s_bitmap_blocks_count (multi-block bitmap). */
+ struct buffer_head **s_bitmap_blkhs;
+ u32 s_bitmap_blocks_count;
+
+ /*
+ * Which bitmap blocks need their on-disk image rebuilt.
+ *
+ * Rebuilding means walking 3824 bytes bit by bit out of the
+ * in-memory bitmap and running sixteen RS encodes over the result.
+ * Doing that per allocation cost one full re-encode per data block
+ * touched -- measured at 25264 rebuilds for 25264 allocations, and
+ * a second RS pass over the superblock alongside it, so two
+ * metadata encodes for every block of payload. Writing 800 MiB
+ * meant 220000 of each.
+ *
+ * The bit itself is what changed; the codeword only has to be
+ * right when the buffer reaches the disk. Marking here and
+ * rebuilding in sync_fs collapses thousands of rebuilds into one
+ * per block that was actually touched.
+ *
+ * s_super_needs_encode is the superblock's equivalent, but the
+ * superblock is not deferred: the flusher can write its buffer at
+ * any time, so beamfs_dirty_super rebuilds it at once and the flag
+ * stays clear.
+ */
+ unsigned long *s_bitmap_needs_encode;
+ /*
+ * Metadata buffers the whole volume shares: bitmap blocks and
+ * indirect parity regions. One list for all of them, never an
+ * inode's. A buffer that moves between per-inode lists meets the
+ * window in mmb_sync where b_mmb is NULL under one inode's lock
+ * while 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).
+ * write_inode, sync_inode_metadata and evict sync this list before
+ * the inode's own; put_super empties it before releasing them.
+ */
+ struct mapping_metadata_bhs s_shared_bhs;
+ bool s_super_needs_encode;
+ spinlock_t s_lock; /* Superblock lock */
+
+ /*
+ * Set once the volume has failed, and never cleared.
+ *
+ * A filesystem whose device has gone away must stop writing to
+ * it. Without this, beamfs kept marking buffers dirty on a device
+ * that answered every request with EIO -- and the kernel says so:
+ * mark_buffer_dirty starts with WARN_ON_ONCE(!buffer_uptodate),
+ * because a buffer that could not be read holds nothing worth
+ * writing back. generic/338 replaces the device with dm-error and
+ * watches for exactly that.
+ *
+ * Checking buffer_uptodate at each of the thirty-nine
+ * mark_buffer_dirty sites would silence the warning and treat the
+ * symptom. The volume is what failed, so the volume is what
+ * records it: one flag, tested where writes begin, and the mount
+ * goes read-only so the VFS stops sending more.
+ */
+ bool s_failed;
+ /* Found MOUNTED or ERRORS set at mount: the last mount did not end well. */
+ bool s_unclean;
+ unsigned long s_free_blocks;
+ unsigned long s_free_inodes;
+
+ /*
+ * Where the last allocation landed.
+ *
+ * find_first_bit scans from zero every time, so the cost of an
+ * allocation grows with how full the volume is and filling one is
+ * quadratic. Measured: 240 writes/s on an empty volume, 17 on a
+ * mostly full one, a fifteenfold fall that made generic/015 and
+ * generic/074 exceed a twenty-minute timeout.
+ *
+ * Resuming from the last position makes the common case -- a
+ * sequential writer -- constant time. Wrapping to zero on the
+ * first miss keeps it exhaustive, so a volume with a hole near
+ * the start still fills completely.
+ *
+ * ext2 has done this since 1993 and calls it the goal.
+ */
+ unsigned long s_alloc_goal;
+ /*
+ * Reservation windows of the inodes being written, under s_lock:
+ * ext2's rsv_window. Each writer allocates inside its own
+ * interval and nobody else does, so three files written at once
+ * come out as three contiguous runs instead of one interleaving.
+ */
+ struct list_head s_rsv_windows;
+ /* enum BEAMFS_DATA_PROTECTION_*, cached from on-disk SB */
+ u32 s_scheme;
+ u64 s_feat_incompat; /* cached from on-disk SB at mount time */
+ bool s_data_csum; /* DATA_CSUM active, cached at mount */
+ bool s_data_selfid; /* DATA_SELFID active, cached at mount */
+
+ /* Indirection parity, cached from the on-disk superblock. */
+ u64 s_ind_parity_blk;
+ u32 s_ind_parity_len;
+ u32 s_ind_parity_mode;
+
+ /* Error budget region, cached from the superblock. */
+ u64 s_budget_blk;
+ u32 s_budget_len;
+
+ /*
+ * Back-pointer to the VFS superblock.
+ *
+ * The sysfs attributes are handed an sb_info and need a
+ * super_block to read the medium. bd_super used to serve; it was
+ * removed from struct block_device, and reaching for it through
+ * the buffer_head was always the long way round for something
+ * this object could simply hold.
+ */
+ struct super_block *s_sb;
+
+ /* Clock anchor, cached and refreshed by the scrubber. */
+ u64 s_anchor_mono;
+ u64 s_anchor_real;
+ u32 s_anchor_quality;
+
+ /* Alert rate limiting, one slot per event class. */
+ unsigned long s_alert_last[3];
+ u64 s_alert_last_corrected;
+ u32 s_alert_rate_limit;
+
+ /*
+ * Background scrubber.
+ *
+ * Correction happens on read, so a block nobody reads accumulates
+ * upsets until it passes the eight-symbol radius and becomes
+ * unrecoverable. Cold data is exactly the case this filesystem
+ * exists for -- an archive on a spacecraft is written once and
+ * read years later -- so waiting for a reader is waiting for the
+ * failure.
+ *
+ * The thread walks allocated blocks at a bounded rate, decodes
+ * each, and journals what it finds. A block that needed
+ * correction is written back whole, under the buffer lock: only
+ * while its owner still maps it at the same place, only if no
+ * subblock was beyond correction and the buffer is unchanged
+ * since the decode, and never on a read-only or frozen volume.
+ * Each upset is correctable on its own; it is their sum that
+ * loses data. Blocks reached through the wear map have no known
+ * owner and are checked and reported, never written.
+ */
+ struct task_struct *s_scrub_thread;
+ unsigned int s_scrub_interval_ms; /* between blocks; 0 = idle */
+ u64 s_scrub_cursor; /* next block to visit */
+ u64 s_scrub_passes; /* completed sweeps */
+ u64 s_scrub_blocks; /* blocks checked */
+ u64 s_scrub_corrected; /* blocks with corrections */
+ u64 s_scrub_uncorrectable;
+
+ /*
+ * Sweep pacing, closed on what the last sweep found.
+ *
+ * The interval was a constant the operator set once, so the
+ * scrubber consumed the same bandwidth on a volume where nothing
+ * had been corrected in months as on one correcting steadily. In
+ * a radiotherapy vault the neutron flux exists only during
+ * treatment: a fixed rate is both wasteful at night and too slow
+ * during a session, and it is the one environment this filesystem
+ * was built for.
+ *
+ * s_scrub_base_ms is what the operator asked for and the pace
+ * returns to when the volume is quiet. s_scrub_last_corrected is
+ * the correction count at the end of the previous sweep, which is
+ * all that is needed to know whether this one found more.
+ */
+ /*
+ * Where the wear scan has reached, and how many worn blocks the
+ * last pass over the budget found. The cursor is a block offset
+ * within the data region, not a byte offset in the budget.
+ */
+ u64 s_wear_cursor;
+ u64 s_wear_visits;
+
+ unsigned int s_scrub_base_ms;
+ u64 s_scrub_last_corrected;
+ struct kobject s_kobj;
+ struct completion s_kobj_unregister;
+};
+
+/*
+ * In-memory inode info (embedded in VFS inode via container_of)
+ */
+struct beamfs_inode_info {
+ __le64 i_direct[BEAMFS_DIRECT_BLOCKS];
+ __le64 i_indirect;
+ __le64 i_dindirect;
+ __le64 i_tindirect;
+ __u32 i_flags;
+ /*
+ * Whether i_blocks holds the count of the blocks the inode has on
+ * the medium. The inode does not store it: beamfs_getattr takes it
+ * from the tree the first time, beamfs_alloc_block and
+ * beamfs_free_block keep it from then on. Set under i_alloc_mutex,
+ * with a release; beamfs_getattr reads it without, with an acquire.
+ */
+ bool i_blocks_counted;
+ struct mutex i_alloc_mutex; /* serialize lookup_or_alloc_phys */
+ /*
+ * This inode's reservation window, [i_rsv_start, i_rsv_end), on
+ * s_rsv_windows while it holds one; i_rsv_next is the first block
+ * of it not yet handed out, i_rsv_size what the next window asks
+ * for, i_last_alloc the goal. All under s_lock, none on the disk.
+ */
+ struct list_head i_rsv_list;
+ unsigned long i_rsv_start;
+ unsigned long i_rsv_end;
+ unsigned long i_rsv_next;
+ unsigned int i_rsv_size;
+ unsigned long i_last_alloc;
+ /*
+ * Metadata buffer_heads owned by this inode (bitmap blocks dirtied
+ * on its behalf). The kernel replaced the old i_data.i_private_list
+ * mechanism, along with mark_buffer_dirty_inode() and
+ * invalidate_inode_buffers(), with this per-inode list and the
+ * mmb_* helpers; ext2 carries the same field as i_metadata_bhs.
+ */
+ struct mapping_metadata_bhs i_metadata_bhs;
+ struct inode vfs_inode; /* Must be last */
+};
+
+static inline struct beamfs_inode_info *BEAMFS_I(struct inode *inode)
+{
+ return container_of(inode, struct beamfs_inode_info, vfs_inode);
+}
+
+static inline struct beamfs_sb_info *BEAMFS_SB(struct super_block *sb)
+{
+ return sb->s_fs_info;
+}
+
+/* Function prototypes */
+/* super.c */
+int beamfs_fill_super(struct super_block *sb, struct fs_context *fc);
+
+/* scrub.c */
+int beamfs_scrub_init(struct super_block *sb);
+void beamfs_scrub_exit(struct super_block *sb);
+int beamfs_scrub_check_block(struct super_block *sb, u64 phys,
+ unsigned int *corrected,
+ struct inode *owner, u64 iblock);
+void beamfs_scrub_stop(struct super_block *sb);
+
+/* file_inline.c */
+int beamfs_count_blocks(struct inode *inode, u64 *count);
+int beamfs_inline_lookup_phys(struct inode *inode, u64 iblock_logical,
+ u64 *phys_out);
+
+/* super.c -- the RS journal */
+/*
+ * beamfs_log_rs_event -- record a Reed-Solomon correction event in the
+ * persistent superblock journal.
+ *
+ * @sb: mounted superblock (sbi must be initialized)
+ * @block_no: block number where correction occurred, OR a SB
+ * sub-block sentinel (see BEAMFS_RS_BLOCK_NO_SB_MARKER)
+ * @positions: array of byte positions corrected within the codeword.
+ * MUST be non-NULL when n_positions >= 1, valid for
+ * n_positions entries. MUST be NULL when n_positions == 0
+ * (uncorrectable event, see UNCORRECTABLE policy below).
+ * @n_positions: number of corrections; MUST be in [0, BEAMFS_RS_PARITY/2].
+ * When >= 1, equals the symbol count and is written
+ * verbatim into re_symbol_count. When == 0, denotes an
+ * uncorrectable event (see UNCORRECTABLE policy below).
+ * @code_len_bytes: data length of the codeword that produced @positions
+ * (BEAMFS_INODE_RS_DATA for inodes, BEAMFS_SUBBLOCK_DATA
+ * for bitmap subblocks, BEAMFS_SB_RS_DATA_LEN for SB
+ * subblocks). Required by the entropy estimator to
+ * compute the bin index from each byte position.
+ * MUST be in [1, BEAMFS_SUBBLOCK_DATA], even for
+ * uncorrectable events.
+ *
+ * Forensic policy:
+ * n_positions >= 2 -> entropy computed, ENTROPY_VALID flag set,
+ * re_symbol_count = n_positions
+ * n_positions == 1 -> entropy zeroed, ENTROPY_VALID cleared,
+ * re_symbol_count = 1 (single-sample event is
+ * not forensically significant; Family A vs B
+ * distinction relies on timestamp clustering
+ * for these entries)
+ * n_positions == 0 -> UNCORRECTABLE flag set, ENTROPY_VALID cleared,
+ * re_symbol_count = 0, re_entropy_q16_16 = 0.
+ * re_block_no and re_timestamp retain their
+ * normal meaning, allowing the entry to
+ * participate in temporal and spatial clustering
+ * analyses alongside correctable events.
+ *
+ * Computes the Shannon entropy estimate from @positions and stores it
+ * in re_entropy_q16_16 with BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID set
+ * (only when n_positions >= 2; see Forensic policy above).
+ *
+ * Invariants enforced via WARN_ON_ONCE: a call that breaks one is not
+ * journalled, and reserved bits in extra_flags are masked off; nothing
+ * panics. Takes s_lock with spin_lock(), so not for interrupt context.
+ */
+/*
+ * beamfs_log_rs_event_flagged -- variant of beamfs_log_rs_event that
+ * accepts an OR-mask of extra_flags to set on the journal entry in
+ * addition to the policy-derived flags (ENTROPY_VALID, UNCORRECTABLE).
+ *
+ * Used by call sites that can distinguish caller context (e.g.
+ * read path vs RMW-neutralised write path in file_inline.c) and
+ * want that distinction recorded in re_flags. Pass extra_flags = 0
+ * for the legacy behaviour (then beamfs_log_rs_event is the
+ * idiomatic alias).
+ *
+ * extra_flags MUST only contain BEAMFS_RS_EVENT_FLAG_* bits and the
+ * subblock field built by beamfs_rs_event_subblock_bits().
+ * ENTROPY_VALID and UNCORRECTABLE are reserved for the policy and MUST
+ * NOT appear in extra_flags (WARN_ON_ONCE enforces).
+ */
+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);
+
+void beamfs_log_rs_event(struct super_block *sb,
+ u64 block_no,
+ const u8 *positions,
+ unsigned int n_positions,
+ size_t code_len_bytes);
+void beamfs_dirty_super(struct beamfs_sb_info *sbi);
+
+/* file.c - BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE data path (v2) */
+extern const struct address_space_operations beamfs_inline_aops;
+extern const struct file_operations beamfs_inline_file_operations;
+
+/* inode.c */
+struct inode *beamfs_iget(struct super_block *sb, unsigned long ino);
+int beamfs_getattr(const struct mnt_idmap *idmap, const struct path *path,
+ struct kstat *stat, u32 request_mask,
+ unsigned int query_flags);
+/* namei.c */
+struct inode *beamfs_new_inode(struct inode *dir, umode_t mode);
+
+/* dir.c */
+extern const struct file_operations beamfs_dir_operations;
+
+/* namei.c */
+extern const struct inode_operations beamfs_dir_inode_operations;
+extern const struct inode_operations beamfs_symlink_inode_operations;
+
+/* Directory block resolver (defined in namei.c). */
+int beamfs_dir_get_block(struct inode *dir, unsigned int block_idx,
+ bool alloc, u64 *out_block);
+
+/* file.c */
+extern const struct file_operations beamfs_file_operations;
+/* S2.2: fiemap support shared by both schemes (file.c). */
+int beamfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
+ u64 start, u64 len);
+extern const struct inode_operations beamfs_file_inode_operations;
+extern const struct address_space_operations beamfs_aops;
+
+/* file_inline.c -- getattr, setattr and fiemap for inline-path regular files */
+extern const struct inode_operations beamfs_inline_inode_operations;
+
+/* edac.c */
+int beamfs_rs_init_tables(void);
+void beamfs_rs_exit_tables(void);
+__u32 beamfs_crc32(const void *buf, size_t len);
+
+/*
+ * beamfs_inode_crc -- checksum over everything the inode's parity
+ * protects, which is the head and the pointers but not i_crc32 itself.
+ *
+ * Staged into a contiguous buffer: the covered bytes are not contiguous
+ * on disk, and computing over them in two calls would need a seeded
+ * primitive that beamfs_crc32 does not offer. 168 bytes on the stack,
+ * once per inode read or write.
+ */
+static inline __u32 beamfs_inode_crc(const struct beamfs_inode *raw)
+{
+ __u8 staging[BEAMFS_INODE_CRC_BYTES];
+
+ memcpy(staging, raw, BEAMFS_INODE_CRC_HEAD_LEN);
+ memcpy(staging + BEAMFS_INODE_CRC_HEAD_LEN,
+ (const __u8 *)raw + BEAMFS_INODE_CRC_TAIL_OFF,
+ BEAMFS_INODE_CRC_TAIL_LEN);
+ return beamfs_crc32(staging, sizeof(staging));
+}
+
+/*
+ * beamfs_data_selfid -- 64-bit identity digest of (ino, iblock), stored in
+ * the block descriptor when DATA_SELFID is active. Two independent CRC32s,
+ * one per dimension, concatenated: reuses the single hashing primitive
+ * already backing i_crc32, s_crc32 and DATA_CSUM rather than introducing a
+ * second algorithm for an auditor to review. Collision probability 2^-64.
+ */
+static inline __u64 beamfs_data_selfid(__u64 ino, __u64 iblock)
+{
+ __le64 a = cpu_to_le64(ino);
+ __le64 b = cpu_to_le64(iblock);
+ __u32 lo = beamfs_crc32(&a, sizeof(a));
+ __u32 hi = beamfs_crc32(&b, sizeof(b));
+
+ return ((__u64)hi << 32) | lo;
+}
+
+__u32 beamfs_crc32_sb(const struct beamfs_super_block *fsb);
+int beamfs_rs_encode(u8 *data, size_t len, u8 *parity);
+
+/*
+ * beamfs_rs_decode -- decode and correct a shortened RS(255,239)
+ * codeword in place, optionally exposing the
+ * list of corrected byte positions for entropy.
+ *
+ * @data: data bytes, corrected in place on success
+ * @len: number of data bytes (must match the encode call)
+ * @parity: parity bytes (BEAMFS_RS_PARITY)
+ * @positions: optional output, BEAMFS_RS_PARITY/2 = 8 entries; on a
+ * positive return holds the corrected DATA byte
+ * positions, in [0, len). Corrections that fell in the
+ * parity are not listed. May be NULL if the caller does
+ * not need the position list (no entropy logging).
+ * @max_positions: capacity of @positions in entries; ignored if NULL.
+ *
+ * Returns:
+ * < 0 uncorrectable (-EBADMSG) or invalid input (-EINVAL)
+ * = 0 no errors detected
+ * > 0 number of symbol errors corrected in place, data and parity
+ *
+ * The return value is therefore not the number of entries in
+ * @positions when a parity symbol was among those corrected; the
+ * entropy estimator must only be given the data positions.
+ */
+int beamfs_rs_decode(u8 *data, size_t len, u8 *parity,
+ u8 *positions, unsigned int max_positions,
+ const char *who);
+
+/*
+ * Shannon entropy estimator for the RS journal.
+ *
+ * @positions: byte positions corrected (output of beamfs_rs_decode)
+ * @n_positions: number of valid entries in @positions, in [1, 8]
+ * @code_len_bytes: data length of the codeword, in [n_positions, 239]
+ *
+ * Returns H in Q16.16, range [0, 3*65536). Deterministic, no FPU,
+ * no runtime division except for bin index computation. Backed by a
+ * compile-time LUT, defined in edac.c.
+ *
+ * Pre: positions != NULL, 1 <= n_positions <= BEAMFS_RS_PARITY/2
+ * code_len_bytes >= n_positions
+ */
+__u32 beamfs_rs_compute_entropy_q16_16(const u8 *positions,
+ unsigned int n_positions,
+ size_t code_len_bytes);
+
+/*
+ * How many bytes of a file one block holds.
+ *
+ * 3824 in the alternating layout, where the whole coded area is user
+ * data. 3808 in a capsule, where the last sixteen of it are the
+ * descriptor -- the csum and the selfid, moved inside the codewords so
+ * a burst cannot condemn a block whose data is intact.
+ *
+ * Every place that turns a file offset into a block number goes
+ * through this. Writing the constant instead reads a file back shifted
+ * by sixteen bytes on a capsule volume, silently: measured on a live
+ * volume as eight megabytes in and a different md5sum out, with every
+ * check green.
+ */
+static inline u32 beamfs_block_payload(struct super_block *sb)
+{
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+ return BEAMFS_CAPSULE_DATA_BYTES;
+
+ return BEAMFS_DATA_INLINE_BYTES;
+}
+
+/*
+ * Lay a data block's payload down, stamp its descriptor and seal it,
+ * in that order and in the volume's layout. Defined in file_inline.c.
+ *
+ * Shared because a symlink's target is a data block written from
+ * namei.c, and it followed the alternating layout on a capsule volume
+ * until generic/360 read back an empty path.
+ */
+void beamfs_lay_data_payload(struct super_block *sb, u8 *block,
+ const u8 *payload);
+int beamfs_seal_data_block(struct super_block *sb, u8 *block);
+void beamfs_stamp_data_block(struct super_block *sb, u8 *block,
+ const u8 *payload, u64 ino, u64 iblock);
+
+int beamfs_rs_encode_block(u8 *block);
+int beamfs_rs_decode_block(u8 *block, int *results, const char *who);
+void beamfs_block_read(const u8 *block, size_t off, size_t len, u8 *out);
+void beamfs_block_write(u8 *block, size_t off, size_t len, const u8 *in);
+int beamfs_rs_encode_woven(u8 *data_buf, u8 *parity_buf,
+ size_t parity_stride, size_t data_len,
+ unsigned int n_subblocks);
+int beamfs_rs_decode_woven(u8 *data_buf, u8 *parity_buf,
+ size_t parity_stride, size_t data_len,
+ unsigned int n_subblocks, int *results,
+ const char *who);
+/*
+ * Encode/decode N RS(255,239) shortened subblocks across a region.
+ * data and parity may live in the same buffer (alternating layout,
+ * like the bitmap) or in two separate buffers (contiguous-parity
+ * layout, like the superblock). The strides decouple the two cases.
+ *
+ * The decode helper additionally exposes per-subblock corrected
+ * position lists for entropy logging. Each subblock writes up to
+ * positions_stride entries into positions_buf at offset
+ * i * positions_stride; results[i] holds beamfs_rs_decode's return
+ * for that subblock, which also counts corrections in the parity
+ * that the list leaves out.
+ *
+ * positions_buf may be NULL (and positions_stride == 0) if the caller
+ * does not need entropy logging for any of the subblocks.
+ */
+int beamfs_rs_encode_region(u8 *data_buf, size_t data_stride,
+ u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks);
+int beamfs_rs_decode_region(u8 *data_buf, size_t data_stride,
+ u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks,
+ int *results,
+ u8 *positions_buf,
+ unsigned int positions_stride,
+ const char *who);
+
+/* alloc.c */
+int beamfs_setup_bitmap(struct super_block *sb);
+void beamfs_bitmap_encode_pending(struct super_block *sb);
+/* super.c */
+void beamfs_super_encode_pending(struct beamfs_sb_info *sbi);
+void beamfs_dirty_super_now(struct beamfs_sb_info *sbi);
+/* alloc.c */
+int beamfs_write_bitmap_block(struct super_block *sb,
+ unsigned long bit_global,
+ struct inode *owner);
+void beamfs_destroy_bitmap(struct super_block *sb);
+u64 beamfs_alloc_block(struct super_block *sb, struct inode *owner);
+void beamfs_rsv_init(struct beamfs_inode_info *fi);
+void beamfs_rsv_discard(struct inode *inode);
+void beamfs_free_block(struct super_block *sb, u64 block, struct inode *owner);
+bool beamfs_block_is_allocated(struct super_block *sb, u64 block);
+/* super.c */
+/*
+ * beamfs_free_ind_range -- free part or all of an indirection subtree.
+ *
+ * @depth: 1 = pointers are data blocks, 2 = single-indirect blocks,
+ * 3 = double-indirect blocks.
+ * @base: logical index of the first data block the subtree covers.
+ * @first: logical index from which to free; everything below survives.
+ *
+ * Returns true when the subtree ended up empty and @blk was freed with
+ * it, false when something survived and @blk was kept.
+ */
+bool beamfs_free_ind_range(struct super_block *sb, u64 blk,
+ unsigned int depth, u64 base, u64 first,
+ struct inode *inode);
+
+/* indparity.c */
+void beamfs_ind_parity_update(struct super_block *sb, struct buffer_head *bh,
+ struct inode *inode);
+int beamfs_ind_parity_verify(struct super_block *sb, struct buffer_head *bh);
+int beamfs_ind_parity_verify_medium(struct super_block *sb, struct buffer_head *bh);
+/*
+ * An indirect block whose copy in memory has been checked against its
+ * parity, or written by us with the parity filed: ext4's
+ * buffer_verified. Set once per life of the buffer, so a pointer
+ * lookup does not decode sixteen codewords of the parity region each
+ * time -- 19 % of the CPU of generic/074 (2026-09-23), because the
+ * decode cache was keyed on a counter every parity update bumped.
+ */
+#define BH_BeamfsVerified BH_PrivateStart
+BUFFER_FNS(BeamfsVerified, beamfs_verified)
+void beamfs_ind_parity_touched(struct beamfs_sb_info *sbi);
+void beamfs_ind_parity_cache_free(void);
+
+#ifdef CONFIG_BEAMFS_DEBUG_TREE
+void beamfs_tc_init(struct beamfs_sb_info *sbi);
+void beamfs_tc_exit(struct beamfs_sb_info *sbi);
+void beamfs_tc_store(struct super_block *sb, unsigned long ino, u64 parent,
+ u32 slot, u64 old, u64 child, const char *who);
+void beamfs_tc_clear(struct super_block *sb, u64 parent, u32 slot);
+void beamfs_tc_zeroed(struct super_block *sb, unsigned long ino, u64 parent,
+ const char *who);
+void beamfs_tc_forget_child(struct super_block *sb, u64 child);
+void beamfs_tc_forget_parent(struct super_block *sb, u64 parent);
+#else
+static inline void beamfs_tc_init(struct beamfs_sb_info *sbi) { }
+static inline void beamfs_tc_exit(struct beamfs_sb_info *sbi) { }
+static inline void beamfs_tc_store(struct super_block *sb, unsigned long ino,
+ u64 parent, u32 slot, u64 old, u64 child,
+ const char *who) { }
+static inline void beamfs_tc_clear(struct super_block *sb, u64 parent,
+ u32 slot) { }
+static inline void beamfs_tc_zeroed(struct super_block *sb, unsigned long ino,
+ u64 parent, const char *who) { }
+static inline void beamfs_tc_forget_child(struct super_block *sb, u64 child) { }
+static inline void beamfs_tc_forget_parent(struct super_block *sb, u64 parent) { }
+#endif
+
+/* Scratch pages, from the mount's reserve. Never NULL under GFP_NOFS. */
+void *beamfs_scratch_get(struct super_block *sb);
+void beamfs_scratch_put(struct super_block *sb, void *p);
+
+/*
+ * Record that the volume has failed and take it read-only.
+ *
+ * Safe to call more than once and from any context; the first caller
+ * logs, the rest are ignored.
+ */
+void beamfs_fail(struct super_block *sb, const char *where, int err);
+
+/*
+ * sb_bread, and mark the volume failed when it comes back empty.
+ *
+ * Forty-seven sites read blocks and each handles its own NULL with an
+ * EIO. That is right locally and blind globally: nothing recorded that
+ * the device had stopped answering, so the next write went ahead and
+ * dirtied a buffer nobody could read -- the WARN_ON_ONCE inside
+ * mark_buffer_dirty, which generic/338 provokes with dm-error.
+ *
+ * One wrapper, so a read that fails is a volume that has failed. What a
+ * failed volume refuses is listed in beamfs_fail.
+ */
+static inline struct buffer_head *beamfs_bread(struct super_block *sb,
+ sector_t block,
+ const char *where)
+{
+ struct buffer_head *bh = sb_bread(sb, block);
+
+ if (!bh)
+ beamfs_fail(sb, where, -EIO);
+ return bh;
+}
+
+/* Has the volume already failed? Writes must not start if it has. */
+static inline bool beamfs_failed(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ return sbi && READ_ONCE(sbi->s_failed);
+}
+
+/* dirent.c -- variable-length directory entries, and parity over them */
+u32 beamfs_dirent_place(u32 off, u16 len);
+u32 beamfs_dirent_next(const struct beamfs_dir_entry *de, u32 off);
+bool beamfs_dirent_valid(const struct beamfs_dir_entry *de, u32 off);
+void beamfs_dirent_encode(struct buffer_head *bh);
+int beamfs_dirent_decode(struct super_block *sb, struct buffer_head *bh,
+ u8 *dst);
+/* file_inline.c */
+int beamfs_inline_decode_symlink(struct super_block *sb,
+ struct buffer_head *bh, u64 phys,
+ struct inode *inode, u8 *dst, u32 len);
+
+/* alert.c -- uevent on threshold crossing */
+enum {
+ BEAMFS_ALERT_UNCORRECTABLE = 0,
+ BEAMFS_ALERT_MARGIN,
+ BEAMFS_ALERT_RATE,
+ BEAMFS_ALERT_CLASSES
+};
+
+void beamfs_alert_uncorrectable(struct super_block *sb, u64 phys);
+void beamfs_alert_margin(struct super_block *sb, u64 phys, unsigned int used);
+void beamfs_alert_check_rate(struct super_block *sb);
+
+/* clock.c -- anchoring the journal's monotonic stamps to a date */
+void beamfs_clock_anchor(struct super_block *sb);
+u64 beamfs_clock_to_real(struct beamfs_sb_info *sbi, u64 mono);
+
+/* budget.c -- per-block correction margin */
+void beamfs_budget_record(struct super_block *sb, u64 phys,
+ unsigned int symbols);
+u8 beamfs_budget_read(struct super_block *sb, u64 phys);
+void beamfs_budget_histogram(struct super_block *sb, u64 *hist);
+u64 beamfs_budget_next_worn(struct super_block *sb, u64 from, u8 threshold);
+
+/* rsbench.c -- codec cost, no block layer underneath */
+void beamfs_debugfs_init(void);
+void beamfs_debugfs_exit(void);
+
+/* alloc.c -- inode numbers */
+u64 beamfs_alloc_inode_num(struct super_block *sb);
+void beamfs_free_inode_num(struct super_block *sb, u64 ino);
+
+/* dir.c */
+struct dentry *beamfs_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags);
+
+/* namei.c */
+int beamfs_write_inode(struct inode *inode, struct writeback_control *wbc);
+int beamfs_write_inode_raw(struct inode *inode);
+
+#endif /* _BEAMFS_H */
diff --git a/fs/beamfs/beamfs_trace.h b/fs/beamfs/beamfs_trace.h
new file mode 100644
index 000000000000..618313a066c1
--- /dev/null
+++ b/fs/beamfs/beamfs_trace.h
@@ -0,0 +1,341 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM beamfs
+
+#if !defined(_BEAMFS_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _BEAMFS_TRACE_H
+
+#include <linux/tracepoint.h>
+#include <linux/kdev_t.h>
+
+/*
+ * One event per store into the block tree, and one per read of a slot.
+ * Filterable in the usual way -- echo 'ino == 60' > filter -- so a run
+ * can be narrowed to the inode that leaked without rebuilding anything.
+ */
+TRACE_EVENT(beamfs_slot_store,
+ TP_PROTO(dev_t dev, unsigned long ino, u64 parent, u64 slot, u64 old, u64 new,
+ unsigned int level),
+ TP_ARGS(dev, ino, parent, slot, old, new, level),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(u64, parent)
+ __field(u64, slot)
+ __field(u64, old)
+ __field(u64, new)
+ __field(unsigned int, level)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->parent = parent;
+ __entry->slot = slot;
+ __entry->old = old;
+ __entry->new = new;
+ __entry->level = level;
+ ),
+ TP_printk("dev=%u:%u ino=%lu parent=%llu slot=%llu %llu->%llu lvl=%u",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->parent, __entry->slot,
+ __entry->old, __entry->new, __entry->level)
+);
+
+/*
+ * An indirect block read on the allocation path.
+ *
+ * generic/464 has two writers install a pointer into the same slot of
+ * the same indirect block, twenty-five seconds apart, and the second
+ * one reads it as zero: xfs_io writes 23967 into slot 5 of block 36508
+ * at t=2158.33, and a writeback kworker writes 105019 into the same
+ * slot at t=2183.22, with the trace showing 0-> both times. The block
+ * held the first pointer; the second writer did not see it.
+ *
+ * @uptodate says whether the buffer came back already valid -- a
+ * buffer that had to be read from the medium is one the cache had
+ * dropped, and what comes back is whatever was last written out,
+ * which need not be what the last writer put in memory.
+ *
+ * @slotval is what sits in the slot about to be written, so a read
+ * that returns zero where a pointer was installed shows up here rather
+ * than being inferred from two slot_store events.
+ */
+TRACE_EVENT(beamfs_ind_read,
+ TP_PROTO(dev_t dev, unsigned long ino, u64 blk, unsigned int slot, u64 slotval,
+ int uptodate, int fresh),
+ TP_ARGS(dev, ino, blk, slot, slotval, uptodate, fresh),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(u64, blk)
+ __field(unsigned int, slot)
+ __field(u64, slotval)
+ __field(int, uptodate)
+ __field(int, fresh)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->blk = blk;
+ __entry->slot = slot;
+ __entry->slotval = slotval;
+ __entry->uptodate = uptodate;
+ __entry->fresh = fresh;
+ ),
+ TP_printk("dev=%u:%u ino=%lu ind=%llu slot=%u val=%llu uptodate=%d fresh=%d",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->blk, __entry->slot,
+ __entry->slotval, __entry->uptodate, __entry->fresh)
+);
+
+/*
+ * One parity slot written.
+ *
+ * A region block holds fourteen slots under RS, and several inodes'
+ * indirect blocks land in the same one. generic/464 leaves slots
+ * describing a block that is not the block on the medium -- subblocks
+ * 0..6 with data and no parity, 7..15 with parity and no data, two
+ * states in one slot -- and the question is which writer left what,
+ * and in which order. @nz is how many of the indirect block's pointers
+ * were set when the parity was taken, so a slot can be matched to the
+ * block it was computed from.
+ */
+TRACE_EVENT(beamfs_parity_slot,
+ TP_PROTO(dev_t dev, unsigned long ino, u64 phys, u64 region, unsigned int slot,
+ unsigned int nz),
+ TP_ARGS(dev, ino, phys, region, slot, nz),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(u64, phys)
+ __field(u64, region)
+ __field(unsigned int, slot)
+ __field(unsigned int, nz)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->phys = phys;
+ __entry->region = region;
+ __entry->slot = slot;
+ __entry->nz = nz;
+ ),
+ TP_printk("dev=%u:%u ino=%lu ind=%llu region=%llu slot=%u ptrs=%u",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->phys, __entry->region,
+ __entry->slot, __entry->nz)
+);
+TRACE_EVENT(beamfs_block_alloc,
+ TP_PROTO(dev_t dev, unsigned long ino, u64 blk, unsigned int level),
+ TP_ARGS(dev, ino, blk, level),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(u64, blk)
+ __field(unsigned int, level)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->blk = blk;
+ __entry->level = level;
+ ),
+ TP_printk("dev=%u:%u ino=%lu blk=%llu lvl=%u",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->blk, __entry->level)
+);
+
+TRACE_EVENT(beamfs_block_free,
+ TP_PROTO(dev_t dev, unsigned long ino, u64 blk, unsigned long site),
+ TP_ARGS(dev, ino, blk, site),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(u64, blk)
+ __field(unsigned long, site)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->blk = blk;
+ __entry->site = site;
+ ),
+ /*
+ * site names the caller, by its return address.
+ *
+ * It was the constant 0 in every one of 56215 frees, so a capture
+ * could say a block had been freed and never which of the
+ * twenty-nine call sites did it. That matters here more than
+ * anywhere: freeing a block whose buffer is still on an inode's
+ * metadata list is how a later owner's pointers are lost, the
+ * fix is right for some callers and wrong for others -- applied
+ * to all of them it turned 58 leaked blocks into 345 -- and
+ * telling them apart needs the caller's name.
+ */
+ TP_printk("dev=%u:%u ino=%lu blk=%llu site=%pS",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->blk, (void *)__entry->site)
+);
+
+/*
+ * An inode's metadata buffers, written or dropped.
+ *
+ * An indirect block is dirtied and put on the inode's metadata list,
+ * and something has to take it from there to the medium. Two things
+ * can: mmb_sync, from write_inode and from sync_inode_metadata, which
+ * writes them; and mmb_invalidate, from evict_inode, which does not --
+ * it empties the list and the buffers go with whatever they held.
+ *
+ * generic/464 leaves one indirect block per run filled with 0xcd, the
+ * kernel's poison for memory nobody wrote: block 41641 of inode 24 in
+ * one capture, 35433 of inode 65 in the next, each named by its inode
+ * on disk and each never written. Something is taking these buffers
+ * off the list without writing them, and @how says which.
+ *
+ * @n is how many buffers the list held when the call started, so a
+ * sync that finds nothing and an evict that drops four are told apart.
+ */
+TRACE_EVENT(beamfs_mmb,
+ TP_PROTO(dev_t dev, unsigned long ino, const char *how, int n, int err),
+ TP_ARGS(dev, ino, how, n, err),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __string(how, how)
+ __field(int, n)
+ __field(int, err)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __assign_str(how);
+ __entry->n = n;
+ __entry->err = err;
+ ),
+ TP_printk("dev=%u:%u ino=%lu %s buffers=%d err=%d",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __get_str(how), __entry->n, __entry->err)
+);
+/*
+ * What a flush of the bitmap found, and what it managed to write.
+ *
+ * The bitmap's blocks are the one place a freed block is recorded, and
+ * 47% of the updates to them are attached to no inode at all -- the
+ * free path passes no owner -- so nothing carries them to the medium
+ * except the flusher, sync_fs and put_super. Those two are static and
+ * inlined, so a kprobe cannot count them and the question "did the
+ * free reach the disk" had no answer from outside.
+ *
+ * @dirty is how many bitmap blocks were dirty when the flush started,
+ * @written how many reached the medium, @err the first failure.
+ */
+TRACE_EVENT(beamfs_bitmap_flush,
+ TP_PROTO(dev_t dev, const char *where, unsigned int dirty,
+ unsigned int written, int err),
+ TP_ARGS(dev, where, dirty, written, err),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __string(where, where)
+ __field(unsigned int, dirty)
+ __field(unsigned int, written)
+ __field(int, err)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __assign_str(where);
+ __entry->dirty = dirty;
+ __entry->written = written;
+ __entry->err = err;
+ ),
+ TP_printk("dev=%u:%u %s dirty=%u written=%u err=%d",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __get_str(where), __entry->dirty, __entry->written,
+ __entry->err)
+);
+
+TRACE_EVENT(beamfs_write_inode,
+ TP_PROTO(dev_t dev, unsigned long ino, int sync, u64 indirect, int err),
+ TP_ARGS(dev, ino, sync, indirect, err),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(int, sync)
+ __field(u64, indirect)
+ __field(int, err)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->sync = sync;
+ __entry->indirect = indirect;
+ __entry->err = err;
+ ),
+ TP_printk("dev=%u:%u ino=%lu sync=%d i_indirect=%llu err=%d",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->sync, __entry->indirect, __entry->err)
+);
+
+TRACE_EVENT(beamfs_inode_dirty,
+ TP_PROTO(dev_t dev, unsigned long ino, unsigned long state),
+ TP_ARGS(dev, ino, state),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned long, ino)
+ __field(unsigned long, state)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->ino = ino;
+ __entry->state = state;
+ ),
+ TP_printk("dev=%u:%u ino=%lu state=0x%lx",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->state)
+);
+
+/*
+ * One event per RS decode that found something, on every path: data
+ * read, RMW transit, directory, bitmap, indirect parity, scrub and
+ * superblock. It is raised in beamfs_log_rs_event_flagged, the one
+ * place every decoder reports to, so a capture misses none of them and
+ * none is rate-limited the way the dmesg lines are. The journal in the
+ * superblock keeps the last sixty-four; this keeps as many as the ring
+ * buffer is sized for.
+ *
+ * @blk is the physical block, the number emufi's flip_log gives as
+ * sector / 8, so a flip and its verdict join on it directly. @sub is
+ * the subblock, -1 when the event has none (a superblock or inode
+ * correction). @symbols is how many the decoder corrected, 0 when it
+ * could not; @flags is re_flags as journalled, so uncorrectable
+ * (BIT(1)), RMW-neutralised (BIT(2)) and inode (BIT(3)) read off it.
+ */
+TRACE_EVENT(beamfs_rs_event,
+ TP_PROTO(dev_t dev, u64 blk, int sub, unsigned int symbols, u32 flags),
+ TP_ARGS(dev, blk, sub, symbols, flags),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(u64, blk)
+ __field(int, sub)
+ __field(unsigned int, symbols)
+ __field(u32, flags)
+ ),
+ TP_fast_assign(
+ __entry->dev = dev;
+ __entry->blk = blk;
+ __entry->sub = sub;
+ __entry->symbols = symbols;
+ __entry->flags = flags;
+ ),
+ TP_printk("dev=%u:%u blk=%llu sub=%d symbols=%u flags=0x%x",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->blk, __entry->sub, __entry->symbols,
+ __entry->flags)
+);
+
+#endif /* _BEAMFS_TRACE_H */
+
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH .
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_FILE beamfs_trace
+#include <trace/define_trace.h>
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 03/18] beamfs: add CRC32 and Reed-Solomon layer
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 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 04/18] beamfs: add error budget, alerts and clock Aurelien DESBRIERES
` (14 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
CRC32 and Reed-Solomon FEC built on lib/reed_solomon: RS(255,239) over
GF(2^8), primitive polynomial 0x187, sixteen parity symbols for each
239-byte subblock, so up to eight corrupted symbols per codeword are
corrected.
The decoder keeps its scratch buffers in struct rs_control, so one
rs_control shared by concurrent callers corrupts their intermediate
state and reports spurious uncorrectable codewords. beamfs allocates
one rs_control per possible CPU. rslib.h does not state this contract;
a patch documenting it was posted to linux-kernel on 2026-05-13.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/edac.c | 785 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 785 insertions(+)
create mode 100644 fs/beamfs/edac.c
diff --git a/fs/beamfs/edac.c b/fs/beamfs/edac.c
new file mode 100644
index 000000000000..1c823bb29611
--- /dev/null
+++ b/fs/beamfs/edac.c
@@ -0,0 +1,785 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - EDAC layer: CRC32 + Reed-Solomon FEC
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Reed-Solomon encoding/decoding uses the kernel's lib/reed_solomon
+ * library (RS(255,239) over GF(2^8), primitive polynomial 0x187)
+ * rather than a private codec: the library is already in the kernel,
+ * well tested, and used by NAND MTD drivers, the RAM oops logger and
+ * dm-verity FEC.
+ *
+ * RS parameters, as passed to init_rs():
+ * symsize = 8 (GF(2^8))
+ * fcr = 0 (first consecutive root)
+ * prim = 1 (primitive element)
+ * nroots = BEAMFS_RS_PARITY = 16 (parity symbols)
+ * data per subblock: BEAMFS_SUBBLOCK_DATA = 239 bytes
+ * codeword length: 255 bytes (BEAMFS_SUBBLOCK_TOTAL)
+ */
+
+#include <linux/kernel.h>
+#include <linux/crc32.h>
+#include <linux/slab.h>
+#include <linux/rslib.h>
+#include "beamfs.h"
+
+/*
+ * RS codec handles -- ONE PER POSSIBLE CPU.
+ *
+ * lib/reed_solomon/decode_rs.c uses scratch buffers (lambda, syn, b,
+ * t, omega, root, reg, loc) stored inside rs_control->buffers[]. A
+ * shared rs_control across concurrent callers races on those buffers,
+ * producing spurious 'uncorrectable' results under parallel decode
+ * load (reproduced May 2026: 8 parallel sha256sum on a beamfs RO
+ * mount -> 160+ uncorrectable per batch).
+ *
+ * This is the library's documented contract: rslib.h describes
+ * rs_control as per instance, and decode_rs8()'s kernel-doc says the
+ * caller has to ensure that decoder invocations are serialized.
+ *
+ * Per-CPU allocation gives each CPU its own rs_control + buffers[].
+ * Combined with preempt-disable via get_cpu_ptr/put_cpu_ptr, this
+ * eliminates the race without locks.
+ */
+static struct rs_control * __percpu *beamfs_rs_ctrl_pcpu;
+
+/*
+ * A codeword's worth of scratch, per cpu.
+ *
+ * Interleaving means a codeword's symbols are not contiguous, and
+ * encode_rs8 takes a contiguous buffer: lib/reed_solomon has no notion
+ * of a stride and is not ours to change. So the symbols are gathered
+ * into this and handed over.
+ *
+ * Per cpu rather than on the stack: 239 bytes is a quarter of a
+ * kernel frame, and this sits under iomap and writeback, where the
+ * frame is already deep. Per cpu rather than from the scratch pool:
+ * this file has no super_block and should not need one -- it is the
+ * codec, not the filesystem.
+ *
+ * Taken with get_cpu_ptr, which disables preemption for the length of
+ * one codeword: 239 bytes of Galois arithmetic, microseconds.
+ */
+static u8 * __percpu *beamfs_rs_scratch_pcpu;
+
+/*
+ * beamfs_rs_init_tables - initialize the RS codec
+ * Called once from beamfs_init() before any mount.
+ *
+ * Returns 0, or -ENOMEM when the per-CPU codecs cannot be set up.
+ * A CPU left without a scratch buffer is not an error.
+ */
+int beamfs_rs_init_tables(void)
+{
+ unsigned int cpu, missing = 0;
+ struct rs_control *ctrl;
+
+ /*
+ * init_rs(symsize=8, gfpoly=0x187, fcr=0, prim=1, nroots=16)
+ * -> GF(2^8), primitive poly x^8+x^7+x^2+x+1, 16 parity symbols
+ * (corrects up to 8 errors per shortened RS(255,239) subblock).
+ *
+ * Per-CPU: alloc the per-CPU pointer slot, then init_rs once per
+ * possible CPU. Each rs_control carries its own scratch buffers[].
+ */
+ beamfs_rs_scratch_pcpu = alloc_percpu(u8 *);
+ if (!beamfs_rs_scratch_pcpu) {
+ /*
+ * Not fatal: the contiguous paths never touch it and the
+ * interleaved ones check before use, so a volume that
+ * needs interleaving refuses rather than the module.
+ */
+ pr_err("beamfs: no per-CPU RS scratch; interleaved blocks will be refused\n");
+ }
+
+ beamfs_rs_ctrl_pcpu = alloc_percpu(struct rs_control *);
+ if (!beamfs_rs_ctrl_pcpu) {
+ free_percpu(beamfs_rs_scratch_pcpu);
+ beamfs_rs_scratch_pcpu = NULL;
+ pr_err("beamfs: failed to alloc per-CPU RS ctrl array\n");
+ return -ENOMEM;
+ }
+
+ for_each_possible_cpu(cpu) {
+ ctrl = init_rs(8, 0x187, 0, 1, BEAMFS_RS_PARITY);
+ if (!ctrl) {
+ pr_err("beamfs: failed to init RS codec for CPU %u\n",
+ cpu);
+ beamfs_rs_exit_tables();
+ return -ENOMEM;
+ }
+ *per_cpu_ptr(beamfs_rs_ctrl_pcpu, cpu) = ctrl;
+ /*
+ * Not fatal either way: the contiguous paths do not use
+ * the scratch, and the interleaved ones check for it.
+ * Without the per-CPU array there is nowhere to keep one.
+ */
+ if (beamfs_rs_scratch_pcpu) {
+ u8 *scratch = kmalloc(BEAMFS_SUBBLOCK_DATA, GFP_KERNEL);
+
+ if (!scratch)
+ missing++;
+ *per_cpu_ptr(beamfs_rs_scratch_pcpu, cpu) = scratch;
+ }
+ }
+
+ if (missing)
+ pr_warn("beamfs: no RS scratch on %u CPU(s); interleaved blocks will be refused there\n",
+ missing);
+
+ pr_debug("beamfs: RS codec initialized per-CPU (RS(%d,%d), %u CPUs)\n",
+ BEAMFS_SUBBLOCK_TOTAL, BEAMFS_SUBBLOCK_DATA,
+ num_possible_cpus());
+ return 0;
+}
+
+/*
+ * beamfs_rs_exit_tables - release the RS codec, at module exit or after
+ * a failed beamfs_rs_init_tables
+ */
+void beamfs_rs_exit_tables(void)
+{
+ unsigned int cpu;
+ struct rs_control *ctrl;
+
+ if (!beamfs_rs_ctrl_pcpu)
+ return;
+
+ for_each_possible_cpu(cpu) {
+ ctrl = *per_cpu_ptr(beamfs_rs_ctrl_pcpu, cpu);
+ if (ctrl) {
+ free_rs(ctrl);
+ *per_cpu_ptr(beamfs_rs_ctrl_pcpu, cpu) = NULL;
+ }
+ }
+
+ if (beamfs_rs_scratch_pcpu) {
+ unsigned int cpu2;
+
+ for_each_possible_cpu(cpu2)
+ kfree(*per_cpu_ptr(beamfs_rs_scratch_pcpu, cpu2));
+ free_percpu(beamfs_rs_scratch_pcpu);
+ beamfs_rs_scratch_pcpu = NULL;
+ }
+
+ free_percpu(beamfs_rs_ctrl_pcpu);
+ beamfs_rs_ctrl_pcpu = NULL;
+}
+
+/*
+ * Shannon entropy term LUT in Q16.16 fixed point.
+ *
+ * LUT[N][k] = round(-(k/N) * log2(k/N) * 2^16) for N in [1,8],
+ * = 0 for k == 0 (or N == 0, unused).
+ *
+ * N is the number of corrected positions in a codeword, at most
+ * BEAMFS_RS_PARITY / 2, and k how many of them fall in one bin. The
+ * entries are generated from the formula above, not typed: a change
+ * to BEAMFS_RS_PARITY means regenerating the table, not editing it.
+ */
+static const __u32 beamfs_rs_entropy_term_q16_16[9][9] = {
+ [0] = { 0, }, /* [0] unused */
+ [1] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+ [2] = { 0, 32768, 0, 0, 0, 0, 0, 0, 0 },
+ [3] = { 0, 34624, 25557, 0, 0, 0, 0, 0, 0 },
+ [4] = { 0, 32768, 32768, 20400, 0, 0, 0, 0, 0 },
+ [5] = { 0, 30434, 34654, 28979, 16878, 0, 0, 0, 0 },
+ [6] = { 0, 28235, 34624, 32768, 25557, 14365, 0, 0, 0 },
+ [7] = { 0, 26283, 33842, 34333, 30235, 22724, 12493, 0, 0 },
+ [8] = { 0, 24576, 32768, 34776, 32768, 27774, 20400, 11047, 0 },
+};
+
+/*
+ * beamfs_rs_compute_entropy_q16_16 -- Shannon entropy estimator over the
+ * spatial distribution of corrected byte positions in an RS codeword.
+ *
+ * @positions: byte positions corrected (output of beamfs_rs_decode)
+ * @n_positions: number of valid entries; MUST be in [1, BEAMFS_RS_PARITY/2]
+ * @code_len_bytes: data length of the codeword, in [n_positions, 239]
+ *
+ * Returns Shannon H = sum_{bin} LUT[N][k_bin] in Q16.16 fixed point,
+ * range [0, 3*65536). Deterministic, no FPU; one u32 division per
+ * position for bin index computation.
+ *
+ * Pre: positions != NULL, 1 <= n_positions <= BEAMFS_RS_PARITY/2,
+ * code_len_bytes >= n_positions
+ *
+ * Caller decides whether to set BEAMFS_RS_EVENT_FLAG_ENTROPY_VALID:
+ * a single sample (n_positions == 1) yields H = 0 mathematically
+ * but is forensically non-significant; beamfs_log_rs_event_flagged
+ * does not call this for one and leaves the flag clear.
+ */
+__u32 beamfs_rs_compute_entropy_q16_16(const u8 *positions,
+ unsigned int n_positions,
+ size_t code_len_bytes)
+{
+ unsigned int bin_count[BEAMFS_RS_ENTROPY_BINS];
+ __u32 h = 0;
+ unsigned int i, b;
+
+ if (WARN_ON_ONCE(!positions))
+ return 0;
+ if (WARN_ON_ONCE(n_positions == 0 ||
+ n_positions > BEAMFS_RS_PARITY / 2))
+ return 0;
+ if (WARN_ON_ONCE(code_len_bytes < n_positions ||
+ code_len_bytes > BEAMFS_SUBBLOCK_DATA))
+ return 0;
+
+ memset(bin_count, 0, sizeof(bin_count));
+
+ for (i = 0; i < n_positions; i++) {
+ /* bin = pos * BINS / code_len. u32 product fits since
+ * pos < 239 and BINS = 8, max product = 238 * 8 = 1904.
+ */
+ unsigned int idx = (unsigned int)positions[i];
+ unsigned int bin;
+
+ if (WARN_ON_ONCE(idx >= code_len_bytes))
+ return 0;
+ bin = (idx * BEAMFS_RS_ENTROPY_BINS) /
+ (unsigned int)code_len_bytes;
+ if (WARN_ON_ONCE(bin >= BEAMFS_RS_ENTROPY_BINS))
+ return 0;
+ bin_count[bin]++;
+ }
+
+ /* H = sum over bins of LUT[N][k_bin]. Bins with k=0 contribute 0
+ * by LUT convention. Total guaranteed < 3 * 65536.
+ */
+ for (b = 0; b < BEAMFS_RS_ENTROPY_BINS; b++)
+ h += beamfs_rs_entropy_term_q16_16[n_positions][bin_count[b]];
+
+ return h;
+}
+
+/*
+ * beamfs_rs_encode - encode @len data bytes, produce BEAMFS_RS_PARITY parity
+ * @data: input data (@len bytes, must be <= BEAMFS_SUBBLOCK_DATA)
+ * @len: number of data bytes (BEAMFS_SUBBLOCK_DATA for the bitmap path,
+ * BEAMFS_INODE_RS_DATA for inodes, etc.)
+ * @parity: output parity (BEAMFS_RS_PARITY bytes)
+ *
+ * lib/reed_solomon supports shortened RS codes natively: passing a
+ * length less than BEAMFS_SUBBLOCK_DATA produces a valid codeword,
+ * mathematically equivalent to padding the data with zeros up to the
+ * full subblock length. The same length must be passed to the matching
+ * beamfs_rs_decode call.
+ *
+ * encode_rs8() takes the data as bytes and returns the parity in a u16
+ * buffer, one symbol in the low byte of each entry.
+ */
+int beamfs_rs_encode(u8 *data, size_t len, u8 *parity)
+{
+ u16 par[BEAMFS_RS_PARITY];
+ struct rs_control **ctrl_p;
+ struct rs_control *ctrl;
+ int i;
+
+ if (!beamfs_rs_ctrl_pcpu)
+ return -EINVAL;
+ if (len > BEAMFS_SUBBLOCK_DATA)
+ return -EINVAL;
+
+ memset(par, 0, sizeof(par));
+ ctrl_p = get_cpu_ptr(beamfs_rs_ctrl_pcpu);
+ ctrl = *ctrl_p;
+ if (!ctrl) {
+ put_cpu_ptr(beamfs_rs_ctrl_pcpu);
+ return -EINVAL;
+ }
+ encode_rs8(ctrl, data, len, par, 0);
+ put_cpu_ptr(beamfs_rs_ctrl_pcpu);
+
+ for (i = 0; i < BEAMFS_RS_PARITY; i++)
+ parity[i] = (u8)par[i];
+
+ return 0;
+}
+
+/*
+ * beamfs_rs_decode -- decode and correct a shortened RS codeword in place,
+ * optionally exposing the list of corrected byte
+ * positions for forensic entropy logging.
+ *
+ * @data: data bytes (@len bytes), corrected in place on success
+ * @len: number of data bytes (must match the length passed to encode)
+ * @parity: parity bytes (BEAMFS_RS_PARITY); read, never rewritten
+ * @positions: optional output, up to @max_positions entries; on a
+ * positive return holds the corrected DATA byte positions
+ * in [0, len), ascending. May be NULL if no entropy
+ * logging is needed.
+ * @max_positions: capacity of @positions; ignored if @positions is NULL.
+ *
+ * Returns:
+ * < 0 uncorrectable (-EBADMSG) or invalid input (-EINVAL)
+ * = 0 no errors detected
+ * > 0 with @positions: the number of DATA bytes corrected, which is
+ * the number of entries written to @positions; without it, the
+ * number of symbols corrected, parity included.
+ *
+ * decode_rs8() either applies its corrections or reports them (see
+ * lib/reed_solomon/decode_rs.c: with eras_pos and corr it stores each
+ * correction's position and pattern and touches nothing). Asked to
+ * report, we apply the data corrections ourselves and leave the
+ * parity alone: the 239-byte snapshot this function used to take, to
+ * find the corrected bytes by diff, is gone from the stack. A parity
+ * symbol decode_rs8 would have corrected is not counted when the
+ * caller wants positions, so that the count it gets is the count of
+ * entries it may read: alloc.c and the others hand that count on to
+ * the journal.
+ */
+int beamfs_rs_decode(u8 *data, size_t len, u8 *parity,
+ u8 *positions, unsigned int max_positions,
+ const char *who)
+{
+ u16 par[BEAMFS_RS_PARITY];
+ u16 corr[BEAMFS_RS_PARITY];
+ int pos[BEAMFS_RS_PARITY];
+ int i, j, nerr;
+ unsigned int n_data = 0;
+
+ if (!beamfs_rs_ctrl_pcpu)
+ return -EINVAL;
+ if (len > BEAMFS_SUBBLOCK_DATA)
+ return -EINVAL;
+ if (positions && max_positions == 0)
+ return -EINVAL;
+
+ for (i = 0; i < BEAMFS_RS_PARITY; i++)
+ par[i] = parity[i];
+
+ /*
+ * Per-CPU: get_cpu_ptr disables preemption while decode runs in
+ * the CPU-local rs_control. decode_rs8 is pure computation, no
+ * allocs, no sleeps -- preempt-disable is safe.
+ */
+ {
+ struct rs_control **ctrl_p = get_cpu_ptr(beamfs_rs_ctrl_pcpu);
+ struct rs_control *ctrl = *ctrl_p;
+
+ if (!ctrl) {
+ put_cpu_ptr(beamfs_rs_ctrl_pcpu);
+ return -EINVAL;
+ }
+ nerr = decode_rs8(ctrl, data, par, len,
+ NULL, 0, pos, 0, corr);
+ put_cpu_ptr(beamfs_rs_ctrl_pcpu);
+ }
+
+ if (nerr < 0) {
+ /*
+ * Silent here, on purpose.
+ *
+ * This function knows a length and nothing else -- not the
+ * block, not which of the sixteen subblocks, not what owns
+ * it. Every caller has results[] and its own context, and
+ * reports from there.
+ */
+ return -EBADMSG;
+ }
+ if (nerr > BEAMFS_RS_PARITY)
+ return -EBADMSG;
+
+ /*
+ * Positions come out in locator order; the journal and its
+ * entropy estimate were written for ascending ones.
+ */
+ for (i = 1; i < nerr; i++) {
+ int p = pos[i];
+ u16 c = corr[i];
+
+ for (j = i - 1; j >= 0 && pos[j] > p; j--) {
+ pos[j + 1] = pos[j];
+ corr[j + 1] = corr[j];
+ }
+ pos[j + 1] = p;
+ corr[j + 1] = c;
+ }
+
+ for (i = 0; i < nerr; i++) {
+ if (pos[i] < 0 || pos[i] >= (int)len)
+ continue; /* a parity symbol: the copy is dropped */
+ data[pos[i]] ^= (u8)corr[i];
+ if (positions && n_data < max_positions)
+ positions[n_data] = (u8)pos[i];
+ n_data++;
+ }
+
+ if (positions)
+ return (int)min(n_data, max_positions);
+ return nerr;
+}
+
+/*
+ * Where symbol @i of codeword @sub lives once interleaved.
+ *
+ * Contiguous, codeword j owns bytes [j*239, (j+1)*239): a nine-byte
+ * burst lands entirely in it and takes it past correction.
+ *
+ * Interleaved, the same burst puts at most one symbol in any codeword.
+ * 129 consecutive bytes are needed to lose one, for not a byte of
+ * extra parity.
+ *
+ * The parity stays with its codeword, sixteen bytes in a row: nine bad
+ * bytes among them still lose that codeword. Spreading the parity as
+ * well would take a different on-disk layout.
+ */
+static inline size_t rs_woven_off(unsigned int sub, size_t i,
+ unsigned int n_subblocks)
+{
+ return i * n_subblocks + sub;
+}
+
+/*
+ * Gather one codeword out of an interleaved block.
+ *
+ * @scratch must hold @data_len bytes. The caller owns it: this runs on
+ * the writeback path and an allocation here would be one more thing to
+ * fail under the memory pressure that writeback is trying to relieve.
+ */
+static void rs_gather(const u8 *blk, unsigned int sub, size_t data_len,
+ unsigned int n_subblocks, u8 *scratch)
+{
+ size_t i;
+
+ for (i = 0; i < data_len; i++)
+ scratch[i] = blk[rs_woven_off(sub, i, n_subblocks)];
+}
+
+static void rs_scatter(u8 *blk, unsigned int sub, size_t data_len,
+ unsigned int n_subblocks, const u8 *scratch)
+{
+ size_t i;
+
+ for (i = 0; i < data_len; i++)
+ blk[rs_woven_off(sub, i, n_subblocks)] = scratch[i];
+}
+
+/*
+ * beamfs_rs_encode_woven -- encode a block whose symbols are interleaved.
+ *
+ * @data_buf: the interleaved data area, @data_len * @n_subblocks bytes
+ * @parity_buf: where the parity goes, @n_subblocks codewords of it
+ * @parity_stride: bytes from one codeword's parity to the next
+ *
+ * Same contract as beamfs_rs_encode_region, same return values. The
+ * difference is where the symbols of a codeword are read from.
+ */
+int beamfs_rs_encode_woven(u8 *data_buf, u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks)
+{
+ unsigned int i;
+ int ret = 0;
+ u8 **sp, *scratch;
+
+ if (!data_buf || !parity_buf || !beamfs_rs_scratch_pcpu)
+ return -EINVAL;
+ if (data_len > BEAMFS_SUBBLOCK_DATA)
+ return -EINVAL;
+
+ /*
+ * Preemption off for the whole block rather than per codeword:
+ * sixteen encodes of 239 bytes, and taking the pointer sixteen
+ * times costs more than holding it once.
+ */
+ sp = get_cpu_ptr(beamfs_rs_scratch_pcpu);
+ scratch = *sp;
+ if (!scratch) {
+ put_cpu_ptr(beamfs_rs_scratch_pcpu);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < n_subblocks; i++) {
+ rs_gather(data_buf, i, data_len, n_subblocks, scratch);
+ ret = beamfs_rs_encode(scratch, data_len,
+ parity_buf + (size_t)i * parity_stride);
+ if (ret < 0)
+ break;
+ }
+
+ put_cpu_ptr(beamfs_rs_scratch_pcpu);
+ return ret < 0 ? ret : 0;
+}
+
+/*
+ * beamfs_rs_decode_woven -- decode one, correcting in place.
+ *
+ * A codeword that decodes is written back into the block; one that
+ * does not is left as it was, so a caller that ignores the result
+ * reads what the medium gave rather than a half-corrected mixture.
+ */
+int beamfs_rs_decode_woven(u8 *data_buf, u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks,
+ int *results, const char *who)
+{
+ unsigned int i;
+ int worst = 0;
+ u8 **sp, *scratch;
+
+ if (!data_buf || !parity_buf || !beamfs_rs_scratch_pcpu)
+ return -EINVAL;
+ if (data_len > BEAMFS_SUBBLOCK_DATA)
+ return -EINVAL;
+
+ sp = get_cpu_ptr(beamfs_rs_scratch_pcpu);
+ scratch = *sp;
+ if (!scratch) {
+ put_cpu_ptr(beamfs_rs_scratch_pcpu);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < n_subblocks; i++) {
+ int rc;
+
+ rs_gather(data_buf, i, data_len, n_subblocks, scratch);
+ rc = beamfs_rs_decode(scratch, data_len,
+ parity_buf + (size_t)i * parity_stride,
+ NULL, 0, who);
+ if (results)
+ results[i] = rc;
+ if (rc < 0) {
+ worst = rc;
+ continue;
+ }
+ rs_scatter(data_buf, i, data_len, n_subblocks, scratch);
+ if (rc > worst)
+ worst = rc;
+ }
+
+ put_cpu_ptr(beamfs_rs_scratch_pcpu);
+ return worst;
+}
+
+/*
+ * Encode a 4096-byte block in the grouped layout.
+ *
+ * The block is its own argument rather than a data pointer and a
+ * parity pointer: the two are at fixed offsets within it and passing
+ * them apart invites the mismatch that cost an afternoon -- woven
+ * functions expecting a compact area, callers handing them an
+ * alternating one.
+ *
+ * Returns 0, or the first encode failure.
+ */
+int beamfs_rs_encode_block(u8 *block)
+{
+ if (!block)
+ return -EINVAL;
+
+ return beamfs_rs_encode_woven(block + BEAMFS_DATA_WOVEN_OFF,
+ block + BEAMFS_DATA_WOVEN_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+}
+
+/*
+ * Decode one, correcting in place.
+ *
+ * @results, when given, holds one entry per codeword: the number of
+ * symbols corrected, or negative for one that would not decode.
+ *
+ * Returns the worst of those, so a caller that wants only "is this
+ * block sound" can test the return and a caller that wants to log
+ * which codewords suffered has the array.
+ */
+int beamfs_rs_decode_block(u8 *block, int *results, const char *who)
+{
+ if (!block)
+ return -EINVAL;
+
+ return beamfs_rs_decode_woven(block + BEAMFS_DATA_WOVEN_OFF,
+ block + BEAMFS_DATA_WOVEN_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, who);
+}
+
+/*
+ * Read @len bytes of user data at @off out of a block.
+ *
+ * Grouped, the data is contiguous but the symbols of a codeword are
+ * not: byte @off of the user's data is at @off within the data area,
+ * because interleaving moves symbols within a codeword and not the
+ * bytes the user sees. The two are the same array read two ways.
+ *
+ * This exists so nothing outside edac has to know that.
+ */
+void beamfs_block_read(const u8 *block, size_t off, size_t len, u8 *out)
+{
+ memcpy(out, block + BEAMFS_DATA_WOVEN_OFF + off, len);
+}
+
+void beamfs_block_write(u8 *block, size_t off, size_t len, const u8 *in)
+{
+ memcpy(block + BEAMFS_DATA_WOVEN_OFF + off, in, len);
+}
+
+/*
+ * beamfs_rs_encode_region -- encode N RS(255,239) shortened subblocks
+ * across a region.
+ *
+ * @data_buf: base pointer of the data region
+ * @data_stride: distance in bytes between the start of two
+ * consecutive data subblocks (e.g.
+ * BEAMFS_SUBBLOCK_TOTAL=255 for the bitmap)
+ * @parity_buf: base pointer of the parity region (may equal
+ * data_buf + data_len when data and parity alternate)
+ * @parity_stride: distance between the start of two consecutive
+ * parity blobs (e.g. BEAMFS_SUBBLOCK_TOTAL=255 for
+ * the bitmap, BEAMFS_RS_PARITY=16 for contiguous
+ * parity placement)
+ * @data_len: number of data bytes per subblock (e.g.
+ * BEAMFS_SUBBLOCK_DATA=239 for a full subblock)
+ * @n_subblocks: number of subblocks to encode
+ *
+ * Returns 0 on success, negative on the first encode failure.
+ * On failure the buffer is in an indeterminate state.
+ */
+int beamfs_rs_encode_region(u8 *data_buf, size_t data_stride,
+ u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks)
+{
+ unsigned int i;
+
+ if (!data_buf || !parity_buf)
+ return -EINVAL;
+
+ /*
+ * Contiguous only: codeword i is @data_len bytes at
+ * data_buf + i * data_stride, its parity at
+ * parity_buf + i * parity_stride. Nothing here interleaves.
+ *
+ * A data block on a volume with BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE
+ * goes to beamfs_rs_encode_woven instead; beamfs_seal_data_block
+ * makes that choice. Data blocks on other volumes, directory blocks,
+ * parity region blocks and the bitmap come here with a stride of
+ * BEAMFS_SUBBLOCK_TOTAL, data and parity alternating every 255
+ * bytes. The superblock, the indirect-block slots and rsbench pass
+ * other strides.
+ */
+
+ for (i = 0; i < n_subblocks; i++) {
+ u8 *d = data_buf + (size_t)i * data_stride;
+ u8 *p = parity_buf + (size_t)i * parity_stride;
+ int rc = beamfs_rs_encode(d, data_len, p);
+
+ if (rc < 0)
+ return rc;
+ }
+ return 0;
+}
+
+/*
+ * beamfs_rs_decode_region -- decode N RS(255,239) shortened subblocks
+ * across a region. Symmetric to beamfs_rs_encode_region.
+ *
+ * @results: optional, n_subblocks entries. On exit each
+ * entry holds the return value of beamfs_rs_decode
+ * for the corresponding subblock: < 0 uncorrectable,
+ * = 0 no errors, > 0 number of corrected symbols.
+ * Pass NULL to skip per-subblock reporting.
+ * @positions_buf: optional, flat array of n_subblocks * positions_stride
+ * int entries. On a positive results[i], the first
+ * results[i] entries of positions_buf[i*stride ..] hold
+ * the corrected DATA positions for subblock i. May be
+ * NULL (then positions_stride must be 0).
+ * @positions_stride: slot count per subblock in positions_buf; should be
+ * >= BEAMFS_RS_PARITY/2. Ignored if positions_buf NULL.
+ *
+ * Returns 0 if every subblock decoded successfully, the first
+ * negative error otherwise. Decoding does not stop on error: all
+ * subblocks are processed, results[] reflects the per-subblock
+ * outcome, and the first negative error is the one returned.
+ */
+int beamfs_rs_decode_region(u8 *data_buf, size_t data_stride,
+ u8 *parity_buf, size_t parity_stride,
+ size_t data_len, unsigned int n_subblocks,
+ int *results,
+ u8 *positions_buf,
+ unsigned int positions_stride,
+ const char *who)
+{
+ unsigned int i;
+ int worst = 0;
+
+ if (!data_buf || !parity_buf)
+ return -EINVAL;
+ if (positions_buf && positions_stride == 0)
+ return -EINVAL;
+ if (!positions_buf && positions_stride != 0)
+ return -EINVAL;
+
+ for (i = 0; i < n_subblocks; i++) {
+ u8 *d = data_buf + (size_t)i * data_stride;
+ u8 *p = parity_buf + (size_t)i * parity_stride;
+ u8 *pos = positions_buf
+ ? positions_buf + (size_t)i * positions_stride
+ : NULL;
+ int rc = beamfs_rs_decode(d, data_len, p,
+ pos, positions_stride, who);
+
+ if (results)
+ results[i] = rc;
+ if (rc < 0 && worst >= 0)
+ worst = rc;
+ }
+ return worst;
+}
+
+/*
+ * beamfs_crc32 - compute CRC32 checksum
+ * @buf: data buffer
+ * @len: length in bytes
+ *
+ * Uses the kernel's hardware-accelerated crc32_le (same as ext4/btrfs).
+ * Seed 0xFFFFFFFF, final XOR 0xFFFFFFFF (standard CRC-32/ISO-HDLC).
+ */
+__u32 beamfs_crc32(const void *buf, size_t len)
+{
+ return crc32_le(0xFFFFFFFF, buf, len) ^ 0xFFFFFFFF;
+}
+
+/*
+ * beamfs_crc32_sb -- compute CRC32 over the meaningful regions of the
+ * superblock, excluding s_crc32 itself and s_pad.
+ *
+ * Coverage (computed at compile time from struct layout):
+ * region A: [0, offsetof(s_crc32)) -- magic, counters,
+ * version, flags
+ * region B: [offsetof(s_uuid), offsetof(s_pad)) -- uuid, label,
+ * RS journal,
+ * bitmap_blk, features,
+ * protection scheme
+ * Total coverage: BEAMFS_SB_RS_COVERAGE_BYTES, asserted at build time
+ * by BUILD_BUG_ON below.
+ *
+ * Chained via crc32_le without intermediate XOR. Must match the
+ * userspace mkfs.beamfs::crc32_sb() byte-for-byte so that superblocks
+ * formatted by mkfs validate at mount time.
+ *
+ * On-disk format compatibility: a superblock written by an mkfs whose
+ * struct layout differs from this kernel's fails this CRC. Images of
+ * an older on-disk version do not get this far: fill_super refuses
+ * them on s_version first.
+ */
+__u32 beamfs_crc32_sb(const struct beamfs_super_block *fsb)
+{
+ const u8 *base = (const u8 *)fsb;
+ 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);
+ u32 c;
+
+ BUILD_BUG_ON(sizeof_field(struct beamfs_super_block, s_crc32) != 4);
+ BUILD_BUG_ON(off_uuid != off_crc32 + 4);
+ BUILD_BUG_ON(off_crc32 + (off_pad - off_uuid) !=
+ BEAMFS_SB_RS_COVERAGE_BYTES);
+
+ c = crc32_le(0xFFFFFFFF, base, off_crc32);
+ c = crc32_le(c, base + off_uuid, off_pad - off_uuid);
+ return c ^ 0xFFFFFFFF;
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 04/18] beamfs: add error budget, alerts and clock
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (2 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 03/18] beamfs: add CRC32 and Reed-Solomon layer Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 05/18] beamfs: add indirect parity, tree checker Aurelien DESBRIERES
` (13 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
budget.c records, for each data block, the largest number of symbols
corrected in any of its codewords: a high-water mark of the correction
capacity the block has used, which reads as the margin it has left.
alert.c sends a KOBJ_CHANGE uevent on the filesystem's kobject when a
threshold is crossed: a block reached zero margin, uncorrectable
codewords within an interval, or blocks at zero margin. Notifications
are rate-limited per class; what to do about them is left to
userspace.
clock.c anchors the monotonic timestamps of the event journal to the
wall clock, with the accuracy of that clock as declared by the
clock_source module parameter.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/alert.c | 153 +++++++++++++++++++++++++++
fs/beamfs/budget.c | 252 +++++++++++++++++++++++++++++++++++++++++++++
fs/beamfs/clock.c | 132 ++++++++++++++++++++++++
3 files changed, 537 insertions(+)
create mode 100644 fs/beamfs/alert.c
create mode 100644 fs/beamfs/budget.c
create mode 100644 fs/beamfs/clock.c
diff --git a/fs/beamfs/alert.c b/fs/beamfs/alert.c
new file mode 100644
index 000000000000..27fcba769742
--- /dev/null
+++ b/fs/beamfs/alert.c
@@ -0,0 +1,153 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- threshold alerting
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Everything beamfs knows about its own health is already recorded:
+ * dmesg carries each uncorrectable, the RS journal holds coordinates,
+ * the error budget holds remaining margin. All of it has to be looked
+ * at, and on an unattended system nobody looks.
+ *
+ * This notifies. A uevent on the filesystem's kobject when a threshold
+ * is crossed, which udev, systemd or a bare daemon can act on. The
+ * kernel says a line has been crossed and stops there; deciding what
+ * that means -- page an operator, schedule a copy, ground the aircraft
+ * -- is policy, and policy does not belong in a filesystem.
+ *
+ * Three thresholds, each answering a different question:
+ *
+ * a block reached zero margin is this volume about to lose data
+ * uncorrectables in an interval is it losing data now
+ * blocks at zero margin how much of it is at the edge
+ *
+ * Rate-limited per event class, because the failure mode this must not
+ * have is a volume degrading fast enough to bury the operator in
+ * notifications about it.
+ *
+ * No overlap with the report side: this signals that something
+ * happened, and the RS journal, read by the userspace decoder, says
+ * what. A notification carrying its own diagnosis would duplicate the
+ * decoder and drift from it.
+ */
+
+#include <linux/fs.h>
+#include <linux/kobject.h>
+#include <linux/jiffies.h>
+#include "beamfs.h"
+
+/* Minimum gap between two notifications of the same class. */
+#define BEAMFS_ALERT_INTERVAL_MS 60000
+
+/*
+ * beamfs_alert -- emit one, if the class has not fired recently.
+ *
+ * @class: index into the per-class rate limiter
+ * @reason: short token, the thing a udev rule matches on
+ * @detail: what was seen, for a human reading the log
+ */
+static void beamfs_alert(struct super_block *sb, unsigned int class,
+ const char *reason, const char *detail)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ char *envp[4];
+ char buf_reason[64];
+ char buf_detail[96];
+ unsigned long now = jiffies;
+
+ if (class >= BEAMFS_ALERT_CLASSES)
+ return;
+ if (!sbi->s_kobj.state_initialized)
+ return;
+
+ /*
+ * time_before handles wraparound; a zero last_alert means never,
+ * and jiffies zero at boot would otherwise suppress the first
+ * alert of the machine's life.
+ */
+ if (sbi->s_alert_last[class] &&
+ time_before(now, sbi->s_alert_last[class] +
+ msecs_to_jiffies(BEAMFS_ALERT_INTERVAL_MS)))
+ return;
+ sbi->s_alert_last[class] = now ? now : 1;
+
+ snprintf(buf_reason, sizeof(buf_reason), "BEAMFS_REASON=%s", reason);
+ snprintf(buf_detail, sizeof(buf_detail), "BEAMFS_DETAIL=%s", detail);
+
+ envp[0] = buf_reason;
+ envp[1] = buf_detail;
+ envp[2] = "BEAMFS_ALERT=1";
+ envp[3] = NULL;
+
+ kobject_uevent_env(&sbi->s_kobj, KOBJ_CHANGE, envp);
+
+ pr_warn("beamfs: alert %s: %s\n", reason, detail);
+}
+
+/*
+ * beamfs_alert_uncorrectable -- a block could not be recovered.
+ *
+ * The most serious of the three: data has been lost, not merely put at
+ * risk. Fires on the first one and then no more often than the
+ * interval, since a volume shedding blocks would otherwise bury the
+ * operator in notices about it.
+ */
+void beamfs_alert_uncorrectable(struct super_block *sb, u64 phys)
+{
+ char detail[96];
+
+ snprintf(detail, sizeof(detail), "block %llu unrecoverable",
+ (unsigned long long)phys);
+ beamfs_alert(sb, BEAMFS_ALERT_UNCORRECTABLE, "uncorrectable", detail);
+}
+
+/*
+ * beamfs_alert_margin -- a block has no correction capacity left.
+ *
+ * It still reads correctly. The next upset in the wrong subblock does
+ * not, and this is the only warning that will come before that read.
+ * Which is the whole argument for the error budget: without it there
+ * is nothing to notify about until the data is already gone.
+ */
+void beamfs_alert_margin(struct super_block *sb, u64 phys, unsigned int used)
+{
+ char detail[96];
+
+ if (used < BEAMFS_ERROR_BUDGET_MAX)
+ return;
+
+ snprintf(detail, sizeof(detail),
+ "block %llu has consumed all %u correctable symbols",
+ (unsigned long long)phys, (unsigned int)BEAMFS_ERROR_BUDGET_MAX);
+ beamfs_alert(sb, BEAMFS_ALERT_MARGIN, "no-margin", detail);
+}
+
+/*
+ * beamfs_alert_check_rate -- has correction activity crossed the line?
+ *
+ * Called by the scrubber at the end of a sweep, where a whole-volume
+ * figure exists. s_alert_rate_limit is the number of corrections per
+ * sweep an operator is willing to see; zero disables the check, which
+ * is the default because the right number depends on the deployment
+ * and inventing one would produce either noise or silence.
+ */
+void beamfs_alert_check_rate(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 delta;
+ char detail[96];
+
+ if (!sbi->s_alert_rate_limit)
+ return;
+
+ delta = sbi->s_scrub_corrected - sbi->s_alert_last_corrected;
+ sbi->s_alert_last_corrected = sbi->s_scrub_corrected;
+
+ if (delta < sbi->s_alert_rate_limit)
+ return;
+
+ snprintf(detail, sizeof(detail),
+ "%llu blocks corrected in one sweep, limit %u",
+ delta, sbi->s_alert_rate_limit);
+ beamfs_alert(sb, BEAMFS_ALERT_RATE, "correction-rate", detail);
+}
diff --git a/fs/beamfs/budget.c b/fs/beamfs/budget.c
new file mode 100644
index 000000000000..61e858d3cfe8
--- /dev/null
+++ b/fs/beamfs/budget.c
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- per-block error budget
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * How much correction capacity a block has left, rather than how many
+ * corrections it has had.
+ *
+ * The RS journal holds the last 64 events. That answers "what happened
+ * recently" and says nothing about the state of the volume: a block
+ * that has quietly taken seven of its eight correctable symbols reads
+ * back perfectly and is one upset away from unrecoverable, and nothing
+ * anywhere would say so before the read that fails.
+ *
+ * One byte per data block records the worst subblock's corrected-symbol
+ * count. An operator can then ask how much margin is left -- a fuel
+ * gauge rather than an odometer.
+ *
+ * btrfs and ZFS count corrections and do not report margin, because
+ * with redundancy elsewhere there is no margin to report: a bad copy is
+ * replaced from a good one and the question does not arise. On a single
+ * device it does, and the answer is finite and worth knowing.
+ *
+ * The byte is a high-water mark, not a running total. What matters is
+ * the worst any subblock of the block has been, since that is the one
+ * that will saturate first; a count that decayed would report a
+ * healthier block than the medium justifies.
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include "beamfs.h"
+
+/* Is the feature active on this volume? */
+static bool budget_active(struct beamfs_sb_info *sbi)
+{
+ return sbi->s_budget_blk != 0 && sbi->s_budget_len != 0;
+}
+
+/*
+ * Where the byte for @phys lives: which region block, and the offset
+ * within it. Computed, not looked up -- a table mapping blocks to
+ * slots would be metadata needing protection of its own, and the
+ * recursion has to stop somewhere.
+ */
+static bool budget_slot(struct super_block *sb, u64 phys,
+ u64 *region_blk, u32 *offset)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 index;
+
+ if (!budget_active(sbi) || phys < sbi->s_data_start)
+ return false;
+
+ index = phys - sbi->s_data_start;
+ *region_blk = sbi->s_budget_blk + index / BEAMFS_BLOCK_SIZE;
+ *offset = (u32)(index % BEAMFS_BLOCK_SIZE);
+
+ return *region_blk < sbi->s_budget_blk + sbi->s_budget_len;
+}
+
+/*
+ * beamfs_budget_record -- note that @phys needed @symbols corrections.
+ *
+ * Raises the high-water mark if this is the worst the block has seen.
+ * A no-op when the value would not change, so a volume being read
+ * repeatedly does not rewrite the region for nothing.
+ */
+void beamfs_budget_record(struct super_block *sb, u64 phys,
+ unsigned int symbols)
+{
+ struct buffer_head *bh;
+ u64 region_blk;
+ u32 offset;
+ u8 capped;
+
+ if (!symbols || !budget_slot(sb, phys, ®ion_blk, &offset))
+ return;
+
+ capped = (u8)min_t(unsigned int, symbols, BEAMFS_ERROR_BUDGET_MAX);
+
+ bh = sb_bread(sb, region_blk);
+ if (!bh)
+ return;
+
+ if (((u8 *)bh->b_data)[offset] < capped) {
+ lock_buffer(bh);
+ ((u8 *)bh->b_data)[offset] = capped;
+ unlock_buffer(bh);
+ mark_buffer_dirty(bh);
+
+ /*
+ * Only on the transition. A block already at the limit
+ * that gets corrected again has not crossed anything, and
+ * notifying every time would say the same thing forever.
+ */
+ beamfs_alert_margin(sb, phys, capped);
+ }
+ brelse(bh);
+}
+
+/*
+ * The next block at or past @from whose margin has fallen to @threshold.
+ *
+ * The budget region is one byte per block -- a quarter of a per mille of
+ * the volume -- so reading it end to end costs four thousand times less
+ * than sweeping the data it describes. It is already the index of where
+ * attention is due; nothing needed building, only reading.
+ *
+ * That asymmetry is the point. A block that has spent six of its eight
+ * correctable symbols is two upsets from unrecoverable and should be
+ * looked at often. A block that has never needed a correction can wait.
+ * Sweeping both at one rate spends the same effort on 262051 untouched
+ * blocks as on the one that matters -- measured, on a volume with
+ * exactly that shape.
+ *
+ * Returns the block number, or 0 when the region holds nothing above the
+ * threshold from @from onward. Reads a budget block at a time rather than
+ * a byte, since the caller walks forward and the buffer cache will have
+ * the next byte already.
+ */
+u64 beamfs_budget_next_worn(struct super_block *sb, u64 from, u8 threshold)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 first, last, phys;
+
+ if (!sbi->s_budget_len)
+ return 0;
+
+ first = max_t(u64, from, sbi->s_data_start);
+ last = sbi->s_data_start + sbi->s_nblocks;
+ phys = first;
+
+ /*
+ * One block read per 4096 blocks examined, not one per block.
+ *
+ * The region is a flat array of one byte per data block, so a
+ * single 4096-byte read covers 4096 of them. Going through
+ * beamfs_budget_read for each would cost an sb_bread and a brelse
+ * per byte: on a 58 GiB volume that is 15115022 calls to find one
+ * worn block, and the sweep asks once per step, every hundred
+ * milliseconds at the default pace.
+ */
+ while (phys < last) {
+ struct buffer_head *bh;
+ u64 region_blk;
+ u32 offset;
+ u32 span;
+ const u8 *p;
+ u32 i;
+
+ if (!budget_slot(sb, phys, ®ion_blk, &offset))
+ return 0;
+
+ /* Blocks described by the remainder of this region block. */
+ span = min_t(u64, BEAMFS_BLOCK_SIZE - offset, last - phys);
+
+ bh = sb_bread(sb, region_blk);
+ if (!bh) {
+ /*
+ * Unreadable region block: skip what it covered
+ * rather than stop. A wear scan that gives up on
+ * one bad block stops watching everything past it.
+ */
+ phys += span;
+ continue;
+ }
+
+ p = (const u8 *)bh->b_data + offset;
+ for (i = 0; i < span; i++) {
+ if (p[i] >= threshold) {
+ brelse(bh);
+ return phys + i;
+ }
+ }
+ brelse(bh);
+ phys += span;
+ }
+ return 0;
+}
+
+/*
+ * beamfs_budget_read -- worst corrected-symbol count seen for @phys.
+ *
+ * Returns 0 when the feature is off or the byte cannot be read, which
+ * reads as "no wear known" rather than "no wear" -- the distinction
+ * belongs to the caller.
+ */
+u8 beamfs_budget_read(struct super_block *sb, u64 phys)
+{
+ struct buffer_head *bh;
+ u64 region_blk;
+ u32 offset;
+ u8 v;
+
+ if (!budget_slot(sb, phys, ®ion_blk, &offset))
+ return 0;
+
+ bh = sb_bread(sb, region_blk);
+ if (!bh)
+ return 0;
+ v = ((u8 *)bh->b_data)[offset];
+ brelse(bh);
+
+ return v;
+}
+
+/*
+ * beamfs_budget_histogram -- how many blocks sit at each wear level.
+ *
+ * @hist must hold BEAMFS_ERROR_BUDGET_MAX + 1 entries. Index n is the
+ * number of blocks whose worst subblock needed n corrections, so
+ * hist[0] is untouched blocks and hist[MAX] is blocks with no margin
+ * left.
+ *
+ * The whole region is read, which on a 931 GiB volume is about 233 MiB.
+ * That is a deliberate cost paid on request: the alternative is
+ * maintaining counters in memory, which would have to be reconstructed
+ * at mount anyway and would drift against the medium in between.
+ */
+void beamfs_budget_histogram(struct super_block *sb, u64 *hist)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 blk;
+ u64 seen = 0;
+
+ memset(hist, 0, (BEAMFS_ERROR_BUDGET_MAX + 1) * sizeof(*hist));
+ if (!budget_active(sbi))
+ return;
+
+ for (blk = 0; blk < sbi->s_budget_len; blk++) {
+ struct buffer_head *bh = sb_bread(sb, sbi->s_budget_blk + blk);
+ u32 i;
+
+ if (!bh)
+ continue;
+
+ for (i = 0; i < BEAMFS_BLOCK_SIZE; i++) {
+ u8 v = ((u8 *)bh->b_data)[i];
+
+ if (seen >= sbi->s_nblocks)
+ break;
+ seen++;
+ if (v > BEAMFS_ERROR_BUDGET_MAX)
+ v = BEAMFS_ERROR_BUDGET_MAX;
+ hist[v]++;
+ }
+ brelse(bh);
+ cond_resched();
+ }
+}
diff --git a/fs/beamfs/clock.c b/fs/beamfs/clock.c
new file mode 100644
index 000000000000..55ea9977af1f
--- /dev/null
+++ b/fs/beamfs/clock.c
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- clock anchor for the radiation event journal
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Journal entries carry ktime_get_ns(): monotonic since boot, which
+ * orders events exactly and dates none of them. That is the right
+ * clock for an entry -- it cannot jump, so the interval between two
+ * corrections is good to the timer's resolution, and a burst of forty
+ * upsets inside three milliseconds reads as exactly that.
+ *
+ * What it cannot say is when. This supplies the other half: what the
+ * wall clock read at a known point on the monotonic scale, and what
+ * that wall clock was worth at the time.
+ *
+ * Declaring the uncertainty rather than implying a precision is the
+ * whole point. A journal that dates events to the nanosecond on a
+ * machine synchronised by NTP is claiming something it cannot support,
+ * and an auditor who notices that has reason to doubt everything else
+ * in it. Instrumentation states its source and its error; so does this.
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/timekeeping.h>
+#include <linux/moduleparam.h>
+#include "beamfs.h"
+
+/*
+ * What the wall clock is worth, declared rather than guessed.
+ *
+ * The obvious approach -- ask the timekeeping core whether NTP thinks
+ * it is synchronised -- does not work from a module: do_adjtimex is
+ * not exported, and nothing else exposes STA_UNSYNC. Which is just as
+ * well, because "NTP reports itself synchronised" is a weak claim to
+ * build a forensic record on: it says a daemon is running, not that
+ * the clock is right.
+ *
+ * So it is declared. An operator who has deployed PTP with hardware
+ * timestamping knows it and says so; one who has an RTC and nothing
+ * else says that. The journal then carries a claim its owner made
+ * rather than one this code inferred.
+ *
+ * MiFID II takes the same line on trade timestamps: the requirement is
+ * traceability to a stated reference, not a number on its own. A
+ * timestamp whose provenance is unstated is not evidence, whatever its
+ * resolution.
+ *
+ * The interval between two entries is unaffected by any of this. Those
+ * come from ktime_get_ns(), are good to the timer's resolution, and
+ * need no anchor at all -- a burst of forty upsets inside three
+ * milliseconds reads as exactly that on the worst-served machine.
+ */
+static u32 beamfs_clock_source = BEAMFS_CLOCK_UNKNOWN;
+module_param_named(clock_source, beamfs_clock_source, uint, 0644);
+MODULE_PARM_DESC(clock_source,
+ "Wall-clock quality for journal anchoring: 0 unknown, 1 RTC, 2 NTP, 3 hardware (PTP/GPS)");
+
+static u32 beamfs_clock_quality(void)
+{
+ u32 q = READ_ONCE(beamfs_clock_source);
+
+ if (q > BEAMFS_CLOCK_HARDWARE)
+ q = BEAMFS_CLOCK_UNKNOWN;
+
+ /*
+ * A machine that has never had its clock set cannot be
+ * hardware-disciplined whatever the parameter says. Cheap
+ * sanity, and it catches the common case of an embedded board
+ * booting with no RTC and a clock_source setting copied from
+ * elsewhere.
+ */
+ if (q > BEAMFS_CLOCK_UNKNOWN &&
+ ktime_get_real_seconds() < 1000000000LL)
+ return BEAMFS_CLOCK_UNKNOWN;
+
+ return q;
+}
+
+/*
+ * beamfs_clock_anchor -- record the two clocks at one instant.
+ *
+ * Sampled as close together as the code can manage; the gap between
+ * the two reads is a handful of nanoseconds and is not the dominant
+ * error by several orders of magnitude.
+ *
+ * Called at mount and refreshed by the scrubber, so drift is bounded
+ * by the refresh interval instead of accumulating from mount. A volume
+ * left mounted six months would otherwise be dated by a reading six
+ * months stale.
+ */
+void beamfs_clock_anchor(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 mono, real;
+ u32 quality;
+
+ quality = beamfs_clock_quality();
+ mono = ktime_get_ns();
+ real = ktime_get_real_ns();
+
+ sbi->s_anchor_mono = mono;
+ sbi->s_anchor_real = real;
+ sbi->s_anchor_quality = quality;
+
+ if (sbi->s_beamfs_sb) {
+ sbi->s_beamfs_sb->s_anchor_mono = cpu_to_le64(mono);
+ sbi->s_beamfs_sb->s_anchor_real = cpu_to_le64(real);
+ sbi->s_beamfs_sb->s_anchor_quality = cpu_to_le32(quality);
+ beamfs_dirty_super(sbi);
+ }
+}
+
+/*
+ * beamfs_clock_to_real -- turn a journal timestamp into a wall time.
+ *
+ * Returns 0 when there is no anchor, which the caller must read as
+ * "no date available" rather than "the epoch". An entry older than the
+ * anchor converts to a time before it, which is correct: the monotonic
+ * scale is continuous across the anchor.
+ */
+u64 beamfs_clock_to_real(struct beamfs_sb_info *sbi, u64 mono)
+{
+ if (!sbi->s_anchor_real)
+ return 0;
+
+ if (mono >= sbi->s_anchor_mono)
+ return sbi->s_anchor_real + (mono - sbi->s_anchor_mono);
+
+ return sbi->s_anchor_real - (sbi->s_anchor_mono - mono);
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 05/18] beamfs: add indirect parity, tree checker
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (3 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 04/18] beamfs: add error budget, alerts and clock Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 06/18] beamfs: add block and inode allocator Aurelien DESBRIERES
` (12 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
An indirect block is 512 little-endian 64-bit pointers filling the
whole block, with no room for a checksum, and a flipped pointer that
still lands on an allocated block in range passes every structural
check. indparity.c keeps Reed-Solomon parity for indirect blocks in a
region of its own, at a slot computed from the block number, so the
indirect block format does not change.
treecheck.c is empty unless CONFIG_BEAMFS_DEBUG_TREE is set. It then
records every pointer stored into an indirect block and warns, naming
the task, the block and the slot, when a store finds zero where a live
pointer was recorded.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/indparity.c | 913 ++++++++++++++++++++++++++++++++++++++++++
fs/beamfs/treecheck.c | 319 +++++++++++++++
2 files changed, 1232 insertions(+)
create mode 100644 fs/beamfs/indparity.c
create mode 100644 fs/beamfs/treecheck.c
diff --git a/fs/beamfs/indparity.c b/fs/beamfs/indparity.c
new file mode 100644
index 000000000000..114a1188d0a0
--- /dev/null
+++ b/fs/beamfs/indparity.c
@@ -0,0 +1,913 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- parity for indirection blocks
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * An indirect block is 512 raw __le64 pointers filling all 4096 bytes,
+ * with nowhere to put a checksum. beamfs_check_intermediate_block()
+ * closes the dominant failure mode -- a pointer outside the data range,
+ * or onto a block the bitmap says is free -- and leaves one open: a
+ * flipped pointer that lands in range, on an allocated block, passes
+ * every check there is. It is then indistinguishable from a valid
+ * pointer, and the read returns someone else's data.
+ *
+ * The asymmetry is what makes this worth closing. A data block gets
+ * eight correctable symbols per 255-byte subblock. A double-indirect
+ * pointer gets none, and losing it costs 262144 blocks.
+ *
+ * Parity lives in a region of its own rather than inside the block, so
+ * BEAMFS_INDIRECT_PTRS stays at 512 and no BEAMFS_MAX_IBLOCK_* moves:
+ * an indirect block is laid out the same whether or not the volume
+ * carries parity for it.
+ *
+ * The slot for a block is computed, not looked up: for physical block
+ * P, with n = P - s_data_start, the parity is slot n % slots of the
+ * region's block n / slots, slots being how many fit in one region
+ * block's payload. A table would be metadata needing protection
+ * itself, and the recursion has to stop somewhere.
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/crc32.h>
+#include <linux/slab.h>
+#include "beamfs.h"
+#include "beamfs_trace.h"
+
+/* Parity bytes one indirect block costs under the active mode. */
+static size_t ind_parity_stride(struct beamfs_sb_info *sbi)
+{
+ switch (sbi->s_ind_parity_mode) {
+ case BEAMFS_IND_PARITY_CRC:
+ return BEAMFS_IND_PARITY_CRC_BYTES;
+ case BEAMFS_IND_PARITY_RS:
+ return BEAMFS_IND_PARITY_RS_BYTES;
+ default:
+ return 0;
+ }
+}
+
+/* Slots one region block holds under the active mode. */
+static unsigned int ind_parity_slots(struct beamfs_sb_info *sbi)
+{
+ switch (sbi->s_ind_parity_mode) {
+ case BEAMFS_IND_PARITY_CRC:
+ return BEAMFS_IND_PARITY_CRC_SLOTS;
+ case BEAMFS_IND_PARITY_RS:
+ return BEAMFS_IND_PARITY_RS_SLOTS;
+ default:
+ return 0;
+ }
+}
+
+/*
+ * Locate the parity for @phys: which region block holds it, and at what
+ * offset into that block's payload. Returns false when the mode is off
+ * or the block sits outside the data area, both of which mean there is
+ * nothing to do.
+ *
+ * The offset is into the decoded payload, not the raw block. A region
+ * block is RS-encoded like a data block, so its 4096 bytes hold 3824 of
+ * payload interleaved with parity, and indexing the raw bytes would
+ * land in the middle of a codeword.
+ */
+static bool ind_parity_slot(struct super_block *sb, u64 phys,
+ u64 *region_blk, u32 *offset, size_t *stride)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ unsigned int slots;
+ u64 index;
+ u32 within;
+
+ *stride = ind_parity_stride(sbi);
+ slots = ind_parity_slots(sbi);
+ if (!*stride || !slots || !sbi->s_ind_parity_blk)
+ return false;
+ if (phys < sbi->s_data_start)
+ return false;
+
+ index = phys - sbi->s_data_start;
+ within = (u32)do_div(index, slots); /* index becomes the quotient */
+ *region_blk = sbi->s_ind_parity_blk + index;
+ *offset = within * (u32)*stride;
+
+ if (*offset + *stride > BEAMFS_DATA_INLINE_BYTES)
+ return false;
+ if (*region_blk >= sbi->s_ind_parity_blk + sbi->s_ind_parity_len)
+ return false;
+
+ return true;
+}
+
+/*
+ * Decode a region block into @scratch, correcting what RS can.
+ *
+ * Returns 0 when the payload can be trusted, -EUCLEAN when a subblock
+ * was beyond correction. On -EUCLEAN the caller must not write the
+ * block back: re-encoding damaged payload makes the damage permanent
+ * and self-consistent, which is worse than leaving it visible.
+ *
+ * Decoding happens in scratch, never in the buffer: decode_rs8 corrects
+ * in place and would rewrite the region itself, which is the mistake
+ * beamfs_ind_parity_verify documents below at the cost of 500-block
+ * leaks.
+ */
+/*
+ * The region block this cpu decoded last.
+ *
+ * A verify decodes sixteen RS codewords over a 4096-byte region block
+ * to read the stride bytes belonging to one indirect block, and perf
+ * puts 12.9% of the machine in there -- nearly half of the time that
+ * is not idle, on a busy volume.
+ *
+ * A region block carries the parity of fourteen indirect blocks, and
+ * walking a file reads them in order: the same 4096 bytes are decoded
+ * fourteen times with nothing changed between. Remembering the last
+ * one skips the other thirteen.
+ *
+ * Keyed on the buffer's own sequence: b_blocknr says which block, and
+ * a write to it goes through lock_buffer, so a decode taken while the
+ * buffer was locked cannot be stale. What can go stale is a decode
+ * from before an update, which is why the generation below exists.
+ */
+struct ind_region_cache {
+ u64 blocknr;
+ u64 gen;
+ int ret;
+ u8 *data;
+};
+
+static DEFINE_PER_CPU(struct ind_region_cache, ind_rcache);
+
+/*
+ * Bumped by every parity update.
+ *
+ * One counter for the filesystem rather than one per region: an update
+ * anywhere invalidates every cached decode, which costs a handful of
+ * redundant decodes and cannot be wrong. A per-region counter would
+ * save those and need a table as large as the region itself.
+ */
+void beamfs_ind_parity_touched(struct beamfs_sb_info *sbi)
+{
+ atomic64_inc(&sbi->s_ind_parity_gen);
+}
+
+/*
+ * Give back the per-cpu decode buffers.
+ *
+ * Called from module exit. One page per cpu is not a leak anybody
+ * notices while the module is loaded and is one the kernel complains
+ * about when it is not.
+ */
+void beamfs_ind_parity_cache_free(void)
+{
+ unsigned int cpu;
+
+ for_each_possible_cpu(cpu) {
+ struct ind_region_cache *c = per_cpu_ptr(&ind_rcache, cpu);
+
+ kfree(c->data);
+ c->data = NULL;
+ }
+}
+
+static int ind_region_read(struct super_block *sb, struct buffer_head *pbh,
+ u8 *scratch)
+{
+ int results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ u8 positions[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+ unsigned int i;
+ int ret = 0;
+ u64 gen_at_read;
+
+ /*
+ * The same region, decoded a moment ago on this cpu?
+ *
+ * Fourteen indirect blocks share a region block and a walk down
+ * a file reads them in order, so the decode below runs fourteen
+ * times over the same 4096 bytes with nothing changed between.
+ *
+ * The generation rules out a decode from before an update. The
+ * cache is per-cpu and taken without a lock: get_cpu_ptr keeps
+ * preemption off from the key check through the copy, so nothing
+ * on this cpu can replace the entry in between.
+ */
+ {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 gen = atomic64_read(&sbi->s_ind_parity_gen);
+ struct ind_region_cache *c = get_cpu_ptr(&ind_rcache);
+ bool hit = c->data && c->blocknr == pbh->b_blocknr &&
+ c->gen == gen;
+ int cached = c->ret;
+
+ if (hit)
+ memcpy(scratch, c->data, BEAMFS_BLOCK_SIZE);
+ put_cpu_ptr(&ind_rcache);
+ if (hit)
+ return cached;
+ }
+
+ /*
+ * Taken before the copy, not after the decode.
+ *
+ * A parity update that lands while this decode runs bumps the
+ * generation. Reading it afterwards stamps a scratch that
+ * describes the state before that update with the generation
+ * that came after it, and the stale decode is then served to
+ * the other thirteen indirect blocks of the region as if it
+ * were current -- a slot that was written reads back as zero.
+ */
+ gen_at_read = atomic64_read(&BEAMFS_SB(sb)->s_ind_parity_gen);
+ memcpy(scratch, pbh->b_data, BEAMFS_BLOCK_SIZE);
+
+ beamfs_rs_decode_region(scratch, BEAMFS_SUBBLOCK_TOTAL,
+ scratch + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "indirect parity region");
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ if (results[i] < 0) {
+ pr_err_ratelimited("beamfs: parity region block %llu subblock %u beyond correction\n",
+ (unsigned long long)pbh->b_blocknr, i);
+ ret = -EUCLEAN;
+ } else if (results[i] > 0) {
+ beamfs_log_rs_event_flagged(sb, pbh->b_blocknr,
+ positions + (size_t)i * (BEAMFS_RS_PARITY / 2),
+ (unsigned int)results[i],
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ }
+ }
+
+ /*
+ * Kept for the next thirteen indirect blocks in this region.
+ *
+ * The buffer is allocated on first use and never freed: one page
+ * per cpu, for the life of the module, against a decode of
+ * sixteen codewords on every read of an indirect block.
+ *
+ * GFP_ATOMIC and a failure that costs nothing: no cache is a
+ * slower filesystem, not a wrong one.
+ */
+ {
+ struct ind_region_cache *c = get_cpu_ptr(&ind_rcache);
+
+ if (!c->data)
+ c->data = kmalloc(BEAMFS_BLOCK_SIZE, GFP_ATOMIC);
+ if (c->data) {
+ memcpy(c->data, scratch, BEAMFS_BLOCK_SIZE);
+ c->blocknr = pbh->b_blocknr;
+ c->gen = gen_at_read;
+ c->ret = ret;
+ }
+ put_cpu_ptr(&ind_rcache);
+ }
+
+ return ret;
+}
+
+/*
+ * Copy one slot out of a decoded region block.
+ *
+ * The payload is interleaved with parity -- 239 bytes of data every 255
+ * -- so a slot at payload offset @off can straddle the boundary between
+ * two subblocks, and a caller cannot simply point into the buffer.
+ *
+ * Gathering the whole 3824-byte payload into a second scratch page was
+ * the first way this worked, and it cost a page per verify on a read
+ * path. beamfs_ind_parity_verify then held three at once out of a pool
+ * of nine; generic/464 emptied it and three readers stalled 191 seconds
+ * each in __bread_gfp, on a machine with 16 MiB free and 7.6 GiB of
+ * page cache that GFP_NOFS could not reclaim. A slot is 256 bytes and
+ * fits on the stack.
+ */
+static void ind_slot_gather(const u8 *scratch, u32 off, size_t stride,
+ u8 *out)
+{
+ size_t done = 0;
+
+ while (done < stride) {
+ unsigned int sub = (unsigned int)((off + done) / BEAMFS_SUBBLOCK_DATA);
+ size_t within = (off + done) % BEAMFS_SUBBLOCK_DATA;
+ size_t run = BEAMFS_SUBBLOCK_DATA - within;
+
+ if (run > stride - done)
+ run = stride - done;
+
+ memcpy(out + done,
+ scratch + (size_t)sub * BEAMFS_SUBBLOCK_TOTAL + within,
+ run);
+ done += run;
+ }
+}
+
+/* Put one slot back into a decoded region block, same geometry. */
+static void ind_slot_scatter(u8 *scratch, u32 off, size_t stride,
+ const u8 *in)
+{
+ size_t done = 0;
+
+ while (done < stride) {
+ unsigned int sub = (unsigned int)((off + done) / BEAMFS_SUBBLOCK_DATA);
+ size_t within = (off + done) % BEAMFS_SUBBLOCK_DATA;
+ size_t run = BEAMFS_SUBBLOCK_DATA - within;
+
+ if (run > stride - done)
+ run = stride - done;
+
+ memcpy(scratch + (size_t)sub * BEAMFS_SUBBLOCK_TOTAL + within,
+ in + done, run);
+ done += run;
+ }
+}
+
+/*
+ * Does the block decode as soon as it is encoded?
+ *
+ * generic/076 on a volume of 131072 blocks reported region block 1051
+ * subblock 9 and region block 8104 subblock 6 beyond correction, and
+ * sixteen indirect blocks became uncheckable behind them. The question
+ * a report on the medium cannot answer is whether the block left this
+ * function already broken or became so on the way out, and this answers
+ * it: decode a copy of what was just encoded, and say so when it fails.
+ *
+ * Debug-only. Every region write pays a full sixteen-codeword decode,
+ * which is the cost of the encode again; CONFIG_BEAMFS_DEBUG_TREE is
+ * the switch the tree checks already live behind.
+ */
+#ifdef CONFIG_BEAMFS_DEBUG_TREE
+static void ind_region_selfcheck(struct super_block *sb,
+ const struct buffer_head *pbh)
+{
+ int results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ u8 *copy = beamfs_scratch_get(sb);
+ unsigned int i;
+
+ if (!copy)
+ return;
+ memcpy(copy, pbh->b_data, BEAMFS_BLOCK_SIZE);
+ beamfs_rs_decode_region(copy, BEAMFS_SUBBLOCK_TOTAL,
+ copy + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, NULL, 0, "region selfcheck");
+ {
+ unsigned int bad = 0;
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ if (results[i] < 0) {
+ bad++;
+ pr_err_ratelimited("beamfs: region block %llu subblock %u does not decode as written\n",
+ (unsigned long long)pbh->b_blocknr, i);
+ } else if (results[i] > 0) {
+ bad++;
+ pr_err_ratelimited("beamfs: region block %llu subblock %u needed %d correction(s) as written\n",
+ (unsigned long long)pbh->b_blocknr, i,
+ results[i]);
+ }
+ }
+ /*
+ * Silent when it passes.
+ *
+ * It said so for a while, because silence meant two things
+ * -- the block encoded correctly, or this function never
+ * ran on it -- and generic/076 needed them told apart. It
+ * did: the block leaves this function decodable every
+ * time, and what arrives on the medium is somebody else's
+ * problem.
+ *
+ * Saying it again costs 1390 lines of a 3422-line dmesg,
+ * which is where a KCSAN report goes to be missed.
+ */
+ (void)bad;
+ }
+ beamfs_scratch_put(sb, copy);
+}
+#endif
+
+/*
+ * Encode @scratch back into the buffer.
+ *
+ * @scratch is a decoded copy of the block and keeps its layout: sixteen
+ * subblocks of BEAMFS_SUBBLOCK_TOTAL, each holding its data then its
+ * parity. It is not a flat payload, and reading it as one -- at
+ * i * BEAMFS_SUBBLOCK_DATA rather than i * BEAMFS_SUBBLOCK_TOTAL --
+ * shifts every subblock against the one it came from.
+ *
+ * That is what happened when the second scratch page was removed: the
+ * gather and scatter helpers were written for the interleaved layout,
+ * this one was left reading the old flat one, and every region write
+ * scrambled the block. Files came back with EUCLEAN moments after being
+ * written -- generic/001 reporting "cp: error copying big.0 to big.1:
+ * Structure needs cleaning" on its second iteration.
+ */
+static void ind_region_write(struct super_block *sb,
+ struct buffer_head *pbh, const u8 *scratch)
+{
+ unsigned int i;
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++)
+ memcpy((u8 *)pbh->b_data + (size_t)i * BEAMFS_SUBBLOCK_TOTAL,
+ scratch + (size_t)i * BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA);
+
+ {
+ int _e = beamfs_rs_encode_region((u8 *)pbh->b_data,
+ BEAMFS_SUBBLOCK_TOTAL,
+ (u8 *)pbh->b_data + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+
+ /*
+ * The region's own FEC. Without it the region is
+ * unreadable, and every block it covers with it.
+ */
+ if (_e < 0)
+ pr_err_ratelimited("beamfs: parity region encode failed: %d\n",
+ _e);
+ }
+#ifdef CONFIG_BEAMFS_DEBUG_TREE
+ ind_region_selfcheck(sb, pbh);
+#else
+ (void)sb;
+#endif
+}
+
+/*
+ * beamfs_ind_parity_update -- recompute parity for an indirect block.
+ *
+ * Takes the buffer_head rather than a block number: b_blocknr already
+ * holds the physical block, so no call site has to name the right
+ * variable among indirect_blk, dindirect_blk, l1_blk and the rest, and
+ * none can pass the wrong one.
+ *
+ * Call after modifying the block and before releasing it. A missed call
+ * leaves stale parity, which reads as corruption on the next verify --
+ * worse than no parity at all, since it turns a healthy volume into one
+ * that reports damage.
+ *
+ * @inode is the inode whose write dirtied the block, or NULL when there
+ * is none -- the scrubber repairing a block nobody is writing. It is
+ * needed because the parity block has to go on that inode's metadata
+ * list: a buffer marked dirty and on no inode's list is never flushed
+ * by __writeback_single_inode, so the parity stays in memory while the
+ * block it describes reaches the disk, and the next verify reads the
+ * mismatch as damage.
+ *
+ * generic/476 left 293 indirect blocks the checker could not decode,
+ * 287 of them almost entirely zero: freshly allocated, a few pointers
+ * installed, and parity that never followed them down.
+ */
+void beamfs_ind_parity_update(struct super_block *sb, struct buffer_head *bh,
+ struct inode *inode)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ const void *block = bh->b_data;
+ u64 phys = bh->b_blocknr;
+ struct buffer_head *pbh;
+ u64 region_blk;
+ u32 offset;
+ size_t stride;
+ unsigned int i;
+ u8 *scratch;
+ /* One slot, 256 bytes. On the stack, so this path holds one
+ * scratch page instead of two.
+ */
+ u8 slotbuf[BEAMFS_IND_PARITY_RS_BYTES];
+
+ if (!ind_parity_slot(sb, phys, ®ion_blk, &offset, &stride))
+ return;
+ if (stride > sizeof(slotbuf))
+ return;
+
+ pbh = beamfs_bread(sb, region_blk, "indirect parity");
+ if (!pbh) {
+ pr_err_ratelimited("beamfs: cannot read parity block %llu for indirect %llu\n",
+ (unsigned long long)region_blk,
+ (unsigned long long)phys);
+ return;
+ }
+
+ scratch = beamfs_scratch_get(sb);
+ if (!scratch) {
+ /*
+ * Said out loud.
+ *
+ * Returning quietly here leaves the block on the medium
+ * with no parity describing it, and nothing anywhere
+ * records that it happened. generic/464 produced exactly
+ * that the day the pool was made to give up rather than
+ * wait: block 41641 of inode 24, written, named by the
+ * inode, and its parity slot at region 2482 + 3584 still
+ * empty.
+ *
+ * The pool must not wait -- it is reached under
+ * i_alloc_mutex from a page fault, and waiting there is
+ * what wedged the node for half an hour three times. But
+ * an allocation that fails on a filesystem whose whole
+ * purpose is parity is a failure of the filesystem, not a
+ * step to skip.
+ */
+ pr_err_ratelimited("beamfs: no scratch page for the parity of indirect %llu; the block goes to the medium undescribed\n",
+ (unsigned long long)phys);
+ brelse(pbh);
+ return;
+ }
+
+ lock_buffer(pbh);
+
+ /*
+ * Read-modify-write on the payload. The region block carries its
+ * own FEC, so one slot cannot be edited in the raw bytes: the
+ * whole block has to be decoded, the slot replaced, and the block
+ * re-encoded.
+ *
+ * A region block beyond correction is left alone. Re-encoding it
+ * would write parity over damaged payload and make the parity of
+ * every other indirect block it covers permanently wrong, in a way
+ * nothing could afterwards detect.
+ */
+ if (ind_region_read(sb, pbh, scratch) == -EUCLEAN) {
+ pr_err_ratelimited("beamfs: parity region block %llu beyond correction; not updating the slot for indirect %llu\n",
+ (unsigned long long)region_blk,
+ (unsigned long long)phys);
+ unlock_buffer(pbh);
+ beamfs_scratch_put(sb, scratch);
+ brelse(pbh);
+ return;
+ }
+
+ ind_slot_gather(scratch, offset, stride, slotbuf);
+
+ if (sbi->s_ind_parity_mode == BEAMFS_IND_PARITY_CRC) {
+ __le32 *slot = (__le32 *)slotbuf;
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++)
+ slot[i] = cpu_to_le32(crc32_le(~0U, (const u8 *)block +
+ (size_t)i * BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_DATA) ^ ~0U);
+ } else {
+ u8 *slot = slotbuf;
+
+ /*
+ * The indirect block is 4096 bytes and an RS codeword
+ * covers 239, so it is split the way a data block is: 16
+ * subblocks, one 16-byte parity set each. That covers
+ * the first 3824 bytes; pointers 478..511 are in no
+ * codeword, as beamfs_ind_parity_verify notes.
+ */
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++)
+ beamfs_rs_encode_region((u8 *)block + (size_t)i * BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_DATA,
+ slot + (size_t)i * BEAMFS_RS_PARITY,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA, 1);
+ }
+
+ ind_slot_scatter(scratch, offset, stride, slotbuf);
+ ind_region_write(sb, pbh, scratch);
+ /* The parity now describes this copy. */
+ set_buffer_beamfs_verified(bh);
+
+ /*
+ * The pointer count serves the tracepoint and nothing else.
+ * Counted unconditionally it is 512 loads for every pointer
+ * installed, on a path that already pays seventeen RS
+ * operations, and it ran with tracing off as readily as on.
+ * generic/013 spends 950 seconds here where ext2 spends
+ * sixty.
+ */
+ if (trace_beamfs_parity_slot_enabled()) {
+ unsigned int nz = 0, k;
+
+ for (k = 0; k < BEAMFS_INDIRECT_PTRS; k++)
+ if (((const __le64 *)block)[k])
+ nz++;
+ trace_beamfs_parity_slot(sb->s_dev, inode ? inode->i_ino : 0,
+ phys,
+ region_blk, offset / (u32)stride, nz);
+ }
+ /*
+ * Bumped before the lock is dropped, not after.
+ *
+ * The bump used to follow unlock_buffer. Between the two, another
+ * cpu updating a neighbour slot of the same region took the lock,
+ * found its cached decode of this region still stamped with the
+ * current generation, hit, scattered its own slot into a payload
+ * that predates this update, and wrote the region: the slot
+ * written here went to the device once and was overwritten by
+ * the next writer's stale copy of the region.
+ *
+ * generic/476 on 2026-09-23, module 0.1.10:
+ * 102 indirect blocks reported by fsck as holding pointers under
+ * a zero parity slot. For every one of them the tracepoint had
+ * recorded an update with exactly the pointer count fsck found,
+ * and the region block had been written to the device after that
+ * update -- never before, never not at all. The write carried
+ * bytes from before the update. 98 of the 102 were never freed;
+ * the region was simply written by someone else's update.
+ *
+ * With the bump inside the lock, any update that takes the lock
+ * after this one reads a generation this decode does not carry,
+ * misses, and decodes the buffer as it is. unlock_buffer orders
+ * the increment before the next holder's reads.
+ */
+ beamfs_ind_parity_touched(sbi);
+ unlock_buffer(pbh);
+ beamfs_scratch_put(sb, scratch);
+ /*
+ * On the volume's shared metadata list, so that the sync of any
+ * inode carries it with the blocks it describes. Not on the
+ * inode's own list: a region holds the parity of fourteen indirect
+ * blocks of as many files, and a buffer that moves between inode
+ * lists meets the window in mmb_sync described at s_shared_bhs.
+ * Without an inode -- the scrubber -- the list is still the one
+ * the next write_inode syncs.
+ */
+ mmb_mark_buffer_dirty(pbh, &BEAMFS_SB(sb)->s_shared_bhs);
+ if (inode) {
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_sync empties the metadata list from write_inode,
+ * which the VFS calls only for an inode it believes is
+ * dirty. Updating a region changes no field of the
+ * inode, so nothing marked it, and the region stayed in
+ * memory holding the only parity that matches the block
+ * just written.
+ *
+ * What is on the medium is then the parity of what the
+ * block held before, and verify reports the block beyond
+ * correction -- 17 of them in one generic/013, on a
+ * volume where treecheck saw no lost pointer and every
+ * indirect block had its parity updated. The update ran;
+ * it just never landed.
+ */
+ mark_inode_dirty(inode);
+ }
+ brelse(pbh);
+}
+
+/*
+ * beamfs_ind_parity_verify -- check an indirect block against its parity.
+ *
+ * Under CRC the block is checked and left alone: detection turns a
+ * flipped pointer that lands in range, on an allocated block, from
+ * silent corruption into a clean fail-closed error, which is the
+ * contract beamfs states everywhere else. Under RS a copy is decoded
+ * and the block is left alone: damage is reported and journalled,
+ * never repaired here -- the comment at the decode below says why a
+ * verify must not write.
+ *
+ * Returns 0 if the block is sound or its damage is within correction,
+ * -EUCLEAN if it is damaged beyond the mode's ability to fix.
+ */
+int beamfs_ind_parity_verify(struct super_block *sb, struct buffer_head *bh)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ void *block = bh->b_data;
+ u64 phys = bh->b_blocknr;
+ struct buffer_head *pbh;
+ u64 region_blk;
+ u32 offset;
+ size_t stride;
+ unsigned int i;
+ int ret = 0;
+ int rc;
+ u8 *rscratch;
+ u8 slotbuf[BEAMFS_IND_PARITY_RS_BYTES];
+
+ if (!ind_parity_slot(sb, phys, ®ion_blk, &offset, &stride))
+ return 0;
+ /*
+ * Already checked, or written by us: the copy in memory is what
+ * the parity describes. Only a buffer freshly read from the
+ * medium, or one the scrub asks about, is decoded.
+ */
+ if (buffer_beamfs_verified(bh))
+ return 0;
+
+ pbh = beamfs_bread(sb, region_blk, "indirect parity");
+ if (!pbh)
+ return 0; /* parity unreadable: do not fail the read on it */
+
+ if (stride > sizeof(slotbuf)) {
+ brelse(pbh);
+ return 0;
+ }
+ rscratch = beamfs_scratch_get(sb);
+ if (!rscratch) {
+ brelse(pbh);
+ return 0;
+ }
+
+ /*
+ * The region block carries its own FEC, so the slot has to be
+ * decoded out of it rather than read from the raw bytes.
+ *
+ * A region beyond correction is not the indirect block's fault
+ * and must not be reported as such: before this, a hit on the
+ * region made every indirect block it covered look damaged, and
+ * fsck blamed the blocks. Saying so and passing the read is the
+ * honest answer -- the block may well be fine and there is now
+ * nothing to check it against.
+ */
+ /*
+ * Under the buffer lock, because the update path holds it while
+ * it rewrites all 4096 bytes.
+ *
+ * Copying the block while that rewrite is in flight yields an
+ * image half old and half new, which no RS code can correct:
+ * the region is reported beyond correction with nothing wrong
+ * on the medium, the indirect block it covers stops being
+ * checked, and its slot stops being maintained.
+ */
+ lock_buffer(pbh);
+ rc = ind_region_read(sb, pbh, rscratch);
+ unlock_buffer(pbh);
+
+ if (rc == -EUCLEAN) {
+ pr_err_ratelimited("beamfs: parity region block %llu beyond correction; indirect %llu cannot be checked\n",
+ (unsigned long long)region_blk,
+ (unsigned long long)phys);
+ beamfs_scratch_put(sb, rscratch);
+ brelse(pbh);
+ return 0;
+ }
+
+ ind_slot_gather(rscratch, offset, stride, slotbuf);
+
+ /*
+ * An empty slot describes nothing, and decoding against it
+ * destroys the block.
+ *
+ * Zero data with zero parity is a valid RS codeword, so a slot of
+ * zeros is a codeword saying "this block is all zeros". An
+ * indirect block early in its life holds one or two pointers --
+ * eight bytes in a 239-byte subblock -- and RS(255,239) corrects
+ * up to eight symbols. The decoder therefore does exactly what it
+ * is asked: it moves those eight bytes to zero and reports a
+ * successful correction.
+ *
+ * generic/076 catches it in the act. treecheck reports parent
+ * 18516 slot 1 holding 18518, read back as 0 and reallocated to
+ * 22512, from writeback -- and the checker says of that block
+ * that no parity was ever written for it.
+ *
+ * A block whose parity has not been written yet is unverifiable,
+ * not wrong. Say so and let the read through: the pointers are
+ * what the writer put there, and destroying them to satisfy a
+ * codeword nobody wrote is the one outcome worse than not
+ * checking.
+ */
+ {
+ size_t k;
+ bool described = false;
+
+ for (k = 0; k < stride; k++) {
+ if (slotbuf[k]) {
+ described = true;
+ break;
+ }
+ }
+ if (!described) {
+ /*
+ * The slot covers BEAMFS_DATA_INLINE_BYTES of the
+ * block, 3824 of 4096: pointers 478..511 are in no
+ * codeword, and a block that holds pointers only
+ * there has, correctly, a zero slot. That is the v5
+ * format and not an event, so it is not warned
+ * about; a zero slot over pointers the slot does
+ * cover is.
+ */
+ const __le64 *ptrs = (const __le64 *)bh->b_data;
+ unsigned int within = 0;
+
+ for (k = 0; k < BEAMFS_DATA_INLINE_BYTES / sizeof(__le64); k++)
+ if (ptrs[k])
+ within++;
+ if (within)
+ pr_warn_ratelimited("beamfs: indirect block %llu: parity slot empty over %u pointer(s) it covers; not checked\n",
+ (unsigned long long)phys, within);
+ /*
+ * Nothing to check it against until an update files
+ * its slot, which marks it verified itself; until
+ * then, asked once per copy, not once per lookup.
+ */
+ set_buffer_beamfs_verified(bh);
+ beamfs_scratch_put(sb, rscratch);
+ brelse(pbh);
+ return 0;
+ }
+ }
+
+ if (sbi->s_ind_parity_mode == BEAMFS_IND_PARITY_CRC) {
+ const __le32 *slot = (const __le32 *)slotbuf;
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ u32 want = le32_to_cpu(slot[i]);
+ u32 got = crc32_le(~0U,
+ (const u8 *)block + (size_t)i * BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_DATA) ^ ~0U;
+
+ if (want != got) {
+ pr_err_ratelimited("beamfs: indirect block %llu subblock %u CRC mismatch\n",
+ (unsigned long long)phys, i);
+ beamfs_log_rs_event_flagged(sb,
+ phys, NULL, 0, BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ ret = -EUCLEAN;
+ }
+ }
+ } else {
+ u8 *slot = slotbuf;
+ int results[1];
+ u8 positions[BEAMFS_RS_PARITY / 2];
+ u8 *copy;
+
+ /*
+ * Decode a copy, never bh->b_data.
+ *
+ * decode_rs8 corrects in place: it writes both the data
+ * and the parity it is handed. Decoding the live indirect
+ * block against the parity region rewrites that block --
+ * and when the region's parity is stale, which it is the
+ * moment a data block is reallocated as an indirect one
+ * without the region being updated, the "correction"
+ * drags the block back toward whatever it held before.
+ *
+ * generic/464 turned that into 500-block leaks: a fresh
+ * L1 filled with 512 pointers was read once by another
+ * process, verify decoded it against block 1934's old
+ * parity, and the 512 pointers collapsed back to the two
+ * the previous owner had left -- orphaning everything
+ * below them.
+ *
+ * A verify has no business modifying the block it checks.
+ * It decodes into scratch, reports what it finds, and
+ * touches nothing. The only real corruption on a device
+ * that flips no bits is stale parity, and the fix for that
+ * is to keep the parity current, not to let the decoder
+ * launder one block's contents into another's.
+ */
+ copy = beamfs_scratch_get(sb);
+ if (!copy) {
+ beamfs_scratch_put(sb, rscratch);
+ brelse(pbh);
+ return 0;
+ }
+ memcpy(copy, block, BEAMFS_BLOCK_SIZE);
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ u8 *dsub = copy + (size_t)i * BEAMFS_SUBBLOCK_DATA;
+ u8 *psub = slot + (size_t)i * BEAMFS_RS_PARITY;
+ int region_rc;
+
+ region_rc = beamfs_rs_decode_region(dsub, BEAMFS_SUBBLOCK_DATA,
+ psub, BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA, 1,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "indirect");
+
+ if (region_rc < 0 || results[0] < 0) {
+ pr_err_ratelimited("beamfs: indirect block %llu subblock %u uncorrectable\n",
+ (unsigned long long)phys, i);
+ beamfs_log_rs_event_flagged(sb, phys, NULL, 0,
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ ret = -EUCLEAN;
+ } else if (results[0] > 0) {
+ beamfs_log_rs_event_flagged(sb, phys, positions,
+ (unsigned int)results[0],
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ }
+ }
+ beamfs_scratch_put(sb, copy);
+ }
+
+ beamfs_scratch_put(sb, rscratch);
+ brelse(pbh);
+ if (ret == 0)
+ set_buffer_beamfs_verified(bh);
+ return ret;
+}
+
+/*
+ * The scrub asks about the medium, not about the copy: check again
+ * whatever was checked before.
+ */
+int beamfs_ind_parity_verify_medium(struct super_block *sb,
+ struct buffer_head *bh)
+{
+ clear_buffer_beamfs_verified(bh);
+ return beamfs_ind_parity_verify(sb, bh);
+}
diff --git a/fs/beamfs/treecheck.c b/fs/beamfs/treecheck.c
new file mode 100644
index 000000000000..8bac91d6c7be
--- /dev/null
+++ b/fs/beamfs/treecheck.c
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs tree checker -- catch a lost pointer where it is lost.
+ *
+ * generic/464 reports the damage through fsck, twenty seconds after the
+ * fact, as a count of blocks the bitmap calls used and no inode
+ * reaches. Working back from that count means reading a million trace
+ * lines and guessing which of them mattered.
+ *
+ * The way out of chasing an intermittent defect is to stop chasing it:
+ * check the invariant at every write, and the first violation arrives
+ * as a stack trace naming the task, the block and the slot, instead of
+ * a number in a log.
+ *
+ * The invariant: a slot that holds a pointer keeps it until the block
+ * it names is freed. Every store into an indirect block is recorded
+ * here; every subsequent store to the same slot is checked against the
+ * record. A store that finds zero where a live pointer was recorded is
+ * the defect, and it is reported at that instant.
+ *
+ * Cost: one hash lookup per store, and one struct beamfs_tc_entry --
+ * 72 bytes on a 64-bit kernel -- per slot recorded. Off unless
+ * CONFIG_BEAMFS_DEBUG_TREE, and the calls compile away entirely when it
+ * is off.
+ */
+
+#include <linux/hashtable.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+#include "beamfs.h"
+
+#ifdef CONFIG_BEAMFS_DEBUG_TREE
+
+#define BEAMFS_TC_BITS 14
+
+struct beamfs_tc_entry {
+ struct hlist_node node;
+ struct hlist_node cnode; /* on s_tc_child while child != 0 */
+ u64 parent;
+ u32 slot;
+ u64 child;
+ unsigned long ino;
+ const char *who;
+};
+
+/*
+ * Keyed on (parent, slot) rather than on the child: the question asked
+ * at every store is "what did this slot hold", and a child that has
+ * been freed and handed out again would collide under the other key.
+ */
+static inline u32 beamfs_tc_key(u64 parent, u32 slot)
+{
+ return hash_64(parent * BEAMFS_INDIRECT_PTRS + slot, BEAMFS_TC_BITS);
+}
+
+static inline u32 beamfs_tc_ckey(u64 child)
+{
+ return hash_64(child, BEAMFS_TC_BITS);
+}
+
+/* Under s_tc_lock. An entry is on the child index while it names a block. */
+static void beamfs_tc_index_child(struct beamfs_sb_info *sbi,
+ struct beamfs_tc_entry *e)
+{
+ if (e->child)
+ hash_add(sbi->s_tc_child, &e->cnode, beamfs_tc_ckey(e->child));
+}
+
+static void beamfs_tc_unindex_child(struct beamfs_tc_entry *e)
+{
+ if (!hlist_unhashed(&e->cnode))
+ hash_del(&e->cnode);
+}
+
+/* Under s_tc_lock. */
+static void beamfs_tc_drop(struct beamfs_sb_info *sbi, struct beamfs_tc_entry *e)
+{
+ beamfs_tc_unindex_child(e);
+ hash_del(&e->node);
+ sbi->s_tc_entries--;
+ kfree(e);
+}
+
+void beamfs_tc_init(struct beamfs_sb_info *sbi)
+{
+ hash_init(sbi->s_tc);
+ hash_init(sbi->s_tc_child);
+ spin_lock_init(&sbi->s_tc_lock);
+ sbi->s_tc_entries = 0;
+ sbi->s_tc_violations = 0;
+}
+
+void beamfs_tc_exit(struct beamfs_sb_info *sbi)
+{
+ struct beamfs_tc_entry *e;
+ struct hlist_node *tmp;
+ unsigned int bkt;
+
+ spin_lock(&sbi->s_tc_lock);
+ hash_for_each_safe(sbi->s_tc, bkt, tmp, e, node) {
+ beamfs_tc_unindex_child(e);
+ hash_del(&e->node);
+ kfree(e);
+ }
+ spin_unlock(&sbi->s_tc_lock);
+
+ if (sbi->s_tc_violations)
+ pr_err("beamfs/treecheck: %u lost pointer(s) over the life of this mount\n",
+ sbi->s_tc_violations);
+ else
+ pr_info("beamfs/treecheck: no lost pointer seen\n");
+}
+
+/*
+ * Record a store, and check what the slot held before it.
+ *
+ * @old is what the caller read out of the slot. If a live pointer was
+ * recorded for this slot and @old is zero, that pointer vanished from
+ * the buffer between the two stores with nothing freeing it -- which is
+ * the defect, reported here with the task and the stack that found it.
+ */
+void beamfs_tc_store(struct super_block *sb, unsigned long ino, u64 parent,
+ u32 slot, u64 old, u64 child, const char *who)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_tc_entry *e, *found = NULL;
+ u32 key = beamfs_tc_key(parent, slot);
+
+ spin_lock(&sbi->s_tc_lock);
+
+ hash_for_each_possible(sbi->s_tc, e, node, key) {
+ if (e->parent == parent && e->slot == slot) {
+ found = e;
+ break;
+ }
+ }
+
+ if (found && found->child && old == 0) {
+ sbi->s_tc_violations++;
+ spin_unlock(&sbi->s_tc_lock);
+ pr_err("beamfs/treecheck: LOST POINTER parent=%llu slot=%u held %llu (ino=%lu, put there by %s), read as 0, now %llu (ino=%lu, by %s)\n",
+ (unsigned long long)parent, slot,
+ (unsigned long long)found->child, found->ino,
+ found->who ? found->who : "?",
+ (unsigned long long)child, ino, who);
+ WARN_ONCE(1, "beamfs: indirect slot lost its pointer\n");
+ spin_lock(&sbi->s_tc_lock);
+ beamfs_tc_unindex_child(found);
+ found->child = child;
+ found->ino = ino;
+ found->who = who;
+ beamfs_tc_index_child(sbi, found);
+ spin_unlock(&sbi->s_tc_lock);
+ return;
+ }
+
+ if (found) {
+ beamfs_tc_unindex_child(found);
+ found->child = child;
+ found->ino = ino;
+ found->who = who;
+ beamfs_tc_index_child(sbi, found);
+ spin_unlock(&sbi->s_tc_lock);
+ return;
+ }
+
+ spin_unlock(&sbi->s_tc_lock);
+
+ /* GFP_NOFS: this runs inside writeback. */
+ e = kmalloc_obj(*e, GFP_NOFS);
+ if (!e)
+ return; /* a checker that fails allocation checks less, not wrongly */
+ e->parent = parent;
+ e->slot = slot;
+ e->child = child;
+ e->ino = ino;
+ e->who = who;
+ INIT_HLIST_NODE(&e->cnode);
+
+ spin_lock(&sbi->s_tc_lock);
+ hash_add(sbi->s_tc, &e->node, key);
+ beamfs_tc_index_child(sbi, e);
+ sbi->s_tc_entries++;
+ spin_unlock(&sbi->s_tc_lock);
+}
+
+/*
+ * A freed block's pointer is allowed to disappear.
+ *
+ * Called from beamfs_free_block. Every slot naming this block is
+ * forgotten, so a later store into that slot is not a violation. Without
+ * this the checker would report every ordinary truncate.
+ */
+void beamfs_tc_forget_child(struct super_block *sb, u64 child)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_tc_entry *e;
+ struct hlist_node *tmp;
+ u32 ckey = beamfs_tc_ckey(child);
+
+ spin_lock(&sbi->s_tc_lock);
+ hash_for_each_possible_safe(sbi->s_tc_child, e, tmp, cnode, ckey) {
+ if (e->child == child)
+ beamfs_tc_drop(sbi, e);
+ }
+ spin_unlock(&sbi->s_tc_lock);
+}
+
+/*
+ * A freed parent takes its slots with it.
+ *
+ * The block is about to be handed to somebody else, who will zero it
+ * and store into the same slots. Those stores are not violations.
+ */
+void beamfs_tc_forget_parent(struct super_block *sb, u64 parent)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_tc_entry *e;
+ struct hlist_node *tmp;
+ u32 slot;
+
+ /*
+ * The 512 slots this block could have, each one probe, rather
+ * than the whole table for every freed block.
+ */
+ spin_lock(&sbi->s_tc_lock);
+ for (slot = 0; slot < BEAMFS_INDIRECT_PTRS; slot++) {
+ u32 key = beamfs_tc_key(parent, slot);
+
+ hash_for_each_possible_safe(sbi->s_tc, e, tmp, node, key) {
+ if (e->parent == parent && e->slot == slot) {
+ beamfs_tc_drop(sbi, e);
+ break;
+ }
+ }
+ }
+ spin_unlock(&sbi->s_tc_lock);
+}
+
+/*
+ * A slot deliberately set to zero.
+ *
+ * Truncate zeroes slots it has freed the children of. Without telling
+ * the checker, the next store into that slot reads as a lost pointer.
+ */
+void beamfs_tc_clear(struct super_block *sb, u64 parent, u32 slot)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_tc_entry *e;
+ u32 key = beamfs_tc_key(parent, slot);
+
+ spin_lock(&sbi->s_tc_lock);
+ hash_for_each_possible(sbi->s_tc, e, node, key) {
+ if (e->parent == parent && e->slot == slot) {
+ beamfs_tc_unindex_child(e);
+ e->child = 0;
+ break;
+ }
+ }
+ spin_unlock(&sbi->s_tc_lock);
+}
+
+/*
+ * A block about to be memset as a fresh indirect block.
+ *
+ * Every memset in the allocator follows sb_getblk on a block the
+ * allocator has just handed out, so the block should hold nothing. If
+ * the checker still has live pointers recorded for it, the allocator
+ * gave out a block that is in service, and the memset is about to erase
+ * a subtree.
+ *
+ * generic/464 writes with pwrite -b 65536 on a truncated file, so each
+ * write rebuilds the whole tree; the lost slots come out at a fixed
+ * stride of 17, which is 65536 / 3824 rounded down: the logical blocks
+ * one write covers. Slots vanishing in groups at a fixed stride is what a
+ * whole block being zeroed looks like, not what a race on one slot
+ * looks like.
+ */
+void beamfs_tc_zeroed(struct super_block *sb, unsigned long ino, u64 parent,
+ const char *who)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_tc_entry *e;
+ struct hlist_node *tmp;
+ unsigned int live = 0;
+ u64 first = 0;
+ u32 first_slot = 0;
+ u32 slot;
+
+ spin_lock(&sbi->s_tc_lock);
+ for (slot = 0; slot < BEAMFS_INDIRECT_PTRS; slot++) {
+ u32 key = beamfs_tc_key(parent, slot);
+
+ hash_for_each_possible_safe(sbi->s_tc, e, tmp, node, key) {
+ if (e->parent != parent || e->slot != slot)
+ continue;
+ if (e->child && !live) {
+ first = e->child;
+ first_slot = e->slot;
+ }
+ if (e->child)
+ live++;
+ beamfs_tc_drop(sbi, e);
+ break;
+ }
+ }
+ spin_unlock(&sbi->s_tc_lock);
+
+ if (live) {
+ sbi->s_tc_violations++;
+ pr_err("beamfs/treecheck: ZEROED IN SERVICE block=%llu had %u live pointer(s) (slot %u held %llu), zeroed by %s for ino=%lu\n",
+ (unsigned long long)parent, live, first_slot,
+ (unsigned long long)first, who, ino);
+ WARN_ONCE(1, "beamfs: an indirect block in service was zeroed\n");
+ }
+}
+
+#endif /* CONFIG_BEAMFS_DEBUG_TREE */
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 06/18] beamfs: add block and inode allocator
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (4 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 05/18] beamfs: add indirect parity, tree checker Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 07/18] beamfs: add superblock operations Aurelien DESBRIERES
` (11 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
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
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 07/18] beamfs: add superblock operations
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (5 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 06/18] beamfs: add block and inode allocator Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 08/18] beamfs: add inode and directory operations Aurelien DESBRIERES
` (10 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
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
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 08/18] beamfs: add inode and directory operations
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (6 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 07/18] beamfs: add superblock operations Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 09/18] beamfs: add namespace operations Aurelien DESBRIERES
` (9 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
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
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 09/18] beamfs: add namespace operations
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (7 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 08/18] beamfs: add inode and directory operations Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 10/18] beamfs: add iomap file operations Aurelien DESBRIERES
` (8 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
Directory entry operations: create, mkdir, unlink, rmdir, link and
rename.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/namei.c | 1538 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1538 insertions(+)
create mode 100644 fs/beamfs/namei.c
diff --git a/fs/beamfs/namei.c b/fs/beamfs/namei.c
new file mode 100644
index 000000000000..80b03c54d8fa
--- /dev/null
+++ b/fs/beamfs/namei.c
@@ -0,0 +1,1538 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - Filename / directory entry operations
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * Implements: create, mkdir, unlink, rmdir, link, symlink, rename
+ */
+
+#include <linux/fs.h>
+#include <linux/buffer_head.h>
+#include <linux/pagemap.h>
+#include <linux/slab.h>
+#include <linux/time.h>
+#include <linux/fs_dirent.h>
+#include "beamfs.h"
+#include "beamfs_trace.h"
+
+/* ------------------------------------------------------------------ */
+/* Helper: write a raw beamfs_inode to disk */
+/* ------------------------------------------------------------------ */
+
+/*
+ * The inode has to reach the medium the way the bitmap does.
+ *
+ * beamfs_write_bitmap_block encodes and dirties a bitmap block on every
+ * allocation and every free, and put_super/sync_fs then sync_dirty_buffer
+ * every one of them: the bitmap is on disk whatever the flusher does.
+ * The inode had only mark_buffer_dirty, and beamfs_write_inode ignored
+ * wbc->sync_mode entirely, so a WB_SYNC_ALL from unmount returned
+ * without waiting for anything.
+ *
+ * That asymmetry is the generic/464 leak. Measured on the failing
+ * volume: the bitmap block was written, twenty-four of twenty-five
+ * inodes seen in the trace were still 0xcd -- never written at all --
+ * and 112 blocks were marked used in the bitmap with no inode left to
+ * reference them. The volume mounted with 201 files totalling zero
+ * bytes.
+ *
+ * ext2 ends __ext2_write_inode with:
+ *
+ * mark_buffer_dirty(bh);
+ * if (do_sync) {
+ * sync_dirty_buffer(bh);
+ * ...
+ * }
+ *
+ * and ext2_write_inode passes wbc->sync_mode == WB_SYNC_ALL. This does
+ * the same.
+ */
+static int beamfs_write_inode_raw_flags(struct inode *inode, bool sync)
+{
+ if (beamfs_failed(inode->i_sb))
+ return -EIO;
+
+ struct super_block *sb = inode->i_sb;
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct beamfs_inode *raw;
+ struct buffer_head *bh;
+ unsigned long inodes_per_block;
+ unsigned long block, offset;
+
+ inodes_per_block = BEAMFS_BLOCK_SIZE / sizeof(struct beamfs_inode);
+ block = le64_to_cpu(sbi->s_beamfs_sb->s_inode_table_blk)
+ + (inode->i_ino - 1) / inodes_per_block;
+ offset = (inode->i_ino - 1) % inodes_per_block;
+
+ bh = beamfs_bread(sb, block, "directory");
+ if (!bh)
+ return -EIO;
+
+ raw = (struct beamfs_inode *)bh->b_data + offset;
+
+ raw->i_mode = cpu_to_le16(inode->i_mode);
+ raw->i_uid = cpu_to_le32(i_uid_read(inode));
+ raw->i_gid = cpu_to_le32(i_gid_read(inode));
+ raw->i_nlink = cpu_to_le16(inode->i_nlink);
+ raw->i_size = cpu_to_le64(inode->i_size);
+ raw->i_atime = cpu_to_le64(inode_get_atime_sec(inode) * NSEC_PER_SEC
+ + inode_get_atime_nsec(inode));
+ raw->i_mtime = cpu_to_le64(inode_get_mtime_sec(inode) * NSEC_PER_SEC
+ + inode_get_mtime_nsec(inode));
+ raw->i_ctime = cpu_to_le64(inode_get_ctime_sec(inode) * NSEC_PER_SEC
+ + inode_get_ctime_nsec(inode));
+ raw->i_flags = cpu_to_le32(fi->i_flags);
+
+ memcpy(raw->i_direct, fi->i_direct, sizeof(fi->i_direct));
+ raw->i_indirect = fi->i_indirect;
+ raw->i_dindirect = fi->i_dindirect;
+ raw->i_tindirect = fi->i_tindirect;
+
+ /*
+ * cpu_to_le32, because i_crc32 is __le32 and beamfs_crc32 returns
+ * host order. Assigning one to the other wrote the checksum in
+ * whatever order the CPU used -- invisible on the little-endian
+ * machines this has run on, and a volume no big-endian host could
+ * mount. sparse called it: "incorrect type in assignment".
+ */
+ raw->i_crc32 = cpu_to_le32(beamfs_inode_crc(raw));
+
+ /*
+ * Compute RS parity over the first BEAMFS_INODE_RS_DATA bytes
+ * (everything up to i_reserved, including i_crc32). Parity goes
+ * into i_reserved[0..15]; i_reserved[16..83] is forced to zero so
+ * the layout is deterministic and any non-zero byte there at read
+ * time signals a tampered inode.
+ *
+ * Under BEAMFS_DATA_PROTECTION_INODE_UNIVERSAL this parity is the
+ * authoritative correction record for the inode. mkfs.beamfs writes
+ * the equivalent parity on the root inode at format time; the
+ * kernel maintains it on every subsequent inode write.
+ */
+ memset(&raw->i_reserved[BEAMFS_RS_PARITY], 0,
+ sizeof(raw->i_reserved) - BEAMFS_RS_PARITY);
+ beamfs_rs_encode((u8 *)raw, BEAMFS_INODE_RS_DATA, raw->i_reserved);
+
+ trace_beamfs_write_inode(inode->i_sb->s_dev, inode->i_ino, sync,
+ (u64)le64_to_cpu(fi->i_indirect), 0);
+ mark_buffer_dirty(bh);
+ if (sync) {
+ sync_dirty_buffer(bh);
+ if (buffer_req(bh) && !buffer_uptodate(bh)) {
+ brelse(bh);
+ return -EIO;
+ }
+ }
+ brelse(bh);
+
+ return 0;
+}
+
+int beamfs_write_inode_raw(struct inode *inode)
+{
+ return beamfs_write_inode_raw_flags(inode, false);
+}
+
+/* ------------------------------------------------------------------ */
+/* Helper: directory block resolver (direct + indirect) */
+/* ------------------------------------------------------------------ */
+
+/*
+ * beamfs_dir_get_block -- look up or allocate a directory data block.
+ *
+ * @dir: directory inode
+ * @block_idx: 0-based logical block index. Range 0..(12 + 512 - 1)
+ * = 0..523 = direct (12) + single indirect (512).
+ * @alloc: if true, allocate on demand. If false, lookup only.
+ * @out_block: resolved physical block number on success. Set to 0 if
+ * the block is a HOLE and @alloc is false.
+ *
+ * Returns:
+ * 0 success (*out_block set)
+ * -ENOSPC no free blocks (when @alloc=true)
+ * -EIO indirect block read failure
+ * -EINVAL block_idx >= 524 (caller bug)
+ *
+ * Direct vs indirect mapping:
+ * block_idx < 12 -> fi->i_direct[block_idx]
+ * block_idx < 524 -> via fi->i_indirect, slot=(block_idx - 12)
+ *
+ * Newly allocated blocks (both data and indirect) are zeroed before
+ * being installed. A new directory block also gets one free record,
+ * d_ino == 0, spanning its 3824-byte payload, then its parity.
+ */
+int beamfs_dir_get_block(struct inode *dir, unsigned int block_idx,
+ bool alloc, u64 *out_block)
+{
+ struct super_block *sb = dir->i_sb;
+ struct beamfs_inode_info *fi = BEAMFS_I(dir);
+ struct buffer_head *ibh;
+ struct buffer_head *dbh;
+ __le64 *ptrs;
+ u64 block_no;
+ u64 indirect_blk;
+ u64 indirect_slot;
+
+ if (!out_block)
+ return -EINVAL;
+ *out_block = 0;
+
+ if (block_idx >= BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS)
+ return -EINVAL;
+
+ /* --- Direct --- */
+ if (block_idx < BEAMFS_DIRECT_BLOCKS) {
+ block_no = le64_to_cpu(fi->i_direct[block_idx]);
+ if (block_no) {
+ *out_block = block_no;
+ return 0;
+ }
+ if (!alloc)
+ return 0;
+
+ block_no = beamfs_alloc_block(sb, dir);
+ if (!block_no)
+ return -ENOSPC;
+ dbh = sb_getblk(sb, block_no);
+ if (!dbh) {
+ beamfs_free_block(sb, block_no, dir);
+ return -EIO;
+ }
+ /*
+ * A new directory block is one free record covering the
+ * whole payload, then parity over it.
+ *
+ * Zeroing alone used to be enough because every slot was a
+ * fixed 268 bytes and an all-zero slot read as free. With
+ * variable-length records the walk needs a d_rec_len to
+ * step by, and a block of zeros would stop it at the first
+ * entry -- so the block has to say, once, that all 3824
+ * bytes are available.
+ */
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ ((struct beamfs_dir_entry *)dbh->b_data)->d_rec_len =
+ cpu_to_le16(BEAMFS_DATA_INLINE_BYTES);
+ beamfs_dirent_encode(dbh);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ mark_buffer_dirty(dbh);
+ brelse(dbh);
+
+ fi->i_direct[block_idx] = cpu_to_le64(block_no);
+ /*
+ * What a directory block holds, not what it occupies.
+ *
+ * A block is 4096 bytes on the medium and carries 3824 of
+ * payload; the rest is Reed-Solomon parity. Counting the
+ * whole block puts 272 bytes of parity inside i_size, and
+ * a walk that trusts i_size reads them as a directory
+ * record. The one at the end of the root happens to
+ * decode as a zero length and stop the walk; nothing
+ * makes that true of the next block.
+ */
+ dir->i_size += BEAMFS_DATA_INLINE_BYTES;
+ mark_inode_dirty(dir);
+ *out_block = block_no;
+ return 0;
+ }
+
+ /* --- Indirect --- */
+ indirect_slot = block_idx - BEAMFS_DIRECT_BLOCKS;
+ indirect_blk = le64_to_cpu(fi->i_indirect);
+
+ /* Allocate indirect block on demand if missing. */
+ if (!indirect_blk) {
+ if (!alloc)
+ return 0;
+ indirect_blk = beamfs_alloc_block(sb, dir);
+ if (!indirect_blk)
+ return -ENOSPC;
+ ibh = sb_getblk(sb, indirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, indirect_blk, dir);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ beamfs_ind_parity_update(sb, ibh, dir);
+ unlock_buffer(ibh);
+ /*
+ * On the inode's metadata list, like every other site that
+ * dirties an indirect block: a buffer attached to nothing
+ * is never flushed by __writeback_single_inode.
+ */
+ mmb_mark_buffer_dirty(ibh, &BEAMFS_I(dir)->i_metadata_bhs);
+ brelse(ibh);
+ fi->i_indirect = cpu_to_le64(indirect_blk);
+ mark_inode_dirty(dir);
+ }
+
+ ibh = beamfs_bread(sb, indirect_blk, "directory");
+ if (!ibh)
+ return -EIO;
+ ptrs = (__le64 *)ibh->b_data;
+ block_no = le64_to_cpu(ptrs[indirect_slot]);
+
+ if (block_no) {
+ brelse(ibh);
+ *out_block = block_no;
+ return 0;
+ }
+
+ if (!alloc) {
+ brelse(ibh);
+ return 0;
+ }
+
+ block_no = beamfs_alloc_block(sb, dir);
+ if (!block_no) {
+ brelse(ibh);
+ return -ENOSPC;
+ }
+ dbh = sb_getblk(sb, block_no);
+ if (!dbh) {
+ beamfs_free_block(sb, block_no, dir);
+ brelse(ibh);
+ return -EIO;
+ }
+ /* Same as the direct case: one free record, then parity. */
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ ((struct beamfs_dir_entry *)dbh->b_data)->d_rec_len =
+ cpu_to_le16(BEAMFS_DATA_INLINE_BYTES);
+ beamfs_dirent_encode(dbh);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ mark_buffer_dirty(dbh);
+ brelse(dbh);
+
+ /*
+ * A directory's indirect block needs its parity like a file's.
+ *
+ * This file had no parity update at all, so a directory
+ * that grew past twelve blocks allocated an indirect block, filled
+ * it with pointers, and left the parity region describing nothing.
+ * The checker then read it as beyond correction and orphaned the
+ * whole subtree.
+ *
+ * generic/310 shows it on inode 3, the test directory: twelve
+ * direct blocks full, indirect block 18455 holding six valid
+ * pointers, and its parity slot zero across all 256 bytes -- the
+ * six blocks below reported lost.
+ */
+ lock_buffer(ibh);
+ ptrs[indirect_slot] = cpu_to_le64(block_no);
+ beamfs_ind_parity_update(sb, ibh, dir);
+ unlock_buffer(ibh);
+ mmb_mark_buffer_dirty(ibh, &BEAMFS_I(dir)->i_metadata_bhs);
+ brelse(ibh);
+ /* Payload, not block size -- see the note above. */
+ dir->i_size += BEAMFS_DATA_INLINE_BYTES;
+ mark_inode_dirty(dir);
+ *out_block = block_no;
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Helper: add a directory entry to a directory inode */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_add_dirent(struct inode *dir, const struct qstr *name,
+ unsigned long ino, u8 file_type)
+{
+ struct super_block *sb = dir->i_sb;
+ struct buffer_head *bh;
+ struct beamfs_dir_entry *de;
+ u8 *payload;
+ u64 block_no;
+ u32 offset, place;
+ u16 want;
+ unsigned int i;
+ int ret = -ENOSPC;
+
+ if (name->len > BEAMFS_MAX_FILENAME)
+ return -ENAMETOOLONG;
+
+ want = (u16)BEAMFS_DIRENT_LEN(name->len);
+
+ payload = beamfs_scratch_get(sb);
+ if (!payload)
+ return -ENOMEM;
+
+ /*
+ * Search first, allocate only when nothing has room.
+ *
+ * Asking dir_get_block to allocate on every pass gave the
+ * directory a fresh block per attempt: generic/006 ran out of
+ * space after 1047 files where it wanted 4096, and the blocks it
+ * burned were never linked to anything, so entries written into
+ * them were invisible to the next walk. That is where the stale
+ * handles and the missing renames came from too.
+ */
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS; i++) {
+ int rc = beamfs_dir_get_block(dir, i, false, &block_no);
+
+ if (rc) {
+ ret = rc;
+ goto out;
+ }
+ if (!block_no) {
+ /*
+ * End of what exists: take one more block, under the
+ * allocation mutex, as a file takes one. The walks
+ * that hold that mutex alone, beamfs_getattr's count
+ * and the scrubber's, then see the directory whole.
+ */
+ mutex_lock(&BEAMFS_I(dir)->i_alloc_mutex);
+ rc = beamfs_dir_get_block(dir, i, true, &block_no);
+ mutex_unlock(&BEAMFS_I(dir)->i_alloc_mutex);
+ if (rc) {
+ ret = rc;
+ goto out;
+ }
+ if (!block_no) {
+ ret = -ENOSPC;
+ goto out;
+ }
+ }
+
+ bh = beamfs_bread(sb, block_no, "directory");
+ if (!bh) {
+ ret = -EIO;
+ goto out;
+ }
+
+ rc = beamfs_dirent_decode(sb, bh, payload);
+ if (rc < 0) {
+ brelse(bh);
+ ret = rc;
+ goto out;
+ }
+
+ /*
+ * Look for a record with room to spare.
+ *
+ * A deleted entry leaves its record in place with d_ino
+ * zeroed, so the space it held is reusable without
+ * compacting the block. An entry in use can also give up
+ * its tail if its record is longer than its name needs --
+ * which is how ext2 has done it since 1993, and what keeps
+ * a directory from fragmenting as names come and go.
+ */
+ offset = 0;
+ while (offset < BEAMFS_DATA_INLINE_BYTES) {
+ u16 rec, used;
+ u32 next;
+
+ de = (struct beamfs_dir_entry *)(payload + offset);
+ if (!beamfs_dirent_valid(de, offset))
+ break;
+
+ rec = le16_to_cpu(de->d_rec_len);
+ used = de->d_ino ?
+ (u16)BEAMFS_DIRENT_LEN(de->d_name_len) : 0;
+
+ if (!de->d_ino && rec >= want) {
+ /* Free record, large enough as it stands. */
+ place = offset;
+ goto place_it;
+ }
+ if (de->d_ino && rec - used >= want &&
+ beamfs_dirent_place(offset + used, want) ==
+ offset + used) {
+ /* Split the tail off a live record. */
+ de->d_rec_len = cpu_to_le16(used);
+ place = offset + used;
+ goto place_it;
+ }
+
+ next = beamfs_dirent_next(de, offset);
+ if (next == BEAMFS_DIRENT_NOSPACE)
+ break;
+ offset = next;
+ }
+ brelse(bh);
+ continue;
+
+place_it:
+ de = (struct beamfs_dir_entry *)(payload + place);
+ {
+ u16 rec = le16_to_cpu(de->d_rec_len);
+ u16 keep = want;
+
+ /*
+ * Take what the name needs and leave the rest as a
+ * free record.
+ *
+ * Keeping the whole thing gave a 24-byte name the
+ * 3800 bytes the free record happened to span: the
+ * block held one more entry and no others, and the
+ * walk stepped 3800 bytes straight past the end. So
+ * generic/006 stopped at 1047 files of 4096, and
+ * anything written afterwards was invisible to
+ * lookup while add_dirent still found the space --
+ * "already present", and rename's del_dirent
+ * failing after its add.
+ *
+ * The remainder becomes a free record only when it
+ * can hold a header; below that it stays as slack
+ * on this one, which is what ext2 does.
+ */
+ if (rec >= (u16)(want + BEAMFS_DIRENT_MIN_LEN)) {
+ struct beamfs_dir_entry *rest =
+ (struct beamfs_dir_entry *)
+ (payload + place + want);
+
+ memset(rest, 0, BEAMFS_DIRENT_HDR_LEN);
+ rest->d_rec_len = cpu_to_le16(rec - want);
+ } else {
+ keep = max(rec, want);
+ }
+
+ memset(de, 0, BEAMFS_DIRENT_HDR_LEN);
+ de->d_ino = cpu_to_le64(ino);
+ de->d_rec_len = cpu_to_le16(keep);
+ de->d_name_len = (u8)name->len;
+ de->d_file_type = file_type;
+ memcpy(de->d_name, name->name, name->len);
+ }
+
+ lock_buffer(bh);
+ memcpy(bh->b_data, payload, BEAMFS_DATA_INLINE_BYTES);
+ beamfs_dirent_encode(bh);
+ set_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ mark_buffer_dirty(bh);
+ brelse(bh);
+
+ /*
+ * The directory changed, so its times did too. Without
+ * these, generic/003 reports that mtime and ctime stand
+ * still across a file creation.
+ */
+ inode_set_mtime_to_ts(dir, current_time(dir));
+ inode_set_ctime_to_ts(dir, current_time(dir));
+ mark_inode_dirty(dir);
+
+ ret = 0;
+ goto out;
+ }
+
+out:
+ beamfs_scratch_put(sb, payload);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Helper: remove a directory entry from a directory */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_del_dirent(struct inode *dir, const struct qstr *name)
+{
+ struct super_block *sb = dir->i_sb;
+ struct buffer_head *bh;
+ struct beamfs_dir_entry *de, *prev;
+ u8 *payload;
+ u64 block_no;
+ u32 offset, prev_off;
+ unsigned int i;
+ int ret = -ENOENT;
+
+ payload = beamfs_scratch_get(sb);
+ if (!payload)
+ return -ENOMEM;
+
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS; i++) {
+ int rc = beamfs_dir_get_block(dir, i, false, &block_no);
+
+ if (rc) {
+ ret = rc;
+ goto out;
+ }
+ if (!block_no)
+ break;
+
+ bh = beamfs_bread(sb, block_no, "directory");
+ if (!bh)
+ continue;
+
+ rc = beamfs_dirent_decode(sb, bh, payload);
+ if (rc < 0) {
+ brelse(bh);
+ ret = rc;
+ goto out;
+ }
+
+ offset = 0;
+ prev_off = BEAMFS_DIRENT_NOSPACE;
+ 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 == name->len &&
+ !memcmp(de->d_name, name->name, name->len)) {
+ /*
+ * Fold the record into its predecessor when
+ * that one is free and in the same subblock,
+ * so the space comes back as a single free
+ * record rather than a hole nothing can use.
+ * Otherwise zero d_ino and leave the record,
+ * which add_dirent will find.
+ *
+ * Never into a live predecessor.
+ *
+ * Growing a live entry's d_rec_len to cover
+ * the space just released makes the walk
+ * step over that space, so anything written
+ * there afterwards is invisible: rename put
+ * the new name in the hole, del_dirent could
+ * not find the old one, and the directory
+ * ended up with both -- "already present",
+ * then drop_nlink below zero when rm caught
+ * up. ext2 merges only into free records for
+ * exactly this reason.
+ *
+ * Zeroing d_ino and leaving the record where
+ * it is costs nothing: add_dirent reuses a
+ * free record and splits what it does not
+ * need.
+ */
+ de->d_ino = 0;
+
+ if (prev_off != BEAMFS_DIRENT_NOSPACE) {
+ prev = (struct beamfs_dir_entry *)
+ (payload + prev_off);
+
+ if (!prev->d_ino &&
+ prev_off / BEAMFS_SUBBLOCK_DATA ==
+ offset / BEAMFS_SUBBLOCK_DATA)
+ le16_add_cpu(&prev->d_rec_len,
+ le16_to_cpu(de->d_rec_len));
+ }
+
+ lock_buffer(bh);
+ memcpy(bh->b_data, payload,
+ BEAMFS_DATA_INLINE_BYTES);
+ beamfs_dirent_encode(bh);
+ set_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ mark_buffer_dirty(bh);
+ brelse(bh);
+
+ /*
+ * Removing an entry changes the directory as
+ * much as adding one does, so mtime and ctime
+ * move here too.
+ */
+ inode_set_mtime_to_ts(dir, current_time(dir));
+ inode_set_ctime_to_ts(dir, current_time(dir));
+ mark_inode_dirty(dir);
+
+ ret = 0;
+ goto out;
+ }
+
+ next = beamfs_dirent_next(de, offset);
+ if (next == BEAMFS_DIRENT_NOSPACE)
+ break;
+ prev_off = offset;
+ offset = next;
+ }
+ brelse(bh);
+ }
+
+out:
+ beamfs_scratch_put(sb, payload);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Helper: allocate and initialize a new VFS inode */
+/* ------------------------------------------------------------------ */
+
+struct inode *beamfs_new_inode(struct inode *dir, umode_t mode)
+{
+ struct super_block *sb = dir->i_sb;
+ struct inode *inode;
+ struct beamfs_inode_info *fi;
+ u64 ino;
+
+ ino = beamfs_alloc_inode_num(sb);
+ if (!ino)
+ return ERR_PTR(-ENOSPC);
+
+ inode = new_inode(sb);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
+ inode->i_ino = ino;
+ inode->i_size = 0;
+ inode_set_atime_to_ts(inode, current_time(inode));
+ inode_set_mtime_to_ts(inode, current_time(inode));
+ inode_set_ctime_to_ts(inode, current_time(inode));
+
+ fi = BEAMFS_I(inode);
+ memset(fi->i_direct, 0, sizeof(fi->i_direct));
+ fi->i_indirect = 0;
+ fi->i_dindirect = 0;
+ fi->i_flags = 0;
+
+ if (S_ISDIR(mode)) {
+ inode->i_op = &beamfs_dir_inode_operations;
+ inode->i_fop = &beamfs_dir_operations;
+ set_nlink(inode, 2);
+ } else {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(inode->i_sb);
+
+ 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;
+ }
+ set_nlink(inode, 1);
+ }
+
+ if (insert_inode_locked(inode) < 0) {
+ make_bad_inode(inode);
+ iput(inode);
+ return ERR_PTR(-EIO);
+ }
+ mark_inode_dirty(inode);
+ trace_beamfs_inode_dirty(inode->i_sb->s_dev, inode->i_ino,
+ (unsigned long)inode_state_read_once(inode));
+ return inode;
+}
+
+/* ------------------------------------------------------------------ */
+/* create - create a regular file */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_create(const struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *dentry, umode_t mode)
+{
+ struct inode *inode;
+ int ret;
+
+ inode = beamfs_new_inode(dir, mode | S_IFREG);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+
+ ret = beamfs_write_inode_raw(inode);
+ if (ret)
+ goto out_iput;
+
+ ret = beamfs_add_dirent(dir, &dentry->d_name, inode->i_ino, DT_REG);
+ if (ret)
+ goto out_iput;
+
+ ret = beamfs_write_inode_raw(dir);
+ if (ret)
+ goto out_iput;
+
+ d_instantiate(dentry, inode);
+ unlock_new_inode(inode);
+ return 0;
+
+out_iput:
+ /*
+ * The inode was allocated, written to disk with nlink=1, and
+ * never linked. beamfs_evict_inode frees an inode only when
+ * nlink has reached zero, so releasing it here without
+ * decrementing leaves it allocated on the medium with no name --
+ * space that nothing can reach and nothing will reclaim.
+ *
+ * generic/204 fills the inode table and shows the result: 1678
+ * inodes, contiguous from 14707 to the end of the table, all
+ * mode 0100644 nlink=1 size=0, every one of them a create that
+ * failed after the inode was written.
+ *
+ * mkdir below already does this, twice, because a directory is
+ * born with nlink=2. discard_new_inode is what the VFS provides
+ * for an inode that was never published -- btrfs, ceph, ext2,
+ * jfs, ntfs3 and udf all use it on this path.
+ */
+ inode_dec_link_count(inode);
+ discard_new_inode(inode);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* mkdir - create a directory */
+/* ------------------------------------------------------------------ */
+
+static struct dentry *beamfs_mkdir(const struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *dentry, umode_t mode)
+{
+ struct inode *inode;
+ int ret;
+
+ inode_inc_link_count(dir);
+
+ inode = beamfs_new_inode(dir, mode | S_IFDIR);
+ if (IS_ERR(inode)) {
+ inode_dec_link_count(dir);
+ return ERR_CAST(inode);
+ }
+
+ /* Add . and .. entries */
+ ret = beamfs_add_dirent(inode, &(struct qstr)QSTR_INIT(".", 1),
+ inode->i_ino, DT_DIR);
+ if (ret)
+ goto out_fail;
+
+ ret = beamfs_add_dirent(inode, &(struct qstr)QSTR_INIT("..", 2),
+ dir->i_ino, DT_DIR);
+ if (ret)
+ goto out_fail;
+
+ ret = beamfs_write_inode_raw(inode);
+ if (ret)
+ goto out_fail;
+
+ ret = beamfs_add_dirent(dir, &dentry->d_name, inode->i_ino,
+ DT_DIR);
+ if (ret)
+ goto out_fail;
+
+ ret = beamfs_write_inode_raw(dir);
+ if (ret)
+ goto out_fail;
+
+ d_instantiate(dentry, inode);
+ unlock_new_inode(inode);
+ return NULL;
+
+out_fail:
+ /* Twice: a directory is born with nlink=2. */
+ inode_dec_link_count(inode);
+ inode_dec_link_count(inode);
+ discard_new_inode(inode);
+ inode_dec_link_count(dir);
+ return ERR_PTR(ret);
+}
+
+/* ------------------------------------------------------------------ */
+/* unlink - remove a file */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_unlink(struct inode *dir, struct dentry *dentry)
+{
+ struct inode *inode = d_inode(dentry);
+ int ret;
+
+ ret = beamfs_del_dirent(dir, &dentry->d_name);
+ if (ret)
+ return ret;
+
+ inode_set_ctime_to_ts(inode, current_time(inode));
+ inode_dec_link_count(inode);
+ /*
+ * The operation has succeeded and the name is gone from the
+ * tree; failing it now would be a lie. But an inode that does
+ * not reach the medium is a link count the next mount disagrees
+ * with, and saying nothing is how that becomes a checker's
+ * problem weeks later.
+ */
+ {
+ int _w = beamfs_write_inode_raw(dir);
+
+ if (_w)
+ pr_err_ratelimited("beamfs: unlink: parent inode %llu not written: %d\n",
+ (unsigned long long)dir->i_ino, _w);
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* rmdir - remove an empty directory */
+/* ------------------------------------------------------------------ */
+
+/*
+ * beamfs_dir_is_empty -- does @inode hold anything but . and .. ?
+ *
+ * i_nlink is not the answer. A regular file does not bump its parent's
+ * link count, so a directory full of files still reads nlink == 2 and
+ * the obvious test says empty. rmdir made that mistake and could never
+ * remove anything; rename made the mirror image of it and silently
+ * destroyed the target's contents -- caught by xfstests generic/023,
+ * where "dire/tree -> Directory not empty" came back as success.
+ *
+ * The blocks have to be walked. Holes are skipped rather than treated
+ * as the end: a directory with a freed slot followed by live entries
+ * is not empty, and stopping at the first hole would say it was.
+ */
+static int beamfs_dir_is_empty(struct inode *inode)
+{
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *bh;
+ struct beamfs_dir_entry *de;
+ u8 *payload;
+ u64 block_no;
+ u32 offset;
+ unsigned int i;
+ /*
+ * Zero means empty. rmdir treats any non-zero return as the
+ * reason it cannot proceed and hands it straight back, so this is
+ * an error code and not a predicate -- getting that backwards
+ * made every rmdir on an empty directory fail.
+ */
+ int ret = 0;
+
+ payload = beamfs_scratch_get(sb);
+ if (!payload)
+ return -ENOMEM;
+
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS; i++) {
+ int rc = beamfs_dir_get_block(inode, i, false, &block_no);
+
+ if (rc) {
+ ret = rc;
+ goto out;
+ }
+ if (!block_no)
+ break;
+
+ bh = beamfs_bread(sb, block_no, "directory");
+ if (!bh)
+ continue;
+
+ rc = beamfs_dirent_decode(sb, bh, payload);
+ brelse(bh);
+ if (rc < 0) {
+ ret = rc;
+ 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 == 1 && de->d_name[0] == '.') &&
+ !(de->d_name_len == 2 && de->d_name[0] == '.' &&
+ de->d_name[1] == '.')) {
+ ret = -ENOTEMPTY;
+ goto out;
+ }
+
+ next = beamfs_dirent_next(de, offset);
+ if (next == BEAMFS_DIRENT_NOSPACE)
+ break;
+ offset = next;
+ }
+ }
+
+out:
+ beamfs_scratch_put(sb, payload);
+ return ret;
+}
+
+static int beamfs_rmdir(struct inode *dir, struct dentry *dentry)
+{
+ struct inode *inode = d_inode(dentry);
+ int ret;
+
+ /* Same walk rename needs; one implementation for both. */
+ ret = beamfs_dir_is_empty(inode);
+ if (ret)
+ return ret;
+
+ ret = beamfs_del_dirent(dir, &dentry->d_name);
+ if (ret)
+ return ret;
+
+ inode_dec_link_count(inode);
+ inode_dec_link_count(inode);
+ inode_dec_link_count(dir);
+ {
+ int _w = beamfs_write_inode_raw(dir);
+
+ /* See unlink: the rmdir has happened either way. */
+ if (_w)
+ pr_err_ratelimited("beamfs: rmdir: parent inode %llu not written: %d\n",
+ (unsigned long long)dir->i_ino, _w);
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* link - create a hard link */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_link(struct dentry *old_dentry, struct inode *dir,
+ struct dentry *dentry)
+{
+ struct inode *inode = d_inode(old_dentry);
+ int ret;
+
+ inode_set_ctime_to_ts(inode, current_time(inode));
+ inode_inc_link_count(inode);
+
+ ret = beamfs_add_dirent(dir, &dentry->d_name, inode->i_ino,
+ fs_umode_to_dtype(inode->i_mode));
+ if (ret) {
+ inode_dec_link_count(inode);
+ return ret;
+ }
+
+ {
+ int _w = beamfs_write_inode_raw(inode);
+
+ if (!_w)
+ _w = beamfs_write_inode_raw(dir);
+ if (_w)
+ pr_err_ratelimited("beamfs: link: inode %llu or parent %llu not written: %d\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)dir->i_ino, _w);
+ }
+ d_instantiate(dentry, inode);
+ ihold(inode);
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* symlink - create a symbolic link */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Put a long target in a data block and point i_direct[0] at it.
+ *
+ * The block is written through the same RS encoder as file data, so the
+ * target is protected exactly as a file of the same length would be.
+ * The tail is zeroed rather than left as whatever the allocator
+ * returned: a symlink is read by length, but an uninitialised tail
+ * still means the block's parity covers bytes nobody chose.
+ */
+static int beamfs_symlink_store_block(struct inode *inode, const char *target,
+ size_t len)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *bh;
+ u64 phys;
+ u8 *staging;
+ int ret = 0;
+
+ staging = beamfs_scratch_get(sb);
+ if (!staging)
+ return -ENOMEM;
+ memset(staging, 0, BEAMFS_DATA_INLINE_BYTES);
+ memcpy(staging, target, len);
+
+ phys = beamfs_alloc_block(sb, inode);
+ if (!phys) {
+ beamfs_scratch_put(sb, staging);
+ return -ENOSPC;
+ }
+
+ bh = sb_getblk(sb, phys);
+ if (!bh) {
+ beamfs_free_block(sb, phys, inode);
+ beamfs_scratch_put(sb, staging);
+ return -EIO;
+ }
+
+ lock_buffer(bh);
+ memset(bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ /*
+ * A symlink's target is a data block, so it follows the volume's
+ * data layout.
+ *
+ * This laid it down in sixteen runs 255 apart and encoded it the
+ * same way, whatever the volume did, so on a capsule volume the
+ * kernel read back sixteen uncorrectable subblocks and an empty
+ * path. generic/360 is exactly that: md5 of nothing where the
+ * target should be, on a block holding 1051 intact bytes of it
+ * and a parity area of zeros.
+ */
+ beamfs_lay_data_payload(sb, (u8 *)bh->b_data, staging);
+ /*
+ * The descriptor too, before the seal, as for any data block: on
+ * a DATA_CSUM volume a target without one failed its check on
+ * every read. Logical block 0, what the reader checks it against.
+ */
+ beamfs_stamp_data_block(sb, (u8 *)bh->b_data, staging, inode->i_ino, 0);
+ {
+ int _e = beamfs_seal_data_block(sb, (u8 *)bh->b_data);
+
+ if (_e < 0)
+ pr_err_ratelimited("beamfs: symlink block encode failed: %d\n",
+ _e);
+ }
+ set_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ mark_buffer_dirty(bh);
+ ret = sync_dirty_buffer(bh);
+ brelse(bh);
+ beamfs_scratch_put(sb, staging);
+
+ if (ret) {
+ beamfs_free_block(sb, phys, inode);
+ return ret;
+ }
+
+ fi->i_direct[0] = cpu_to_le64(phys);
+ return 0;
+}
+
+/*
+ * beamfs_symlink -- create a symbolic link in directory @dir.
+ *
+ * A target shorter than i_direct[] (96 bytes) is stored inline, in the
+ * bytes of i_direct[]. A longer one, up to what a data block holds,
+ * goes in a data block whose number is in i_direct[0]; i_size tells the
+ * two apart. Anything longer, or an empty target, gets -ENAMETOOLONG.
+ */
+static int beamfs_symlink(const struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *dentry, const char *symname)
+{
+ struct inode *inode;
+ struct beamfs_inode_info *fi;
+ size_t len;
+ int ret;
+
+ len = strlen(symname);
+ /*
+ * PATH_MAX is the ceiling the VFS enforces. Ninety-six bytes was not
+ * a design limit, it was the size of the field the short form
+ * happens to reuse, and any absolute path of moderate depth exceeds
+ * it -- generic/360 links to a 1019-byte path and got ENAMETOOLONG.
+ * The limit is what a data block holds, beamfs_block_payload():
+ * 3808 bytes in a capsule, 3824 in the alternating layout. Held to
+ * BEAMFS_DATA_INLINE_BYTES, a capsule cut a target of 3809 to 3824
+ * bytes to 3808. Longer, up to PATH_MAX, is still refused.
+ */
+ if (len == 0 || len > beamfs_block_payload(dir->i_sb))
+ return -ENAMETOOLONG;
+
+ inode = beamfs_new_inode(dir, S_IFLNK | 0777);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+
+ fi = BEAMFS_I(inode);
+ memset(fi->i_direct, 0, sizeof(fi->i_direct));
+
+ if (len < sizeof(fi->i_direct)) {
+ /*
+ * Short form: the target lives in i_direct[] as raw bytes,
+ * carried to disk by write_inode_raw's verbatim memcpy and
+ * covered by the inode's own CRC and RS parity.
+ */
+ memcpy(fi->i_direct, symname, len);
+ } else {
+ /*
+ * Long form: the target goes in a data block, and
+ * i_direct[0] holds its number. i_size tells the two apart
+ * on the way back, so nothing new is needed on disk.
+ *
+ * A block brings the RS codewords with it, so a long target
+ * ends up better protected than a short one -- which is the
+ * right way round, since it carries more to lose.
+ */
+ ret = beamfs_symlink_store_block(inode, symname, len);
+ if (ret)
+ goto out_iput;
+ }
+
+ inode->i_size = len;
+ inode->i_op = &beamfs_symlink_inode_operations;
+
+ ret = beamfs_write_inode_raw(inode);
+ if (ret)
+ goto out_iput;
+
+ ret = beamfs_add_dirent(dir, &dentry->d_name, inode->i_ino, DT_LNK);
+ if (ret)
+ goto out_iput;
+
+ ret = beamfs_write_inode_raw(dir);
+ if (ret)
+ goto out_iput;
+
+ d_instantiate(dentry, inode);
+ unlock_new_inode(inode);
+ return 0;
+
+out_iput:
+ /*
+ * As in beamfs_create: the inode was written with nlink=1 and
+ * never linked, so the link is dropped before the inode is
+ * discarded, or it stays allocated on the medium with no name.
+ */
+ inode_dec_link_count(inode);
+ discard_new_inode(inode);
+ return ret;
+}
+
+/*
+ * beamfs_get_link -- VFS i_op->get_link callback.
+ *
+ * A short target is returned in place from i_direct[], which lives as
+ * long as the in-memory inode. A long one is read from its data block
+ * into a buffer the VFS frees through @done.
+ */
+static const char *beamfs_get_link(struct dentry *dentry,
+ struct inode *inode,
+ struct delayed_call *done)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *bh;
+ loff_t len = i_size_read(inode);
+ u64 phys;
+ char *buf;
+ int ret;
+
+ if (len < (loff_t)sizeof(fi->i_direct))
+ return (const char *)fi->i_direct;
+
+ /*
+ * Long form. RCU-walk cannot take the buffer lock or sleep on a
+ * read, so hand it back to the caller for a ref-walk retry.
+ */
+ if (!dentry)
+ return ERR_PTR(-ECHILD);
+
+ if (len > beamfs_block_payload(sb))
+ return ERR_PTR(-EUCLEAN);
+
+ phys = le64_to_cpu(fi->i_direct[0]);
+ if (!phys)
+ return ERR_PTR(-EUCLEAN);
+
+ /*
+ * kzalloc and not the mount reserve: this buffer is handed to the
+ * VFS through set_delayed_call(kfree_link), so the kernel frees
+ * it long after this function returns and it cannot come from a
+ * pool this filesystem owns.
+ */
+ buf = kzalloc(BEAMFS_DATA_INLINE_BYTES + 1, GFP_NOFS);
+ if (!buf)
+ return ERR_PTR(-ENOMEM);
+
+ bh = beamfs_bread(sb, phys, "directory");
+ if (!bh) {
+ kfree(buf);
+ return ERR_PTR(-EIO);
+ }
+
+ lock_buffer(bh);
+ ret = beamfs_inline_decode_symlink(sb, bh, phys, inode, buf,
+ (u32)len);
+ unlock_buffer(bh);
+ brelse(bh);
+
+ if (ret) {
+ kfree(buf);
+ return ERR_PTR(ret);
+ }
+
+ buf[len] = '\0';
+ set_delayed_call(done, kfree_link, buf);
+ return buf;
+}
+
+const struct inode_operations beamfs_symlink_inode_operations = {
+ .getattr = beamfs_getattr,
+ .get_link = beamfs_get_link,
+};
+
+/* ------------------------------------------------------------------ */
+/* write_inode - VFS super_op: persist inode to disk */
+/* ------------------------------------------------------------------ */
+
+int beamfs_write_inode(struct inode *inode, struct writeback_control *wbc)
+{
+ bool sync = wbc->sync_mode == WB_SYNC_ALL;
+ int ret, mret, sret;
+
+ /*
+ * Flush the inode's metadata buffers, not just the inode.
+ *
+ * The indirect blocks are attached to i_metadata_bhs by
+ * mmb_mark_buffer_dirty on every pointer install. Nothing empties
+ * that list except beamfs_fsync -- and generic/464 never calls
+ * fsync. ext2 has the same list but its indirect blocks also live
+ * in the block device's page cache, which sync_blockdev empties at
+ * unmount; a buffer only on the private list is written by nobody.
+ *
+ * That is the leak. An overnight run caught it 107 times over 2414
+ * loops: the owning inode was on disk in every single case, but
+ * carrying an old state -- inode 41 took 1037 mark_inode_dirty
+ * calls and reached write_inode once. __writeback_single_inode
+ * reads and clears I_DIRTY before calling ->write_inode, so every
+ * pointer installed during that window leaves the inode looking
+ * clean, and its indirect block sits dirty on a list no one walks.
+ *
+ * Writing the buffers here, on the same call that writes the
+ * inode, keeps the two together: whatever i_indirect points at is
+ * on the medium by the time the inode naming it is.
+ */
+ /*
+ * The volume's shared metadata first -- bitmap blocks and parity
+ * regions, see s_shared_bhs -- so that whatever this inode's
+ * pointers rely on is on the medium before the inode naming them.
+ */
+ {
+ int had = mmb_has_buffers(&BEAMFS_SB(inode->i_sb)->s_shared_bhs);
+
+ sret = mmb_sync(&BEAMFS_SB(inode->i_sb)->s_shared_bhs);
+ trace_beamfs_mmb(inode->i_sb->s_dev, inode->i_ino,
+ "write_inode shared", had, sret);
+ }
+ ret = beamfs_write_inode_raw_flags(inode, sync);
+
+ {
+ int had = mmb_has_buffers(&BEAMFS_I(inode)->i_metadata_bhs);
+
+ mret = mmb_sync(&BEAMFS_I(inode)->i_metadata_bhs);
+ trace_beamfs_mmb(inode->i_sb->s_dev, inode->i_ino,
+ "write_inode sync", had, mret);
+ }
+ if (!ret)
+ ret = mret;
+ if (!ret)
+ ret = sret;
+
+ /*
+ * Tell the VFS this inode may have metadata in flight.
+ *
+ * __writeback_single_inode calls ->sync_inode_metadata only for an
+ * inode carrying I_METADATA_WRITEBACK, and nothing sets that flag
+ * but the filesystem itself. beamfs declared the operation and
+ * never set the flag, so the operation was never called once --
+ * zero times in a 414000-line trace of generic/464 -- and the only
+ * thing writing the metadata buffers was this function.
+ *
+ * That is not enough. An inode written by WB_SYNC_NONE writeback
+ * leaves its buffers queued, and the WB_SYNC_ALL pass that should
+ * wait for them skipped the call that waits. ext2, ext4, fat,
+ * minix and bfs all set it here, at the end of ->write_inode, for
+ * the same reason.
+ */
+ set_inode_metadata_writeback(inode);
+
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* rename - move/rename a directory entry */
+/* ------------------------------------------------------------------ */
+
+static int beamfs_rename(const struct mnt_idmap *idmap,
+ struct inode *old_dir, struct dentry *old_dentry,
+ struct inode *new_dir, struct dentry *new_dentry,
+ unsigned int flags)
+{
+ struct inode *old_inode = d_inode(old_dentry);
+ struct inode *new_inode = d_inode(new_dentry);
+ int is_dir = S_ISDIR(old_inode->i_mode);
+ int ret;
+
+ /* No RENAME_EXCHANGE or RENAME_WHITEOUT. */
+ if (flags & ~RENAME_NOREPLACE)
+ return -EINVAL;
+
+ if (flags & RENAME_NOREPLACE && new_inode)
+ return -EEXIST;
+
+ /*
+ * If destination exists, unlink it first.
+ * For directories: target must be empty, which takes a walk of
+ * its blocks (beamfs_dir_is_empty) -- nlink cannot tell.
+ */
+ if (new_inode) {
+ if (is_dir) {
+ ret = beamfs_dir_is_empty(new_inode);
+ if (ret)
+ return ret;
+ }
+
+ ret = beamfs_del_dirent(new_dir, &new_dentry->d_name);
+ if (ret)
+ return ret;
+
+ if (is_dir) {
+ inode_dec_link_count(new_inode);
+ inode_dec_link_count(new_inode);
+ inode_dec_link_count(new_dir);
+ } else {
+ inode_dec_link_count(new_inode);
+ }
+
+ inode_set_ctime_to_ts(new_inode, current_time(new_inode));
+ }
+
+ /* Add entry in new_dir */
+ ret = beamfs_add_dirent(new_dir, &new_dentry->d_name,
+ old_inode->i_ino,
+ is_dir ? DT_DIR : DT_REG);
+ if (ret) {
+ /*
+ * The destination was unlinked above, before this was
+ * attempted. Failing now without putting it back loses
+ * a file that existed when the call started, and the
+ * caller is told the rename did not happen -- so the
+ * loss is silent.
+ *
+ * The name goes back to the inode it named. The link
+ * counts were lowered above and are raised again here,
+ * in the same order.
+ */
+ if (new_inode) {
+ int back = beamfs_add_dirent(new_dir,
+ &new_dentry->d_name,
+ new_inode->i_ino,
+ S_ISDIR(new_inode->i_mode)
+ ? DT_DIR : DT_REG);
+
+ if (back) {
+ pr_err("beamfs: rename: '%.*s' was removed from inode %llu and could not be restored: add %d, restore %d\n",
+ (int)new_dentry->d_name.len,
+ new_dentry->d_name.name,
+ (unsigned long long)new_dir->i_ino,
+ ret, back);
+ } else {
+ if (S_ISDIR(new_inode->i_mode)) {
+ inode_inc_link_count(new_inode);
+ inode_inc_link_count(new_inode);
+ inode_inc_link_count(new_dir);
+ } else {
+ inode_inc_link_count(new_inode);
+ }
+ }
+ }
+ return ret;
+ }
+
+ /* Remove entry from old_dir */
+ ret = beamfs_del_dirent(old_dir, &old_dentry->d_name);
+ if (ret) {
+ int undo;
+
+ /*
+ * Put it back, rather than leaving two names for one
+ * inode.
+ *
+ * The inode's link count was not raised for the name
+ * just added, so two entries share one link: the first
+ * unlink frees the inode and the other entry is left
+ * naming a free one. generic/076 came back with five of
+ * those and two inodes no directory reached, which is
+ * the same event seen from the other side.
+ *
+ * Removing what this call added restores the directory
+ * to what it was, and the caller gets an honest error
+ * instead of a filesystem that disagrees with itself.
+ * It cannot fail for want of space -- the record is
+ * there and only its d_ino is cleared -- and if it does
+ * fail anyway there is nothing further to try, so say
+ * both errors and let fsck see a bounded mess rather
+ * than a silent one.
+ */
+ undo = beamfs_del_dirent(new_dir, &new_dentry->d_name);
+ pr_err("beamfs: rename: could not remove '%.*s' from inode %llu after adding '%.*s' to inode %llu: %d; rollback %s\n",
+ (int)old_dentry->d_name.len, old_dentry->d_name.name,
+ (unsigned long long)old_dir->i_ino,
+ (int)new_dentry->d_name.len, new_dentry->d_name.name,
+ (unsigned long long)new_dir->i_ino,
+ ret, undo ? "FAILED" : "done");
+ return ret;
+ }
+
+ /*
+ * Update ".." in the moved directory to point to new_dir.
+ * Also fix nlink on old_dir and new_dir.
+ */
+ if (is_dir && old_dir != new_dir) {
+ struct qstr dotdot = QSTR_INIT("..", 2);
+
+ ret = beamfs_del_dirent(old_inode, &dotdot);
+ if (ret)
+ return ret;
+
+ ret = beamfs_add_dirent(old_inode, &dotdot,
+ new_dir->i_ino, DT_DIR);
+ if (ret)
+ return ret;
+
+ inode_dec_link_count(old_dir);
+ inode_inc_link_count(new_dir);
+
+ ret = beamfs_write_inode_raw(old_inode);
+ if (ret)
+ return ret;
+ }
+
+ /* Update timestamps */
+ inode_set_ctime_to_ts(old_inode, current_time(old_inode));
+ inode_set_mtime_to_ts(old_dir, current_time(old_dir));
+ inode_set_ctime_to_ts(old_dir, current_time(old_dir));
+ inode_set_mtime_to_ts(new_dir, current_time(new_dir));
+ inode_set_ctime_to_ts(new_dir, current_time(new_dir));
+
+ /* Persist all touched inodes */
+ ret = beamfs_write_inode_raw(old_inode);
+ if (ret)
+ return ret;
+
+ ret = beamfs_write_inode_raw(old_dir);
+ if (ret)
+ return ret;
+
+ if (old_dir != new_dir) {
+ ret = beamfs_write_inode_raw(new_dir);
+ if (ret)
+ return ret;
+ }
+
+ if (new_inode) {
+ int _w = beamfs_write_inode_raw(new_inode);
+
+ if (_w)
+ pr_err_ratelimited("beamfs: rename: replaced inode %llu not written: %d\n",
+ (unsigned long long)new_inode->i_ino, _w);
+ }
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* dir inode_operations - exported */
+/* ------------------------------------------------------------------ */
+
+const struct inode_operations beamfs_dir_inode_operations = {
+ .lookup = beamfs_lookup,
+ .create = beamfs_create,
+ .mkdir = beamfs_mkdir,
+ .unlink = beamfs_unlink,
+ .rmdir = beamfs_rmdir,
+ .link = beamfs_link,
+ .symlink = beamfs_symlink,
+ .rename = beamfs_rename,
+ .getattr = beamfs_getattr,
+};
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 10/18] beamfs: add iomap file operations
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (8 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 09/18] beamfs: add namespace operations Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 11/18] beamfs: inline data path 1/4, block map Aurelien DESBRIERES
` (7 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
file.c holds the file, inode and address_space operations of the
iomap-based path used by the INODE_UNIVERSAL scheme (scheme 5). That
path does not maintain the parity of indirect blocks, so the kernel
refuses to mount the scheme and mkfs.beamfs refuses to format it: on
volumes mkfs.beamfs creates, file data takes the inline path of the
next four patches. inode.c and namei.c choose between the two from the
volume's scheme.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/file.c | 549 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 549 insertions(+)
create mode 100644 fs/beamfs/file.c
diff --git a/fs/beamfs/file.c b/fs/beamfs/file.c
new file mode 100644
index 000000000000..553d177e5c31
--- /dev/null
+++ b/fs/beamfs/file.c
@@ -0,0 +1,549 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - File operations
+ * Author: roastercode - Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/iomap.h>
+#include <linux/pagemap.h>
+#include <linux/buffer_head.h>
+#include <linux/fiemap.h>
+#include "beamfs.h"
+
+/* Forward declaration - defined after iomap_ops */
+static ssize_t beamfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from);
+
+/*
+ * generic_file_fsync() was removed along with sync_mapping_buffers() and
+ * the i_private_list mechanism it drove. Metadata buffer_heads owned by
+ * an inode now live in a per-inode mapping_metadata_bhs list, flushed
+ * through mmb_fsync(); ext2_fsync() has the same shape.
+ */
+static int beamfs_fsync(struct file *file, loff_t start, loff_t end,
+ int datasync)
+{
+ return simple_fsync(file, start, end, datasync);
+}
+
+const struct file_operations beamfs_file_operations = {
+ .llseek = generic_file_llseek,
+ .read_iter = generic_file_read_iter,
+ .write_iter = beamfs_file_write_iter,
+ .mmap = generic_file_mmap,
+ .fsync = beamfs_fsync,
+ .splice_read = filemap_splice_read,
+};
+
+const struct inode_operations beamfs_file_inode_operations = {
+ .getattr = simple_getattr,
+ .fiemap = beamfs_fiemap, /* S2.2: file-precise bench targeting */
+};
+
+/*
+ * beamfs_iomap_begin -- map a file range to disk blocks for iomap.
+ * Handles read (no allocation) and write (allocate on demand).
+ * Supports direct blocks (iblock 0..11) and single indirect
+ * (iblock 12..523, covering up to ~2 MiB per file).
+ */
+static int beamfs_iomap_begin(struct inode *inode, loff_t pos, loff_t length,
+ unsigned int flags, struct iomap *iomap,
+ struct iomap *srcmap)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ u64 iblock = pos >> BEAMFS_BLOCK_SHIFT;
+ u64 new_block;
+ u64 phys;
+
+ iomap->offset = iblock << BEAMFS_BLOCK_SHIFT;
+ iomap->length = BEAMFS_BLOCK_SIZE;
+ iomap->bdev = sb->s_bdev;
+ iomap->flags = 0;
+
+ if (iblock < BEAMFS_DIRECT_BLOCKS) {
+ /* --- Direct block --- */
+ phys = le64_to_cpu(fi->i_direct[iblock]);
+ if (phys) {
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = phys << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+ if (!(flags & IOMAP_WRITE)) {
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ pr_err("beamfs: iomap: no free blocks\n");
+ return -ENOSPC;
+ }
+ fi->i_direct[iblock] = cpu_to_le64(new_block);
+ mark_inode_dirty(inode);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = new_block << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+
+ if (iblock < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS) {
+ /* --- Single indirect block --- */
+ u64 indirect_slot = iblock - BEAMFS_DIRECT_BLOCKS;
+ u64 indirect_blk = le64_to_cpu(fi->i_indirect);
+ struct buffer_head *ibh;
+ __le64 *ptrs;
+
+ if (!indirect_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ indirect_blk = beamfs_alloc_block(sb, inode);
+ if (!indirect_blk) {
+ pr_err("beamfs: iomap: no free blocks for indirect\n");
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, indirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, indirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ unlock_buffer(ibh);
+ mark_buffer_dirty(ibh);
+ brelse(ibh);
+ fi->i_indirect = cpu_to_le64(indirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ ibh = sb_bread(sb, indirect_blk);
+ if (!ibh)
+ return -EIO;
+ ptrs = (__le64 *)ibh->b_data;
+ phys = le64_to_cpu(ptrs[indirect_slot]);
+
+ if (phys) {
+ brelse(ibh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = phys << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(ibh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(ibh);
+ pr_err("beamfs: iomap: no free blocks\n");
+ return -ENOSPC;
+ }
+ ptrs[indirect_slot] = cpu_to_le64(new_block);
+ mark_buffer_dirty(ibh);
+ brelse(ibh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = new_block << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+
+ if (iblock < BEAMFS_MAX_IBLOCK_DINDIRECT) {
+ /* --- Double indirect block (scheme=5 iomap path) --- */
+ u64 didx = iblock - BEAMFS_MAX_IBLOCK_INDIRECT;
+ u64 l1_slot = didx / BEAMFS_INDIRECT_PTRS;
+ u64 l2_slot = didx % BEAMFS_INDIRECT_PTRS;
+ u64 dindirect_blk, l1_blk;
+ struct buffer_head *ibh, *l1bh;
+ __le64 *ptrs;
+
+ dindirect_blk = le64_to_cpu(fi->i_dindirect);
+ if (!dindirect_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ dindirect_blk = beamfs_alloc_block(sb, inode);
+ if (!dindirect_blk) {
+ pr_err("beamfs: iomap: no free blocks for dindirect\n");
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, dindirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, dindirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ unlock_buffer(ibh);
+ mark_buffer_dirty(ibh);
+ brelse(ibh);
+ fi->i_dindirect = cpu_to_le64(dindirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ ibh = sb_bread(sb, dindirect_blk);
+ if (!ibh)
+ return -EIO;
+ ptrs = (__le64 *)ibh->b_data;
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ if (!l1_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(ibh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ l1_blk = beamfs_alloc_block(sb, inode);
+ if (!l1_blk) {
+ brelse(ibh);
+ pr_err("beamfs: iomap: no free blocks for dindirect L1\n");
+ return -ENOSPC;
+ }
+ l1bh = sb_getblk(sb, l1_blk);
+ if (!l1bh) {
+ beamfs_free_block(sb, l1_blk, inode);
+ brelse(ibh);
+ return -EIO;
+ }
+ lock_buffer(l1bh);
+ memset(l1bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l1bh);
+ unlock_buffer(l1bh);
+ mark_buffer_dirty(l1bh);
+ brelse(l1bh);
+ ptrs[l1_slot] = cpu_to_le64(l1_blk);
+ mark_buffer_dirty(ibh);
+ }
+ brelse(ibh);
+
+ l1bh = sb_bread(sb, l1_blk);
+ if (!l1bh)
+ return -EIO;
+ ptrs = (__le64 *)l1bh->b_data;
+ phys = le64_to_cpu(ptrs[l2_slot]);
+
+ if (phys) {
+ brelse(l1bh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = phys << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(l1bh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(l1bh);
+ pr_err("beamfs: iomap: no free blocks (dindirect data)\n");
+ return -ENOSPC;
+ }
+ ptrs[l2_slot] = cpu_to_le64(new_block);
+ mark_buffer_dirty(l1bh);
+ brelse(l1bh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = new_block << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+
+ if (iblock < BEAMFS_MAX_IBLOCK_TINDIRECT) {
+ /* --- Triple indirect block (scheme=5 iomap path) --- */
+ u64 tidx = iblock - BEAMFS_MAX_IBLOCK_DINDIRECT;
+ u64 l1_slot = tidx / (BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS);
+ u64 l2_slot = (tidx / BEAMFS_INDIRECT_PTRS) % BEAMFS_INDIRECT_PTRS;
+ u64 l3_slot = tidx % BEAMFS_INDIRECT_PTRS;
+ u64 tindirect_blk, l1_blk, l2_blk;
+ struct buffer_head *ibh, *l1bh, *l2bh;
+ __le64 *ptrs;
+
+ tindirect_blk = le64_to_cpu(fi->i_tindirect);
+ if (!tindirect_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ tindirect_blk = beamfs_alloc_block(sb, inode);
+ if (!tindirect_blk) {
+ pr_err("beamfs: iomap: no free blocks for tindirect\n");
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, tindirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, tindirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ unlock_buffer(ibh);
+ mark_buffer_dirty(ibh);
+ brelse(ibh);
+ fi->i_tindirect = cpu_to_le64(tindirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ ibh = sb_bread(sb, tindirect_blk);
+ if (!ibh)
+ return -EIO;
+ ptrs = (__le64 *)ibh->b_data;
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ if (!l1_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(ibh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ l1_blk = beamfs_alloc_block(sb, inode);
+ if (!l1_blk) {
+ brelse(ibh);
+ pr_err("beamfs: iomap: no free blocks for tindirect L1\n");
+ return -ENOSPC;
+ }
+ l1bh = sb_getblk(sb, l1_blk);
+ if (!l1bh) {
+ beamfs_free_block(sb, l1_blk, inode);
+ brelse(ibh);
+ return -EIO;
+ }
+ lock_buffer(l1bh);
+ memset(l1bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l1bh);
+ unlock_buffer(l1bh);
+ mark_buffer_dirty(l1bh);
+ brelse(l1bh);
+ ptrs[l1_slot] = cpu_to_le64(l1_blk);
+ mark_buffer_dirty(ibh);
+ }
+ brelse(ibh);
+
+ l1bh = sb_bread(sb, l1_blk);
+ if (!l1bh)
+ return -EIO;
+ ptrs = (__le64 *)l1bh->b_data;
+ l2_blk = le64_to_cpu(ptrs[l2_slot]);
+ if (!l2_blk) {
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(l1bh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ l2_blk = beamfs_alloc_block(sb, inode);
+ if (!l2_blk) {
+ brelse(l1bh);
+ pr_err("beamfs: iomap: no free blocks for tindirect L2\n");
+ return -ENOSPC;
+ }
+ l2bh = sb_getblk(sb, l2_blk);
+ if (!l2bh) {
+ beamfs_free_block(sb, l2_blk, inode);
+ brelse(l1bh);
+ return -EIO;
+ }
+ lock_buffer(l2bh);
+ memset(l2bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l2bh);
+ unlock_buffer(l2bh);
+ mark_buffer_dirty(l2bh);
+ brelse(l2bh);
+ ptrs[l2_slot] = cpu_to_le64(l2_blk);
+ mark_buffer_dirty(l1bh);
+ }
+ brelse(l1bh);
+
+ l2bh = sb_bread(sb, l2_blk);
+ if (!l2bh)
+ return -EIO;
+ ptrs = (__le64 *)l2bh->b_data;
+ phys = le64_to_cpu(ptrs[l3_slot]);
+
+ if (phys) {
+ brelse(l2bh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = phys << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+ if (!(flags & IOMAP_WRITE)) {
+ brelse(l2bh);
+ iomap->type = IOMAP_HOLE;
+ iomap->addr = IOMAP_NULL_ADDR;
+ return 0;
+ }
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(l2bh);
+ pr_err("beamfs: iomap: no free blocks (tindirect data)\n");
+ return -ENOSPC;
+ }
+ ptrs[l3_slot] = cpu_to_le64(new_block);
+ mark_buffer_dirty(l2bh);
+ brelse(l2bh);
+ iomap->type = IOMAP_MAPPED;
+ iomap->addr = new_block << BEAMFS_BLOCK_SHIFT;
+ return 0;
+ }
+
+ /* Beyond triple indirect: not supported */
+ pr_err_ratelimited("beamfs: iomap: offset beyond tindirect blocks\n");
+ return -EOPNOTSUPP;
+}
+
+static int beamfs_iomap_end(struct inode *inode, loff_t pos, loff_t length,
+ ssize_t written, unsigned int flags,
+ struct iomap *iomap)
+{
+ return 0;
+}
+
+static const struct iomap_ops beamfs_iomap_ops = {
+ .iomap_begin = beamfs_iomap_begin,
+ .iomap_end = beamfs_iomap_end,
+};
+
+/*
+ * S2.2: fiemap support for file-precise bench targeting (TODO.md l.1979).
+ * Thin wrapper over iomap_fiemap() reusing beamfs_iomap_ops. Pattern
+ * follows fs/ext2/inode.c::ext2_fiemap() (kernel 7.0.x). Required by
+ * userspace filefrag(8) to expose the file -> physical block mapping
+ * for beamfs-bench file-level RS-FEC validation.
+ *
+ * Shared by both schemes (UNIVERSAL_INLINE and INODE_UNIVERSAL) since
+ * both use the same direct[12] + indirect[512] block layout, handled
+ * uniformly by beamfs_iomap_begin().
+ */
+int beamfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
+ u64 start, u64 len)
+{
+ int ret;
+ loff_t i_size;
+
+ inode_lock(inode);
+ i_size = i_size_read(inode);
+ /*
+ * iomap_fiemap() returns -EINVAL for len == 0. Trim the request to
+ * the file size but never below 1 to keep the call valid for empty
+ * files (where it will simply return zero extents).
+ */
+ if (i_size == 0)
+ i_size = 1;
+ len = min_t(u64, len, i_size);
+ ret = iomap_fiemap(inode, fieinfo, start, len, &beamfs_iomap_ops);
+ inode_unlock(inode);
+ return ret;
+}
+
+/*
+ * Write path - beamfs_iomap_write_ops
+ * get_folio/put_folio use generic helpers (no journaling required).
+ */
+static struct folio *beamfs_iomap_get_folio(struct iomap_iter *iter,
+ loff_t pos, unsigned int len)
+{
+ return iomap_get_folio(iter, pos, len);
+}
+
+static void beamfs_iomap_put_folio(struct inode *inode, loff_t pos,
+ unsigned int copied, struct folio *folio)
+{
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
+static const struct iomap_write_ops beamfs_iomap_write_ops = {
+ .get_folio = beamfs_iomap_get_folio,
+ .put_folio = beamfs_iomap_put_folio,
+};
+
+static ssize_t beamfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
+{
+ return iomap_file_buffered_write(iocb, from, &beamfs_iomap_ops,
+ &beamfs_iomap_write_ops, NULL);
+}
+
+/*
+ * Writeback path - beamfs_writeback_ops
+ */
+static ssize_t beamfs_writeback_range(struct iomap_writepage_ctx *wpc,
+ struct folio *folio, u64 offset,
+ unsigned int len, u64 end_pos)
+{
+ if ((loff_t)offset < wpc->iomap.offset ||
+ (loff_t)offset >= wpc->iomap.offset + (loff_t)wpc->iomap.length) {
+ int ret;
+
+ memset(&wpc->iomap, 0, sizeof(wpc->iomap));
+ ret = beamfs_iomap_begin(wpc->inode,
+ offset, INT_MAX, 0,
+ &wpc->iomap, NULL);
+ if (ret)
+ return ret;
+ }
+ return iomap_add_to_ioend(wpc, folio, offset, end_pos, len);
+}
+
+static const struct iomap_writeback_ops beamfs_writeback_ops = {
+ .writeback_range = beamfs_writeback_range,
+ .writeback_submit = iomap_ioend_writeback_submit,
+};
+
+static int beamfs_writepages(struct address_space *mapping,
+ struct writeback_control *wbc)
+{
+ struct iomap_writepage_ctx wpc = {
+ .inode = mapping->host,
+ .wbc = wbc,
+ .ops = &beamfs_writeback_ops,
+ };
+
+ return iomap_writepages(&wpc);
+}
+
+/*
+ * Read path - uses iomap_bio_read_ops (kernel-provided)
+ */
+static int beamfs_read_folio(struct file *file, struct folio *folio)
+{
+ struct iomap_read_folio_ctx ctx = {
+ .ops = &iomap_bio_read_ops,
+ .cur_folio = folio,
+ };
+
+ iomap_read_folio(&beamfs_iomap_ops, &ctx, NULL);
+ return 0;
+}
+
+static void beamfs_readahead(struct readahead_control *rac)
+{
+ struct iomap_read_folio_ctx ctx = {
+ .ops = &iomap_bio_read_ops,
+ .rac = rac,
+ };
+
+ iomap_readahead(&beamfs_iomap_ops, &ctx, NULL);
+}
+
+static sector_t beamfs_bmap(struct address_space *mapping, sector_t block)
+{
+ return iomap_bmap(mapping, block, &beamfs_iomap_ops);
+}
+
+const struct address_space_operations beamfs_aops = {
+ .read_folio = beamfs_read_folio,
+ .readahead = beamfs_readahead,
+ .writepages = beamfs_writepages,
+ .bmap = beamfs_bmap,
+ .dirty_folio = iomap_dirty_folio,
+ .invalidate_folio = iomap_invalidate_folio,
+ .release_folio = iomap_release_folio,
+ .migrate_folio = filemap_migrate_folio,
+};
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 11/18] beamfs: inline data path 1/4, block map
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (9 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 10/18] beamfs: add iomap file operations Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 12/18] beamfs: inline data path 2/4, allocation Aurelien DESBRIERES
` (6 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
file_inline.c is the data path of the UNIVERSAL_INLINE protection
scheme (scheme 2), the one mkfs.beamfs formats. Each 4096-byte data
block holds sixteen RS(255,239) codewords, 3824 coded bytes and 256
bytes of parity, followed by a 16-byte tail; every read decodes the
block.
The file is 4875 lines, too large for one message to the list, so it
is split across four patches along function boundaries. It builds only
as a whole, once the Kconfig and Makefile patch is applied.
This part: forward declarations, the check of intermediate (indirect)
blocks, the lookup of a logical block through the indirect tree, and
the helpers that lay out and seal a data block before it is written.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/file_inline.c | 825 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 825 insertions(+)
create mode 100644 fs/beamfs/file_inline.c
diff --git a/fs/beamfs/file_inline.c b/fs/beamfs/file_inline.c
new file mode 100644
index 000000000000..6c071d623fcb
--- /dev/null
+++ b/fs/beamfs/file_inline.c
@@ -0,0 +1,825 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs - File operations for BEAMFS_DATA_PROTECTION_UNIVERSAL_INLINE
+ *
+ * Per-block Reed-Solomon FEC on user data: each 4096-byte disk block
+ * carries 16 shortened RS(255,239) codewords, in one of two layouts.
+ *
+ * With BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE (the capsule), 3808 data
+ * bytes, an 8-byte checksum and an 8-byte self-identity make 3824 coded
+ * bytes, interleaved across the 16 codewords. 256 parity bytes follow,
+ * 16 per codeword, then 16 bytes outside the codewords: an 8-byte
+ * generation and 8 reserved bytes.
+ *
+ * Without it (alternating), each codeword is 239 data bytes followed by
+ * its 16 parity bytes, 16 x 255 = 4080 bytes, and the last 16 bytes of
+ * the block, outside every codeword, hold the checksum and the
+ * self-identity.
+ *
+ * This file implements the data path for scheme=2 (UNIVERSAL_INLINE).
+ * The legacy iomap-based path in file.c is preserved for scheme=5
+ * (INODE_UNIVERSAL); the dispatch happens in inode.c / namei.c when
+ * setting i_fop and a_ops based on sbi->s_scheme.
+ *
+ * Each codeword corrects up to 8 corrupted bytes, so a block survives up
+ * to 128 when they fall 8 to a codeword. Interleaving puts consecutive
+ * coded bytes in different codewords, so a burst within the coded bytes
+ * is shared among the 16 codewords instead of landing in one.
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ */
+#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+#include <linux/buffer_head.h>
+#include <linux/bio.h>
+#include <linux/mempool.h>
+#include <linux/slab.h>
+#include <linux/uio.h>
+#include <linux/writeback.h>
+#include <linux/crc32.h>
+#include <linux/unaligned.h>
+#include "beamfs.h"
+#include "beamfs_trace.h"
+#include <linux/iomap.h>
+#include <linux/refcount.h>
+
+/* ------------------------------------------------------------------------- */
+/* Forward declarations */
+/* ------------------------------------------------------------------------- */
+
+static int beamfs_inline_read_folio(struct file *file,
+ struct folio *folio);
+static int beamfs_inline_writepages(struct address_space *mapping,
+ struct writeback_control *wbc);
+static ssize_t beamfs_inline_file_write_iter(struct kiocb *iocb,
+ struct iov_iter *from);
+static int beamfs_inline_setattr(const struct mnt_idmap *idmap,
+ struct dentry *dentry,
+ struct iattr *attr);
+static void beamfs_inline_free_blocks_from(struct inode *inode,
+ u64 b_first_freed);
+static void beamfs_inline_stamp_tail_pad(struct beamfs_sb_info *sbi,
+ u8 *block, const u8 *payload,
+ u64 ino, u64 iblock);
+static int beamfs_inline_zero_tail_block(struct inode *inode,
+ u64 b, u32 zero_offset);
+static int beamfs_check_intermediate_block(struct super_block *sb,
+ u64 block, ino_t ino,
+ u64 iblock_logical,
+ const char *label);
+
+/* ------------------------------------------------------------------------- */
+/* Block-mapping helpers */
+/* */
+/* These mirror the layout used by beamfs_iomap_begin() in file.c (legacy */
+/* iomap path, scheme=5 INODE_UNIVERSAL): direct blocks 0..11 in */
+/* fi->i_direct[], single indirect via fi->i_indirect (512 entries) for */
+/* iblocks 12..523. Beyond those, fi->i_dindirect and fi->i_tindirect map */
+/* double and triple indirect trees, up to BEAMFS_MAX_IBLOCK_TINDIRECT. */
+/* */
+/* Pure lookup, no allocation. Used by the read paths, by iomap_begin for */
+/* anything but a write, by the scrubber, and by writeback and */
+/* zero_tail_block to log a pointer the allocating variant changed. The */
+/* allocating variant lives below. */
+/* */
+/* Returns: */
+/* 0 + *phys_out = block number (mapped) */
+/* 0 + *phys_out = 0 (HOLE: not allocated yet) */
+/* <0 on error (-EIO indirect read fail, */
+/* -EUCLEAN corrupted or unallocated */
+/* pointer, or indirect parity failure, */
+/* -EOPNOTSUPP beyond triple indirect, */
+/* -EINVAL on null phys_out) */
+/* ------------------------------------------------------------------------- */
+/*
+ * beamfs_check_intermediate_block -- bounds and allocation check for a
+ * pointer-to-pointers block (indirect, dindirect, tindirect and their L1/L2
+ * levels) before it is read as 512 raw __le64 entries.
+ *
+ * beamfs_inline_lookup_phys checks every terminal pointer -- the one that
+ * addresses a leaf data block -- against both the
+ * [s_data_start, s_data_start + s_nblocks) range and the allocation bitmap.
+ * The blocks that hold indirection pointers need the same two checks: a
+ * bit flip on fi->i_indirect (or i_dindirect, i_tindirect, or an L1/L2
+ * entry) reaching a value inside the valid range but pointing at an
+ * unrelated or unallocated block would otherwise be read via sb_bread and
+ * its raw bytes reinterpreted as 512 pointers, with no signal. Fail closed
+ * before the read rather than trust whatever the corrupted pointer happens
+ * to reach, as DATA_SELFID and beamfs_block_is_allocated do for file data
+ * and terminal pointers.
+ */
+static int beamfs_check_intermediate_block(struct super_block *sb,
+ u64 block, ino_t ino,
+ u64 iblock_logical,
+ const char *label)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (block < sbi->s_data_start ||
+ block >= sbi->s_data_start + sbi->s_nblocks) {
+ pr_err_ratelimited("beamfs/inline: corrupted %s block ino=%llu iblock=%llu phys=%llu (out of [%lu, %lu))\n",
+ label, (unsigned long long)ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)block,
+ sbi->s_data_start,
+ sbi->s_data_start + sbi->s_nblocks);
+ beamfs_log_rs_event_flagged(sb, block, NULL, 0,
+ BEAMFS_SUBBLOCK_DATA, 0);
+ return -EUCLEAN;
+ }
+ if (!beamfs_block_is_allocated(sb, block)) {
+ /*
+ * Journal it as uncorrectable, same as the DATA_CSUM and
+ * DATA_SELFID fail-closed paths. An indirection pointer
+ * corrupted beyond the RS correction radius is exactly what
+ * the forensic journal exists to record; without an entry
+ * the run reads as an unexplained failure, since dmesg
+ * carries the pr_err but DMESG_UNCORRECTABLE stays 0. That
+ * is how the 2026-08-27 multifs run scored RS_FAILED rather
+ * than RS_FAIL_CLOSED. positions == NULL with
+ * n_positions == 0 is the uncorrectable call shape; the flag
+ * is reserved and set by beamfs_log_rs_event_flagged itself;
+ * passing it in extra_flags would trip the reserved-mask
+ * guard.
+ */
+ beamfs_log_rs_event_flagged(sb, block, NULL, 0,
+ BEAMFS_SUBBLOCK_DATA, 0);
+ pr_err_ratelimited("beamfs/inline: unallocated %s block ino=%llu iblock=%llu phys=%llu\n",
+ label, (unsigned long long)ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)block);
+ return -EUCLEAN;
+ }
+ return 0;
+}
+
+/*
+ * Count a block of pointers and what it points to, @depth levels down:
+ * 1 for a block of pointers to data, 2 and 3 for the double and triple
+ * trees. The pointers are copied out under the buffer lock, as the
+ * allocator installs them under it, and the children walked after.
+ */
+static int beamfs_count_tree(struct inode *inode, u64 blk,
+ unsigned int depth, u64 *count)
+{
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *bh;
+ __le64 *ptrs;
+ u64 *kids = NULL;
+ unsigned int i, n = 0;
+ int ret;
+
+ ret = beamfs_check_intermediate_block(sb, blk, inode->i_ino, 0,
+ "indirect");
+ if (ret)
+ return ret;
+ bh = sb_bread(sb, blk);
+ if (!bh)
+ return -EIO;
+ if (beamfs_ind_parity_verify(sb, bh)) {
+ brelse(bh);
+ return -EUCLEAN;
+ }
+ if (depth > 1) {
+ kids = kmalloc_array(BEAMFS_INDIRECT_PTRS, sizeof(*kids),
+ GFP_NOFS);
+ if (!kids) {
+ brelse(bh);
+ return -ENOMEM;
+ }
+ }
+
+ (*count)++;
+ lock_buffer(bh);
+ ptrs = (__le64 *)bh->b_data;
+ for (i = 0; i < BEAMFS_INDIRECT_PTRS; i++) {
+ u64 p = le64_to_cpu(ptrs[i]);
+
+ if (!p)
+ continue;
+ if (kids)
+ kids[n++] = p;
+ else
+ (*count)++;
+ }
+ unlock_buffer(bh);
+ brelse(bh);
+
+ for (i = 0; i < n && !ret; i++)
+ ret = beamfs_count_tree(inode, kids[i], depth - 1, count);
+ kfree(kids);
+ return ret;
+}
+
+/*
+ * beamfs_count_blocks -- the blocks @inode holds on the medium, data and
+ * indirect alike, read from its tree. The inode does not store the
+ * count; beamfs_getattr takes it from here once. A short symlink target
+ * lives in i_direct[] as bytes, not pointers, and a long one is a block.
+ *
+ * Returns 0, or the error of a block of pointers that failed its check;
+ * *count then holds what was counted before it.
+ */
+int beamfs_count_blocks(struct inode *inode, u64 *count)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ u64 roots[3];
+ unsigned int i;
+ int ret = 0;
+
+ *count = 0;
+ if (S_ISLNK(inode->i_mode)) {
+ if (i_size_read(inode) >= (loff_t)sizeof(fi->i_direct) &&
+ fi->i_direct[0])
+ *count = 1;
+ return 0;
+ }
+ if (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))
+ return 0;
+
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS; i++) {
+ if (fi->i_direct[i])
+ (*count)++;
+ }
+ roots[0] = le64_to_cpu(fi->i_indirect);
+ roots[1] = le64_to_cpu(fi->i_dindirect);
+ roots[2] = le64_to_cpu(fi->i_tindirect);
+ for (i = 0; i < ARRAY_SIZE(roots) && !ret; i++) {
+ if (roots[i])
+ ret = beamfs_count_tree(inode, roots[i], i + 1, count);
+ }
+ return ret;
+}
+
+int beamfs_inline_lookup_phys(struct inode *inode, u64 iblock_logical,
+ u64 *phys_out)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *ibh = NULL;
+ __le64 *ptrs;
+ u64 indirect_blk;
+ u64 indirect_slot;
+ u64 phys;
+ int ret;
+
+ if (!phys_out)
+ return -EINVAL;
+
+ *phys_out = 0;
+
+ if (iblock_logical < BEAMFS_DIRECT_BLOCKS) {
+ u64 dphys = le64_to_cpu(fi->i_direct[iblock_logical]);
+
+ if (dphys != 0) {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (dphys < sbi->s_data_start ||
+ dphys >= sbi->s_data_start + sbi->s_nblocks) {
+ pr_err_ratelimited("beamfs/inline: corrupted direct pointer ino=%llu iblock=%llu phys=%llu (out of [%lu, %lu))\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)dphys,
+ sbi->s_data_start,
+ sbi->s_data_start + sbi->s_nblocks);
+ return -EUCLEAN;
+ }
+ /*
+ * A direct pointer into a freed block fails closed
+ * here, as the same case on an indirect pointer does
+ * in beamfs_check_intermediate_block.
+ */
+ if (!beamfs_block_is_allocated(sb, dphys)) {
+ pr_err_ratelimited("beamfs/inline: unallocated direct pointer ino=%llu iblock=%llu phys=%llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)dphys);
+ return -EUCLEAN;
+ }
+ }
+ *phys_out = dphys;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS) {
+ indirect_slot = iblock_logical - BEAMFS_DIRECT_BLOCKS;
+ indirect_blk = le64_to_cpu(fi->i_indirect);
+
+ if (!indirect_blk)
+ return 0; /* HOLE: indirect block not yet allocated */
+
+ ret = beamfs_check_intermediate_block(sb, indirect_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "indirect");
+ if (ret)
+ return ret;
+
+ {
+ int fresh = 0;
+
+ if (!ibh) {
+ /*
+ * find_get_block, not sb_getblk: the
+ * question is whether the cache already
+ * holds this block, and sb_getblk would
+ * answer it by creating one. A probe that
+ * changes what it measures is worse than
+ * no probe -- generic/464 passed with one
+ * and the result could not be read.
+ */
+ struct buffer_head *probe =
+ sb_find_get_block(sb, indirect_blk);
+
+ fresh = !probe;
+ if (probe)
+ brelse(probe);
+ ibh = sb_bread(sb, indirect_blk);
+ }
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read indirect block %llu\n",
+ (unsigned long long)indirect_blk);
+ return -EIO;
+ }
+ trace_beamfs_ind_read(inode->i_sb->s_dev, inode->i_ino, indirect_blk,
+ (unsigned int)indirect_slot,
+ le64_to_cpu(((__le64 *)ibh->b_data)[indirect_slot]),
+ buffer_uptodate(ibh), fresh);
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it.
+ *
+ * lookup_phys took no lock at all while
+ * lookup_or_alloc_phys installs a pointer holding
+ * both i_alloc_mutex and lock_buffer, so a reader
+ * could see a slot mid-install: treecheck reported
+ * one that held 18806 and read as 0, in the same
+ * function for the same inode, with the write order
+ * forced -- so the block was on the medium and the
+ * read was simply early.
+ */
+ lock_buffer(ibh);
+ phys = le64_to_cpu(ptrs[indirect_slot]);
+ unlock_buffer(ibh);
+ brelse(ibh);
+
+ if (phys != 0) {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (phys < sbi->s_data_start ||
+ phys >= sbi->s_data_start + sbi->s_nblocks) {
+ pr_err_ratelimited("beamfs/inline: corrupted indirect pointer ino=%llu iblock=%llu slot=%llu phys=%llu (out of [%lu, %lu))\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)indirect_slot,
+ (unsigned long long)phys,
+ sbi->s_data_start,
+ sbi->s_data_start + sbi->s_nblocks);
+ return -EUCLEAN;
+ }
+ if (!beamfs_block_is_allocated(sb, phys)) {
+ pr_err_ratelimited("beamfs/inline: unallocated indirect pointer ino=%llu iblock=%llu phys=%llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)phys);
+ return -EUCLEAN;
+ }
+ }
+ *phys_out = phys;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_MAX_IBLOCK_DINDIRECT) {
+ /*
+ * Double-indirect lookup. Map iblock to (level-1 slot,
+ * level-2 slot) where level-1 is an indirect block of
+ * pointers to indirect blocks, and level-2 is the leaf
+ * indirect block whose entries point to user data blocks.
+ * Coverage: 12 + 512 + 512*512 = 262668 iblocks ~= 1 GiB.
+ */
+ u64 didx, l1_slot, l2_slot, dindirect_blk, l1_blk;
+ struct buffer_head *l1bh = NULL;
+
+ didx = iblock_logical - BEAMFS_MAX_IBLOCK_INDIRECT;
+ l1_slot = didx / BEAMFS_INDIRECT_PTRS;
+ l2_slot = didx % BEAMFS_INDIRECT_PTRS;
+
+ dindirect_blk = le64_to_cpu(fi->i_dindirect);
+ if (!dindirect_blk)
+ return 0; /* HOLE: dindirect block not yet allocated */
+
+ ret = beamfs_check_intermediate_block(sb, dindirect_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "dindirect");
+ if (ret)
+ return ret;
+
+ ibh = sb_bread(sb, dindirect_blk);
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read dindirect block %llu\n",
+ (unsigned long long)dindirect_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it. See the single-indirect case above.
+ */
+ lock_buffer(ibh);
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ unlock_buffer(ibh);
+ brelse(ibh);
+
+ if (!l1_blk)
+ return 0; /* HOLE: level-1 indirect not allocated */
+
+ ret = beamfs_check_intermediate_block(sb, l1_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "dindirect L1");
+ if (ret)
+ return ret;
+
+ if (!l1bh)
+ l1bh = sb_bread(sb, l1_blk);
+ if (!l1bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read dindirect L1 block %llu\n",
+ (unsigned long long)l1_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l1bh)) {
+ brelse(l1bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l1bh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it.
+ *
+ * lookup_phys took no lock at all while
+ * lookup_or_alloc_phys installs a pointer holding
+ * both i_alloc_mutex and lock_buffer, so a reader
+ * could see a slot mid-install: treecheck reported
+ * one that held 18806 and read as 0, in the same
+ * function for the same inode, with the write order
+ * forced -- so the block was on the medium and the
+ * read was simply early.
+ */
+ lock_buffer(l1bh);
+ phys = le64_to_cpu(ptrs[l2_slot]);
+ unlock_buffer(l1bh);
+ brelse(l1bh);
+
+ if (phys != 0) {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (phys < sbi->s_data_start ||
+ phys >= sbi->s_data_start + sbi->s_nblocks) {
+ pr_err_ratelimited("beamfs/inline: corrupted dindirect pointer ino=%llu iblock=%llu phys=%llu (out of [%lu, %lu))\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)phys,
+ sbi->s_data_start,
+ sbi->s_data_start + sbi->s_nblocks);
+ return -EUCLEAN;
+ }
+ if (!beamfs_block_is_allocated(sb, phys)) {
+ pr_err_ratelimited("beamfs/inline: unallocated dindirect pointer ino=%llu iblock=%llu phys=%llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)phys);
+ return -EUCLEAN;
+ }
+ }
+ *phys_out = phys;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_MAX_IBLOCK_TINDIRECT) {
+ /*
+ * Triple-indirect lookup. Map iblock to (level-1, level-2,
+ * level-3) where level-1 is an indirect block of pointers to
+ * dindirect blocks, level-2 is an indirect block of pointers
+ * to indirect blocks, and level-3 is the leaf indirect block
+ * whose entries point to user data blocks.
+ * Coverage: 12 + 512 + 512^2 + 512^3 = 134480396 iblocks
+ * ~= 478 GiB at 3824 user bytes/block.
+ */
+ u64 tidx, l1_slot, l2_slot, l3_slot;
+ u64 tindirect_blk, l1_blk, l2_blk;
+ struct buffer_head *l1bh = NULL, *l2bh = NULL;
+
+ tidx = iblock_logical - BEAMFS_MAX_IBLOCK_DINDIRECT;
+ l1_slot = tidx / (BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS);
+ l2_slot = (tidx / BEAMFS_INDIRECT_PTRS) % BEAMFS_INDIRECT_PTRS;
+ l3_slot = tidx % BEAMFS_INDIRECT_PTRS;
+
+ tindirect_blk = le64_to_cpu(fi->i_tindirect);
+ if (!tindirect_blk)
+ return 0; /* HOLE: tindirect block not yet allocated */
+
+ ret = beamfs_check_intermediate_block(sb, tindirect_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "tindirect");
+ if (ret)
+ return ret;
+
+ ibh = sb_bread(sb, tindirect_blk);
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect block %llu\n",
+ (unsigned long long)tindirect_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it. See the single-indirect case above.
+ */
+ lock_buffer(ibh);
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ unlock_buffer(ibh);
+ brelse(ibh);
+
+ if (!l1_blk)
+ return 0; /* HOLE: level-1 not allocated */
+
+ ret = beamfs_check_intermediate_block(sb, l1_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "tindirect L1");
+ if (ret)
+ return ret;
+
+ if (!l1bh)
+ l1bh = sb_bread(sb, l1_blk);
+ if (!l1bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect L1 block %llu\n",
+ (unsigned long long)l1_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l1bh)) {
+ brelse(l1bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l1bh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it. See the single-indirect case above.
+ */
+ lock_buffer(l1bh);
+ l2_blk = le64_to_cpu(ptrs[l2_slot]);
+ unlock_buffer(l1bh);
+ brelse(l1bh);
+
+ if (!l2_blk)
+ return 0; /* HOLE: level-2 not allocated */
+
+ ret = beamfs_check_intermediate_block(sb, l2_blk,
+ (unsigned long long)inode->i_ino,
+ iblock_logical,
+ "tindirect L2");
+ if (ret)
+ return ret;
+
+ if (!l2bh)
+ l2bh = sb_bread(sb, l2_blk);
+ if (!l2bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect L2 block %llu\n",
+ (unsigned long long)l2_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l2bh)) {
+ brelse(l2bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l2bh->b_data;
+ /*
+ * Read under the buffer lock, as the writer installs
+ * under it. See the single-indirect case above.
+ */
+ lock_buffer(l2bh);
+ phys = le64_to_cpu(ptrs[l3_slot]);
+ unlock_buffer(l2bh);
+ brelse(l2bh);
+
+ if (phys != 0) {
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (phys < sbi->s_data_start ||
+ phys >= sbi->s_data_start + sbi->s_nblocks) {
+ pr_err_ratelimited("beamfs/inline: corrupted tindirect pointer ino=%llu iblock=%llu phys=%llu (out of [%lu, %lu))\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)phys,
+ sbi->s_data_start,
+ sbi->s_data_start + sbi->s_nblocks);
+ return -EUCLEAN;
+ }
+ if (!beamfs_block_is_allocated(sb, phys)) {
+ pr_err_ratelimited("beamfs/inline: unallocated tindirect pointer ino=%llu iblock=%llu phys=%llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical,
+ (unsigned long long)phys);
+ return -EUCLEAN;
+ }
+ }
+ *phys_out = phys;
+ return 0;
+ }
+
+ pr_err_ratelimited("beamfs/inline: iblock %llu beyond tindirect capacity\n",
+ (unsigned long long)iblock_logical);
+ return -EOPNOTSUPP;
+}
+
+/* ------------------------------------------------------------------------- */
+/* Allocating block-mapping (write path) */
+/* */
+/* Variant of beamfs_inline_lookup_phys() that allocates on demand. Used by */
+/* iomap_begin for a write, by writeback_range and by zero_tail_block to */
+/* map a logical iblock to a physical block, allocating direct, indirect, */
+/* double and triple indirect and data blocks as needed. A freshly */
+/* allocated data block is zeroed, stamped and RS-encoded at once, so a */
+/* subsequent read finds a valid block of zeros rather than whatever the */
+/* medium held. */
+/* */
+/* Allocates: */
+/* - Direct: writes phys into fi->i_direct[iblock] + mark_inode_dirty. */
+/* - Indirect levels: allocates each missing level first (zero-init), */
+/* then the data block, and writes phys into the leaf ptrs[slot]. */
+/* */
+/* The freshly allocated data block is built via sb_getblk + memset, NOT */
+/* via sb_bread, because there is no on-disk content to read (the block */
+/* was unallocated). The buffer is marked uptodate + dirty, so a partial */
+/* write at writeback finds it in the buffer cache instead of reading the */
+/* device. */
+/* */
+/* Returns: */
+/* 0 + *phys_out = block number (mapped or freshly allocated) */
+/* <0 on error (-ENOSPC if alloc fails, -EIO on indirect read fail, on */
+/* a failed volume or an inode being evicted, -EUCLEAN on */
+/* indirect parity failure, -EOPNOTSUPP beyond triple */
+/* indirect, -EINVAL on null phys_out) */
+/* ------------------------------------------------------------------------- */
+/*
+ * Is this a block a file may point at?
+ *
+ * Everything below s_data_start is metadata: the superblock, the inode
+ * table, the bitmap, the indirect parity region. A file's data never
+ * lives there, and a pointer naming it is wrong whatever produced it.
+ *
+ * generic/075 found two blocks of the inode table holding fsx's fill
+ * bytes, seven inodes in them beyond correction and 40437 pointers
+ * naming blocks past the end of the device -- and the first thing to
+ * notice was fsck, three minutes and one unmount later, by which time
+ * the path that did it was gone.
+ *
+ * Refused here instead, with the block and the inode named, while the
+ * stack that produced it is still on the stack.
+ */
+static bool beamfs_phys_is_sane(struct super_block *sb, struct inode *inode,
+ u64 phys, const char *where)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ u64 last = (u64)sbi->s_data_start + sbi->s_nblocks;
+
+ if (phys == 0)
+ return true; /* a hole, not an address */
+
+ if (phys < sbi->s_data_start || phys >= last) {
+ /*
+ * The inode number in its own variable: a ternary between
+ * i_ino and a literal is promoted, and %lu given the
+ * result compiles clean where the promotion happens to
+ * land on unsigned long and breaks where it does not.
+ */
+ unsigned long ino = inode ? inode->i_ino : 0;
+
+ pr_err_ratelimited("beamfs: %s: inode %lu names block %llu, outside %llu..%llu\n",
+ where, ino,
+ (unsigned long long)phys,
+ (unsigned long long)sbi->s_data_start,
+ (unsigned long long)last - 1);
+ beamfs_fail(sb, where, -EUCLEAN);
+ return false;
+ }
+ return true;
+}
+
+#ifdef CONFIG_BEAMFS_ORDERED_META
+/*
+ * Put a metadata block on the medium before anything names it.
+ *
+ * The safe order for a tree is pointee before pointer, always, and
+ * nothing in beamfs establishes it: mark_buffer_dirty hands the buffer
+ * to writeback, which sorts by age and by position.
+ *
+ * This is the brute form -- one synchronous write per level, up to
+ * four for a block reached through triple indirection -- and it is
+ * meant to answer one question: is the missing order the reason nine
+ * xfstests fail? A journal is how a filesystem gets the same property
+ * without paying this.
+ */
+static void beamfs_order_before_pointer(struct buffer_head *bh)
+{
+ if (!bh)
+ return;
+ /*
+ * write_dirty_buffer submits and returns; the wait is what makes
+ * this an ordering and not a hint.
+ */
+ write_dirty_buffer(bh, REQ_SYNC);
+ wait_on_buffer(bh);
+}
+#else
+static inline void beamfs_order_before_pointer(struct buffer_head *bh)
+{
+ (void)bh;
+}
+#endif
+
+/*
+ * Did this call allocate, or did it find what was already there?
+ *
+ * iomap needs the answer. A write shorter than the mapping it was
+ * given leaves the rest of that mapping allocated, and only the blocks
+ * this call created may be given back -- a block that was already
+ * there belongs to the file whether or not this write reached it.
+ *
+ * generic/013 leaves 28 consecutive blocks allocated and never
+ * written, the length of one write, and nothing can tell them from
+ * blocks the file already owned.
+ *
+ * @allocated may be NULL for callers that do not care.
+ */
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 12/18] beamfs: inline data path 2/4, allocation
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (10 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 11/18] beamfs: inline data path 1/4, block map Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 13/18] beamfs: inline data path 3/4, read side Aurelien DESBRIERES
` (5 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
file_inline.c is the data path of the UNIVERSAL_INLINE protection
scheme (scheme 2), the one mkfs.beamfs formats. Each 4096-byte data
block holds sixteen RS(255,239) codewords, 3824 coded bytes and 256
bytes of parity, followed by a 16-byte tail; every read decodes the
block.
The file is 4875 lines, too large for one message to the list, so it
is split across four patches along function boundaries. It builds only
as a whole, once the Kconfig and Makefile patch is applied.
This part: beamfs_inline_lookup_or_alloc_phys_new(), which allocates
data and indirect blocks and installs them in the tree.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/file_inline.c | 1575 +++++++++++++++++++++++++++++++++++++++
1 file changed, 1575 insertions(+)
diff --git a/fs/beamfs/file_inline.c b/fs/beamfs/file_inline.c
index 6c071d623fcb..dca00ae1f645 100644
--- a/fs/beamfs/file_inline.c
+++ b/fs/beamfs/file_inline.c
@@ -823,3 +823,1578 @@ static inline void beamfs_order_before_pointer(struct buffer_head *bh)
*
* @allocated may be NULL for callers that do not care.
*/
+static int beamfs_inline_lookup_or_alloc_phys_new(struct inode *inode,
+ u64 iblock_logical,
+ u64 *phys_out,
+ bool *allocated);
+
+static int beamfs_inline_lookup_or_alloc_phys(struct inode *inode,
+ u64 iblock_logical,
+ u64 *phys_out)
+{
+ return beamfs_inline_lookup_or_alloc_phys_new(inode, iblock_logical,
+ phys_out, NULL);
+}
+
+/*
+ * Lay a block's payload down in whichever layout the volume uses.
+ *
+ * Alternating, the payload is sixteen runs of 239 bytes 255 apart, and
+ * the copy has to break it up. A capsule holds it contiguously, and
+ * breaking it up writes every run 16 bytes further along than it
+ * belongs -- measured as the last 222 bytes of every block coming back
+ * wrong, which is where the fifteenth run ends up.
+ *
+ * Paired with beamfs_seal_data_block, which encodes what this writes.
+ */
+void beamfs_lay_data_payload(struct super_block *sb, u8 *block,
+ const u8 *payload)
+{
+ unsigned int i;
+
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE) {
+ memcpy(block + BEAMFS_CAPSULE_DATA_OFF, payload,
+ BEAMFS_CAPSULE_DATA_BYTES);
+ return;
+ }
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++)
+ memcpy(block + (size_t)i * BEAMFS_SUBBLOCK_TOTAL,
+ payload + (size_t)i * BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_DATA);
+}
+
+/*
+ * Seal a data block: compute its RS parity in whichever layout the
+ * volume uses. Every site that encodes a data block goes through here,
+ * so a layout change touches one place.
+ */
+int beamfs_seal_data_block(struct super_block *sb, u8 *block)
+{
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+ return beamfs_rs_encode_woven(block + BEAMFS_CAPSULE_DATA_OFF,
+ block + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+
+ return beamfs_rs_encode_region(block, BEAMFS_SUBBLOCK_TOTAL,
+ block + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+}
+
+/*
+ * Dirty an indirect block and attach it to @inode, unless its last
+ * write failed.
+ *
+ * An install reads the parent block, allocates the child -- which can
+ * sleep -- and only then stores the pointer and dirties the parent. A
+ * write of the parent that fails in between leaves it not uptodate
+ * (end_buffer_write_sync clears the bit on an error): the medium holds
+ * an older block than memory does, and dirtying it anyway is what
+ * mark_buffer_dirty warns about. The volume is failed instead, as
+ * beamfs_write_bitmap_block does for a bitmap block.
+ *
+ * Tested under the buffer lock: a write in flight holds it until its
+ * completion has set or cleared uptodate.
+ */
+static void beamfs_attach_dirty(struct inode *inode, struct buffer_head *bh)
+{
+ lock_buffer(bh);
+ if (!buffer_uptodate(bh)) {
+ unlock_buffer(bh);
+ pr_err_ratelimited("beamfs: block %llu: its last write failed, not dirtied again\n",
+ (unsigned long long)bh->b_blocknr);
+ beamfs_fail(inode->i_sb, "indirect block write", -EIO);
+ return;
+ }
+ mmb_mark_buffer_dirty(bh, &BEAMFS_I(inode)->i_metadata_bhs);
+ unlock_buffer(bh);
+}
+
+static int beamfs_inline_lookup_or_alloc_phys_new(struct inode *inode,
+ u64 iblock_logical,
+ u64 *phys_out,
+ bool *allocated)
+{
+ /*
+ * False until proven otherwise: a caller reading this after an
+ * error path must not find a stale true.
+ */
+ if (allocated)
+ *allocated = false;
+
+ /*
+ * Nothing to allocate for an inode being destroyed.
+ *
+ * __mark_inode_dirty returns without queueing when I_FREEING is
+ * set -- fs-writeback.c, "if (inode_state_read(inode) &
+ * I_FREEING) goto out_unlock". A block allocated here has its
+ * pointer installed into a tree whose inode will never be
+ * written: the bitmap says used, nothing references it, and fsck
+ * calls it used-but-unreferenced.
+ *
+ * The check belongs here rather than in the callers, because all
+ * three of them -- iomap_begin, writeback_range, zero_tail_block
+ * -- can run against an evicting inode, and guarding one of the
+ * three leaves the other two leaking.
+ *
+ * ext2 cannot reach this: ext2_get_block runs with create=0 on
+ * the writeback path, and an evicting inode has been through
+ * truncate_inode_pages_final. beamfs allocates at writeback and
+ * has to check for itself.
+ */
+ if (inode_state_read_once(inode) &
+ (I_FREEING | I_WILL_FREE | I_CLEAR))
+ return -EIO;
+
+ /*
+ * Nothing free: say so once and return, rather than walking the
+ * whole indirection tree to discover it at every level.
+ *
+ * Without this a full volume produced one failed allocation per
+ * block per level -- 19531 suppressed log callbacks in three
+ * seconds under generic/224 -- and the writeback path spent its
+ * time in the allocator and the ring buffer instead of returning
+ * ENOSPC. The test then exceeded its timeout for want of an error
+ * the filesystem already knew.
+ *
+ * s_free_blocks is read without the lock. It is a hint here: a
+ * concurrent free racing this check costs one retry, and the
+ * allocation below is still the thing that decides.
+ */
+ if (BEAMFS_SB(inode->i_sb)->s_free_blocks == 0)
+ return -ENOSPC;
+
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *ibh = NULL;
+ struct buffer_head *dbh;
+ __le64 *ptrs;
+ u64 indirect_blk;
+ u64 indirect_slot;
+ u64 phys;
+ u64 new_block;
+
+ if (!phys_out)
+ return -EINVAL;
+
+ *phys_out = 0;
+
+ if (iblock_logical < BEAMFS_DIRECT_BLOCKS) {
+ /* --- Direct block --- */
+ phys = le64_to_cpu(fi->i_direct[iblock_logical]);
+ if (phys) {
+ *phys_out = phys;
+ return 0;
+ }
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ /* Zero-init the freshly allocated data block on disk. */
+ dbh = sb_getblk(sb, new_block);
+ if (!dbh) {
+ beamfs_free_block(sb, new_block, inode);
+ return -EIO;
+ }
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ /*
+ * Stamp the DATA_CSUM descriptor on the freshly allocated data
+ * block. An all-zero block is a valid RS codeword by linearity,
+ * so decode accepts it, but the descriptor would stay 0x00 and
+ * the read path would have nothing to verify against. Before
+ * the type byte was made fail-closed that meant silent
+ * acceptance; afterwards it means a legitimate newly allocated
+ * block is rejected. Observed 2026-08-18: iblocks 0..5 of a
+ * test file logged "bad descriptor type=0x00" on five runs out
+ * of six, because allocation writes the block here while the
+ * only other stamp sites are writeback and
+ * zero_tail_block, neither of which runs for a block that is
+ * allocated but not yet written through the folio path. The
+ * payload is the zeroed block itself. Indirect blocks carry no
+ * DATA_CSUM descriptor and are deliberately left untouched.
+ */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), (u8 *)dbh->b_data,
+ (const u8 *)dbh->b_data,
+ (unsigned long long)inode->i_ino, iblock_logical);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ /*
+ * A data block carries sixteen RS codewords, not indirect
+ * parity.
+ *
+ * ind_parity_update files a CRC for this block number in
+ * the separate parity region that covers indirect blocks.
+ * A newly allocated data block was being registered there,
+ * and its own sixteen codewords were left as the zeros
+ * memset put down -- correct for a zero block, wrong the
+ * moment anything is written into it and the parity is not
+ * recomputed.
+ *
+ * The sweep found exactly that: subblocks holding data,
+ * uncorrectable; subblocks still zero, fine. Encoding here
+ * makes a fresh block valid on its own terms from the
+ * start.
+ */
+ {
+ int _e = beamfs_seal_data_block(sb, (u8 *)dbh->b_data);
+
+ if (_e < 0)
+ pr_err_ratelimited("beamfs/inline: encode of new data block %llu failed: %d\n",
+ (unsigned long long)new_block,
+ _e);
+ }
+ mark_buffer_dirty(dbh);
+
+ /*
+ * The direct array is a slot store like any other.
+ *
+ * The three indirect levels emit this and the direct array
+ * did not, so a probe pairing allocations against stores
+ * counted every direct block as an allocation that went
+ * nowhere: 176180 of them in one generic/269, against 327
+ * blocks fsck actually finds leaked.
+ *
+ * parent 0 and level 0: there is no block above a direct
+ * pointer, and nothing else uses those values.
+ */
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, 0, iblock_logical,
+ le64_to_cpu(fi->i_direct[iblock_logical]),
+ new_block, 0);
+
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(dbh);
+ brelse(dbh);
+ fi->i_direct[iblock_logical] = cpu_to_le64(new_block);
+ mark_inode_dirty(inode);
+
+ if (allocated)
+ *allocated = true;
+ *phys_out = new_block;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_DIRECT_BLOCKS + BEAMFS_INDIRECT_PTRS) {
+ /* --- Single indirect block --- */
+ indirect_slot = iblock_logical - BEAMFS_DIRECT_BLOCKS;
+ indirect_blk = le64_to_cpu(fi->i_indirect);
+
+ if (!indirect_blk) {
+ /* Allocate indirect block first, zero-init. */
+ indirect_blk = beamfs_alloc_block(sb, inode);
+ if (!indirect_blk) {
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, indirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, indirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ beamfs_tc_zeroed(sb, inode->i_ino, ibh->b_blocknr, __func__);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ /*
+ * Parity taken under the lock, like the install
+ * sites do.
+ *
+ * The block is allocated and visible before its
+ * pointer is installed a few lines down, so
+ * another writer can reach it. Computing parity
+ * after unlock_buffer describes whatever the block
+ * held at that instant, which need not be what
+ * reaches the medium: generic/464 writes and
+ * fsyncs the same files from several tasks and
+ * produced 194 indirect blocks whose parity was
+ * written and did not match, seven orphaned blocks
+ * under each.
+ */
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(ibh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(ibh);
+ /*
+ * The inode's own pointer is a slot store too.
+ *
+ * parent 0 and level 0, as for the direct array:
+ * there is no block above an inode's pointer. Without
+ * it a probe following a block from allocation to its
+ * tree loses every root indirect block.
+ */
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, 0, 0, 0,
+ indirect_blk, 0);
+ fi->i_indirect = cpu_to_le64(indirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ /* Read indirect to look up / install the slot. */
+ /* Already held when this call created it. */
+ {
+ int cached = 1;
+
+ if (!ibh) {
+ /*
+ * Same probe as the read path: was this
+ * block already in the cache, or does it
+ * come off the medium?
+ *
+ * The allocation path had no tracepoint at
+ * all, so a slot installed here and read
+ * back as zero 150 ms later could not be
+ * told apart from a slot never written.
+ * generic/083 lost 47 blocks that way and
+ * the trace could not say which.
+ */
+ struct buffer_head *probe =
+ sb_find_get_block(sb, indirect_blk);
+
+ cached = !!probe;
+ if (probe)
+ brelse(probe);
+ ibh = sb_bread(sb, indirect_blk);
+ }
+ if (ibh)
+ trace_beamfs_ind_read(inode->i_sb->s_dev,
+ inode->i_ino, indirect_blk,
+ (unsigned int)indirect_slot,
+ le64_to_cpu(((__le64 *)ibh->b_data)[indirect_slot]),
+ buffer_uptodate(ibh), !cached);
+ }
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read indirect block %llu\n",
+ (unsigned long long)indirect_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ phys = le64_to_cpu(ptrs[indirect_slot]);
+
+ if (phys) {
+ brelse(ibh);
+ *phys_out = phys;
+ return 0;
+ }
+
+ /* Allocate data block and zero-init. */
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(ibh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ dbh = sb_getblk(sb, new_block);
+ if (!dbh) {
+ beamfs_free_block(sb, new_block, inode);
+ brelse(ibh);
+ return -EIO;
+ }
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ /*
+ * Stamp the DATA_CSUM descriptor on the freshly allocated data
+ * block. An all-zero block is a valid RS codeword by linearity,
+ * so decode accepts it, but the descriptor would stay 0x00 and
+ * the read path would have nothing to verify against. Before
+ * the type byte was made fail-closed that meant silent
+ * acceptance; afterwards it means a legitimate newly allocated
+ * block is rejected. Observed 2026-08-18: iblocks 0..5 of a
+ * test file logged "bad descriptor type=0x00" on five runs out
+ * of six, because allocation writes the block here while the
+ * only other stamp sites are writeback and
+ * zero_tail_block, neither of which runs for a block that is
+ * allocated but not yet written through the folio path. The
+ * payload is the zeroed block itself. Indirect blocks carry no
+ * DATA_CSUM descriptor and are deliberately left untouched.
+ */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), (u8 *)dbh->b_data,
+ (const u8 *)dbh->b_data,
+ (unsigned long long)inode->i_ino, iblock_logical);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ /*
+ * A data block carries sixteen RS codewords, not indirect
+ * parity.
+ *
+ * ind_parity_update files a CRC for this block number in
+ * the separate parity region that covers indirect blocks.
+ * A newly allocated data block was being registered there,
+ * and its own sixteen codewords were left as the zeros
+ * memset put down -- correct for a zero block, wrong the
+ * moment anything is written into it and the parity is not
+ * recomputed.
+ *
+ * The sweep found exactly that: subblocks holding data,
+ * uncorrectable; subblocks still zero, fine. Encoding here
+ * makes a fresh block valid on its own terms from the
+ * start.
+ */
+ {
+ int _e = beamfs_seal_data_block(sb, (u8 *)dbh->b_data);
+
+ if (_e < 0)
+ pr_err_ratelimited("beamfs/inline: encode of new data block %llu failed: %d\n",
+ (unsigned long long)new_block,
+ _e);
+ }
+ mark_buffer_dirty(dbh);
+
+ /*
+ * Under the buffer lock, like ext2 splicing a branch.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. generic/464 lost
+ * blocks whose final event was exactly this store into an L1
+ * shared across inodes, with no free and no truncate after.
+ * ind_parity_update reads b_data, so it belongs inside the
+ * lock too.
+ */
+ lock_buffer(ibh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, ibh->b_blocknr,
+ indirect_slot, le64_to_cpu(ptrs[indirect_slot]),
+ new_block, 1);
+ beamfs_tc_store(sb, inode->i_ino, ibh->b_blocknr,
+ (u32)indirect_slot, le64_to_cpu(ptrs[indirect_slot]), new_block,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(dbh);
+ brelse(dbh);
+ ptrs[indirect_slot] = cpu_to_le64(new_block);
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /*
+ * Attach the block to the inode and dirty the inode, the
+ * way ext2_splice_branch ends:
+ *
+ * mmb_mark_buffer_dirty(where->bh,
+ * &EXT2_I(inode)->i_metadata_bhs);
+ * inode_set_ctime_current(inode);
+ * mark_inode_dirty(inode);
+ *
+ * A plain mark_buffer_dirty leaves the block on no inode's
+ * metadata list, so __writeback_single_inode never flushes
+ * it: the pointer is in memory, the buffer is dirty, and
+ * nothing carries it to the disk. The three sites that
+ * create a fresh indirect block already dirty the inode --
+ * they change i_indirect -- and these three, installing
+ * into a block that already exists, did not.
+ *
+ * That is the generic/464 leak: 28 blocks allocated by one
+ * writer into one inode from this site, no free after, and
+ * the flusher reallocating for the same inode seconds later
+ * because the tree it read had none of them.
+ */
+ beamfs_attach_dirty(inode, ibh);
+ inode_set_ctime_current(inode);
+ mark_inode_dirty(inode);
+ brelse(ibh);
+
+ if (allocated)
+ *allocated = true;
+ *phys_out = new_block;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_MAX_IBLOCK_DINDIRECT) {
+ /*
+ * Double-indirect write path: allocate dindirect block,
+ * level-1 indirect block, and data block in a cascade,
+ * each zero-initialized. The allocation order minimizes
+ * orphaned blocks on crash: data block last, so a crash
+ * between dindirect alloc and data alloc leaves a zero
+ * level-1 pointer (treated as HOLE on read).
+ */
+ u64 didx, l1_slot, l2_slot;
+ u64 dindirect_blk, l1_blk;
+ struct buffer_head *l1bh = NULL;
+
+ didx = iblock_logical - BEAMFS_MAX_IBLOCK_INDIRECT;
+ l1_slot = didx / BEAMFS_INDIRECT_PTRS;
+ l2_slot = didx % BEAMFS_INDIRECT_PTRS;
+
+ /* --- Stage 1: dindirect block --- */
+ dindirect_blk = le64_to_cpu(fi->i_dindirect);
+ if (!dindirect_blk) {
+ dindirect_blk = beamfs_alloc_block(sb, inode);
+ if (!dindirect_blk) {
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, dindirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, dindirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ beamfs_tc_zeroed(sb, inode->i_ino, ibh->b_blocknr, __func__);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ /*
+ * Parity taken under the lock, like the install
+ * sites do.
+ *
+ * The block is allocated and visible before its
+ * pointer is installed a few lines down, so
+ * another writer can reach it. Computing parity
+ * after unlock_buffer describes whatever the block
+ * held at that instant, which need not be what
+ * reaches the medium: generic/464 writes and
+ * fsyncs the same files from several tasks and
+ * produced 194 indirect blocks whose parity was
+ * written and did not match, seven orphaned blocks
+ * under each.
+ */
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(ibh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(ibh);
+ /*
+ * The inode's own pointer is a slot store too.
+ *
+ * parent 0 and level 0, as for the direct array:
+ * there is no block above an inode's pointer. Without
+ * it a probe following a block from allocation to its
+ * tree loses every root indirect block.
+ */
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, 0, 0, 0,
+ dindirect_blk, 0);
+ fi->i_dindirect = cpu_to_le64(dindirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ /* --- Stage 2: level-1 indirect block --- */
+ {
+ /*
+ * The last unlit sb_bread of the allocation path.
+ *
+ * generic/083 on 2026-09-18, inode 39: block 1067
+ * became its i_dindirect at 479.487 and slots 0
+ * to 4 were filled; from 483.756 slots 0, 1, 2 and
+ * 3 were each written again, every one of them
+ * reading zero, and treecheck reported three lost
+ * pointers. Not one of those stores had an
+ * ind_read beside it, because the read that
+ * decides them happens here and here alone.
+ *
+ * 1067 had been a DATA block of inode 7 until
+ * 479.485 -- 1.7 ms earlier. A data block recycled
+ * into an indirect one passes through no path that
+ * detaches its buffer from the old owner.
+ */
+ struct buffer_head *probe =
+ sb_find_get_block(sb, dindirect_blk);
+ int cached = !!probe;
+
+ if (probe)
+ brelse(probe);
+ ibh = sb_bread(sb, dindirect_blk);
+ if (ibh)
+ trace_beamfs_ind_read(inode->i_sb->s_dev,
+ inode->i_ino, dindirect_blk,
+ (unsigned int)l1_slot,
+ le64_to_cpu(((__le64 *)ibh->b_data)[l1_slot]),
+ buffer_uptodate(ibh), !cached);
+ }
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read dindirect block %llu\n",
+ (unsigned long long)dindirect_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ if (!l1_blk) {
+ l1_blk = beamfs_alloc_block(sb, inode);
+ if (!l1_blk) {
+ brelse(ibh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ l1bh = sb_getblk(sb, l1_blk);
+ if (!l1bh) {
+ beamfs_free_block(sb, l1_blk, inode);
+ brelse(ibh);
+ return -EIO;
+ }
+ lock_buffer(l1bh);
+ beamfs_tc_zeroed(sb, inode->i_ino, l1bh->b_blocknr, __func__);
+ memset(l1bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l1bh);
+ unlock_buffer(l1bh);
+ beamfs_ind_parity_update(sb, l1bh, inode);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(l1bh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+ /*
+ * Traced like every other slot install.
+ *
+ * Three of the ten allocation sites called tc_store
+ * and not this, so a probe pairing allocations with
+ * stores counted every L1 and L2 indirect block as an
+ * allocation that went nowhere: 4126 of them in one
+ * generic/476, against the one block fsck found
+ * leaked.
+ */
+ /*
+ * Under the buffer lock, like the data installs above.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. treecheck caught it
+ * from wb_workfn -- LOST POINTER, held N, read as 0 -- and the
+ * child block then has no parity written for it and its whole
+ * subtree is unreachable. The three data installs took this
+ * lock; the three indirect installs did not.
+ * ind_parity_update reads b_data, so it belongs inside too.
+ */
+ lock_buffer(ibh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, ibh->b_blocknr,
+ l1_slot, le64_to_cpu(ptrs[l1_slot]),
+ l1_blk, 2);
+ beamfs_tc_store(sb, inode->i_ino, ibh->b_blocknr,
+ (u32)l1_slot, le64_to_cpu(ptrs[l1_slot]), l1_blk,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(l1bh);
+ ptrs[l1_slot] = cpu_to_le64(l1_blk);
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /* Splicing a child into its parent is an install like
+ * any other: the parent goes on the inode's list too,
+ * or it is dirty on nobody's.
+ */
+ beamfs_attach_dirty(inode, ibh);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ }
+ brelse(ibh);
+
+ /* --- Stage 3: data block --- */
+ /* Already held when this call created it. */
+ {
+ int cached = 1;
+
+ if (!l1bh) {
+ /*
+ * The level-1 path had this probe and the
+ * double indirect had none, so every lost
+ * pointer below a dindirect was invisible.
+ *
+ * generic/083 on 2026-09-18: inode 112 put
+ * 2127 into slot 0 of block 1248, then put
+ * 11106 into that same slot 4.3 seconds
+ * later, both stores reading zero. Far too
+ * far apart for a race, and the trace had
+ * nothing at all between them because the
+ * read that returned zero was here.
+ */
+ struct buffer_head *probe =
+ sb_find_get_block(sb, l1_blk);
+
+ cached = !!probe;
+ if (probe)
+ brelse(probe);
+ l1bh = sb_bread(sb, l1_blk);
+ }
+ if (l1bh)
+ trace_beamfs_ind_read(inode->i_sb->s_dev,
+ inode->i_ino, l1_blk,
+ (unsigned int)l2_slot,
+ le64_to_cpu(((__le64 *)l1bh->b_data)[l2_slot]),
+ buffer_uptodate(l1bh), !cached);
+ }
+ if (!l1bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read L1 indirect block %llu\n",
+ (unsigned long long)l1_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l1bh)) {
+ brelse(l1bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l1bh->b_data;
+ phys = le64_to_cpu(ptrs[l2_slot]);
+ if (phys) {
+ brelse(l1bh);
+ *phys_out = phys;
+ return 0;
+ }
+
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(l1bh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ dbh = sb_getblk(sb, new_block);
+ if (!dbh) {
+ beamfs_free_block(sb, new_block, inode);
+ brelse(l1bh);
+ return -EIO;
+ }
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ /*
+ * Stamp the DATA_CSUM descriptor on the freshly allocated data
+ * block. An all-zero block is a valid RS codeword by linearity,
+ * so decode accepts it, but the descriptor would stay 0x00 and
+ * the read path would have nothing to verify against. Before
+ * the type byte was made fail-closed that meant silent
+ * acceptance; afterwards it means a legitimate newly allocated
+ * block is rejected. Observed 2026-08-18: iblocks 0..5 of a
+ * test file logged "bad descriptor type=0x00" on five runs out
+ * of six, because allocation writes the block here while the
+ * only other stamp sites are writeback and
+ * zero_tail_block, neither of which runs for a block that is
+ * allocated but not yet written through the folio path. The
+ * payload is the zeroed block itself. Indirect blocks carry no
+ * DATA_CSUM descriptor and are deliberately left untouched.
+ */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), (u8 *)dbh->b_data,
+ (const u8 *)dbh->b_data,
+ (unsigned long long)inode->i_ino, iblock_logical);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ /*
+ * A data block carries sixteen RS codewords, not indirect
+ * parity.
+ *
+ * ind_parity_update files a CRC for this block number in
+ * the separate parity region that covers indirect blocks.
+ * A newly allocated data block was being registered there,
+ * and its own sixteen codewords were left as the zeros
+ * memset put down -- correct for a zero block, wrong the
+ * moment anything is written into it and the parity is not
+ * recomputed.
+ *
+ * The sweep found exactly that: subblocks holding data,
+ * uncorrectable; subblocks still zero, fine. Encoding here
+ * makes a fresh block valid on its own terms from the
+ * start.
+ */
+ {
+ int _e = beamfs_seal_data_block(sb, (u8 *)dbh->b_data);
+
+ if (_e < 0)
+ pr_err_ratelimited("beamfs/inline: encode of new data block %llu failed: %d\n",
+ (unsigned long long)new_block,
+ _e);
+ }
+ mark_buffer_dirty(dbh);
+
+ /*
+ * Under the buffer lock, like ext2 splicing a branch.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. generic/464 lost
+ * blocks whose final event was exactly this store into an L1
+ * shared across inodes, with no free and no truncate after.
+ * ind_parity_update reads b_data, so it belongs inside the
+ * lock too.
+ */
+ lock_buffer(l1bh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, l1bh->b_blocknr,
+ l2_slot, le64_to_cpu(ptrs[l2_slot]),
+ new_block, 2);
+ beamfs_tc_store(sb, inode->i_ino, l1bh->b_blocknr,
+ (u32)l2_slot, le64_to_cpu(ptrs[l2_slot]), new_block,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(dbh);
+ brelse(dbh);
+ ptrs[l2_slot] = cpu_to_le64(new_block);
+ beamfs_ind_parity_update(sb, l1bh, inode);
+ unlock_buffer(l1bh);
+ /*
+ * Attach the block to the inode and dirty the inode, the
+ * way ext2_splice_branch ends:
+ *
+ * mmb_mark_buffer_dirty(where->bh,
+ * &EXT2_I(inode)->i_metadata_bhs);
+ * inode_set_ctime_current(inode);
+ * mark_inode_dirty(inode);
+ *
+ * A plain mark_buffer_dirty leaves the block on no inode's
+ * metadata list, so __writeback_single_inode never flushes
+ * it: the pointer is in memory, the buffer is dirty, and
+ * nothing carries it to the disk. The three sites that
+ * create a fresh indirect block already dirty the inode --
+ * they change i_indirect -- and these three, installing
+ * into a block that already exists, did not.
+ *
+ * That is the generic/464 leak: 28 blocks allocated by one
+ * writer into one inode from this site, no free after, and
+ * the flusher reallocating for the same inode seconds later
+ * because the tree it read had none of them.
+ */
+ beamfs_attach_dirty(inode, l1bh);
+ inode_set_ctime_current(inode);
+ mark_inode_dirty(inode);
+ brelse(l1bh);
+
+ if (allocated)
+ *allocated = true;
+ *phys_out = new_block;
+ return 0;
+ }
+
+ if (iblock_logical < BEAMFS_MAX_IBLOCK_TINDIRECT) {
+ /*
+ * Triple-indirect write path: allocate tindirect block,
+ * level-1 indirect block, level-2 indirect block, and data
+ * block in a cascade, each zero-initialized. Allocation order
+ * minimizes orphaned blocks on crash: data block last.
+ */
+ u64 tidx, l1_slot, l2_slot, l3_slot;
+ u64 tindirect_blk, l1_blk, l2_blk;
+ struct buffer_head *l1bh = NULL, *l2bh = NULL;
+
+ tidx = iblock_logical - BEAMFS_MAX_IBLOCK_DINDIRECT;
+ l1_slot = tidx / (BEAMFS_INDIRECT_PTRS * BEAMFS_INDIRECT_PTRS);
+ l2_slot = (tidx / BEAMFS_INDIRECT_PTRS) % BEAMFS_INDIRECT_PTRS;
+ l3_slot = tidx % BEAMFS_INDIRECT_PTRS;
+
+ /* --- Stage 1: tindirect block --- */
+ tindirect_blk = le64_to_cpu(fi->i_tindirect);
+ if (!tindirect_blk) {
+ tindirect_blk = beamfs_alloc_block(sb, inode);
+ if (!tindirect_blk) {
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ ibh = sb_getblk(sb, tindirect_blk);
+ if (!ibh) {
+ beamfs_free_block(sb, tindirect_blk, inode);
+ return -EIO;
+ }
+ lock_buffer(ibh);
+ beamfs_tc_zeroed(sb, inode->i_ino, ibh->b_blocknr, __func__);
+ memset(ibh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(ibh);
+ /*
+ * Parity taken under the lock, like the install
+ * sites do.
+ *
+ * The block is allocated and visible before its
+ * pointer is installed a few lines down, so
+ * another writer can reach it. Computing parity
+ * after unlock_buffer describes whatever the block
+ * held at that instant, which need not be what
+ * reaches the medium: generic/464 writes and
+ * fsyncs the same files from several tasks and
+ * produced 194 indirect blocks whose parity was
+ * written and did not match, seven orphaned blocks
+ * under each.
+ */
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(ibh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(ibh);
+ /*
+ * The inode's own pointer is a slot store too.
+ *
+ * parent 0 and level 0, as for the direct array:
+ * there is no block above an inode's pointer. Without
+ * it a probe following a block from allocation to its
+ * tree loses every root indirect block.
+ */
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, 0, 0, 0,
+ tindirect_blk, 0);
+ fi->i_tindirect = cpu_to_le64(tindirect_blk);
+ mark_inode_dirty(inode);
+ }
+
+ /* --- Stage 2: level-1 indirect block --- */
+ ibh = sb_bread(sb, tindirect_blk);
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect block %llu\n",
+ (unsigned long long)tindirect_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+ l1_blk = le64_to_cpu(ptrs[l1_slot]);
+ if (!l1_blk) {
+ l1_blk = beamfs_alloc_block(sb, inode);
+ if (!l1_blk) {
+ brelse(ibh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ l1bh = sb_getblk(sb, l1_blk);
+ if (!l1bh) {
+ beamfs_free_block(sb, l1_blk, inode);
+ brelse(ibh);
+ return -EIO;
+ }
+ lock_buffer(l1bh);
+ beamfs_tc_zeroed(sb, inode->i_ino, l1bh->b_blocknr, __func__);
+ memset(l1bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l1bh);
+ unlock_buffer(l1bh);
+ beamfs_ind_parity_update(sb, l1bh, inode);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(l1bh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+ /*
+ * Traced like every other slot install.
+ *
+ * Three of the ten allocation sites called tc_store
+ * and not this, so a probe pairing allocations with
+ * stores counted every L1 and L2 indirect block as an
+ * allocation that went nowhere: 4126 of them in one
+ * generic/476, against the one block fsck found
+ * leaked.
+ */
+ /*
+ * Under the buffer lock, like the data installs above.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. treecheck caught it
+ * from wb_workfn -- LOST POINTER, held N, read as 0 -- and the
+ * child block then has no parity written for it and its whole
+ * subtree is unreachable. The three data installs took this
+ * lock; the three indirect installs did not.
+ * ind_parity_update reads b_data, so it belongs inside too.
+ */
+ lock_buffer(ibh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, ibh->b_blocknr,
+ l1_slot, le64_to_cpu(ptrs[l1_slot]),
+ l1_blk, 2);
+ beamfs_tc_store(sb, inode->i_ino, ibh->b_blocknr,
+ (u32)l1_slot, le64_to_cpu(ptrs[l1_slot]), l1_blk,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(l1bh);
+ ptrs[l1_slot] = cpu_to_le64(l1_blk);
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ /* Splicing a child into its parent is an install like
+ * any other: the parent goes on the inode's list too,
+ * or it is dirty on nobody's.
+ */
+ beamfs_attach_dirty(inode, ibh);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ }
+ brelse(ibh);
+
+ /* --- Stage 3: level-2 indirect block --- */
+ /* Already held when this call created it. */
+ if (!l1bh)
+ l1bh = sb_bread(sb, l1_blk);
+ if (!l1bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect L1 block %llu\n",
+ (unsigned long long)l1_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l1bh)) {
+ brelse(l1bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l1bh->b_data;
+ l2_blk = le64_to_cpu(ptrs[l2_slot]);
+ if (!l2_blk) {
+ l2_blk = beamfs_alloc_block(sb, inode);
+ if (!l2_blk) {
+ brelse(l1bh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ l2bh = sb_getblk(sb, l2_blk);
+ if (!l2bh) {
+ beamfs_free_block(sb, l2_blk, inode);
+ brelse(l1bh);
+ return -EIO;
+ }
+ lock_buffer(l2bh);
+ beamfs_tc_zeroed(sb, inode->i_ino, l2bh->b_blocknr, __func__);
+ memset(l2bh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(l2bh);
+ unlock_buffer(l2bh);
+ beamfs_ind_parity_update(sb, l2bh, inode);
+ /*
+ * Attach it to the inode as the install sites do.
+ *
+ * A fresh indirect block is dirtied here and its first
+ * pointer installed a few lines further down, and the
+ * install attaches it. But sb_getblk here and sb_bread
+ * there need not hand back the same buffer: if this one
+ * is evicted in between, it goes with nothing written,
+ * on no inode's list for anyone to flush.
+ *
+ * generic/464 loses exactly that. Inode 150 in one
+ * capture: four pointers installed into indirect block
+ * 33190, i_indirect written to disk naming it, and 33190
+ * itself still 0xcd -- never written at all -- so fsck
+ * finds four blocks marked used that nothing references.
+ */
+ mmb_mark_buffer_dirty(l2bh, &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ /*
+ * Held, not released and read back.
+ *
+ * This released the buffer here and read the same block
+ * again a few lines down. Between the two it can be
+ * evicted, and the read then comes from the medium --
+ * where nothing has been written yet. The block returns
+ * holding whatever was there before, and every pointer
+ * installed into it afterwards lands in a block that is
+ * never written at all.
+ *
+ * A frozen generic/464 volume shows the whole thing:
+ * inode 36's indirect block 16469, named by the inode on
+ * disk, 0xcd across 464 of its 512 slots, and 273 blocks
+ * reported used and referenced by nothing.
+ *
+ * Attaching it to the inode was meant to close this. It
+ * does not: the attach makes the buffer flushable, and
+ * dropping the last reference still lets it go before
+ * anything flushes it. Holding the reference closes it.
+ */
+ /*
+ * Traced like every other slot install.
+ *
+ * Three of the ten allocation sites called tc_store
+ * and not this, so a probe pairing allocations with
+ * stores counted every L1 and L2 indirect block as an
+ * allocation that went nowhere: 4126 of them in one
+ * generic/476, against the one block fsck found
+ * leaked.
+ */
+ /*
+ * Under the buffer lock, like the data installs above.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. treecheck caught it
+ * from wb_workfn -- LOST POINTER, held N, read as 0 -- and the
+ * child block then has no parity written for it and its whole
+ * subtree is unreachable. The three data installs took this
+ * lock; the three indirect installs did not.
+ * ind_parity_update reads b_data, so it belongs inside too.
+ */
+ lock_buffer(l1bh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, l1bh->b_blocknr,
+ l2_slot, le64_to_cpu(ptrs[l2_slot]),
+ l2_blk, 3);
+ beamfs_tc_store(sb, inode->i_ino, l1bh->b_blocknr,
+ (u32)l2_slot, le64_to_cpu(ptrs[l2_slot]), l2_blk,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(l2bh);
+ ptrs[l2_slot] = cpu_to_le64(l2_blk);
+ beamfs_ind_parity_update(sb, l1bh, inode);
+ unlock_buffer(l1bh);
+ /* Splicing a child into its parent is an install like
+ * any other: the parent goes on the inode's list too,
+ * or it is dirty on nobody's.
+ */
+ beamfs_attach_dirty(inode, l1bh);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ }
+ brelse(l1bh);
+
+ /* --- Stage 4: data block --- */
+ /* Already held when this call created it. */
+ if (!l2bh)
+ l2bh = sb_bread(sb, l2_blk);
+ if (!l2bh) {
+ pr_err_ratelimited("beamfs/inline: failed to read tindirect L2 block %llu\n",
+ (unsigned long long)l2_blk);
+ return -EIO;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, l2bh)) {
+ brelse(l2bh);
+ return -EUCLEAN;
+ }
+ ptrs = (__le64 *)l2bh->b_data;
+ phys = le64_to_cpu(ptrs[l3_slot]);
+ if (phys) {
+ brelse(l2bh);
+ *phys_out = phys;
+ return 0;
+ }
+
+ new_block = beamfs_alloc_block(sb, inode);
+ if (!new_block) {
+ brelse(l2bh);
+ if (beamfs_failed(sb))
+ return -EIO;
+ return -ENOSPC;
+ }
+ dbh = sb_getblk(sb, new_block);
+ if (!dbh) {
+ beamfs_free_block(sb, new_block, inode);
+ brelse(l2bh);
+ return -EIO;
+ }
+ lock_buffer(dbh);
+ memset(dbh->b_data, 0, BEAMFS_BLOCK_SIZE);
+ /*
+ * Stamp the DATA_CSUM descriptor on the freshly allocated data
+ * block. An all-zero block is a valid RS codeword by linearity,
+ * so decode accepts it, but the descriptor would stay 0x00 and
+ * the read path would have nothing to verify against. Before
+ * the type byte was made fail-closed that meant silent
+ * acceptance; afterwards it means a legitimate newly allocated
+ * block is rejected. Observed 2026-08-18: iblocks 0..5 of a
+ * test file logged "bad descriptor type=0x00" on five runs out
+ * of six, because allocation writes the block here while the
+ * only other stamp sites are writeback and
+ * zero_tail_block, neither of which runs for a block that is
+ * allocated but not yet written through the folio path. The
+ * payload is the zeroed block itself. Indirect blocks carry no
+ * DATA_CSUM descriptor and are deliberately left untouched.
+ */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), (u8 *)dbh->b_data,
+ (const u8 *)dbh->b_data,
+ (unsigned long long)inode->i_ino, iblock_logical);
+ set_buffer_uptodate(dbh);
+ unlock_buffer(dbh);
+ /*
+ * A data block carries sixteen RS codewords, not indirect
+ * parity.
+ *
+ * ind_parity_update files a CRC for this block number in
+ * the separate parity region that covers indirect blocks.
+ * A newly allocated data block was being registered there,
+ * and its own sixteen codewords were left as the zeros
+ * memset put down -- correct for a zero block, wrong the
+ * moment anything is written into it and the parity is not
+ * recomputed.
+ *
+ * The sweep found exactly that: subblocks holding data,
+ * uncorrectable; subblocks still zero, fine. Encoding here
+ * makes a fresh block valid on its own terms from the
+ * start.
+ */
+ {
+ int _e = beamfs_seal_data_block(sb, (u8 *)dbh->b_data);
+
+ if (_e < 0)
+ pr_err_ratelimited("beamfs/inline: encode of new data block %llu failed: %d\n",
+ (unsigned long long)new_block,
+ _e);
+ }
+ mark_buffer_dirty(dbh);
+
+ /*
+ * Under the buffer lock, like ext2 splicing a branch.
+ *
+ * This stores a pointer into a shared indirect block. Without
+ * the lock the store races the flusher submitting that same
+ * buffer: the pointer reaches disk half-written, or a
+ * concurrent installer's store is lost. generic/464 lost
+ * blocks whose final event was exactly this store into an L1
+ * shared across inodes, with no free and no truncate after.
+ * ind_parity_update reads b_data, so it belongs inside the
+ * lock too.
+ */
+ lock_buffer(l2bh);
+ trace_beamfs_slot_store(inode->i_sb->s_dev, inode->i_ino, l2bh->b_blocknr,
+ l3_slot, le64_to_cpu(ptrs[l3_slot]),
+ new_block, 3);
+ beamfs_tc_store(sb, inode->i_ino, l2bh->b_blocknr,
+ (u32)l3_slot, le64_to_cpu(ptrs[l3_slot]), new_block,
+ __func__);
+ /* The block, before the pointer that names it. */
+ beamfs_order_before_pointer(dbh);
+ brelse(dbh);
+ ptrs[l3_slot] = cpu_to_le64(new_block);
+ beamfs_ind_parity_update(sb, l2bh, inode);
+ unlock_buffer(l2bh);
+ /*
+ * Attach the block to the inode and dirty the inode, the
+ * way ext2_splice_branch ends:
+ *
+ * mmb_mark_buffer_dirty(where->bh,
+ * &EXT2_I(inode)->i_metadata_bhs);
+ * inode_set_ctime_current(inode);
+ * mark_inode_dirty(inode);
+ *
+ * A plain mark_buffer_dirty leaves the block on no inode's
+ * metadata list, so __writeback_single_inode never flushes
+ * it: the pointer is in memory, the buffer is dirty, and
+ * nothing carries it to the disk. The three sites that
+ * create a fresh indirect block already dirty the inode --
+ * they change i_indirect -- and these three, installing
+ * into a block that already exists, did not.
+ *
+ * That is the generic/464 leak: 28 blocks allocated by one
+ * writer into one inode from this site, no free after, and
+ * the flusher reallocating for the same inode seconds later
+ * because the tree it read had none of them.
+ */
+ beamfs_attach_dirty(inode, l2bh);
+ inode_set_ctime_current(inode);
+ mark_inode_dirty(inode);
+ brelse(l2bh);
+
+ if (allocated)
+ *allocated = true;
+ *phys_out = new_block;
+ return 0;
+ }
+
+ pr_err_ratelimited("beamfs/inline: iblock %llu beyond tindirect capacity (write)\n",
+ (unsigned long long)iblock_logical);
+ return -EOPNOTSUPP;
+}
+
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 13/18] beamfs: inline data path 3/4, read side
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (11 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 12/18] beamfs: inline data path 2/4, allocation Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 14/18] beamfs: inline data path 4/4, write side Aurelien DESBRIERES
` (4 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
file_inline.c is the data path of the UNIVERSAL_INLINE protection
scheme (scheme 2), the one mkfs.beamfs formats. Each 4096-byte data
block holds sixteen RS(255,239) codewords, 3824 coded bytes and 256
bytes of parity, followed by a 16-byte tail; every read decodes the
block.
The file is 4875 lines, too large for one message to the list, so it
is split across four patches along function boundaries. It builds only
as a whole, once the Kconfig and Makefile patch is applied.
This part: the tail descriptor and its payload CRC, block decode,
symlink decode, ->iomap_begin and ->iomap_end, and the read side:
read_folio and the read of partial blocks before a write.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/file_inline.c | 1316 +++++++++++++++++++++++++++++++++++++++
1 file changed, 1316 insertions(+)
diff --git a/fs/beamfs/file_inline.c b/fs/beamfs/file_inline.c
index dca00ae1f645..2ea7e7e10168 100644
--- a/fs/beamfs/file_inline.c
+++ b/fs/beamfs/file_inline.c
@@ -2398,3 +2398,1319 @@ static int beamfs_inline_lookup_or_alloc_phys_new(struct inode *inode,
return -EOPNOTSUPP;
}
+/* ------------------------------------------------------------------------- */
+/*
+ * beamfs_inline_stamp_tail_pad -- write the DATA_CSUM descriptor and the
+ * DATA_SELFID digest into a data block. The 16-byte tail at
+ * BEAMFS_DATA_INLINE_TOTAL is always zeroed first; in the alternating
+ * layout that is where both go, and the zeroing covers the reserved
+ * bytes and the csum-disabled (NONE) case. When the volume has DATA_CSUM
+ * active, stamp csum_type = CRC32 and the beamfs_crc32 (crc32_le) of the
+ * payload, beamfs_block_payload() bytes of it. Where the descriptor and
+ * the digest live in each layout is below.
+ */
+/*
+ * Where the descriptor lives, and how much it covers.
+ *
+ * Alternating: at 4080, outside every codeword, over the 3824 bytes of
+ * payload. A flip there fails a read closed rather than accepting
+ * wrong data.
+ *
+ * Capsule: at 3808, inside the coded area, over the 3808 bytes of
+ * payload. The argument for staying outside was that a descriptor
+ * cannot be repaired -- interleaved it can, along with everything
+ * else, and nine bad bytes there stop condemning a block whose data is
+ * intact. The sixteen bytes it costs are already spent: the payload is
+ * 3808 and the encoder covers 3824, so 3808..3823 were protected and
+ * empty.
+ */
+static u32 beamfs_desc_off(struct super_block *sb)
+{
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+ return BEAMFS_CAPSULE_CSUM_OFF;
+
+ return BEAMFS_DATA_CSUM_TYPE_OFF;
+}
+
+static u32 beamfs_selfid_off(struct super_block *sb)
+{
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+ return BEAMFS_CAPSULE_SELFID_OFF;
+
+ return BEAMFS_DATA_SELFID_OFF;
+}
+
+static void beamfs_inline_stamp_tail_pad(struct beamfs_sb_info *sbi,
+ u8 *block, const u8 *payload,
+ u64 ino, u64 iblock)
+{
+ struct super_block *sb = sbi->s_sb;
+ u32 doff = beamfs_desc_off(sb);
+ u32 soff = beamfs_selfid_off(sb);
+ u32 covered = beamfs_block_payload(sb);
+
+ memset(block + BEAMFS_DATA_INLINE_TOTAL, 0, BEAMFS_DATA_INLINE_PAD);
+ /*
+ * The descriptor whole, whatever is stamped in it below: in a
+ * capsule it is coded data, and left as the page held it, it
+ * carried sixteen bytes of kernel memory to the medium.
+ */
+ memset(block + doff, 0,
+ BEAMFS_DATA_CSUM_DESC_BYTES + BEAMFS_DATA_SELFID_BYTES);
+ if (sbi->s_data_csum) {
+ u32 crc = beamfs_crc32(payload, covered);
+
+ block[doff] = BEAMFS_CSUM_CRC32;
+ put_unaligned_le32(crc, block + doff + 4);
+ }
+ /*
+ * DATA_SELFID: bind the block to the (inode, logical index) pair it
+ * was written for, so a read reaching it through a corrupted pointer
+ * fails closed instead of returning another file's intact data.
+ */
+ if (sbi->s_data_selfid) {
+ u64 id = beamfs_data_selfid(ino, iblock);
+
+ put_unaligned_le64(id, block + soff);
+ }
+}
+
+/*
+ * Stamp a data block's descriptor, before its seal. Exposed for
+ * namei.c: the target of a long symlink is a data block written there.
+ */
+void beamfs_stamp_data_block(struct super_block *sb, u8 *block,
+ const u8 *payload, u64 ino, u64 iblock)
+{
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), block, payload, ino, iblock);
+}
+
+/*
+ * beamfs_inline_payload_crc -- recompute the DATA_CSUM over the decoded
+ * payload held in @codeword, in whichever layout it is written.
+ *
+ * A capsule holds the payload contiguously: one crc32_le over its 3808
+ * bytes. The alternating layout holds it in sixteen runs of 239 bytes,
+ * 255 apart: crc32_le is chained over the sixteen runs, which equals
+ * beamfs_crc32() over the contiguous 3824-byte payload, the value the
+ * write path stored. Walking one layout as the other hashes the parity
+ * along with the data and disagrees with what was stamped. No alloc.
+ */
+static u32 beamfs_inline_payload_crc(struct super_block *sb,
+ const u8 *codeword)
+{
+ u32 c = 0xFFFFFFFF;
+ unsigned int i;
+
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE)
+ return crc32_le(c, codeword + BEAMFS_CAPSULE_DATA_OFF,
+ BEAMFS_CAPSULE_DATA_BYTES) ^ 0xFFFFFFFF;
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++)
+ c = crc32_le(c,
+ codeword + (size_t)i * BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA);
+ return c ^ 0xFFFFFFFF;
+}
+
+/* beamfs_inline_decode_block_into_buf -- RS-decode a raw block image and */
+/* copy a user-byte slice into the */
+/* supplied buffer. */
+/* */
+/* Called from read_folio_range, the partial-write fill */
+/* (write_read_folio_range), writeback's read of a partial block */
+/* (beamfs_wb_read_old), zero_tail_block and the symlink reader. */
+/* */
+/* Inputs: */
+/* sb superblock (layout, journal) */
+/* raw the block as read, BEAMFS_BLOCK_SIZE bytes; */
+/* copied, never modified */
+/* phys non-zero physical disk block number to decode */
+/* inode inode (its ino for DATA_SELFID and log lines) */
+/* iblock_logical_for_log logical iblock, checked against DATA_SELFID */
+/* and named in log lines */
+/* dst_buf destination buffer; caller-allocated, must */
+/* have at least slice_length bytes available */
+/* slice_offset byte offset within the disk block user-area */
+/* (0..BEAMFS_DATA_INLINE_BYTES-1) */
+/* slice_length number of user bytes to copy into dst_buf */
+/* (1..BEAMFS_DATA_INLINE_BYTES) */
+/* rmw_path caller context flag: true when invoked from a */
+/* read-modify-write transit (beamfs_wb_read_old */
+/* or zero_tail_block), false otherwise. */
+/* Propagated into the journal entry as */
+/* BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED so that */
+/* silent neutralisation events become */
+/* empirically distinguishable from user-visible */
+/* corrections in post-run forensic analysis. */
+/* Also discriminates the pr_warn / pr_err log */
+/* line wording between "neutralised" and */
+/* "corrected" / "uncorrectable". */
+/* */
+/* Slice contract: slice_offset + slice_length <= BEAMFS_DATA_INLINE_BYTES */
+/* (3824). HOLE (phys == 0) is NOT handled here -- caller must check. */
+/* */
+/* On any subblock corrected: the correction is journalled and the */
+/* corrected bytes are copied out; the block on the medium is not */
+/* rewritten here (see the end of the function). */
+/* On any subblock uncorrectable, or a DATA_CSUM or DATA_SELFID mismatch: */
+/* journal entry with UNCORRECTABLE flag, then -EIO; no slice copy is */
+/* performed. */
+/* */
+/* This isolates the RS decode from folio lifecycle management. The folio */
+/* lock and the end of the read are the caller's responsibility. */
+/* ------------------------------------------------------------------------- */
+static int beamfs_inline_decode_block_into_buf(struct super_block *sb,
+ const u8 *raw,
+ u64 phys,
+ struct inode *inode,
+ u64 iblock_logical_for_log,
+ u8 *dst_buf,
+ u32 slice_offset,
+ u32 slice_length,
+ bool rmw_path)
+{
+ u8 *tmp = NULL;
+ int rs_results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ u8 rs_positions[BEAMFS_DATA_INLINE_SUBBLOCKS *
+ (BEAMFS_RS_PARITY / 2)];
+ bool corrected = false;
+ bool uncorrectable = false;
+ unsigned int i;
+ int ret = 0;
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ /* Defensive contract checks (cheap; helpful in audit and fuzzing). */
+ if (WARN_ON_ONCE(phys == 0))
+ return -EINVAL;
+ if (WARN_ON_ONCE(!dst_buf))
+ return -EINVAL;
+ if (WARN_ON_ONCE(slice_length == 0))
+ return -EINVAL;
+ if (WARN_ON_ONCE((u64)slice_offset + slice_length >
+ BEAMFS_DATA_INLINE_BYTES))
+ return -EINVAL;
+
+ /*
+ * @raw is only read: it is copied into a private scratch buffer
+ * (tmp) and decoded there, never in place. decode_rs8 mutates both
+ * data and parity bytes in its input, and @raw may be the b_data of
+ * a buffer that other paths read. The decode_rs8 upstream race on
+ * rs_control->buffers[] is resolved by the per-CPU rs_control
+ * allocation in edac.c.
+ */
+ tmp = beamfs_scratch_get(sb);
+ if (!tmp)
+ return -ENOMEM;
+ /*
+ * No lock_buffer here, and the reason is in the callers.
+ *
+ * Those that pass a buffer's b_data -- write_read_folio_range,
+ * zero_tail_block and the symlink reader -- already hold its lock,
+ * so taking it again deadlocks on the first decode, which on this
+ * filesystem means the rootfs mount. That was tried and the node
+ * never reached a login prompt.
+ *
+ * The others -- read_folio_range and beamfs_wb_read_old -- pass a
+ * copy taken under the buffer lock, or a block read straight from
+ * the device into a scratch page.
+ */
+ memcpy(tmp, raw, BEAMFS_BLOCK_SIZE);
+
+ /*
+ * A capsule is gathered, not walked.
+ *
+ * Interleaved, symbol i of codeword j is at byte i*16 + j rather
+ * than j*255 + i, and the header sits inside the coded area. The
+ * two layouts share no arithmetic: reading one with the other's
+ * gathers the wrong symbols and decodes to noise with every
+ * check passing. The feature bit is what keeps them apart.
+ */
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE) {
+ beamfs_rs_decode_woven(tmp + BEAMFS_CAPSULE_DATA_OFF,
+ tmp + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ rs_results, "file data");
+ } else {
+ beamfs_rs_decode_region(tmp, BEAMFS_SUBBLOCK_TOTAL,
+ tmp + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ rs_results, rs_positions,
+ BEAMFS_RS_PARITY / 2, "file data");
+ }
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ int rc = rs_results[i];
+
+ if (rc < 0) {
+ u32 xflags = rmw_path ?
+ BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED : 0;
+
+ /*
+ * Journal the uncorrectable event before raising the
+ * error: forensic record takes priority over the alert.
+ *
+ * rmw_path=true here means the uncorrectable was
+ * detected during a write transit. The caller fails the
+ * read-modify-write on the -EIO below, so the damaged
+ * block is not re-encoded over; the journal entry
+ * preserves the forensic record of the event.
+ */
+ beamfs_log_rs_event_flagged(sb, (u64)phys,
+ NULL, 0,
+ BEAMFS_SUBBLOCK_DATA,
+ xflags | beamfs_rs_event_subblock_bits(i));
+ pr_err_ratelimited("beamfs/inline: ino=%llu iblock=%llu subblock=%u uncorrectable%s\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical_for_log,
+ i,
+ rmw_path ? " (rmw)" : "");
+ beamfs_alert_uncorrectable(sb, phys);
+ uncorrectable = true;
+ } else if (rc > 0) {
+ unsigned int np = (unsigned int)rc;
+ u8 *pos = rs_positions +
+ (size_t)i * (BEAMFS_RS_PARITY / 2);
+ u32 xflags = rmw_path ?
+ BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED : 0;
+
+ if (np > BEAMFS_RS_PARITY / 2)
+ np = BEAMFS_RS_PARITY / 2;
+ pr_warn_ratelimited("beamfs/inline: ino=%llu iblock=%llu subblock=%u: %d symbol(s) %s\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical_for_log,
+ i, rc,
+ rmw_path ? "neutralised" : "corrected");
+ beamfs_log_rs_event_flagged(sb, (u64)phys,
+ pos, np,
+ BEAMFS_SUBBLOCK_DATA,
+ xflags | beamfs_rs_event_subblock_bits(i));
+ /*
+ * The journal says this happened; the budget says
+ * how close the block now is to having nothing left.
+ * A high-water mark, so the byte reports the worst
+ * any subblock of this block has been rather than
+ * the most recent, since the worst is what will
+ * saturate first.
+ */
+ beamfs_budget_record(sb, phys, np);
+ corrected = true;
+ }
+ }
+
+ if (uncorrectable) {
+ ret = -EIO;
+ goto out_brelse;
+ }
+
+ /*
+ * DATA_CSUM verification. decode_rs8 can return success while having
+ * converged to a wrong valid codeword (silent miscorrection).
+ * Recompute the payload checksum and reject on mismatch. The
+ * descriptor is read from tmp: in a capsule it sits inside the coded
+ * area and has just been decoded with the rest; in the alternating
+ * layout it sits in the tail pad, which decode does not touch. Any
+ * type other than CRC32 fails closed (see below). On mismatch,
+ * journal an UNCORRECTABLE event and fail closed before any slice
+ * copy, as the per-subblock uncorrectable path does.
+ */
+ if (sbi->s_data_csum) {
+ u32 doff = beamfs_desc_off(sb);
+ u8 ctype = tmp[doff];
+ u32 want = get_unaligned_le32(tmp + doff + 4);
+ u32 got = beamfs_inline_payload_crc(sb, tmp);
+
+ /*
+ * csum_type gates the whole check. In the alternating layout
+ * the descriptor is outside every RS codeword, so it is
+ * neither corrected nor detected by the code. Accepting any
+ * type other than CRC32 as "no checksum present" therefore let
+ * a single flip on offset 4080 disable verification for that
+ * block, silently. Measured 2026-08-17 at 128 flips on a
+ * 64-block file: 20 RS symbols corrected, no uncorrectable, no
+ * csum mismatch logged, and 15296 wrong bits returned to
+ * userspace across two blocks -- the silent accept a descriptor
+ * outside the codewords was meant to rule out. That holds for
+ * the csum value (a corrupted CRC can only over-reject) but not
+ * for the type byte.
+ *
+ * On a DATA_CSUM volume every data block is stamped at write
+ * time, so any other type is a corrupted descriptor, not an
+ * unstamped block: fail closed instead of waving it through.
+ */
+ if (ctype != BEAMFS_CSUM_CRC32) {
+ /*
+ * UNCORRECTABLE is reserved:
+ * beamfs_log_rs_event_flagged sets it itself for
+ * the positions == NULL, n_positions == 0 call shape
+ * used here. Passing it in extra_flags trips the
+ * WARN_ON_ONCE reserved-mask guard there; that
+ * mistake is what made the master node report
+ * RS_FAILED on 2026-08-24.
+ */
+ u32 xflags = 0;
+
+ if (rmw_path)
+ xflags |= BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED;
+
+ beamfs_log_rs_event_flagged(sb,
+ (u64)phys * BEAMFS_DATA_INLINE_SUBBLOCKS,
+ NULL, 0,
+ BEAMFS_SUBBLOCK_DATA,
+ xflags);
+ pr_err_ratelimited("beamfs/inline: ino=%llu iblock=%llu data_csum bad descriptor type=0x%02x (expected 0x%02x)%s\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical_for_log,
+ ctype, BEAMFS_CSUM_CRC32,
+ rmw_path ? " (rmw)" : "");
+ ret = -EIO;
+ goto out_brelse;
+ }
+
+ if (want != got) {
+ beamfs_log_rs_event_flagged(sb,
+ (u64)phys * BEAMFS_DATA_INLINE_SUBBLOCKS,
+ NULL, 0,
+ BEAMFS_SUBBLOCK_DATA,
+ rmw_path ? BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED : 0);
+ pr_err_ratelimited("beamfs/inline: ino=%llu iblock=%llu data_csum mismatch want=0x%08x got=0x%08x%s\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical_for_log,
+ want, got,
+ rmw_path ? " (rmw)" : "");
+ ret = -EIO;
+ goto out_brelse;
+ }
+ }
+
+ /*
+ * DATA_SELFID verification. DATA_CSUM proves the block is intact; it
+ * cannot prove it is the block that was asked for. A corrupted
+ * pointer landing inside the data area reaches a different but valid
+ * block whose own descriptor verifies, so every check above passes on
+ * the wrong data. Measured 2026-08-19: a direct pointer moved from
+ * physical 471 to 503 and the read returned that block's contents,
+ * 15245 wrong bits, with no kernel signal. Comparing the stored
+ * identity digest against the (inode, iblock) actually being read
+ * closes that path.
+ */
+ if (sbi->s_data_selfid) {
+ u64 want_id = beamfs_data_selfid(inode->i_ino,
+ iblock_logical_for_log);
+ u64 got_id = get_unaligned_le64(tmp +
+ beamfs_selfid_off(sb));
+
+ if (want_id != got_id) {
+ /*
+ * UNCORRECTABLE is reserved:
+ * beamfs_log_rs_event_flagged sets it itself for
+ * the positions == NULL, n_positions == 0 call shape
+ * used here. Passing it in extra_flags trips the
+ * WARN_ON_ONCE reserved-mask guard there; that
+ * mistake is what made the master node report
+ * RS_FAILED on 2026-08-24.
+ */
+ u32 xflags = 0;
+
+ if (rmw_path)
+ xflags |= BEAMFS_RS_EVENT_FLAG_RMW_NEUTRALISED;
+
+ beamfs_log_rs_event_flagged(sb,
+ (u64)phys * BEAMFS_DATA_INLINE_SUBBLOCKS,
+ NULL, 0,
+ BEAMFS_SUBBLOCK_DATA,
+ xflags);
+ pr_err_ratelimited("beamfs/inline: ino=%llu iblock=%llu data_selfid mismatch want=0x%016llx got=0x%016llx%s\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)iblock_logical_for_log,
+ want_id, got_id,
+ rmw_path ? " (rmw)" : "");
+ ret = -EIO;
+ goto out_brelse;
+ }
+ }
+
+ /*
+ * Slice gather: copy [slice_offset, slice_offset + slice_length)
+ * of the payload into dst_buf. The alternating layout holds it as
+ * 16 segments of 239 bytes with the parity between them: walk the
+ * relevant subblocks and copy the intersecting portion of each.
+ */
+ {
+ u32 slice_end = slice_offset + slice_length;
+ u32 sb_first = slice_offset / BEAMFS_SUBBLOCK_DATA;
+ u32 sb_last = (slice_end - 1) / BEAMFS_SUBBLOCK_DATA;
+ u32 dst_off = 0;
+ u32 sb_idx;
+
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE) {
+ /*
+ * A capsule holds its payload contiguously, so
+ * the slice is one copy. Walking it in sixteen
+ * runs 255 apart reads each one 16 bytes further
+ * along than it lies.
+ */
+ memcpy(dst_buf + dst_off,
+ tmp + BEAMFS_CAPSULE_DATA_OFF + slice_offset,
+ slice_length);
+ dst_off += slice_length;
+ } else {
+ for (sb_idx = sb_first; sb_idx <= sb_last; sb_idx++) {
+ u32 sb_user_start = sb_idx * BEAMFS_SUBBLOCK_DATA;
+ u32 sb_user_end = sb_user_start +
+ BEAMFS_SUBBLOCK_DATA;
+ u32 from_in_sb = (slice_offset > sb_user_start)
+ ? (slice_offset - sb_user_start) : 0;
+ u32 to_in_sb = (slice_end < sb_user_end)
+ ? (slice_end - sb_user_start)
+ : BEAMFS_SUBBLOCK_DATA;
+ u32 copy_len = to_in_sb - from_in_sb;
+
+ memcpy(dst_buf + dst_off,
+ tmp
+ + (size_t)sb_idx * BEAMFS_SUBBLOCK_TOTAL
+ + from_in_sb,
+ copy_len);
+ dst_off += copy_len;
+ }
+ }
+ }
+
+ /*
+ * No write-back of the correction from here.
+ *
+ * The corrected bytes are in tmp; @raw, and the medium, still hold
+ * what was read. Writing the block back from this path would need
+ * either:
+ * (a) an exclusive lock on phys around the read-modify-write
+ * window, OR
+ * (b) a CoW write path that allocates a fresh phys and
+ * updates the indirect pointer.
+ * The correction is journalled above. The background scrubber
+ * writes corrected blocks back (beamfs_scrub_check_block), and a
+ * later write to the block re-encodes it whole.
+ */
+ (void)corrected;
+
+ beamfs_scratch_put(sb, tmp);
+ return 0;
+
+out_brelse:
+ beamfs_scratch_put(sb, tmp);
+ return ret;
+}
+
+/*
+ * beamfs_inline_decode_symlink -- RS-decode a block holding a link target.
+ *
+ * The same decode as file data, exposed for namei.c because a long
+ * symlink is a data block in every respect but the path that reaches it.
+ * Separate entry point rather than a wider signature on the file path:
+ * this one takes no folio, no iomap iterator and no RMW flag, and
+ * threading three unused arguments through the reader to avoid twelve
+ * lines here would be the worse trade.
+ */
+int beamfs_inline_decode_symlink(struct super_block *sb,
+ struct buffer_head *bh, u64 phys,
+ struct inode *inode, u8 *dst, u32 len)
+{
+ if (len > beamfs_block_payload(sb))
+ return -EUCLEAN;
+
+ return beamfs_inline_decode_block_into_buf(sb, bh->b_data, phys, inode, 0,
+ dst, 0, len, false);
+}
+
+/* ------------------------------------------------------------------------- */
+/* iomap read path */
+/* */
+/* Every 4096-byte block carries its RS parity alongside its payload: 3808 */
+/* bytes of file data in a capsule, 3824 in the alternating layout. File */
+/* offset is therefore not disk offset plus a constant -- which is what */
+/* iomap_sector() assumes. */
+/* */
+/* Two consequences shape this code. iomap_begin bounds a write or fiemap */
+/* mapping by the block that backs pos, and iomap calls back for the rest; */
+/* a read mapping keeps the full requested length (see iomap_begin). And */
+/* the read is done by beamfs_inline_read_folio_range() rather than by */
+/* iomap_bio_read_ops, because the bytes that reach the page cache are not */
+/* the bytes on disk: they are what RS decoding produces from them. */
+/* ------------------------------------------------------------------------- */
+
+/*
+ * A mapping iomap_begin allocated, for iomap_end to give back if the
+ * write stops short of it.
+ *
+ * Not IOMAP_F_NEW. To iomap that flag means the folio range is new:
+ * __iomap_write_begin zeroes it outside the bytes written instead of
+ * reading it (iomap_block_needs_zeroing). A folio here holds the end
+ * of one block and the start of the next, so a write starting a fresh
+ * block zeroed the data of the block before it in the same folio.
+ * Without the flag iomap reads the folio through
+ * beamfs_inline_write_read_folio_range, which finds the fresh block
+ * zeroed in the buffer cache.
+ */
+#define BEAMFS_IOMAP_F_FRESH IOMAP_F_PRIVATE
+
+static int beamfs_inline_iomap_begin(struct inode *inode, loff_t pos,
+ loff_t length, unsigned int flags,
+ struct iomap *iomap,
+ struct iomap *srcmap)
+{
+ loff_t i_size = i_size_read(inode);
+ u32 payload = beamfs_block_payload(inode->i_sb);
+ u64 b = div_u64((u64)pos, payload);
+ u64 phys = 0;
+ int ret;
+
+ iomap->flags = 0;
+ iomap->bdev = inode->i_sb->s_bdev;
+ iomap->addr = IOMAP_NULL_ADDR;
+
+ /*
+ * Start the mapping at the position the caller asked for, not at
+ * the start of the INLINE block that backs it. iomap tracks its
+ * position within the folio against the mapping it was given, and
+ * readahead ends a folio when offset_in_folio(folio, pos) reaches
+ * zero; handing back a range that starts before the requested
+ * position leaves that comparison never matching, and the iterator
+ * walks off a folio it has already released.
+ */
+ iomap->offset = pos;
+
+ /*
+ * Past EOF the whole remaining range is a hole, in one mapping:
+ * iomap zero-fills it without calling back. Splitting it into
+ * INLINE-sized pieces would leave iomap iterating after
+ * iomap_read_folio_iter() has released the folio, which
+ * dereferences NULL on the next round.
+ */
+ /*
+ * Past EOF is a hole to a reader and nothing of the sort to a
+ * writer.
+ *
+ * A write at pos == i_size is the ordinary case of extending a
+ * file, and the block holding that position usually exists
+ * already: at 3808 or 3824 usable bytes per block, appending to a
+ * 32-byte file writes into block 0, which has data in it.
+ *
+ * Reporting IOMAP_HOLE there made iomap_block_needs_zeroing true
+ * on the first term, so __iomap_write_begin took the zeroing
+ * branch and called folio_zero_segments over the whole folio
+ * outside the written range -- erasing bytes 0..31, which were on
+ * disk and below i_size. generic/639 writes 32 bytes, cycles the
+ * mount, writes 32 more, and reads back thirty-two zeros followed
+ * by the second write. Silent loss of data the caller never
+ * touched.
+ *
+ * iomap works in i_blocksize units, 4096, while a block carries
+ * 3808 or 3824 bytes of payload, so its block boundaries are not ours
+ * and a range it considers past EOF can hold live bytes. Answering
+ * the question it actually asked -- what is mapped here -- rather
+ * than a shortcut about EOF keeps the two views consistent.
+ */
+ if (!(flags & IOMAP_WRITE) && pos >= i_size) {
+ iomap->length = length;
+ iomap->type = IOMAP_HOLE;
+ return 0;
+ }
+
+ /*
+ * One mapping covers the whole requested range, even where that
+ * spans two or three INLINE blocks.
+ *
+ * iomap_read_folio_iter() releases the folio as soon as a call
+ * returns with no iomap_folio_state attached -- which is always
+ * the case here, folio size and block size both being 4096, so
+ * ifs_alloc() has nothing to track. A mapping shorter than the
+ * folio therefore leaves iomap iterating over a folio it has
+ * already given up, and the next round dereferences NULL at
+ * folio_size(). Verified by disassembly: iomap_read_folio_iter+0x7c
+ * is mov (%r12),%rax with r12 = ctx->cur_folio.
+ *
+ * Spanning blocks is fine because read_folio_range does its own
+ * reading: it walks the blocks the range covers and decodes each.
+ * iomap->addr is reported to fiemap and tracing but nothing
+ * derives an I/O sector from it.
+ */
+ /*
+ * Report asks about one block; read asks about the whole range.
+ *
+ * IOMAP_REPORT is fiemap, and it wants the truth: this mapping
+ * describes the block pos falls in and nothing beyond it.
+ * Claiming the requested length told fiemap that the answer for
+ * the first block held for the entire file, so a leading hole
+ * was reported as spanning 1 MiB, iomap stepped past EOF, and a
+ * file with 512 KiB of data came back with no extents at all.
+ *
+ * Everything else keeps the full length, and must. A mapping
+ * shorter than the folio makes iomap_read_folio_iter release the
+ * folio -- nothing attached an iomap_folio_state, folio and
+ * block size both being 4096 -- and then dereference it on the
+ * next round at folio_size(). That is the NULL documented above,
+ * confirmed by disassembly, and a 3824-byte mapping is always
+ * shorter than a 4096-byte folio.
+ */
+ /*
+ * A write is bounded by the block it allocated; a read is not.
+ *
+ * The write leg allocates exactly one block -- b = pos / payload
+ * -- and then declared a mapping covering the whole requested
+ * length. iomap believes it: iomap_write_iter copies the entire
+ * range into the page cache without asking again, so a 1 MiB
+ * write allocated one block of 3808 bytes and accepted 275 more
+ * on the strength of it.
+ *
+ * Measured on 2026-09-19: 1.25 GiB written in fourteen seconds
+ * onto a 256 MiB volume, df moving by 11 MiB, dd returning
+ * success throughout. The file reached 1.41 GiB before the dirty
+ * page limit stopped it, and the allocation it needs happens at
+ * writeback, where ENOSPC has nobody to report to. That is the
+ * EFBIG generic/015 ends on, and the silent acceptance the whole
+ * filesystem exists to prevent.
+ *
+ * iomap handles a short mapping by calling back for the rest;
+ * every filesystem in the tree relies on that. The read leg is
+ * the one that cannot take it -- iomap_read_folio_iter releases
+ * a folio no iomap_folio_state is attached to and dereferences
+ * it on the next round, which is what the comment above records
+ * and disassembly confirmed -- so it keeps the full length.
+ */
+ if (flags & (IOMAP_REPORT | IOMAP_WRITE)) {
+ u64 in_block = (u64)pos - b * payload;
+
+ /*
+ * payload, not BEAMFS_DATA_INLINE_BYTES.
+ *
+ * The remainder above is taken modulo payload and the
+ * bound was taken on the constant. On a capsule volume
+ * those are 3808 and 3824, so every extent fiemap
+ * reported ran up to sixteen bytes past the block that
+ * backs it, and the next one started inside it.
+ *
+ * beamfs_block_payload says what this costs in as many
+ * words: writing the constant instead "reads a file back
+ * shifted by sixteen bytes on a capsule volume,
+ * silently". generic/473 is where it shows: the test
+ * expects [128..255] and gets [128..254].
+ */
+ iomap->length = min_t(u64, (u64)length,
+ (u64)payload - in_block);
+ } else {
+ iomap->length = length;
+ }
+
+ /*
+ * A write allocates here, not at writeback.
+ *
+ * Only lookup_phys was called, for every caller, so a write to an
+ * unallocated block was told IOMAP_HOLE and accepted into the page
+ * cache; the allocation happened later in the flusher, where an
+ * ENOSPC has nobody left to report to. A volume at 100% took a
+ * 10 MiB write at 5.5 GB/s -- nothing reached the disk, and the
+ * application was told it had.
+ *
+ * That is silent data loss, on a filesystem whose whole purpose is
+ * that data survives.
+ *
+ * It also explains the EFBIG that ended generic/015 and
+ * generic/269: dd kept writing past the end of a full 1 GiB volume
+ * until the file hit the 134480396-block ceiling of the
+ * indirection tree -- 479 GiB of file on a gigabyte of disk --
+ * because no write ever failed.
+ *
+ * ext2 draws the same line: create = flags & IOMAP_WRITE.
+ */
+ if (flags & IOMAP_WRITE) {
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+
+ /*
+ * Allocation is serialised by i_alloc_mutex, and this is
+ * an allocation site.
+ *
+ * The two other callers take it; this one did not, so two
+ * threads writing the same logical block each allocated a
+ * physical one and the second overwrote the first's
+ * pointer. The overwritten block stayed marked used with
+ * nothing referencing it -- 36 leaked out of 317559
+ * allocations under generic/344, which runs holetest with
+ * 256 threads on one sparse file. A narrow window, hit
+ * often enough by 256 threads.
+ */
+ bool fresh = false;
+
+ /*
+ * Not on a failed volume.
+ *
+ * beamfs_fail says "refusing further writes" and nothing in
+ * this file asked: a write into a block the file already had
+ * was accepted, and one that needed a block was told ENOSPC by
+ * the allocator, which answers 0 for a failed volume as for a
+ * full one. generic/361 on aarch64, 2026-10-01: "pwrite: No
+ * space left on device" on a 1 GiB volume holding 520 MiB.
+ */
+ if (beamfs_failed(inode->i_sb))
+ return -EIO;
+
+ mutex_lock(&fi->i_alloc_mutex);
+ ret = beamfs_inline_lookup_or_alloc_phys_new(inode, b, &phys,
+ &fresh);
+ mutex_unlock(&fi->i_alloc_mutex);
+
+ /*
+ * BEAMFS_IOMAP_F_FRESH: this mapping did not exist before the call.
+ *
+ * iomap_end may only give back what this call created. A
+ * block the file already owned belongs to it whether or
+ * not the write that asked for the mapping reached it, and
+ * freeing that would lose data nobody asked to lose.
+ */
+ if (ret == 0 && fresh)
+ iomap->flags |= BEAMFS_IOMAP_F_FRESH;
+ } else {
+ ret = beamfs_inline_lookup_phys(inode, b, &phys);
+ }
+ /*
+ * The last place a wrong address can still be stopped.
+ *
+ * What goes into the iomap below becomes a bio, and a bio does
+ * not ask where it is going: an address under s_data_start lands
+ * on the inode table. generic/075 put fsx's fill bytes in blocks
+ * 7 and 246 that way and left no trace in any buffer head,
+ * because iomap does not use them.
+ *
+ * Checked here rather than at each place the two lookups return a
+ * block: one gate the address must pass, and one place to read when
+ * it does not.
+ */
+ if (ret == 0 &&
+ !beamfs_phys_is_sane(inode->i_sb, inode, phys, "iomap_begin"))
+ ret = -EUCLEAN;
+
+ if (ret < 0)
+ return ret;
+
+ if (phys == 0) {
+ iomap->type = IOMAP_HOLE;
+ return 0;
+ }
+
+ iomap->type = IOMAP_MAPPED;
+ /*
+ * Physical block start. Nothing derives a sector from it for I/O --
+ * read_folio_range reads the block itself -- but fiemap and the
+ * tracepoints report it, so it has to be the real address.
+ */
+ iomap->addr = (u64)phys * BEAMFS_BLOCK_SIZE;
+
+ (void)srcmap;
+ return 0;
+}
+
+/*
+ * Give back what a short write allocated and did not use.
+ *
+ * iomap_begin allocates a block per call as it walks the mapping, and
+ * a write that stops short -- ENOSPC, a signal, an error partway --
+ * leaves the rest of them allocated and named by nothing. generic/013
+ * ends with ten such blocks in two runs, the longer of nine: one
+ * write's worth.
+ *
+ * Only what this mapping created, which BEAMFS_IOMAP_F_FRESH says. A
+ * block the file already owned belongs to it whether or not this write
+ * reached it, and freeing that loses data nobody asked to lose.
+ *
+ * Rounded up to the next block: a partially written block is kept
+ * whole, because the bytes before the stopping point are the user's.
+ */
+static int beamfs_inline_iomap_end(struct inode *inode, loff_t pos,
+ loff_t length, ssize_t written,
+ unsigned int flags, struct iomap *iomap)
+{
+ u32 payload = beamfs_block_payload(inode->i_sb);
+ u64 first_unused;
+ loff_t reached;
+
+ if (!(flags & IOMAP_WRITE) || !(iomap->flags & BEAMFS_IOMAP_F_FRESH))
+ return 0;
+ if (written < 0 || written >= length)
+ return 0;
+
+ /*
+ * The first block entirely past what the write reached. Anything
+ * from here to the end of the mapping was allocated by it and
+ * never filled.
+ */
+ reached = pos + written;
+ first_unused = DIV_ROUND_UP_ULL((u64)reached, payload);
+
+ if ((loff_t)(first_unused * payload) >= pos + length)
+ return 0;
+
+ /*
+ * Only past the end of the file.
+ *
+ * free_blocks_from frees everything from a block onwards, which
+ * is what a truncate wants and not what this does. A write that
+ * fails at offset zero on a file that already holds data would
+ * take all of it: the mapping is new, the blocks past it are
+ * not.
+ *
+ * i_size is what iomap has already set from what was written, so
+ * anything beyond it is out of the file by definition.
+ */
+ {
+ loff_t size = i_size_read(inode);
+ u64 first_past_eof = DIV_ROUND_UP_ULL((u64)size, payload);
+
+ if (first_unused < first_past_eof)
+ return 0;
+ }
+
+ mutex_lock(&BEAMFS_I(inode)->i_alloc_mutex);
+ beamfs_inline_free_blocks_from(inode, first_unused);
+ mutex_unlock(&BEAMFS_I(inode)->i_alloc_mutex);
+
+ return 0;
+}
+
+static const struct iomap_ops beamfs_inline_iomap_ops = {
+ .iomap_begin = beamfs_inline_iomap_begin,
+ .iomap_end = beamfs_inline_iomap_end,
+};
+
+/*
+ * Read one mapped range into the folio, decoding it on the way.
+ *
+ * iomap has already resolved which INLINE block backs this file range;
+ * what is left is to fetch the physical block, run the 16 RS codewords
+ * through the decoder, and copy out the slice the folio asked for. The
+ * read is synchronous, as iomap_bio_read_folio_range_sync() shows is
+ * expected, so iomap_finish_folio_read() is called before returning.
+ */
+static int beamfs_inline_read_folio_range(const struct iomap_iter *iter,
+ struct iomap_read_folio_ctx *ctx,
+ size_t len)
+{
+ struct inode *inode = iter->inode;
+ struct super_block *sb = inode->i_sb;
+ struct folio *folio = ctx->cur_folio;
+ loff_t i_size = i_size_read(inode);
+ u64 pos = (u64)iter->pos;
+ u64 end = pos + len;
+ size_t done = 0;
+ int ret = 0;
+
+ /*
+ * The range can straddle INLINE blocks, so walk them: for each,
+ * read the physical block, run its 16 RS codewords through the
+ * decoder, and copy out the slice this folio wants.
+ */
+ /*
+ * Past EOF within this range: zero it here rather than returning a
+ * second HOLE mapping. iomap releases the folio as soon as a call
+ * returns without an iomap_folio_state attached -- always the case
+ * at 4096-byte folios and blocks -- so a folio must be covered by
+ * exactly one mapping. Traced on 2026-08-29: a 1000-byte file
+ * produced MAPPED [0, 0x3e8) then HOLE [0x3e8, 0x1000), and the
+ * second iteration dereferenced the released folio.
+ */
+ if (end > (u64)i_size) {
+ u64 eof = max_t(u64, pos, (u64)i_size);
+
+ folio_zero_range(folio, offset_in_folio(folio, eof),
+ (size_t)(end - eof));
+ end = eof;
+ }
+
+ while (pos < end) {
+ u32 payload = beamfs_block_payload(inode->i_sb);
+ u64 b = div_u64(pos, payload);
+ u32 slice_offset = (u32)(pos - b * payload);
+ u32 slice_length = (u32)min_t(u64, end - pos,
+ payload - slice_offset);
+ size_t folio_off = offset_in_folio(folio, pos);
+ u64 phys = 0;
+ u8 *dst;
+
+ /*
+ * The tree read under the allocator's mutex.
+ *
+ * lookup_phys walks i_direct and the indirect blocks while
+ * writeback, truncate and eviction are free to rewrite them.
+ * Reading a pointer mid-update returns a block number that
+ * belongs to no one, or to another file once it has been
+ * reallocated. ext2 holds truncate_mutex across the same walk.
+ *
+ * Held for the lookup only, released before the read that
+ * follows: the block I/O may sleep and has no business
+ * holding up the allocator.
+ */
+ mutex_lock(&BEAMFS_I(inode)->i_alloc_mutex);
+ ret = beamfs_inline_lookup_phys(inode, b, &phys);
+ mutex_unlock(&BEAMFS_I(inode)->i_alloc_mutex);
+ if (ret < 0)
+ break;
+
+ if (phys == 0) {
+ folio_zero_range(folio, folio_off, slice_length);
+ pos += slice_length;
+ done += slice_length;
+ continue;
+ }
+
+ /*
+ * Not sb_bread, and that is the whole point.
+ *
+ * __bread_gfp adds __GFP_NOFAIL whatever mask it is given,
+ * so this read could not fail -- it looped in the
+ * allocator instead. On a machine whose page cache is full
+ * of this filesystem's own blocks, that is a machine that
+ * stops: captured 2026-09-18 with the node at 270% CPU,
+ * kswapd running, eight tasks on this exact line and
+ * nothing advancing, with generic/083 under -I crc.
+ *
+ * The cache is consulted first and never created. A block
+ * with a dirty buffer still holds the newest bytes -- the
+ * medium has the previous ones until writeback lands -- so
+ * reading past it would hand back stale data. When there
+ * is no buffer, the block is read straight into a scratch
+ * page: one bounded allocation that can fail cleanly,
+ * instead of an unbounded one that cannot fail at all.
+ *
+ * A capsule is exactly BEAMFS_BLOCK_SIZE and the scratch
+ * pool hands out exactly that, so the block lands in one
+ * piece with no bounce.
+ */
+ {
+ struct buffer_head *cached = sb_find_get_block(sb, phys);
+ u8 *raw;
+
+ if (cached) {
+ raw = beamfs_scratch_get(sb);
+ if (!raw) {
+ brelse(cached);
+ ret = -ENOMEM;
+ break;
+ }
+ lock_buffer(cached);
+ memcpy(raw, cached->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(cached);
+ brelse(cached);
+ } else {
+ raw = beamfs_scratch_get(sb);
+ if (!raw) {
+ ret = -ENOMEM;
+ break;
+ }
+ ret = bdev_rw_virt(sb->s_bdev,
+ (sector_t)phys *
+ (BEAMFS_BLOCK_SIZE >> SECTOR_SHIFT),
+ raw, BEAMFS_BLOCK_SIZE,
+ REQ_OP_READ);
+ if (ret) {
+ pr_err_ratelimited("beamfs/inline: read_folio_range: read of phys=%llu failed: %d\n",
+ (unsigned long long)phys,
+ ret);
+ beamfs_scratch_put(sb, raw);
+ ret = -EIO;
+ break;
+ }
+ }
+
+ dst = kmap_local_folio(folio, folio_off);
+ ret = beamfs_inline_decode_block_into_buf(sb, raw, phys,
+ inode, b, dst,
+ slice_offset,
+ slice_length,
+ false);
+ kunmap_local(dst);
+ beamfs_scratch_put(sb, raw);
+ if (ret < 0)
+ break;
+ }
+
+ pos += slice_length;
+ done += slice_length;
+ }
+
+ /*
+ * On failure, return without finishing the folio.
+ * iomap_read_folio_iter() propagates a non-zero return before
+ * counting the range as submitted, and iomap_read_folio() then
+ * ends the folio itself through iomap_read_end(). Calling
+ * iomap_finish_folio_read() here as well runs folio_end_read()
+ * on a folio iomap still owns -- with no iomap_folio_state
+ * attached, which is always the case at 4096-byte folios and
+ * blocks, that call is unconditional -- and the second end
+ * leaves any waiter parked in folio_wait_bit_common() forever.
+ *
+ * Observed on 2026-08-30 by the saturation protocol: a block with
+ * nine symbol errors in one subblock was correctly detected and
+ * journalled, but the read never returned EIO -- cat sat in
+ * uninterruptible sleep instead. The fail-closed half of the
+ * saturation contract was the casualty, not the detection half.
+ *
+ * iomap_bio_read_folio_range_sync() has the same shape: it
+ * returns the error and finishes nothing.
+ */
+ if (ret < 0)
+ return ret;
+
+ iomap_finish_folio_read(folio, offset_in_folio(folio, iter->pos),
+ len, 0);
+ (void)done;
+ return 0;
+}
+
+static const struct iomap_read_ops beamfs_inline_read_ops = {
+ .read_folio_range = beamfs_inline_read_folio_range,
+};
+
+/*
+ * No .readahead. iomap_readahead_iter() moves to the next folio only when
+ * offset_in_folio(cur_folio, iter->pos) reaches zero, so it assumes
+ * mappings line up with folio boundaries. INLINE mappings end on block
+ * payload boundaries, 3808 or 3824 bytes apart, and folios are 4096
+ * bytes, so that comparison never matches at the right moment:
+ * iomap_read_folio_iter() clears cur_folio once the folio is full, the
+ * transition to the next folio does not fire, and the following
+ * iteration dereferences NULL. Confirmed twice on kernel 7.1.3.
+ *
+ * readahead is optional in address_space_operations. Aligning the format
+ * to folios would mean moving parity out of the data block, which is what
+ * lets a single read correct a flip without depending on a second block --
+ * the property the filesystem exists for. Sequential reads simply go one
+ * folio at a time through read_folio.
+ *
+ * read_folio: iomap resolves which INLINE block backs each
+ * file range, then calls beamfs_inline_read_folio_range() to produce the
+ * bytes. The folio locking and uptodate accounting are iomap's; what
+ * stays here is the RS decode.
+ */
+static int beamfs_inline_read_folio(struct file *file, struct folio *folio)
+{
+ struct iomap_read_folio_ctx ctx = {
+ .ops = &beamfs_inline_read_ops,
+ .cur_folio = folio,
+ };
+
+ iomap_read_folio(&beamfs_inline_iomap_ops, &ctx, NULL);
+ return 0;
+}
+
+/* ------------------------------------------------------------------------- */
+/* iomap write path */
+/* */
+/* Same shape as the read side. iomap owns the folio lifecycle -- locking, */
+/* dirty accounting, writeback tagging -- and beamfs owns what the bytes */
+/* look like on disk: the read-modify-write cycle that decodes a block, */
+/* splices the folio's contribution in, re-encodes all 16 RS codewords and */
+/* stamps the DATA_CSUM descriptor. */
+/* ------------------------------------------------------------------------- */
+
+/*
+ * Fill a folio range for a partial write. iomap calls this when a write
+ * covers only part of a folio that is not uptodate, so the rest has to
+ * come off disk first. The contract requires a synchronous read, which
+ * is what the decode path does anyway.
+ */
+static int beamfs_inline_write_read_folio_range(const struct iomap_iter *iter,
+ struct folio *folio,
+ loff_t pos, size_t len)
+{
+ struct inode *inode = iter->inode;
+ struct super_block *sb = inode->i_sb;
+ loff_t i_size = i_size_read(inode);
+ u64 p = (u64)pos;
+ u64 end = p + len;
+ int ret = 0;
+
+ if (end > (u64)i_size) {
+ u64 eof = max_t(u64, p, (u64)i_size);
+
+ folio_zero_range(folio, offset_in_folio(folio, eof),
+ (size_t)(end - eof));
+ end = eof;
+ }
+
+ while (p < end) {
+ u32 payload = beamfs_block_payload(inode->i_sb);
+ u64 b = div_u64(p, payload);
+ u32 slice_offset = (u32)(p - b * payload);
+ u32 slice_length = (u32)min_t(u64, end - p,
+ payload - slice_offset);
+ size_t folio_off = offset_in_folio(folio, p);
+ struct buffer_head *bh;
+ u64 phys = 0;
+ u8 *dst;
+
+ /*
+ * The tree read under the allocator's mutex.
+ *
+ * lookup_phys walks i_direct and the indirect blocks while
+ * writeback, truncate and eviction are free to rewrite them.
+ * Reading a pointer mid-update returns a block number that
+ * belongs to no one, or to another file once it has been
+ * reallocated. ext2 holds truncate_mutex across the same walk.
+ *
+ * Held for the lookup only, released before the read that
+ * follows: the block I/O may sleep and has no business
+ * holding up the allocator.
+ */
+ mutex_lock(&BEAMFS_I(inode)->i_alloc_mutex);
+ ret = beamfs_inline_lookup_phys(inode, b, &phys);
+ mutex_unlock(&BEAMFS_I(inode)->i_alloc_mutex);
+ if (ret < 0)
+ break;
+
+ if (phys == 0) {
+ folio_zero_range(folio, folio_off, slice_length);
+ p += slice_length;
+ continue;
+ }
+
+ bh = sb_bread(sb, phys);
+ if (!bh) {
+ ret = -EIO;
+ break;
+ }
+
+ dst = kmap_local_folio(folio, folio_off);
+ lock_buffer(bh);
+ ret = beamfs_inline_decode_block_into_buf(sb, bh->b_data, phys, inode,
+ b, dst, slice_offset,
+ slice_length, false);
+ unlock_buffer(bh);
+ kunmap_local(dst);
+ brelse(bh);
+ if (ret < 0)
+ break;
+
+ p += slice_length;
+ }
+
+ return ret;
+}
+
+static const struct iomap_write_ops beamfs_inline_write_ops = {
+ .read_folio_range = beamfs_inline_write_read_folio_range,
+};
+
+/*
+ * Writeback: encode into bounce pages, gather the pages into bios.
+ *
+ * How ext2 and ext4 write a file back, functionally: the flusher hands
+ * the filesystem one folio after another; the filesystem resolves
+ * where each block goes, appends the folio's pages to the bio it is
+ * building while the blocks stay contiguous, and submits that bio
+ * when they stop being contiguous, when it is full, or at the end of
+ * the pass. Nothing waits per block, dozens of writes are in flight at
+ * once, the device sees requests of 128 KiB rather than 4, and a
+ * folio's writeback ends from the completion of the bio that carried
+ * it. A block is a page, so no block is ever half a folio's and half
+ * another's, and nothing is read back to be rewritten.
+ *
+ * beamfs cannot hand the folio's own pages to the device: a block
+ * carries 3808 or 3824 bytes of payload alongside 256 bytes of
+ * Reed-Solomon parity and its checksum and identity fields, so every
+ * block has to be encoded into a bounce page first. And because the
+ * payload is not 4096, a folio ends inside a block that the next folio
+ * continues.
+ *
+ * What was here before did the encode through the block device's
+ * buffer cache, one buffer_head per block, one bio per buffer_head,
+ * and took the buffer lock for each: the block a folio ended in was
+ * read back, decoded, merged, re-encoded and written, and then the
+ * next folio did the same to the same block -- waiting first on the
+ * lock, which the write in flight still held. Measured on 2026-09-22
+ * under generic/074: 2 176 467 writes of one block, 1.12 ms each, one
+ * in flight at a time, 4.8 MB/s, every boundary block written twice
+ * and decoded once; ext2 on the same device ran the same test in 17
+ * seconds against a budget beamfs could not fit in.
+ *
+ * Now the boundary block is not written until the folio that
+ * continues it has arrived: its payload waits in wb->pend, the next
+ * folio fills the rest, and the block is encoded once and queued
+ * once. Encoded pages come from a pool, not the buffer cache, so no
+ * buffer lock is held across anything that can sleep -- the rule whose
+ * breach generic/076 turned into a four-party deadlock. Blocks
+ * contiguous on the medium go into one bio of up to
+ * 32 pages, submitted when the run breaks, when it is full, or from
+ * writeback_submit at the end of the pass. Each folio carries a
+ * counter of the blocks that hold its bytes; the bio completion
+ * decrements it and finishes the folio at zero, so a folio whose two
+ * blocks travel in two bios is finished exactly once.
+ *
+ * The buffer cache still holds aliases of data blocks: the zero image
+ * a fresh allocation writes through sb_getblk, the copy a read-modify-
+ * write took through sb_bread, the copy read_folio_range consults
+ * first. Before an encoded page is queued, any alias is brought to the
+ * same bytes and made clean, under its lock, so a reader that finds it
+ * reads what the device will hold, and the block device's own flusher
+ * does not later write the stale zeros over the data.
+ */
+
+/* 32 blocks of 4 KiB: one bio of 128 KiB, the size ext4 builds. */
+#define BEAMFS_WB_MAX_BLOCKS 32
+
+/*
+ * One folio in flight. pending counts the blocks that carry bytes of
+ * this folio and have not completed; the folio is finished at zero.
+ * abandoned is set when writeback_range failed for this folio after
+ * queueing some of its blocks: iomap ends such a folio itself, and a
+ * second finish from here would end a writeback that is not ours.
+ */
+struct beamfs_wb_folio {
+ struct inode *inode;
+ struct folio *folio;
+ size_t len;
+ refcount_t pending;
+ bool abandoned;
+};
+
+/* One encoded block in a bio, and the folio or folios it came from. */
+struct beamfs_wb_block {
+ struct page *page;
+ struct beamfs_wb_folio *owner;
+ struct beamfs_wb_folio *owner2;
+};
+
+/* A bio being built: contiguous encoded blocks, submitted together. */
+struct beamfs_wb_ioend {
+ struct bio *bio;
+ struct super_block *sb;
+ unsigned int n;
+ sector_t next;
+ struct beamfs_wb_block blk[BEAMFS_WB_MAX_BLOCKS];
+};
+
+/*
+ * The state of one writeback pass, kept in wpc->wb_ctx from the first
+ * folio to writeback_submit.
+ */
+struct beamfs_wb {
+ struct super_block *sb;
+ struct beamfs_wb_ioend *ioend;
+ /* The boundary block waiting for the folio that continues it. */
+ bool have_pend;
+ u64 pend_phys;
+ u64 pend_b;
+ u32 pend_filled;
+ loff_t pend_next_pos;
+ struct inode *pend_inode;
+ struct beamfs_wb_folio *pend_owner;
+ u8 *pend_payload;
+ /* The first error a completion or a flush saw. */
+ int error;
+};
+
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 14/18] beamfs: inline data path 4/4, write side
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (12 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 13/18] beamfs: inline data path 3/4, read side Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 15/18] beamfs: add scrubber and RS benchmark Aurelien DESBRIERES
` (3 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
file_inline.c is the data path of the UNIVERSAL_INLINE protection
scheme (scheme 2), the one mkfs.beamfs formats. Each 4096-byte data
block holds sixteen RS(255,239) codewords, 3824 coded bytes and 256
bytes of parity, followed by a 16-byte tail; every read decodes the
block.
The file is 4875 lines, too large for one message to the list, so it
is split across four patches along function boundaries. It builds only
as a whole, once the Kconfig and Makefile patch is applied.
This part: writeback, ->write_iter, fsync, truncation, setattr,
fiemap, and the address_space, file and inode operation tables.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/file_inline.c | 1186 +++++++++++++++++++++++++++++++++++++++
1 file changed, 1186 insertions(+)
diff --git a/fs/beamfs/file_inline.c b/fs/beamfs/file_inline.c
index 2ea7e7e10168..cdb1dc75dc86 100644
--- a/fs/beamfs/file_inline.c
+++ b/fs/beamfs/file_inline.c
@@ -3714,3 +3714,1189 @@ struct beamfs_wb {
int error;
};
+static void beamfs_wb_folio_done(struct beamfs_wb_folio *o)
+{
+ if (!o)
+ return;
+ if (refcount_dec_and_test(&o->pending)) {
+ if (!o->abandoned)
+ iomap_finish_folio_write(o->inode, o->folio, o->len);
+ kfree(o);
+ }
+}
+
+static void beamfs_wb_end_bio(struct bio *bio)
+{
+ struct beamfs_wb_ioend *io = bio->bi_private;
+ struct beamfs_sb_info *sbi = BEAMFS_SB(io->sb);
+ bool failed = bio->bi_status != BLK_STS_OK;
+ unsigned int i;
+
+ for (i = 0; i < io->n; i++) {
+ struct beamfs_wb_block *blk = &io->blk[i];
+
+ if (failed) {
+ if (blk->owner)
+ mapping_set_error(blk->owner->inode->i_mapping, -EIO);
+ if (blk->owner2)
+ mapping_set_error(blk->owner2->inode->i_mapping, -EIO);
+ }
+ mempool_free(blk->page, sbi->s_wb_pages);
+ beamfs_wb_folio_done(blk->owner);
+ beamfs_wb_folio_done(blk->owner2);
+ }
+ bio_put(bio);
+ kfree(io);
+}
+
+static void beamfs_wb_submit_ioend(struct beamfs_wb *wb)
+{
+ struct beamfs_wb_ioend *io = wb->ioend;
+
+ if (!io)
+ return;
+ wb->ioend = NULL;
+ submit_bio(io->bio);
+}
+
+/*
+ * A page for one encoded block.
+ *
+ * Without waiting first: the pool is shared by every flusher thread,
+ * and a thread that waits on it while holding an unsubmitted bio full
+ * of pool pages is waiting for pages that only its own submission can
+ * return. So: try without waiting, then submit what is held, then
+ * wait -- what is held is on its way back by then.
+ */
+static struct page *beamfs_wb_page(struct beamfs_wb *wb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(wb->sb);
+ struct page *page;
+
+ page = mempool_alloc(sbi->s_wb_pages, GFP_NOWAIT | __GFP_NOWARN);
+ if (page)
+ return page;
+ beamfs_wb_submit_ioend(wb);
+ return mempool_alloc(sbi->s_wb_pages, GFP_NOFS);
+}
+
+/*
+ * Bring the buffer cache's alias of @phys, if there is one, to the
+ * bytes the device is about to receive, and make it clean.
+ *
+ * The lock waits for a write of the alias already in flight -- the
+ * zero image of a fresh block, most often -- so that write lands
+ * before ours, whatever the device does with two requests for the
+ * same sectors. Nothing else is held here.
+ */
+static void beamfs_wb_sync_alias(struct super_block *sb, u64 phys,
+ const void *block)
+{
+ struct buffer_head *bh = sb_find_get_block(sb, phys);
+
+ if (!bh)
+ return;
+ lock_buffer(bh);
+ memcpy(bh->b_data, block, BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(bh);
+ clear_buffer_dirty(bh);
+ unlock_buffer(bh);
+ brelse(bh);
+}
+
+/*
+ * Encode @payload into a fresh page and queue the page for @phys.
+ *
+ * Contiguous with the bio being built: appended. Otherwise, or when
+ * the bio is full, that bio goes and a new one starts here.
+ */
+static int beamfs_wb_queue(struct beamfs_wb *wb, struct inode *inode,
+ u64 b, u64 phys, const u8 *payload,
+ struct beamfs_wb_folio *owner,
+ struct beamfs_wb_folio *owner2,
+ struct writeback_control *wbc)
+{
+ struct super_block *sb = wb->sb;
+ sector_t sector = (sector_t)phys << (BEAMFS_BLOCK_SHIFT - SECTOR_SHIFT);
+ struct beamfs_wb_ioend *io;
+ struct page *page;
+ u8 *block;
+ int ret;
+
+ page = beamfs_wb_page(wb);
+ block = page_address(page);
+
+ beamfs_lay_data_payload(sb, block, payload);
+ /*
+ * The descriptor before the parity. In a capsule it is coded
+ * data: stamped after the seal, the next read corrected it back
+ * to what the page held when the parity was computed.
+ */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(sb), block, payload,
+ inode->i_ino, b);
+ ret = beamfs_seal_data_block(sb, block);
+ if (ret < 0) {
+ pr_err_ratelimited("beamfs/inline: writeback: rs_encode_region failed for block %llu: %d\n",
+ (unsigned long long)phys, ret);
+ mempool_free(page, BEAMFS_SB(sb)->s_wb_pages);
+ return ret;
+ }
+
+ beamfs_wb_sync_alias(sb, phys, block);
+
+ if (wb->ioend &&
+ (wb->ioend->n == BEAMFS_WB_MAX_BLOCKS || wb->ioend->next != sector))
+ beamfs_wb_submit_ioend(wb);
+
+ if (!wb->ioend) {
+ io = kzalloc_obj(*io, GFP_NOFS);
+ if (!io) {
+ mempool_free(page, BEAMFS_SB(sb)->s_wb_pages);
+ return -ENOMEM;
+ }
+ io->sb = sb;
+ io->bio = bio_alloc(sb->s_bdev, BEAMFS_WB_MAX_BLOCKS,
+ REQ_OP_WRITE |
+ ((wbc && wbc->sync_mode == WB_SYNC_ALL) ?
+ REQ_SYNC : 0),
+ GFP_NOFS);
+ io->bio->bi_iter.bi_sector = sector;
+ io->bio->bi_private = io;
+ io->bio->bi_end_io = beamfs_wb_end_bio;
+ io->next = sector;
+ wb->ioend = io;
+ }
+ io = wb->ioend;
+ __bio_add_page(io->bio, page, BEAMFS_BLOCK_SIZE, 0);
+ io->blk[io->n].page = page;
+ io->blk[io->n].owner = owner;
+ io->blk[io->n].owner2 = owner2;
+ io->n++;
+ io->next += BEAMFS_BLOCK_SIZE >> SECTOR_SHIFT;
+ return 0;
+}
+
+/*
+ * The payload a block holds now, for the bytes a partial write does
+ * not touch. The buffer cache's alias first, the device otherwise --
+ * not sb_bread, which would create an alias to keep coherent for a
+ * block read once.
+ */
+static int beamfs_wb_read_old(struct super_block *sb, struct inode *inode,
+ u64 b, u64 phys, u8 *payload)
+{
+ struct buffer_head *bh = sb_find_get_block(sb, phys);
+ u8 *raw = beamfs_scratch_get(sb);
+ int ret;
+
+ if (!raw) {
+ if (bh)
+ brelse(bh);
+ return -ENOMEM;
+ }
+ if (bh && buffer_uptodate(bh)) {
+ lock_buffer(bh);
+ memcpy(raw, bh->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(bh);
+ brelse(bh);
+ } else {
+ if (bh)
+ brelse(bh);
+ ret = bdev_rw_virt(sb->s_bdev,
+ (sector_t)phys << (BEAMFS_BLOCK_SHIFT - SECTOR_SHIFT),
+ raw, BEAMFS_BLOCK_SIZE, REQ_OP_READ);
+ if (ret) {
+ pr_err_ratelimited("beamfs/inline: writeback: read of block %llu for a partial write failed: %d\n",
+ (unsigned long long)phys, ret);
+ beamfs_scratch_put(sb, raw);
+ return -EIO;
+ }
+ }
+ ret = beamfs_inline_decode_block_into_buf(sb, raw, phys, inode, b,
+ payload, 0,
+ BEAMFS_DATA_INLINE_BYTES,
+ true);
+ beamfs_scratch_put(sb, raw);
+ return ret < 0 ? ret : 0;
+}
+
+/*
+ * The boundary block's continuation did not come: finish it with what
+ * the block holds beyond the bytes written, and queue it.
+ */
+static int beamfs_wb_flush_pending(struct beamfs_wb *wb,
+ struct writeback_control *wbc)
+{
+ struct beamfs_wb_folio *owner = wb->pend_owner;
+ u8 *old;
+ int ret;
+
+ if (!wb->have_pend)
+ return 0;
+ wb->have_pend = false;
+ wb->pend_owner = NULL;
+
+ old = beamfs_scratch_get(wb->sb);
+ if (!old) {
+ ret = -ENOMEM;
+ goto fail;
+ }
+ ret = beamfs_wb_read_old(wb->sb, wb->pend_inode, wb->pend_b,
+ wb->pend_phys, old);
+ if (ret == 0) {
+ memcpy(old, wb->pend_payload, wb->pend_filled);
+ ret = beamfs_wb_queue(wb, wb->pend_inode, wb->pend_b,
+ wb->pend_phys, old, owner, NULL, wbc);
+ }
+ beamfs_scratch_put(wb->sb, old);
+ if (ret == 0)
+ return 0;
+fail:
+ /*
+ * The block is lost to this pass; the folio it belonged to is
+ * told, and not finished twice.
+ */
+ if (owner) {
+ mapping_set_error(owner->inode->i_mapping, ret);
+ beamfs_wb_folio_done(owner);
+ }
+ if (!wb->error)
+ wb->error = ret;
+ return ret;
+}
+
+static struct beamfs_wb *beamfs_wb_get(struct iomap_writepage_ctx *wpc)
+{
+ struct beamfs_wb *wb = wpc->wb_ctx;
+
+ if (wb)
+ return wb;
+ wb = kzalloc_obj(*wb, GFP_NOFS);
+ if (!wb)
+ return NULL;
+ wb->sb = wpc->inode->i_sb;
+ wb->pend_payload = beamfs_scratch_get(wb->sb);
+ if (!wb->pend_payload) {
+ kfree(wb);
+ return NULL;
+ }
+ wpc->wb_ctx = wb;
+ return wb;
+}
+
+/*
+ * Map and encode one folio's range.
+ *
+ * Each slice of the range is one block. A slice that starts the block
+ * and fills it is encoded and queued at once. A slice that starts the
+ * block and stops short is the folio's last: its bytes wait in
+ * wb->pend for the folio that continues it. A slice that starts inside
+ * the block is that continuation when it matches what waits, and a
+ * partial write over an existing block otherwise, which reads the
+ * block first.
+ *
+ * The folio counts one block per slice and is finished when the last
+ * of them completes.
+ */
+static ssize_t beamfs_inline_writeback_range(struct iomap_writepage_ctx *wpc,
+ struct folio *folio, u64 pos,
+ unsigned int len, u64 end_pos)
+{
+ struct inode *inode = wpc->inode;
+ struct super_block *sb = inode->i_sb;
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ u32 payload = beamfs_block_payload(sb);
+ u64 p = pos;
+ u64 end = pos + len;
+ struct beamfs_wb *wb;
+ struct beamfs_wb_folio *owner;
+ u8 *scratch = NULL;
+ unsigned int nslices, queued = 0;
+ int ret = 0;
+
+ /*
+ * iomap_writeback_range() adds the return value to
+ * bytes_submitted only when wpc->iomap.type is not IOMAP_HOLE;
+ * left zeroed, every folio read as "nothing submitted" and iomap
+ * ended it itself, once too many.
+ */
+ wpc->iomap.type = IOMAP_MAPPED;
+ wpc->iomap.offset = pos;
+ wpc->iomap.length = len;
+ wpc->iomap.bdev = sb->s_bdev;
+
+ wb = beamfs_wb_get(wpc);
+ if (!wb)
+ return -ENOMEM;
+
+ /* One block per slice: the first block, then whole blocks. */
+ nslices = (u32)(div_u64(end - 1, payload) - div_u64(pos, payload)) + 1;
+
+ owner = kzalloc_obj(*owner, GFP_NOFS);
+ if (!owner)
+ return -ENOMEM;
+ owner->inode = inode;
+ owner->folio = folio;
+ owner->len = len;
+ refcount_set(&owner->pending, (int)nslices);
+
+ scratch = beamfs_scratch_get(sb);
+ if (!scratch) {
+ kfree(owner);
+ return -ENOMEM;
+ }
+
+ /*
+ * A boundary block waiting for another folio, or another file,
+ * is not continued by this one.
+ */
+ if (wb->have_pend &&
+ (wb->pend_inode != inode || wb->pend_next_pos != (loff_t)pos)) {
+ ret = beamfs_wb_flush_pending(wb, wpc->wbc);
+ if (ret < 0)
+ goto out;
+ }
+
+ while (p < end) {
+ u64 b = div_u64(p, payload);
+ u32 slice_offset = (u32)(p - b * payload);
+ u32 slice_length = (u32)min_t(u64, end - p,
+ payload - slice_offset);
+ size_t folio_off = offset_in_folio(folio, p);
+ u64 phys = 0;
+ u8 *src;
+
+ {
+ u64 before = 0;
+
+ mutex_lock(&fi->i_alloc_mutex);
+ (void)beamfs_inline_lookup_phys(inode, b, &before);
+ ret = beamfs_inline_lookup_or_alloc_phys(inode, b,
+ &phys);
+ mutex_unlock(&fi->i_alloc_mutex);
+ if (ret < 0)
+ break;
+ if (before && phys && before != phys)
+ pr_err("beamfs/leak: ino=%llu iblock=%llu %llu -> %llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)b,
+ (unsigned long long)before,
+ (unsigned long long)phys);
+ }
+ if (!phys) {
+ pr_err_ratelimited("beamfs/inline: writeback: no block for ino=%llu iblock=%llu\n",
+ (unsigned long long)inode->i_ino,
+ (unsigned long long)b);
+ ret = -EIO;
+ break;
+ }
+
+ if (slice_offset != 0) {
+ /* The continuation of the block that waits. */
+ if (wb->have_pend && wb->pend_b == b &&
+ wb->pend_phys == phys &&
+ wb->pend_filled == slice_offset) {
+ struct beamfs_wb_folio *first = wb->pend_owner;
+
+ src = kmap_local_folio(folio, folio_off);
+ memcpy(wb->pend_payload + slice_offset, src,
+ slice_length);
+ kunmap_local(src);
+ wb->pend_filled += slice_length;
+ wb->have_pend = false;
+ wb->pend_owner = NULL;
+ if (wb->pend_filled < payload) {
+ /*
+ * Still short: a block spanning three
+ * folios, which a 4096-byte folio
+ * cannot produce. Finished from the
+ * medium rather than deferred again,
+ * because a block has two owners here
+ * and no more.
+ */
+ ret = beamfs_wb_read_old(sb, inode, b, phys,
+ scratch);
+ if (ret < 0) {
+ beamfs_wb_folio_done(first);
+ break;
+ }
+ memcpy(scratch, wb->pend_payload,
+ wb->pend_filled);
+ ret = beamfs_wb_queue(wb, inode, b, phys,
+ scratch, first, owner,
+ wpc->wbc);
+ } else {
+ ret = beamfs_wb_queue(wb, inode, b, phys,
+ wb->pend_payload,
+ first, owner,
+ wpc->wbc);
+ }
+ if (ret < 0) {
+ beamfs_wb_folio_done(first);
+ break;
+ }
+ queued++;
+ } else {
+ /* A partial write over an existing block. */
+ if (wb->have_pend) {
+ ret = beamfs_wb_flush_pending(wb, wpc->wbc);
+ if (ret < 0)
+ break;
+ }
+ ret = beamfs_wb_read_old(sb, inode, b, phys, scratch);
+ if (ret < 0)
+ break;
+ src = kmap_local_folio(folio, folio_off);
+ memcpy(scratch + slice_offset, src, slice_length);
+ kunmap_local(src);
+ ret = beamfs_wb_queue(wb, inode, b, phys, scratch,
+ owner, NULL, wpc->wbc);
+ if (ret < 0)
+ break;
+ queued++;
+ }
+ } else if (slice_length == payload) {
+ /* A whole block: nothing to preserve, nothing to wait for. */
+ if (wb->have_pend) {
+ ret = beamfs_wb_flush_pending(wb, wpc->wbc);
+ if (ret < 0)
+ break;
+ }
+ src = kmap_local_folio(folio, folio_off);
+ memcpy(scratch, src, slice_length);
+ kunmap_local(src);
+ ret = beamfs_wb_queue(wb, inode, b, phys, scratch,
+ owner, NULL, wpc->wbc);
+ if (ret < 0)
+ break;
+ queued++;
+ } else {
+ /* The folio ends inside this block: wait for the next. */
+ if (wb->have_pend) {
+ ret = beamfs_wb_flush_pending(wb, wpc->wbc);
+ if (ret < 0)
+ break;
+ }
+ src = kmap_local_folio(folio, folio_off);
+ memcpy(wb->pend_payload, src, slice_length);
+ kunmap_local(src);
+ wb->have_pend = true;
+ wb->pend_phys = phys;
+ wb->pend_b = b;
+ wb->pend_filled = slice_length;
+ wb->pend_next_pos = (loff_t)(p + slice_length);
+ wb->pend_inode = inode;
+ wb->pend_owner = owner;
+ queued++;
+ }
+
+ p += slice_length;
+ }
+
+out:
+ beamfs_scratch_put(sb, scratch);
+
+ if (ret < 0) {
+ /*
+ * iomap ends this folio itself when the range fails.
+ * Whatever was queued for it still writes, and must not
+ * finish the folio a second time: the slices never
+ * queued are taken off the count, and the owner is
+ * marked so the last completion frees it and does no
+ * more.
+ */
+ owner->abandoned = true;
+ if (nslices > queued &&
+ refcount_sub_and_test((int)(nslices - queued), &owner->pending))
+ kfree(owner);
+ if (!wb->error)
+ wb->error = ret;
+ return ret;
+ }
+
+ (void)end_pos;
+ return (ssize_t)len;
+}
+
+/*
+ * The end of the pass: the boundary block nobody continued is finished
+ * from what the medium holds, and the bio being built goes.
+ */
+static int beamfs_inline_writeback_submit(struct iomap_writepage_ctx *wpc,
+ int error)
+{
+ struct beamfs_wb *wb = wpc->wb_ctx;
+ int ret;
+
+ if (!wb)
+ return error;
+ ret = beamfs_wb_flush_pending(wb, wpc->wbc);
+ beamfs_wb_submit_ioend(wb);
+ if (!error)
+ error = ret < 0 ? ret : wb->error;
+ beamfs_scratch_put(wb->sb, wb->pend_payload);
+ kfree(wb);
+ wpc->wb_ctx = NULL;
+ return error;
+}
+
+static const struct iomap_writeback_ops beamfs_inline_writeback_ops = {
+ .writeback_range = beamfs_inline_writeback_range,
+ .writeback_submit = beamfs_inline_writeback_submit,
+};
+
+/* ------------------------------------------------------------------------- */
+/* writepages */
+/* */
+/* iomap_writepages walks the dirty folios and hands each range to */
+/* beamfs_inline_writeback_range, which maps and allocates under the */
+/* per-inode i_alloc_mutex and encodes into bounce pages; */
+/* beamfs_inline_writeback_submit ends the pass. See the writeback comment */
+/* above. */
+/* ------------------------------------------------------------------------- */
+static int beamfs_inline_writepages(struct address_space *mapping,
+ struct writeback_control *wbc)
+{
+ struct iomap_writepage_ctx wpc = {
+ .inode = mapping->host,
+ .wbc = wbc,
+ .ops = &beamfs_inline_writeback_ops,
+ };
+
+ return iomap_writepages(&wpc);
+}
+
+/* ------------------------------------------------------------------------- */
+/* write_iter entry point */
+/* ------------------------------------------------------------------------- */
+static ssize_t beamfs_inline_file_write_iter(struct kiocb *iocb,
+ struct iov_iter *from)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ loff_t before;
+ ssize_t ret;
+
+ /*
+ * generic_write_checks first, and it is not optional.
+ *
+ * It is what moves ki_pos to i_size when the file was opened
+ * O_APPEND, and it enforces the rlimit and the s_maxbytes bound
+ * along the way. Without it every append wrote at whatever
+ * position the descriptor happened to hold, which for a fresh
+ * open is zero: three four-byte appends left a four-byte file
+ * reading CCCC, and xfstests generic/069 reported "maybe corrupt
+ * O_APPEND" with one file holding twelve megabytes that belonged
+ * to its siblings.
+ *
+ * generic_file_write_iter calls it, which is why filesystems
+ * built on that never notice. iomap_file_buffered_write sits
+ * below it and expects the caller to have done the checks --
+ * xfs_file_write_checks exists for this, and says so in a
+ * comment: "that assigns ki_pos for O_APPEND".
+ */
+ /*
+ * inode_lock, like every other buffered write in the tree.
+ *
+ * generic_file_write_iter takes it for ext2; ext4 and XFS take
+ * it by hand. Without it a truncate -- do_truncate holds
+ * inode_lock through setattr -- runs while this write is still
+ * populating the page cache of the same inode: i_size is written
+ * by both sides in no order, truncate_setsize drops folios this
+ * write is still creating, and the block tree is freed under a
+ * mapping iomap_begin has already handed out.
+ *
+ * generic/464 makes that the common case: sixteen processes doing
+ * pwrite -ftc on two hundred files chosen at random.
+ *
+ * i_alloc_mutex serialises the tree walks. It does not serialise
+ * the write against the truncate; that is what the inode lock is
+ * for.
+ */
+ inode_lock(inode);
+ ret = generic_write_checks(iocb, from);
+ if (ret <= 0)
+ goto out;
+
+ /*
+ * The set-user-ID and set-group-ID bits, and the times, before
+ * the data. generic_file_write_iter does this through
+ * file_remove_privs; iomap_file_buffered_write sits below it and
+ * leaves it to the caller, as XFS does in xfs_file_write_checks.
+ */
+ ret = kiocb_modified(iocb);
+ if (ret)
+ goto out;
+
+ before = i_size_read(inode);
+
+ ret = iomap_file_buffered_write(iocb, from, &beamfs_inline_iomap_ops,
+ &beamfs_inline_write_ops, NULL);
+ if (ret <= 0)
+ goto out;
+
+ /*
+ * iomap_write_iter() states the division of labour outright:
+ * "Update the in-memory inode size after copying the data into the
+ * page cache. It's up to the file system to write the updated size
+ * to disk." It grows i_size and raises IOMAP_F_SIZE_CHANGED; the
+ * inode does not reach the medium unless someone here says so.
+ *
+ * Returning its result directly meant a write that extended a file
+ * without a following fsync had the new size in stat and the old
+ * one on disk. xfstests generic/169: write 5 bytes, fsync, write 5
+ * more, unmount -- the file comes back 5 bytes long. The first
+ * half of that test passes only because every write there is
+ * fsynced.
+ *
+ * generic_write_sync last, as fuse does: it honours O_SYNC and
+ * O_DSYNC on the kiocb, so a caller that asked for durability gets
+ * it, and one that did not pays nothing.
+ */
+ if (i_size_read(inode) != before)
+ inode_set_ctime_current(inode);
+ inode_set_mtime_to_ts(inode, current_time(inode));
+ mark_inode_dirty(inode);
+
+out:
+ inode_unlock(inode);
+ /*
+ * Outside the lock, as ext4 does: it may wait on the device and
+ * has no business holding the inode while it does.
+ */
+ if (ret > 0)
+ ret = generic_write_sync(iocb, ret);
+ return ret;
+}
+
+/* ------------------------------------------------------------------------- */
+/* Public ops structures */
+/* ------------------------------------------------------------------------- */
+
+const struct address_space_operations beamfs_inline_aops = {
+ .read_folio = beamfs_inline_read_folio,
+ .writepages = beamfs_inline_writepages,
+ /*
+ * iomap_dirty_folio, not filemap_dirty_folio.
+ *
+ * filemap's marks the folio dirty and stops there. iomap's also
+ * allocates the iomap_folio_state and calls
+ * iomap_set_range_dirty, which is what iomap_find_dirty_range
+ * later reads to decide what to write back.
+ *
+ * Without it the folio is dirty to the VFS and clean to iomap:
+ * iomap_writeback_folio calls folio_start_writeback, finds no
+ * dirty range to hand to ->writeback_range, submits nothing --
+ * and nothing ever ends the writeback it just started. The folio
+ * stays that way for the life of the mount, and the next process
+ * to touch the file waits in folio_wait_writeback forever.
+ *
+ * Found through xfstests: rm sat in truncate_inode_partial_folio
+ * under beamfs_evict_inode for nine hours while the tracing said
+ * every folio that reached writeback_range was accounted for --
+ * correctly, because the one that mattered never got there.
+ *
+ * gfs2 declares the same, which is the shortest way to see it.
+ */
+ .dirty_folio = iomap_dirty_folio,
+ /* Required once the folio state is iomap's: the kernel warns at
+ * compaction time without it.
+ */
+ .migrate_folio = filemap_migrate_folio,
+ .release_folio = iomap_release_folio,
+ .invalidate_folio = iomap_invalidate_folio,
+};
+
+/*
+ * Same as the non-inline path: generic_file_fsync() went away with
+ * sync_mapping_buffers() and i_private_list. Metadata buffer_heads
+ * owned by the inode are flushed through mmb_fsync().
+ */
+static int beamfs_inline_fsync(struct file *file, loff_t start, loff_t end,
+ int datasync)
+{
+ return simple_fsync(file, start, end, datasync);
+}
+
+const struct file_operations beamfs_inline_file_operations = {
+ .llseek = generic_file_llseek,
+ .read_iter = generic_file_read_iter,
+ .write_iter = beamfs_inline_file_write_iter,
+ .mmap = generic_file_mmap,
+ .fsync = beamfs_inline_fsync,
+ .splice_read = filemap_splice_read,
+ /*
+ * splice_write is what copy_file_range falls back to.
+ *
+ * The VFS has no .copy_file_range here and no .remap_file_range,
+ * so vfs_copy_file_range reaches do_splice_direct -- which needs
+ * an output side. Without it the syscall returns EINVAL, and fsx
+ * stops on its first COPY operation, taking generic/075 with it.
+ *
+ * iter_file_splice_write goes through write_iter, so the RS
+ * encode and the append handling apply to spliced data exactly as
+ * they do to written data. ext2 declares the same pair, which is
+ * why it never had this gap.
+ */
+ .splice_write = iter_file_splice_write,
+};
+
+/* ------------------------------------------------------------------------- */
+/* Truncate support */
+/* */
+/* The allocator reaches every level: lookup_or_alloc_phys has allocation */
+/* sites for direct, indirect, double and triple indirect, so the write */
+/* path is not capped at 524 blocks. Measured 2026-08-20 on a freshly */
+/* formatted volume: a 64 MiB file, about 17500 blocks and well into double */
+/* indirect, was written and read back intact at 24 MB/s with no */
+/* EOPNOTSUPP. Capacity per file is BEAMFS_MAX_IBLOCK_TINDIRECT blocks of */
+/* payload: about 479 GiB at 3824 bytes per block, 477 GiB at 3808. */
+/* */
+/* beamfs_inline_free_blocks_from walks the same four levels, so a */
+/* truncate frees everything past the new end of the file. */
+/* ------------------------------------------------------------------------- */
+
+/*
+ * beamfs_inline_free_blocks_from - free disk blocks at logical iblock
+ * >= b_first_freed.
+ *
+ * Mirror of beamfs_free_data_blocks (super.c) restricted to a starting
+ * logical block, used for truncate-down and by iomap_end to give back
+ * what a short write allocated. Walks direct[b_first_freed..N-1], then
+ * the single indirect block, then the double and triple indirect trees
+ * through beamfs_free_ind_range. If b_first_freed <= BEAMFS_DIRECT_BLOCKS
+ * the indirect block itself is freed; otherwise individual indirect slots
+ * are zeroed and the indirect block is kept.
+ *
+ * Callers hold inode_lock and i_alloc_mutex: this rewrites the tree the
+ * allocator walks.
+ */
+static void beamfs_inline_free_blocks_from(struct inode *inode,
+ u64 b_first_freed)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ unsigned int i;
+
+ /* --- Direct blocks --- */
+ for (i = b_first_freed; i < BEAMFS_DIRECT_BLOCKS; i++) {
+ u64 blk = le64_to_cpu(fi->i_direct[i]);
+
+ if (blk) {
+ beamfs_free_block(sb, blk, inode);
+ fi->i_direct[i] = 0;
+ }
+ }
+
+ /* --- Single indirect block --- */
+ if (fi->i_indirect) {
+ u64 indirect_blk = le64_to_cpu(fi->i_indirect);
+ struct buffer_head *ibh = NULL;
+ __le64 *ptrs;
+ u64 nptrs = BEAMFS_INDIRECT_PTRS;
+ u64 slot_first;
+ u64 j;
+
+ ibh = sb_bread(sb, indirect_blk);
+ if (!ibh) {
+ pr_err_ratelimited("beamfs/inline: truncate: failed to read indirect block %llu\n",
+ (unsigned long long)indirect_blk);
+ return;
+ }
+ /*
+ * Verify before trusting the pointers. Under CRC this turns a
+ * flipped pointer that lands in range and on an allocated block
+ * -- the residual left open by the bounds check above -- into a
+ * clean failure instead of someone else's data. Under RS a copy
+ * is decoded and what it corrects is journalled, but the
+ * pointers are read as the medium holds them: a correctable
+ * flip is reported, not repaired.
+ */
+ if (beamfs_ind_parity_verify(sb, ibh)) {
+ brelse(ibh);
+ pr_err_ratelimited("beamfs/inline: truncate: indirect block %llu failed parity, subtree left allocated\n",
+ (unsigned long long)indirect_blk);
+ return;
+ }
+ ptrs = (__le64 *)ibh->b_data;
+
+ if (b_first_freed >= BEAMFS_DIRECT_BLOCKS)
+ slot_first = b_first_freed - BEAMFS_DIRECT_BLOCKS;
+ else
+ slot_first = 0;
+
+ /*
+ * Under the buffer lock, as the install side is.
+ *
+ * Zeroing a pointer is a store into a shared indirect
+ * block, no different from installing one: the flusher
+ * can be submitting that same buffer while the loop runs,
+ * and a slot cleared halfway through the write reaches
+ * the disk as neither the old value nor zero. The three
+ * install sites take the lock; the two free sites did not.
+ *
+ * beamfs_free_block sleeps -- write_bitmap_block takes
+ * lock_buffer on a bitmap block -- so the pointers are
+ * collected under the lock first and freed after it, one
+ * pass each, rather than sleeping with the buffer held.
+ */
+
+ {
+ u64 *doomed;
+ u64 n_doomed = 0;
+
+ doomed = kvmalloc_array(nptrs, sizeof(*doomed),
+ GFP_NOFS);
+ if (!doomed) {
+ brelse(ibh);
+ return;
+ }
+
+ lock_buffer(ibh);
+ for (j = slot_first; j < nptrs; j++) {
+ u64 blk = le64_to_cpu(ptrs[j]);
+
+ if (blk) {
+ doomed[n_doomed++] = blk;
+ beamfs_tc_clear(sb, ibh->b_blocknr, (u32)j);
+ ptrs[j] = 0;
+ }
+ }
+ beamfs_ind_parity_update(sb, ibh, inode);
+ unlock_buffer(ibh);
+ mmb_mark_buffer_dirty(ibh,
+ &BEAMFS_I(inode)->i_metadata_bhs);
+ /*
+ * And the inode, so the list gets flushed.
+ *
+ * mmb_mark_buffer_dirty puts the buffer on the inode's
+ * metadata list, and mmb_sync empties that list -- from
+ * write_inode, which the VFS calls only for an inode it
+ * believes is dirty. Installing a pointer into a parent block
+ * changes no field of the inode, so nothing marked it, and
+ * both buffers stayed in memory with the pointer in them.
+ *
+ * generic/013 loses fourteen blocks that way: allocated,
+ * placed in a slot, and the block holding the slot never
+ * written -- "indirect block N has no parity written yet".
+ */
+ mark_inode_dirty(inode);
+ brelse(ibh);
+
+ for (j = 0; j < n_doomed; j++)
+ beamfs_free_block(sb, doomed[j], inode);
+ kvfree(doomed);
+ }
+
+ /* If we freed the entire indirect range, drop the indirect
+ * block itself.
+ */
+ if (slot_first == 0) {
+ beamfs_free_block(sb, indirect_blk, inode);
+ fi->i_indirect = 0;
+ }
+ }
+
+ /*
+ * Double and triple indirect. The allocator reaches both, so a
+ * truncate stopping at single indirect leaves every block a file
+ * held past 2 MiB allocated with nothing left pointing at it.
+ *
+ * skip counts data blocks to preserve within each subtree: zero
+ * once the truncation point is below the level, otherwise the
+ * offset into it. A subtree entirely below the point is skipped
+ * whole, which is why the walk returns its span.
+ */
+ /*
+ * The truncation point goes in as an absolute logical index, and
+ * the pointer is cleared only if the walk reports the whole
+ * subtree gone. Deriving that from the truncation point instead
+ * was how a partially freed subtree ended up with a null parent
+ * pointer and its surviving blocks orphaned.
+ */
+ if (fi->i_dindirect &&
+ beamfs_free_ind_range(sb, le64_to_cpu(fi->i_dindirect), 2,
+ BEAMFS_MAX_IBLOCK_INDIRECT, b_first_freed, 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, b_first_freed, inode))
+ fi->i_tindirect = 0;
+
+ mark_inode_dirty(inode);
+}
+
+/*
+ * beamfs_inline_zero_tail_block - zero user bytes [zero_offset .. end of
+ * payload) in INLINE disk block b, via
+ * RMW + RS re-encode.
+ *
+ * Used when truncate-down lands inside a block: the surviving block must
+ * be valid (RS-encoded) with stale tail bytes zeroed. The block is
+ * decoded into scratch, the tail is memset to zero, and the block is
+ * re-encoded and synced.
+ *
+ * Caller holds inode_lock. i_alloc_mutex is taken around the lookup;
+ * the read-modify-write itself runs under the buffer lock.
+ */
+static int beamfs_inline_zero_tail_block(struct inode *inode, u64 b,
+ u32 zero_offset)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ struct buffer_head *bh;
+ u8 *scratch;
+ u64 phys = 0;
+ int ret;
+
+ if (zero_offset >= BEAMFS_DATA_INLINE_BYTES)
+ return 0;
+
+ {
+ u64 before = 0;
+
+ mutex_lock(&fi->i_alloc_mutex);
+ (void)beamfs_inline_lookup_phys(inode, b, &before);
+ ret = beamfs_inline_lookup_or_alloc_phys(inode, b, &phys);
+ /*
+ * The same gate: this one reads the block and writes it
+ * back, so a wrong address here overwrites metadata with
+ * a zeroed tail.
+ */
+ if (ret == 0 &&
+ !beamfs_phys_is_sane(inode->i_sb, inode, phys, "zero_tail"))
+ ret = -EUCLEAN;
+ mutex_unlock(&fi->i_alloc_mutex);
+ if (ret < 0)
+ return ret;
+ if (before && phys && before != phys)
+ pr_err("beamfs/leak: zero_tail ino=%llu iblock=%llu %llu -> %llu\n",
+ (unsigned long long)inode->i_ino, (unsigned long long)b,
+ (unsigned long long)before,
+ (unsigned long long)phys);
+ }
+ if (phys == 0)
+ return 0; /* HOLE: nothing to zero, sparse semantics */
+
+ scratch = beamfs_scratch_get(sb);
+ if (!scratch)
+ return -ENOMEM;
+
+ /*
+ * zero_tail is part of the RMW write path (truncate-induced tail
+ * clear). rmw_path=true so any flip detected on disk is journalled
+ * with RMW_NEUTRALISED, as writeback's read of a partial block is.
+ */
+ bh = sb_bread(sb, phys);
+ if (!bh) {
+ pr_err_ratelimited("beamfs/inline: zero_tail: sb_bread phys=%llu failed\n",
+ (unsigned long long)phys);
+ beamfs_scratch_put(sb, scratch);
+ return -EIO;
+ }
+ /*
+ * Lock bh for the full RMW transit: readers copy this block under
+ * its lock, and writeback brings the same buffer up to date under it
+ * (beamfs_wb_sync_alias), so none of them sees it half rewritten.
+ */
+ lock_buffer(bh);
+
+ ret = beamfs_inline_decode_block_into_buf(sb, bh->b_data, phys, inode, b,
+ scratch, 0,
+ BEAMFS_DATA_INLINE_BYTES,
+ true);
+ if (ret < 0) {
+ unlock_buffer(bh);
+ brelse(bh);
+ beamfs_scratch_put(sb, scratch);
+ return ret;
+ }
+
+ memset(scratch + zero_offset, 0,
+ BEAMFS_DATA_INLINE_BYTES - zero_offset);
+
+ beamfs_lay_data_payload(sb, (u8 *)bh->b_data, scratch);
+ /* The descriptor before the parity, as in beamfs_wb_queue. */
+ beamfs_inline_stamp_tail_pad(BEAMFS_SB(inode->i_sb),
+ (u8 *)bh->b_data, scratch,
+ (unsigned long long)inode->i_ino, b);
+
+ ret = beamfs_seal_data_block(sb, (u8 *)bh->b_data);
+ if (ret < 0) {
+ pr_err_ratelimited("beamfs/inline: zero_tail: rs_encode_region failed: %d\n",
+ ret);
+ unlock_buffer(bh);
+ brelse(bh);
+ beamfs_scratch_put(sb, scratch);
+ return ret;
+ }
+
+ mark_buffer_dirty(bh);
+ unlock_buffer(bh);
+ ret = sync_dirty_buffer(bh);
+ brelse(bh);
+ beamfs_scratch_put(sb, scratch);
+ return ret;
+}
+
+/*
+ * beamfs_inline_setattr - VFS setattr hook for INLINE inodes.
+ *
+ * Handles ATTR_SIZE (truncate up and down). Truncate-up extends i_size
+ * sparsely (HOLE handling in read_folio returns zero for unallocated
+ * blocks). Truncate-down frees disk blocks beyond the new size and
+ * zeros the partial trailing bytes in the surviving block.
+ *
+ * Other attribute changes (mode, owner, times) are forwarded to
+ * setattr_copy + mark_inode_dirty.
+ */
+static int beamfs_inline_setattr(const struct mnt_idmap *idmap,
+ struct dentry *dentry, struct iattr *attr)
+{
+ struct inode *inode = d_inode(dentry);
+ int ret;
+
+ ret = setattr_prepare(idmap, dentry, attr);
+ if (ret)
+ return ret;
+
+ if (attr->ia_valid & ATTR_SIZE) {
+ loff_t old_size = i_size_read(inode);
+ loff_t new_size = attr->ia_size;
+
+ ret = inode_newsize_ok(inode, new_size);
+ if (ret)
+ return ret;
+
+ if (new_size < old_size) {
+ u64 b_first_freed;
+ u32 tail_off;
+ u32 payload = beamfs_block_payload(inode->i_sb);
+
+ /* Drop pagecache beyond new_size before freeing
+ * the underlying disk blocks.
+ */
+ truncate_setsize(inode, new_size);
+
+ /*
+ * Logical block index of the first block to be
+ * fully freed: ceil(new_size / payload).
+ */
+ b_first_freed = DIV_ROUND_UP_ULL((u64)new_size, payload);
+
+ /* If new_size is not block-aligned, the surviving
+ * last block has stale user bytes beyond new_size.
+ * Zero them via RMW + RS re-encode.
+ */
+ div_u64_rem((u64)new_size, payload, &tail_off);
+ if (tail_off != 0 && new_size > 0) {
+ u64 b_last_kept = div_u64((u64)new_size, payload);
+
+ ret = beamfs_inline_zero_tail_block(inode,
+ b_last_kept,
+ tail_off);
+ if (ret)
+ return ret;
+ }
+
+ /*
+ * The same mutex the allocator takes.
+ *
+ * Truncate walks the same indirection tree that
+ * lookup_or_alloc_phys writes into, and took no
+ * lock at all. The trace caught it: pid 3212
+ * freeing block 0xe09 at 196.057356 between pid
+ * 3112 allocating it at 196.042955 and allocating
+ * it again at 196.058203 -- one writer's pointer
+ * overwritten by another's, and the block left
+ * marked used with nothing referencing it.
+ *
+ * generic/464 runs sixteen processes doing
+ * pwrite -ftc against two hundred files chosen at
+ * random, so a truncate on one file races an
+ * allocation on another constantly. The leak
+ * varied from 1 to 734 blocks between runs, which
+ * is what a race looks like when you count it.
+ */
+ mutex_lock(&BEAMFS_I(inode)->i_alloc_mutex);
+ beamfs_inline_free_blocks_from(inode, b_first_freed);
+ mutex_unlock(&BEAMFS_I(inode)->i_alloc_mutex);
+ } else if (new_size > old_size) {
+ /* Sparse extension: just adjust i_size. read_folio
+ * returns zero for unallocated (HOLE) blocks.
+ */
+ truncate_setsize(inode, new_size);
+ }
+
+ /*
+ * A truncate changes the file, so it changes mtime, and it
+ * changes the inode, so it changes ctime.
+ *
+ * setattr_copy below only carries the times the VFS asked
+ * for, and truncate asks for none: the filesystem is
+ * expected to set them itself, the way it does for a write.
+ * generic/313 reports all four cases -- ctime and mtime,
+ * shrinking and growing -- and says so in as many words.
+ *
+ * Both are set even when the size did not actually move,
+ * because setattr_prepare has already accepted the request
+ * and POSIX makes no exception for a truncate to the
+ * current length.
+ */
+ inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
+ }
+
+ setattr_copy(idmap, inode, attr);
+ mark_inode_dirty(inode);
+ return 0;
+}
+
+/*
+ * fiemap for the INLINE scheme.
+ *
+ * beamfs_fiemap in file.c calls iomap_fiemap with beamfs_iomap_ops --
+ * the legacy scheme=5 map, where a logical block is 4096 bytes and disk
+ * offset is file offset. Under INLINE a logical block is 3824 bytes
+ * inside a 4096-byte physical one, so that map describes a geometry
+ * this file does not have: generic/473 saw data starting at block 136
+ * where it had written at 128, a drift of 272 bytes per block.
+ *
+ * What this reports is one extent per block, and that is not a
+ * limitation of the code. fiemap merges two extents when
+ * addr + length == next addr; here addr advances by 4096 while length
+ * is at most the block's payload, 3808 or 3824 bytes, so the test never
+ * holds and never can. The mapping from file offset to disk offset is
+ * not affine, which is the price of putting each block's parity inside
+ * the block: 288 or 272 bytes of every 4096 hold the parity and the
+ * checks that protect the rest.
+ *
+ * So generic/473 still fails, and it fails for a reason worth stating
+ * rather than hiding: a tool asking where a file lives now gets the
+ * truth, block by block, instead of confident nonsense.
+ */
+static int beamfs_inline_fiemap(struct inode *inode,
+ struct fiemap_extent_info *fieinfo,
+ u64 start, u64 len)
+{
+ int ret;
+ loff_t i_size;
+
+ inode_lock(inode);
+ i_size = i_size_read(inode);
+ /* iomap_fiemap rejects len == 0; keep the call valid for an
+ * empty file, where it returns no extents.
+ */
+ if (i_size == 0)
+ i_size = 1;
+ len = min_t(u64, len, i_size);
+ ret = iomap_fiemap(inode, fieinfo, start, len,
+ &beamfs_inline_iomap_ops);
+ inode_unlock(inode);
+ return ret;
+}
+
+const struct inode_operations beamfs_inline_inode_operations = {
+ .getattr = beamfs_getattr,
+ .setattr = beamfs_inline_setattr,
+ .fiemap = beamfs_inline_fiemap,
+};
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 15/18] beamfs: add scrubber and RS benchmark
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (13 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 14/18] beamfs: inline data path 4/4, write side Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 16/18] Documentation: filesystems: add beamfs Aurelien DESBRIERES
` (2 subsequent siblings)
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
beamfs corrects on read, so a block nobody reads accumulates upsets
until it is past the correction radius. scrub.c runs one kernel thread
per mounted volume, beamfs-scrub/<dev>, that walks the allocated
blocks at a bounded pace and decodes each one, so that drift is found
while it is still correctable. Its pace and counters are in
/sys/fs/beamfs/<dev>/, and the scrub_interval_ms module parameter sets
the pace for volumes mounted later.
rsbench.c times the codec alone: reading
/sys/kernel/debug/beamfs/rs_bench encodes and decodes in memory and
reports nanoseconds per block; rs_bench_iters sets the iteration
count.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/rsbench.c | 261 ++++++++
fs/beamfs/scrub.c | 1454 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1715 insertions(+)
create mode 100644 fs/beamfs/rsbench.c
create mode 100644 fs/beamfs/scrub.c
diff --git a/fs/beamfs/rsbench.c b/fs/beamfs/rsbench.c
new file mode 100644
index 000000000000..c154dd04d498
--- /dev/null
+++ b/fs/beamfs/rsbench.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- Reed-Solomon micro-benchmark
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * What the codec costs, with no block layer underneath it.
+ *
+ * Every performance figure the harness produces so far is a
+ * filesystem figure: it contains the medium, the emulator and the
+ * codec together, and cannot say which of the three it is measuring.
+ * On the lab hardware that ambiguity is total -- beamfs writes at
+ * 1.8 MB/s on a USB stick and 1.5 MB/s on a SSD capable of 700, so
+ * the medium is plainly not the limit, but nothing said whether the
+ * remainder was TCG emulation or Reed-Solomon.
+ *
+ * This encodes and decodes in memory, in a loop, and reports
+ * nanoseconds per block. Subtracting it from the filesystem figure
+ * leaves everything else. A reviewer asking "how much of the overhead
+ * is the error correction" gets a number rather than an argument.
+ *
+ * Reading /sys/kernel/debug/beamfs/rs_bench runs the benchmark and
+ * returns the result; the iteration count is settable alongside it.
+ * Running on read is unusual for debugfs, and deliberate: a benchmark
+ * that needs a write to arm and a read to collect has a window in
+ * which two callers interleave, and this has no state to protect.
+ */
+
+#include <linux/fs.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+#include <linux/ktime.h>
+#include <linux/random.h>
+#include "beamfs.h"
+
+/* Blocks per run. Enough to swamp timer granularity under TCG, where
+ * one block takes tens of microseconds; small enough that the read
+ * returns while an operator is still watching.
+ */
+#define BEAMFS_RSBENCH_DEFAULT_ITERS 1000
+#define BEAMFS_RSBENCH_MAX_ITERS 1000000
+
+static struct dentry *beamfs_debugfs_root;
+static u32 beamfs_rsbench_iters = BEAMFS_RSBENCH_DEFAULT_ITERS;
+
+/*
+ * One encode and one decode over a block's payload: 3824 bytes, as
+ * BEAMFS_DATA_INLINE_SUBBLOCKS full-length RS(255,239) codewords.
+ *
+ * The decode runs on undamaged data. That is the case that matters:
+ * every read pays it, whether or not anything is wrong, because the
+ * syndromes have to be computed before the codec can say there is
+ * nothing to correct. Correction itself is rarer and costs more, but
+ * it is not what sets the steady-state read cost.
+ */
+static int beamfs_rsbench_run(u32 iters, u64 *enc_ns, u64 *dec_ns)
+{
+ u8 *data, *parity;
+ int *results;
+ u8 *positions;
+ ktime_t t0;
+ u32 i;
+ int ret = 0;
+
+ data = kvmalloc(BEAMFS_DATA_INLINE_BYTES, GFP_KERNEL);
+ parity = kvmalloc((size_t)BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY,
+ GFP_KERNEL);
+ results = kcalloc(BEAMFS_DATA_INLINE_SUBBLOCKS, sizeof(*results),
+ GFP_KERNEL);
+ positions = kcalloc((size_t)BEAMFS_DATA_INLINE_SUBBLOCKS *
+ (BEAMFS_RS_PARITY / 2), sizeof(*positions),
+ GFP_KERNEL);
+ if (!data || !parity || !results || !positions) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ get_random_bytes(data, BEAMFS_DATA_INLINE_BYTES);
+
+ t0 = ktime_get();
+ for (i = 0; i < iters; i++) {
+ ret = beamfs_rs_encode_region(data, BEAMFS_SUBBLOCK_DATA,
+ parity, BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+ if (ret)
+ goto out;
+ cond_resched();
+ }
+ *enc_ns = ktime_to_ns(ktime_sub(ktime_get(), t0));
+
+ t0 = ktime_get();
+ for (i = 0; i < iters; i++) {
+ ret = beamfs_rs_decode_region(data, BEAMFS_SUBBLOCK_DATA,
+ parity, BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "bench");
+ if (ret < 0)
+ goto out;
+ cond_resched();
+ }
+ *dec_ns = ktime_to_ns(ktime_sub(ktime_get(), t0));
+ ret = 0;
+
+out:
+ kfree(positions);
+ kfree(results);
+ kvfree(parity);
+ kvfree(data);
+ return ret;
+}
+
+/*
+ * How long a burst each layout survives.
+ *
+ * Reported rather than asserted: a number in a file anyone can read
+ * beats a BUG_ON nobody triggers, and the answer is a property of the
+ * code, not of this volume.
+ */
+static void beamfs_rsbench_burst(struct seq_file *m)
+{
+ unsigned int subs = BEAMFS_DATA_INLINE_SUBBLOCKS;
+ size_t dlen = BEAMFS_SUBBLOCK_DATA;
+ u8 *blk, *par, *saved;
+ unsigned int burst;
+ unsigned int plain_limit = 0, woven_limit = 0;
+ size_t i;
+
+ blk = kmalloc(dlen * subs, GFP_KERNEL);
+ saved = kmalloc(dlen * subs, GFP_KERNEL);
+ par = kmalloc((size_t)subs * BEAMFS_RS_PARITY, GFP_KERNEL);
+ if (!blk || !saved || !par)
+ goto out;
+
+ for (i = 0; i < dlen * subs; i++)
+ saved[i] = (u8)(i * 31 + 7);
+
+ /*
+ * Walk the burst length up until each layout stops correcting.
+ * The first failure is the limit.
+ */
+ for (burst = 1; burst <= 160; burst++) {
+ int rc;
+
+ /* Contiguous. */
+ memcpy(blk, saved, dlen * subs);
+ beamfs_rs_encode_region(blk, dlen, par, BEAMFS_RS_PARITY,
+ dlen, subs);
+ for (i = 0; i < burst; i++)
+ blk[100 + i] ^= 0xff;
+ rc = beamfs_rs_decode_region(blk, dlen, par, BEAMFS_RS_PARITY,
+ dlen, subs, NULL, NULL, 0,
+ "rsbench");
+ if (rc >= 0 && plain_limit == burst - 1)
+ plain_limit = burst;
+
+ /* Interleaved. */
+ memcpy(blk, saved, dlen * subs);
+ beamfs_rs_encode_woven(blk, par, BEAMFS_RS_PARITY, dlen,
+ subs);
+ for (i = 0; i < burst; i++)
+ blk[100 + i] ^= 0xff;
+ rc = beamfs_rs_decode_woven(blk, par, BEAMFS_RS_PARITY, dlen,
+ subs, NULL, "rsbench");
+ if (rc >= 0 && woven_limit == burst - 1)
+ woven_limit = burst;
+ }
+
+ seq_printf(m, "BURST_BYTES_CONTIGUOUS=%u\n", plain_limit);
+ seq_printf(m, "BURST_BYTES_INTERLEAVED=%u\n", woven_limit);
+
+out:
+ kfree(par);
+ kfree(saved);
+ kfree(blk);
+}
+
+static int beamfs_rsbench_show(struct seq_file *m, void *v)
+{
+ u64 enc_ns = 0, dec_ns = 0;
+ u32 iters = READ_ONCE(beamfs_rsbench_iters);
+ u64 bytes;
+ int ret;
+
+ ret = beamfs_rsbench_run(iters, &enc_ns, &dec_ns);
+ if (ret) {
+ seq_printf(m, "error %d\n", ret);
+ return 0;
+ }
+
+ bytes = (u64)iters * BEAMFS_DATA_INLINE_BYTES;
+
+ /*
+ * Key=value, one per line: the harness parses this, and a
+ * format that needs a parser written for it is a format that
+ * will be parsed wrongly.
+ */
+ seq_printf(m, "ITERS=%u\n", iters);
+ seq_printf(m, "BLOCK_BYTES=%u\n", BEAMFS_DATA_INLINE_BYTES);
+ seq_printf(m, "SUBBLOCKS=%u\n", BEAMFS_DATA_INLINE_SUBBLOCKS);
+ seq_printf(m, "ENCODE_NS_TOTAL=%llu\n", enc_ns);
+ seq_printf(m, "DECODE_NS_TOTAL=%llu\n", dec_ns);
+ seq_printf(m, "ENCODE_NS_PER_BLOCK=%llu\n", enc_ns / iters);
+ seq_printf(m, "DECODE_NS_PER_BLOCK=%llu\n", dec_ns / iters);
+
+ /*
+ * And what a burst costs, either way.
+ *
+ * Nine consecutive bad bytes laid out contiguously all land in
+ * one codeword and it is lost; interleaved they land one per
+ * codeword and the block comes back whole. Asked here of the
+ * kernel's own codec, the one that decodes the volume.
+ */
+ beamfs_rsbench_burst(m);
+
+ /*
+ * Throughput in KiB/s, computed here rather than left to the
+ * caller: integer division in a shell script is where units go
+ * wrong.
+ */
+ if (enc_ns)
+ seq_printf(m, "ENCODE_KIB_PER_SEC=%llu\n",
+ div64_u64(bytes * 1000000ULL, enc_ns * 1024ULL / 1000ULL));
+ if (dec_ns)
+ seq_printf(m, "DECODE_KIB_PER_SEC=%llu\n",
+ div64_u64(bytes * 1000000ULL, dec_ns * 1024ULL / 1000ULL));
+
+ return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(beamfs_rsbench);
+
+/*
+ * beamfs_debugfs_init -- create /sys/kernel/debug/beamfs.
+ *
+ * Module-wide rather than per-superblock: the codec is a property of
+ * the build, not of a mounted volume, and the number it produces does
+ * not change with what is mounted.
+ */
+void beamfs_debugfs_init(void)
+{
+ beamfs_debugfs_root = debugfs_create_dir("beamfs", NULL);
+ if (IS_ERR(beamfs_debugfs_root)) {
+ beamfs_debugfs_root = NULL;
+ return;
+ }
+
+ debugfs_create_file("rs_bench", 0444, beamfs_debugfs_root, NULL,
+ &beamfs_rsbench_fops);
+ debugfs_create_u32("rs_bench_iters", 0644, beamfs_debugfs_root,
+ &beamfs_rsbench_iters);
+}
+
+void beamfs_debugfs_exit(void)
+{
+ debugfs_remove_recursive(beamfs_debugfs_root);
+ beamfs_debugfs_root = NULL;
+}
diff --git a/fs/beamfs/scrub.c b/fs/beamfs/scrub.c
new file mode 100644
index 000000000000..41193283413f
--- /dev/null
+++ b/fs/beamfs/scrub.c
@@ -0,0 +1,1454 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- background scrubber
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * beamfs corrects on read. A block nobody reads therefore accumulates
+ * upsets until it passes the eight-symbol correction radius and becomes
+ * unrecoverable, with nothing having reported the drift on the way.
+ * Cold data is the case this filesystem exists for -- an archive
+ * written once and read years later -- so waiting for a reader is
+ * waiting for the failure.
+ *
+ * The scrubber steps through the inode table and, for each regular
+ * file nobody is writing, follows the file's own pointers and decodes
+ * every data block; an indirect block is checked against its parity,
+ * where the volume keeps one, before its pointers are followed. At
+ * most one block named by the error-budget wear map is checked per
+ * step as well.
+ *
+ * A corrected block is written back, whole, under the buffer lock:
+ * each upset is correctable, their sum is not. It is written only
+ * while the owner still maps the block at the same place, asked under
+ * i_alloc_mutex, only if the buffer still holds the bytes the decode
+ * started from, and never on a read-only or frozen volume. A block
+ * with an uncorrectable codeword is not written and is retried on a
+ * later pass. Blocks reached through the wear map are checked and
+ * reported, never written: nothing says whose they are.
+ *
+ * Rate rather than priority. A thread that read every block on the
+ * device at RT priority would starve the workload it is meant to
+ * protect; a bounded interval gives the guarantee that matters -- a
+ * full sweep completes in a knowable time -- without that cost. The
+ * thread sleeps the interval after each file block it checks and after
+ * each inode-table slot it steps past, except at the end of a pass,
+ * where it sleeps a second. The interval is settable through sysfs, so
+ * an operator sizes the sweep against the expected upset rate;
+ * beamfs_scrub_pace then adjusts it once per pass, halving it after a
+ * pass that corrected anything and adding an eighth of the value set
+ * back after a quiet one, never below 1 ms and never above the value
+ * set.
+ */
+
+#include <linux/fs.h>
+#include <linux/kthread.h>
+#include <linux/delay.h>
+#include <linux/buffer_head.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+#include <linux/kobject.h>
+#include "beamfs.h"
+#include <linux/bio.h>
+#include <linux/math64.h>
+
+#define BEAMFS_SCRUB_DEFAULT_INTERVAL_MS 100
+
+/*
+ * The pace a volume starts at, in milliseconds between blocks.
+ *
+ * Per-volume it is settable through sysfs once mounted, which is no
+ * help for a volume something else mounts: xfstests makes and mounts
+ * its scratch device itself, and the scrubber is running on it before
+ * anything outside the kernel can say otherwise. Worse, on a machine
+ * whose root filesystem is beamfs, a sweep runs under every measurement
+ * taken there -- up to ten blocks a second at the default pace, and
+ * faster after any pass that needed a correction.
+ *
+ * As a parameter it is settable before the mount rather than after:
+ * beamfs.scrub_interval_ms= on the kernel command line, or through
+ * /sys/module/beamfs/parameters/ for the mounts still to come. 0 parks
+ * the scrubber, which is a measurement decision and not a safe
+ * default: correction on read is unaffected, but nothing then goes
+ * looking for the block nobody reads.
+ */
+static unsigned int scrub_interval_ms = BEAMFS_SCRUB_DEFAULT_INTERVAL_MS;
+module_param(scrub_interval_ms, uint, 0644);
+MODULE_PARM_DESC(scrub_interval_ms,
+ "milliseconds between blocks for newly mounted volumes (0 parks the scrubber)");
+
+/*
+ * Read @phys from the medium into @buf, around the block device's cache.
+ *
+ * The confirming read of an uncorrectable subblock asks whether the
+ * damage is on the medium. It used to ask by clearing the uptodate bit
+ * of the shared buffer and reading into it again: whatever the buffer
+ * held that the medium did not -- a writer's change not yet written,
+ * a block that had become another file's -- was replaced by what the
+ * disk held, in a buffer other paths read and may still write back. A
+ * private page and a bio ask the same question and change nothing
+ * anyone else sees.
+ */
+static int beamfs_scrub_read_medium(struct super_block *sb, u64 phys, u8 *buf)
+{
+ struct page *page;
+ struct bio *bio;
+ int ret;
+
+ page = alloc_page(GFP_NOFS);
+ if (!page)
+ return -ENOMEM;
+ bio = bio_alloc(sb->s_bdev, 1, REQ_OP_READ | REQ_SYNC, GFP_NOFS);
+ bio->bi_iter.bi_sector = phys << (sb->s_blocksize_bits - SECTOR_SHIFT);
+ __bio_add_page(bio, page, BEAMFS_BLOCK_SIZE, 0);
+ ret = submit_bio_wait(bio);
+ bio_put(bio);
+ if (!ret)
+ memcpy(buf, page_address(page), BEAMFS_BLOCK_SIZE);
+ __free_page(page);
+ return ret;
+}
+
+/*
+ * Decode what the medium holds for @phys into @r2, in the layout the
+ * first decode settled on. @buf is a scratch page.
+ */
+static noinline_for_stack void beamfs_scrub_confirm(struct super_block *sb,
+ u64 phys, u8 *buf,
+ bool woven, int *r2)
+{
+ u8 p2[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+ unsigned int k;
+ int mr;
+
+ mr = beamfs_scrub_read_medium(sb, phys, buf);
+ if (mr) {
+ /*
+ * No page: nothing confirmed, nothing reported. The
+ * medium refusing the read is damage.
+ */
+ for (k = 0; k < BEAMFS_DATA_INLINE_SUBBLOCKS; k++)
+ r2[k] = mr == -ENOMEM ? 0 : -1;
+ return;
+ }
+ /*
+ * The confirming read takes the layout the first decode settled
+ * on. Reading it the other way would confirm damage that is not
+ * there, which is the whole reason this second read exists.
+ */
+ if (woven)
+ beamfs_rs_decode_woven(buf + BEAMFS_CAPSULE_DATA_OFF,
+ buf + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ r2, "sweep-confirm");
+ else
+ beamfs_rs_decode_region(buf, BEAMFS_SUBBLOCK_TOTAL,
+ buf + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ r2, p2, BEAMFS_RS_PARITY / 2,
+ "sweep-confirm");
+}
+
+/* The first logical block under each root: single, double, triple. */
+static u64 beamfs_scrub_root_base(unsigned int top)
+{
+ if (top == 1)
+ return BEAMFS_DIRECT_BLOCKS;
+ if (top == 2)
+ return BEAMFS_MAX_IBLOCK_INDIRECT;
+ return BEAMFS_MAX_IBLOCK_DINDIRECT;
+}
+
+/*
+ * Is @blk still the block at @depth on the path to logical block @base
+ * of @inode, in the tree whose root is at depth @top?
+ *
+ * The walk copies an indirect block's pointers and descends into them
+ * one at a time, sleeping the pace interval between leaves; a child
+ * freed and allocated again in the meantime is another file's block,
+ * and its "pointers" are whatever that file put there. Asked under
+ * i_alloc_mutex, from the root the inode holds now, reading the raw
+ * pointers: an identity check, not a verify.
+ */
+static bool beamfs_scrub_node_current(struct inode *inode, unsigned int top,
+ unsigned int depth, u64 base, u64 blk)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ u64 off = base - beamfs_scrub_root_base(top);
+ u64 node, span = 1;
+ unsigned int d;
+
+ lockdep_assert_held(&fi->i_alloc_mutex);
+ if (top == 1)
+ node = le64_to_cpu(fi->i_indirect);
+ else if (top == 2)
+ node = le64_to_cpu(fi->i_dindirect);
+ else
+ node = le64_to_cpu(fi->i_tindirect);
+
+ for (d = 1; d < top; d++)
+ span *= BEAMFS_INDIRECT_PTRS;
+
+ for (d = top; d > depth && node; d--) {
+ struct buffer_head *bh;
+ u64 slot = div64_u64(off, span);
+
+ if (slot >= BEAMFS_INDIRECT_PTRS)
+ return false;
+ off -= slot * span;
+ span = div64_u64(span, BEAMFS_INDIRECT_PTRS);
+ bh = sb_bread(sb, node);
+ if (!bh)
+ return false;
+ lock_buffer(bh);
+ node = le64_to_cpu(((__le64 *)bh->b_data)[slot]);
+ unlock_buffer(bh);
+ brelse(bh);
+ }
+ return node == blk;
+}
+
+/*
+ * Is @phys still logical block @iblock of @inode?
+ *
+ * Asked under i_alloc_mutex, which every path that frees one of the
+ * inode's blocks or installs a new one holds: truncate through both
+ * callers of beamfs_inline_free_blocks_from, eviction around
+ * beamfs_free_data_blocks, allocation throughout.
+ *
+ * A reserved inode is answered from its own pointers. Its blocks are
+ * placed by mkfs and never change hands: iget marks it immutable and
+ * the allocator skips it. The canary at inode 2 sits just below the
+ * data region, where lookup_phys refuses any file's pointer as
+ * corrupted: asked there, the sweep skipped the canary it used to
+ * check and logged "corrupted direct pointer ino=2" on every volume,
+ * every pass.
+ */
+static bool beamfs_scrub_leaf_current(struct inode *inode, u64 iblock,
+ u64 phys)
+{
+ u64 now = 0;
+
+ lockdep_assert_held(&BEAMFS_I(inode)->i_alloc_mutex);
+ if (beamfs_ino_is_reserved(inode->i_ino))
+ return iblock < BEAMFS_DIRECT_BLOCKS &&
+ le64_to_cpu(BEAMFS_I(inode)->i_direct[iblock]) == phys;
+ return beamfs_inline_lookup_phys(inode, iblock, &now) == 0 &&
+ now == phys;
+}
+
+/*
+ * Read @phys and copy the whole block into @orig. The buffer is
+ * returned held, or an ERR_PTR when it cannot be read.
+ */
+static struct buffer_head *beamfs_scrub_read_copy(struct super_block *sb,
+ u64 phys, u8 *orig)
+{
+ struct buffer_head *bh = sb_bread(sb, phys);
+
+ if (!bh)
+ return ERR_PTR(-EIO);
+ /*
+ * Decode into staging rather than in place: the
+ * buffer_head is shared through the block device page
+ * cache, and a scrub has no business modifying what a
+ * concurrent reader sees.
+ */
+ /*
+ * Under the buffer lock, or the snapshot is of two blocks.
+ *
+ * Writers hold lock_buffer across memcpy plus
+ * rs_encode_region -- the payload changes, then the
+ * sixteen parity fields catch up. Copying without the
+ * lock catches that halfway and hands the decoder a
+ * payload from after the write with parity from before,
+ * which is uncorrectable by construction.
+ *
+ * That is what the sweep kept reporting: block 1110
+ * failing all sixteen codewords while fsck called the
+ * same volume clean, and blocks 1062 and 1631 failing
+ * only the subblocks that held data. Twenty such reports
+ * appeared the moment new data blocks started being
+ * encoded on allocation, because there were simply more
+ * writes to collide with.
+ *
+ * The lock is held for a 4096-byte copy and nothing
+ * else. The decode runs afterwards, on the copy.
+ */
+ 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(orig, (u8 *)bh->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(bh);
+ return bh;
+}
+
+/*
+ * The snapshot beamfs_scrub_check_block decodes: @phys copied into
+ * @orig, under @owner's allocation mutex when there is an owner, and
+ * only while @owner still names @phys at @iblock (-ESTALE
+ * otherwise). The lock and the unlock are unconditional on each path,
+ * so that neither a reader nor a checker has to pair two "if (owner)".
+ */
+static struct buffer_head *beamfs_scrub_snapshot(struct super_block *sb,
+ u64 phys, struct inode *owner,
+ u64 iblock, u8 *orig)
+{
+ struct beamfs_inode_info *fi;
+ struct buffer_head *bh;
+
+ if (!owner)
+ return beamfs_scrub_read_copy(sb, phys, orig);
+ fi = BEAMFS_I(owner);
+ mutex_lock(&fi->i_alloc_mutex);
+ if (beamfs_scrub_leaf_current(owner, iblock, phys))
+ bh = beamfs_scrub_read_copy(sb, phys, orig);
+ else
+ bh = ERR_PTR(-ESTALE);
+ mutex_unlock(&fi->i_alloc_mutex);
+ return bh;
+}
+
+/*
+ * beamfs_scrub_check_block -- decode one block, report what it found,
+ * and repair it while it is still what the caller took it for.
+ *
+ * @corrected receives the number of subblocks that needed correction.
+ * @owner and @iblock name the file and the logical block @phys is
+ * believed to be. Returns 0 if the block decodes, -EUCLEAN if any
+ * subblock exceeded the correction radius on the medium, -ESTALE if
+ * @owner no longer maps @iblock to @phys, or a negative errno on read
+ * failure.
+ *
+ * Without an owner -- the wear step, which visits whatever is
+ * allocated -- the block is checked and reported and never written:
+ * nothing says what it is.
+ *
+ * It used to work on the physical block alone, and that was wrong. The
+ * sweep walks a file's leaves from a copy of its pointers taken
+ * seconds earlier, and a leaf freed and allocated again as another
+ * file's indirect block was decoded as a data block of the first file:
+ * a nearly empty pointer array is within eight bytes of a zero
+ * codeword, so it was "corrected" to zero and written back. Measured
+ * on generic/083: 17 of 17 write-backs and uncorrectable verdicts of
+ * the sweep over 8 trials were on blocks that had changed hands and
+ * held pointers; one of them, indirect block 15011, took 15012 and its
+ * 17 children with it. Another trial rewrote a data block of another
+ * file over its 16 subblocks. The owner is now asked, under its
+ * allocation mutex, before the block is read and again before it is
+ * written, and the write happens only if the buffer still holds
+ * exactly the bytes that were decoded.
+ */
+int beamfs_scrub_check_block(struct super_block *sb, u64 phys,
+ unsigned int *corrected,
+ struct inode *owner, u64 iblock)
+{
+ struct buffer_head *bh = NULL;
+ /*
+ * Off the stack: submit-checklist allows a frame 512 bytes, and
+ * with these three on it the frame was 656. The allocation is
+ * small and GFP_NOFS; a sweep that cannot get it skips the block
+ * and comes back to it.
+ */
+ struct beamfs_scrub_scratch {
+ int results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ int r2[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ u8 positions[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+ } *sc;
+ int *results, *r2;
+ u8 *positions;
+ u8 *staging, *orig;
+ unsigned int i, n_corrected = 0;
+ int ret = 0, dret;
+ /*
+ * Which layout the first decode settled on, so the confirming
+ * read takes the same one.
+ */
+ bool woven = false;
+ bool confirmed = false, repairable = true, wrote = false;
+
+ *corrected = 0;
+
+ sc = kzalloc_obj(*sc, GFP_NOFS);
+ if (!sc)
+ return -ENOMEM;
+ results = sc->results;
+ r2 = sc->r2;
+ positions = sc->positions;
+ /*
+ * Both pages before any mutex: the pool waits when it is empty,
+ * and nothing may wait on it holding the owner's allocation
+ * mutex. orig keeps the bytes the decode started from, to tell
+ * at write-back whether anybody wrote the buffer since.
+ */
+ staging = beamfs_scratch_get(sb);
+ orig = beamfs_scratch_get(sb);
+ if (!staging || !orig) {
+ ret = -ENOMEM;
+ goto out_free;
+ }
+
+ /*
+ * Still the owner's block, at the place the walk took it from?
+ * Asked under the mutex every free of the owner's blocks holds,
+ * and the snapshot is taken inside it: the block cannot change
+ * hands between the answer and the copy.
+ */
+ bh = beamfs_scrub_snapshot(sb, phys, owner, iblock, orig);
+ if (IS_ERR(bh)) {
+ ret = PTR_ERR(bh);
+ bh = NULL;
+ goto out_free;
+ }
+ memcpy(staging, orig, BEAMFS_BLOCK_SIZE);
+
+ /*
+ * Which layout, when the sweep does not know what it is holding.
+ *
+ * The callers with an owner know: the children of a level-one
+ * indirect block and an inode's i_direct are data blocks, and on
+ * an interleaved volume they are capsules. The wear step walks
+ * the wear map and visits whatever is allocated -- a capsule, an
+ * indirect block, a directory block -- with nothing to say
+ * which.
+ *
+ * So: try the layout the volume uses for data, and fall back to
+ * the alternating one. A block does not decode cleanly under
+ * both by accident -- sixteen codewords of sixteen parity bytes
+ * each agreeing on the wrong symbols is not something that
+ * happens -- so whichever succeeds is the one it was written in.
+ *
+ * Without this the sweep reported "block 17725 subblock 6/16
+ * uncorrectable" on a block that was intact, which is worse than
+ * missing damage: it condemns what it was meant to protect.
+ */
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE) {
+ woven = true;
+ ret = beamfs_rs_decode_woven(staging + BEAMFS_CAPSULE_DATA_OFF,
+ staging + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, "sweep");
+ if (ret != -EUCLEAN)
+ goto decoded;
+
+ /*
+ * Not a capsule: an indirect or directory block. From
+ * the snapshot, not from the buffer, which is not
+ * locked now and may have been written since.
+ */
+ woven = false;
+ memcpy(staging, orig, BEAMFS_BLOCK_SIZE);
+ }
+
+ ret = beamfs_rs_decode_region(staging, BEAMFS_SUBBLOCK_TOTAL,
+ staging + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "sweep");
+decoded:
+ dret = ret;
+ ret = 0;
+ if (dret < 0 && dret != -EUCLEAN) {
+ ret = dret;
+ goto out_brelse;
+ }
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ if (results[i] > 0) {
+ n_corrected++;
+ beamfs_log_rs_event_flagged(sb,
+ phys,
+ positions + (size_t)i * (BEAMFS_RS_PARITY / 2),
+ (unsigned int)results[i],
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ /*
+ * The sweep is where the budget earns its keep: it
+ * visits blocks nobody reads, which are the ones
+ * that drift unobserved.
+ */
+ beamfs_budget_record(sb, phys,
+ (unsigned int)results[i]);
+ } else if (results[i] < 0) {
+ /*
+ * Confirm by re-reading before crying wolf. The
+ * sweep reads a block via sb_bread on the block
+ * device while the owning inode may still be
+ * flushing, and can catch data updated with parity
+ * not yet written, or the reverse -- uncorrectable
+ * on a block that is sound once the flush lands.
+ * Every first-boot report of this kind decoded
+ * clean offline, byte-identical on disk. A real
+ * defect survives a re-read; a half-flushed block
+ * does not.
+ *
+ * Once per block, into a private page: see
+ * beamfs_scrub_read_medium. A block with an
+ * uncorrectable subblock is not written back
+ * this pass; the next sweep retries from a
+ * fresh read.
+ */
+ repairable = false;
+ if (!confirmed) {
+ confirmed = true;
+ beamfs_scrub_confirm(sb, phys, staging,
+ woven, r2);
+ }
+ if (r2[i] >= 0)
+ continue;
+ /*
+ * Say which block and which codeword. An
+ * uncorrectable subblock is the event this
+ * filesystem exists to report, and a line that
+ * names neither is an alarm nobody can follow.
+ */
+ pr_err_ratelimited("beamfs: sweep: block %llu subblock %u/%u uncorrectable\n",
+ (unsigned long long)phys, i,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+ /*
+ * Past the radius. Journalled with the same shape
+ * the read path uses, so a forensic pass cannot tell
+ * whether a reader or the scrubber found it -- which
+ * is correct: the damage is the same either way.
+ */
+ beamfs_log_rs_event_flagged(sb,
+ phys,
+ NULL, 0, BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ ret = -EUCLEAN;
+ }
+ }
+
+ /*
+ * Write the repair back, or it was not a repair.
+ *
+ * The decoder puts corrected data in staging and corrected parity
+ * straight into the buffer; without this the buffer was released
+ * and both were discarded. The block on disk stayed damaged, the
+ * next sweep found the same errors, and the counter climbed
+ * forever -- measured: 120 corrections of one block over 119
+ * sweeps, none of which fixed anything.
+ *
+ * That is the failure this filesystem exists to prevent. Each
+ * upset is individually correctable; what kills the data is their
+ * sum. A scrubber that detects without repairing lets them
+ * accumulate to the ninth, and the ninth is unrecoverable.
+ *
+ * Written under the buffer lock so a concurrent reader sees the
+ * block whole, and only when something was corrected: an
+ * untouched block must not be dirtied, or a quiet volume would
+ * rewrite itself endlessly and wear the medium for nothing.
+ *
+ * And only to the block the owner still names at @iblock, only
+ * if the buffer still holds the bytes the decode started from --
+ * a writer that came in between is right and the scrubber is
+ * not -- and never on a volume that is read-only or frozen.
+ */
+ if (n_corrected && repairable && owner && !sb_rdonly(sb) &&
+ sb_start_write_trylock(sb)) {
+ mutex_lock(&BEAMFS_I(owner)->i_alloc_mutex);
+ if (beamfs_scrub_leaf_current(owner, iblock, phys)) {
+ lock_buffer(bh);
+ if (!memcmp(bh->b_data, orig, BEAMFS_BLOCK_SIZE)) {
+ /*
+ * The whole block. staging holds the
+ * corrected codewords in their on-disk
+ * layout now -- data and parity both, at
+ * a 255-byte stride -- because that is
+ * what decode_rs8 produces and what the
+ * block has to go back as.
+ */
+ memcpy((u8 *)bh->b_data, staging,
+ BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(bh);
+ wrote = true;
+ }
+ unlock_buffer(bh);
+ if (wrote)
+ mark_buffer_dirty(bh);
+ }
+ mutex_unlock(&BEAMFS_I(owner)->i_alloc_mutex);
+ sb_end_write(sb);
+ }
+
+ *corrected = n_corrected;
+out_brelse:
+ brelse(bh);
+out_free:
+ beamfs_scratch_put(sb, orig);
+ beamfs_scratch_put(sb, staging);
+ kfree(sc);
+ return ret;
+}
+
+/*
+ * Walk one inode's data blocks, decoding each.
+ *
+ * The bitmap says a block is in use; it does not say what it holds.
+ * Sweeping it directly meant handing indirect blocks and directory
+ * blocks to the RS decoder, which has no parity to find in them, and
+ * journalling every one as uncorrectable -- 24 false entries in the
+ * first minutes of the first run, in the same journal that exists to
+ * be forensic evidence. A scrubber that cries wolf is worse than none.
+ *
+ * Following the inode's own pointers is what the read path does, so
+ * the scrubber sees exactly the blocks a reader would decode and
+ * nothing else. Indirect blocks are not decoded as data: each is
+ * checked against its own parity, when the volume keeps an
+ * indirect-parity region, before its pointers are copied and followed.
+ *
+ * Returns the number of data blocks visited.
+ */
+static u64 beamfs_scrub_walk_level(struct super_block *sb, u64 blk,
+ unsigned int depth,
+ struct beamfs_sb_info *sbi,
+ struct inode *inode, unsigned int top,
+ u64 base)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ u64 nptrs = BEAMFS_BLOCK_SIZE / sizeof(__le64);
+ struct buffer_head *ibh;
+ __le64 *ptrs;
+ u64 visited = 0;
+ u64 span = 1;
+ u64 j;
+ unsigned int d;
+ int bad;
+
+ if (!blk || !beamfs_block_is_allocated(sb, blk))
+ return 0;
+
+ /* Logical blocks under one pointer of this block. */
+ for (d = 1; d < depth; d++)
+ span *= nptrs;
+
+ /*
+ * Copy the pointers and let the buffer go, for the same reason
+ * the inode is snapshotted above: the loop sleeps for the pace
+ * interval after every block, and a pointer into a block-device
+ * buffer does not survive that reliably. Reading ptrs[j] after a
+ * recycle gives whatever now occupies the page, and the sweep
+ * then reads and releases blocks at random -- which is how brelse
+ * came to be called on an already-free buffer.
+ *
+ * One page copied per indirect block visited, against a sweep
+ * that walks at one block per interval. Allocated before the
+ * mutex below, which the allocator must not be waited on under.
+ */
+ ptrs = kmalloc(BEAMFS_BLOCK_SIZE, GFP_NOFS);
+ if (!ptrs)
+ return 0;
+
+ /*
+ * Still this file's block, at this place in its tree?
+ *
+ * @blk comes from a copy of its parent taken when the walk
+ * started on the parent, which may be minutes ago; a block
+ * freed and allocated again since is another file's, and its
+ * verify below reports that file's state as this one's damage
+ * -- or its pointers are walked as this file's leaves. Asked
+ * under i_alloc_mutex, and the verify and the copy are done
+ * inside it, so the answer holds for both.
+ */
+ mutex_lock(&fi->i_alloc_mutex);
+ if (!beamfs_scrub_node_current(inode, top, depth, base, blk)) {
+ mutex_unlock(&fi->i_alloc_mutex);
+ kfree(ptrs);
+ return 0;
+ }
+ ibh = sb_bread(sb, blk);
+ if (!ibh) {
+ mutex_unlock(&fi->i_alloc_mutex);
+ kfree(ptrs);
+ return 0;
+ }
+
+ /*
+ * Check the indirect block itself before reading it.
+ *
+ * The sweep walked every data block an inode owns and never
+ * looked at the blocks holding the pointers, which carry their
+ * own parity scheme -- ind_parity, not the sixteen RS codewords
+ * a data block uses. A corrupted pointer array is worse than a
+ * corrupted data block: it loses everything below it.
+ */
+ bad = beamfs_ind_parity_verify_medium(sb, ibh);
+ if (!bad) {
+ /*
+ * The copy under the buffer lock, for the reason
+ * beamfs_scrub_check_block gives about data blocks and
+ * that was never applied to the blocks holding the
+ * pointers.
+ *
+ * Every site in file_inline.c that installs a pointer
+ * holds lock_buffer across the store. Copying without it
+ * catches the block halfway, and the walk below then
+ * descends into slots holding neither the old pointer
+ * nor the new one.
+ *
+ * The verify above stays outside the lock on purpose. It
+ * reads the parity region through beamfs_bread and takes
+ * a scratch page from a pool of thirty-two, and holding
+ * a buffer lock across that is the shape super.c
+ * documents at the scratch pool: three tasks stalled 191
+ * seconds in __bread_gfp under beamfs_ind_parity_verify,
+ * the machine at 330% CPU, never returning. A verify
+ * that reads a half-written block reports a block that
+ * is sound as uncorrectable, which is noise in dmesg; a
+ * walk over a half-written copy loses pointers, which is
+ * not. The lock goes where the damage is.
+ */
+ lock_buffer(ibh);
+ memcpy(ptrs, ibh->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(ibh);
+ }
+ mutex_unlock(&fi->i_alloc_mutex);
+ brelse(ibh);
+ if (bad) {
+ pr_err_ratelimited("beamfs: sweep: indirect block %llu fails its parity\n",
+ (unsigned long long)blk);
+ sbi->s_scrub_uncorrectable++;
+ kfree(ptrs);
+ return 0;
+ }
+
+ for (j = 0; j < nptrs && !kthread_should_stop() && inode->i_nlink;
+ j++) {
+ u64 child = le64_to_cpu(ptrs[j]);
+ unsigned int corrected = 0;
+ int ret;
+
+ if (!child)
+ continue;
+
+ if (depth > 1) {
+ visited += beamfs_scrub_walk_level(sb, child,
+ depth - 1, sbi,
+ inode, top,
+ base + j * span);
+ continue;
+ }
+
+ if (!beamfs_block_is_allocated(sb, child))
+ continue;
+
+ /*
+ * The copy is as old as this walk, and the leaf is not
+ * trusted for what it says: check_block asks the owner
+ * whether logical block @base + j is still @child, and
+ * skips it with -ESTALE when it is not.
+ */
+ ret = beamfs_scrub_check_block(sb, child, &corrected,
+ inode, base + j);
+ if (ret == -ESTALE)
+ continue;
+ if (ret == -EUCLEAN)
+ pr_err_ratelimited("beamfs: sweep: block %llu is a leaf under indirect %llu (depth %u, slot %llu)\n",
+ (unsigned long long)child,
+ (unsigned long long)blk, depth,
+ (unsigned long long)j);
+ visited++;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+ if (ret == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+
+ kfree(ptrs);
+ return visited;
+}
+
+static void beamfs_scrub_one_inode(struct super_block *sb, unsigned long ino)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_inode *raw;
+ struct buffer_head *bh;
+ unsigned long inodes_per_block, block, offset;
+ struct beamfs_inode snap;
+ unsigned int i;
+ umode_t mode;
+
+ /*
+ * Skip an inode somebody is writing.
+ *
+ * The sweep reads the inode table and the data blocks straight
+ * off the block device, in parallel with the writeback that is
+ * updating them. It can see a block with its payload written and
+ * its parity not yet, or the reverse, and report uncorrectable on
+ * something no reader would ever see in that state. Every such
+ * report checked out clean once the writer was done: fsck passed
+ * the image, the offline decoder found nothing, and the bytes on
+ * disk were identical to the ones that had just been rejected.
+ *
+ * Re-reading the block confirms the ones that settle between two
+ * reads, but a file being appended to continuously -- a system log
+ * during a test run -- is never settled, and both reads land in
+ * the same window.
+ *
+ * ilookup returns the inode only if it is already in cache; it
+ * never reads from disk, so it cannot recurse into the filesystem.
+ * An inode nobody has open is not being written and is safe to
+ * sweep. One that is dirty or under writeback is skipped and comes
+ * round again on the next pass, which is what a scrubber should do
+ * with a moving target.
+ */
+ {
+ struct inode *vi = ilookup(sb, ino);
+
+ if (vi) {
+ unsigned long st = inode_state_read_once(vi);
+ bool busy = (st & (I_DIRTY_ALL | I_SYNC |
+ I_NEW | I_FREEING | I_WILL_FREE)) ||
+ mapping_writably_mapped(vi->i_mapping);
+
+ iput(vi);
+ if (busy)
+ return;
+ }
+ }
+
+ 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 = sb_bread(sb, block);
+ if (!bh)
+ return;
+ raw = (struct beamfs_inode *)bh->b_data + offset;
+
+ /*
+ * Take a copy of what the sweep needs, then let the buffer go.
+ *
+ * The loops below sleep for the pace interval between blocks --
+ * hundreds of milliseconds, sometimes seconds -- and reading
+ * raw->i_direct[i] after each nap means holding a pointer into a
+ * block-device buffer across all of it. The buffer can be
+ * recycled in that window, and then the pointers read out of it
+ * are whatever now occupies the page: brelse on a buffer that was
+ * already free, from beamfs_scrub_check_block, WARNING at
+ * fs/buffer.c:1141.
+ *
+ * Ninety-six bytes of pointers, copied once. The sweep works from
+ * a snapshot, which is what it wants anyway -- a file being
+ * rewritten underneath it is not something the scrubber has to
+ * follow.
+ */
+ memcpy(&snap, raw, sizeof(snap));
+ brelse(bh);
+ bh = NULL;
+ raw = &snap;
+ mode = le16_to_cpu(snap.i_mode);
+
+ /*
+ * Regular files only. A directory block has its own layout, and a
+ * fast symlink keeps its target in the pointer array as raw bytes
+ * -- feeding either to the decoder is the same mistake as sweeping
+ * the bitmap.
+ */
+ if (!S_ISREG(mode))
+ return;
+
+ {
+ /*
+ * The inode, held from here to the end of the walk.
+ *
+ * The check above says nobody is writing the file
+ * now. The walk takes seconds to minutes, and a file
+ * unlinked in the meantime was evicted, its blocks
+ * freed and handed to other files, while the walk
+ * still took them for its leaves: that is how the
+ * sweep came to "correct" other files' indirect
+ * blocks to zero. A reference keeps eviction, and so
+ * those frees, until iput; truncate frees under
+ * i_alloc_mutex, under which every block of the walk
+ * is checked. The walk stops at its next block once
+ * the file is unlinked, so the frees wait one pace
+ * interval at most. beamfs_iget refuses, with
+ * -ESTALE, a slot freed since the snapshot above.
+ *
+ * The tree walked is the one in memory, under the same
+ * mutex: the snapshot says what the inode was when it
+ * last reached the medium, not what its writers have
+ * made of it since.
+ */
+ struct inode *inode;
+ struct beamfs_inode_info *fi;
+ __le64 direct[BEAMFS_DIRECT_BLOCKS];
+ u64 roots[3];
+
+ inode = beamfs_iget(sb, ino);
+ if (IS_ERR(inode))
+ return;
+ if (!S_ISREG(inode->i_mode) || !inode->i_nlink) {
+ iput(inode);
+ return;
+ }
+ fi = BEAMFS_I(inode);
+ mutex_lock(&fi->i_alloc_mutex);
+ memcpy(direct, fi->i_direct, sizeof(direct));
+ roots[0] = le64_to_cpu(fi->i_indirect);
+ roots[1] = le64_to_cpu(fi->i_dindirect);
+ roots[2] = le64_to_cpu(fi->i_tindirect);
+ mutex_unlock(&fi->i_alloc_mutex);
+
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS &&
+ !kthread_should_stop() && inode->i_nlink; i++) {
+ u64 blk = le64_to_cpu(direct[i]);
+ unsigned int corrected = 0;
+ int ret;
+
+ if (!blk || !beamfs_block_is_allocated(sb, blk))
+ continue;
+
+ ret = beamfs_scrub_check_block(sb, blk, &corrected,
+ inode, i);
+ if (ret == -ESTALE)
+ continue;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+ if (ret == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+
+ for (i = 0; i < 3; i++)
+ beamfs_scrub_walk_level(sb, roots[i], i + 1, sbi,
+ inode, i + 1,
+ beamfs_scrub_root_base(i + 1));
+ iput(inode);
+ }
+}
+
+/*
+ * Set the pace from what the sweep just found.
+ *
+ * One rule, applied once per sweep: corrections mean go faster, silence
+ * means drift back. Halve the interval on any correction, and add an
+ * eighth back when there were none.
+ *
+ * Halving is deliberate. A rise in flux is abrupt -- a beam turns on,
+ * an aircraft reaches altitude -- and a scrubber that eased into it
+ * would spend the interesting minutes still at its idle rate. Recovery
+ * is gradual for the opposite reason: nothing is lost by staying alert
+ * a while after the last correction, and a volume that oscillates
+ * around the threshold should settle rather than thrash.
+ *
+ * Bounded below at 1 ms, because faster is a busy loop rather than a
+ * scrub, and above by whatever the operator set, because they know
+ * what the volume is for and this only ever tightens their number.
+ *
+ * The rate is a response to measured corrections, not a prediction. It
+ * follows the environment; it does not model it.
+ */
+static void beamfs_scrub_pace(struct beamfs_sb_info *sbi)
+{
+ unsigned int cur = READ_ONCE(sbi->s_scrub_interval_ms);
+ unsigned int base = sbi->s_scrub_base_ms;
+ u64 found;
+
+ if (!cur || !base)
+ return;
+
+ found = sbi->s_scrub_corrected - sbi->s_scrub_last_corrected;
+ sbi->s_scrub_last_corrected = sbi->s_scrub_corrected;
+
+ if (found) {
+ cur = max_t(unsigned int, 1u, cur / 2);
+ } else if (cur < base) {
+ cur += max_t(unsigned int, 1u, base / 8);
+ cur = min(cur, base);
+ }
+
+ WRITE_ONCE(sbi->s_scrub_interval_ms, cur);
+}
+
+/*
+ * What the pacing is doing, and why.
+ *
+ * A mechanism that changes its own rate has to be able to account for
+ * it: an operator seeing the scrubber consume more bandwidth than they
+ * configured needs to find the reason here rather than guess. The
+ * ratio to base is the useful number -- 1 means quiet, 32 means the
+ * volume has been correcting for a while.
+ */
+static ssize_t scrub_pace_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ unsigned int cur = READ_ONCE(sbi->s_scrub_interval_ms);
+ unsigned int base = sbi->s_scrub_base_ms;
+
+ return sysfs_emit(buf,
+ "interval_ms %u\nbase_ms %u\nfactor %u\ncorrected %llu\nwear_visits %llu\n",
+ cur, base,
+ (cur && base > cur) ? base / cur : 1,
+ sbi->s_scrub_corrected,
+ sbi->s_wear_visits);
+}
+
+/*
+ * Visit one worn block, if the budget knows of any.
+ *
+ * The sequential sweep answers "has anything changed"; this answers
+ * "where is the margin thin". They are different questions and the
+ * pacing above cannot serve the second: raising the rate for the whole
+ * volume to watch three blocks doubles the cost of 262051 that are
+ * untouched, which is what the measurement showed it doing.
+ *
+ * Called once per sweep step, so a worn block is seen every few
+ * hundred milliseconds while a fresh one waits for the sweep to reach
+ * it -- minutes or hours apart on a real volume. The ratio comes out of
+ * the geometry rather than a tuning knob: there is one wear visit per
+ * ordinary visit, and the number of worn blocks is small by definition.
+ * If it stops being small, the volume has a bigger problem than
+ * scheduling.
+ *
+ * Threshold at half the correctable symbols. Below that a block has
+ * consumed margin but retains most of it; at four of eight it is closer
+ * to the edge than to the start, and that is the point where seeing it
+ * often begins to matter.
+ */
+#define BEAMFS_WEAR_THRESHOLD (BEAMFS_RS_PARITY / 4)
+
+static void beamfs_scrub_wear_step(struct super_block *sb,
+ struct beamfs_sb_info *sbi)
+{
+ unsigned int corrected = 0;
+ u64 phys;
+
+ if (!sbi->s_budget_len)
+ return;
+
+ phys = beamfs_budget_next_worn(sb, sbi->s_wear_cursor,
+ BEAMFS_WEAR_THRESHOLD);
+ if (!phys) {
+ /* Round again from the start of the data region. */
+ sbi->s_wear_cursor = 0;
+ return;
+ }
+
+ sbi->s_wear_cursor = phys + 1;
+ sbi->s_wear_visits++;
+
+ if (!beamfs_block_is_allocated(sb, phys))
+ return;
+
+ /*
+ * Checked and reported, never written: the wear map names a
+ * physical block, and nothing says what it holds now or whose it
+ * is. A worn block is repaired when the sweep reaches the file
+ * that owns it, which asks the owner first.
+ */
+ if (beamfs_scrub_check_block(sb, phys, &corrected, NULL, 0) == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+}
+
+static int beamfs_scrub_thread(void *data)
+{
+ struct super_block *sb = data;
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ while (!kthread_should_stop()) {
+ unsigned long ino;
+
+ if (!READ_ONCE(sbi->s_scrub_interval_ms)) {
+ set_current_state(TASK_INTERRUPTIBLE);
+ if (!kthread_should_stop())
+ schedule_timeout(HZ);
+ __set_current_state(TASK_RUNNING);
+ continue;
+ }
+
+ ino = (unsigned long)sbi->s_scrub_cursor + 1;
+
+ /*
+ * s_inode_bitmap marks inodes that are FREE: setup_bitmap
+ * sets the bit when i_mode reads zero. An allocated inode
+ * is one whose bit is clear, which is the opposite of the
+ * obvious reading and why the first version swept nothing
+ * -- it visited only empty slots and found no data blocks
+ * on any of them.
+ */
+ if (sbi->s_inode_bitmap &&
+ !test_bit(ino, sbi->s_inode_bitmap))
+ beamfs_scrub_one_inode(sb, ino);
+
+ /*
+ * One worn block per step, alongside the sweep. The two
+ * run at the same cadence but cover sets of wildly
+ * different size, which is what gives worn blocks their
+ * attention without slowing the sweep.
+ */
+ beamfs_scrub_wear_step(sb, sbi);
+
+ sbi->s_scrub_cursor++;
+ if (sbi->s_scrub_cursor >= sbi->s_ninodes) {
+ sbi->s_scrub_cursor = 0;
+ sbi->s_scrub_passes++;
+ beamfs_scrub_pace(sbi);
+ /*
+ * Re-anchor once per sweep, so the wall-clock
+ * reference is at most one sweep old rather than as
+ * old as the mount. A volume left mounted for six
+ * months would otherwise date its events by a
+ * reading taken six months earlier.
+ *
+ * Once per sweep rather than on a timer: a sweep is
+ * already the unit in which this thread thinks, and
+ * a superblock write per sweep is nothing against
+ * the reads the sweep just did.
+ */
+ beamfs_clock_anchor(sb);
+ beamfs_alert_check_rate(sb);
+ /*
+ * A pass over an idle volume costs one inode-table
+ * read per inode and nothing else. Pause between
+ * sweeps so an empty filesystem does not spin.
+ */
+ msleep_interruptible(1000);
+ } else {
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* sysfs */
+/* ------------------------------------------------------------------ */
+
+struct beamfs_scrub_attr {
+ struct attribute attr;
+ ssize_t (*show)(struct beamfs_sb_info *sbi, char *buf);
+ ssize_t (*store)(struct beamfs_sb_info *sbi, const char *buf,
+ size_t len);
+};
+
+static ssize_t interval_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", READ_ONCE(sbi->s_scrub_interval_ms));
+}
+
+static ssize_t interval_store(struct beamfs_sb_info *sbi, const char *buf,
+ size_t len)
+{
+ unsigned int v;
+
+ if (kstrtouint(buf, 10, &v))
+ return -EINVAL;
+ /*
+ * Zero parks the thread. There is no upper clamp: an operator who
+ * wants one block a minute on a nearly idle archive is making a
+ * reasonable choice, and one who wants a tight sweep before a
+ * mission is making another.
+ */
+ /*
+ * Writing the interval sets the ceiling as well as the current
+ * pace: an operator asking for a rate is stating what the volume
+ * should cost when nothing is wrong, and the pacing only ever
+ * tightens from there.
+ */
+ sbi->s_scrub_base_ms = v;
+ WRITE_ONCE(sbi->s_scrub_interval_ms, v);
+ return len;
+}
+
+static ssize_t cursor_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_cursor);
+}
+
+static ssize_t passes_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_passes);
+}
+
+static ssize_t blocks_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_blocks);
+}
+
+static ssize_t corrected_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_corrected);
+}
+
+static ssize_t uncorrectable_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_uncorrectable);
+}
+
+/*
+ * error_budget -- how much correction capacity the volume has left.
+ *
+ * One line per wear level: the number of blocks whose worst subblock
+ * has needed that many corrections. Level 0 is untouched, level
+ * BEAMFS_ERROR_BUDGET_MAX is blocks with no margin at all -- the next
+ * upset in the wrong subblock takes them out.
+ *
+ * Reading walks the whole region, 244 MiB on a 931 GiB volume, so this
+ * is a deliberate cost paid on request rather than a counter kept in
+ * memory. A counter would have to be rebuilt at mount and would drift
+ * against the medium in between; the medium is the record.
+ */
+static ssize_t error_budget_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ u64 hist[BEAMFS_ERROR_BUDGET_MAX + 1];
+ struct super_block *sb = sbi->s_sb;
+ unsigned int i;
+ int len = 0;
+
+ if (!sb)
+ return sysfs_emit(buf, "unavailable\n");
+
+ if (!sbi->s_budget_blk)
+ return sysfs_emit(buf, "disabled\n");
+
+ beamfs_budget_histogram(sb, hist);
+
+ for (i = 0; i <= BEAMFS_ERROR_BUDGET_MAX; i++)
+ len += sysfs_emit_at(buf, len, "%u %llu\n", i, hist[i]);
+
+ return len;
+}
+
+static ssize_t alert_rate_limit_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", sbi->s_alert_rate_limit);
+}
+
+/*
+ * How many blocks may be corrected in one sweep before it is worth
+ * telling somebody.
+ *
+ * Zero disables the check and is the default: the right number depends
+ * entirely on where the machine sits, and a value invented here would
+ * either cry wolf on a spacecraft or stay silent in a hospital.
+ */
+static ssize_t alert_rate_limit_store(struct beamfs_sb_info *sbi,
+ const char *buf, size_t len)
+{
+ u32 v;
+
+ if (kstrtouint(buf, 10, &v))
+ return -EINVAL;
+ sbi->s_alert_rate_limit = v;
+ return len;
+}
+
+/*
+ * clock_anchor -- what the two clocks read at the same instant.
+ *
+ * Three fields: the monotonic stamp, the wall time it corresponded to,
+ * and what that wall time was worth. A reader converting a journal
+ * entry to a date needs all three, and dropping the third turns a
+ * qualified statement into an unqualified one.
+ */
+static ssize_t clock_anchor_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ static const char * const q[] = {
+ "unknown", "rtc", "ntp", "hardware"
+ };
+ u32 i = sbi->s_anchor_quality;
+
+ if (i >= ARRAY_SIZE(q))
+ i = 0;
+
+ return sysfs_emit(buf, "mono %llu\nreal %llu\nquality %s\n",
+ sbi->s_anchor_mono, sbi->s_anchor_real, q[i]);
+}
+
+#define BEAMFS_SCRUB_RO(_name) \
+ static struct beamfs_scrub_attr beamfs_scrub_attr_##_name = { \
+ .attr = { .name = __stringify(_name), .mode = 0444 }, \
+ .show = _name##_show, \
+ }
+
+#define BEAMFS_SCRUB_RW(_name) \
+ static struct beamfs_scrub_attr beamfs_scrub_attr_##_name = { \
+ .attr = { .name = __stringify(_name), .mode = 0644 }, \
+ .show = _name##_show, .store = _name##_store, \
+ }
+
+BEAMFS_SCRUB_RW(interval);
+BEAMFS_SCRUB_RO(cursor);
+BEAMFS_SCRUB_RO(passes);
+BEAMFS_SCRUB_RO(blocks);
+BEAMFS_SCRUB_RO(corrected);
+BEAMFS_SCRUB_RO(uncorrectable);
+BEAMFS_SCRUB_RO(error_budget);
+BEAMFS_SCRUB_RO(clock_anchor);
+BEAMFS_SCRUB_RO(scrub_pace);
+BEAMFS_SCRUB_RW(alert_rate_limit);
+
+static struct attribute *beamfs_scrub_attrs[] = {
+ &beamfs_scrub_attr_interval.attr,
+ &beamfs_scrub_attr_cursor.attr,
+ &beamfs_scrub_attr_passes.attr,
+ &beamfs_scrub_attr_blocks.attr,
+ &beamfs_scrub_attr_corrected.attr,
+ &beamfs_scrub_attr_uncorrectable.attr,
+ &beamfs_scrub_attr_error_budget.attr,
+ &beamfs_scrub_attr_clock_anchor.attr,
+ &beamfs_scrub_attr_scrub_pace.attr,
+ &beamfs_scrub_attr_alert_rate_limit.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(beamfs_scrub);
+
+static ssize_t beamfs_scrub_attr_show(struct kobject *kobj,
+ struct attribute *attr, char *buf)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+ struct beamfs_scrub_attr *a =
+ container_of(attr, struct beamfs_scrub_attr, attr);
+
+ return a->show ? a->show(sbi, buf) : -EIO;
+}
+
+static ssize_t beamfs_scrub_attr_store(struct kobject *kobj,
+ struct attribute *attr,
+ const char *buf, size_t len)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+ struct beamfs_scrub_attr *a =
+ container_of(attr, struct beamfs_scrub_attr, attr);
+
+ return a->store ? a->store(sbi, buf, len) : -EIO;
+}
+
+static const struct sysfs_ops beamfs_scrub_sysfs_ops = {
+ .show = beamfs_scrub_attr_show,
+ .store = beamfs_scrub_attr_store,
+};
+
+static void beamfs_scrub_release(struct kobject *kobj)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+
+ complete(&sbi->s_kobj_unregister);
+}
+
+static const struct kobj_type beamfs_scrub_ktype = {
+ .default_groups = beamfs_scrub_groups,
+ .sysfs_ops = &beamfs_scrub_sysfs_ops,
+ .release = beamfs_scrub_release,
+};
+
+static struct kset *beamfs_kset;
+
+int beamfs_scrub_init(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ int ret;
+
+ sbi->s_scrub_interval_ms = READ_ONCE(scrub_interval_ms);
+ sbi->s_scrub_base_ms = READ_ONCE(scrub_interval_ms);
+ sbi->s_scrub_last_corrected = 0;
+ sbi->s_wear_cursor = 0;
+ sbi->s_wear_visits = 0;
+ sbi->s_scrub_cursor = 0;
+ sbi->s_scrub_passes = 0;
+ sbi->s_scrub_blocks = 0;
+ sbi->s_scrub_corrected = 0;
+ sbi->s_scrub_uncorrectable = 0;
+ init_completion(&sbi->s_kobj_unregister);
+
+ if (!beamfs_kset) {
+ beamfs_kset = kset_create_and_add("beamfs", NULL, fs_kobj);
+ if (!beamfs_kset)
+ return -ENOMEM;
+ }
+
+ sbi->s_kobj.kset = beamfs_kset;
+ ret = kobject_init_and_add(&sbi->s_kobj, &beamfs_scrub_ktype,
+ NULL, "%s", sb->s_id);
+ if (ret) {
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ return ret;
+ }
+
+ sbi->s_scrub_thread = kthread_run(beamfs_scrub_thread, sb,
+ "beamfs-scrub/%s", sb->s_id);
+ if (IS_ERR(sbi->s_scrub_thread)) {
+ ret = PTR_ERR(sbi->s_scrub_thread);
+ sbi->s_scrub_thread = NULL;
+ kobject_del(&sbi->s_kobj);
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * Stop the sweep. Safe to call more than once.
+ *
+ * The sweep holds a reference on the inode it walks, and put_super runs
+ * after generic_shutdown_super has evicted the volume's inodes: an
+ * inode still held then is busy after unmount, and its iput later
+ * touches a superblock that is gone. beamfs_kill_sb calls this before
+ * kill_block_super; put_super calls beamfs_scrub_exit, which finds the
+ * thread already stopped and takes down the sysfs directory.
+ */
+void beamfs_scrub_stop(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (sbi && sbi->s_scrub_thread) {
+ kthread_stop(sbi->s_scrub_thread);
+ sbi->s_scrub_thread = NULL;
+ }
+}
+
+void beamfs_scrub_exit(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (!sbi)
+ return;
+
+ beamfs_scrub_stop(sb);
+ if (sbi->s_kobj.state_initialized) {
+ kobject_del(&sbi->s_kobj);
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 16/18] Documentation: filesystems: add beamfs
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (14 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 15/18] beamfs: add scrubber and RS benchmark Aurelien DESBRIERES
@ 2026-10-19 4:17 ` 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
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
What beamfs protects and how, its runtime interfaces (module
parameters, sysfs, uevents, debugfs, tracepoints), its userspace tools
and its known limitations.
Documentation/ABI/testing/sysfs-fs-beamfs describes the attributes
under /sys/fs/beamfs/<dev>/ and the uevent raised there.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
Documentation/ABI/testing/sysfs-fs-beamfs | 88 ++++++++++
Documentation/filesystems/beamfs.rst | 197 ++++++++++++++++++++++
Documentation/filesystems/index.rst | 1 +
3 files changed, 286 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-fs-beamfs
create mode 100644 Documentation/filesystems/beamfs.rst
diff --git a/Documentation/ABI/testing/sysfs-fs-beamfs b/Documentation/ABI/testing/sysfs-fs-beamfs
new file mode 100644
index 000000000000..d59b96239a9a
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-fs-beamfs
@@ -0,0 +1,88 @@
+What: /sys/fs/beamfs/<dev>/
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: One directory per mounted beamfs volume, named after the
+ block device, holding the scrubber's controls and counters
+ and the volume's clock anchor. The counters are kept since
+ the mount.
+
+ A KOBJ_CHANGE uevent is raised on this directory, at most
+ once a minute per reason, with the variables BEAMFS_ALERT=1,
+ BEAMFS_REASON=<reason> and BEAMFS_DETAIL=<text>; the reasons
+ are "uncorrectable" (a block could not be recovered),
+ "no-margin" (a block whose worst codeword has no correction
+ left) and "correction-rate" (more blocks corrected in one
+ sweep than alert_rate_limit allows).
+
+What: /sys/fs/beamfs/<dev>/interval
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RW) Milliseconds the scrubber waits between two blocks of
+ this volume. Writing it sets the pace the scrubber returns
+ to when nothing is wrong, and the pacing only tightens from
+ there. 0 parks the scrubber; correction on read is not
+ affected. The value a volume starts with is the module
+ parameter scrub_interval_ms.
+
+What: /sys/fs/beamfs/<dev>/cursor
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) Where the sweep is, as a physical block number.
+
+What: /sys/fs/beamfs/<dev>/passes
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) Complete sweeps of the volume since the mount.
+
+What: /sys/fs/beamfs/<dev>/blocks
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) Blocks the scrubber has read since the mount.
+
+What: /sys/fs/beamfs/<dev>/corrected
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) Blocks the scrubber corrected and wrote back since
+ the mount.
+
+What: /sys/fs/beamfs/<dev>/uncorrectable
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) Blocks with a codeword the scrubber could not correct
+ since the mount.
+
+What: /sys/fs/beamfs/<dev>/error_budget
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) How much correction capacity the volume has left: one
+ line per wear level, "<level> <blocks>", the number of
+ blocks whose worst codeword has needed that many
+ corrections, from 0 (untouched) to 8 (no correction left).
+ Reading walks the volume's budget region, which is a cost
+ paid on request rather than a counter kept in memory.
+ "disabled" on a volume formatted without a budget region.
+
+What: /sys/fs/beamfs/<dev>/clock_anchor
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) What the two clocks read at the same instant, taken
+ once per sweep: "mono <monotonic>", "real <wall clock>" and
+ "quality <unknown|rtc|ntp|hardware>", the worth of that wall
+ clock reading. A reader dating a journal entry needs all
+ three.
+
+What: /sys/fs/beamfs/<dev>/scrub_pace
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RO) The pacing of the scrubber: "interval_ms <current>",
+ "base_ms <the interval written>", "factor <base / current>",
+ "corrected <blocks corrected>" and "wear_visits <blocks
+ visited through the wear map>".
+
+What: /sys/fs/beamfs/<dev>/alert_rate_limit
+Date: October 2026
+Contact: Aurelien DESBRIERES <aurelien@hackers.camp>
+Description: (RW) Blocks that may be corrected in one sweep before the
+ "correction-rate" uevent is raised. 0, the default, disables
+ the check: the right number depends on where the machine
+ sits.
diff --git a/Documentation/filesystems/beamfs.rst b/Documentation/filesystems/beamfs.rst
new file mode 100644
index 000000000000..7e5d6ca2d079
--- /dev/null
+++ b/Documentation/filesystems/beamfs.rst
@@ -0,0 +1,197 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+======
+beamfs
+======
+
+beamfs is a block-device filesystem that corrects corrupted bytes in
+place, on a single device, with no second copy to repair from. File
+data, directory blocks, inodes, the allocation bitmap and the
+superblock carry Reed-Solomon parity, built on lib/reed_solomon, and
+every read decodes. A read the decoder cannot vouch for fails with
+``-EIO`` instead of returning wrong data.
+
+It is meant for systems that keep their data on one storage device in
+a radiation environment: on-board computers in space, and equipment in
+radiotherapy rooms, where high-energy photon beams and proton or ion
+beams produce secondary neutrons that cause single-event upsets in
+electronics [#tg203]_.
+
+beamfs protects the path between the medium and the page cache. It
+does not protect memory after a read, and it does not replace ECC
+memory or redundancy across devices; a beamfs volume can sit on a
+mirror.
+
+
+Data blocks
+===========
+
+The block size is 4096 bytes. A data block holds sixteen RS(255,239)
+codewords over GF(2^8), primitive polynomial 0x187: 3824 coded bytes
+and 256 bytes of parity, then a 16-byte tail. Each codeword corrects
+up to eight corrupted bytes. In the default layout 288 bytes of every
+4096 are overhead, 256 of parity, 16 of descriptor and 16 of
+generation, which leaves 3808 bytes of file data per block.
+
+By default the symbols of the sixteen codewords are interleaved across
+the block, so a burst in the coded bytes is spread over all of them.
+The parity of each codeword stays together, so nine corrupted bytes in
+the parity of one codeword are enough to defeat it.
+
+A read decodes every codeword of the block into a private copy. The
+corrected bytes reach the page cache; the read does not write them
+back to the medium. A codeword the decoder reports as uncorrectable
+fails the read with ``-EIO``.
+
+Beyond eight corrupted bytes in a codeword, a Reed-Solomon decoder can
+also land on a different valid codeword and report success. With the
+``DATA_CSUM`` feature a CRC32 of the payload, stored in the block, is
+checked after decoding, and such a read fails closed. ``DATA_CSUM``
+and ``DATA_SELFID`` are enabled together by ``mkfs.beamfs
+--data-csum``; neither is on by default.
+
+
+Metadata
+========
+
+The superblock carries a CRC32 and Reed-Solomon parity over the region
+the CRC covers. It also holds the correction journal: a ring of 64
+events, each naming the block, the codeword and the number of symbols
+corrected, with a monotonic timestamp. The ``raf-decode`` tool in the
+beamfs repository decodes it.
+
+Each inode is one Reed-Solomon codeword, with a CRC32 that covers its
+block pointers. Directory blocks carry sixteen codewords of inline
+parity, as data blocks do, and hold variable-length entries. The block
+allocation bitmap is Reed-Solomon protected.
+
+An indirect block is 512 little-endian 64-bit pointers and has no room
+for parity. Its parity lives in a separate region, at a slot computed
+from the block number; ``mkfs.beamfs -I`` chooses ``rs`` (the
+default), ``crc`` or ``none``. In ``rs`` mode the parity detects and
+journals a damaged indirect block but does not repair the buffer,
+pointers 478 to 511 have no parity, and a pointer flipped onto another
+allocated block is detected only with ``DATA_SELFID``.
+
+
+Background scrubbing
+====================
+
+Correction happens on read, so a block nobody reads accumulates upsets
+until it is past the correction radius. Each mounted volume runs a
+kernel thread, ``beamfs-scrub/<dev>``, that walks the allocated blocks
+at a bounded pace and decodes each one, so that drift is found while
+it is still correctable.
+
+When the volume has an error budget, each data block also records the
+largest number of symbols corrected in any of its codewords: how much
+of its correction capacity it has used.
+
+
+Using beamfs
+============
+
+The userspace tools are in https://github.com/roastercode/beamfs,
+under ``tools/``::
+
+ mkfs.beamfs /dev/sdX
+ mount -t beamfs /dev/sdX /mnt
+ fsck.beamfs /dev/sdX
+
+beamfs takes no mount options.
+
+Module parameters
+-----------------
+
+``scrub_interval_ms``
+ Milliseconds between two blocks for volumes mounted from then on
+ (default 100). 0 parks the scrubber; correction on read is
+ unaffected.
+
+``clock_source``
+ Declared quality of the wall clock that anchors journal timestamps:
+ 0 unknown (the default), 1 RTC, 2 NTP, 3 hardware (PTP or GPS).
+
+sysfs
+-----
+
+Each mounted volume has a directory ``/sys/fs/beamfs/<dev>/``:
+
+``interval`` (rw)
+ Milliseconds between two blocks for this volume's scrubber.
+
+``cursor``, ``passes``, ``blocks`` (ro)
+ Position of the scrubber, completed sweeps, blocks decoded.
+
+``corrected``, ``uncorrectable`` (ro)
+ Blocks the scrubber corrected, and blocks it could not correct.
+
+``error_budget`` (ro)
+ For each number of corrected symbols, how many blocks reached it;
+ ``disabled`` if the volume has no error budget.
+
+``clock_anchor`` (ro)
+ Monotonic and wall-clock time at the same instant, and the declared
+ quality of the wall clock.
+
+``scrub_pace`` (ro)
+ The scrubber's current pace.
+
+``alert_rate_limit`` (rw)
+ Blocks corrected in one sweep at or above which an alert is sent; 0,
+ the default, disables the check.
+
+Alerts
+------
+
+When a threshold is crossed, beamfs sends a ``KOBJ_CHANGE`` uevent on
+``/sys/fs/beamfs/<dev>`` with ``BEAMFS_ALERT=1``, ``BEAMFS_REASON``
+and ``BEAMFS_DETAIL``, and logs the same line. ``BEAMFS_REASON`` is
+one of:
+
+``uncorrectable``
+ A block could not be recovered.
+
+``no-margin``
+ A block has used all its correctable symbols. It still reads
+ correctly.
+
+``correction-rate``
+ A sweep corrected at least ``alert_rate_limit`` blocks.
+
+Each reason is sent at most once a minute. What to do about an alert
+is left to userspace.
+
+debugfs and tracepoints
+-----------------------
+
+Reading ``/sys/kernel/debug/beamfs/rs_bench`` encodes and decodes in
+memory and reports the cost per block; ``rs_bench_iters`` sets the
+number of iterations. The tracepoints are in the ``beamfs`` trace
+system.
+
+
+Limitations
+===========
+
+- There is no journal and no copy-on-write. Metadata reaches the
+ medium through writeback, in no guaranteed order; after an unclean
+ shutdown the volume is checked with ``fsck.beamfs``.
+- Metadata goes through buffer heads.
+- Without ``DATA_SELFID``, an indirect pointer flipped onto another
+ allocated block in range is not detected.
+- The measurements published so far emulate single-bit upsets on the
+ read path, with at most two erroneous symbols per codeword and per
+ decode [#beamfsv3]_. The eight-symbol limit, bursts, miscorrection
+ and performance are not measured.
+
+
+References
+==========
+
+.. [#tg203] M. Miften et al., Management of radiotherapy patients with
+ implanted cardiac pacemakers and defibrillators: a report of the
+ AAPM TG-203, Medical Physics, 2019. doi:10.1002/mp.13838
+
+.. [#beamfsv3] A. Desbrieres, beamfs v3 technical report, Zenodo,
+ 2026. doi:10.5281/zenodo.23253350
diff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/index.rst
index 7cce7cc28d12..bff91a3c82ff 100644
--- a/Documentation/filesystems/index.rst
+++ b/Documentation/filesystems/index.rst
@@ -74,6 +74,7 @@ Documentation for filesystem implementations.
afs
autofs
autofs-mount-control
+ beamfs
befs
btrfs
ceph
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 17/18] beamfs: add Kconfig and Makefile
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (15 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 16/18] Documentation: filesystems: add beamfs Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 18/18] MAINTAINERS: add beamfs Aurelien DESBRIERES
17 siblings, 0 replies; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
Hook beamfs into fs/Kconfig, among the miscellaneous filesystems, and
into fs/Makefile.
Besides BEAMFS_FS there are two debugging options: BEAMFS_DEBUG_TREE
enables the tree checker, and BEAMFS_ORDERED_META writes metadata
blocks synchronously in dependency order, an experiment kept to
measure ordering questions. Both default to off.
The Makefile adds the warnings the module is developed with; whether
they stop the build is left to CONFIG_WERROR.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/Kconfig | 1 +
fs/Makefile | 1 +
fs/beamfs/Kconfig | 64 ++++++++++++++++++++++++++++++++++++++++++++++
fs/beamfs/Makefile | 21 +++++++++++++++
4 files changed, 87 insertions(+)
create mode 100644 fs/beamfs/Kconfig
create mode 100644 fs/beamfs/Makefile
diff --git a/fs/Kconfig b/fs/Kconfig
index 46313f65ee54..7c864b9549d4 100644
--- a/fs/Kconfig
+++ b/fs/Kconfig
@@ -313,6 +313,7 @@ source "fs/affs/Kconfig"
source "fs/ecryptfs/Kconfig"
source "fs/hfs/Kconfig"
source "fs/hfsplus/Kconfig"
+source "fs/beamfs/Kconfig"
source "fs/befs/Kconfig"
source "fs/jffs2/Kconfig"
# UBIFS File system configuration
diff --git a/fs/Makefile b/fs/Makefile
index 16f1108b64e1..632c15c85254 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -110,6 +110,7 @@ obj-$(CONFIG_XFS_FS) += xfs/
obj-$(CONFIG_9P_FS) += 9p/
obj-$(CONFIG_AFS_FS) += afs/
obj-$(CONFIG_NILFS2_FS) += nilfs2/
+obj-$(CONFIG_BEAMFS_FS) += beamfs/
obj-$(CONFIG_BEFS_FS) += befs/
obj-y += hostfs/
obj-$(CONFIG_CACHEFILES) += cachefiles/
diff --git a/fs/beamfs/Kconfig b/fs/beamfs/Kconfig
new file mode 100644
index 000000000000..56be9beeb588
--- /dev/null
+++ b/fs/beamfs/Kconfig
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# beamfs filesystem configuration
+#
+
+config BEAMFS_FS
+ tristate "beamfs filesystem support"
+ depends on BLOCK
+ select BUFFER_HEAD
+ select FS_IOMAP
+ select CRC32
+ select REED_SOLOMON
+ select REED_SOLOMON_ENC8
+ select REED_SOLOMON_DEC8
+ help
+ beamfs is a block-device filesystem that corrects corrupted bytes
+ in place, on a single device, without a second copy to repair
+ from. Data and directory blocks carry Reed-Solomon RS(255,239)
+ parity over GF(2^8) inside the block, indirect blocks carry it in
+ a region of their own, and every read decodes.
+
+ Up to eight corrupted bytes per 255-byte codeword are corrected.
+ A codeword the decoder reports as uncorrectable fails the read
+ with -EIO. Nothing is guaranteed beyond eight bytes per codeword.
+
+ It is meant for storage that takes bit flips the device does not
+ correct itself, such as single-event upsets. See
+ Documentation/filesystems/beamfs.rst.
+
+ To compile this file system support as a module, choose M here:
+ the module will be called beamfs.
+
+ If unsure, say N.
+
+config BEAMFS_DEBUG_TREE
+ bool "beamfs: check the indirect tree invariant at every store"
+ depends on BEAMFS_FS
+ help
+ Record every pointer installed in an indirect block and check
+ each later store to the same slot against that record. A slot
+ that held a live pointer and reads back as zero is reported
+ with a stack trace at the instant it happens, instead of being
+ counted by fsck long after.
+
+ Costs one hash lookup and sixteen bytes per live pointer. Say
+ N for production.
+
+config BEAMFS_ORDERED_META
+ bool "Write metadata in dependency order (slow, experimental)"
+ depends on BEAMFS_FS
+ default n
+ help
+ Write every metadata block to the medium before anything names
+ it: one synchronous write per level of indirection, up to four
+ for a block reached through the triple indirect.
+
+ This exists to answer a question, not to be enabled. beamfs
+ relies on mark_buffer_dirty for ordering and mark_buffer_dirty
+ gives none, and this forces the safe order at whatever cost in
+ speed so that the hypothesis can be measured rather than
+ argued. A journal is how the same property is had for one
+ sequential write instead.
+
+ Say N.
diff --git a/fs/beamfs/Makefile b/fs/beamfs/Makefile
new file mode 100644
index 000000000000..d01ad86a0c14
--- /dev/null
+++ b/fs/beamfs/Makefile
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the beamfs filesystem.
+#
+
+obj-$(CONFIG_BEAMFS_FS) += beamfs.o
+
+beamfs-y := treecheck.o super.o inode.o dir.o dirent.o file.o \
+ file_inline.o scrub.o indparity.o rsbench.o budget.o clock.o \
+ alert.o edac.o alloc.o namei.o
+
+# define_trace.h includes beamfs_trace.h by name, from super.c.
+CFLAGS_super.o += -I$(src)
+
+# The warnings the module is developed with. Whether they stop the
+# build is left to CONFIG_WERROR.
+ccflags-y += -Wall -Wextra -Wformat=2 -Werror=format
+ccflags-y += -Wmissing-prototypes -Wmissing-declarations
+ccflags-y += -Wundef -Wstrict-prototypes
+# The VFS passes handlers more arguments than most of them use.
+ccflags-y += -Wno-unused-parameter
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [RFC PATCH 18/18] MAINTAINERS: add beamfs
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
` (16 preceding siblings ...)
2026-10-19 4:17 ` [RFC PATCH 17/18] beamfs: add Kconfig and Makefile Aurelien DESBRIERES
@ 2026-10-19 4:17 ` Aurelien DESBRIERES
2026-10-19 21:38 ` Randy Dunlap
17 siblings, 1 reply; 20+ messages in thread
From: Aurelien DESBRIERES @ 2026-10-19 4:17 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
Add an entry for beamfs and its documentation.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
MAINTAINERS | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 62744835f86f..e5b95458da43 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3082,6 +3082,15 @@ F: drivers/usb/host/ohci-nxp.c
F: drivers/watchdog/pnx4008_wdt.c
N: lpc32xx
+BEAMFS FILE SYSTEM
+M: Aurelien DESBRIERES <aurelien@hackers.camp>
+L: linux-fsdevel@vger.kernel.org
+S: Maintained
+W: https://github.com/roastercode/beamfs
+F: Documentation/ABI/testing/sysfs-fs-beamfs
+F: Documentation/filesystems/beamfs.rst
+F: fs/beamfs/
+
LPC32XX DMAMUX SUPPORT
M: J.M.B. Downing <jonathan.downing@nautel.com>
M: Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>
--
2.55.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [RFC PATCH 18/18] MAINTAINERS: add beamfs
2026-10-19 4:17 ` [RFC PATCH 18/18] MAINTAINERS: add beamfs Aurelien DESBRIERES
@ 2026-10-19 21:38 ` Randy Dunlap
0 siblings, 0 replies; 20+ messages in thread
From: Randy Dunlap @ 2026-10-19 21:38 UTC (permalink / raw)
To: Aurelien DESBRIERES, Jonathan Corbet, Shuah Khan, Alexander Viro,
Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, linux-doc, aurelien.desbrieres,
linux-api
Hi--
On 10/18/26 9:17 PM, Aurelien DESBRIERES wrote:
> Add an entry for beamfs and its documentation.
>
> Assisted-by: LLM
> Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
> ---
> MAINTAINERS | 9 +++++++++
> 1 file changed, 9 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 62744835f86f..e5b95458da43 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3082,6 +3082,15 @@ F: drivers/usb/host/ohci-nxp.c
> F: drivers/watchdog/pnx4008_wdt.c
> N: lpc32xx
>
> +BEAMFS FILE SYSTEM
> +M: Aurelien DESBRIERES <aurelien@hackers.camp>
> +L: linux-fsdevel@vger.kernel.org
> +S: Maintained
> +W: https://github.com/roastercode/beamfs
> +F: Documentation/ABI/testing/sysfs-fs-beamfs
> +F: Documentation/filesystems/beamfs.rst
> +F: fs/beamfs/
> +
Entries in the MAINTAINERS file should be in alphabetical order,
so between these 2 entries, please:
BDISP ST MEDIA DRIVER
BECKHOFF CX5020 ETHERCAT MASTER DRIVER
> LPC32XX DMAMUX SUPPORT
> M: J.M.B. Downing <jonathan.downing@nautel.com>
> M: Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>
--
~Randy
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-10-19 21:39 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [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
2026-10-19 21:38 ` Randy Dunlap
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox