* Re: [BUG] ext4: BUG_ON in ext4_write_inline_data (fs/ext4/inline.c:240)
From: Theodore Tso @ 2026-04-29 4:40 UTC (permalink / raw)
To: Demi Marie Obenour
Cc: Zw Tang, Andreas Dilger, libaokun, jack, ojaswin, linux-ext4,
linux-kernel, yi.zhang, syzkaller-bugs
In-Reply-To: <5981069b-8c54-4b5b-a808-5ebdc8cd7265@gmail.com>
On Tue, Apr 28, 2026 at 04:50:14PM -0400, Demi Marie Obenour wrote:
> This is an example of a BadUSB attack, which has been known since
> at least 2014. USB sticks *do* have their own microcontrollers to
> run their firmware. At least in the past this firmware has been
> programmable and not been digitally signed. This means that the USB
> stick *can* be reprogrammed to perform a TOCTOU attack on a filesystem,
> or indeed to implement a completely different kind of USB device.
Honestly, if that's what you are worried about, then best solution is
put epoxy in every single USB port. I've since financial institutions
that have done precisely this, and both Android and Chrome OS supports
enterprise security policies which does the equivalent in software.
> Protecting against replay attacks requires a Merkle tree. The only
> Linux filesystems that I know have one are ZFS, bcachefs, and BTRFS.
> The first two are out of tree and the third is not shipped in RHEL
> at least.
FYI, there was a patch for btrfs, but it was never landed. It's
unclear how many people would be willing to pay the performance tax of
using hmac-sha256 for every single data and metadata block write.
> Of course, you are free to choose which (if any) of these attacks you
> care about. One can that USB sticks should be mounted in userspace,
> UEFI secure boot with Microsoft keys is irrelevant as long as
> administrator -> kernel isn't a security boundary on Windows, and that
> confidential computing only makes sense for stateless workloads (which
> can use dm-verity) until there is a way to trust storage devices.
> But it's always best to be aware that an attack vector exists,
> whether or not one chooses to address it.
Sure, but a drive-by comment on a patch review advocating that we slow
down ext4 to protect a single instance where the attacker has
read/write access to a mounted block device, when the file system
doesn't have generalized protections against that whole class of
attacks.... isn't particularly helpful.
By the way, I'm not aware of *any* company that has been interested in
funding work to protect against this class of attacks. Given that
most file system developers prefer food with their meals, and have
enough *other* unfunded mandates from our user community, it doesn't
seem likely that we're going to see much forward progress towards your
desires/interests.
Cheers,
- Ted
^ permalink raw reply
* Re: [BUG] ext4: BUG_ON in ext4_write_inline_data (fs/ext4/inline.c:240)
From: Demi Marie Obenour @ 2026-04-28 20:50 UTC (permalink / raw)
To: Theodore Tso
Cc: Zw Tang, Andreas Dilger, libaokun, jack, ojaswin, linux-ext4,
linux-kernel, yi.zhang, syzkaller-bugs
In-Reply-To: <20260426032211.GD22489@macsyma-wired.lan>
[-- Attachment #1.1.1: Type: text/plain, Size: 3806 bytes --]
On 4/25/26 23:22, Theodore Tso wrote:
> On Sat, Apr 25, 2026 at 02:00:23PM -0400, Demi Marie Obenour wrote:
>>
>> Changing block devices that are mounted is also reachable via USB.
>> Yes, some distros may disable automount, but users who have stuff to
>> get done will mount USB devices anyway. Telling users "don't do this"
>> very rarely works in practice.
>
> How can an unprivileged user change the contents of a USB device while
> it is mounted?
>
> Are you positing evil USB devices that can return block contents A at
> time t, and block contents B at time t+1?
Correct.
> The threat model that we are using is that if the USB device is set to
> a particular state *before* the file system is mounted, and then the
> KGB scatters the USB device in the parking lot, and then someone picks
> up the USB device in the Raytheon parking lot, and says, "hey, free
> hardware", takes it into the classified machinem room, inserts it into
> the server, and mounts it. This might be considered likely or not
> likely, but speaking as someone who has been in a top secret machine
> room at a defense contractor, they were *way* less protected than what
> I've seen at a financial services company, or at a data center at a
> hyperscaler.
>
> But be that as it may, even *then* you're not modifying the block
> device while it is mounted.
>
>> 2. Harden the kernel filesystem drivers against malicious devices,
>> including TOCTOU.
>
> Malicious devices that have their own microcomputer and can change the
> block contents under the control of the attacker is *just* not
> something I care about. I also don't think it's a particularly
> realistic threat model.
This is an example of a BadUSB attack, which has been known since
at least 2014. USB sticks *do* have their own microcontrollers to
run their firmware. At least in the past this firmware has been
programmable and not been digitally signed. This means that the USB
stick *can* be reprogrammed to perform a TOCTOU attack on a filesystem,
or indeed to implement a completely different kind of USB device.
There are also attacks possible in confidential computing scenarios.
dm-verity protects against both tampering and replay attacks,
but it is read-only. dm-crypt and dm-integrity are writable, but
dm-crypt does not block tampering and dm-verity does not block replay
of a previously stored sector.
Protecting against replay attacks requires a Merkle tree. The only
Linux filesystems that I know have one are ZFS, bcachefs, and BTRFS.
The first two are out of tree and the third is not shipped in RHEL
at least.
If dm-integrity is used, an attacker can return an old value that
was stored at a given block in the past. With only dm-crypt, an
attacker can replace any cipher block (typically 16 bytes) with
either its old contents or garbage the attacker doesn't control.
If this can be used to compromise a confidential VM, this violates
the confidential computing security boundary.
Finally, this can be used to violate kernel lockdown, breaking the
guarantees UEFI secure boot is supposed to provide. Whether that is
worth caring about is a totally different question of course.
Of course, you are free to choose which (if any) of these attacks you
care about. One can that USB sticks should be mounted in userspace,
UEFI secure boot with Microsoft keys is irrelevant as long as
administrator -> kernel isn't a security boundary on Windows, and that
confidential computing only makes sense for stateless workloads (which
can use dm-verity) until there is a way to trust storage devices.
But it's always best to be aware that an attack vector exists,
whether or not one chooses to address it.
--
Sincerely,
Demi Marie Obenour (she/her/hers)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply
* [PATCH] dept: update documentation function names to match implementation
From: Yunseong Kim @ 2026-04-28 16:26 UTC (permalink / raw)
To: bagasdotme
Cc: 2407018371, Dai.Ngo, Liam.Howlett, a.hindborg, ada.coupriediaz,
adilger.kernel, akpm, alex.gaynor, alexander.shishkin, aliceryhl,
amir73il, andi.shyti, andrii, anna, arnd, ast, baolin.wang,
bigeasy, bjorn3_gh, boqun.feng, bp, brauner, broonie, bsegall,
byungchul, catalin.marinas, chenhuacai, chris.p.wilson,
christian.koenig, chuck.lever, cl, clrkwllms, corbet, da.gomez,
dakr, damien.lemoal, dan.j.williams, daniel.vetter, dave.hansen,
david, dennis, dietmar.eggemann, djwong, dri-devel, duyuyang,
dwmw, francesco, frederic, gary, geert+renesas, geert, gregkh,
guoweikang.kernel, gustavo, gwan-gyeong.mun, hamohammed.sa,
hannes, harry.yoo, hch, her0gyugyu, hpa, jack, jglisse,
jiangshanlai, jlayton, joel.granados, joel, joelagnelf,
johannes.berg, josef, josh, jpoimboe, juri.lelli, kees,
kernel-team, kernel_team, kevin.brodsky, kristina.martsenko,
lillian, linaro-mm-sig, link, linux-arch, linux-arm-kernel,
linux-block, linux-doc, linux-ext4, linux-fsdevel, linux-i2c,
linux-ide, linux-kernel, linux-media, linux-mm, linux-modules,
linux-nfs, linux-rt-devel, linux, longman, lorenzo.stoakes,
lossin, luto, mark.rutland, masahiroy, mathieu.desnoyers,
matthew.brost, max.byungchul.park, mcgrof, melissa.srw, mgorman,
mhocko, miguel.ojeda.sandonis, minchan, mingo, mjguzik,
neeraj.upadhyay, neil, neilb, netdev, ngupta, ojeda, okorniev,
oleg, paulmck, penberg, peterz, petr.pavlu, qiang.zhang, rcu,
richard.weiyang, rientjes, rodrigosiqueiramelo, rostedt, rppt,
rust-for-linux, samitolvanen, sashal, shakeel.butt, sj,
sumit.semwal, surenb, tglx, thomas.weissschuh, tim.c.chen, tj,
tmgross, tom, torvalds, trondmy, tytso, urezki, usamaarif642,
vbabka, vdavydov.dev, vincent.guittot, vschneid, wangfushuai,
wangkefeng.wang, will, willy, wsa+renesas, x86, yeoreum.yun, ysk,
yunseong.kim, yuzhao, ziy, Yunseong Kim
In-Reply-To: <aTN38kJjBftxnjm9@archie.me>
Synchronize function names in the documentation with the actual
implementation to fix naming inconsistencies.
Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
Documentation/dev-tools/dept.rst | 2 +-
Documentation/dev-tools/dept_api.rst | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Documentation/dev-tools/dept.rst b/Documentation/dev-tools/dept.rst
index 333166464543..31b2fe629fab 100644
--- a/Documentation/dev-tools/dept.rst
+++ b/Documentation/dev-tools/dept.rst
@@ -97,7 +97,7 @@ No. What about the following?
mutex_lock A
mutex_lock A <- DEADLOCK
- wait_for_complete B <- DEADLOCK
+ wait_for_completion B <- DEADLOCK
complete B
mutex_unlock A
mutex_unlock A
diff --git a/Documentation/dev-tools/dept_api.rst b/Documentation/dev-tools/dept_api.rst
index 409116a62849..74e7b1424ad5 100644
--- a/Documentation/dev-tools/dept_api.rst
+++ b/Documentation/dev-tools/dept_api.rst
@@ -113,7 +113,7 @@ Do not use these APIs directly. The raw APIs of dept are:
dept_stage_wait(map, key, ip, wait_func, time);
dept_request_event_wait_commit();
dept_clean_stage();
- dept_stage_event(task, ip);
+ dept_ttwu_stage_wait(task, ip);
dept_ecxt_enter(map, evt_flags, ip, ecxt_func, evt_func, sub_local);
dept_ecxt_holding(map, evt_flags);
dept_request_event(map, ext_wgen);
--
2.53.0
^ permalink raw reply related
* [PATCH v2] iomap: add simple read path for small direct I/O
From: Fengnan Chang @ 2026-04-28 11:47 UTC (permalink / raw)
To: brauner, djwong, hch, ojaswin, dgc, linux-xfs, linux-fsdevel,
linux-ext4, linux-kernel, lidiangang
Cc: Fengnan Chang
When running 4K random read workloads on high-performance Gen5 NVMe
SSDs, the software overhead in the iomap direct I/O path
(__iomap_dio_rw) becomes a significant bottleneck.
Using io_uring with poll mode for a 4K randread test on a raw block
device:
taskset -c 30 ./t/io_uring -p1 -d512 -b4096 -s32 -c32 -F1 -B1 -R1 -X1
-n1 -P1 /dev/nvme10n1
Result: ~3.2M IOPS
Running the exact same workload on ext4 and XFS:
taskset -c 30 ./t/io_uring -p1 -d512 -b4096 -s32 -c32 -F1 -B1 -R1 -X1
-n1 -P1 /mnt/testfile
Result: ~1.84M IOPS
Profiling the ext4 workload reveals that a significant portion of CPU
time is spent on memory allocation and the iomap state machine
iteration:
5.33% [kernel] [k] __iomap_dio_rw
3.26% [kernel] [k] iomap_iter
2.37% [kernel] [k] iomap_dio_bio_iter
2.35% [kernel] [k] kfree
1.33% [kernel] [k] iomap_dio_complete
Introduce simple reads to reduce the overhead of iomap, simple read path
is triggered when the request satisfies:
- I/O size is <= inode blocksize (fits in a single block, no splits).
- No custom `iomap_dio_ops` (dops) registered by the filesystem.
After this optimization, the heavy generic functions disappear from the
profile, replaced by a single streamlined execution path:
4.83% [kernel] [k] iomap_dio_simple_read
With this patch, 4K random read IOPS on ext4 increases from 1.84M to
2.19M in the original single-core io_uring poll-mode workload.
Below are the test results using fio:
fs workload qd simple=0 simple=1 gain
ext4 libaio 1 18,738 18,761 +0.12%
ext4 libaio 128 455,383 471,473 +3.53%
ext4 libaio 256 453,273 468,555 +3.37%
ext4 libaio 512 447,320 469,036 +4.85%
ext4 io_uring 1 18,798 18,824 +0.14%
ext4 io_uring 128 503,834 528,353 +4.87%
ext4 io_uring 256 503,635 527,617 +4.76%
ext4 io_uring 512 501,802 527,882 +5.20%
ext4 io_uring_poll 1 19,246 19,270 +0.12%
ext4 io_uring_poll 128 1,463,343 1,565,019 +6.95%
ext4 io_uring_poll 256 1,651,112 1,888,182 +14.36%
ext4 io_uring_poll 512 1,632,641 1,893,259 +15.96%
xfs libaio 1 18,715 18,734 +0.10%
xfs libaio 128 452,974 473,459 +4.52%
xfs libaio 256 454,435 470,855 +3.61%
xfs libaio 512 456,796 473,047 +3.56%
xfs io_uring 1 18,755 18,795 +0.21%
xfs io_uring 128 509,459 534,819 +4.98%
xfs io_uring 256 509,853 536,051 +5.14%
xfs io_uring 512 507,926 533,558 +5.05%
xfs io_uring_poll 1 19,230 19,269 +0.20%
xfs io_uring_poll 128 1,467,398 1,567,840 +6.84%
xfs io_uring_poll 256 1,636,852 1,878,917 +14.79%
xfs io_uring_poll 512 1,639,495 1,874,813 +14.35%
Assisted-by: Gemini:gemini-3.1-pro-preview
Assisted-by: Codex:gpt-5-5
Signed-off-by: Fengnan Chang <changfengnan@bytedance.com>
---
fs/iomap/direct-io.c | 382 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 371 insertions(+), 11 deletions(-)
diff --git a/fs/iomap/direct-io.c b/fs/iomap/direct-io.c
index e911daedff65a..807d8c628a464 100644
--- a/fs/iomap/direct-io.c
+++ b/fs/iomap/direct-io.c
@@ -9,6 +9,9 @@
#include <linux/iomap.h>
#include <linux/task_io_accounting_ops.h>
#include <linux/fserror.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
+#include <linux/init.h>
#include "internal.h"
#include "trace.h"
@@ -236,20 +239,26 @@ static void iomap_dio_done(struct iomap_dio *dio)
iomap_dio_complete_work(&dio->aio.work);
}
-static void __iomap_dio_bio_end_io(struct bio *bio, bool inline_completion)
+static inline void iomap_dio_bio_release_pages(struct bio *bio,
+ unsigned int dio_flags, bool error)
{
- struct iomap_dio *dio = bio->bi_private;
-
- if (dio->flags & IOMAP_DIO_BOUNCE) {
- bio_iov_iter_unbounce(bio, !!dio->error,
- dio->flags & IOMAP_DIO_USER_BACKED);
+ if (dio_flags & IOMAP_DIO_BOUNCE) {
+ bio_iov_iter_unbounce(bio, error,
+ dio_flags & IOMAP_DIO_USER_BACKED);
bio_put(bio);
- } else if (dio->flags & IOMAP_DIO_USER_BACKED) {
+ } else if (dio_flags & IOMAP_DIO_USER_BACKED) {
bio_check_pages_dirty(bio);
} else {
bio_release_pages(bio, false);
bio_put(bio);
}
+}
+
+static void __iomap_dio_bio_end_io(struct bio *bio, bool inline_completion)
+{
+ struct iomap_dio *dio = bio->bi_private;
+
+ iomap_dio_bio_release_pages(bio, dio->flags, !!dio->error);
/* Do not touch bio below, we just gave up our reference. */
@@ -387,6 +396,14 @@ static ssize_t iomap_dio_bio_iter_one(struct iomap_iter *iter,
return ret;
}
+static inline unsigned int iomap_dio_alignment(struct inode *inode,
+ struct block_device *bdev, unsigned int dio_flags)
+{
+ if (dio_flags & IOMAP_DIO_FSBLOCK_ALIGNED)
+ return i_blocksize(inode);
+ return bdev_logical_block_size(bdev);
+}
+
static int iomap_dio_bio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
{
const struct iomap *iomap = &iter->iomap;
@@ -405,10 +422,7 @@ static int iomap_dio_bio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
* File systems that write out of place and always allocate new blocks
* need each bio to be block aligned as that's the unit of allocation.
*/
- if (dio->flags & IOMAP_DIO_FSBLOCK_ALIGNED)
- alignment = fs_block_size;
- else
- alignment = bdev_logical_block_size(iomap->bdev);
+ alignment = iomap_dio_alignment(inode, iomap->bdev, dio->flags);
if ((pos | length) & (alignment - 1))
return -EINVAL;
@@ -880,12 +894,350 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
}
EXPORT_SYMBOL_GPL(__iomap_dio_rw);
+struct iomap_dio_simple_read {
+ struct kiocb *iocb;
+ size_t size;
+ unsigned int dio_flags;
+ atomic_t state;
+ union {
+ struct task_struct *waiter;
+ struct work_struct work;
+ };
+ /*
+ * Align @bio to a cacheline boundary so that, combined with the
+ * front_pad passed to bioset_init(), the bio sits at the start of
+ * a cacheline in memory returned by the (HWCACHE-aligned) bio
+ * slab. This keeps the hot fields block layer touches on submit
+ * and completion (bi_iter, bi_status, ...) within a single line.
+ */
+ struct bio bio ____cacheline_aligned_in_smp;
+};
+
+static struct bio_set iomap_dio_simple_read_pool;
+
+/*
+ * In the async simple read path, we need to prevent bio_endio() from
+ * triggering iocb->ki_complete() before the submitter has returned
+ * -EIOCBQUEUED. Otherwise, the caller might free the iocb concurrently.
+ *
+ * We use a three-state rendezvous to synchronize the submitter and end_io:
+ *
+ * IOMAP_DIO_SIMPLE_SUBMITTING: Initial state set before submitting the bio.
+ *
+ * IOMAP_DIO_SIMPLE_QUEUED: The submitter has safely queued the IO and will
+ * return -EIOCBQUEUED. If end_io sees this state, it takes over and calls
+ * ki_complete().
+ *
+ * IOMAP_DIO_SIMPLE_DONE: end_io fired before the submitter finished the
+ * submit path. end_io sets this state and does nothing else. The submitter
+ * will see this state and handle the completion synchronously (bypassing
+ * ki_complete() and returning the actual result).
+ */
+enum {
+ IOMAP_DIO_SIMPLE_SUBMITTING = 0,
+ IOMAP_DIO_SIMPLE_QUEUED,
+ IOMAP_DIO_SIMPLE_DONE,
+};
+
+static ssize_t iomap_dio_simple_read_finish(struct kiocb *iocb,
+ struct bio *bio, ssize_t ret)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ struct iomap_dio_simple_read *sr = bio->bi_private;
+
+ if (likely(!ret)) {
+ ret = sr->size;
+ iocb->ki_pos += ret;
+ } else {
+ fserror_report_io(inode, FSERR_DIRECTIO_READ, iocb->ki_pos,
+ sr->size, ret, GFP_NOFS);
+ }
+
+ iomap_dio_bio_release_pages(bio, sr->dio_flags, ret < 0);
+
+ return ret;
+}
+
+static ssize_t iomap_dio_simple_read_complete(struct kiocb *iocb,
+ struct bio *bio)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ ssize_t ret;
+
+ WRITE_ONCE(iocb->private, NULL);
+
+ ret = iomap_dio_simple_read_finish(iocb, bio,
+ blk_status_to_errno(bio->bi_status));
+
+ inode_dio_end(inode);
+ trace_iomap_dio_complete(iocb, ret < 0 ? ret : 0, ret > 0 ? ret : 0);
+ return ret;
+}
+
+static void iomap_dio_simple_read_complete_work(struct work_struct *work)
+{
+ struct iomap_dio_simple_read *sr =
+ container_of(work, struct iomap_dio_simple_read, work);
+ struct kiocb *iocb = sr->iocb;
+ ssize_t ret;
+
+ ret = iomap_dio_simple_read_complete(iocb, &sr->bio);
+ iocb->ki_complete(iocb, ret);
+}
+
+static void iomap_dio_simple_read_async_done(struct iomap_dio_simple_read *sr)
+{
+ struct kiocb *iocb = sr->iocb;
+
+ if (unlikely(sr->bio.bi_status)) {
+ struct inode *inode = file_inode(iocb->ki_filp);
+
+ INIT_WORK(&sr->work, iomap_dio_simple_read_complete_work);
+ queue_work(inode->i_sb->s_dio_done_wq, &sr->work);
+ return;
+ }
+
+ iomap_dio_simple_read_complete_work(&sr->work);
+}
+
+static void iomap_dio_simple_read_end_io(struct bio *bio)
+{
+ struct iomap_dio_simple_read *sr = bio->bi_private;
+
+ if (sr->waiter) {
+ struct task_struct *waiter = sr->waiter;
+
+ WRITE_ONCE(sr->waiter, NULL);
+ blk_wake_io_task(waiter);
+ return;
+ }
+
+ if (likely(atomic_read(&sr->state) == IOMAP_DIO_SIMPLE_QUEUED) ||
+ atomic_cmpxchg(&sr->state, IOMAP_DIO_SIMPLE_SUBMITTING,
+ IOMAP_DIO_SIMPLE_DONE) == IOMAP_DIO_SIMPLE_QUEUED)
+ iomap_dio_simple_read_async_done(sr);
+}
+
+static inline bool iomap_dio_simple_read_supported(struct kiocb *iocb,
+ struct iov_iter *iter, unsigned int dio_flags)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ size_t count = iov_iter_count(iter);
+
+ if (iov_iter_rw(iter) != READ)
+ return false;
+ /*
+ * Simple read is an optimization for small IO. Filter out large IO
+ * early as it's the most common case to fail for typical direct IO
+ * workloads.
+ */
+ if (count > inode->i_sb->s_blocksize)
+ return false;
+ if (dio_flags & (IOMAP_DIO_FORCE_WAIT | IOMAP_DIO_PARTIAL))
+ return false;
+ if (iocb->ki_pos + count > i_size_read(inode))
+ return false;
+
+ return true;
+}
+
+static ssize_t iomap_dio_simple_read(struct kiocb *iocb,
+ struct iov_iter *iter, const struct iomap_ops *ops,
+ void *private, unsigned int dio_flags)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ size_t count = iov_iter_count(iter);
+ int nr_pages;
+ struct iomap_dio_simple_read *sr;
+ unsigned int alignment;
+ struct iomap_iter iomi = {
+ .inode = inode,
+ .pos = iocb->ki_pos,
+ .len = count,
+ .flags = IOMAP_DIRECT,
+ .private = private,
+ };
+ struct bio *bio;
+ bool wait_for_completion = is_sync_kiocb(iocb);
+ ssize_t ret;
+
+ if (dio_flags & IOMAP_DIO_BOUNCE)
+ nr_pages = bio_iov_bounce_nr_vecs(iter, REQ_OP_READ);
+ else
+ nr_pages = bio_iov_vecs_to_alloc(iter, BIO_MAX_VECS);
+
+ if (iocb->ki_flags & IOCB_NOWAIT)
+ iomi.flags |= IOMAP_NOWAIT;
+
+ ret = kiocb_write_and_wait(iocb, count);
+ if (ret)
+ return ret;
+
+ inode_dio_begin(inode);
+
+ ret = ops->iomap_begin(inode, iomi.pos, count, iomi.flags,
+ &iomi.iomap, &iomi.srcmap);
+ if (ret) {
+ inode_dio_end(inode);
+ return ret;
+ }
+
+ if (iomi.iomap.type != IOMAP_MAPPED ||
+ iomi.iomap.offset > iomi.pos ||
+ iomi.iomap.offset + iomi.iomap.length < iomi.pos + count) {
+ ret = -ENOTBLK;
+ goto out_iomap_end;
+ }
+
+ alignment = iomap_dio_alignment(inode, iomi.iomap.bdev, dio_flags);
+ if ((iomi.pos | count) & (alignment - 1)) {
+ ret = -EINVAL;
+ goto out_iomap_end;
+ }
+
+ if (unlikely(!inode->i_sb->s_dio_done_wq)) {
+ ret = sb_init_dio_done_wq(inode->i_sb);
+ if (ret < 0)
+ goto out_iomap_end;
+ }
+
+ trace_iomap_dio_rw_begin(iocb, iter, dio_flags, 0);
+
+ if (user_backed_iter(iter))
+ dio_flags |= IOMAP_DIO_USER_BACKED;
+
+ bio = bio_alloc_bioset(iomi.iomap.bdev, nr_pages,
+ REQ_OP_READ | REQ_SYNC | REQ_IDLE,
+ GFP_KERNEL, &iomap_dio_simple_read_pool);
+ sr = container_of(bio, struct iomap_dio_simple_read, bio);
+
+ fscrypt_set_bio_crypt_ctx(bio, inode, iomi.pos >> inode->i_blkbits,
+ GFP_KERNEL);
+ sr->iocb = iocb;
+ sr->dio_flags = dio_flags;
+
+ bio->bi_iter.bi_sector = iomap_sector(&iomi.iomap, iomi.pos);
+ bio->bi_ioprio = iocb->ki_ioprio;
+ bio->bi_private = sr;
+ bio->bi_end_io = iomap_dio_simple_read_end_io;
+
+ if (dio_flags & IOMAP_DIO_BOUNCE)
+ ret = bio_iov_iter_bounce(bio, iter);
+ else
+ ret = bio_iov_iter_get_pages(bio, iter, alignment - 1);
+ if (unlikely(ret))
+ goto out_bio_put;
+
+ if (bio->bi_iter.bi_size != count) {
+ iov_iter_revert(iter, bio->bi_iter.bi_size);
+ ret = -ENOTBLK;
+ goto out_bio_release_pages;
+ }
+
+ sr->size = bio->bi_iter.bi_size;
+
+ if ((dio_flags & IOMAP_DIO_USER_BACKED) &&
+ !(dio_flags & IOMAP_DIO_BOUNCE))
+ bio_set_pages_dirty(bio);
+
+ if (iocb->ki_flags & IOCB_NOWAIT)
+ bio->bi_opf |= REQ_NOWAIT;
+ if ((iocb->ki_flags & IOCB_HIPRI) && !wait_for_completion) {
+ bio->bi_opf |= REQ_POLLED;
+ bio_set_polled(bio, iocb);
+ WRITE_ONCE(iocb->private, bio);
+ }
+
+ if (wait_for_completion) {
+ sr->waiter = current;
+ blk_crypto_submit_bio(bio);
+ } else {
+ atomic_set(&sr->state, IOMAP_DIO_SIMPLE_SUBMITTING);
+ sr->waiter = NULL;
+ blk_crypto_submit_bio(bio);
+ ret = -EIOCBQUEUED;
+ }
+
+ if (ops->iomap_end)
+ ops->iomap_end(inode, iomi.pos, count, count, iomi.flags,
+ &iomi.iomap);
+
+ if (wait_for_completion) {
+ for (;;) {
+ set_current_state(TASK_UNINTERRUPTIBLE);
+ if (!READ_ONCE(sr->waiter))
+ break;
+ blk_io_schedule();
+ }
+ __set_current_state(TASK_RUNNING);
+
+ ret = iomap_dio_simple_read_finish(iocb, bio,
+ blk_status_to_errno(bio->bi_status));
+ inode_dio_end(inode);
+ trace_iomap_dio_complete(iocb, ret < 0 ? ret : 0,
+ ret > 0 ? ret : 0);
+ } else if (atomic_cmpxchg(&sr->state, IOMAP_DIO_SIMPLE_SUBMITTING,
+ IOMAP_DIO_SIMPLE_QUEUED) ==
+ IOMAP_DIO_SIMPLE_DONE) {
+ ret = iomap_dio_simple_read_complete(iocb, bio);
+ } else {
+ trace_iomap_dio_rw_queued(inode, iomi.pos, count);
+ }
+
+ return ret;
+
+out_bio_release_pages:
+ if (dio_flags & IOMAP_DIO_BOUNCE)
+ bio_iov_iter_unbounce(bio, true, false);
+ else
+ bio_release_pages(bio, false);
+out_bio_put:
+ bio_put(bio);
+out_iomap_end:
+ if (ops->iomap_end)
+ ops->iomap_end(inode, iomi.pos, count, 0, iomi.flags,
+ &iomi.iomap);
+ inode_dio_end(inode);
+ return ret;
+}
+
ssize_t
iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
const struct iomap_ops *ops, const struct iomap_dio_ops *dops,
unsigned int dio_flags, void *private, size_t done_before)
{
struct iomap_dio *dio;
+ ssize_t ret;
+
+ /*
+ * Fast path for small, block-aligned reads that map to a single
+ * contiguous on-disk extent.
+ *
+ * @dops must be NULL: a non-NULL @dops means the caller wants its
+ * ->end_io / ->submit_io hooks invoked, and in particular wants its
+ * bios to be allocated from the filesystem-private @dops->bio_set
+ * (whose front_pad sizes a filesystem-private wrapper around the
+ * bio). The fast path instead allocates from the shared
+ * iomap_dio_simple_read_pool, whose front_pad matches
+ * struct iomap_dio_simple_read; the two wrappers are not
+ * interchangeable, so we must fall back to __iomap_dio_rw() in
+ * that case.
+ *
+ * @done_before must be zero: a non-zero caller-accumulated residual
+ * cannot be carried through a single-bio inline completion.
+ *
+ * -ENOTBLK is the private sentinel returned by iomap_dio_simple_read()
+ * when it decides the request does not fit the fast path.
+ * In that case we proceed to the generic __iomap_dio_rw() slow
+ * path. Any other errno is a real result and is propagated as-is,
+ * in particular -EAGAIN for IOCB_NOWAIT must reach the caller.
+ */
+ if (!dops && !done_before &&
+ iomap_dio_simple_read_supported(iocb, iter, dio_flags)) {
+ ret = iomap_dio_simple_read(iocb, iter, ops, private, dio_flags);
+ if (ret != -ENOTBLK)
+ return ret;
+ }
dio = __iomap_dio_rw(iocb, iter, ops, dops, dio_flags, private,
done_before);
@@ -894,3 +1246,11 @@ iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
return iomap_dio_complete(dio);
}
EXPORT_SYMBOL_GPL(iomap_dio_rw);
+
+static int __init iomap_dio_init(void)
+{
+ return bioset_init(&iomap_dio_simple_read_pool, 4,
+ offsetof(struct iomap_dio_simple_read, bio),
+ BIOSET_NEED_BVECS | BIOSET_PERCPU_CACHE);
+}
+fs_initcall(iomap_dio_init);
--
2.39.5 (Apple Git-154)
^ permalink raw reply related
* Re: [PATCH v2] generic/790: test post-EOF gap zeroing persistence
From: Zhang Yi @ 2026-04-28 8:52 UTC (permalink / raw)
To: Brian Foster
Cc: fstests, zlang, linux-ext4, linux-fsdevel, jack, yi.zhang,
yizhang089, yangerkun
In-Reply-To: <ae9euwyg53TRutsK@bfoster>
On 4/27/2026 9:03 PM, Brian Foster wrote:
> On Sat, Apr 25, 2026 at 11:06:27AM +0800, Zhang Yi wrote:
>> On 4/24/2026 9:09 PM, Brian Foster wrote:
>>> On Fri, Apr 24, 2026 at 05:22:28PM +0800, Zhang Yi wrote:
>>>> From: Zhang Yi <yi.zhang@huawei.com>
>>>>
>>>> Test that extending a file past a non-block-aligned EOF correctly
>>>> zero-fills the gap [old_EOF, block_boundary), and that this zeroing
>>>> persists through a filesystem shutdown+remount cycle.
>>>>
>>>> Stale data beyond EOF can persist on disk when append write data blocks
>>>> are flushed before the on-disk file size update, or when concurrent
>>>> append writeback and mmap writes persist non-zero data past EOF.
>>>> Subsequent post-EOF operations (append write, fallocate, truncate up)
>>>> must zero-fill and persist the gap to prevent exposing stale data.
>>>>
>>>> The test pollutes the file's last physical block (via FIEMAP + raw
>>>> device write) with a sentinel pattern beyond i_size, then performs each
>>>> extend operation and verifies the gap is zeroed both in memory and on
>>>> disk.
>>>>
>>>> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
>>>> ---
>>>> v1->v2:
>>>> - Add _require_no_realtime to prevent testing on XFS realtime devices,
>>>> where file data may reside on $SCRATCH_RTDEV.
>>>> - Add _exclude_fs btrfs since FIEMAP returns logical addresses, not
>>>> physical device offsets, writing to these offsets on $SCRATCH_DEV
>>>> would corrupt the filesystem in multi-device setups. Besides, since
>>>> btrfs doesn't support shutdown right now, we can support it later.
>>>> - Add -v flag to od in _check_gap_zero() to prevent line folding of
>>>> identical consecutive lines.
>>>> - Add expected_new_sz parameter to _test_eof_zeroing(), verify file
>>>> size was not rolled back after shutdown+remount cycle, and also drop
>>>> the unnecessary file size check before the shutdown as well.
>>>> - Clarify the comment regarding when stale data beyond EOF can persist.
>>>>
>>>
>>> Thanks for the tweaks. This all LGTM from a review standpoint. I gave it
>>> a quick test on latest master and I see a few failures in a couple runs:
>>>
>>> - On XFS (mkfs defaults) I saw one unexpected i_size failure and one
>>> zeroing failure, both on write extension fwiw.
>>
>> Previously, I only discovered the zeroing failure of append write. This
>> is because xfs_file_write_checks() -> xfs_file_write_zero_eof() only
>> zeroes the gap range in the page cache, without providing any
>> synchronous or asynchronous persistence (instead, truncate up does
>> synchronously writeback in xfs_vn_setattr_size(), and ext4 achieves
>> persistence via asynchronous writeback in data=ordered mode). So I think
>> this is a XFS problem.
>>
>
> Ah Ok.. the truncate flush has been there for a while to combat the NULL
> files problem, and the zeroing check ties into that. I suspect the
> reason we don't see this often is typical ascending offset writeback
> order, whereas this test is doing a targeted sync range of a later
> offset in the file (i.e. technically not overlapping with the range that
> was zeroed internally).
Yes, this use case is quite rare.
>
> I guess the simple thing to do there would be to similarly flush on
> write extension. I'd want to think about that just a bit because we
> haven't historically flushed there and I know the zeroing improvements
> I've made in iomap have caused a couple performance regressions along
> the way related to aggressive flushing that had to be fixed.
I am also concerned that synchronous write-back might introduce
performance regressions. Although the probability is relatively low
(I'm not sure), perhaps some users who use post-EOF writing will
complain.
In this corner case, ext4+iomap also encountered the same dilemma, as it
can no longer use data=ordered to achieve asynchronous persistence.
FYI, the current approach of ext4+iomap is to submit an I/O immediately
after zeroing the EOF block, but it does not wait for the I/O to
complete. In the subsequent endio process, it waits for this zero I/O to
complete before updating i_disksize beyond the end of the gap.
Best Regards,
Yi
>
> Maybe this is less of an issue now, but regardless if my assumptions are
> correct here than I agree this isn't a test issue.
>
>> Regarding the i_size failure, I did not directly reproduce this issue.
>> After analysis, I believe it is because the test case did not include
>> the -a option in sync_range, meaning it did not wait for IO writeback
>> completion and file size update persistence. I reproduced this issue by
>> adding a delay in the XFS end IO path. This is a problem with the test
>> case, and I will fix it in v3. Thank you for pointing this out.
>>
>
> Makes sense. It will be interesting to see if we can still reproduce the
> above issue with this change.
>
>>> - On ext4 I saw a few unexpected i_size failures (both with mkfs
>>> defaults and 1k block size).
>>>
>>
>> This is an ext4 issue on the shutdown path. Since ext4 set the shutdown
>> flag too early, it was unable to write back ordered zero data when
>> flushing the journal, which led to a journal abort and prevented the file
>> size update from being persisted. I have submitted a patch to fix this
>> issue. Please see below link for details.
>>
>> https://lore.kernel.org/linux-ext4/20260424104201.1930823-1-yi.zhang@huaweicloud.com/
>>
>
> Ok, thanks.
>
> Brian
>
>> Thanks
>> Yi.
>>
>>> I haven't dug into anything beyond that. Does this match what you're
>>> seeing on current kernels or are these unexpected failures?
>>>
>>> Brian
>>>
>>>> tests/generic/790 | 164 ++++++++++++++++++++++++++++++++++++++++++
>>>> tests/generic/790.out | 4 ++
>>>> 2 files changed, 168 insertions(+)
>>>> create mode 100755 tests/generic/790
>>>> create mode 100644 tests/generic/790.out
>>>>
>>>> diff --git a/tests/generic/790 b/tests/generic/790
>>>> new file mode 100755
>>>> index 00000000..2adc06f8
>>>> --- /dev/null
>>>> +++ b/tests/generic/790
>>>> @@ -0,0 +1,164 @@
>>>> +#! /bin/bash
>>>> +# SPDX-License-Identifier: GPL-2.0
>>>> +# Copyright (c) 2026 Huawei. All Rights Reserved.
>>>> +#
>>>> +# FS QA Test No. 790
>>>> +#
>>>> +# Test that extending a file past a non-block-aligned EOF correctly zero-fills
>>>> +# the gap [old_EOF, block_boundary), and that this zeroing persists through a
>>>> +# filesystem shutdown+remount cycle.
>>>> +#
>>>> +# Stale data beyond EOF can persist on disk when:
>>>> +# 1) append write data blocks are flushed before the on-disk file size update,
>>>> +# and the system crashes in this window.
>>>> +# 2) concurrent append writeback and mmap writes persist non-zero data past EOF.
>>>> +#
>>>> +# Subsequent post-EOF operations (append write, fallocate, truncate up) must
>>>> +# zero-fill and persist the gap to prevent exposing stale data.
>>>> +#
>>>> +# The test pollutes the file's last physical block (via FIEMAP + raw device
>>>> +# write) with a sentinel pattern beyond i_size, then performs each extend
>>>> +# operation and verifies the gap is zeroed both in memory and on disk.
>>>> +#
>>>> +. ./common/preamble
>>>> +_begin_fstest auto quick rw shutdown
>>>> +
>>>> +. ./common/filter
>>>> +
>>>> +_require_scratch
>>>> +_require_block_device $SCRATCH_DEV
>>>> +_require_no_realtime
>>>> +_require_scratch_shutdown
>>>> +_require_metadata_journaling $SCRATCH_DEV
>>>> +
>>>> +# FIEMAP on Btrfs returns logical addresses within the filesystem's address
>>>> +# space, not physical device offsets. Writing to these offsets on $SCRATCH_DEV
>>>> +# would corrupt the filesystem in multi-device setups.
>>>> +_exclude_fs btrfs
>>>> +
>>>> +_require_xfs_io_command "fiemap"
>>>> +_require_xfs_io_command "falloc"
>>>> +_require_xfs_io_command "pwrite"
>>>> +_require_xfs_io_command "truncate"
>>>> +_require_xfs_io_command "sync_range"
>>>> +
>>>> +# Check that gap region [offset, offset+nbytes) is entirely zero
>>>> +_check_gap_zero()
>>>> +{
>>>> + local file="$1"
>>>> + local offset="$2"
>>>> + local nbytes="$3"
>>>> + local label="$4"
>>>> + local data
>>>> + local stripped
>>>> +
>>>> + data=$(od -A n -t x1 -v -j $offset -N $nbytes "$file" 2>/dev/null)
>>>> +
>>>> + # Remove whitespace and check if any byte is non-zero
>>>> + stripped=$(printf '%s' "$data" | tr -d ' \n\t')
>>>> + if [ -n "$stripped" ] && ! echo "$stripped" | grep -qE "^0+$"; then
>>>> + echo "FAIL: non-zero data in gap [$offset,$((offset + nbytes))) $label"
>>>> + _hexdump -N $((offset + nbytes)) "$file"
>>>> + return 1
>>>> + fi
>>>> + return 0
>>>> +}
>>>> +
>>>> +# Get the physical block offset (in bytes) of the file's first block on device
>>>> +_get_phys_offset()
>>>> +{
>>>> + local file="$1"
>>>> + local fiemap_output
>>>> + local phys_blk
>>>> +
>>>> + fiemap_output=$($XFS_IO_PROG -r -c "fiemap -v" "$file" 2>/dev/null)
>>>> + phys_blk=$(echo "$fiemap_output" | _filter_xfs_io_fiemap | head -1 | awk '{print $3}')
>>>> + if [ -z "$phys_blk" ]; then
>>>> + echo ""
>>>> + return
>>>> + fi
>>>> + # Convert 512-byte blocks to bytes
>>>> + echo $((phys_blk * 512))
>>>> +}
>>>> +
>>>> +_test_eof_zeroing()
>>>> +{
>>>> + local test_name="$1"
>>>> + local extend_cmd="$2"
>>>> + local expected_new_sz="$3"
>>>> + local file=$SCRATCH_MNT/testfile_${test_name}
>>>> +
>>>> + echo "$test_name" | tee -a $seqres.full
>>>> +
>>>> + # Compute non-block-aligned EOF offset
>>>> + local gap_bytes=16
>>>> + local eof_offset=$((blksz - gap_bytes))
>>>> +
>>>> + # Step 1: Write one full block to ensure the filesystem allocates a
>>>> + # physical block for the file instead of using inline data.
>>>> + $XFS_IO_PROG -f -c "pwrite -S 0x5a 0 $blksz" -c fsync \
>>>> + "$file" >> $seqres.full 2>&1
>>>> +
>>>> + # Step 2: Get physical block offset on device via FIEMAP
>>>> + local phys_offset
>>>> + phys_offset=$(_get_phys_offset "$file")
>>>> + if [ -z "$phys_offset" ]; then
>>>> + _fail "$test_name: failed to get physical block offset via fiemap"
>>>> + fi
>>>> +
>>>> + # Step 3: Truncate file to non-block-aligned size and fsync.
>>>> + # The on-disk region [eof_offset, blksz) may or may not be
>>>> + # zeroed by the filesystem at this point.
>>>> + $XFS_IO_PROG -c "truncate $eof_offset" -c fsync \
>>>> + "$file" >> $seqres.full 2>&1
>>>> +
>>>> + # Step 4: Unmount and restore the physical block to all-0x5a on disk.
>>>> + # This bypasses the kernel's pagecache EOF-zeroing to ensure
>>>> + # the stale pattern is present on disk. Then remount.
>>>> + _scratch_unmount
>>>> + $XFS_IO_PROG -d -c "pwrite -S 0x5a $phys_offset $blksz" \
>>>> + $SCRATCH_DEV >> $seqres.full 2>&1
>>>> + _scratch_mount >> $seqres.full 2>&1
>>>> +
>>>> + # Step 5: Execute the extend operation.
>>>> + $XFS_IO_PROG -c "$extend_cmd" "$file" >> $seqres.full 2>&1
>>>> +
>>>> + # Step 6: Verify gap [eof_offset, blksz) is zeroed BEFORE shutdown
>>>> + _check_gap_zero "$file" $eof_offset $gap_bytes "before shutdown" || return 1
>>>> +
>>>> + # Step 7: Sync the extended range and shutdown the filesystem with
>>>> + # journal flush. This persists the file size extending, and
>>>> + # the filesystem should persist the zeroed data in the gap
>>>> + # range as well.
>>>> + if [ "$extend_cmd" != "${extend_cmd#pwrite}" ]; then
>>>> + $XFS_IO_PROG -c "sync_range -w $blksz $blksz" \
>>>> + "$file" >> $seqres.full 2>&1
>>>> + fi
>>>> + _scratch_shutdown -f
>>>> +
>>>> + # Step 8: Remount and verify gap is still zeroed
>>>> + _scratch_cycle_mount
>>>> +
>>>> + # Verify file size was not rolled back after shutdown+remount
>>>> + local sz
>>>> + sz=$(stat -c %s "$file")
>>>> + if [ "$sz" -ne "$expected_new_sz" ]; then
>>>> + _fail "$test_name: file size rolled back after shutdown+remount: $sz != $expected_new_sz"
>>>> + fi
>>>> +
>>>> + _check_gap_zero "$file" $eof_offset $gap_bytes "after shutdown+remount" || return 1
>>>> +}
>>>> +
>>>> +_scratch_mkfs >> $seqres.full 2>&1
>>>> +_scratch_mount
>>>> +
>>>> +blksz=$(_get_block_size $SCRATCH_MNT)
>>>> +
>>>> +# Test three variants of EOF-extending operations
>>>> +_test_eof_zeroing "append_write" "pwrite -S 0x42 $blksz $blksz" $((blksz * 2))
>>>> +_test_eof_zeroing "truncate_up" "truncate $((blksz * 2))" $((blksz * 2))
>>>> +_test_eof_zeroing "fallocate" "falloc $blksz $blksz" $((blksz * 2))
>>>> +
>>>> +# success, all done
>>>> +status=0
>>>> +exit
>>>> diff --git a/tests/generic/790.out b/tests/generic/790.out
>>>> new file mode 100644
>>>> index 00000000..e5e2cc09
>>>> --- /dev/null
>>>> +++ b/tests/generic/790.out
>>>> @@ -0,0 +1,4 @@
>>>> +QA output created by 790
>>>> +append_write
>>>> +truncate_up
>>>> +fallocate
>>>> --
>>>> 2.52.0
>>>>
>>
>
^ permalink raw reply
* [PATCH v3] generic/790: test post-EOF gap zeroing persistence
From: Zhang Yi @ 2026-04-28 8:57 UTC (permalink / raw)
To: fstests, zlang
Cc: linux-ext4, linux-fsdevel, bfoster, jack, yi.zhang, yi.zhang,
yizhang089, yangerkun
From: Zhang Yi <yi.zhang@huawei.com>
Test that extending a file past a non-block-aligned EOF correctly
zero-fills the gap [old_EOF, block_boundary), and that this zeroing
persists through a filesystem shutdown+remount cycle.
Stale data beyond EOF can persist on disk when append write data blocks
are flushed before the on-disk file size update, or when concurrent
append writeback and mmap writes persist non-zero data past EOF.
Subsequent post-EOF operations (append write, fallocate, truncate up)
must zero-fill and persist the gap to prevent exposing stale data.
The test pollutes the file's last physical block (via FIEMAP + raw
device write) with a sentinel pattern beyond i_size, then performs each
extend operation and verifies the gap is zeroed both in memory and on
disk.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
v2->v3:
- Add error check for the raw device pwrite, a failed pwrite would
silently leave the test continuing with an unpolluted block,
producing false-positive passes.
- Add sync_range -a to wait until the extending I/O completes and to
ensure file size update is persisted before shutdown, preventing
unexpected file size errors.
v1->v2:
- Add _require_no_realtime to prevent testing on XFS realtime devices,
where file data may reside on $SCRATCH_RTDEV.
- Add _exclude_fs btrfs since FIEMAP returns logical addresses, not
physical device offsets, writing to these offsets on $SCRATCH_DEV
would corrupt the filesystem in multi-device setups. Besides, since
btrfs doesn't support shutdown right now, we can support it later.
- Add -v flag to od in _check_gap_zero() to prevent line folding of
identical consecutive lines.
- Add expected_new_sz parameter to _test_eof_zeroing(), verify file
size was not rolled back after shutdown+remount cycle, and also drop
the unnecessary file size check before the shutdown as well.
- Clarify the comment regarding when stale data beyond EOF can persist.
tests/generic/790 | 168 ++++++++++++++++++++++++++++++++++++++++++
tests/generic/790.out | 4 +
2 files changed, 172 insertions(+)
create mode 100755 tests/generic/790
create mode 100644 tests/generic/790.out
diff --git a/tests/generic/790 b/tests/generic/790
new file mode 100755
index 00000000..6daf3793
--- /dev/null
+++ b/tests/generic/790
@@ -0,0 +1,168 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Huawei. All Rights Reserved.
+#
+# FS QA Test No. 790
+#
+# Test that extending a file past a non-block-aligned EOF correctly zero-fills
+# the gap [old_EOF, block_boundary), and that this zeroing persists through a
+# filesystem shutdown+remount cycle.
+#
+# Stale data beyond EOF can persist on disk when:
+# 1) append write data blocks are flushed before the on-disk file size update,
+# and the system crashes in this window.
+# 2) concurrent append writeback and mmap writes persist non-zero data past EOF.
+#
+# Subsequent post-EOF operations (append write, fallocate, truncate up) must
+# zero-fill and persist the gap to prevent exposing stale data.
+#
+# The test pollutes the file's last physical block (via FIEMAP + raw device
+# write) with a sentinel pattern beyond i_size, then performs each extend
+# operation and verifies the gap is zeroed both in memory and on disk.
+#
+. ./common/preamble
+_begin_fstest auto quick rw shutdown
+
+. ./common/filter
+
+_require_scratch
+_require_block_device $SCRATCH_DEV
+_require_no_realtime
+_require_scratch_shutdown
+_require_metadata_journaling $SCRATCH_DEV
+
+# FIEMAP on Btrfs returns logical addresses within the filesystem's address
+# space, not physical device offsets. Writing to these offsets on $SCRATCH_DEV
+# would corrupt the filesystem in multi-device setups.
+_exclude_fs btrfs
+
+_require_xfs_io_command "fiemap"
+_require_xfs_io_command "falloc"
+_require_xfs_io_command "pwrite"
+_require_xfs_io_command "truncate"
+_require_xfs_io_command "sync_range"
+
+# Check that gap region [offset, offset+nbytes) is entirely zero
+_check_gap_zero()
+{
+ local file="$1"
+ local offset="$2"
+ local nbytes="$3"
+ local label="$4"
+ local data
+ local stripped
+
+ data=$(od -A n -t x1 -v -j $offset -N $nbytes "$file" 2>/dev/null)
+
+ # Remove whitespace and check if any byte is non-zero
+ stripped=$(printf '%s' "$data" | tr -d ' \n\t')
+ if [ -n "$stripped" ] && ! echo "$stripped" | grep -qE "^0+$"; then
+ echo "FAIL: non-zero data in gap [$offset,$((offset + nbytes))) $label"
+ _hexdump -N $((offset + nbytes)) "$file"
+ return 1
+ fi
+ return 0
+}
+
+# Get the physical block offset (in bytes) of the file's first block on device
+_get_phys_offset()
+{
+ local file="$1"
+ local fiemap_output
+ local phys_blk
+
+ fiemap_output=$($XFS_IO_PROG -r -c "fiemap -v" "$file" 2>/dev/null)
+ phys_blk=$(echo "$fiemap_output" | _filter_xfs_io_fiemap | head -1 | awk '{print $3}')
+ if [ -z "$phys_blk" ]; then
+ echo ""
+ return
+ fi
+ # Convert 512-byte blocks to bytes
+ echo $((phys_blk * 512))
+}
+
+_test_eof_zeroing()
+{
+ local test_name="$1"
+ local extend_cmd="$2"
+ local expected_new_sz="$3"
+ local file=$SCRATCH_MNT/testfile_${test_name}
+
+ echo "$test_name" | tee -a $seqres.full
+
+ # Compute non-block-aligned EOF offset
+ local gap_bytes=16
+ local eof_offset=$((blksz - gap_bytes))
+
+ # Step 1: Write one full block to ensure the filesystem allocates a
+ # physical block for the file instead of using inline data.
+ $XFS_IO_PROG -f -c "pwrite -S 0x5a 0 $blksz" -c fsync \
+ "$file" >> $seqres.full 2>&1
+
+ # Step 2: Get physical block offset on device via FIEMAP
+ local phys_offset
+ phys_offset=$(_get_phys_offset "$file")
+ if [ -z "$phys_offset" ]; then
+ _fail "$test_name: failed to get physical block offset via fiemap"
+ fi
+
+ # Step 3: Truncate file to non-block-aligned size and fsync.
+ # The on-disk region [eof_offset, blksz) may or may not be
+ # zeroed by the filesystem at this point.
+ $XFS_IO_PROG -c "truncate $eof_offset" -c fsync \
+ "$file" >> $seqres.full 2>&1
+
+ # Step 4: Unmount and restore the physical block to all-0x5a on disk.
+ # This bypasses the kernel's pagecache EOF-zeroing to ensure
+ # the stale pattern is present on disk. Then remount.
+ _scratch_unmount
+ $XFS_IO_PROG -d -c "pwrite -S 0x5a $phys_offset $blksz" \
+ $SCRATCH_DEV >> $seqres.full 2>&1
+ if [ $? -ne 0 ]; then
+ _fail "$test_name: failed to inject stale data on disk"
+ fi
+ _scratch_mount >> $seqres.full 2>&1
+
+ # Step 5: Execute the extend operation.
+ $XFS_IO_PROG -c "$extend_cmd" "$file" >> $seqres.full 2>&1
+
+ # Step 6: Verify gap [eof_offset, blksz) is zeroed BEFORE shutdown
+ _check_gap_zero "$file" $eof_offset $gap_bytes "before shutdown" || return 1
+
+ # Step 7: Sync the extended range and shutdown the filesystem with
+ # journal flush. This persists the file size extending, and
+ # the filesystem should persist the zeroed data in the gap
+ # range as well.
+ if [ "$extend_cmd" != "${extend_cmd#pwrite}" ]; then
+ $XFS_IO_PROG -c "sync_range -w $blksz $blksz" \
+ -c "sync_range -a $blksz $blksz" \
+ "$file" >> $seqres.full 2>&1
+ fi
+ _scratch_shutdown -f
+
+ # Step 8: Remount and verify gap is still zeroed
+ _scratch_cycle_mount
+
+ # Verify file size was not rolled back after shutdown+remount
+ local sz
+ sz=$(stat -c %s "$file")
+ if [ "$sz" -ne "$expected_new_sz" ]; then
+ _fail "$test_name: file size rolled back after shutdown+remount: $sz != $expected_new_sz"
+ fi
+
+ _check_gap_zero "$file" $eof_offset $gap_bytes "after shutdown+remount" || return 1
+}
+
+_scratch_mkfs >> $seqres.full 2>&1
+_scratch_mount
+
+blksz=$(_get_block_size $SCRATCH_MNT)
+
+# Test three variants of EOF-extending operations
+_test_eof_zeroing "append_write" "pwrite -S 0x42 $blksz $blksz" $((blksz * 2))
+_test_eof_zeroing "truncate_up" "truncate $((blksz * 2))" $((blksz * 2))
+_test_eof_zeroing "fallocate" "falloc $blksz $blksz" $((blksz * 2))
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/790.out b/tests/generic/790.out
new file mode 100644
index 00000000..e5e2cc09
--- /dev/null
+++ b/tests/generic/790.out
@@ -0,0 +1,4 @@
+QA output created by 790
+append_write
+truncate_up
+fallocate
--
2.52.0
^ permalink raw reply related
* [PATCH v9 22/22] xfs: enable ro-compat fs-verity flag
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Finalize fs-verity integration in XFS by making kernel fs-verity
aware with ro-compat flag.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
[djwong: add spaces]
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/libxfs/xfs_format.h | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/fs/xfs/libxfs/xfs_format.h b/fs/xfs/libxfs/xfs_format.h
index 4dff29659e40..0ce46c234b9c 100644
--- a/fs/xfs/libxfs/xfs_format.h
+++ b/fs/xfs/libxfs/xfs_format.h
@@ -378,8 +378,9 @@ xfs_sb_has_compat_feature(
#define XFS_SB_FEAT_RO_COMPAT_ALL \
(XFS_SB_FEAT_RO_COMPAT_FINOBT | \
XFS_SB_FEAT_RO_COMPAT_RMAPBT | \
- XFS_SB_FEAT_RO_COMPAT_REFLINK| \
- XFS_SB_FEAT_RO_COMPAT_INOBTCNT)
+ XFS_SB_FEAT_RO_COMPAT_REFLINK | \
+ XFS_SB_FEAT_RO_COMPAT_INOBTCNT | \
+ XFS_SB_FEAT_RO_COMPAT_VERITY)
#define XFS_SB_FEAT_RO_COMPAT_UNKNOWN ~XFS_SB_FEAT_RO_COMPAT_ALL
static inline bool
xfs_sb_has_ro_compat_feature(
--
2.51.2
^ permalink raw reply related
* [PATCH v9 21/22] xfs: introduce health state for corrupted fsverity metadata
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Report corrupted fsverity descriptor through health system.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/libxfs/xfs_fs.h | 1 +
fs/xfs/libxfs/xfs_health.h | 4 +++-
fs/xfs/xfs_fsverity.c | 13 ++++++++++---
fs/xfs/xfs_health.c | 1 +
4 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/fs/xfs/libxfs/xfs_fs.h b/fs/xfs/libxfs/xfs_fs.h
index ebf17a0b0722..cece31ecee81 100644
--- a/fs/xfs/libxfs/xfs_fs.h
+++ b/fs/xfs/libxfs/xfs_fs.h
@@ -422,6 +422,7 @@ struct xfs_bulkstat {
#define XFS_BS_SICK_SYMLINK (1 << 6) /* symbolic link remote target */
#define XFS_BS_SICK_PARENT (1 << 7) /* parent pointers */
#define XFS_BS_SICK_DIRTREE (1 << 8) /* directory tree structure */
+#define XFS_BS_SICK_FSVERITY (1 << 9) /* fsverity metadata */
/*
* Project quota id helpers (previously projid was 16bit only
diff --git a/fs/xfs/libxfs/xfs_health.h b/fs/xfs/libxfs/xfs_health.h
index 1d45cf5789e8..932b447190da 100644
--- a/fs/xfs/libxfs/xfs_health.h
+++ b/fs/xfs/libxfs/xfs_health.h
@@ -104,6 +104,7 @@ struct xfs_rtgroup;
/* Don't propagate sick status to ag health summary during inactivation */
#define XFS_SICK_INO_FORGET (1 << 12)
#define XFS_SICK_INO_DIRTREE (1 << 13) /* directory tree structure */
+#define XFS_SICK_INO_FSVERITY (1 << 14) /* fsverity metadata */
/* Primary evidence of health problems in a given group. */
#define XFS_SICK_FS_PRIMARY (XFS_SICK_FS_COUNTERS | \
@@ -140,7 +141,8 @@ struct xfs_rtgroup;
XFS_SICK_INO_XATTR | \
XFS_SICK_INO_SYMLINK | \
XFS_SICK_INO_PARENT | \
- XFS_SICK_INO_DIRTREE)
+ XFS_SICK_INO_DIRTREE | \
+ XFS_SICK_INO_FSVERITY)
#define XFS_SICK_INO_ZAPPED (XFS_SICK_INO_BMBTD_ZAPPED | \
XFS_SICK_INO_BMBTA_ZAPPED | \
diff --git a/fs/xfs/xfs_fsverity.c b/fs/xfs/xfs_fsverity.c
index 298d712b5ba2..82f5ca542c97 100644
--- a/fs/xfs/xfs_fsverity.c
+++ b/fs/xfs/xfs_fsverity.c
@@ -84,16 +84,23 @@ xfs_fsverity_get_descriptor(
return error;
desc_size = be32_to_cpu(d_desc_size);
- if (XFS_IS_CORRUPT(mp, desc_size > FS_VERITY_MAX_DESCRIPTOR_SIZE))
+ if (XFS_IS_CORRUPT(mp, desc_size > FS_VERITY_MAX_DESCRIPTOR_SIZE)) {
+ xfs_inode_mark_sick(XFS_I(inode), XFS_SICK_INO_FSVERITY);
return -ERANGE;
- if (XFS_IS_CORRUPT(mp, desc_size > desc_size_pos))
+ }
+
+ if (XFS_IS_CORRUPT(mp, desc_size > desc_size_pos)) {
+ xfs_inode_mark_sick(XFS_I(inode), XFS_SICK_INO_FSVERITY);
return -ERANGE;
+ }
if (!buf_size)
return desc_size;
- if (XFS_IS_CORRUPT(mp, desc_size > buf_size))
+ if (XFS_IS_CORRUPT(mp, desc_size > buf_size)) {
+ xfs_inode_mark_sick(XFS_I(inode), XFS_SICK_INO_FSVERITY);
return -ERANGE;
+ }
desc_pos = round_down(desc_size_pos - desc_size, blocksize);
error = fsverity_pagecache_read(inode, buf, desc_size, desc_pos);
diff --git a/fs/xfs/xfs_health.c b/fs/xfs/xfs_health.c
index 239b843e83d4..be66760fb120 100644
--- a/fs/xfs/xfs_health.c
+++ b/fs/xfs/xfs_health.c
@@ -625,6 +625,7 @@ static const struct ioctl_sick_map ino_map[] = {
{ XFS_SICK_INO_DIR_ZAPPED, XFS_BS_SICK_DIR },
{ XFS_SICK_INO_SYMLINK_ZAPPED, XFS_BS_SICK_SYMLINK },
{ XFS_SICK_INO_DIRTREE, XFS_BS_SICK_DIRTREE },
+ { XFS_SICK_INO_FSVERITY, XFS_BS_SICK_FSVERITY },
};
/* Fill out bulkstat health info. */
--
2.51.2
^ permalink raw reply related
* [PATCH v9 20/22] xfs: check and repair the verity inode flag state
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Darrick J. Wong, hch, linux-ext4, linux-f2fs-devel, linux-btrfs,
linux-unionfs, Andrey Albershteyn
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
From: "Darrick J. Wong" <djwong@kernel.org>
If an inode has the incore verity iflag set, make sure that we can
actually activate fsverity on that inode. If activation fails due to
a fsverity metadata validation error, clear the flag. The usage model
for fsverity requires that any program that cares about verity state is
required to call statx/getflags to check that the flag is set after
opening the file, so clearing the flag will not compromise that model.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/scrub/attr.c | 7 +++++
fs/xfs/scrub/common.c | 53 +++++++++++++++++++++++++++++++++++++
fs/xfs/scrub/common.h | 2 ++
fs/xfs/scrub/inode.c | 7 +++++
fs/xfs/scrub/inode_repair.c | 36 +++++++++++++++++++++++++
5 files changed, 105 insertions(+)
diff --git a/fs/xfs/scrub/attr.c b/fs/xfs/scrub/attr.c
index 390ac2e11ee0..daf7962c2374 100644
--- a/fs/xfs/scrub/attr.c
+++ b/fs/xfs/scrub/attr.c
@@ -649,6 +649,13 @@ xchk_xattr(
if (!xfs_inode_hasattr(sc->ip))
return -ENOENT;
+ /*
+ * If this is a verity file that won't activate, we cannot check the
+ * merkle tree geometry.
+ */
+ if (xchk_inode_verity_broken(sc->ip))
+ xchk_set_incomplete(sc);
+
/* Allocate memory for xattr checking. */
error = xchk_setup_xattr_buf(sc, 0);
if (error == -ENOMEM)
diff --git a/fs/xfs/scrub/common.c b/fs/xfs/scrub/common.c
index 20e63069088b..6cc6bea9c554 100644
--- a/fs/xfs/scrub/common.c
+++ b/fs/xfs/scrub/common.c
@@ -45,6 +45,8 @@
#include "scrub/health.h"
#include "scrub/tempfile.h"
+#include <linux/fsverity.h>
+
/* Common code for the metadata scrubbers. */
/*
@@ -1743,3 +1745,54 @@ xchk_inode_count_blocks(
return xfs_bmap_count_blocks(sc->tp, sc->ip, whichfork, nextents,
count);
}
+
+/*
+ * If this inode has S_VERITY set on it, read the verity info. If the reading
+ * fails with anything other than ENOMEM, the file is corrupt, which we can
+ * detect later with fsverity_active.
+ *
+ * Callers must hold the IOLOCK and must not hold the ILOCK of sc->ip because
+ * activation reads inode data.
+ */
+int
+xchk_inode_setup_verity(
+ struct xfs_scrub *sc)
+{
+ int error;
+
+ if (!fsverity_active(VFS_I(sc->ip)))
+ return 0;
+
+ error = fsverity_ensure_verity_info(VFS_I(sc->ip));
+ switch (error) {
+ case 0:
+ /* fsverity is active */
+ break;
+ case -ENODATA:
+ case -EMSGSIZE:
+ case -EINVAL:
+ case -EFSCORRUPTED:
+ case -EFBIG:
+ /*
+ * The nonzero errno codes above are the error codes that can
+ * be returned from fsverity on metadata validation errors.
+ */
+ return 0;
+ default:
+ /* runtime errors */
+ return error;
+ }
+
+ return 0;
+}
+
+/*
+ * Is this a verity file that failed to activate? Callers must have tried to
+ * activate fsverity via xchk_inode_setup_verity.
+ */
+bool
+xchk_inode_verity_broken(
+ struct xfs_inode *ip)
+{
+ return fsverity_active(VFS_I(ip)) && !fsverity_get_info(VFS_I(ip));
+}
diff --git a/fs/xfs/scrub/common.h b/fs/xfs/scrub/common.h
index f2ecc68538f0..aa16d310bd6d 100644
--- a/fs/xfs/scrub/common.h
+++ b/fs/xfs/scrub/common.h
@@ -264,6 +264,8 @@ int xchk_inode_is_allocated(struct xfs_scrub *sc, xfs_agino_t agino,
bool *inuse);
int xchk_inode_count_blocks(struct xfs_scrub *sc, int whichfork,
xfs_extnum_t *nextents, xfs_filblks_t *count);
+int xchk_inode_setup_verity(struct xfs_scrub *sc);
+bool xchk_inode_verity_broken(struct xfs_inode *ip);
bool xchk_inode_is_dirtree_root(const struct xfs_inode *ip);
bool xchk_inode_is_sb_rooted(const struct xfs_inode *ip);
diff --git a/fs/xfs/scrub/inode.c b/fs/xfs/scrub/inode.c
index 948d04dcba2a..8ce6917e22b4 100644
--- a/fs/xfs/scrub/inode.c
+++ b/fs/xfs/scrub/inode.c
@@ -36,6 +36,10 @@ xchk_prepare_iscrub(
xchk_ilock(sc, XFS_IOLOCK_EXCL);
+ error = xchk_inode_setup_verity(sc);
+ if (error)
+ return error;
+
error = xchk_trans_alloc(sc, 0);
if (error)
return error;
@@ -833,6 +837,9 @@ xchk_inode(
if (S_ISREG(VFS_I(sc->ip)->i_mode))
xchk_inode_check_reflink_iflag(sc, sc->ip->i_ino);
+ if (xchk_inode_verity_broken(sc->ip))
+ xchk_ino_set_corrupt(sc, sc->sm->sm_ino);
+
xchk_inode_check_unlinked(sc);
xchk_inode_xref(sc, sc->ip->i_ino, &di);
diff --git a/fs/xfs/scrub/inode_repair.c b/fs/xfs/scrub/inode_repair.c
index 9738b9ce3f2d..3761e3922466 100644
--- a/fs/xfs/scrub/inode_repair.c
+++ b/fs/xfs/scrub/inode_repair.c
@@ -573,6 +573,8 @@ xrep_dinode_flags(
dip->di_nrext64_pad = 0;
else if (dip->di_version >= 3)
dip->di_v3_pad = 0;
+ if (!xfs_has_verity(mp) || !S_ISREG(mode))
+ flags2 &= ~XFS_DIFLAG2_VERITY;
if (flags2 & XFS_DIFLAG2_METADATA) {
xfs_failaddr_t fa;
@@ -1613,6 +1615,10 @@ xrep_dinode_core(
if (iget_error)
return iget_error;
+ error = xchk_inode_setup_verity(sc);
+ if (error)
+ return error;
+
error = xchk_trans_alloc(sc, 0);
if (error)
return error;
@@ -2032,6 +2038,27 @@ xrep_inode_unlinked(
return 0;
}
+/*
+ * If this file is a fsverity file, xchk_prepare_iscrub or xrep_dinode_core
+ * should have activated it. If it's still not active, then there's something
+ * wrong with the verity descriptor and we should turn it off.
+ */
+STATIC int
+xrep_inode_verity(
+ struct xfs_scrub *sc)
+{
+ struct inode *inode = VFS_I(sc->ip);
+
+ if (xchk_inode_verity_broken(sc->ip)) {
+ sc->ip->i_diflags2 &= ~XFS_DIFLAG2_VERITY;
+ inode->i_flags &= ~S_VERITY;
+
+ xfs_trans_log_inode(sc->tp, sc->ip, XFS_ILOG_CORE);
+ }
+
+ return 0;
+}
+
/* Repair an inode's fields. */
int
xrep_inode(
@@ -2081,6 +2108,15 @@ xrep_inode(
return error;
}
+ /*
+ * Disable fsverity if it cannot be activated. Activation failure
+ * prohibits the file from being opened, so there cannot be another
+ * program with an open fd to what it thinks is a verity file.
+ */
+ error = xrep_inode_verity(sc);
+ if (error)
+ return error;
+
/* Reconnect incore unlinked list */
error = xrep_inode_unlinked(sc);
if (error)
--
2.51.2
^ permalink raw reply related
* [PATCH v9 19/22] xfs: advertise fs-verity being available on filesystem
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Darrick J. Wong, hch, linux-ext4, linux-f2fs-devel, linux-btrfs,
linux-unionfs, Andrey Albershteyn, Andrey Albershteyn
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
From: "Darrick J. Wong" <djwong@kernel.org>
Advertise that this filesystem supports fsverity.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Andrey Albershteyn <aalbersh@redhat.com>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/libxfs/xfs_fs.h | 1 +
fs/xfs/libxfs/xfs_sb.c | 2 ++
2 files changed, 3 insertions(+)
diff --git a/fs/xfs/libxfs/xfs_fs.h b/fs/xfs/libxfs/xfs_fs.h
index d165de607d17..ebf17a0b0722 100644
--- a/fs/xfs/libxfs/xfs_fs.h
+++ b/fs/xfs/libxfs/xfs_fs.h
@@ -250,6 +250,7 @@ typedef struct xfs_fsop_resblks {
#define XFS_FSOP_GEOM_FLAGS_PARENT (1 << 25) /* linux parent pointers */
#define XFS_FSOP_GEOM_FLAGS_METADIR (1 << 26) /* metadata directories */
#define XFS_FSOP_GEOM_FLAGS_ZONED (1 << 27) /* zoned rt device */
+#define XFS_FSOP_GEOM_FLAGS_VERITY (1 << 28) /* fs-verity */
/*
* Minimum and maximum sizes need for growth checks.
diff --git a/fs/xfs/libxfs/xfs_sb.c b/fs/xfs/libxfs/xfs_sb.c
index a15510ebd2f1..222bbe5559df 100644
--- a/fs/xfs/libxfs/xfs_sb.c
+++ b/fs/xfs/libxfs/xfs_sb.c
@@ -1590,6 +1590,8 @@ xfs_fs_geometry(
geo->flags |= XFS_FSOP_GEOM_FLAGS_METADIR;
if (xfs_has_zoned(mp))
geo->flags |= XFS_FSOP_GEOM_FLAGS_ZONED;
+ if (xfs_has_verity(mp))
+ geo->flags |= XFS_FSOP_GEOM_FLAGS_VERITY;
geo->rtsectsize = sbp->sb_blocksize;
geo->dirblocksize = xfs_dir2_dirblock_bytes(sbp);
--
2.51.2
^ permalink raw reply related
* [PATCH v9 18/22] xfs: add fs-verity ioctls
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Add fs-verity ioctls to enable, dump metadata (descriptor and Merkle
tree pages) and obtain file's digest.
[djwong: remove unnecessary casting]
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_ioctl.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/fs/xfs/xfs_ioctl.c b/fs/xfs/xfs_ioctl.c
index facffdc8dca8..e633d56cad00 100644
--- a/fs/xfs/xfs_ioctl.c
+++ b/fs/xfs/xfs_ioctl.c
@@ -46,6 +46,7 @@
#include <linux/mount.h>
#include <linux/fileattr.h>
+#include <linux/fsverity.h>
/* Return 0 on success or positive error */
int
@@ -1426,6 +1427,19 @@ xfs_file_ioctl(
case XFS_IOC_VERIFY_MEDIA:
return xfs_ioc_verify_media(filp, arg);
+ case FS_IOC_ENABLE_VERITY:
+ if (!xfs_has_verity(mp))
+ return -EOPNOTSUPP;
+ return fsverity_ioctl_enable(filp, arg);
+ case FS_IOC_MEASURE_VERITY:
+ if (!xfs_has_verity(mp))
+ return -EOPNOTSUPP;
+ return fsverity_ioctl_measure(filp, arg);
+ case FS_IOC_READ_VERITY_METADATA:
+ if (!xfs_has_verity(mp))
+ return -EOPNOTSUPP;
+ return fsverity_ioctl_read_metadata(filp, arg);
+
default:
return -ENOTTY;
}
--
2.51.2
^ permalink raw reply related
* [PATCH v9 17/22] xfs: remove unwritten extents after preallocations in fsverity metadata
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
XFS preallocates spaces during writes. In normal I/O this space, if
unused, is removed by truncate. For files with fsverity XFS does not use
truncate as fsverity metadata is stored past EOF.
After we're done with writing fsverity metadata iterate over extents in
that region and remove any unwritten ones. These would be left overs in
the holes in the merkle tree and past fsverity descriptor.
Reviewed-by: Christoph Hellwig <hch@lst.de>
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_fsverity.c | 69 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 69 insertions(+)
diff --git a/fs/xfs/xfs_fsverity.c b/fs/xfs/xfs_fsverity.c
index a06885291412..298d712b5ba2 100644
--- a/fs/xfs/xfs_fsverity.c
+++ b/fs/xfs/xfs_fsverity.c
@@ -21,6 +21,8 @@
#include "xfs_iomap.h"
#include "xfs_error.h"
#include "xfs_health.h"
+#include "xfs_bmap.h"
+#include "xfs_bmap_util.h"
#include <linux/fsverity.h>
#include <linux/iomap.h>
#include <linux/pagemap.h>
@@ -171,6 +173,65 @@ xfs_fsverity_delete_metadata(
return error;
}
+static int
+xfs_fsverity_cancel_unwritten(
+ struct xfs_inode *ip,
+ loff_t start,
+ loff_t end)
+{
+ struct xfs_mount *mp = ip->i_mount;
+ struct xfs_trans *tp;
+ xfs_fileoff_t offset_fsb = XFS_B_TO_FSB(mp, start);
+ xfs_fileoff_t end_fsb = XFS_B_TO_FSB(mp, end);
+ struct xfs_bmbt_irec imap;
+ int nimaps;
+ int error = 0;
+ int done;
+
+
+ while (offset_fsb < end_fsb) {
+ nimaps = 1;
+
+ error = xfs_trans_alloc(mp, &M_RES(mp)->tr_write, 0, 0,
+ 0, &tp);
+ if (error)
+ return error;
+
+ xfs_ilock(ip, XFS_ILOCK_EXCL);
+ error = xfs_bmapi_read(ip, offset_fsb, end_fsb - offset_fsb,
+ &imap, &nimaps, 0);
+ if (error)
+ goto out_cancel;
+
+ if (nimaps == 0)
+ goto out_cancel;
+
+ if (imap.br_state == XFS_EXT_UNWRITTEN) {
+ xfs_trans_ijoin(tp, ip, 0);
+
+ error = xfs_bunmapi(tp, ip, imap.br_startoff,
+ imap.br_blockcount, 0, 1, &done);
+ if (error)
+ goto out_cancel;
+
+ error = xfs_trans_commit(tp);
+ if (error)
+ return error;
+ } else {
+ xfs_trans_cancel(tp);
+ }
+ xfs_iunlock(ip, XFS_ILOCK_EXCL);
+
+ offset_fsb = imap.br_startoff + imap.br_blockcount;
+ }
+
+ return error;
+out_cancel:
+ xfs_trans_cancel(tp);
+ xfs_iunlock(ip, XFS_ILOCK_EXCL);
+ return error;
+}
+
/*
* Prepare to enable fsverity by clearing old metadata.
@@ -248,6 +309,14 @@ xfs_fsverity_end_enable(
if (error)
goto out;
+ /*
+ * Remove unwritten extents left by COW preallocations and write
+ * preallocation in the merkle tree holes and past descriptor
+ */
+ error = xfs_fsverity_cancel_unwritten(ip, range_start, LLONG_MAX);
+ if (error)
+ goto out;
+
/*
* Proactively drop any delayed allocations in COW fork, the fsverity
* files are read-only
--
2.51.2
^ permalink raw reply related
* [PATCH v9 16/22] xfs: add fs-verity support
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Add integration with fs-verity. XFS stores fs-verity descriptor and
Merkle tree in the inode data fork at first block aligned to 64k past
EOF.
The Merkle tree reading/writing is done through iomap interface. The
data itself is read to the inode's page cache. When XFS reads from this
region iomap doesn't call into fsverity to verify it against Merkle
tree. For data, verification is done at ioend completion in a workqueue.
When fs-verity is enabled on an inode, the XFS_IVERITY_CONSTRUCTION
flag is set meaning that the Merkle tree is being build. The
initialization ends with storing of verity descriptor and setting
inode on-disk flag (XFS_DIFLAG2_VERITY). Lastly, the
XFS_IVERITY_CONSTRUCTION is dropped and I_VERITY is set on inode.
The descriptor is stored in a new block aligned to 64k after the last
Merkle tree block. The size of the descriptor is stored at the end of
the last descriptor block (descriptor can be multiple blocks).
Reviewed-by: Christoph Hellwig <hch@lst.de>
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_bmap_util.c | 8 +
fs/xfs/xfs_fsverity.c | 353 ++++++++++++++++++++++++++++++++++++++++-
fs/xfs/xfs_fsverity.h | 2 +
fs/xfs/xfs_message.c | 4 +
fs/xfs/xfs_message.h | 1 +
fs/xfs/xfs_mount.h | 2 +
fs/xfs/xfs_super.c | 7 +
7 files changed, 376 insertions(+), 1 deletion(-)
diff --git a/fs/xfs/xfs_bmap_util.c b/fs/xfs/xfs_bmap_util.c
index 0ab00615f1ad..18348f4fd2aa 100644
--- a/fs/xfs/xfs_bmap_util.c
+++ b/fs/xfs/xfs_bmap_util.c
@@ -31,6 +31,7 @@
#include "xfs_rtbitmap.h"
#include "xfs_rtgroup.h"
#include "xfs_zone_alloc.h"
+#include <linux/fsverity.h>
/* Kernel only BMAP related definitions and functions */
@@ -553,6 +554,13 @@ xfs_can_free_eofblocks(
if (last_fsb <= end_fsb)
return false;
+ /*
+ * Nothing to clean on fsverity inodes as they don't use prealloc and
+ * there no delalloc as only written data is fsverity metadata
+ */
+ if (IS_VERITY(VFS_I(ip)))
+ return false;
+
/*
* Check if there is an post-EOF extent to free. If there are any
* delalloc blocks attached to the inode (data fork delalloc
diff --git a/fs/xfs/xfs_fsverity.c b/fs/xfs/xfs_fsverity.c
index b983e20bb5e1..a06885291412 100644
--- a/fs/xfs/xfs_fsverity.c
+++ b/fs/xfs/xfs_fsverity.c
@@ -4,14 +4,26 @@
*/
#include "xfs_platform.h"
#include "xfs_format.h"
-#include "xfs_inode.h"
#include "xfs_shared.h"
#include "xfs_trans_resv.h"
#include "xfs_mount.h"
#include "xfs_fsverity.h"
+#include "xfs_da_format.h"
+#include "xfs_da_btree.h"
+#include "xfs_inode.h"
+#include "xfs_log_format.h"
+#include "xfs_bmap_util.h"
+#include "xfs_log_format.h"
+#include "xfs_trans.h"
+#include "xfs_trace.h"
+#include "xfs_quota.h"
#include "xfs_fsverity.h"
+#include "xfs_iomap.h"
+#include "xfs_error.h"
+#include "xfs_health.h"
#include <linux/fsverity.h>
#include <linux/iomap.h>
+#include <linux/pagemap.h>
loff_t
xfs_fsverity_metadata_offset(
@@ -28,3 +40,342 @@ xfs_fsverity_is_file_data(
return fsverity_active(VFS_IC(ip)) &&
offset < xfs_fsverity_metadata_offset(ip);
}
+
+/*
+ * Retrieve the verity descriptor.
+ */
+static int
+xfs_fsverity_get_descriptor(
+ struct inode *inode,
+ void *buf,
+ size_t buf_size)
+{
+ struct xfs_inode *ip = XFS_I(inode);
+ struct xfs_mount *mp = ip->i_mount;
+ __be32 d_desc_size;
+ u32 desc_size;
+ u64 desc_size_pos;
+ int error;
+ u64 desc_pos;
+ struct xfs_bmbt_irec rec;
+ int is_empty;
+ uint32_t blocksize = i_blocksize(VFS_I(ip));
+ xfs_fileoff_t last_block_offset;
+
+ ASSERT(inode->i_flags & S_VERITY);
+ error = xfs_bmap_last_extent(NULL, ip, XFS_DATA_FORK, &rec, &is_empty);
+ if (error)
+ return error;
+
+ if (is_empty)
+ return -ENODATA;
+
+ last_block_offset =
+ XFS_FSB_TO_B(mp, rec.br_startoff + rec.br_blockcount);
+ if (last_block_offset < xfs_fsverity_metadata_offset(ip))
+ return -ENODATA;
+
+ desc_size_pos = last_block_offset - sizeof(__be32);
+ error = fsverity_pagecache_read(inode, (char *)&d_desc_size,
+ sizeof(d_desc_size), desc_size_pos);
+ if (error)
+ return error;
+
+ desc_size = be32_to_cpu(d_desc_size);
+ if (XFS_IS_CORRUPT(mp, desc_size > FS_VERITY_MAX_DESCRIPTOR_SIZE))
+ return -ERANGE;
+ if (XFS_IS_CORRUPT(mp, desc_size > desc_size_pos))
+ return -ERANGE;
+
+ if (!buf_size)
+ return desc_size;
+
+ if (XFS_IS_CORRUPT(mp, desc_size > buf_size))
+ return -ERANGE;
+
+ desc_pos = round_down(desc_size_pos - desc_size, blocksize);
+ error = fsverity_pagecache_read(inode, buf, desc_size, desc_pos);
+ if (error)
+ return error;
+
+ return desc_size;
+}
+
+static int
+xfs_fsverity_write_descriptor(
+ struct file *file,
+ const void *desc,
+ u32 desc_size,
+ u64 merkle_tree_size)
+{
+ int error;
+ struct inode *inode = file_inode(file);
+ struct xfs_inode *ip = XFS_I(inode);
+ unsigned int blksize = ip->i_mount->m_sb.sb_blocksize;
+ u64 tree_last_block =
+ xfs_fsverity_metadata_offset(ip) + merkle_tree_size;
+ u64 desc_pos =
+ round_up(tree_last_block, XFS_FSVERITY_START_ALIGN);
+ u64 desc_end = desc_pos + desc_size;
+ __be32 desc_size_disk = cpu_to_be32(desc_size);
+ u64 desc_size_pos =
+ round_up(desc_end + sizeof(desc_size_disk), blksize) -
+ sizeof(desc_size_disk);
+
+ error = iomap_fsverity_write(file, desc_size_pos, sizeof(__be32),
+ (const void *)&desc_size_disk,
+ &xfs_buffered_write_iomap_ops,
+ &xfs_iomap_write_ops);
+ if (error)
+ return error;
+
+ return iomap_fsverity_write(file, desc_pos, desc_size, desc,
+ &xfs_buffered_write_iomap_ops,
+ &xfs_iomap_write_ops);
+}
+
+/*
+ * Try to remove all the fsverity metadata after a failed enablement.
+ */
+static int
+xfs_fsverity_delete_metadata(
+ struct xfs_inode *ip)
+{
+ struct xfs_trans *tp;
+ struct xfs_mount *mp = ip->i_mount;
+ int error;
+
+ error = xfs_trans_alloc(mp, &M_RES(mp)->tr_itruncate, 0, 0, 0, &tp);
+ if (error)
+ return error;
+
+ xfs_ilock(ip, XFS_ILOCK_EXCL);
+ xfs_trans_ijoin(tp, ip, 0);
+
+ /*
+ * We removing post EOF data, no need to update i_size as fsverity
+ * didn't move i_size in the first place
+ */
+ error = xfs_itruncate_extents(&tp, ip, XFS_DATA_FORK, XFS_ISIZE(ip));
+ if (error)
+ goto err_cancel;
+
+ error = xfs_trans_commit(tp);
+ xfs_iunlock(ip, XFS_ILOCK_EXCL);
+
+ return error;
+
+err_cancel:
+ xfs_iunlock(ip, XFS_ILOCK_EXCL);
+ xfs_trans_cancel(tp);
+ return error;
+}
+
+
+/*
+ * Prepare to enable fsverity by clearing old metadata.
+ */
+static int
+xfs_fsverity_begin_enable(
+ struct file *filp)
+{
+ struct inode *inode = file_inode(filp);
+ struct xfs_inode *ip = XFS_I(inode);
+ int error;
+
+ xfs_assert_ilocked(ip, XFS_IOLOCK_EXCL);
+
+ if (IS_DAX(inode))
+ return -EINVAL;
+
+ if (inode->i_size > XFS_FSVERITY_LARGEST_FILE)
+ return -EFBIG;
+
+ /*
+ * Flush pagecache before building Merkle tree. Inode is locked and no
+ * further writes will happen to the file except fsverity metadata
+ */
+ error = filemap_write_and_wait(inode->i_mapping);
+ if (error)
+ return error;
+
+ if (xfs_iflags_test_and_set(ip, XFS_VERITY_CONSTRUCTION))
+ return -EBUSY;
+
+ error = xfs_qm_dqattach(ip);
+ if (error)
+ return error;
+
+ return xfs_fsverity_delete_metadata(ip);
+}
+
+/*
+ * Complete (or fail) the process of enabling fsverity.
+ */
+static int
+xfs_fsverity_end_enable(
+ struct file *file,
+ const void *desc,
+ size_t desc_size,
+ u64 merkle_tree_size)
+{
+ struct inode *inode = file_inode(file);
+ struct xfs_inode *ip = XFS_I(inode);
+ struct xfs_mount *mp = ip->i_mount;
+ struct xfs_trans *tp;
+ int error = 0;
+ loff_t range_start = xfs_fsverity_metadata_offset(ip);
+
+ xfs_assert_ilocked(ip, XFS_IOLOCK_EXCL);
+
+ /* fs-verity failed, just cleanup */
+ if (desc == NULL) {
+ error = xfs_fsverity_delete_metadata(ip);
+ goto out;
+ }
+
+ error = xfs_fsverity_write_descriptor(file, desc, desc_size,
+ merkle_tree_size);
+ if (error)
+ goto out;
+
+ /*
+ * Wait for Merkle tree get written to disk before setting on-disk inode
+ * flag and clearing XFS_VERITY_CONSTRUCTION
+ */
+ error = filemap_write_and_wait_range(inode->i_mapping, range_start,
+ LLONG_MAX);
+ if (error)
+ goto out;
+
+ /*
+ * Proactively drop any delayed allocations in COW fork, the fsverity
+ * files are read-only
+ */
+ if (xfs_is_cow_inode(ip))
+ xfs_bmap_punch_delalloc_range(ip, XFS_COW_FORK, 0, LLONG_MAX,
+ NULL);
+
+ /*
+ * Set fsverity inode flag
+ */
+ error = xfs_trans_alloc_inode(ip, &M_RES(mp)->tr_ichange,
+ 0, 0, false, &tp);
+ if (error)
+ goto out;
+
+ /*
+ * Ensure that we've persisted the verity information before we enable
+ * it on the inode and tell the caller we have sealed the inode.
+ */
+ ip->i_diflags2 |= XFS_DIFLAG2_VERITY;
+
+ xfs_trans_log_inode(tp, ip, XFS_ILOG_CORE);
+ xfs_trans_set_sync(tp);
+
+ error = xfs_trans_commit(tp);
+ xfs_iunlock(ip, XFS_ILOCK_EXCL);
+
+ if (!error)
+ inode->i_flags |= S_VERITY;
+
+out:
+ if (error) {
+ int error2;
+
+ error2 = xfs_fsverity_delete_metadata(ip);
+ if (error2)
+ xfs_alert(ip->i_mount,
+"ino 0x%llx failed to clean up new fsverity metadata, err %d",
+ ip->i_ino, error2);
+ }
+
+ xfs_iflags_clear(ip, XFS_VERITY_CONSTRUCTION);
+ return error;
+}
+
+/*
+ * Retrieve a merkle tree block.
+ */
+static struct page *
+xfs_fsverity_read_merkle(
+ struct inode *inode,
+ pgoff_t index)
+{
+ index += xfs_fsverity_metadata_offset(XFS_I(inode)) >> PAGE_SHIFT;
+
+ return generic_read_merkle_tree_page(inode, index);
+}
+
+/*
+ * Retrieve a merkle tree block.
+ */
+static void
+xfs_fsverity_readahead_merkle_tree(
+ struct inode *inode,
+ pgoff_t index,
+ unsigned long nr_pages)
+{
+ index += xfs_fsverity_metadata_offset(XFS_I(inode)) >> PAGE_SHIFT;
+
+ generic_readahead_merkle_tree(inode, index, nr_pages);
+}
+
+/*
+ * Write a merkle tree block.
+ */
+static int
+xfs_fsverity_write_merkle(
+ struct file *file,
+ const void *buf,
+ u64 pos,
+ unsigned int size,
+ const u8 *zero_digest,
+ unsigned int digest_size)
+{
+ struct inode *inode = file_inode(file);
+ struct xfs_inode *ip = XFS_I(inode);
+ loff_t position = pos +
+ xfs_fsverity_metadata_offset(ip);
+
+ if (position + size > inode->i_sb->s_maxbytes)
+ return -EFBIG;
+
+ /*
+ * If this is a block full of hashes of zeroed blocks, don't bother
+ * storing the block. We can synthesize them later.
+ *
+ * However, do this only in case Merkle tree block == fs block size.
+ * Iomap synthesizes these blocks based on holes in the merkle tree. We
+ * won't be able to tell if something need to be synthesizes for the
+ * range in the fs block. For example, for 4k filesystem block
+ *
+ * [ 1k | zero hashes | zero hashes | 1k ]
+ *
+ * Iomap won't know about these empty blocks.
+ */
+ if (size == ip->i_mount->m_sb.sb_blocksize &&
+ /*
+ * First digest is zero_digest
+ */
+ memcmp(buf, zero_digest, digest_size) == 0 &&
+ /*
+ * Every digest is same as previous, thus all are
+ * zero_digest
+ */
+ memcmp(buf + digest_size, buf, size - digest_size) == 0)
+ return 0;
+
+ return iomap_fsverity_write(file, position, size, buf,
+ &xfs_buffered_write_iomap_ops,
+ &xfs_iomap_write_ops);
+}
+
+const struct fsverity_operations xfs_fsverity_ops = {
+ .begin_enable_verity = xfs_fsverity_begin_enable,
+ .end_enable_verity = xfs_fsverity_end_enable,
+ .get_verity_descriptor = xfs_fsverity_get_descriptor,
+ .read_merkle_tree_page = xfs_fsverity_read_merkle,
+ .readahead_merkle_tree = xfs_fsverity_readahead_merkle_tree,
+ .write_merkle_tree_block = xfs_fsverity_write_merkle,
+};
diff --git a/fs/xfs/xfs_fsverity.h b/fs/xfs/xfs_fsverity.h
index ec77ba571106..6a981e20a75b 100644
--- a/fs/xfs/xfs_fsverity.h
+++ b/fs/xfs/xfs_fsverity.h
@@ -6,8 +6,10 @@
#define __XFS_FSVERITY_H__
#include "xfs_platform.h"
+#include <linux/fsverity.h>
#ifdef CONFIG_FS_VERITY
+extern const struct fsverity_operations xfs_fsverity_ops;
loff_t xfs_fsverity_metadata_offset(const struct xfs_inode *ip);
bool xfs_fsverity_is_file_data(const struct xfs_inode *ip, loff_t offset);
#else
diff --git a/fs/xfs/xfs_message.c b/fs/xfs/xfs_message.c
index fd297082aeb8..9818d8f8f239 100644
--- a/fs/xfs/xfs_message.c
+++ b/fs/xfs/xfs_message.c
@@ -153,6 +153,10 @@ xfs_warn_experimental(
.opstate = XFS_OPSTATE_WARNED_ZONED,
.name = "zoned RT device",
},
+ [XFS_EXPERIMENTAL_FSVERITY] = {
+ .opstate = XFS_OPSTATE_WARNED_FSVERITY,
+ .name = "fsverity",
+ },
};
ASSERT(feat >= 0 && feat < XFS_EXPERIMENTAL_MAX);
BUILD_BUG_ON(ARRAY_SIZE(features) != XFS_EXPERIMENTAL_MAX);
diff --git a/fs/xfs/xfs_message.h b/fs/xfs/xfs_message.h
index 49b0ef40d299..083403944f11 100644
--- a/fs/xfs/xfs_message.h
+++ b/fs/xfs/xfs_message.h
@@ -94,6 +94,7 @@ enum xfs_experimental_feat {
XFS_EXPERIMENTAL_SHRINK,
XFS_EXPERIMENTAL_LARP,
XFS_EXPERIMENTAL_ZONED,
+ XFS_EXPERIMENTAL_FSVERITY,
XFS_EXPERIMENTAL_MAX,
};
diff --git a/fs/xfs/xfs_mount.h b/fs/xfs/xfs_mount.h
index 07f6aa3c3f26..84d7cfb5e2c7 100644
--- a/fs/xfs/xfs_mount.h
+++ b/fs/xfs/xfs_mount.h
@@ -583,6 +583,8 @@ __XFS_HAS_FEAT(nouuid, NOUUID)
#define XFS_OPSTATE_WARNED_ZONED 19
/* (Zoned) GC is in progress */
#define XFS_OPSTATE_ZONEGC_RUNNING 20
+/* Kernel has logged a warning about fsverity support */
+#define XFS_OPSTATE_WARNED_FSVERITY 21
#define __XFS_IS_OPSTATE(name, NAME) \
static inline bool xfs_is_ ## name (struct xfs_mount *mp) \
diff --git a/fs/xfs/xfs_super.c b/fs/xfs/xfs_super.c
index f8de44443e81..d9d442009610 100644
--- a/fs/xfs/xfs_super.c
+++ b/fs/xfs/xfs_super.c
@@ -30,6 +30,7 @@
#include "xfs_filestream.h"
#include "xfs_quota.h"
#include "xfs_sysfs.h"
+#include "xfs_fsverity.h"
#include "xfs_ondisk.h"
#include "xfs_rmap_item.h"
#include "xfs_refcount_item.h"
@@ -1686,6 +1687,9 @@ xfs_fs_fill_super(
sb->s_quota_types = QTYPE_MASK_USR | QTYPE_MASK_GRP | QTYPE_MASK_PRJ;
#endif
sb->s_op = &xfs_super_operations;
+#ifdef CONFIG_FS_VERITY
+ sb->s_vop = &xfs_fsverity_ops;
+#endif
/*
* Delay mount work if the debug hook is set. This is debug
@@ -1939,6 +1943,9 @@ xfs_fs_fill_super(
if (error)
goto out_filestream_unmount;
+ if (xfs_has_verity(mp))
+ xfs_warn_experimental(mp, XFS_EXPERIMENTAL_FSVERITY);
+
root = igrab(VFS_I(mp->m_rootip));
if (!root) {
error = -ENOENT;
--
2.51.2
^ permalink raw reply related
* [PATCH v9 15/22] xfs: use read ioend for fsverity data verification
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Use read ioends for fsverity verification. Do not issues fsverity
metadata I/O through the same workqueue due to risk of a deadlock by a
filled workqueue.
Pass fsverity_info from iomap context down to the ioend as hashtable
lookups are expensive.
Add a simple helper to check that this is not fsverity metadata but file
data that needs verification.
Reviewed-by: Christoph Hellwig <hch@lst.de>
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_aops.c | 46 ++++++++++++++++++++++++++++++++++---------
fs/xfs/xfs_fsverity.c | 9 +++++++++
fs/xfs/xfs_fsverity.h | 6 ++++++
3 files changed, 52 insertions(+), 9 deletions(-)
diff --git a/fs/xfs/xfs_aops.c b/fs/xfs/xfs_aops.c
index 9503252a0fa4..ecb07f250956 100644
--- a/fs/xfs/xfs_aops.c
+++ b/fs/xfs/xfs_aops.c
@@ -24,6 +24,7 @@
#include "xfs_rtgroup.h"
#include "xfs_fsverity.h"
#include <linux/bio-integrity.h>
+#include <linux/fsverity.h>
struct xfs_writepage_ctx {
struct iomap_writepage_ctx ctx;
@@ -171,6 +172,23 @@ xfs_end_ioend_write(
memalloc_nofs_restore(nofs_flag);
}
+/*
+ * IO read completion.
+ */
+static void
+xfs_end_ioend_read(
+ struct iomap_ioend *ioend)
+{
+ struct xfs_inode *ip = XFS_I(ioend->io_inode);
+
+ if (!ioend->io_bio.bi_status &&
+ xfs_fsverity_is_file_data(ip, ioend->io_offset))
+ fsverity_verify_bio(ioend->io_vi,
+ &ioend->io_bio);
+ iomap_finish_ioends(ioend,
+ blk_status_to_errno(ioend->io_bio.bi_status));
+}
+
/*
* Finish all pending IO completions that require transactional modifications.
*
@@ -205,8 +223,7 @@ xfs_end_io(
list_del_init(&ioend->io_list);
iomap_ioend_try_merge(ioend, &tmp);
if (bio_op(&ioend->io_bio) == REQ_OP_READ)
- iomap_finish_ioends(ioend,
- blk_status_to_errno(ioend->io_bio.bi_status));
+ xfs_end_ioend_read(ioend);
else
xfs_end_ioend_write(ioend);
cond_resched();
@@ -232,9 +249,14 @@ xfs_end_bio(
}
spin_lock_irqsave(&ip->i_ioend_lock, flags);
- if (list_empty(&ip->i_ioend_list))
- WARN_ON_ONCE(!queue_work(mp->m_unwritten_workqueue,
+ if (list_empty(&ip->i_ioend_list)) {
+ if (IS_ENABLED(CONFIG_FS_VERITY) && ioend->io_vi &&
+ ioend->io_offset < xfs_fsverity_metadata_offset(ip))
+ fsverity_enqueue_verify_work(&ip->i_ioend_work);
+ else
+ WARN_ON_ONCE(!queue_work(mp->m_unwritten_workqueue,
&ip->i_ioend_work));
+ }
list_add_tail(&ioend->io_list, &ip->i_ioend_list);
spin_unlock_irqrestore(&ip->i_ioend_lock, flags);
}
@@ -764,9 +786,13 @@ xfs_bio_submit_read(
struct iomap_read_folio_ctx *ctx)
{
struct bio *bio = ctx->read_ctx;
+ struct iomap_ioend *ioend;
/* defer read completions to the ioend workqueue */
- iomap_init_ioend(iter->inode, bio, ctx->read_ctx_file_offset, 0);
+ ioend = iomap_init_ioend(iter->inode, bio, ctx->read_ctx_file_offset,
+ 0);
+ ioend->io_vi = ctx->vi;
+
bio->bi_end_io = xfs_end_bio;
submit_bio(bio);
}
@@ -779,11 +805,13 @@ static const struct iomap_read_ops xfs_iomap_read_ops = {
static inline const struct iomap_read_ops *
xfs_get_iomap_read_ops(
- const struct address_space *mapping)
+ const struct address_space *mapping,
+ loff_t position)
{
struct xfs_inode *ip = XFS_I(mapping->host);
- if (bdev_has_integrity_csum(xfs_inode_buftarg(ip)->bt_bdev))
+ if (bdev_has_integrity_csum(xfs_inode_buftarg(ip)->bt_bdev) ||
+ xfs_fsverity_is_file_data(ip, position))
return &xfs_iomap_read_ops;
return &iomap_bio_read_ops;
}
@@ -795,7 +823,7 @@ xfs_vm_read_folio(
{
struct iomap_read_folio_ctx ctx = { .cur_folio = folio };
- ctx.ops = xfs_get_iomap_read_ops(folio->mapping);
+ ctx.ops = xfs_get_iomap_read_ops(folio->mapping, folio_pos(folio));
iomap_read_folio(&xfs_read_iomap_ops, &ctx, NULL);
return 0;
}
@@ -806,7 +834,7 @@ xfs_vm_readahead(
{
struct iomap_read_folio_ctx ctx = { .rac = rac };
- ctx.ops = xfs_get_iomap_read_ops(rac->mapping),
+ ctx.ops = xfs_get_iomap_read_ops(rac->mapping, readahead_pos(rac));
iomap_readahead(&xfs_read_iomap_ops, &ctx, NULL);
}
diff --git a/fs/xfs/xfs_fsverity.c b/fs/xfs/xfs_fsverity.c
index 6e6a8636a577..b983e20bb5e1 100644
--- a/fs/xfs/xfs_fsverity.c
+++ b/fs/xfs/xfs_fsverity.c
@@ -19,3 +19,12 @@ xfs_fsverity_metadata_offset(
{
return round_up(i_size_read(VFS_IC(ip)), XFS_FSVERITY_START_ALIGN);
}
+
+bool
+xfs_fsverity_is_file_data(
+ const struct xfs_inode *ip,
+ loff_t offset)
+{
+ return fsverity_active(VFS_IC(ip)) &&
+ offset < xfs_fsverity_metadata_offset(ip);
+}
diff --git a/fs/xfs/xfs_fsverity.h b/fs/xfs/xfs_fsverity.h
index 5771db2cd797..ec77ba571106 100644
--- a/fs/xfs/xfs_fsverity.h
+++ b/fs/xfs/xfs_fsverity.h
@@ -9,12 +9,18 @@
#ifdef CONFIG_FS_VERITY
loff_t xfs_fsverity_metadata_offset(const struct xfs_inode *ip);
+bool xfs_fsverity_is_file_data(const struct xfs_inode *ip, loff_t offset);
#else
static inline loff_t xfs_fsverity_metadata_offset(const struct xfs_inode *ip)
{
WARN_ON_ONCE(1);
return ULLONG_MAX;
}
+static inline bool xfs_fsverity_is_file_data(const struct xfs_inode *ip,
+ loff_t offset)
+{
+ return false;
+}
#endif /* CONFIG_FS_VERITY */
#endif /* __XFS_FSVERITY_H__ */
--
2.51.2
^ permalink raw reply related
* [PATCH v9 14/22] xfs: handle fsverity I/O in write/read path
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
For write/writeback set IOMAP_F_FSVERITY flag telling iomap to not
update inode size and to not skip folios beyond EOF.
Initiate fsverity writeback with IOMAP_F_FSVERITY set to tell iomap
should not skip folio that is dirty beyond EOF.
In read path let iomap know that we are reading fsverity metadata. So,
treat holes in the tree as request to synthesize tree blocks and hole
after descriptor as end of the fsverity region.
Introduce a new inode flag meaning that merkle tree is being build on
the inode.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/Makefile | 1 +
fs/xfs/libxfs/xfs_bmap.c | 7 +++++++
fs/xfs/xfs_aops.c | 16 +++++++++++++++-
fs/xfs/xfs_fsverity.c | 21 +++++++++++++++++++++
fs/xfs/xfs_fsverity.h | 20 ++++++++++++++++++++
fs/xfs/xfs_inode.h | 6 ++++++
fs/xfs/xfs_iomap.c | 15 +++++++++++++--
7 files changed, 83 insertions(+), 3 deletions(-)
create mode 100644 fs/xfs/xfs_fsverity.c
create mode 100644 fs/xfs/xfs_fsverity.h
diff --git a/fs/xfs/Makefile b/fs/xfs/Makefile
index 9f7133e02576..38b7f51e5d84 100644
--- a/fs/xfs/Makefile
+++ b/fs/xfs/Makefile
@@ -149,6 +149,7 @@ xfs-$(CONFIG_XFS_POSIX_ACL) += xfs_acl.o
xfs-$(CONFIG_SYSCTL) += xfs_sysctl.o
xfs-$(CONFIG_COMPAT) += xfs_ioctl32.o
xfs-$(CONFIG_EXPORTFS_BLOCK_OPS) += xfs_pnfs.o
+xfs-$(CONFIG_FS_VERITY) += xfs_fsverity.o
# notify failure
ifeq ($(CONFIG_MEMORY_FAILURE),y)
diff --git a/fs/xfs/libxfs/xfs_bmap.c b/fs/xfs/libxfs/xfs_bmap.c
index 7a4c8f1aa76c..931d02678d19 100644
--- a/fs/xfs/libxfs/xfs_bmap.c
+++ b/fs/xfs/libxfs/xfs_bmap.c
@@ -41,6 +41,8 @@
#include "xfs_inode_util.h"
#include "xfs_rtgroup.h"
#include "xfs_zone_alloc.h"
+#include "xfs_fsverity.h"
+#include <linux/fsverity.h>
struct kmem_cache *xfs_bmap_intent_cache;
@@ -4451,6 +4453,11 @@ xfs_bmapi_convert_one_delalloc(
XFS_STATS_ADD(mp, xs_xstrat_bytes, XFS_FSB_TO_B(mp, bma.length));
XFS_STATS_INC(mp, xs_xstrat_quick);
+ if (xfs_iflags_test(ip, XFS_VERITY_CONSTRUCTION) &&
+ XFS_FSB_TO_B(mp, bma.got.br_startoff) >=
+ xfs_fsverity_metadata_offset(ip))
+ flags |= IOMAP_F_FSVERITY;
+
ASSERT(!isnullstartblock(bma.got.br_startblock));
xfs_bmbt_to_iomap(ip, iomap, &bma.got, 0, flags,
xfs_iomap_inode_sequence(ip, flags));
diff --git a/fs/xfs/xfs_aops.c b/fs/xfs/xfs_aops.c
index f279055fcea0..9503252a0fa4 100644
--- a/fs/xfs/xfs_aops.c
+++ b/fs/xfs/xfs_aops.c
@@ -22,6 +22,7 @@
#include "xfs_icache.h"
#include "xfs_zone_alloc.h"
#include "xfs_rtgroup.h"
+#include "xfs_fsverity.h"
#include <linux/bio-integrity.h>
struct xfs_writepage_ctx {
@@ -339,12 +340,16 @@ xfs_map_blocks(
int retries = 0;
int error = 0;
unsigned int *seq;
+ unsigned int iomap_flags = 0;
if (xfs_is_shutdown(mp))
return -EIO;
XFS_ERRORTAG_DELAY(mp, XFS_ERRTAG_WB_DELAY_MS);
+ if (xfs_iflags_test(ip, XFS_VERITY_CONSTRUCTION))
+ iomap_flags |= IOMAP_F_FSVERITY;
+
/*
* COW fork blocks can overlap data fork blocks even if the blocks
* aren't shared. COW I/O always takes precedent, so we must always
@@ -432,7 +437,8 @@ xfs_map_blocks(
isnullstartblock(imap.br_startblock))
goto allocate_blocks;
- xfs_bmbt_to_iomap(ip, &wpc->iomap, &imap, 0, 0, XFS_WPC(wpc)->data_seq);
+ xfs_bmbt_to_iomap(ip, &wpc->iomap, &imap, 0, iomap_flags,
+ XFS_WPC(wpc)->data_seq);
trace_xfs_map_blocks_found(ip, offset, count, whichfork, &imap);
return 0;
allocate_blocks:
@@ -705,6 +711,14 @@ xfs_vm_writepages(
},
};
+ /*
+ * Writeback does not work for folios past EOF, let it know that
+ * I/O happens for fsverity metadata and this restriction need
+ * to be skipped
+ */
+ if (xfs_iflags_test(ip, XFS_VERITY_CONSTRUCTION))
+ wpc.ctx.iomap.flags |= IOMAP_F_FSVERITY;
+
return iomap_writepages(&wpc.ctx);
}
}
diff --git a/fs/xfs/xfs_fsverity.c b/fs/xfs/xfs_fsverity.c
new file mode 100644
index 000000000000..6e6a8636a577
--- /dev/null
+++ b/fs/xfs/xfs_fsverity.c
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+#include "xfs_platform.h"
+#include "xfs_format.h"
+#include "xfs_inode.h"
+#include "xfs_shared.h"
+#include "xfs_trans_resv.h"
+#include "xfs_mount.h"
+#include "xfs_fsverity.h"
+#include "xfs_fsverity.h"
+#include <linux/fsverity.h>
+#include <linux/iomap.h>
+
+loff_t
+xfs_fsverity_metadata_offset(
+ const struct xfs_inode *ip)
+{
+ return round_up(i_size_read(VFS_IC(ip)), XFS_FSVERITY_START_ALIGN);
+}
diff --git a/fs/xfs/xfs_fsverity.h b/fs/xfs/xfs_fsverity.h
new file mode 100644
index 000000000000..5771db2cd797
--- /dev/null
+++ b/fs/xfs/xfs_fsverity.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+#ifndef __XFS_FSVERITY_H__
+#define __XFS_FSVERITY_H__
+
+#include "xfs_platform.h"
+
+#ifdef CONFIG_FS_VERITY
+loff_t xfs_fsverity_metadata_offset(const struct xfs_inode *ip);
+#else
+static inline loff_t xfs_fsverity_metadata_offset(const struct xfs_inode *ip)
+{
+ WARN_ON_ONCE(1);
+ return ULLONG_MAX;
+}
+#endif /* CONFIG_FS_VERITY */
+
+#endif /* __XFS_FSVERITY_H__ */
diff --git a/fs/xfs/xfs_inode.h b/fs/xfs/xfs_inode.h
index bd6d33557194..6df48d68a919 100644
--- a/fs/xfs/xfs_inode.h
+++ b/fs/xfs/xfs_inode.h
@@ -415,6 +415,12 @@ static inline bool xfs_inode_can_sw_atomic_write(const struct xfs_inode *ip)
*/
#define XFS_IREMAPPING (1U << 15)
+/*
+ * fs-verity's Merkle tree is under construction. The file is read-only, the
+ * only writes happening are for the fsverity metadata.
+ */
+#define XFS_VERITY_CONSTRUCTION (1U << 16)
+
/* All inode state flags related to inode reclaim. */
#define XFS_ALL_IRECLAIM_FLAGS (XFS_IRECLAIMABLE | \
XFS_IRECLAIM | \
diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c
index 9c2f12d5fec9..71ccd4ff5f48 100644
--- a/fs/xfs/xfs_iomap.c
+++ b/fs/xfs/xfs_iomap.c
@@ -32,6 +32,8 @@
#include "xfs_rtbitmap.h"
#include "xfs_icache.h"
#include "xfs_zone_alloc.h"
+#include "xfs_fsverity.h"
+#include <linux/fsverity.h>
#define XFS_ALLOC_ALIGN(mp, off) \
(((off) >> mp->m_allocsize_log) << mp->m_allocsize_log)
@@ -1789,6 +1791,9 @@ xfs_buffered_write_iomap_begin(
return xfs_direct_write_iomap_begin(inode, offset, count,
flags, iomap, srcmap);
+ if (xfs_iflags_test(ip, XFS_VERITY_CONSTRUCTION))
+ iomap_flags |= IOMAP_F_FSVERITY;
+
error = xfs_qm_dqattach(ip);
if (error)
return error;
@@ -2113,12 +2118,17 @@ xfs_read_iomap_begin(
bool shared = false;
unsigned int lockmode = XFS_ILOCK_SHARED;
u64 seq;
+ unsigned int iomap_flags = 0;
ASSERT(!(flags & (IOMAP_WRITE | IOMAP_ZERO)));
if (xfs_is_shutdown(mp))
return -EIO;
+ if (fsverity_active(inode) &&
+ (offset >= xfs_fsverity_metadata_offset(ip)))
+ iomap_flags |= IOMAP_F_FSVERITY;
+
error = xfs_ilock_for_iomap(ip, flags, &lockmode);
if (error)
return error;
@@ -2132,8 +2142,9 @@ xfs_read_iomap_begin(
if (error)
return error;
trace_xfs_iomap_found(ip, offset, length, XFS_DATA_FORK, &imap);
- return xfs_bmbt_to_iomap(ip, iomap, &imap, flags,
- shared ? IOMAP_F_SHARED : 0, seq);
+ iomap_flags |= shared ? IOMAP_F_SHARED : 0;
+
+ return xfs_bmbt_to_iomap(ip, iomap, &imap, flags, iomap_flags, seq);
}
const struct iomap_ops xfs_read_iomap_ops = {
--
2.51.2
^ permalink raw reply related
* [PATCH v9 13/22] xfs: disable direct read path for fs-verity files
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
The direct path is not supported on verity files. Attempts to use direct
I/O path on such files should fall back to buffered I/O path.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_file.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c
index a980ac5196a8..6fa9835f9531 100644
--- a/fs/xfs/xfs_file.c
+++ b/fs/xfs/xfs_file.c
@@ -282,7 +282,8 @@ xfs_file_dax_read(
struct kiocb *iocb,
struct iov_iter *to)
{
- struct xfs_inode *ip = XFS_I(iocb->ki_filp->f_mapping->host);
+ struct inode *inode = iocb->ki_filp->f_mapping->host;
+ struct xfs_inode *ip = XFS_I(inode);
ssize_t ret = 0;
trace_xfs_file_dax_read(iocb, to);
@@ -333,6 +334,14 @@ xfs_file_read_iter(
if (xfs_is_shutdown(mp))
return -EIO;
+ /*
+ * In case fs-verity is enabled, we also fallback to the buffered read
+ * from the direct read path. Therefore, IOCB_DIRECT is set and need to
+ * be cleared (see generic_file_read_iter())
+ */
+ if (fsverity_active(inode))
+ iocb->ki_flags &= ~IOCB_DIRECT;
+
if (IS_DAX(inode))
ret = xfs_file_dax_read(iocb, to);
else if (iocb->ki_flags & IOCB_DIRECT)
--
2.51.2
^ permalink raw reply related
* [PATCH v9 12/22] xfs: don't allow to enable DAX on fs-verity sealed inode
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
fs-verity doesn't support DAX. Forbid filesystem to enable DAX on
inodes which already have fs-verity enabled. The opposite is checked
when fs-verity is enabled, it won't be enabled if DAX is.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_iops.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/fs/xfs/xfs_iops.c b/fs/xfs/xfs_iops.c
index ca369eb96561..17efc83a86ed 100644
--- a/fs/xfs/xfs_iops.c
+++ b/fs/xfs/xfs_iops.c
@@ -1387,6 +1387,8 @@ xfs_inode_should_enable_dax(
return false;
if (!xfs_inode_supports_dax(ip))
return false;
+ if (ip->i_diflags2 & XFS_DIFLAG2_VERITY)
+ return false;
if (xfs_has_dax_always(ip->i_mount))
return true;
if (ip->i_diflags2 & XFS_DIFLAG2_DAX)
--
2.51.2
^ permalink raw reply related
* [PATCH v9 11/22] xfs: initialize fs-verity on file open
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
fs-verity will read and attach metadata (not the tree itself) from
a disk for those inodes which already have fs-verity enabled.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/xfs_file.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c
index 6246f34df9fd..a980ac5196a8 100644
--- a/fs/xfs/xfs_file.c
+++ b/fs/xfs/xfs_file.c
@@ -37,6 +37,7 @@
#include <linux/fadvise.h>
#include <linux/mount.h>
#include <linux/filelock.h>
+#include <linux/fsverity.h>
static const struct vm_operations_struct xfs_file_vm_ops;
@@ -1640,11 +1641,18 @@ xfs_file_open(
struct inode *inode,
struct file *file)
{
+ int error;
+
if (xfs_is_shutdown(XFS_M(inode->i_sb)))
return -EIO;
file->f_mode |= FMODE_NOWAIT | FMODE_CAN_ODIRECT;
if (xfs_get_atomic_write_min(XFS_I(inode)) > 0)
file->f_mode |= FMODE_CAN_ATOMIC_WRITE;
+
+ error = fsverity_file_open(inode, file);
+ if (error)
+ return error;
+
return generic_file_open(inode, file);
}
--
2.51.2
^ permalink raw reply related
* [PATCH v9 10/22] xfs: introduce fsverity on-disk changes
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Introduce XFS_DIFLAG2_VERITY for inodes with fsverity. This flag
indicates that inode has fs-verity enabled (i.e. descriptor exist,
tree is built and file is read-only).
Introduce XFS_SB_FEAT_RO_COMPAT_VERITY for filesystems having
fsverity inodes. As on-disk changes applies to fsverity inodes only, let
older kernels read-only access. This will be enabled in the further
patch after full fsverity support.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/xfs/libxfs/xfs_format.h | 30 +++++++++++++++++++++++++++++-
fs/xfs/libxfs/xfs_inode_buf.c | 8 ++++++++
fs/xfs/libxfs/xfs_inode_util.c | 2 ++
fs/xfs/libxfs/xfs_sb.c | 2 ++
fs/xfs/xfs_iops.c | 2 ++
fs/xfs/xfs_mount.h | 2 ++
6 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/fs/xfs/libxfs/xfs_format.h b/fs/xfs/libxfs/xfs_format.h
index 779dac59b1f3..4dff29659e40 100644
--- a/fs/xfs/libxfs/xfs_format.h
+++ b/fs/xfs/libxfs/xfs_format.h
@@ -374,6 +374,7 @@ xfs_sb_has_compat_feature(
#define XFS_SB_FEAT_RO_COMPAT_RMAPBT (1 << 1) /* reverse map btree */
#define XFS_SB_FEAT_RO_COMPAT_REFLINK (1 << 2) /* reflinked files */
#define XFS_SB_FEAT_RO_COMPAT_INOBTCNT (1 << 3) /* inobt block counts */
+#define XFS_SB_FEAT_RO_COMPAT_VERITY (1 << 4) /* fs-verity */
#define XFS_SB_FEAT_RO_COMPAT_ALL \
(XFS_SB_FEAT_RO_COMPAT_FINOBT | \
XFS_SB_FEAT_RO_COMPAT_RMAPBT | \
@@ -1230,16 +1231,21 @@ static inline void xfs_dinode_put_rdev(struct xfs_dinode *dip, xfs_dev_t rdev)
*/
#define XFS_DIFLAG2_METADATA_BIT 5
+/* inodes sealed with fs-verity */
+#define XFS_DIFLAG2_VERITY_BIT 6
+
#define XFS_DIFLAG2_DAX (1ULL << XFS_DIFLAG2_DAX_BIT)
#define XFS_DIFLAG2_REFLINK (1ULL << XFS_DIFLAG2_REFLINK_BIT)
#define XFS_DIFLAG2_COWEXTSIZE (1ULL << XFS_DIFLAG2_COWEXTSIZE_BIT)
#define XFS_DIFLAG2_BIGTIME (1ULL << XFS_DIFLAG2_BIGTIME_BIT)
#define XFS_DIFLAG2_NREXT64 (1ULL << XFS_DIFLAG2_NREXT64_BIT)
#define XFS_DIFLAG2_METADATA (1ULL << XFS_DIFLAG2_METADATA_BIT)
+#define XFS_DIFLAG2_VERITY (1ULL << XFS_DIFLAG2_VERITY_BIT)
#define XFS_DIFLAG2_ANY \
(XFS_DIFLAG2_DAX | XFS_DIFLAG2_REFLINK | XFS_DIFLAG2_COWEXTSIZE | \
- XFS_DIFLAG2_BIGTIME | XFS_DIFLAG2_NREXT64 | XFS_DIFLAG2_METADATA)
+ XFS_DIFLAG2_BIGTIME | XFS_DIFLAG2_NREXT64 | XFS_DIFLAG2_METADATA | \
+ XFS_DIFLAG2_VERITY)
static inline bool xfs_dinode_has_bigtime(const struct xfs_dinode *dip)
{
@@ -2021,4 +2027,26 @@ struct xfs_acl {
#define SGI_ACL_FILE_SIZE (sizeof(SGI_ACL_FILE)-1)
#define SGI_ACL_DEFAULT_SIZE (sizeof(SGI_ACL_DEFAULT)-1)
+/*
+ * At maximum of 8 levels with 128 hashes per block (32 bytes SHA-256) maximum
+ * tree size is ((128^8 − 1)/(128 − 1)) = 567*10^12 blocks. This should fit in
+ * 53 bits address space.
+ *
+ * At this Merkle tree size we can cover 295EB large file. This is much larger
+ * than the currently supported file size.
+ *
+ * For sha512 the largest file we can cover ends at 1 << 50 offset, this is also
+ * good.
+ */
+#define XFS_FSVERITY_LARGEST_FILE ((loff_t)1ULL << 53)
+
+/*
+ * Alignment of the fsverity metadata placement. This is largest supported PAGE
+ * SIZE for fsverity. This is used to space out data and metadata in page cache.
+ * The spacing is necessary for non-exposure of metadata to userspace and
+ * correct merkle tree synethesis in the iomap.
+ */
+#define XFS_FSVERITY_START_ALIGN (65536)
+
+
#endif /* __XFS_FORMAT_H__ */
diff --git a/fs/xfs/libxfs/xfs_inode_buf.c b/fs/xfs/libxfs/xfs_inode_buf.c
index 3794e5412eba..f2181c1bed54 100644
--- a/fs/xfs/libxfs/xfs_inode_buf.c
+++ b/fs/xfs/libxfs/xfs_inode_buf.c
@@ -760,6 +760,14 @@ xfs_dinode_verify(
!xfs_has_rtreflink(mp))
return __this_address;
+ /* only regular files can have fsverity */
+ if (flags2 & XFS_DIFLAG2_VERITY) {
+ if (!xfs_has_verity(mp))
+ return __this_address;
+ if (!S_ISREG(mode))
+ return __this_address;
+ }
+
if (xfs_has_zoned(mp) &&
dip->di_metatype == cpu_to_be16(XFS_METAFILE_RTRMAP)) {
if (be32_to_cpu(dip->di_used_blocks) > mp->m_sb.sb_rgextents)
diff --git a/fs/xfs/libxfs/xfs_inode_util.c b/fs/xfs/libxfs/xfs_inode_util.c
index 551fa51befb6..6b1e20a4bb9b 100644
--- a/fs/xfs/libxfs/xfs_inode_util.c
+++ b/fs/xfs/libxfs/xfs_inode_util.c
@@ -126,6 +126,8 @@ xfs_ip2xflags(
flags |= FS_XFLAG_DAX;
if (ip->i_diflags2 & XFS_DIFLAG2_COWEXTSIZE)
flags |= FS_XFLAG_COWEXTSIZE;
+ if (ip->i_diflags2 & XFS_DIFLAG2_VERITY)
+ flags |= FS_XFLAG_VERITY;
}
if (xfs_inode_has_attr_fork(ip))
diff --git a/fs/xfs/libxfs/xfs_sb.c b/fs/xfs/libxfs/xfs_sb.c
index 47322adb7690..a15510ebd2f1 100644
--- a/fs/xfs/libxfs/xfs_sb.c
+++ b/fs/xfs/libxfs/xfs_sb.c
@@ -165,6 +165,8 @@ xfs_sb_version_to_features(
features |= XFS_FEAT_REFLINK;
if (sbp->sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_INOBTCNT)
features |= XFS_FEAT_INOBTCNT;
+ if (sbp->sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_VERITY)
+ features |= XFS_FEAT_VERITY;
if (sbp->sb_features_incompat & XFS_SB_FEAT_INCOMPAT_FTYPE)
features |= XFS_FEAT_FTYPE;
if (sbp->sb_features_incompat & XFS_SB_FEAT_INCOMPAT_SPINODES)
diff --git a/fs/xfs/xfs_iops.c b/fs/xfs/xfs_iops.c
index 208543e57eda..ca369eb96561 100644
--- a/fs/xfs/xfs_iops.c
+++ b/fs/xfs/xfs_iops.c
@@ -1415,6 +1415,8 @@ xfs_diflags_to_iflags(
flags |= S_NOATIME;
if (init && xfs_inode_should_enable_dax(ip))
flags |= S_DAX;
+ if (xflags & FS_XFLAG_VERITY)
+ flags |= S_VERITY;
/*
* S_DAX can only be set during inode initialization and is never set by
diff --git a/fs/xfs/xfs_mount.h b/fs/xfs/xfs_mount.h
index ddd4028be8d6..07f6aa3c3f26 100644
--- a/fs/xfs/xfs_mount.h
+++ b/fs/xfs/xfs_mount.h
@@ -385,6 +385,7 @@ typedef struct xfs_mount {
#define XFS_FEAT_EXCHANGE_RANGE (1ULL << 27) /* exchange range */
#define XFS_FEAT_METADIR (1ULL << 28) /* metadata directory tree */
#define XFS_FEAT_ZONED (1ULL << 29) /* zoned RT device */
+#define XFS_FEAT_VERITY (1ULL << 30) /* fs-verity */
/* Mount features */
#define XFS_FEAT_NOLIFETIME (1ULL << 47) /* disable lifetime hints */
@@ -442,6 +443,7 @@ __XFS_HAS_FEAT(exchange_range, EXCHANGE_RANGE)
__XFS_HAS_FEAT(metadir, METADIR)
__XFS_HAS_FEAT(zoned, ZONED)
__XFS_HAS_FEAT(nolifetime, NOLIFETIME)
+__XFS_HAS_FEAT(verity, VERITY)
static inline bool xfs_has_rtgroups(const struct xfs_mount *mp)
{
--
2.51.2
^ permalink raw reply related
* [PATCH v9 09/22] iomap: introduce iomap_fsverity_write() for writing fsverity metadata
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
This is just a wrapper around iomap_file_buffered_write() to create
necessary iterator over metadata.
Reviewed-by: Christoph Hellwig <hch@lst.de>
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/iomap/buffered-io.c | 25 +++++++++++++++++++++++++
include/linux/iomap.h | 3 +++
2 files changed, 28 insertions(+)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 843314852663..5c9fd925a62f 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -1287,6 +1287,31 @@ iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i,
}
EXPORT_SYMBOL_GPL(iomap_file_buffered_write);
+int iomap_fsverity_write(struct file *file, loff_t pos, size_t length,
+ const void *buf, const struct iomap_ops *ops,
+ const struct iomap_write_ops *write_ops)
+{
+ int ret;
+ struct iov_iter iiter;
+ struct kvec kvec = {
+ .iov_base = (void *)buf,
+ .iov_len = length,
+ };
+ struct kiocb iocb = {
+ .ki_filp = file,
+ .ki_ioprio = get_current_ioprio(),
+ .ki_pos = pos,
+ };
+
+ iov_iter_kvec(&iiter, WRITE, &kvec, 1, length);
+
+ ret = iomap_file_buffered_write(&iocb, &iiter, ops, write_ops, NULL);
+ if (ret < 0)
+ return ret;
+ return ret == length ? 0 : -EIO;
+}
+EXPORT_SYMBOL_GPL(iomap_fsverity_write);
+
static void iomap_write_delalloc_ifs_punch(struct inode *inode,
struct folio *folio, loff_t start_byte, loff_t end_byte,
struct iomap *iomap, iomap_punch_t punch)
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index 4d9202cae29f..83586f09f365 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -359,6 +359,9 @@ static inline bool iomap_want_unshare_iter(const struct iomap_iter *iter)
ssize_t iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *from,
const struct iomap_ops *ops,
const struct iomap_write_ops *write_ops, void *private);
+int iomap_fsverity_write(struct file *file, loff_t pos, size_t length,
+ const void *buf, const struct iomap_ops *ops,
+ const struct iomap_write_ops *write_ops);
void iomap_read_folio(const struct iomap_ops *ops,
struct iomap_read_folio_ctx *ctx, void *private);
void iomap_readahead(const struct iomap_ops *ops,
--
2.51.2
^ permalink raw reply related
* [PATCH v9 08/22] iomap: teach iomap to read files with fsverity
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Obtain fsverity info for folios with file data and fsverity metadata.
Filesystem can pass vi down to ioend and then to fsverity for
verification. This is different from other filesystems ext4, f2fs, btrfs
supporting fsverity, these filesystems don't need fsverity_info for
reading fsverity metadata. While reading merkle tree iomap requires
fsverity info to synthesize hashes for zeroed data block.
fsverity metadata has two kinds of holes - ones in merkle tree and one
after fsverity descriptor.
Merkle tree holes are blocks full of hashes of zeroed data blocks. These
are not stored on the disk but synthesized on the fly. This saves a bit
of space for sparse files. Due to this iomap also need to lookup
fsverity_info for folios with fsverity metadata. ->vi has a hash of the
zeroed data block which will be used to fill the merkle tree block.
The hole past descriptor is interpreted as end of metadata region. As we
don't have EOF here we use this hole as an indication that rest of the
folio is empty. This patch marks rest of the folio beyond fsverity
descriptor as uptodate.
For file data, fsverity needs to verify consistency of the whole file
against the root hash, hashes of holes are included in the merkle tree.
Verify them too.
Issue reading of fsverity merkle tree on the fsverity inodes. This way
metadata will be available at I/O completion time.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/iomap/buffered-io.c | 41 +++++++++++++++++++++++++++++++++++++++--
include/linux/iomap.h | 2 ++
2 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 38c9592fba43..843314852663 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -9,6 +9,7 @@
#include <linux/swap.h>
#include <linux/migrate.h>
#include <linux/fserror.h>
+#include <linux/fsverity.h>
#include "internal.h"
#include "trace.h"
@@ -561,9 +562,27 @@ static int iomap_read_folio_iter(struct iomap_iter *iter,
if (plen == 0)
return 0;
- /* zero post-eof blocks as the page may be mapped */
- if (iomap_block_needs_zeroing(iter, pos)) {
+ /*
+ * Handling of fsverity "holes". We hit this for two case:
+ * 1. No need to go further, the hole after fsverity
+ * descriptor is the end of the fsverity metadata.
+ *
+ * 2. This folio contains merkle tree blocks which need to be
+ * synthesized. If we already have fsverity info (ctx->vi)
+ * synthesize these blocks.
+ */
+ if ((iomap->flags & IOMAP_F_FSVERITY) &&
+ iomap->type == IOMAP_HOLE) {
+ if (ctx->vi)
+ fsverity_fill_zerohash(folio, poff, plen,
+ ctx->vi);
+ iomap_set_range_uptodate(folio, poff, plen);
+ } else if (iomap_block_needs_zeroing(iter, pos)) {
+ /* zero post-eof blocks as the page may be mapped */
folio_zero_range(folio, poff, plen);
+ if (ctx->vi &&
+ !fsverity_verify_blocks(ctx->vi, folio, plen, poff))
+ return -EIO;
iomap_set_range_uptodate(folio, poff, plen);
} else {
if (!*bytes_submitted)
@@ -614,6 +633,15 @@ void iomap_read_folio(const struct iomap_ops *ops,
trace_iomap_readpage(iter.inode, 1);
+ /*
+ * Fetch fsverity_info for both data and fsverity metadata, as iomap
+ * needs zeroed hash for merkle tree block synthesis
+ */
+ ctx->vi = fsverity_get_info(iter.inode);
+ if (ctx->vi && iter.pos < i_size_read(iter.inode))
+ fsverity_readahead(ctx->vi, folio->index,
+ folio_nr_pages(folio));
+
while ((ret = iomap_iter(&iter, ops)) > 0)
iter.status = iomap_read_folio_iter(&iter, ctx,
&bytes_submitted);
@@ -681,6 +709,15 @@ void iomap_readahead(const struct iomap_ops *ops,
trace_iomap_readahead(rac->mapping->host, readahead_count(rac));
+ /*
+ * Fetch fsverity_info for both data and fsverity metadata, as iomap
+ * needs zeroed hash for merkle tree block synthesis
+ */
+ ctx->vi = fsverity_get_info(iter.inode);
+ if (ctx->vi && iter.pos < i_size_read(iter.inode))
+ fsverity_readahead(ctx->vi, readahead_index(rac),
+ readahead_count(rac));
+
while (iomap_iter(&iter, ops) > 0)
iter.status = iomap_readahead_iter(&iter, ctx,
&cur_bytes_submitted);
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index 4506a99d5285..4d9202cae29f 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -435,6 +435,7 @@ struct iomap_ioend {
loff_t io_offset; /* offset in the file */
sector_t io_sector; /* start sector of ioend */
void *io_private; /* file system private data */
+ struct fsverity_info *io_vi; /* fsverity info */
struct bio io_bio; /* MUST BE LAST! */
};
@@ -509,6 +510,7 @@ struct iomap_read_folio_ctx {
struct readahead_control *rac;
void *read_ctx;
loff_t read_ctx_file_offset;
+ struct fsverity_info *vi;
};
struct iomap_read_ops {
--
2.51.2
^ permalink raw reply related
* [PATCH v9 07/22] iomap: introduce IOMAP_F_FSVERITY and teach writeback to handle fsverity
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
This flag indicates that I/O is for fsverity metadata.
In the write path skip i_size check and i_size updates as metadata is
past EOF. In writeback don't update i_size and continue writeback if
even folio is beyond EOF. In read path don't zero fsverity folios, again
they are past EOF.
The iomap_block_needs_zeroing() is also called from write path. For
folios of larger order we don't want to zero out pages in the folio as
these could contain other merkle tree blocks. For fsverity, filesystem
will request to read PAGE_SIZE memory regions. For data folios, iomap
will zero the rest of the folio for anything which is beyond EOF. We
don't want this for fsverity folios.
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
---
fs/iomap/buffered-io.c | 43 +++++++++++++++++++++++++++++++++---------
fs/iomap/trace.h | 3 ++-
include/linux/iomap.h | 8 ++++++++
3 files changed, 44 insertions(+), 10 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index e4b6886e5c3c..38c9592fba43 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -353,9 +353,26 @@ static inline bool iomap_block_needs_zeroing(const struct iomap_iter *iter,
{
const struct iomap *srcmap = iomap_iter_srcmap(iter);
- return srcmap->type != IOMAP_MAPPED ||
- (srcmap->flags & IOMAP_F_NEW) ||
- pos >= i_size_read(iter->inode);
+ /*
+ * If this block has not been written, there's nothing to read
+ */
+ if (srcmap->type != IOMAP_MAPPED)
+ return true;
+
+ /*
+ * Newly allocated blocks have not been written
+ */
+ if (srcmap->flags & IOMAP_F_NEW)
+ return true;
+
+ /*
+ * fsverity metadata is stored past i_size, we need to read it instead
+ * of zeroing
+ */
+ if (srcmap->flags & IOMAP_F_FSVERITY)
+ return false;
+
+ return pos >= i_size_read(iter->inode);
}
/**
@@ -1167,13 +1184,14 @@ static int iomap_write_iter(struct iomap_iter *iter, struct iov_iter *i,
* unlock and release the folio.
*/
old_size = iter->inode->i_size;
- if (pos + written > old_size) {
+ if (pos + written > old_size &&
+ !(iter->iomap.flags & IOMAP_F_FSVERITY)) {
i_size_write(iter->inode, pos + written);
iter->iomap.flags |= IOMAP_F_SIZE_CHANGED;
}
__iomap_put_folio(iter, write_ops, written, folio);
- if (old_size < pos)
+ if (old_size < pos && !(iter->iomap.flags & IOMAP_F_FSVERITY))
pagecache_isize_extended(iter->inode, old_size, pos);
cond_resched();
@@ -1801,13 +1819,20 @@ static int iomap_writeback_range(struct iomap_writepage_ctx *wpc,
* Check interaction of the folio with the file end.
*
* If the folio is entirely beyond i_size, return false. If it straddles
- * i_size, adjust end_pos and zero all data beyond i_size.
+ * i_size, adjust end_pos and zero all data beyond i_size. Don't skip fsverity
+ * folios as those are beyond i_size.
*/
-static bool iomap_writeback_handle_eof(struct folio *folio, struct inode *inode,
- u64 *end_pos)
+static bool iomap_writeback_handle_eof(struct folio *folio,
+ struct iomap_writepage_ctx *wpc, u64 *end_pos)
{
+ struct inode *inode = wpc->inode;
u64 isize = i_size_read(inode);
+ if (wpc->iomap.flags & IOMAP_F_FSVERITY) {
+ WARN_ON_ONCE(folio_pos(folio) < isize);
+ return true;
+ }
+
if (*end_pos > isize) {
size_t poff = offset_in_folio(folio, isize);
pgoff_t end_index = isize >> PAGE_SHIFT;
@@ -1873,7 +1898,7 @@ int iomap_writeback_folio(struct iomap_writepage_ctx *wpc, struct folio *folio)
trace_iomap_writeback_folio(inode, pos, folio_size(folio));
- if (!iomap_writeback_handle_eof(folio, inode, &end_pos))
+ if (!iomap_writeback_handle_eof(folio, wpc, &end_pos))
return 0;
WARN_ON_ONCE(end_pos <= pos);
diff --git a/fs/iomap/trace.h b/fs/iomap/trace.h
index 532787277b16..5252051cc137 100644
--- a/fs/iomap/trace.h
+++ b/fs/iomap/trace.h
@@ -118,7 +118,8 @@ DEFINE_RANGE_EVENT(iomap_zero_iter);
{ IOMAP_F_ATOMIC_BIO, "ATOMIC_BIO" }, \
{ IOMAP_F_PRIVATE, "PRIVATE" }, \
{ IOMAP_F_SIZE_CHANGED, "SIZE_CHANGED" }, \
- { IOMAP_F_STALE, "STALE" }
+ { IOMAP_F_STALE, "STALE" }, \
+ { IOMAP_F_FSVERITY, "FSVERITY" }
#define IOMAP_DIO_STRINGS \
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index 531f9ebdeeae..4506a99d5285 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -87,6 +87,14 @@ struct vm_fault;
#define IOMAP_F_INTEGRITY 0
#endif /* CONFIG_BLK_DEV_INTEGRITY */
+/*
+ * Indicates reads and writes of fsverity metadata.
+ *
+ * Fsverity metadata is stored after the regular file data and thus beyond
+ * i_size.
+ */
+#define IOMAP_F_FSVERITY (1U << 10)
+
/*
* Flag reserved for file system specific usage
*/
--
2.51.2
^ permalink raw reply related
* [PATCH v9 06/22] fsverity: hoist pagecache_read from f2fs/ext4 to fsverity
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
This is the same function to read from pageache. XFS will also need
this, so move this to core fsverity.
Note that f2fs and ext4 functions diverged a bit, as ext4 operated over
folios and f2fs operated over pages. The common one will operate over
folios.
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Acked-by: Eric Biggers <ebiggers@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/ext4/verity.c | 32 +++-----------------------------
fs/f2fs/verity.c | 30 +-----------------------------
fs/verity/pagecache.c | 33 +++++++++++++++++++++++++++++++++
include/linux/fsverity.h | 2 ++
4 files changed, 39 insertions(+), 58 deletions(-)
diff --git a/fs/ext4/verity.c b/fs/ext4/verity.c
index 347945ac23a4..ac5c133f5529 100644
--- a/fs/ext4/verity.c
+++ b/fs/ext4/verity.c
@@ -34,32 +34,6 @@ static inline loff_t ext4_verity_metadata_pos(const struct inode *inode)
return round_up(inode->i_size, 65536);
}
-/*
- * Read some verity metadata from the inode. __vfs_read() can't be used because
- * we need to read beyond i_size.
- */
-static int pagecache_read(struct inode *inode, void *buf, size_t count,
- loff_t pos)
-{
- while (count) {
- struct folio *folio;
- size_t n;
-
- folio = read_mapping_folio(inode->i_mapping, pos >> PAGE_SHIFT,
- NULL);
- if (IS_ERR(folio))
- return PTR_ERR(folio);
-
- n = memcpy_from_file_folio(buf, folio, pos, count);
- folio_put(folio);
-
- buf += n;
- pos += n;
- count -= n;
- }
- return 0;
-}
-
/*
* Write some verity metadata to the inode for FS_IOC_ENABLE_VERITY.
* kernel_write() can't be used because the file descriptor is readonly.
@@ -311,8 +285,8 @@ static int ext4_get_verity_descriptor_location(struct inode *inode,
goto bad;
desc_size_pos -= sizeof(desc_size_disk);
- err = pagecache_read(inode, &desc_size_disk, sizeof(desc_size_disk),
- desc_size_pos);
+ err = fsverity_pagecache_read(inode, &desc_size_disk,
+ sizeof(desc_size_disk), desc_size_pos);
if (err)
return err;
desc_size = le32_to_cpu(desc_size_disk);
@@ -352,7 +326,7 @@ static int ext4_get_verity_descriptor(struct inode *inode, void *buf,
if (buf_size) {
if (desc_size > buf_size)
return -ERANGE;
- err = pagecache_read(inode, buf, desc_size, desc_pos);
+ err = fsverity_pagecache_read(inode, buf, desc_size, desc_pos);
if (err)
return err;
}
diff --git a/fs/f2fs/verity.c b/fs/f2fs/verity.c
index b3b3e71604ac..5ea0a9b40443 100644
--- a/fs/f2fs/verity.c
+++ b/fs/f2fs/verity.c
@@ -36,34 +36,6 @@ static inline loff_t f2fs_verity_metadata_pos(const struct inode *inode)
return round_up(inode->i_size, 65536);
}
-/*
- * Read some verity metadata from the inode. __vfs_read() can't be used because
- * we need to read beyond i_size.
- */
-static int pagecache_read(struct inode *inode, void *buf, size_t count,
- loff_t pos)
-{
- while (count) {
- size_t n = min_t(size_t, count,
- PAGE_SIZE - offset_in_page(pos));
- struct page *page;
-
- page = read_mapping_page(inode->i_mapping, pos >> PAGE_SHIFT,
- NULL);
- if (IS_ERR(page))
- return PTR_ERR(page);
-
- memcpy_from_page(buf, page, offset_in_page(pos), n);
-
- put_page(page);
-
- buf += n;
- pos += n;
- count -= n;
- }
- return 0;
-}
-
/*
* Write some verity metadata to the inode for FS_IOC_ENABLE_VERITY.
* kernel_write() can't be used because the file descriptor is readonly.
@@ -248,7 +220,7 @@ static int f2fs_get_verity_descriptor(struct inode *inode, void *buf,
if (buf_size) {
if (size > buf_size)
return -ERANGE;
- res = pagecache_read(inode, buf, size, pos);
+ res = fsverity_pagecache_read(inode, buf, size, pos);
if (res)
return res;
}
diff --git a/fs/verity/pagecache.c b/fs/verity/pagecache.c
index 99f5f53eea98..9d82e6b74ba1 100644
--- a/fs/verity/pagecache.c
+++ b/fs/verity/pagecache.c
@@ -78,3 +78,36 @@ void fsverity_fill_zerohash(struct folio *folio, size_t offset, size_t len,
vi->tree_params.digest_size);
}
EXPORT_SYMBOL_GPL(fsverity_fill_zerohash);
+
+/**
+ * fsverity_pagecache_read() - read page and copy data to buffer
+ * @inode: copy from this inode's address space
+ * @buf: buffer to copy to
+ * @count: number of bytes to copy
+ * @pos: position of the folio to copy from
+ *
+ * Read some verity metadata from the inode. __vfs_read() can't be used because
+ * we need to read beyond i_size.
+ */
+int fsverity_pagecache_read(struct inode *inode, void *buf, size_t count,
+ loff_t pos)
+{
+ while (count) {
+ struct folio *folio;
+ size_t n;
+
+ folio = read_mapping_folio(inode->i_mapping, pos >> PAGE_SHIFT,
+ NULL);
+ if (IS_ERR(folio))
+ return PTR_ERR(folio);
+
+ n = memcpy_from_file_folio(buf, folio, pos, count);
+ folio_put(folio);
+
+ buf += n;
+ pos += n;
+ count -= n;
+ }
+ return 0;
+}
+EXPORT_SYMBOL_GPL(fsverity_pagecache_read);
diff --git a/include/linux/fsverity.h b/include/linux/fsverity.h
index 9e7d946676b9..f9433332c274 100644
--- a/include/linux/fsverity.h
+++ b/include/linux/fsverity.h
@@ -328,5 +328,7 @@ void fsverity_cleanup_inode(struct inode *inode);
struct page *generic_read_merkle_tree_page(struct inode *inode, pgoff_t index);
void generic_readahead_merkle_tree(struct inode *inode, pgoff_t index,
unsigned long nr_pages);
+int fsverity_pagecache_read(struct inode *inode, void *buf, size_t count,
+ loff_t pos);
#endif /* _LINUX_FSVERITY_H */
--
2.51.2
^ permalink raw reply related
* [PATCH v9 05/22] fsverity: pass digest size and hash of the all-zeroes block to ->write
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Let filesystem iterate over hashes in the block and check if these are
hashes of zeroed data blocks. XFS will use this to decide if it want to
store tree block full of these hashes.
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
Acked-by: Eric Biggers <ebiggers@kernel.org>
---
fs/btrfs/verity.c | 6 +++++-
fs/ext4/verity.c | 4 +++-
fs/f2fs/verity.c | 4 +++-
fs/verity/enable.c | 4 +++-
include/linux/fsverity.h | 6 +++++-
5 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/fs/btrfs/verity.c b/fs/btrfs/verity.c
index 0062b3a55781..fd3696d3f4ce 100644
--- a/fs/btrfs/verity.c
+++ b/fs/btrfs/verity.c
@@ -773,11 +773,15 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode,
* @buf: Merkle tree block to write
* @pos: the position of the block in the Merkle tree (in bytes)
* @size: the Merkle tree block size (in bytes)
+ * @zero_digest: the hash of the all-zeroes block
+ * @digest_size: size of zero_digest, in bytes
*
* Returns 0 on success or negative error code on failure
*/
static int btrfs_write_merkle_tree_block(struct file *file, const void *buf,
- u64 pos, unsigned int size)
+ u64 pos, unsigned int size,
+ const u8 *zero_digest,
+ unsigned int digest_size)
{
struct inode *inode = file_inode(file);
loff_t merkle_pos = merkle_file_pos(inode);
diff --git a/fs/ext4/verity.c b/fs/ext4/verity.c
index ca61da53f313..347945ac23a4 100644
--- a/fs/ext4/verity.c
+++ b/fs/ext4/verity.c
@@ -374,7 +374,9 @@ static void ext4_readahead_merkle_tree(struct inode *inode, pgoff_t index,
}
static int ext4_write_merkle_tree_block(struct file *file, const void *buf,
- u64 pos, unsigned int size)
+ u64 pos, unsigned int size,
+ const u8 *zero_digest,
+ unsigned int digest_size)
{
pos += ext4_verity_metadata_pos(file_inode(file));
diff --git a/fs/f2fs/verity.c b/fs/f2fs/verity.c
index 92ebcc19cab0..b3b3e71604ac 100644
--- a/fs/f2fs/verity.c
+++ b/fs/f2fs/verity.c
@@ -270,7 +270,9 @@ static void f2fs_readahead_merkle_tree(struct inode *inode, pgoff_t index,
}
static int f2fs_write_merkle_tree_block(struct file *file, const void *buf,
- u64 pos, unsigned int size)
+ u64 pos, unsigned int size,
+ const u8 *zero_digest,
+ unsigned int digest_size)
{
pos += f2fs_verity_metadata_pos(file_inode(file));
diff --git a/fs/verity/enable.c b/fs/verity/enable.c
index 42dfed1ce0ce..ad4ff71d7dd9 100644
--- a/fs/verity/enable.c
+++ b/fs/verity/enable.c
@@ -50,7 +50,9 @@ static int write_merkle_tree_block(struct file *file, const u8 *buf,
int err;
err = inode->i_sb->s_vop->write_merkle_tree_block(file, buf, pos,
- params->block_size);
+ params->block_size,
+ params->zero_digest,
+ params->digest_size);
if (err)
fsverity_err(inode, "Error %d writing Merkle tree block %lu",
err, index);
diff --git a/include/linux/fsverity.h b/include/linux/fsverity.h
index 3c3250f6f272..9e7d946676b9 100644
--- a/include/linux/fsverity.h
+++ b/include/linux/fsverity.h
@@ -124,6 +124,8 @@ struct fsverity_operations {
* @buf: the Merkle tree block to write
* @pos: the position of the block in the Merkle tree (in bytes)
* @size: the Merkle tree block size (in bytes)
+ * @zero_digest: the hash of the all-zeroes block
+ * @digest_size: size of zero_digest, in bytes
*
* This is only called between ->begin_enable_verity() and
* ->end_enable_verity().
@@ -131,7 +133,9 @@ struct fsverity_operations {
* Return: 0 on success, -errno on failure
*/
int (*write_merkle_tree_block)(struct file *file, const void *buf,
- u64 pos, unsigned int size);
+ u64 pos, unsigned int size,
+ const u8 *zero_digest,
+ unsigned int digest_size);
};
#ifdef CONFIG_FS_VERITY
--
2.51.2
^ permalink raw reply related
* [PATCH v9 04/22] fsverity: generate and store zero-block hash
From: Andrey Albershteyn @ 2026-04-28 8:33 UTC (permalink / raw)
To: linux-xfs, fsverity, linux-fsdevel, ebiggers
Cc: Andrey Albershteyn, hch, linux-ext4, linux-f2fs-devel,
linux-btrfs, linux-unionfs, djwong
In-Reply-To: <20260428083332.768693-1-aalbersh@kernel.org>
Compute the hash of one filesystem block's worth of zeros. A filesystem
implementation can decide to elide merkle tree blocks containing only
this hash and synthesize the contents at read time.
Let's pretend that there's a file containing 131 data block and whose
merkle tree looks roughly like this:
root
+--leaf0
| +--data0
| +--data1
| +--...
| `--data128
`--leaf1
+--data129
+--data130
`--data131
If data[0-128] are sparse holes, then leaf0 will contain a repeating
sequence of @zero_digest. Therefore, leaf0 need not be written to disk
because its contents can be synthesized.
A subsequent xfs patch will use this to reduce the size of the merkle
tree when dealing with sparse gold master disk images and the like.
Note that this works only on the first-level (data holes). fsverity
doesn't store/generate zero_digest for any higher levels.
Add a helper to pre-fill folio with hashes of empty blocks. This will be
used by iomap to synthesize blocks full of zero hashes on the fly.
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Acked-by: Eric Biggers <ebiggers@kernel.org>
Signed-off-by: Andrey Albershteyn <aalbersh@kernel.org>
---
fs/verity/fsverity_private.h | 3 +++
fs/verity/measure.c | 4 ++--
fs/verity/open.c | 3 +++
fs/verity/pagecache.c | 22 ++++++++++++++++++++++
include/linux/fsverity.h | 8 ++++++++
5 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/fs/verity/fsverity_private.h b/fs/verity/fsverity_private.h
index 6e6854c19078..881d46f25e08 100644
--- a/fs/verity/fsverity_private.h
+++ b/fs/verity/fsverity_private.h
@@ -53,6 +53,9 @@ struct merkle_tree_params {
u64 tree_size; /* Merkle tree size in bytes */
unsigned long tree_pages; /* Merkle tree size in pages */
+ /* the hash of an all-zeroes block */
+ u8 zero_digest[FS_VERITY_MAX_DIGEST_SIZE];
+
/*
* Starting block index for each tree level, ordered from leaf level (0)
* to root level ('num_levels - 1')
diff --git a/fs/verity/measure.c b/fs/verity/measure.c
index 6a35623ebdf0..818083507885 100644
--- a/fs/verity/measure.c
+++ b/fs/verity/measure.c
@@ -68,8 +68,8 @@ EXPORT_SYMBOL_GPL(fsverity_ioctl_measure);
* @alg: (out) the digest's algorithm, as a FS_VERITY_HASH_ALG_* value
* @halg: (out) the digest's algorithm, as a HASH_ALGO_* value
*
- * Retrieves the fsverity digest of the given file. The file must have been
- * opened at least once since the inode was last loaded into the inode cache;
+ * Retrieves the fsverity digest of the given file. The
+ * fsverity_ensure_verity_info() must be called on the inode beforehand;
* otherwise this function will not recognize when fsverity is enabled.
*
* The file's fsverity digest consists of @raw_digest in combination with either
diff --git a/fs/verity/open.c b/fs/verity/open.c
index d32d0899df25..875e8850ccba 100644
--- a/fs/verity/open.c
+++ b/fs/verity/open.c
@@ -153,6 +153,9 @@ int fsverity_init_merkle_tree_params(struct merkle_tree_params *params,
goto out_err;
}
+ fsverity_hash_block(params, page_address(ZERO_PAGE(0)),
+ params->zero_digest);
+
params->tree_size = offset << log_blocksize;
params->tree_pages = PAGE_ALIGN(params->tree_size) >> PAGE_SHIFT;
return 0;
diff --git a/fs/verity/pagecache.c b/fs/verity/pagecache.c
index 1819314ecaa3..99f5f53eea98 100644
--- a/fs/verity/pagecache.c
+++ b/fs/verity/pagecache.c
@@ -2,6 +2,7 @@
/*
* Copyright 2019 Google LLC
*/
+#include "fsverity_private.h"
#include <linux/export.h>
#include <linux/fsverity.h>
@@ -56,3 +57,24 @@ void generic_readahead_merkle_tree(struct inode *inode, pgoff_t index,
folio_put(folio);
}
EXPORT_SYMBOL_GPL(generic_readahead_merkle_tree);
+
+/**
+ * fsverity_fill_zerohash() - fill folio with hashes of zero data block
+ * @folio: folio to fill
+ * @offset: offset in the folio to start
+ * @len: length of the range to fill with hashes
+ * @vi: fsverity info
+ */
+void fsverity_fill_zerohash(struct folio *folio, size_t offset, size_t len,
+ struct fsverity_info *vi)
+{
+ size_t off = offset;
+
+ WARN_ON_ONCE(!IS_ALIGNED(offset, vi->tree_params.digest_size));
+ WARN_ON_ONCE(!IS_ALIGNED(len, vi->tree_params.digest_size));
+
+ for (; off < (offset + len); off += vi->tree_params.digest_size)
+ memcpy_to_folio(folio, off, vi->tree_params.zero_digest,
+ vi->tree_params.digest_size);
+}
+EXPORT_SYMBOL_GPL(fsverity_fill_zerohash);
diff --git a/include/linux/fsverity.h b/include/linux/fsverity.h
index 5562271bd628..3c3250f6f272 100644
--- a/include/linux/fsverity.h
+++ b/include/linux/fsverity.h
@@ -201,6 +201,8 @@ bool fsverity_verify_blocks(struct fsverity_info *vi, struct folio *folio,
size_t len, size_t offset);
void fsverity_verify_bio(struct fsverity_info *vi, struct bio *bio);
void fsverity_enqueue_verify_work(struct work_struct *work);
+void fsverity_fill_zerohash(struct folio *folio, size_t offset, size_t len,
+ struct fsverity_info *vi);
#else /* !CONFIG_FS_VERITY */
@@ -281,6 +283,12 @@ static inline void fsverity_enqueue_verify_work(struct work_struct *work)
WARN_ON_ONCE(1);
}
+static inline void fsverity_fill_zerohash(struct folio *folio, size_t offset,
+ size_t len, struct fsverity_info *vi)
+{
+ WARN_ON_ONCE(1);
+}
+
#endif /* !CONFIG_FS_VERITY */
static inline bool fsverity_verify_folio(struct fsverity_info *vi,
--
2.51.2
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox