* [PATCH] ubifs: zero stale data beyond EOF before file size grows
@ 2026-10-16 9:04 Nan Lin
2026-10-16 9:44 ` sashiko-bot
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-16 9:04 UTC (permalink / raw)
To: richard, chengzhihao1
Cc: linux-mtd, linux-kernel, lin.nan, wangxiaoqing, zhou.kete
mmap writes can dirty the page cache beyond EOF without changing
i_size. A later extending write or ftruncate turns that region into a
hole, but UBIFS never zeroes the partial page at the old EOF, so reads
expose the stale data.
Zero the tail of the old-EOF folio in all three size-extending paths:
ubifs_write_begin() for writes starting beyond EOF, do_setattr()
before truncate_setsize() grows the size, and ubifs_vm_page_mkwrite()
so mmap writes cannot leave such a tail behind. This mirrors what
block_page_mkwrite() does for buffer_head filesystems and what ext4
does on size extension.
All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
the folio_lock -> ui_mutex order of ->writepage() and
->page_mkwrite(); the reverse order deadlocks.
Signed-off-by: Nan Lin <lin.nan@h3c.com>
---
fs/ubifs/file.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
index e73c28b12f97..e09b17d36495 100644
--- a/fs/ubifs/file.c
+++ b/fs/ubifs/file.c
@@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
ubifs_release_budget(c, &req);
}
+/**
+ * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
+ * @inode: inode whose size is about to grow
+ * @new_end: new end of the file, only used to bound the tail
+ *
+ * Zero the cached folio containing the old EOF for [old EOF,
+ * min(folio end, @new_end)) so a size extension does not expose stale
+ * data left beyond EOF by mmap writes.
+ *
+ * Must be called without ui->ui_mutex held: we block on the old-EOF
+ * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
+ * lock while taking ui_mutex; the reverse order deadlocks.
+ */
+static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
+{
+ loff_t old_size = i_size_read(inode);
+ unsigned int from, to;
+ struct folio *folio;
+
+ if (new_end <= old_size)
+ return;
+
+ folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
+ if (IS_ERR(folio))
+ return;
+
+ /*
+ * Re-read i_size under the folio lock: a concurrent truncate may
+ * have shrunk the file, turning this folio into valid data.
+ */
+ old_size = i_size_read(inode);
+ if (new_end <= old_size)
+ goto out;
+
+ from = old_size & (PAGE_SIZE - 1);
+ if (!from)
+ goto out;
+ to = (old_size >> PAGE_SHIFT) == (new_end >> PAGE_SHIFT) ?
+ new_end & (PAGE_SIZE - 1) : PAGE_SIZE;
+ if (to > from)
+ folio_zero_range(folio, from, to - from);
+
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
static int write_begin_slow(struct address_space *mapping,
loff_t pos, unsigned len, struct folio **foliop)
{
@@ -423,6 +470,14 @@ static int ubifs_write_begin(const struct kiocb *iocb,
if (unlikely(c->ro_error))
return -EROFS;
+ /*
+ * The write starts beyond EOF: zero the stale tail of the old-EOF
+ * folio before budgeting takes ui->ui_mutex. do_readpage() only
+ * sanitizes the write folio, not the old-EOF folio.
+ */
+ if (pos > inode->i_size)
+ ubifs_zero_eof_hole(inode, pos);
+
/* Try out the fast-path part first */
folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
mapping_gfp_mask(mapping));
@@ -1220,6 +1275,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
if (attr->ia_valid & ATTR_SIZE) {
dbg_gen("size %lld -> %lld", inode->i_size, new_size);
+
+ /* Must run before the size change loses the old boundary. */
+ ubifs_zero_eof_hole(inode, new_size);
+
truncate_setsize(inode, new_size);
}
@@ -1494,6 +1553,7 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
struct timespec64 now = current_time(inode);
struct ubifs_budget_req req = { .new_page = 1 };
int err, update_time;
+ loff_t i_size;
dbg_gen("ino %llu, pg %lu, i_size %lld", inode->i_ino, folio->index,
i_size_read(inode));
@@ -1553,6 +1613,19 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
filemap_dirty_folio(folio->mapping, folio);
}
+ /*
+ * Zero the part of the EOF folio beyond i_size so mmap writes
+ * cannot leave a stale tail behind (see ubifs_zero_eof_hole()).
+ * For a page-aligned i_size the mkwrite entry check has already
+ * returned SIGBUS, so @from is always non-zero here.
+ */
+ i_size = i_size_read(inode);
+ if (folio->index == (i_size >> PAGE_SHIFT)) {
+ unsigned int from = i_size & (PAGE_SIZE - 1);
+
+ folio_zero_range(folio, from, PAGE_SIZE - from);
+ }
+
if (update_time) {
int release;
struct ubifs_inode *ui = ubifs_inode(inode);
--
2.33.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH] ubifs: zero stale data beyond EOF before file size grows
2026-10-16 9:04 [PATCH] ubifs: zero stale data beyond EOF before file size grows Nan Lin
@ 2026-10-16 9:44 ` sashiko-bot
2026-10-19 3:25 ` [PATCH v2] " Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-10-16 9:44 UTC (permalink / raw)
To: Nan Lin, richard, chengzhihao1; +Cc: linux-mtd, wangxiaoqing, zhou.kete
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ubifs: data corruption when extending files due to page-based arithmetic on large folios
--
commit 979d29c432b12d088592388772df15df41ba2373
Author: Nan Lin <lin.nan@h3c.com>
ubifs: zero stale data beyond EOF before file size grows
This commit zeroes the tail of the old-EOF folio before file size grows
to prevent exposing stale data from mmap writes. It handles writes
starting beyond EOF, truncation size growth, and page_mkwrite operations.
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97f..e09b17d364952 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
[ ... ]
> @@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Must be called without ui->ui_mutex held: we block on the old-EOF
> + * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
> + * lock while taking ui_mutex; the reverse order deadlocks.
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
[ ... ]
> + from = old_size & (PAGE_SIZE - 1);
> + if (!from)
> + goto out;
> + to = (old_size >> PAGE_SHIFT) == (new_end >> PAGE_SHIFT) ?
> + new_end & (PAGE_SIZE - 1) : PAGE_SIZE;
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
[Severity: High]
Does this use of page-relative arithmetic with folio_zero_range() cause
data corruption when dealing with large folios?
The offset 'from' is calculated as 'old_size & (PAGE_SIZE - 1)'. If 'old_size'
is located beyond the first page of a large folio (e.g., at byte 4097), 'from'
evaluates to an offset within the sub-page (1).
However, folio_zero_range() expects a byte offset relative to the start of
the entire folio. Passing 1 here would cause it to zero starting from byte 1
of the folio (and thus the file), corrupting valid file data. Furthermore,
'to' is capped at PAGE_SIZE, which would fail to zero the rest of the hole if
it spans multiple pages within the same large folio.
Should the calculation use offset_in_folio() and folio_size() instead?
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
[ ... ]
> @@ -1553,6 +1613,19 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
> filemap_dirty_folio(folio->mapping, folio);
> }
>
> + /*
> + * Zero the part of the EOF folio beyond i_size so mmap writes
> + * cannot leave a stale tail behind (see ubifs_zero_eof_hole()).
> + * For a page-aligned i_size the mkwrite entry check has already
> + * returned SIGBUS, so @from is always non-zero here.
> + */
> + i_size = i_size_read(inode);
> + if (folio->index == (i_size >> PAGE_SHIFT)) {
> + unsigned int from = i_size & (PAGE_SIZE - 1);
> +
> + folio_zero_range(folio, from, PAGE_SIZE - from);
> + }
[Severity: High]
Does the condition 'folio->index == (i_size >> PAGE_SHIFT)' evaluate to false
for the second or later page of a large folio?
If so, it would skip tail zeroing entirely when the EOF is located in a large
folio but not within its first page. The calculation for 'from' here also seems
to apply incorrect page-relative sizes rather than folio-relative.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261016090458.1891272-1-lin.nan@h3c.com?part=1
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-16 9:44 ` sashiko-bot
@ 2026-10-19 3:25 ` Nan Lin
2026-10-19 12:38 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-19 3:25 UTC (permalink / raw)
To: sashiko-bot
Cc: chengzhihao1, lin.nan, linux-mtd, richard, sashiko-reviews,
wangxiaoqing, zhou.kete
mmap writes can dirty the page cache beyond EOF without changing
i_size. A later extending write or ftruncate turns that region into a
hole, but UBIFS never zeroes the partial page at the old EOF, so reads
expose the stale data.
Zero the tail of the old-EOF folio in all three size-extending paths:
ubifs_write_begin() for writes starting beyond EOF, do_setattr()
before truncate_setsize() grows the size, and ubifs_vm_page_mkwrite()
so mmap writes cannot leave such a tail behind. This mirrors what
block_page_mkwrite() does for buffer_head filesystems and what ext4
does on size extension.
All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
the folio_lock -> ui_mutex order of ->writepage() and
->page_mkwrite(); the reverse order deadlocks.
UBIFS does not enable large folios, so the EOF folio is always a
single page today; nevertheless, use folio-relative arithmetic
(offset_in_folio(), folio_contains(), folio_size()) throughout so the
code is correct if that ever changes.
Signed-off-by: Nan Lin <lin.nan@h3c.com>
Changes since v1:
- Both zeroing sites used PAGE_SIZE-relative arithmetic
(offset_in_page()-style) while folio_zero_range() expects
folio-relative offsets. UBIFS does not currently enable large
folios, so PAGE_SIZE == folio_size() and the two coincide in
practice, but switch to offset_in_folio()/folio_size() anyway so
the code is correct if UBIFS ever opts into large folios
(suggested by Sashiko AI review).
---
fs/ubifs/file.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
index e73c28b12f97..1726714161a1 100644
--- a/fs/ubifs/file.c
+++ b/fs/ubifs/file.c
@@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
ubifs_release_budget(c, &req);
}
+/**
+ * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
+ * @inode: inode whose size is about to grow
+ * @new_end: new end of the file, only used to bound the tail
+ *
+ * Zero the cached folio containing the old EOF for [old EOF,
+ * min(folio end, @new_end)) so a size extension does not expose stale
+ * data left beyond EOF by mmap writes.
+ *
+ * Must be called without ui->ui_mutex held: we block on the old-EOF
+ * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
+ * lock while taking ui_mutex; the reverse order deadlocks.
+ */
+static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
+{
+ loff_t old_size = i_size_read(inode);
+ size_t from, to;
+ struct folio *folio;
+
+ if (new_end <= old_size)
+ return;
+
+ folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
+ if (IS_ERR(folio))
+ return;
+
+ /*
+ * Re-read i_size under the folio lock: a concurrent truncate may
+ * have shrunk the file, turning this folio into valid data.
+ */
+ old_size = i_size_read(inode);
+ if (new_end <= old_size)
+ goto out;
+
+ from = offset_in_folio(folio, old_size);
+ if (!from)
+ goto out;
+ to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
+ offset_in_folio(folio, new_end) : folio_size(folio);
+ if (to > from)
+ folio_zero_range(folio, from, to - from);
+
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
static int write_begin_slow(struct address_space *mapping,
loff_t pos, unsigned len, struct folio **foliop)
{
@@ -423,6 +470,14 @@ static int ubifs_write_begin(const struct kiocb *iocb,
if (unlikely(c->ro_error))
return -EROFS;
+ /*
+ * The write starts beyond EOF: zero the stale tail of the old-EOF
+ * folio before budgeting takes ui->ui_mutex. do_readpage() only
+ * sanitizes the write folio, not the old-EOF folio.
+ */
+ if (pos > inode->i_size)
+ ubifs_zero_eof_hole(inode, pos);
+
/* Try out the fast-path part first */
folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
mapping_gfp_mask(mapping));
@@ -1220,6 +1275,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
if (attr->ia_valid & ATTR_SIZE) {
dbg_gen("size %lld -> %lld", inode->i_size, new_size);
+
+ /* Must run before the size change loses the old boundary. */
+ ubifs_zero_eof_hole(inode, new_size);
+
truncate_setsize(inode, new_size);
}
@@ -1494,6 +1553,7 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
struct timespec64 now = current_time(inode);
struct ubifs_budget_req req = { .new_page = 1 };
int err, update_time;
+ loff_t i_size;
dbg_gen("ino %llu, pg %lu, i_size %lld", inode->i_ino, folio->index,
i_size_read(inode));
@@ -1553,6 +1613,19 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
filemap_dirty_folio(folio->mapping, folio);
}
+ /*
+ * Zero the part of the EOF folio beyond i_size so mmap writes
+ * cannot leave a stale tail behind (see ubifs_zero_eof_hole()).
+ * For a folio-aligned i_size the mkwrite entry check has already
+ * returned SIGBUS, so @from is always non-zero here.
+ */
+ i_size = i_size_read(inode);
+ if (folio_contains(folio, i_size >> PAGE_SHIFT)) {
+ size_t from = offset_in_folio(folio, i_size);
+
+ folio_zero_range(folio, from, folio_size(folio) - from);
+ }
+
if (update_time) {
int release;
struct ubifs_inode *ui = ubifs_inode(inode);
--
2.33.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-19 3:25 ` [PATCH v2] " Nan Lin
@ 2026-10-19 12:38 ` Zhihao Cheng
2026-10-19 12:45 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-19 12:38 UTC (permalink / raw)
To: Nan Lin, sashiko-bot
Cc: linux-mtd, richard, sashiko-reviews, wangxiaoqing, zhou.kete
在 2026/10/19 11:25, Nan Lin 写道:
Hi Nan Lin,
> mmap writes can dirty the page cache beyond EOF without changing
> i_size. A later extending write or ftruncate turns that region into a
> hole, but UBIFS never zeroes the partial page at the old EOF, so reads
> expose the stale data.
>
> Zero the tail of the old-EOF folio in all three size-extending paths:
> ubifs_write_begin() for writes starting beyond EOF, do_setattr()
> before truncate_setsize() grows the size, and ubifs_vm_page_mkwrite()
> so mmap writes cannot leave such a tail behind. This mirrors what
> block_page_mkwrite() does for buffer_head filesystems and what ext4
> does on size extension.
>
> All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
> the folio_lock -> ui_mutex order of ->writepage() and
> ->page_mkwrite(); the reverse order deadlocks.
>
> UBIFS does not enable large folios, so the EOF folio is always a
> single page today; nevertheless, use folio-relative arithmetic
> (offset_in_folio(), folio_contains(), folio_size()) throughout so the
> code is correct if that ever changes.
>
> Signed-off-by: Nan Lin <lin.nan@h3c.com>
>
> Changes since v1:
> - Both zeroing sites used PAGE_SIZE-relative arithmetic
> (offset_in_page()-style) while folio_zero_range() expects
> folio-relative offsets. UBIFS does not currently enable large
> folios, so PAGE_SIZE == folio_size() and the two coincide in
> practice, but switch to offset_in_folio()/folio_size() anyway so
> the code is correct if UBIFS ever opts into large folios
> (suggested by Sashiko AI review).
>
> ---
> fs/ubifs/file.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 73 insertions(+)
>
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97..1726714161a1 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
> @@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Must be called without ui->ui_mutex held: we block on the old-EOF
> + * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
> + * lock while taking ui_mutex; the reverse order deadlocks.
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
> + loff_t old_size = i_size_read(inode);
> + size_t from, to;
> + struct folio *folio;
> +
> + if (new_end <= old_size)
> + return;
> +
> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
> + if (IS_ERR(folio))
> + return;
The filemap_lock_folio gets folio with FGP_LOCK, I think we should use
'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh() does,
because the folio may not exist in ram when calling do_setattr().
> +
> + /*
> + * Re-read i_size under the folio lock: a concurrent truncate may
> + * have shrunk the file, turning this folio into valid data.
> + */
> + old_size = i_size_read(inode);
> + if (new_end <= old_size)
> + goto out;
> +
> + from = offset_in_folio(folio, old_size);
> + if (!from)
> + goto out;
Why skipping 'from == 0'? What if a user writes begin from
offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
**block_start**, **from**), but ubifs not handle it.
> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
> + offset_in_folio(folio, new_end) : folio_size(folio);
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
> +
> static int write_begin_slow(struct address_space *mapping,
> loff_t pos, unsigned len, struct folio **foliop)
> {
> @@ -423,6 +470,14 @@ static int ubifs_write_begin(const struct kiocb *iocb,
> if (unlikely(c->ro_error))
> return -EROFS;
>
> + /*
> + * The write starts beyond EOF: zero the stale tail of the old-EOF
> + * folio before budgeting takes ui->ui_mutex. do_readpage() only
> + * sanitizes the write folio, not the old-EOF folio.
> + */
> + if (pos > inode->i_size)
> + ubifs_zero_eof_hole(inode, pos);
> +
The ext4 protection check is ext4_write_checks->ext4_block_zero_eof, I
think we can add the zero hole check in ubifs_write_iter().
> /* Try out the fast-path part first */
> folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
> mapping_gfp_mask(mapping));
> @@ -1220,6 +1275,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
>
> if (attr->ia_valid & ATTR_SIZE) {
> dbg_gen("size %lld -> %lld", inode->i_size, new_size);
> +
> + /* Must run before the size change loses the old boundary. */
> + ubifs_zero_eof_hole(inode, new_size);
> +
> truncate_setsize(inode, new_size);
> }
>
> @@ -1494,6 +1553,7 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
> struct timespec64 now = current_time(inode);
> struct ubifs_budget_req req = { .new_page = 1 };
> int err, update_time;
> + loff_t i_size;
>
> dbg_gen("ino %llu, pg %lu, i_size %lld", inode->i_ino, folio->index,
> i_size_read(inode));
> @@ -1553,6 +1613,19 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct vm_fault *vmf)
> filemap_dirty_folio(folio->mapping, folio);
> }
>
> + /*
> + * Zero the part of the EOF folio beyond i_size so mmap writes
> + * cannot leave a stale tail behind (see ubifs_zero_eof_hole()).
> + * For a folio-aligned i_size the mkwrite entry check has already
> + * returned SIGBUS, so @from is always non-zero here.
> + */
> + i_size = i_size_read(inode);
> + if (folio_contains(folio, i_size >> PAGE_SHIFT)) {
> + size_t from = offset_in_folio(folio, i_size);
> +
If the program enters into this branch, the ubifs file's isize is not
zero, ubifs_read_folio has already zerod tail, do we still need this
folio_zero_range?
> + folio_zero_range(folio, from, folio_size(folio) - from);
> + }
> +
> if (update_time) {
> int release;
> struct ubifs_inode *ui = ubifs_inode(inode);
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-19 12:38 ` Zhihao Cheng
@ 2026-10-19 12:45 ` Zhihao Cheng
2026-10-22 3:18 ` Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-19 12:45 UTC (permalink / raw)
To: Nan Lin, sashiko-bot
Cc: linux-mtd, richard, sashiko-reviews, wangxiaoqing, zhou.kete
在 2026/10/19 20:38, Zhihao Cheng 写道:
> 在 2026/10/19 11:25, Nan Lin 写道:
> Hi Nan Lin,
>> mmap writes can dirty the page cache beyond EOF without changing
>> i_size. A later extending write or ftruncate turns that region into a
>> hole, but UBIFS never zeroes the partial page at the old EOF, so reads
>> expose the stale data.
>>
>> Zero the tail of the old-EOF folio in all three size-extending paths:
>> ubifs_write_begin() for writes starting beyond EOF, do_setattr()
>> before truncate_setsize() grows the size, and ubifs_vm_page_mkwrite()
>> so mmap writes cannot leave such a tail behind. This mirrors what
>> block_page_mkwrite() does for buffer_head filesystems and what ext4
>> does on size extension.
>>
>> All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
>> the folio_lock -> ui_mutex order of ->writepage() and
>> ->page_mkwrite(); the reverse order deadlocks.
>>
>> UBIFS does not enable large folios, so the EOF folio is always a
>> single page today; nevertheless, use folio-relative arithmetic
>> (offset_in_folio(), folio_contains(), folio_size()) throughout so the
>> code is correct if that ever changes.
>>
>> Signed-off-by: Nan Lin <lin.nan@h3c.com>
>>
>> Changes since v1:
>> - Both zeroing sites used PAGE_SIZE-relative arithmetic
>> (offset_in_page()-style) while folio_zero_range() expects
>> folio-relative offsets. UBIFS does not currently enable large
>> folios, so PAGE_SIZE == folio_size() and the two coincide in
>> practice, but switch to offset_in_folio()/folio_size() anyway so
>> the code is correct if UBIFS ever opts into large folios
>> (suggested by Sashiko AI review).
>>
>> ---
>> fs/ubifs/file.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 73 insertions(+)
>>
>> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
>> index e73c28b12f97..1726714161a1 100644
>> --- a/fs/ubifs/file.c
>> +++ b/fs/ubifs/file.c
>> @@ -202,6 +202,53 @@ static void release_existing_page_budget(struct
>> ubifs_info *c)
>> ubifs_release_budget(c, &req);
>> }
>> +/**
>> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
>> + * @inode: inode whose size is about to grow
>> + * @new_end: new end of the file, only used to bound the tail
>> + *
>> + * Zero the cached folio containing the old EOF for [old EOF,
>> + * min(folio end, @new_end)) so a size extension does not expose stale
>> + * data left beyond EOF by mmap writes.
>> + *
>> + * Must be called without ui->ui_mutex held: we block on the old-EOF
>> + * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
>> + * lock while taking ui_mutex; the reverse order deadlocks.
>> + */
>> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
>> +{
>> + loff_t old_size = i_size_read(inode);
>> + size_t from, to;
>> + struct folio *folio;
>> +
>> + if (new_end <= old_size)
>> + return;
>> +
>> + folio = filemap_lock_folio(inode->i_mapping, old_size >>
>> PAGE_SHIFT);
>> + if (IS_ERR(folio))
>> + return;
> The filemap_lock_folio gets folio with FGP_LOCK, I think we should use
> 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh() does,
> because the folio may not exist in ram when calling do_setattr().
>> +
>> + /*
>> + * Re-read i_size under the folio lock: a concurrent truncate may
>> + * have shrunk the file, turning this folio into valid data.
>> + */
>> + old_size = i_size_read(inode);
>> + if (new_end <= old_size)
>> + goto out;
>> +
>> + from = offset_in_folio(folio, old_size);
>> + if (!from)
>> + goto out;
> Why skipping 'from == 0'? What if a user writes begin from
> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
> **block_start**, **from**), but ubifs not handle it.
>> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
>> + offset_in_folio(folio, new_end) : folio_size(folio);
>> + if (to > from)
>> + folio_zero_range(folio, from, to - from);
>> +
>> +out:
>> + folio_unlock(folio);
>> + folio_put(folio);
>> +}
>> +
>> static int write_begin_slow(struct address_space *mapping,
>> loff_t pos, unsigned len, struct folio **foliop)
>> {
>> @@ -423,6 +470,14 @@ static int ubifs_write_begin(const struct kiocb
>> *iocb,
>> if (unlikely(c->ro_error))
>> return -EROFS;
>> + /*
>> + * The write starts beyond EOF: zero the stale tail of the old-EOF
>> + * folio before budgeting takes ui->ui_mutex. do_readpage() only
>> + * sanitizes the write folio, not the old-EOF folio.
>> + */
>> + if (pos > inode->i_size)
>> + ubifs_zero_eof_hole(inode, pos);
>> +
>
> The ext4 protection check is ext4_write_checks->ext4_block_zero_eof, I
> think we can add the zero hole check in ubifs_write_iter().
>> /* Try out the fast-path part first */
>> folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
>> mapping_gfp_mask(mapping));
>> @@ -1220,6 +1275,10 @@ static int do_setattr(struct ubifs_info *c,
>> struct inode *inode,
>> if (attr->ia_valid & ATTR_SIZE) {
>> dbg_gen("size %lld -> %lld", inode->i_size, new_size);
>> +
>> + /* Must run before the size change loses the old boundary. */
>> + ubifs_zero_eof_hole(inode, new_size);
>> +
>> truncate_setsize(inode, new_size);
>> }
>> @@ -1494,6 +1553,7 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct
>> vm_fault *vmf)
>> struct timespec64 now = current_time(inode);
>> struct ubifs_budget_req req = { .new_page = 1 };
>> int err, update_time;
>> + loff_t i_size;
>> dbg_gen("ino %llu, pg %lu, i_size %lld", inode->i_ino,
>> folio->index,
>> i_size_read(inode));
>> @@ -1553,6 +1613,19 @@ static vm_fault_t ubifs_vm_page_mkwrite(struct
>> vm_fault *vmf)
>> filemap_dirty_folio(folio->mapping, folio);
>> }
>> + /*
>> + * Zero the part of the EOF folio beyond i_size so mmap writes
>> + * cannot leave a stale tail behind (see ubifs_zero_eof_hole()).
>> + * For a folio-aligned i_size the mkwrite entry check has already
>> + * returned SIGBUS, so @from is always non-zero here.
>> + */
>> + i_size = i_size_read(inode);
>> + if (folio_contains(folio, i_size >> PAGE_SHIFT)) {
>> + size_t from = offset_in_folio(folio, i_size);
>> +
> If the program enters into this branch, the ubifs file's isize is not
> zero, ubifs_read_folio has already zerod tail, do we still need this
> folio_zero_range?
Oh, there is one case, UBIFS file has isize without data nodes(eg. isize
= 3, the first block is a data hole), ubifs_read_folio won't zero the
page tail, we still need folio_zero_range().
>> + folio_zero_range(folio, from, folio_size(folio) - from);
>> + }
>> +
>> if (update_time) {
>> int release;
>> struct ubifs_inode *ui = ubifs_inode(inode);
>>
>
>
> ______________________________________________________
> Linux MTD discussion mailing list
> http://lists.infradead.org/mailman/listinfo/linux-mtd/
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-19 12:45 ` Zhihao Cheng
@ 2026-10-22 3:18 ` Nan Lin
2026-10-22 7:14 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-22 3:18 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-bot, sashiko-reviews,
wangxiaoqing, zhou.kete
Hi Cheng,
Thanks for the detailed review. My responses are inline.
> The filemap_lock_folio gets folio with FGP_LOCK, I think we should use
> 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh() does,
> because the folio may not exist in ram when calling do_setattr().
ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind an
unwritten/delayed extent holds garbage on disk, so the zeroing must
reach the media. UBIFS does not have this problem -- stale data can
only live in the page cache, never on flash:
writeback: ubifs_writepage() zeroes the straddling folio on every
invocation and skips folios fully beyond i_size, so
stale tails never reach the media.
read-in: do_readpage()/read_block() zero the tails of holes and
short data nodes, so a folio loaded from flash is always
tail-clean.
So when the EOF folio is absent from the cache, there is nothing to
zero anywhere: FGP_CREAT would merely allocate a fresh (already zero)
folio to zero it again. Returning early on IS_ERR() is the correct and
cheaper equivalent of Ext4's path.
> Why skipping 'from == 0'? What if a user writes begin from
> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
> block_start, from), but ubifs not handle it.
from == 0 means the old EOF is folio-aligned, so the EOF folio has no
bytes beyond EOF and there is no hole in it:
from != 0: |/////valid/////|stale| <- zero [from, to)
from == 0: |/////valid/////| <- nothing beyond EOF here
For the i_size == 0 case: a zero-size file cannot hold a polluted
folio. mmap cannot create one (page_mkwrite returns SIGBUS when
folio_pos >= i_size), and write() followed by ftruncate(0) invalidates
the folio. The [0, X) part of the write folio itself is zeroed by the
existing do_readpage() path ("block >= beyond" ->
folio_zero_range()), which is UBIFS's counterpart of
ext4_block_write_begin()'s folio_zero_segments() -- the work happens
at folio read-in because UBIFS's block size always equals the page
size.
> The ext4 protection check is ext4_write_checks->ext4_block_zero_eof,
> I think we can add the zero hole check in ubifs_write_iter().
Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof() and
runs once per syscall instead of once per write_begin() iteration.
Will move it there in v3.
> If the program enters into this branch, the ubifs file's isize is not
> zero, ubifs_read_folio has already zerod tail, do we still need this
> folio_zero_range?
> Oh, there is one case, UBIFS file has isize without data nodes(eg.
> isize = 3, the first block is a data hole), ubifs_read_folio won't
> zero the page tail, we still need folio_zero_range().
Right -- and one more case on top of the hole-inode one: mkwrite also
runs on already-cached folios, and earlier mmap stores may have
re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
fully beyond EOF get SIGBUS; the straddling folio stays writable
across its whole length). So the tail must be re-zeroed on every
(re-)fault, and the folio_zero_range() stays.
For reproducibility I have attached a small single-writer reproducer.
It covers both size-extending paths (S1: pwrite beyond EOF ->
write_begin/write_end, S2: ftruncate up -> do_setattr) against a
polluted old-EOF folio.
Build: gcc -O2 -Wall -o eof_hole_test eof_hole_test.c
Run: ./eof_hole_test <mountpoint>/eof_hole.bin
-----------------[ eof_hole_test.c ]-----------------
/*
* eof_hole_repro.c -- repro for "stale data in hole" bug
*
* Reproduce:
* 1. pwrite data to [0, 0x2000) - full page 0 and 1
* 2. ftruncate DOWN to 0x1100 - VFS zeroes page-1 tail
* 3. mmap page 1; write junk into [0x1100, 0x1200) via the mapping
* (MAP_SHARED, past EOF) - pollutes the cache tail,
* i_size unchanged, legal
* 4. pwrite at 0x3000 (beyond EOF) - i_size jumps; UBIFS's
* ubifs_write_end() does NOT
* zero [0x1100, 0x3000)
* 5. pread [0x1100, 0x3000): must be all zero.
* Non-zero bytes = stale mmap junk exposed = bug reproduced.
*
* Scenario 2 uses ftruncate UP instead of pwrite to exercise
* do_setattr() instead of write_end().
*
* Build: gcc -O2 -Wall -o eof_hole_test eof_hole_test.c
* Run: ./eof_hole_test /mnt/flash/eof_hole.bin
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#define PS 4096
#define OLD_EOF (PS + 0x100) /* 0x1100: old EOF mid-page-1 */
#define NEW_END (3 * PS) /* 0x3000: hole end */
#define JUNK_LEN 0x100 /* 256 bytes of pollution */
static int check_hole(int fd, off_t off, size_t len, const char *what)
{
unsigned char buf[512];
size_t n, i;
while (len) {
n = len > sizeof(buf) ? sizeof(buf) : len;
if (pread(fd, buf, n, off) != (ssize_t)n) {
printf("FAIL: %s: pread error: %s\n", what,
strerror(errno));
return 1;
}
for (i = 0; i < n; i++) {
if (buf[i] != 0) {
printf("FAIL: %s: byte 0x%02x at offset 0x%llx "
"(hole offset +%zu)\n",
what, buf[i],
(unsigned long long)(off + i), i);
return 1;
}
}
off += n;
len -= n;
}
printf("PASS: %s\n", what);
return 0;
}
/* Create a file with EOF mid-page-1, then pollute the page-cache tail
* beyond EOF via an mmap write (the only path that skips zeroing). */
static int make_file(const char *path)
{
unsigned char junk[PS];
void *map;
int fd;
memset(junk, 0x5a, sizeof(junk));
fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return -1;
}
/* page 1 full of junk, durable */
if (pwrite(fd, junk, PS, PS) != PS) {
perror("pwrite");
goto bad;
}
fsync(fd);
/* shrink: VFS zeroes page-1 tail [OLD_EOF, 0x2000) itself */
if (ftruncate(fd, OLD_EOF)) {
perror("ftruncate");
goto bad;
}
/* re-pollute [OLD_EOF, OLD_EOF+JUNK_LEN) through a shared
* mapping. This goes through ->page_mkwrite() which does no
* zeroing. */
map = mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_SHARED, fd, PS);
if (map == MAP_FAILED) {
perror("mmap");
goto bad;
}
memcpy((char *)map + 0x100, junk, JUNK_LEN);
munmap(map, PS);
return fd;
bad:
close(fd);
return -1;
}
int main(int argc, char **argv)
{
const char *path = argc > 1 ? argv[1] : "./eof_hole.bin";
unsigned char junk[JUNK_LEN];
int fd, fail = 0;
memset(junk, 0x5a, sizeof(junk));
/* ---------- S1: buffered write beyond EOF ----------
* exercises ubifs_write_end() appending branch */
fd = make_file(path);
if (fd < 0)
return 1;
if (pwrite(fd, junk, JUNK_LEN, NEW_END) != JUNK_LEN) {
perror("pwrite");
return 1;
}
fsync(fd);
fail |= check_hole(fd, OLD_EOF, NEW_END - OLD_EOF,
"S1 write_end: hole [0x1100,0x3000)");
close(fd);
/* ---------- S2: ftruncate UP ----------
* exercises do_setattr() ATTR_SIZE branch */
fd = make_file(path);
if (fd < 0)
return 1;
if (ftruncate(fd, NEW_END)) {
perror("ftruncate");
return 1;
}
fail |= check_hole(fd, OLD_EOF, NEW_END - OLD_EOF,
"S2 do_setattr: hole [0x1100,0x3000)");
close(fd);
unlink(path);
if (fail) {
printf("\n===> BUG REPRODUCED: stale data visible in hole\n");
return 1;
}
printf("\n===> OK: hole reads as zeros on both paths\n");
return 0;
}
------------------------------------------------------
--
Nan Lin
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-22 3:18 ` Nan Lin
@ 2026-10-22 7:14 ` Zhihao Cheng
2026-10-22 13:15 ` Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-22 7:14 UTC (permalink / raw)
To: Nan Lin
Cc: linux-mtd, richard, sashiko-bot, sashiko-reviews, wangxiaoqing,
zhou.kete
在 2026/10/22 11:18, Nan Lin 写道:
> Hi Cheng,
>
> Thanks for the detailed review. My responses are inline.
>
>> The filemap_lock_folio gets folio with FGP_LOCK, I think we should use
>> 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh() does,
>> because the folio may not exist in ram when calling do_setattr().
>
> ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind an
> unwritten/delayed extent holds garbage on disk, so the zeroing must
> reach the media. UBIFS does not have this problem -- stale data can
> only live in the page cache, never on flash:
>
> writeback: ubifs_writepage() zeroes the straddling folio on every
> invocation and skips folios fully beyond i_size, so
> stale tails never reach the media.
> read-in: do_readpage()/read_block() zero the tails of holes and
> short data nodes, so a folio loaded from flash is always
> tail-clean.
>
> So when the EOF folio is absent from the cache, there is nothing to
> zero anywhere: FGP_CREAT would merely allocate a fresh (already zero)
> folio to zero it again. Returning early on IS_ERR() is the correct and
> cheaper equivalent of Ext4's path.
There is one case makes me confused:
File f, isize = 0
1. setattr(f, isize = 3)
do_setattr -> ubifs_zero_eof_hole -> filemap_lock_folio // gets no
folio and returns, nothing happens
2. pwrite(f, "123", offset = 6, len = 3)
ubifs_write_begin
ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and
returns, nothing happens
folio = __filemap_get_folio(FGP_WRITEBEGIN) // create a new folio
with random content
if (!folio_test_uptodate(folio))
do_readpage // does nothing
while (1) {
ENOENT = read_block
}
ubifs_write_end
isize = 9
ubifs_writepage
folio content looks like
| random | random | 123 | zero |
0 3 6 9 4096
According to the current implementations, could above process happen?
If it is, I think we need to do two points:
1. The folio(even not cached) zero range is necessary when setting a new
bigger size? -- For 0~3 case
2. The folio(even not cached) zero range is necessary when writting
starts after old isize? -- For 3~6 case
I think zeroing the whole folio in ubifs_read_folio(ENOENT case) is a
simple solution?
>
>> Why skipping 'from == 0'? What if a user writes begin from
>> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
>> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
>> block_start, from), but ubifs not handle it.
>
> from == 0 means the old EOF is folio-aligned, so the EOF folio has no
> bytes beyond EOF and there is no hole in it:
>
> from != 0: |/////valid/////|stale| <- zero [from, to)
> from == 0: |/////valid/////| <- nothing beyond EOF here
>
> For the i_size == 0 case: a zero-size file cannot hold a polluted
> folio. mmap cannot create one (page_mkwrite returns SIGBUS when
> folio_pos >= i_size), and write() followed by ftruncate(0) invalidates
> the folio. The [0, X) part of the write folio itself is zeroed by the
> existing do_readpage() path ("block >= beyond" ->
> folio_zero_range()), which is UBIFS's counterpart of
> ext4_block_write_begin()'s folio_zero_segments() -- the work happens
> at folio read-in because UBIFS's block size always equals the page
> size.
>
This question is similar with the previous one.
>> The ext4 protection check is ext4_write_checks->ext4_block_zero_eof,
>> I think we can add the zero hole check in ubifs_write_iter().
>
> Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof() and
> runs once per syscall instead of once per write_begin() iteration.
> Will move it there in v3.
>
>> If the program enters into this branch, the ubifs file's isize is not
>> zero, ubifs_read_folio has already zerod tail, do we still need this
>> folio_zero_range?
>> Oh, there is one case, UBIFS file has isize without data nodes(eg.
>> isize = 3, the first block is a data hole), ubifs_read_folio won't
>> zero the page tail, we still need folio_zero_range().
>
> Right -- and one more case on top of the hole-inode one: mkwrite also
> runs on already-cached folios, and earlier mmap stores may have
> re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
> fully beyond EOF get SIGBUS; the straddling folio stays writable
> across its whole length). So the tail must be re-zeroed on every
> (re-)fault, and the folio_zero_range() stays.
I;m still confusing about the necessary of this folio_zero_range().
For a hole inode(isize = 3, no data nodes), if the folio is cached,
ubifs_read_folio has zerod the whole folio content(Suspend we have
applied the simple solution from the previous question). The subsequent
dirty setting will not write the page content after the 3rd byte to a
data node.
>
> For reproducibility I have attached a small single-writer reproducer.
> It covers both size-extending paths (S1: pwrite beyond EOF ->
> write_begin/write_end, S2: ftruncate up -> do_setattr) against a
> polluted old-EOF folio.
Thanks for providing that.
>
> Build: gcc -O2 -Wall -o eof_hole_test eof_hole_test.c
> Run: ./eof_hole_test <mountpoint>/eof_hole.bin
>
> -----------------[ eof_hole_test.c ]-----------------
> /*
> * eof_hole_repro.c -- repro for "stale data in hole" bug
> *
> * Reproduce:
> * 1. pwrite data to [0, 0x2000) - full page 0 and 1
> * 2. ftruncate DOWN to 0x1100 - VFS zeroes page-1 tail
> * 3. mmap page 1; write junk into [0x1100, 0x1200) via the mapping
> * (MAP_SHARED, past EOF) - pollutes the cache tail,
> * i_size unchanged, legal
> * 4. pwrite at 0x3000 (beyond EOF) - i_size jumps; UBIFS's
> * ubifs_write_end() does NOT
> * zero [0x1100, 0x3000)
> * 5. pread [0x1100, 0x3000): must be all zero.
> * Non-zero bytes = stale mmap junk exposed = bug reproduced.
> *
> * Scenario 2 uses ftruncate UP instead of pwrite to exercise
> * do_setattr() instead of write_end().
> *
> * Build: gcc -O2 -Wall -o eof_hole_test eof_hole_test.c
> * Run: ./eof_hole_test /mnt/flash/eof_hole.bin
> */
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> #include <fcntl.h>
> #include <errno.h>
> #include <sys/mman.h>
>
> #define PS 4096
> #define OLD_EOF (PS + 0x100) /* 0x1100: old EOF mid-page-1 */
> #define NEW_END (3 * PS) /* 0x3000: hole end */
> #define JUNK_LEN 0x100 /* 256 bytes of pollution */
>
> static int check_hole(int fd, off_t off, size_t len, const char *what)
> {
> unsigned char buf[512];
> size_t n, i;
>
> while (len) {
> n = len > sizeof(buf) ? sizeof(buf) : len;
> if (pread(fd, buf, n, off) != (ssize_t)n) {
> printf("FAIL: %s: pread error: %s\n", what,
> strerror(errno));
> return 1;
> }
> for (i = 0; i < n; i++) {
> if (buf[i] != 0) {
> printf("FAIL: %s: byte 0x%02x at offset 0x%llx "
> "(hole offset +%zu)\n",
> what, buf[i],
> (unsigned long long)(off + i), i);
> return 1;
> }
> }
> off += n;
> len -= n;
> }
> printf("PASS: %s\n", what);
> return 0;
> }
>
> /* Create a file with EOF mid-page-1, then pollute the page-cache tail
> * beyond EOF via an mmap write (the only path that skips zeroing). */
> static int make_file(const char *path)
> {
> unsigned char junk[PS];
> void *map;
> int fd;
>
> memset(junk, 0x5a, sizeof(junk));
>
> fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
> if (fd < 0) {
> perror("open");
> return -1;
> }
>
> /* page 1 full of junk, durable */
> if (pwrite(fd, junk, PS, PS) != PS) {
> perror("pwrite");
> goto bad;
> }
> fsync(fd);
>
> /* shrink: VFS zeroes page-1 tail [OLD_EOF, 0x2000) itself */
> if (ftruncate(fd, OLD_EOF)) {
> perror("ftruncate");
> goto bad;
> }
>
> /* re-pollute [OLD_EOF, OLD_EOF+JUNK_LEN) through a shared
> * mapping. This goes through ->page_mkwrite() which does no
> * zeroing. */
> map = mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_SHARED, fd, PS);
> if (map == MAP_FAILED) {
> perror("mmap");
> goto bad;
> }
> memcpy((char *)map + 0x100, junk, JUNK_LEN);
> munmap(map, PS);
> return fd;
>
> bad:
> close(fd);
> return -1;
> }
>
> int main(int argc, char **argv)
> {
> const char *path = argc > 1 ? argv[1] : "./eof_hole.bin";
> unsigned char junk[JUNK_LEN];
> int fd, fail = 0;
>
> memset(junk, 0x5a, sizeof(junk));
>
> /* ---------- S1: buffered write beyond EOF ----------
> * exercises ubifs_write_end() appending branch */
> fd = make_file(path);
> if (fd < 0)
> return 1;
> if (pwrite(fd, junk, JUNK_LEN, NEW_END) != JUNK_LEN) {
> perror("pwrite");
> return 1;
> }
> fsync(fd);
> fail |= check_hole(fd, OLD_EOF, NEW_END - OLD_EOF,
> "S1 write_end: hole [0x1100,0x3000)");
> close(fd);
>
> /* ---------- S2: ftruncate UP ----------
> * exercises do_setattr() ATTR_SIZE branch */
> fd = make_file(path);
> if (fd < 0)
> return 1;
> if (ftruncate(fd, NEW_END)) {
> perror("ftruncate");
> return 1;
> }
> fail |= check_hole(fd, OLD_EOF, NEW_END - OLD_EOF,
> "S2 do_setattr: hole [0x1100,0x3000)");
> close(fd);
>
> unlink(path);
>
> if (fail) {
> printf("\n===> BUG REPRODUCED: stale data visible in hole\n");
> return 1;
> }
> printf("\n===> OK: hole reads as zeros on both paths\n");
> return 0;
> }
> ------------------------------------------------------
>
> --
> Nan Lin
>
> .
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-22 7:14 ` Zhihao Cheng
@ 2026-10-22 13:15 ` Nan Lin
2026-10-22 13:54 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-22 13:15 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-bot, sashiko-reviews,
wangxiaoqing, zhou.kete
>> Hi Cheng,
>>
>> Thanks for the detailed review. My responses are inline.
>>
>>> The filemap_lock_folio gets folio with FGP_LOCK, I think we should
>>> use 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh()
>>> does, because the folio may not exist in ram when calling do_setattr().
>>
>> ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind
>> an unwritten/delayed extent holds garbage on disk, so the zeroing must
>> reach the media. UBIFS does not have this problem -- stale data can
>> only live in the page cache, never on flash:
>>
>> writeback: ubifs_writepage() zeroes the straddling folio on every
>> invocation and skips folios fully beyond i_size, so
>> stale tails never reach the media.
>> read-in: do_readpage()/read_block() zero the tails of holes and
>> short data nodes, so a folio loaded from flash is always
>> tail-clean.
>>
>> So when the EOF folio is absent from the cache, there is nothing to
>> zero anywhere: FGP_CREAT would merely allocate a fresh (already zero)
>> folio to zero it again. Returning early on IS_ERR() is the correct and
>> cheaper equivalent of Ext4's path.
>
>There is one case makes me confused:
>File f, isize = 0
>1. setattr(f, isize = 3)
> do_setattr -> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens 2. pwrite(f, "123", offset = 6, len = 3)
> ubifs_write_begin
> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens
> folio = __filemap_get_folio(FGP_WRITEBEGIN) // create a new folio with random content
> if (!folio_test_uptodate(folio))
> do_readpage // does nothing
> while (1) {
> ENOENT = read_block
> }
> ubifs_write_end
> isize = 9
>
> ubifs_writepage
> folio content looks like
> | random | random | 123 | zero |
> 0 3 6 9 4096
>
do_readpage() does run here: pos(6) != folio_pos(0), so the
skipped_read branch is not taken. With i_size = 3:
do_readpage():
beyond = (3 + 4095) >> 12 = 1
block(0) < beyond(1) -> no early exit
read_block(block=0):
ubifs_tnc_lookup() -> -ENOENT (no data nodes yet)
-> folio_zero_range(folio, 0, 4096) /* whole 4K block */
-> return -ENOENT /* file.c:52-59 */
hole branch: err = 0, folio_set_checked(), folio_mark_uptodate()
So read_block() zeroes the full 4K block *before* returning -ENOENT. The folio VFS copies "123" into is:
| 0 ... 0 | 123 | 0 ... 0 |
0 3/6 9 4096
not "| random | random | 123 |". The random bytes never survive:
every !uptodate folio passes through do_readpage(), and read_block()
zeroes every block whose TNC lookup returns -ENOENT.
>According to the current implementations, could above process happen?
>If it is, I think we need to do two points:
>1. The folio(even not cached) zero range is necessary when setting a new
>bigger size? -- For 0~3 case
>2. The folio(even not cached) zero range is necessary when writting
>starts after old isize? -- For 3~6 case
>I think zeroing the whole folio in ubifs_read_folio(ENOENT case) is a simple solution?
>>
>>> Why skipping 'from == 0'? What if a user writes begin from
>>> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
>>> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
>>> block_start, from), but ubifs not handle it.
>>
>> from == 0 means the old EOF is folio-aligned, so the EOF folio has no
>> bytes beyond EOF and there is no hole in it:
>>
>> from != 0: |/////valid/////|stale| <- zero [from, to)
>> from == 0: |/////valid/////| <- nothing beyond EOF here
>>
>> For the i_size == 0 case: a zero-size file cannot hold a polluted
>> folio. mmap cannot create one (page_mkwrite returns SIGBUS when
>> folio_pos >= i_size), and write() followed by ftruncate(0) invalidates
>> the folio. The [0, X) part of the write folio itself is zeroed by the
>> existing do_readpage() path ("block >= beyond" -> folio_zero_range()),
>> which is UBIFS's counterpart of ext4_block_write_begin()'s
>> folio_zero_segments() -- the work happens at folio read-in because
>> UBIFS's block size always equals the page size.
>>
>
>This question is similar with the previous one.
>
>>> The ext4 protection check is ext4_write_checks->ext4_block_zero_eof,
>>> I think we can add the zero hole check in ubifs_write_iter().
>>
>> Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof() and
>> runs once per syscall instead of once per write_begin() iteration.
>> Will move it there in v3.
>>
>>> If the program enters into this branch, the ubifs file's isize is not
>>> zero, ubifs_read_folio has already zerod tail, do we still need this
>>> folio_zero_range?
>>> Oh, there is one case, UBIFS file has isize without data nodes(eg.
>>> isize = 3, the first block is a data hole), ubifs_read_folio won't
>>> zero the page tail, we still need folio_zero_range().
>>
>> Right -- and one more case on top of the hole-inode one: mkwrite also
>> runs on already-cached folios, and earlier mmap stores may have
>> re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
>> fully beyond EOF get SIGBUS; the straddling folio stays writable
>> across its whole length). So the tail must be re-zeroed on every
>> (re-)fault, and the folio_zero_range() stays.
>
>I;m still confusing about the necessary of this folio_zero_range().
>For a hole inode(isize = 3, no data nodes), if the folio is cached, ubifs_read_folio has zerod the whole folio content(Suspend we have applied the simple solution from the previous question). The subsequent dirty setting will not write the page content after the 3rd byte to a data node.
Yes, I agree: ubifs_read_folio() zeroes the tail at read-in, and
writeback does not persist bytes beyond EOF (the straddling folio's
tail is zeroed and do_writepage() only writes i_size
bytes). My concern is the cached tail of the EOF folio: a previous
shared mapping may have modified it, and a later write fault may
reuse the uptodate folio without calling ubifs_read_folio() again.
The zeroing will clear that stale tail before page_mkwrite() returns.
This is not a general fix for read-only re-mappings, since those do
not invoke page_mkwrite(). In this scenario, I see the hunk as a
defensive cleanup of the cached EOF tail on a later write fault,
rather than a fix for writeback correctness. A userspace reproducer
may race with writeback. I'd lean toward keeping it as a defensive
measure, but I haven't yet demonstrated a correctness issue without
it. What do you think?
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-22 13:15 ` Nan Lin
@ 2026-10-22 13:54 ` Zhihao Cheng
2026-10-23 1:23 ` Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-22 13:54 UTC (permalink / raw)
To: Nan Lin
Cc: linux-mtd, richard, sashiko-bot, sashiko-reviews, wangxiaoqing,
zhou.kete
在 2026/10/22 21:15, Nan Lin 写道:
>>> Hi Cheng,
>>>
>>> Thanks for the detailed review. My responses are inline.
>>>
>>>> The filemap_lock_folio gets folio with FGP_LOCK, I think we should
>>>> use 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh()
>>>> does, because the folio may not exist in ram when calling do_setattr().
>>>
>>> ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind
>>> an unwritten/delayed extent holds garbage on disk, so the zeroing must
>>> reach the media. UBIFS does not have this problem -- stale data can
>>> only live in the page cache, never on flash:
>>>
>>> writeback: ubifs_writepage() zeroes the straddling folio on every
>>> invocation and skips folios fully beyond i_size, so
>>> stale tails never reach the media.
>>> read-in: do_readpage()/read_block() zero the tails of holes and
>>> short data nodes, so a folio loaded from flash is always
>>> tail-clean.
>>>
>>> So when the EOF folio is absent from the cache, there is nothing to
>>> zero anywhere: FGP_CREAT would merely allocate a fresh (already zero)
>>> folio to zero it again. Returning early on IS_ERR() is the correct and
>>> cheaper equivalent of Ext4's path.
>>
>> There is one case makes me confused:
>> File f, isize = 0
>> 1. setattr(f, isize = 3)
>> do_setattr -> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens 2. pwrite(f, "123", offset = 6, len = 3)
>> ubifs_write_begin
>> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens
>> folio = __filemap_get_folio(FGP_WRITEBEGIN) // create a new folio with random content
>> if (!folio_test_uptodate(folio))
>> do_readpage // does nothing
>> while (1) {
>> ENOENT = read_block
>> }
>> ubifs_write_end
>> isize = 9
>>
>> ubifs_writepage
>> folio content looks like
>> | random | random | 123 | zero |
>> 0 3 6 9 4096
>>
>
> do_readpage() does run here: pos(6) != folio_pos(0), so the
> skipped_read branch is not taken. With i_size = 3:
>
> do_readpage():
> beyond = (3 + 4095) >> 12 = 1
> block(0) < beyond(1) -> no early exit
> read_block(block=0):
> ubifs_tnc_lookup() -> -ENOENT (no data nodes yet)
Oh, I missed the folio_zero_range in read_block(), sorry for the noisy.
> -> folio_zero_range(folio, 0, 4096) /* whole 4K block */
> -> return -ENOENT /* file.c:52-59 */
> hole branch: err = 0, folio_set_checked(), folio_mark_uptodate()
>
> So read_block() zeroes the full 4K block *before* returning -ENOENT. The folio VFS copies "123" into is:
>
> | 0 ... 0 | 123 | 0 ... 0 |
> 0 3/6 9 4096
>
> not "| random | random | 123 |". The random bytes never survive:
> every !uptodate folio passes through do_readpage(), and read_block()
> zeroes every block whose TNC lookup returns -ENOENT.
>
>> According to the current implementations, could above process happen?
>> If it is, I think we need to do two points:
>> 1. The folio(even not cached) zero range is necessary when setting a new
>> bigger size? -- For 0~3 case
>> 2. The folio(even not cached) zero range is necessary when writting
>> starts after old isize? -- For 3~6 case
>> I think zeroing the whole folio in ubifs_read_folio(ENOENT case) is a simple solution?
>>>
>>>> Why skipping 'from == 0'? What if a user writes begin from
>>>> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
>>>> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
>>>> block_start, from), but ubifs not handle it.
>>>
>>> from == 0 means the old EOF is folio-aligned, so the EOF folio has no
>>> bytes beyond EOF and there is no hole in it:
>>>
>>> from != 0: |/////valid/////|stale| <- zero [from, to)
>>> from == 0: |/////valid/////| <- nothing beyond EOF here
>>>
>>> For the i_size == 0 case: a zero-size file cannot hold a polluted
>>> folio. mmap cannot create one (page_mkwrite returns SIGBUS when
>>> folio_pos >= i_size), and write() followed by ftruncate(0) invalidates
>>> the folio. The [0, X) part of the write folio itself is zeroed by the
>>> existing do_readpage() path ("block >= beyond" -> folio_zero_range()),
>>> which is UBIFS's counterpart of ext4_block_write_begin()'s
>>> folio_zero_segments() -- the work happens at folio read-in because
>>> UBIFS's block size always equals the page size.
>>>
>>
>> This question is similar with the previous one.
>>
>>>> The ext4 protection check is ext4_write_checks->ext4_block_zero_eof,
>>>> I think we can add the zero hole check in ubifs_write_iter().
>>>
>>> Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof() and
>>> runs once per syscall instead of once per write_begin() iteration.
>>> Will move it there in v3.
>>>
>>>> If the program enters into this branch, the ubifs file's isize is not
>>>> zero, ubifs_read_folio has already zerod tail, do we still need this
>>>> folio_zero_range?
>>>> Oh, there is one case, UBIFS file has isize without data nodes(eg.
>>>> isize = 3, the first block is a data hole), ubifs_read_folio won't
>>>> zero the page tail, we still need folio_zero_range().
>>>
>>> Right -- and one more case on top of the hole-inode one: mkwrite also
>>> runs on already-cached folios, and earlier mmap stores may have
>>> re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
>>> fully beyond EOF get SIGBUS; the straddling folio stays writable
>>> across its whole length). So the tail must be re-zeroed on every
>>> (re-)fault, and the folio_zero_range() stays.
>>
>> I;m still confusing about the necessary of this folio_zero_range().
>> For a hole inode(isize = 3, no data nodes), if the folio is cached, ubifs_read_folio has zerod the whole folio content(Suspend we have applied the simple solution from the previous question). The subsequent dirty setting will not write the page content after the 3rd byte to a data node.
>
> Yes, I agree: ubifs_read_folio() zeroes the tail at read-in, and
> writeback does not persist bytes beyond EOF (the straddling folio's
> tail is zeroed and do_writepage() only writes i_size
> bytes). My concern is the cached tail of the EOF folio: a previous
> shared mapping may have modified it, and a later write fault may
> reuse the uptodate folio without calling ubifs_read_folio() again.
> The zeroing will clear that stale tail before page_mkwrite() returns.
>
Agreed with the analysis of the multi writing processes. Anyway, user
must be aware that he is modifying a file beyonds EOF by mmap(). The
user should be responsible for the memory content beyonds EOF.
> This is not a general fix for read-only re-mappings, since those do
> not invoke page_mkwrite(). In this scenario, I see the hunk as a
> defensive cleanup of the cached EOF tail on a later write fault,
> rather than a fix for writeback correctness. A userspace reproducer
> may race with writeback. I'd lean toward keeping it as a defensive
> measure, but I haven't yet demonstrated a correctness issue without
> it. What do you think?
>
Well, this is not a practical issue and will not have any impact on the
user, and other filesystems(eg. ext4) behave like UBIFS for this case,
so I prefer to keep the original implementation.
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-22 13:54 ` Zhihao Cheng
@ 2026-10-23 1:23 ` Nan Lin
2026-10-23 1:38 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-23 1:23 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-bot, sashiko-reviews,
wangxiaoqing, zhou.kete
>>>> Hi Cheng,
>>>>
>>>> Thanks for the detailed review. My responses are inline.
>>>>
>>>>> The filemap_lock_folio gets folio with FGP_LOCK, I think we should
>>>>> use 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh()
>>>>> does, because the folio may not exist in ram when calling do_setattr().
>>>>
>>>> ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind
>>>> an unwritten/delayed extent holds garbage on disk, so the zeroing
>>>> must reach the media. UBIFS does not have this problem -- stale data
>>>> can only live in the page cache, never on flash:
>>>>
>>>> writeback: ubifs_writepage() zeroes the straddling folio on every
>>>> invocation and skips folios fully beyond i_size, so
>>>> stale tails never reach the media.
>>>> read-in: do_readpage()/read_block() zero the tails of holes and
>>>> short data nodes, so a folio loaded from flash is always
>>>> tail-clean.
>>>>
>>>> So when the EOF folio is absent from the cache, there is nothing to
>>>> zero anywhere: FGP_CREAT would merely allocate a fresh (already
>>>> zero) folio to zero it again. Returning early on IS_ERR() is the
>>>> correct and cheaper equivalent of Ext4's path.
>>>
>>> There is one case makes me confused:
>>> File f, isize = 0
>>> 1. setattr(f, isize = 3)
>>> do_setattr -> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens 2. pwrite(f, "123", offset = 6, len = 3)
>>> ubifs_write_begin
>>> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens
>>> folio = __filemap_get_folio(FGP_WRITEBEGIN) // create a new folio with random content
>>> if (!folio_test_uptodate(folio))
>>> do_readpage // does nothing
>>> while (1) {
>>> ENOENT = read_block
>>> }
>>> ubifs_write_end
>>> isize = 9
>>>
>>> ubifs_writepage
>>> folio content looks like
>>> | random | random | 123 | zero |
>>> 0 3 6 9 4096
>>>
>>
>> do_readpage() does run here: pos(6) != folio_pos(0), so the
>> skipped_read branch is not taken. With i_size = 3:
>>
>> do_readpage():
>> beyond = (3 + 4095) >> 12 = 1
>> block(0) < beyond(1) -> no early exit
>> read_block(block=0):
>> ubifs_tnc_lookup() -> -ENOENT (no data nodes yet)
>Oh, I missed the folio_zero_range in read_block(), sorry for the noisy.
>> -> folio_zero_range(folio, 0, 4096) /* whole 4K block */
>> -> return -ENOENT /* file.c:52-59 */
>> hole branch: err = 0, folio_set_checked(), folio_mark_uptodate()
>>
>> So read_block() zeroes the full 4K block *before* returning -ENOENT. The folio VFS copies "123" into is:
>>
>> | 0 ... 0 | 123 | 0 ... 0 |
>> 0 3/6 9 4096
>>
>> not "| random | random | 123 |". The random bytes never survive:
>> every !uptodate folio passes through do_readpage(), and read_block()
>> zeroes every block whose TNC lookup returns -ENOENT.
>>
>>> According to the current implementations, could above process happen?
>>> If it is, I think we need to do two points:
>>> 1. The folio(even not cached) zero range is necessary when setting a new
>>> bigger size? -- For 0~3 case
>>> 2. The folio(even not cached) zero range is necessary when writting
>>> starts after old isize? -- For 3~6 case
>>> I think zeroing the whole folio in ubifs_read_folio(ENOENT case) is a simple solution?
>>>>
>>>>> Why skipping 'from == 0'? What if a user writes begin from
>>>>> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
>>>>> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
>>>>> block_start, from), but ubifs not handle it.
>>>>
>>>> from == 0 means the old EOF is folio-aligned, so the EOF folio has
>>>> no bytes beyond EOF and there is no hole in it:
>>>>
>>>> from != 0: |/////valid/////|stale| <- zero [from, to)
>>>> from == 0: |/////valid/////| <- nothing beyond EOF here
>>>>
>>>> For the i_size == 0 case: a zero-size file cannot hold a polluted
>>>> folio. mmap cannot create one (page_mkwrite returns SIGBUS when
>>>> folio_pos >= i_size), and write() followed by ftruncate(0)
>>>> invalidates the folio. The [0, X) part of the write folio itself is
>>>> zeroed by the existing do_readpage() path ("block >= beyond" ->
>>>> folio_zero_range()), which is UBIFS's counterpart of
>>>> ext4_block_write_begin()'s
>>>> folio_zero_segments() -- the work happens at folio read-in because
>>>> UBIFS's block size always equals the page size.
>>>>
>>>
>>> This question is similar with the previous one.
>>>
>>>>> The ext4 protection check is
>>>>> ext4_write_checks->ext4_block_zero_eof,
>>>>> I think we can add the zero hole check in ubifs_write_iter().
>>>>
>>>> Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof()
>>>> and runs once per syscall instead of once per write_begin() iteration.
>>>> Will move it there in v3.
>>>>
>>>>> If the program enters into this branch, the ubifs file's isize is
>>>>> not zero, ubifs_read_folio has already zerod tail, do we still need
>>>>> this folio_zero_range?
>>>>> Oh, there is one case, UBIFS file has isize without data nodes(eg.
>>>>> isize = 3, the first block is a data hole), ubifs_read_folio won't
>>>>> zero the page tail, we still need folio_zero_range().
>>>>
>>>> Right -- and one more case on top of the hole-inode one: mkwrite
>>>> also runs on already-cached folios, and earlier mmap stores may have
>>>> re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
>>>> fully beyond EOF get SIGBUS; the straddling folio stays writable
>>>> across its whole length). So the tail must be re-zeroed on every
>>>> (re-)fault, and the folio_zero_range() stays.
>>>
>>> I;m still confusing about the necessary of this folio_zero_range().
>>> For a hole inode(isize = 3, no data nodes), if the folio is cached, ubifs_read_folio has zerod the whole folio content(Suspend we have applied the simple solution from the previous question). The subsequent dirty setting will not write the page content after the 3rd byte to a data node.
>>
>> Yes, I agree: ubifs_read_folio() zeroes the tail at read-in, and
>> writeback does not persist bytes beyond EOF (the straddling folio's
>> tail is zeroed and do_writepage() only writes i_size bytes). My
>> concern is the cached tail of the EOF folio: a previous shared mapping
>> may have modified it, and a later write fault may reuse the uptodate
>> folio without calling ubifs_read_folio() again.
>> The zeroing will clear that stale tail before page_mkwrite() returns.
>>
>Agreed with the analysis of the multi writing processes. Anyway, user must be aware that he is modifying a file beyonds EOF by mmap(). The user should be responsible for the memory content beyonds EOF.
>> This is not a general fix for read-only re-mappings, since those do
>> not invoke page_mkwrite(). In this scenario, I see the hunk as a
>> defensive cleanup of the cached EOF tail on a later write fault,
>> rather than a fix for writeback correctness. A userspace reproducer
>> may race with writeback. I'd lean toward keeping it as a defensive
>> measure, but I haven't yet demonstrated a correctness issue without
>> it. What do you think?
>>
>
>Well, this is not a practical issue and will not have any impact on the user, and other filesystems(eg. ext4) behave like UBIFS for this case, so I prefer to keep the original implementation.
Understood. I verified your point against the code:
ext4_page_mkwrite() -> ext4_block_page_mkwrite() ->
ext4_block_write_begin(handle, folio, 0, len) with len =
i_size - folio_pos, and nothing zeroes [i_size, folio_size) on the
write fault path; block_page_mkwrite() behaves the same. I agree
the mmap-past-EOF semantics put the responsibility on the user.
I'll drop the mkwrite hunk in v3 and fix the commit message
accordingly (drop the inaccurate "mirrors block_page_mkwrite()"
claim, refer to the ext4 size-growth zeroing instead), keeping the
write_iter() and do_setattr() zeroing. Will post after testing.
Best regards,
Nan Lin
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v2] ubifs: zero stale data beyond EOF before file size grows
2026-10-23 1:23 ` Nan Lin
@ 2026-10-23 1:38 ` Zhihao Cheng
2026-10-23 7:11 ` [PATCH v3] " Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-23 1:38 UTC (permalink / raw)
To: Nan Lin
Cc: linux-mtd, richard, sashiko-bot, sashiko-reviews, wangxiaoqing,
zhou.kete
在 2026/10/23 9:23, Nan Lin 写道:
>>>>> Hi Cheng,
>>>>>
>>>>> Thanks for the detailed review. My responses are inline.
>>>>>
>>>>>> The filemap_lock_folio gets folio with FGP_LOCK, I think we should
>>>>>> use 'FGP_WRITEBEGIN | FGP_ACCESSED' just like ext4_load_tail_bh()
>>>>>> does, because the folio may not exist in ram when calling do_setattr().
>>>>>
>>>>> ext4_load_tail_bh() needs FGP_CREAT because in Ext4 the block behind
>>>>> an unwritten/delayed extent holds garbage on disk, so the zeroing
>>>>> must reach the media. UBIFS does not have this problem -- stale data
>>>>> can only live in the page cache, never on flash:
>>>>>
>>>>> writeback: ubifs_writepage() zeroes the straddling folio on every
>>>>> invocation and skips folios fully beyond i_size, so
>>>>> stale tails never reach the media.
>>>>> read-in: do_readpage()/read_block() zero the tails of holes and
>>>>> short data nodes, so a folio loaded from flash is always
>>>>> tail-clean.
>>>>>
>>>>> So when the EOF folio is absent from the cache, there is nothing to
>>>>> zero anywhere: FGP_CREAT would merely allocate a fresh (already
>>>>> zero) folio to zero it again. Returning early on IS_ERR() is the
>>>>> correct and cheaper equivalent of Ext4's path.
>>>>
>>>> There is one case makes me confused:
>>>> File f, isize = 0
>>>> 1. setattr(f, isize = 3)
>>>> do_setattr -> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens 2. pwrite(f, "123", offset = 6, len = 3)
>>>> ubifs_write_begin
>>>> ubifs_zero_eof_hole -> filemap_lock_folio // gets no folio and returns, nothing happens
>>>> folio = __filemap_get_folio(FGP_WRITEBEGIN) // create a new folio with random content
>>>> if (!folio_test_uptodate(folio))
>>>> do_readpage // does nothing
>>>> while (1) {
>>>> ENOENT = read_block
>>>> }
>>>> ubifs_write_end
>>>> isize = 9
>>>>
>>>> ubifs_writepage
>>>> folio content looks like
>>>> | random | random | 123 | zero |
>>>> 0 3 6 9 4096
>>>>
>>>
>>> do_readpage() does run here: pos(6) != folio_pos(0), so the
>>> skipped_read branch is not taken. With i_size = 3:
>>>
>>> do_readpage():
>>> beyond = (3 + 4095) >> 12 = 1
>>> block(0) < beyond(1) -> no early exit
>>> read_block(block=0):
>>> ubifs_tnc_lookup() -> -ENOENT (no data nodes yet)
>> Oh, I missed the folio_zero_range in read_block(), sorry for the noisy.
>>> -> folio_zero_range(folio, 0, 4096) /* whole 4K block */
>>> -> return -ENOENT /* file.c:52-59 */
>>> hole branch: err = 0, folio_set_checked(), folio_mark_uptodate()
>>>
>>> So read_block() zeroes the full 4K block *before* returning -ENOENT. The folio VFS copies "123" into is:
>>>
>>> | 0 ... 0 | 123 | 0 ... 0 |
>>> 0 3/6 9 4096
>>>
>>> not "| random | random | 123 |". The random bytes never survive:
>>> every !uptodate folio passes through do_readpage(), and read_block()
>>> zeroes every block whose TNC lookup returns -ENOENT.
>>>
>>>> According to the current implementations, could above process happen?
>>>> If it is, I think we need to do two points:
>>>> 1. The folio(even not cached) zero range is necessary when setting a new
>>>> bigger size? -- For 0~3 case
>>>> 2. The folio(even not cached) zero range is necessary when writting
>>>> starts after old isize? -- For 3~6 case
>>>> I think zeroing the whole folio in ubifs_read_folio(ENOENT case) is a simple solution?
>>>>>
>>>>>> Why skipping 'from == 0'? What if a user writes begin from
>>>>>> offset-X(0<X<4096) to a new file(isize = 0)? Ext4 zero the range by
>>>>>> ext4_block_write_begin->folio_zero_segments(folio, to, block_end,
>>>>>> block_start, from), but ubifs not handle it.
>>>>>
>>>>> from == 0 means the old EOF is folio-aligned, so the EOF folio has
>>>>> no bytes beyond EOF and there is no hole in it:
>>>>>
>>>>> from != 0: |/////valid/////|stale| <- zero [from, to)
>>>>> from == 0: |/////valid/////| <- nothing beyond EOF here
>>>>>
>>>>> For the i_size == 0 case: a zero-size file cannot hold a polluted
>>>>> folio. mmap cannot create one (page_mkwrite returns SIGBUS when
>>>>> folio_pos >= i_size), and write() followed by ftruncate(0)
>>>>> invalidates the folio. The [0, X) part of the write folio itself is
>>>>> zeroed by the existing do_readpage() path ("block >= beyond" ->
>>>>> folio_zero_range()), which is UBIFS's counterpart of
>>>>> ext4_block_write_begin()'s
>>>>> folio_zero_segments() -- the work happens at folio read-in because
>>>>> UBIFS's block size always equals the page size.
>>>>>
>>>>
>>>> This question is similar with the previous one.
>>>>
>>>>>> The ext4 protection check is
>>>>>> ext4_write_checks->ext4_block_zero_eof,
>>>>>> I think we can add the zero hole check in ubifs_write_iter().
>>>>>
>>>>> Agreed. This mirrors ext4_write_checks() -> ext4_block_zero_eof()
>>>>> and runs once per syscall instead of once per write_begin() iteration.
>>>>> Will move it there in v3.
>>>>>
>>>>>> If the program enters into this branch, the ubifs file's isize is
>>>>>> not zero, ubifs_read_folio has already zerod tail, do we still need
>>>>>> this folio_zero_range?
>>>>>> Oh, there is one case, UBIFS file has isize without data nodes(eg.
>>>>>> isize = 3, the first block is a data hole), ubifs_read_folio won't
>>>>>> zero the page tail, we still need folio_zero_range().
>>>>>
>>>>> Right -- and one more case on top of the hole-inode one: mkwrite
>>>>> also runs on already-cached folios, and earlier mmap stores may have
>>>>> re-dirtied the tail after ubifs_read_folio() cleaned it (only folios
>>>>> fully beyond EOF get SIGBUS; the straddling folio stays writable
>>>>> across its whole length). So the tail must be re-zeroed on every
>>>>> (re-)fault, and the folio_zero_range() stays.
>>>>
>>>> I;m still confusing about the necessary of this folio_zero_range().
>>>> For a hole inode(isize = 3, no data nodes), if the folio is cached, ubifs_read_folio has zerod the whole folio content(Suspend we have applied the simple solution from the previous question). The subsequent dirty setting will not write the page content after the 3rd byte to a data node.
>>>
>>> Yes, I agree: ubifs_read_folio() zeroes the tail at read-in, and
>>> writeback does not persist bytes beyond EOF (the straddling folio's
>>> tail is zeroed and do_writepage() only writes i_size bytes). My
>>> concern is the cached tail of the EOF folio: a previous shared mapping
>>> may have modified it, and a later write fault may reuse the uptodate
>>> folio without calling ubifs_read_folio() again.
>>> The zeroing will clear that stale tail before page_mkwrite() returns.
>>>
>> Agreed with the analysis of the multi writing processes. Anyway, user must be aware that he is modifying a file beyonds EOF by mmap(). The user should be responsible for the memory content beyonds EOF.
>>> This is not a general fix for read-only re-mappings, since those do
>>> not invoke page_mkwrite(). In this scenario, I see the hunk as a
>>> defensive cleanup of the cached EOF tail on a later write fault,
>>> rather than a fix for writeback correctness. A userspace reproducer
>>> may race with writeback. I'd lean toward keeping it as a defensive
>>> measure, but I haven't yet demonstrated a correctness issue without
>>> it. What do you think?
>>>
>>
>> Well, this is not a practical issue and will not have any impact on the user, and other filesystems(eg. ext4) behave like UBIFS for this case, so I prefer to keep the original implementation.
>
> Understood. I verified your point against the code:
> ext4_page_mkwrite() -> ext4_block_page_mkwrite() ->
> ext4_block_write_begin(handle, folio, 0, len) with len =
> i_size - folio_pos, and nothing zeroes [i_size, folio_size) on the
> write fault path; block_page_mkwrite() behaves the same. I agree
> the mmap-past-EOF semantics put the responsibility on the user.
>
> I'll drop the mkwrite hunk in v3 and fix the commit message
> accordingly (drop the inaccurate "mirrors block_page_mkwrite()"
> claim, refer to the ext4 size-growth zeroing instead), keeping the
> write_iter() and do_setattr() zeroing. Will post after testing.
>
Sounds fine to me.
> Best regards,
> Nan Lin
>
>
> .
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v3] ubifs: zero stale data beyond EOF before file size grows
2026-10-23 1:38 ` Zhihao Cheng
@ 2026-10-23 7:11 ` Nan Lin
2026-10-23 7:35 ` sashiko-bot
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-23 7:11 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-bot, sashiko-reviews,
wangxiaoqing, zhou.kete
mmap writes can dirty the page cache beyond EOF without changing
i_size. A later extending write or ftruncate turns that region into a
hole, but UBIFS never zeroes the partial page at the old EOF, so reads
expose the stale data.
Zero the tail of the cached old-EOF folio in both size-extending
paths: ubifs_write_iter() for writes starting beyond EOF, and
do_setattr() before truncate_setsize() grows the size. This mirrors
what ext4 does on size extension;
All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
the folio_lock -> ui_mutex order of ->writepage(); the reverse order
deadlocks.
UBIFS does not enable large folios, so the EOF folio is always a
single page today; nevertheless, use folio-relative arithmetic
(offset_in_folio(), folio_contains(), folio_size()) throughout so the
code is correct if that ever changes.
Signed-off-by: Nan Lin <lin.nan@h3c.com>
Changes since v2:
- Move the extending-write check from ubifs_write_begin() to
ubifs_write_iter(), mirroring ext4_write_checks() ->
ext4_block_zero_eof(): the hole zeroing runs once per write()
syscall instead of once per ->write_begin() iteration (suggested
by Zhihao Cheng).
- Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
(ext4_block_write_begin() with len = i_size - folio_pos),
->page_mkwrite() does not zero the tail beyond EOF, and mmap
writes past EOF are the user's responsibility (suggested by
Zhihao Cheng).
Changes since v1:
- Both zeroing sites used PAGE_SIZE-relative arithmetic
(offset_in_page()-style) while folio_zero_range() expects
folio-relative offsets. UBIFS does not currently enable large
folios, so PAGE_SIZE == folio_size() and the two coincide in
practice, but switch to offset_in_folio()/folio_size() anyway so
the code is correct if UBIFS ever opts into large folios
(suggested by Sashiko AI review).
---
fs/ubifs/file.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 62 insertions(+), 1 deletion(-)
diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
index e73c28b12f97..e94fbcae6d3d 100644
--- a/fs/ubifs/file.c
+++ b/fs/ubifs/file.c
@@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
ubifs_release_budget(c, &req);
}
+/**
+ * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
+ * @inode: inode whose size is about to grow
+ * @new_end: new end of the file, only used to bound the tail
+ *
+ * Zero the cached folio containing the old EOF for [old EOF,
+ * min(folio end, @new_end)) so a size extension does not expose stale
+ * data left beyond EOF by mmap writes.
+ *
+ * Must be called without ui->ui_mutex held: we block on the old-EOF
+ * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
+ * lock while taking ui_mutex; the reverse order deadlocks.
+ */
+static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
+{
+ loff_t old_size = i_size_read(inode);
+ size_t from, to;
+ struct folio *folio;
+
+ if (new_end <= old_size)
+ return;
+
+ folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
+ if (IS_ERR(folio))
+ return;
+
+ /*
+ * Re-read i_size under the folio lock: a concurrent truncate may
+ * have shrunk the file, turning this folio into valid data.
+ */
+ old_size = i_size_read(inode);
+ if (new_end <= old_size)
+ goto out;
+
+ from = offset_in_folio(folio, old_size);
+ if (!from)
+ goto out;
+ to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
+ offset_in_folio(folio, new_end) : folio_size(folio);
+ if (to > from)
+ folio_zero_range(folio, from, to - from);
+
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
static int write_begin_slow(struct address_space *mapping,
loff_t pos, unsigned len, struct folio **foliop)
{
@@ -1220,6 +1267,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
if (attr->ia_valid & ATTR_SIZE) {
dbg_gen("size %lld -> %lld", inode->i_size, new_size);
+
+ /* Must run before the size change loses the old boundary. */
+ ubifs_zero_eof_hole(inode, new_size);
+
truncate_setsize(inode, new_size);
}
@@ -1433,7 +1484,17 @@ static int update_mctime(struct inode *inode)
static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
- int err = update_mctime(file_inode(iocb->ki_filp));
+ struct inode *inode = file_inode(iocb->ki_filp);
+ int err;
+
+ /*
+ * The write may extend beyond EOF: zero the stale tail of the
+ * old-EOF folio first.
+ */
+ if (iocb->ki_pos > i_size_read(inode))
+ ubifs_zero_eof_hole(inode, iocb->ki_pos);
+
+ err = update_mctime(inode);
if (err)
return err;
--
2.33.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH v3] ubifs: zero stale data beyond EOF before file size grows
2026-10-23 7:11 ` [PATCH v3] " Nan Lin
@ 2026-10-23 7:35 ` sashiko-bot
2026-10-26 8:05 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-10-23 7:35 UTC (permalink / raw)
To: Nan Lin, chengzhihao1; +Cc: linux-mtd, richard, wangxiaoqing, zhou.kete
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] ubifs: unlocked ubifs_zero_eof_hole() in ubifs_write_iter() corrupts data
--
commit f10b956c6f33e7d0c8aba1ed8f979c3d9e1197bf
Author: Nan Lin <lin.nan@h3c.com>
ubifs: zero stale data beyond EOF before file size grows
This commit adds ubifs_zero_eof_hole() and calls it from do_setattr() and
ubifs_write_iter() to zero stale post-EOF data in the cached old-EOF folio
before extending a file's size.
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97f..e94fbcae6d3d5 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
> @@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Must be called without ui->ui_mutex held: we block on the old-EOF
> + * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
> + * lock while taking ui_mutex; the reverse order deadlocks.
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
> + loff_t old_size = i_size_read(inode);
> + size_t from, to;
> + struct folio *folio;
> +
> + if (new_end <= old_size)
> + return;
> +
> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
> + if (IS_ERR(folio))
> + return;
> +
> + /*
> + * Re-read i_size under the folio lock: a concurrent truncate may
> + * have shrunk the file, turning this folio into valid data.
> + */
> + old_size = i_size_read(inode);
> + if (new_end <= old_size)
> + goto out;
> +
> + from = offset_in_folio(folio, old_size);
> + if (!from)
> + goto out;
> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
> + offset_in_folio(folio, new_end) : folio_size(folio);
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
[ ... ]
> @@ -1433,7 +1484,17 @@ static int update_mctime(struct inode *inode)
>
> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
> {
> - int err = update_mctime(file_inode(iocb->ki_filp));
> + struct inode *inode = file_inode(iocb->ki_filp);
> + int err;
> +
> + /*
> + * The write may extend beyond EOF: zero the stale tail of the
> + * old-EOF folio first.
> + */
> + if (iocb->ki_pos > i_size_read(inode))
> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
> +
> + err = update_mctime(inode);
> if (err)
> return err;
[Severity: Critical]
Unlike do_setattr() (called with inode_lock(inode) held via notify_change())
and ext4_buffered_write_iter() -> ext4_write_checks() ->
ext4_block_zero_eof(), ubifs_write_iter() calls ubifs_zero_eof_hole() before
generic_file_write_iter() acquires inode_lock(inode) and runs
generic_write_checks(iocb, from):
mm/filemap.c:generic_file_write_iter() {
inode_lock(inode);
ret = generic_write_checks(iocb, from);
if (ret > 0)
ret = __generic_file_write_iter(iocb, from);
inode_unlock(inode);
}
Can calling ubifs_zero_eof_hole() without holding inode_lock(inode) and
before generic_write_checks() cause data corruption and other regressions?
1. If ubifs_zero_eof_hole() reads an initial old_size in folio 0 (for
example, old_size = 100) and calls filemap_lock_folio() on index 0, a
concurrent writer or extending ftruncate() holding inode_lock(inode) can
extend inode->i_size into a later folio (for example, old_size becomes 5100
in folio 1, while new_end is 10000 in folio 2) before folio 0 is locked.
At that point, all of folio 0 is valid committed file data.
When ubifs_zero_eof_hole() acquires the lock on folio 0 and re-reads
old_size = i_size_read(inode), new_end <= old_size is still false, and there
is no check that folio_contains(folio, old_size >> PAGE_SHIFT). Computing
from = offset_in_folio(folio, old_size) masks the new old_size into folio 0
and calls folio_zero_range(folio, 1004, 3092) on folio 0, zeroing committed
in-bounds file data while leaving the actual old-EOF folio (folio 1)
unzeroed.
A concurrent shrinking truncate that moves i_size into an earlier folio
before truncate_pagecache() evicts the locked folio will similarly zero the
wrong folio. And if a concurrent extending ftruncate() updates i_size in
truncate_setsize() right after ubifs_zero_eof_hole() re-reads old_size,
userspace mmap writes to the newly in-bounds region can be overwritten by
folio_zero_range().
2. Because inode_lock(inode) is not held and folio_lock is dropped before
update_mctime() (which can sleep in ubifs_budget_space()) and before
generic_file_write_iter() locks the inode, a concurrent truncate or write can
change i_size or an mmap write can re-dirty the post-EOF tail in the window
between ubifs_zero_eof_hole() and __generic_file_write_iter(), leaving stale
data beyond EOF unzeroed.
3. Because generic_write_checks() has not run yet:
- For O_APPEND or RWF_APPEND writes where ki_pos was positioned past EOF,
generic_write_checks_count() later resets iocb->ki_pos to i_size_read(inode)
so no hole is created, but ubifs_zero_eof_hole() has already modified the
page cache.
- For zero-length writes (iov_iter_count(from) == 0) with ki_pos > i_size,
generic_write_checks() returns 0 without extending the file, after
ubifs_zero_eof_hole() has already zeroed the folio tail.
- For RWF_NOWAIT writes (which generic_write_checks_count() rejects with
-EINVAL since FOP_BUFFER_WASYNC is not set) or writes failing
generic_write_check_limits() (-EFBIG), ubifs_zero_eof_hole() can block on
filemap_lock_folio() and mutate the page cache before the write is
rejected.
Should ubifs_write_iter() acquire inode_lock(inode) and run
generic_write_checks() before calling update_mctime(),
ubifs_zero_eof_hole(), and __generic_file_write_iter(), or otherwise ensure
that ubifs_zero_eof_hole() runs under inode_lock(inode) after write checks?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261023071141.3507024-1-lin.nan@h3c.com?part=1
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v3] ubifs: zero stale data beyond EOF before file size grows
2026-10-23 7:35 ` sashiko-bot
@ 2026-10-26 8:05 ` Zhihao Cheng
2026-10-26 12:50 ` [PATCH v4] " Nan Lin
0 siblings, 1 reply; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-26 8:05 UTC (permalink / raw)
To: sashiko-reviews, Nan Lin; +Cc: linux-mtd, richard, wangxiaoqing, zhou.kete
在 2026/10/23 15:35, sashiko-bot@kernel.org 写道:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Critical] ubifs: unlocked ubifs_zero_eof_hole() in ubifs_write_iter() corrupts data
> --
>
> commit f10b956c6f33e7d0c8aba1ed8f979c3d9e1197bf
> Author: Nan Lin <lin.nan@h3c.com>
>
> ubifs: zero stale data beyond EOF before file size grows
>
> This commit adds ubifs_zero_eof_hole() and calls it from do_setattr() and
> ubifs_write_iter() to zero stale post-EOF data in the cached old-EOF folio
> before extending a file's size.
>
>> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
>> index e73c28b12f97f..e94fbcae6d3d5 100644
>> --- a/fs/ubifs/file.c
>> +++ b/fs/ubifs/file.c
>> @@ -202,6 +202,53 @@ static void release_existing_page_budget(struct ubifs_info *c)
>> ubifs_release_budget(c, &req);
>> }
>>
>> +/**
>> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
>> + * @inode: inode whose size is about to grow
>> + * @new_end: new end of the file, only used to bound the tail
>> + *
>> + * Zero the cached folio containing the old EOF for [old EOF,
>> + * min(folio end, @new_end)) so a size extension does not expose stale
>> + * data left beyond EOF by mmap writes.
>> + *
>> + * Must be called without ui->ui_mutex held: we block on the old-EOF
>> + * folio lock, and ->writepage() and ->page_mkwrite() hold that folio
>> + * lock while taking ui_mutex; the reverse order deadlocks.
>> + */
>> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
>> +{
>> + loff_t old_size = i_size_read(inode);
>> + size_t from, to;
>> + struct folio *folio;
>> +
>> + if (new_end <= old_size)
>> + return;
>> +
>> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
>> + if (IS_ERR(folio))
>> + return;
>> +
>> + /*
>> + * Re-read i_size under the folio lock: a concurrent truncate may
>> + * have shrunk the file, turning this folio into valid data.
>> + */
>> + old_size = i_size_read(inode);
>> + if (new_end <= old_size)
>> + goto out;
>> +
>> + from = offset_in_folio(folio, old_size);
>> + if (!from)
>> + goto out;
>> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
>> + offset_in_folio(folio, new_end) : folio_size(folio);
>> + if (to > from)
>> + folio_zero_range(folio, from, to - from);
>> +
>> +out:
>> + folio_unlock(folio);
>> + folio_put(folio);
>> +}
>
> [ ... ]
>
>> @@ -1433,7 +1484,17 @@ static int update_mctime(struct inode *inode)
>>
>> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
>> {
>> - int err = update_mctime(file_inode(iocb->ki_filp));
>> + struct inode *inode = file_inode(iocb->ki_filp);
>> + int err;
>> +
>> + /*
>> + * The write may extend beyond EOF: zero the stale tail of the
>> + * old-EOF folio first.
>> + */
>> + if (iocb->ki_pos > i_size_read(inode))
>> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
>> +
>> + err = update_mctime(inode);
>> if (err)
>> return err;
>
Hi, Nan Lin
I think the sashiko report is an effective improvement. The inode_lock
is needed, and zero eof check should be executed after
generic_write_checks().
> [Severity: Critical]
> Unlike do_setattr() (called with inode_lock(inode) held via notify_change())
> and ext4_buffered_write_iter() -> ext4_write_checks() ->
> ext4_block_zero_eof(), ubifs_write_iter() calls ubifs_zero_eof_hole() before
> generic_file_write_iter() acquires inode_lock(inode) and runs
> generic_write_checks(iocb, from):
>
> mm/filemap.c:generic_file_write_iter() {
> inode_lock(inode);
> ret = generic_write_checks(iocb, from);
> if (ret > 0)
> ret = __generic_file_write_iter(iocb, from);
> inode_unlock(inode);
> }
>
> Can calling ubifs_zero_eof_hole() without holding inode_lock(inode) and
> before generic_write_checks() cause data corruption and other regressions?
>
> 1. If ubifs_zero_eof_hole() reads an initial old_size in folio 0 (for
> example, old_size = 100) and calls filemap_lock_folio() on index 0, a
> concurrent writer or extending ftruncate() holding inode_lock(inode) can
> extend inode->i_size into a later folio (for example, old_size becomes 5100
> in folio 1, while new_end is 10000 in folio 2) before folio 0 is locked.
> At that point, all of folio 0 is valid committed file data.
>
> When ubifs_zero_eof_hole() acquires the lock on folio 0 and re-reads
> old_size = i_size_read(inode), new_end <= old_size is still false, and there
> is no check that folio_contains(folio, old_size >> PAGE_SHIFT). Computing
> from = offset_in_folio(folio, old_size) masks the new old_size into folio 0
> and calls folio_zero_range(folio, 1004, 3092) on folio 0, zeroing committed
> in-bounds file data while leaving the actual old-EOF folio (folio 1)
> unzeroed.
>
> A concurrent shrinking truncate that moves i_size into an earlier folio
> before truncate_pagecache() evicts the locked folio will similarly zero the
> wrong folio. And if a concurrent extending ftruncate() updates i_size in
> truncate_setsize() right after ubifs_zero_eof_hole() re-reads old_size,
> userspace mmap writes to the newly in-bounds region can be overwritten by
> folio_zero_range().
>
> 2. Because inode_lock(inode) is not held and folio_lock is dropped before
> update_mctime() (which can sleep in ubifs_budget_space()) and before
> generic_file_write_iter() locks the inode, a concurrent truncate or write can
> change i_size or an mmap write can re-dirty the post-EOF tail in the window
> between ubifs_zero_eof_hole() and __generic_file_write_iter(), leaving stale
> data beyond EOF unzeroed.
>
> 3. Because generic_write_checks() has not run yet:
> - For O_APPEND or RWF_APPEND writes where ki_pos was positioned past EOF,
> generic_write_checks_count() later resets iocb->ki_pos to i_size_read(inode)
> so no hole is created, but ubifs_zero_eof_hole() has already modified the
> page cache.
> - For zero-length writes (iov_iter_count(from) == 0) with ki_pos > i_size,
> generic_write_checks() returns 0 without extending the file, after
> ubifs_zero_eof_hole() has already zeroed the folio tail.
> - For RWF_NOWAIT writes (which generic_write_checks_count() rejects with
> -EINVAL since FOP_BUFFER_WASYNC is not set) or writes failing
> generic_write_check_limits() (-EFBIG), ubifs_zero_eof_hole() can block on
> filemap_lock_folio() and mutate the page cache before the write is
> rejected.
>
> Should ubifs_write_iter() acquire inode_lock(inode) and run
> generic_write_checks() before calling update_mctime(),
> ubifs_zero_eof_hole(), and __generic_file_write_iter(), or otherwise ensure
> that ubifs_zero_eof_hole() runs under inode_lock(inode) after write checks?
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-26 8:05 ` Zhihao Cheng
@ 2026-10-26 12:50 ` Nan Lin
2026-10-26 13:30 ` sashiko-bot
2026-10-27 4:51 ` Zhihao Cheng
0 siblings, 2 replies; 22+ messages in thread
From: Nan Lin @ 2026-10-26 12:50 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-reviews, wangxiaoqing,
zhou.kete
mmap writes can dirty the page cache beyond EOF without changing
i_size. A later extending write or ftruncate turns that region into a
hole, but UBIFS never zeroes the partial page at the old EOF, so reads
expose the stale data.
Zero the tail of the cached old-EOF folio in both size-extending
paths: ubifs_write_iter() for writes starting beyond EOF, and
do_setattr() before truncate_setsize() grows the size. This mirrors
what ext4 does on size extension;
Restructure ubifs_write_iter() to match ext4_buffered_write_iter():
acquire inode_lock, perform write checks, call kiocb_modified() for
setuid/setgid clearing, update mtime/ctime via update_mctime()
(UBIFS sets S_NOCMTIME so VFS time updates short-circuit), zero the
EOF hole, then execute the write with generic_perform_write(). This
ensures proper serialization and security handling.
All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
the folio_lock -> ui_mutex order of ->writepage(); the reverse order
deadlocks.
UBIFS does not enable large folios, so the EOF folio is always a
single page today; nevertheless, use folio-relative arithmetic
(offset_in_folio(), folio_contains(), folio_size()) throughout so the
code is correct if that ever changes.
Signed-off-by: Nan Lin <lin.nan@h3c.com>
Changes since v3:
- Add inode_lock and move zero-eof after generic_write_checks(),
matching ext4_buffered_write_iter structure
(suggested by Sashiko AI review & Zhihao Cheng).
- Add kiocb_modified() for setuid/setgid clearing.
- Keep explicit update_mctime() as UBIFS sets S_NOCMTIME.
Changes since v2:
- Move the extending-write check from ubifs_write_begin() to
ubifs_write_iter(), mirroring ext4_write_checks() ->
ext4_block_zero_eof(): the hole zeroing runs once per write()
syscall instead of once per ->write_begin() iteration (suggested
by Zhihao Cheng).
- Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
(ext4_block_write_begin() with len = i_size - folio_pos),
->page_mkwrite() does not zero the tail beyond EOF, and mmap
writes past EOF are the user's responsibility (suggested by
Zhihao Cheng).
Changes since v1:
- Both zeroing sites used PAGE_SIZE-relative arithmetic
(offset_in_page()-style) while folio_zero_range() expects
folio-relative offsets. UBIFS does not currently enable large
folios, so PAGE_SIZE == folio_size() and the two coincide in
practice, but switch to offset_in_folio()/folio_size() anyway so
the code is correct if UBIFS ever opts into large folios
(suggested by Sashiko AI review).
---
fs/ubifs/file.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 82 insertions(+), 4 deletions(-)
diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
index e73c28b12f97..6f2c83b6f433 100644
--- a/fs/ubifs/file.c
+++ b/fs/ubifs/file.c
@@ -202,6 +202,45 @@ static void release_existing_page_budget(struct ubifs_info *c)
ubifs_release_budget(c, &req);
}
+/**
+ * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
+ * @inode: inode whose size is about to grow
+ * @new_end: new end of the file, only used to bound the tail
+ *
+ * Zero the cached folio containing the old EOF for [old EOF,
+ * min(folio end, @new_end)) so a size extension does not expose stale
+ * data left beyond EOF by mmap writes.
+ *
+ * Must be called with inode_lock(inode) held and without ui->ui_mutex held:
+ * we block on the old-EOF folio lock, and ->writepage() and ->page_mkwrite()
+ * hold that folio lock while taking ui_mutex; the reverse order deadlocks.
+ */
+static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
+{
+ loff_t old_size = i_size_read(inode);
+ size_t from, to;
+ struct folio *folio;
+
+ if (new_end <= old_size)
+ return;
+
+ folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
+ if (IS_ERR(folio))
+ return;
+
+ from = offset_in_folio(folio, old_size);
+ if (!from)
+ goto out;
+ to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
+ offset_in_folio(folio, new_end) : folio_size(folio);
+ if (to > from)
+ folio_zero_range(folio, from, to - from);
+
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
static int write_begin_slow(struct address_space *mapping,
loff_t pos, unsigned len, struct folio **foliop)
{
@@ -1220,6 +1259,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
if (attr->ia_valid & ATTR_SIZE) {
dbg_gen("size %lld -> %lld", inode->i_size, new_size);
+
+ /* Must run before the size change loses the old boundary. */
+ ubifs_zero_eof_hole(inode, new_size);
+
truncate_setsize(inode, new_size);
}
@@ -1433,11 +1476,46 @@ static int update_mctime(struct inode *inode)
static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
- int err = update_mctime(file_inode(iocb->ki_filp));
- if (err)
- return err;
+ struct inode *inode = file_inode(iocb->ki_filp);
+ ssize_t ret;
+
+ if (iocb->ki_flags & IOCB_NOWAIT)
+ return -EOPNOTSUPP;
+
+ inode_lock(inode);
+
+ ret = generic_write_checks(iocb, from);
+ if (ret <= 0)
+ goto out;
+
+ ret = kiocb_modified(iocb);
+ if (ret)
+ goto out;
- return generic_file_write_iter(iocb, from);
+ /*
+ * UBIFS sets S_NOCMTIME on all inodes, so VFS's file_update_time_flags()
+ * short-circuits and never calls ubifs_update_time() for mtime/ctime.
+ * We must call update_mctime() explicitly here.
+ */
+ ret = update_mctime(inode);
+ if (ret)
+ goto out;
+
+ /*
+ * If the write position is beyond EOF, zero the stale tail of the
+ * old-EOF folio first. generic_write_checks() has already resolved
+ * O_APPEND/RWF_APPEND and rejected invalid writes, so iocb->ki_pos
+ * is the final write position.
+ */
+ if (iocb->ki_pos > i_size_read(inode))
+ ubifs_zero_eof_hole(inode, iocb->ki_pos);
+
+ ret = generic_perform_write(iocb, from);
+out:
+ inode_unlock(inode);
+ if (ret > 0)
+ ret = generic_write_sync(iocb, ret);
+ return ret;
}
static bool ubifs_dirty_folio(struct address_space *mapping,
--
2.33.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-26 12:50 ` [PATCH v4] " Nan Lin
@ 2026-10-26 13:30 ` sashiko-bot
2026-10-27 6:12 ` Nan Lin
2026-10-27 4:51 ` Zhihao Cheng
1 sibling, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-10-26 13:30 UTC (permalink / raw)
To: Nan Lin, chengzhihao1; +Cc: linux-mtd, richard, wangxiaoqing, zhou.kete
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ubifs: stale mmap data exposed on short copy in ubifs_write_iter()
--
commit afed492b9caec08994a0a97a427ac39e28d77180
Author: Nan Lin <lin.nan@h3c.com>
ubifs: zero stale data beyond EOF before file size grows
This commit adds ubifs_zero_eof_hole() to zero stale mmap data beyond EOF in
the old-EOF folio during do_setattr() and ubifs_write_iter(), and restructures
ubifs_write_iter() to serialize writes under inode_lock().
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97f..6f2c83b6f4333 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
> @@ -202,6 +202,45 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Must be called with inode_lock(inode) held and without ui->ui_mutex held:
> + * we block on the old-EOF folio lock, and ->writepage() and ->page_mkwrite()
> + * hold that folio lock while taking ui_mutex; the reverse order deadlocks.
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
> + loff_t old_size = i_size_read(inode);
> + size_t from, to;
> + struct folio *folio;
> +
> + if (new_end <= old_size)
> + return;
> +
> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
> + if (IS_ERR(folio))
> + return;
> +
> + from = offset_in_folio(folio, old_size);
> + if (!from)
> + goto out;
> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
> + offset_in_folio(folio, new_end) : folio_size(folio);
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
[ ... ]
> @@ -1433,11 +1476,46 @@ static int update_mctime(struct inode *inode)
>
> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
> {
> - int err = update_mctime(file_inode(iocb->ki_filp));
> - if (err)
> - return err;
> + struct inode *inode = file_inode(iocb->ki_filp);
> + ssize_t ret;
> +
> + if (iocb->ki_flags & IOCB_NOWAIT)
> + return -EOPNOTSUPP;
> +
> + inode_lock(inode);
> +
> + ret = generic_write_checks(iocb, from);
> + if (ret <= 0)
> + goto out;
> +
> + ret = kiocb_modified(iocb);
> + if (ret)
> + goto out;
>
> - return generic_file_write_iter(iocb, from);
> + /*
> + * UBIFS sets S_NOCMTIME on all inodes, so VFS's file_update_time_flags()
> + * short-circuits and never calls ubifs_update_time() for mtime/ctime.
> + * We must call update_mctime() explicitly here.
> + */
> + ret = update_mctime(inode);
> + if (ret)
> + goto out;
> +
> + /*
> + * If the write position is beyond EOF, zero the stale tail of the
> + * old-EOF folio first. generic_write_checks() has already resolved
> + * O_APPEND/RWF_APPEND and rejected invalid writes, so iocb->ki_pos
> + * is the final write position.
> + */
> + if (iocb->ki_pos > i_size_read(inode))
> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
> +
> + ret = generic_perform_write(iocb, from);
[Severity: High]
Can stale mmap data still be exposed when generic_perform_write() encounters a
short or zero-byte copy in copy_folio_from_iter_atomic()?
Suppose a file has a non-page-aligned size old_size and a shared mmap writes
beyond EOF, leaving the old-EOF folio uptodate with stale data in
[old_size, folio_size(folio)).
When a subsequent write() or pwrite() starts at pos >= old_size within that
same folio, ubifs_write_iter() skips ubifs_zero_eof_hole() if pos == old_size,
or only zeroes [old_size, pos) if pos > old_size, leaving [pos, folio_size(folio))
containing the stale mmap data under the assumption that the write will
overwrite [pos, pos + len).
However, if copy_folio_from_iter_atomic() copies fewer than len bytes (for
example, copied == 0 due to an unmapped user buffer returning -EFAULT, or an
unfaulted user page when a signal is pending), ubifs_write_end() only takes
the short-copy bailout when !folio_test_uptodate(folio), and otherwise extends
i_size to end_pos = pos + len rather than pos + copied:
fs/ubifs/file.c:ubifs_write_end() {
loff_t end_pos = pos + len;
int appending = !!(end_pos > inode->i_size);
...
if (unlikely(copied < len && !folio_test_uptodate(folio))) {
...
goto out;
}
...
if (appending) {
i_size_write(inode, end_pos);
ui->ui_size = end_pos;
...
}
}
Because the old-EOF folio is already uptodate, wouldn't ubifs_write_end()
advance inode->i_size and ui->ui_size to pos + len even when copied < len,
exposing the unzeroed stale mmap data in [pos + copied, pos + len)?
By comparison, ext4_block_zero_eof() zeroes the entire tail of the old-EOF
folio from old_size to folio_size(folio), and ext4_write_end() only advances
i_size to pos + copied. Should ubifs_zero_eof_hole() also zero to the end of
the folio (or be called whenever the write extends past i_size), or should
ubifs_write_end() avoid extending i_size past pos + copied?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261026125001.2570619-1-lin.nan@h3c.com?part=1
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-26 12:50 ` [PATCH v4] " Nan Lin
2026-10-26 13:30 ` sashiko-bot
@ 2026-10-27 4:51 ` Zhihao Cheng
2026-10-27 6:10 ` Nan Lin
2026-10-27 8:05 ` [PATCH v5] " Nan Lin
1 sibling, 2 replies; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-27 4:51 UTC (permalink / raw)
To: Nan Lin; +Cc: linux-mtd, richard, sashiko-reviews, wangxiaoqing, zhou.kete
在 2026/10/26 20:50, Nan Lin 写道:
> mmap writes can dirty the page cache beyond EOF without changing
> i_size. A later extending write or ftruncate turns that region into a
> hole, but UBIFS never zeroes the partial page at the old EOF, so reads
> expose the stale data.
>
> Zero the tail of the cached old-EOF folio in both size-extending
> paths: ubifs_write_iter() for writes starting beyond EOF, and
> do_setattr() before truncate_setsize() grows the size. This mirrors
> what ext4 does on size extension;
>
> Restructure ubifs_write_iter() to match ext4_buffered_write_iter():
> acquire inode_lock, perform write checks, call kiocb_modified() for
> setuid/setgid clearing, update mtime/ctime via update_mctime()
> (UBIFS sets S_NOCMTIME so VFS time updates short-circuit), zero the
> EOF hole, then execute the write with generic_perform_write(). This
> ensures proper serialization and security handling.
>
> All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
> the folio_lock -> ui_mutex order of ->writepage(); the reverse order
> deadlocks.
>
> UBIFS does not enable large folios, so the EOF folio is always a
> single page today; nevertheless, use folio-relative arithmetic
> (offset_in_folio(), folio_contains(), folio_size()) throughout so the
> code is correct if that ever changes.
>
> Signed-off-by: Nan Lin <lin.nan@h3c.com>
>
> Changes since v3:
> - Add inode_lock and move zero-eof after generic_write_checks(),
> matching ext4_buffered_write_iter structure
> (suggested by Sashiko AI review & Zhihao Cheng).
> - Add kiocb_modified() for setuid/setgid clearing.
> - Keep explicit update_mctime() as UBIFS sets S_NOCMTIME.
>
> Changes since v2:
> - Move the extending-write check from ubifs_write_begin() to
> ubifs_write_iter(), mirroring ext4_write_checks() ->
> ext4_block_zero_eof(): the hole zeroing runs once per write()
> syscall instead of once per ->write_begin() iteration (suggested
> by Zhihao Cheng).
> - Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
> (ext4_block_write_begin() with len = i_size - folio_pos),
> ->page_mkwrite() does not zero the tail beyond EOF, and mmap
> writes past EOF are the user's responsibility (suggested by
> Zhihao Cheng).
>
> Changes since v1:
> - Both zeroing sites used PAGE_SIZE-relative arithmetic
> (offset_in_page()-style) while folio_zero_range() expects
> folio-relative offsets. UBIFS does not currently enable large
> folios, so PAGE_SIZE == folio_size() and the two coincide in
> practice, but switch to offset_in_folio()/folio_size() anyway so
> the code is correct if UBIFS ever opts into large folios
> (suggested by Sashiko AI review).
> ---
> fs/ubifs/file.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 82 insertions(+), 4 deletions(-)
>
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97..6f2c83b6f433 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
> @@ -202,6 +202,45 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Must be called with inode_lock(inode) held and without ui->ui_mutex held:
> + * we block on the old-EOF folio lock, and ->writepage() and ->page_mkwrite()
> + * hold that folio lock while taking ui_mutex; the reverse order deadlocks.
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
> + loff_t old_size = i_size_read(inode);
> + size_t from, to;
> + struct folio *folio;
> +
> + if (new_end <= old_size)
> + return;
> +
> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
> + if (IS_ERR(folio))
> + return;
> +
> + from = offset_in_folio(folio, old_size);
> + if (!from)
> + goto out;
> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
> + offset_in_folio(folio, new_end) : folio_size(folio);
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
> +
> static int write_begin_slow(struct address_space *mapping,
> loff_t pos, unsigned len, struct folio **foliop)
> {
> @@ -1220,6 +1259,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
>
> if (attr->ia_valid & ATTR_SIZE) {
> dbg_gen("size %lld -> %lld", inode->i_size, new_size);
> +
> + /* Must run before the size change loses the old boundary. */
> + ubifs_zero_eof_hole(inode, new_size);
> +
> truncate_setsize(inode, new_size);
> }
>
> @@ -1433,11 +1476,46 @@ static int update_mctime(struct inode *inode)
>
> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
> {
> - int err = update_mctime(file_inode(iocb->ki_filp));
> - if (err)
> - return err;
> + struct inode *inode = file_inode(iocb->ki_filp);
> + ssize_t ret;
> +
> + if (iocb->ki_flags & IOCB_NOWAIT)
> + return -EOPNOTSUPP;
> +
> + inode_lock(inode);
> +
> + ret = generic_write_checks(iocb, from);
> + if (ret <= 0)
> + goto out;
> +
> + ret = kiocb_modified(iocb);
> + if (ret)
> + goto out;
Hi,
I think file_remove_privs() is enouth, because ubifs inode has
S_NOCMTIME flag and the iocb->ki_flags without 'IOCB_NOWAIT'.
>
> - return generic_file_write_iter(iocb, from);
> + /*
> + * UBIFS sets S_NOCMTIME on all inodes, so VFS's file_update_time_flags()
> + * short-circuits and never calls ubifs_update_time() for mtime/ctime.
> + * We must call update_mctime() explicitly here.
> + */
> + ret = update_mctime(inode);
> + if (ret)
> + goto out;
> +
> + /*
> + * If the write position is beyond EOF, zero the stale tail of the
> + * old-EOF folio first. generic_write_checks() has already resolved
> + * O_APPEND/RWF_APPEND and rejected invalid writes, so iocb->ki_pos
> + * is the final write position.
> + */
> + if (iocb->ki_pos > i_size_read(inode))
> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
> +
> + ret = generic_perform_write(iocb, from);
> +out:
> + inode_unlock(inode);
> + if (ret > 0)
> + ret = generic_write_sync(iocb, ret);
> + return ret;
> }
>
> static bool ubifs_dirty_folio(struct address_space *mapping,
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-27 4:51 ` Zhihao Cheng
@ 2026-10-27 6:10 ` Nan Lin
2026-10-27 8:05 ` [PATCH v5] " Nan Lin
1 sibling, 0 replies; 22+ messages in thread
From: Nan Lin @ 2026-10-27 6:10 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-reviews, wangxiaoqing,
zhou.kete
>> mmap writes can dirty the page cache beyond EOF without changing
>> i_size. A later extending write or ftruncate turns that region into a
>> hole, but UBIFS never zeroes the partial page at the old EOF, so reads
>> expose the stale data.
>>
>> Zero the tail of the cached old-EOF folio in both size-extending
>> paths: ubifs_write_iter() for writes starting beyond EOF, and
>> do_setattr() before truncate_setsize() grows the size. This mirrors
>> what ext4 does on size extension;
>>
>> Restructure ubifs_write_iter() to match ext4_buffered_write_iter():
>> acquire inode_lock, perform write checks, call kiocb_modified() for
>> setuid/setgid clearing, update mtime/ctime via update_mctime()
>> (UBIFS sets S_NOCMTIME so VFS time updates short-circuit), zero the
>> EOF hole, then execute the write with generic_perform_write(). This
>> ensures proper serialization and security handling.
>>
>> All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
>> the folio_lock -> ui_mutex order of ->writepage(); the reverse order
>> deadlocks.
>>
>> UBIFS does not enable large folios, so the EOF folio is always a
>> single page today; nevertheless, use folio-relative arithmetic
>> (offset_in_folio(), folio_contains(), folio_size()) throughout so the
>> code is correct if that ever changes.
>>
>> Signed-off-by: Nan Lin <lin.nan@h3c.com>
>>
>> Changes since v3:
>> - Add inode_lock and move zero-eof after generic_write_checks(),
>> matching ext4_buffered_write_iter structure
>> (suggested by Sashiko AI review & Zhihao Cheng).
>> - Add kiocb_modified() for setuid/setgid clearing.
>> - Keep explicit update_mctime() as UBIFS sets S_NOCMTIME.
>>
>> Changes since v2:
>> - Move the extending-write check from ubifs_write_begin() to
>> ubifs_write_iter(), mirroring ext4_write_checks() ->
>> ext4_block_zero_eof(): the hole zeroing runs once per write()
>> syscall instead of once per ->write_begin() iteration (suggested
>> by Zhihao Cheng).
>> - Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
>> (ext4_block_write_begin() with len = i_size - folio_pos),
>> ->page_mkwrite() does not zero the tail beyond EOF, and mmap
>> writes past EOF are the user's responsibility (suggested by
>> Zhihao Cheng).
>>
>> Changes since v1:
>> - Both zeroing sites used PAGE_SIZE-relative arithmetic
>> (offset_in_page()-style) while folio_zero_range() expects
>> folio-relative offsets. UBIFS does not currently enable large
>> folios, so PAGE_SIZE == folio_size() and the two coincide in
>> practice, but switch to offset_in_folio()/folio_size() anyway so
>> the code is correct if UBIFS ever opts into large folios
>> (suggested by Sashiko AI review).
>> ---
>> fs/ubifs/file.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++---
>> 1 file changed, 82 insertions(+), 4 deletions(-)
>>
>> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
>> index e73c28b12f97..6f2c83b6f433 100644
>> --- a/fs/ubifs/file.c
>> +++ b/fs/ubifs/file.c
>> @@ -202,6 +202,45 @@ static void release_existing_page_budget(struct ubifs_info *c)
>> ubifs_release_budget(c, &req);
>> }
>>
>> +/**
>> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
>> + * @inode: inode whose size is about to grow
>> + * @new_end: new end of the file, only used to bound the tail
>> + *
>> + * Zero the cached folio containing the old EOF for [old EOF,
>> + * min(folio end, @new_end)) so a size extension does not expose stale
>> + * data left beyond EOF by mmap writes.
>> + *
>> + * Must be called with inode_lock(inode) held and without ui->ui_mutex held:
>> + * we block on the old-EOF folio lock, and ->writepage() and ->page_mkwrite()
>> + * hold that folio lock while taking ui_mutex; the reverse order deadlocks.
>> + */
>> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
>> +{
>> + loff_t old_size = i_size_read(inode);
>> + size_t from, to;
>> + struct folio *folio;
>> +
>> + if (new_end <= old_size)
>> + return;
>> +
>> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
>> + if (IS_ERR(folio))
>> + return;
>> +
>> + from = offset_in_folio(folio, old_size);
>> + if (!from)
>> + goto out;
>> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
>> + offset_in_folio(folio, new_end) : folio_size(folio);
>> + if (to > from)
>> + folio_zero_range(folio, from, to - from);
>> +
>> +out:
>> + folio_unlock(folio);
>> + folio_put(folio);
>> +}
>> +
>> static int write_begin_slow(struct address_space *mapping,
>> loff_t pos, unsigned len, struct folio **foliop)
>> {
>> @@ -1220,6 +1259,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
>>
>> if (attr->ia_valid & ATTR_SIZE) {
>> dbg_gen("size %lld -> %lld", inode->i_size, new_size);
>> +
>> + /* Must run before the size change loses the old boundary. */
>> + ubifs_zero_eof_hole(inode, new_size);
>> +
>> truncate_setsize(inode, new_size);
>> }
>>
>> @@ -1433,11 +1476,46 @@ static int update_mctime(struct inode *inode)
>>
>> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
>> {
>> - int err = update_mctime(file_inode(iocb->ki_filp));
>> - if (err)
>> - return err;
>> + struct inode *inode = file_inode(iocb->ki_filp);
>> + ssize_t ret;
>> +
>> + if (iocb->ki_flags & IOCB_NOWAIT)
>> + return -EOPNOTSUPP;
>> +
>> + inode_lock(inode);
>> +
>> + ret = generic_write_checks(iocb, from);
>> + if (ret <= 0)
>> + goto out;
>> +
>> + ret = kiocb_modified(iocb);
>> + if (ret)
>> + goto out;
>Hi,
>I think file_remove_privs() is enouth, because ubifs inode has
>S_NOCMTIME flag and the iocb->ki_flags without 'IOCB_NOWAIT'.
Ack, This makes the code clearer.
I'll make this change in the next revision.
>>
>> - return generic_file_write_iter(iocb, from);
>> + /*
>> + * UBIFS sets S_NOCMTIME on all inodes, so VFS's file_update_time_flags()
>> + * short-circuits and never calls ubifs_update_time() for mtime/ctime.
>> + * We must call update_mctime() explicitly here.
>> + */
>> + ret = update_mctime(inode);
>> + if (ret)
>> + goto out;
>> +
>> + /*
>> + * If the write position is beyond EOF, zero the stale tail of the
>> + * old-EOF folio first. generic_write_checks() has already resolved
>> + * O_APPEND/RWF_APPEND and rejected invalid writes, so iocb->ki_pos
>> + * is the final write position.
>> + */
>> + if (iocb->ki_pos > i_size_read(inode))
>> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
>> +
>> + ret = generic_perform_write(iocb, from);
>> +out:
>> + inode_unlock(inode);
>> + if (ret > 0)
>> + ret = generic_write_sync(iocb, ret);
>> + return ret;
>> }
>>
>> static bool ubifs_dirty_folio(struct address_space *mapping,
>>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-26 13:30 ` sashiko-bot
@ 2026-10-27 6:12 ` Nan Lin
2026-10-27 6:48 ` Zhihao Cheng
0 siblings, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-27 6:12 UTC (permalink / raw)
To: sashiko-bot
Cc: chengzhihao1, lin.nan, linux-mtd, richard, sashiko-reviews,
wangxiaoqing, zhou.kete
>> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
>> index e73c28b12f97f..6f2c83b6f4333 100644
>> --- a/fs/ubifs/file.c
>> +++ b/fs/ubifs/file.c
>> @@ -202,6 +202,45 @@ static void release_existing_page_budget(struct ubifs_info *c)
>> ubifs_release_budget(c, &req);
>> }
>>
>> +/**
>> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
>> + * @inode: inode whose size is about to grow
>> + * @new_end: new end of the file, only used to bound the tail
>> + *
>> + * Zero the cached folio containing the old EOF for [old EOF,
>> + * min(folio end, @new_end)) so a size extension does not expose stale
>> + * data left beyond EOF by mmap writes.
>> + *
>> + * Must be called with inode_lock(inode) held and without ui->ui_mutex held:
>> + * we block on the old-EOF folio lock, and ->writepage() and ->page_mkwrite()
>> + * hold that folio lock while taking ui_mutex; the reverse order deadlocks.
>> + */
>> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
>> +{
>> + loff_t old_size = i_size_read(inode);
>> + size_t from, to;
>> + struct folio *folio;
>> +
>> + if (new_end <= old_size)
>> + return;
>> +
>> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
>> + if (IS_ERR(folio))
>> + return;
>> +
>> + from = offset_in_folio(folio, old_size);
>> + if (!from)
>> + goto out;
>> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
>> + offset_in_folio(folio, new_end) : folio_size(folio);
>> + if (to > from)
>> + folio_zero_range(folio, from, to - from);
>> +
>> +out:
>> + folio_unlock(folio);
>> + folio_put(folio);
>> +}
>
>[ ... ]
>
>> @@ -1433,11 +1476,46 @@ static int update_mctime(struct inode *inode)
>>
>> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
>> {
>> - int err = update_mctime(file_inode(iocb->ki_filp));
>> - if (err)
>> - return err;
>> + struct inode *inode = file_inode(iocb->ki_filp);
>> + ssize_t ret;
>> +
>> + if (iocb->ki_flags & IOCB_NOWAIT)
>> + return -EOPNOTSUPP;
>> +
>> + inode_lock(inode);
>> +
>> + ret = generic_write_checks(iocb, from);
>> + if (ret <= 0)
>> + goto out;
>> +
>> + ret = kiocb_modified(iocb);
>> + if (ret)
>> + goto out;
>>
>> - return generic_file_write_iter(iocb, from);
>> + /*
>> + * UBIFS sets S_NOCMTIME on all inodes, so VFS's file_update_time_flags()
>> + * short-circuits and never calls ubifs_update_time() for mtime/ctime.
>> + * We must call update_mctime() explicitly here.
>> + */
>> + ret = update_mctime(inode);
>> + if (ret)
>> + goto out;
>> +
>> + /*
>> + * If the write position is beyond EOF, zero the stale tail of the
>> + * old-EOF folio first. generic_write_checks() has already resolved
>> + * O_APPEND/RWF_APPEND and rejected invalid writes, so iocb->ki_pos
>> + * is the final write position.
>> + */
>> + if (iocb->ki_pos > i_size_read(inode))
>> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
>> +
>> + ret = generic_perform_write(iocb, from);
>
>[Severity: High]
>Can stale mmap data still be exposed when generic_perform_write() encounters a
>short or zero-byte copy in copy_folio_from_iter_atomic()?
>
>Suppose a file has a non-page-aligned size old_size and a shared mmap writes
>beyond EOF, leaving the old-EOF folio uptodate with stale data in
>[old_size, folio_size(folio)).
>
>When a subsequent write() or pwrite() starts at pos >= old_size within that
>same folio, ubifs_write_iter() skips ubifs_zero_eof_hole() if pos == old_size,
>or only zeroes [old_size, pos) if pos > old_size, leaving [pos, folio_size(folio))
>containing the stale mmap data under the assumption that the write will
>overwrite [pos, pos + len).
>
>However, if copy_folio_from_iter_atomic() copies fewer than len bytes (for
>example, copied == 0 due to an unmapped user buffer returning -EFAULT, or an
>unfaulted user page when a signal is pending), ubifs_write_end() only takes
>the short-copy bailout when !folio_test_uptodate(folio), and otherwise extends
>i_size to end_pos = pos + len rather than pos + copied:
>
>fs/ubifs/file.c:ubifs_write_end() {
> loff_t end_pos = pos + len;
> int appending = !!(end_pos > inode->i_size);
> ...
> if (unlikely(copied < len && !folio_test_uptodate(folio))) {
> ...
> goto out;
> }
> ...
> if (appending) {
> i_size_write(inode, end_pos);
> ui->ui_size = end_pos;
> ...
> }
>}
>
>Because the old-EOF folio is already uptodate, wouldn't ubifs_write_end()
>advance inode->i_size and ui->ui_size to pos + len even when copied < len,
>exposing the unzeroed stale mmap data in [pos + copied, pos + len)?
>
>By comparison, ext4_block_zero_eof() zeroes the entire tail of the old-EOF
>folio from old_size to folio_size(folio), and ext4_write_end() only advances
>i_size to pos + copied. Should ubifs_zero_eof_hole() also zero to the end of
>the folio (or be called whenever the write extends past i_size), or should
>ubifs_write_end() avoid extending i_size past pos + copied?
Valid concern. I think the short-copy issue is best handled separately
since it involves write_end() budget semantics and affects all buffered
writes, not just extending ones. Note that the i_size advance to pos + len on
short copies is pre-existing UBIFS behavior, not introduced by this patch.
Worth a dedicated analysis/patch but outside the scope of this fix.
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v4] ubifs: zero stale data beyond EOF before file size grows
2026-10-27 6:12 ` Nan Lin
@ 2026-10-27 6:48 ` Zhihao Cheng
0 siblings, 0 replies; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-27 6:48 UTC (permalink / raw)
To: Nan Lin, sashiko-bot
Cc: linux-mtd, richard, sashiko-reviews, wangxiaoqing, zhou.kete
在 2026/10/27 14:12, Nan Lin 写道:
>>> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
>
[...]
>
>> Because the old-EOF folio is already uptodate, wouldn't ubifs_write_end()
>
>> advance inode->i_size and ui->ui_size to pos + len even when copied < len,
>
>> exposing the unzeroed stale mmap data in [pos + copied, pos + len)?
>
>>
>
>> By comparison, ext4_block_zero_eof() zeroes the entire tail of the old-EOF
>
>> folio from old_size to folio_size(folio), and ext4_write_end() only advances
>
>> i_size to pos + copied. Should ubifs_zero_eof_hole() also zero to the end of
>
>> the folio (or be called whenever the write extends past i_size), or should
>
>> ubifs_write_end() avoid extending i_size past pos + copied?
>
>
>
> Valid concern. I think the short-copy issue is best handled separately
>
> since it involves write_end() budget semantics and affects all buffered
>
> writes, not just extending ones. Note that the i_size advance to pos + len on
>
> short copies is pre-existing UBIFS behavior, not introduced by this patch.
>
> Worth a dedicated analysis/patch but outside the scope of this fix.
>
Agreed
>
> .
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v5] ubifs: zero stale data beyond EOF before file size grows
2026-10-27 4:51 ` Zhihao Cheng
2026-10-27 6:10 ` Nan Lin
@ 2026-10-27 8:05 ` Nan Lin
2026-10-27 8:40 ` Zhihao Cheng
1 sibling, 1 reply; 22+ messages in thread
From: Nan Lin @ 2026-10-27 8:05 UTC (permalink / raw)
To: chengzhihao1
Cc: lin.nan, linux-mtd, richard, sashiko-reviews, wangxiaoqing,
zhou.kete
mmap writes can dirty the page cache beyond EOF without changing
i_size. A later extending write or ftruncate turns that region into a
hole, but UBIFS never zeroes the partial page at the old EOF, so reads
expose the stale data.
Zero the tail of the cached old-EOF folio in both size-extending
paths: ubifs_write_iter() for writes starting beyond EOF, and
do_setattr() before truncate_setsize() grows the size. This mirrors
what ext4 does on size extension;
Restructure ubifs_write_iter() to match ext4_buffered_write_iter():
acquire inode_lock, perform write checks, call file_remove_privs() for
setuid/setgid clearing (UBIFS sets S_NOCMTIME so file_modified() is a
no-op), update mtime/ctime via update_mctime(), zero the EOF hole,
then execute the write with generic_perform_write(). This ensures
proper serialization and security handling.
All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
the folio_lock -> ui_mutex order of ->writepage(); the reverse order
deadlocks.
UBIFS does not enable large folios, so the EOF folio is always a
single page today; nevertheless, use folio-relative arithmetic
(offset_in_folio(), folio_contains(), folio_size()) throughout so the
code is correct if that ever changes.
Signed-off-by: Nan Lin <lin.nan@h3c.com>
Changes since v4:
- Use file_remove_privs() instead of kiocb_modified(), as UBIFS sets
S_NOCMTIME making file_modified() a no-op (suggested by
Zhihao Cheng).
Changes since v3:
- Add inode_lock and move zero-eof after generic_write_checks(),
matching ext4_buffered_write_iter structure
(suggested by Sashiko AI review & Zhihao Cheng).
- Add kiocb_modified() for setuid/setgid clearing.
- Keep explicit update_mctime() as UBIFS sets S_NOCMTIME.
Changes since v2:
- Move the extending-write check from ubifs_write_begin() to
ubifs_write_iter(), mirroring ext4_write_checks() ->
ext4_block_zero_eof(): the hole zeroing runs once per write()
syscall instead of once per ->write_begin() iteration (suggested
by Zhihao Cheng).
- Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
(ext4_block_write_begin() with len = i_size - folio_pos),
->page_mkwrite() does not zero the tail beyond EOF, and mmap
writes past EOF are the user's responsibility (suggested by
Zhihao Cheng).
Changes since v1:
- Both zeroing sites used PAGE_SIZE-relative arithmetic
(offset_in_page()-style) while folio_zero_range() expects
folio-relative offsets. UBIFS does not currently enable large
folios, so PAGE_SIZE == folio_size() and the two coincide in
practice, but switch to offset_in_folio()/folio_size() anyway so
the code is correct if UBIFS ever opts into large folios
(suggested by Sashiko AI review).
Note: The short-copy issue in ubifs_write_end() where i_size may
advance to pos + len on partial copies is pre-existing UBIFS behavior,
not introduced by this patch. It affects all buffered writes and
involves write_end() budget/ui_mutex handling; worth a dedicated
analysis/patch but outside the scope of this fix.
---
fs/ubifs/file.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 76 insertions(+), 4 deletions(-)
diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
index e73c28b12f97..534e93793744 100644
--- a/fs/ubifs/file.c
+++ b/fs/ubifs/file.c
@@ -202,6 +202,44 @@ static void release_existing_page_budget(struct ubifs_info *c)
ubifs_release_budget(c, &req);
}
+/**
+ * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
+ * @inode: inode whose size is about to grow
+ * @new_end: new end of the file, only used to bound the tail
+ *
+ * Zero the cached folio containing the old EOF for [old EOF,
+ * min(folio end, @new_end)) so a size extension does not expose stale
+ * data left beyond EOF by mmap writes.
+ *
+ * Context: inode_lock held, ui_mutex not held. The old-EOF folio_lock
+ * must nest outside ui_mutex to match ->writepage()/->page_mkwrite().
+ */
+static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
+{
+ loff_t old_size = i_size_read(inode);
+ size_t from, to;
+ struct folio *folio;
+
+ if (new_end <= old_size)
+ return;
+
+ folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
+ if (IS_ERR(folio))
+ return;
+
+ from = offset_in_folio(folio, old_size);
+ if (!from)
+ goto out;
+ to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
+ offset_in_folio(folio, new_end) : folio_size(folio);
+ if (to > from)
+ folio_zero_range(folio, from, to - from);
+
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
static int write_begin_slow(struct address_space *mapping,
loff_t pos, unsigned len, struct folio **foliop)
{
@@ -1220,6 +1258,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
if (attr->ia_valid & ATTR_SIZE) {
dbg_gen("size %lld -> %lld", inode->i_size, new_size);
+
+ /* Zero before truncate_setsize() changes i_size. */
+ ubifs_zero_eof_hole(inode, new_size);
+
truncate_setsize(inode, new_size);
}
@@ -1433,11 +1475,41 @@ static int update_mctime(struct inode *inode)
static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
- int err = update_mctime(file_inode(iocb->ki_filp));
- if (err)
- return err;
+ struct inode *inode = file_inode(iocb->ki_filp);
+ ssize_t ret;
+
+ if (iocb->ki_flags & IOCB_NOWAIT)
+ return -EOPNOTSUPP;
- return generic_file_write_iter(iocb, from);
+ inode_lock(inode);
+
+ ret = generic_write_checks(iocb, from);
+ if (ret <= 0)
+ goto out;
+
+ /* Clear setuid/setgid; skip file_modified() due to S_NOCMTIME. */
+ ret = file_remove_privs(iocb->ki_filp);
+ if (ret)
+ goto out;
+
+ /* Update mtime/ctime explicitly (VFS skips it for S_NOCMTIME). */
+ ret = update_mctime(inode);
+ if (ret)
+ goto out;
+
+ /*
+ * Zero stale data beyond old EOF if extending. At this point
+ * ki_pos is final (O_APPEND/RWF_APPEND already resolved).
+ */
+ if (iocb->ki_pos > i_size_read(inode))
+ ubifs_zero_eof_hole(inode, iocb->ki_pos);
+
+ ret = generic_perform_write(iocb, from);
+out:
+ inode_unlock(inode);
+ if (ret > 0)
+ ret = generic_write_sync(iocb, ret);
+ return ret;
}
static bool ubifs_dirty_folio(struct address_space *mapping,
--
2.33.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH v5] ubifs: zero stale data beyond EOF before file size grows
2026-10-27 8:05 ` [PATCH v5] " Nan Lin
@ 2026-10-27 8:40 ` Zhihao Cheng
0 siblings, 0 replies; 22+ messages in thread
From: Zhihao Cheng @ 2026-10-27 8:40 UTC (permalink / raw)
To: Nan Lin; +Cc: linux-mtd, richard, sashiko-reviews, wangxiaoqing, zhou.kete
在 2026/10/27 16:05, Nan Lin 写道:
> mmap writes can dirty the page cache beyond EOF without changing
> i_size. A later extending write or ftruncate turns that region into a
> hole, but UBIFS never zeroes the partial page at the old EOF, so reads
> expose the stale data.
>
> Zero the tail of the cached old-EOF folio in both size-extending
> paths: ubifs_write_iter() for writes starting beyond EOF, and
> do_setattr() before truncate_setsize() grows the size. This mirrors
> what ext4 does on size extension;
>
> Restructure ubifs_write_iter() to match ext4_buffered_write_iter():
> acquire inode_lock, perform write checks, call file_remove_privs() for
> setuid/setgid clearing (UBIFS sets S_NOCMTIME so file_modified() is a
> no-op), update mtime/ctime via update_mctime(), zero the EOF hole,
> then execute the write with generic_perform_write(). This ensures
> proper serialization and security handling.
>
> All zeroing takes the old-EOF folio lock without ui->ui_mutex, keeping
> the folio_lock -> ui_mutex order of ->writepage(); the reverse order
> deadlocks.
>
> UBIFS does not enable large folios, so the EOF folio is always a
> single page today; nevertheless, use folio-relative arithmetic
> (offset_in_folio(), folio_contains(), folio_size()) throughout so the
> code is correct if that ever changes.
>
> Signed-off-by: Nan Lin <lin.nan@h3c.com>
>
> Changes since v4:
> - Use file_remove_privs() instead of kiocb_modified(), as UBIFS sets
> S_NOCMTIME making file_modified() a no-op (suggested by
> Zhihao Cheng).
>
> Changes since v3:
> - Add inode_lock and move zero-eof after generic_write_checks(),
> matching ext4_buffered_write_iter structure
> (suggested by Sashiko AI review & Zhihao Cheng).
> - Add kiocb_modified() for setuid/setgid clearing.
> - Keep explicit update_mctime() as UBIFS sets S_NOCMTIME.
>
> Changes since v2:
> - Move the extending-write check from ubifs_write_begin() to
> ubifs_write_iter(), mirroring ext4_write_checks() ->
> ext4_block_zero_eof(): the hole zeroing runs once per write()
> syscall instead of once per ->write_begin() iteration (suggested
> by Zhihao Cheng).
> - Drop the ubifs_vm_page_mkwrite() zeroing: like ext4
> (ext4_block_write_begin() with len = i_size - folio_pos),
> ->page_mkwrite() does not zero the tail beyond EOF, and mmap
> writes past EOF are the user's responsibility (suggested by
> Zhihao Cheng).
>
> Changes since v1:
> - Both zeroing sites used PAGE_SIZE-relative arithmetic
> (offset_in_page()-style) while folio_zero_range() expects
> folio-relative offsets. UBIFS does not currently enable large
> folios, so PAGE_SIZE == folio_size() and the two coincide in
> practice, but switch to offset_in_folio()/folio_size() anyway so
> the code is correct if UBIFS ever opts into large folios
> (suggested by Sashiko AI review).
>
> Note: The short-copy issue in ubifs_write_end() where i_size may
> advance to pos + len on partial copies is pre-existing UBIFS behavior,
> not introduced by this patch. It affects all buffered writes and
> involves write_end() budget/ui_mutex handling; worth a dedicated
> analysis/patch but outside the scope of this fix.
> ---
> fs/ubifs/file.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 76 insertions(+), 4 deletions(-)
>
Reviewed-by: Zhihao Cheng <chengzhihao1@huawei.com>
> diff --git a/fs/ubifs/file.c b/fs/ubifs/file.c
> index e73c28b12f97..534e93793744 100644
> --- a/fs/ubifs/file.c
> +++ b/fs/ubifs/file.c
> @@ -202,6 +202,44 @@ static void release_existing_page_budget(struct ubifs_info *c)
> ubifs_release_budget(c, &req);
> }
>
> +/**
> + * ubifs_zero_eof_hole - zero the stale tail of the old-EOF folio.
> + * @inode: inode whose size is about to grow
> + * @new_end: new end of the file, only used to bound the tail
> + *
> + * Zero the cached folio containing the old EOF for [old EOF,
> + * min(folio end, @new_end)) so a size extension does not expose stale
> + * data left beyond EOF by mmap writes.
> + *
> + * Context: inode_lock held, ui_mutex not held. The old-EOF folio_lock
> + * must nest outside ui_mutex to match ->writepage()/->page_mkwrite().
> + */
> +static void ubifs_zero_eof_hole(struct inode *inode, loff_t new_end)
> +{
> + loff_t old_size = i_size_read(inode);
> + size_t from, to;
> + struct folio *folio;
> +
> + if (new_end <= old_size)
> + return;
> +
> + folio = filemap_lock_folio(inode->i_mapping, old_size >> PAGE_SHIFT);
> + if (IS_ERR(folio))
> + return;
> +
> + from = offset_in_folio(folio, old_size);
> + if (!from)
> + goto out;
> + to = folio_contains(folio, new_end >> PAGE_SHIFT) ?
> + offset_in_folio(folio, new_end) : folio_size(folio);
> + if (to > from)
> + folio_zero_range(folio, from, to - from);
> +
> +out:
> + folio_unlock(folio);
> + folio_put(folio);
> +}
> +
> static int write_begin_slow(struct address_space *mapping,
> loff_t pos, unsigned len, struct folio **foliop)
> {
> @@ -1220,6 +1258,10 @@ static int do_setattr(struct ubifs_info *c, struct inode *inode,
>
> if (attr->ia_valid & ATTR_SIZE) {
> dbg_gen("size %lld -> %lld", inode->i_size, new_size);
> +
> + /* Zero before truncate_setsize() changes i_size. */
> + ubifs_zero_eof_hole(inode, new_size);
> +
> truncate_setsize(inode, new_size);
> }
>
> @@ -1433,11 +1475,41 @@ static int update_mctime(struct inode *inode)
>
> static ssize_t ubifs_write_iter(struct kiocb *iocb, struct iov_iter *from)
> {
> - int err = update_mctime(file_inode(iocb->ki_filp));
> - if (err)
> - return err;
> + struct inode *inode = file_inode(iocb->ki_filp);
> + ssize_t ret;
> +
> + if (iocb->ki_flags & IOCB_NOWAIT)
> + return -EOPNOTSUPP;
>
> - return generic_file_write_iter(iocb, from);
> + inode_lock(inode);
> +
> + ret = generic_write_checks(iocb, from);
> + if (ret <= 0)
> + goto out;
> +
> + /* Clear setuid/setgid; skip file_modified() due to S_NOCMTIME. */
> + ret = file_remove_privs(iocb->ki_filp);
> + if (ret)
> + goto out;
> +
> + /* Update mtime/ctime explicitly (VFS skips it for S_NOCMTIME). */
> + ret = update_mctime(inode);
> + if (ret)
> + goto out;
> +
> + /*
> + * Zero stale data beyond old EOF if extending. At this point
> + * ki_pos is final (O_APPEND/RWF_APPEND already resolved).
> + */
> + if (iocb->ki_pos > i_size_read(inode))
> + ubifs_zero_eof_hole(inode, iocb->ki_pos);
> +
> + ret = generic_perform_write(iocb, from);
> +out:
> + inode_unlock(inode);
> + if (ret > 0)
> + ret = generic_write_sync(iocb, ret);
> + return ret;
> }
>
> static bool ubifs_dirty_folio(struct address_space *mapping,
>
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-10-27 8:40 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 9:04 [PATCH] ubifs: zero stale data beyond EOF before file size grows Nan Lin
2026-10-16 9:44 ` sashiko-bot
2026-10-19 3:25 ` [PATCH v2] " Nan Lin
2026-10-19 12:38 ` Zhihao Cheng
2026-10-19 12:45 ` Zhihao Cheng
2026-10-22 3:18 ` Nan Lin
2026-10-22 7:14 ` Zhihao Cheng
2026-10-22 13:15 ` Nan Lin
2026-10-22 13:54 ` Zhihao Cheng
2026-10-23 1:23 ` Nan Lin
2026-10-23 1:38 ` Zhihao Cheng
2026-10-23 7:11 ` [PATCH v3] " Nan Lin
2026-10-23 7:35 ` sashiko-bot
2026-10-26 8:05 ` Zhihao Cheng
2026-10-26 12:50 ` [PATCH v4] " Nan Lin
2026-10-26 13:30 ` sashiko-bot
2026-10-27 6:12 ` Nan Lin
2026-10-27 6:48 ` Zhihao Cheng
2026-10-27 4:51 ` Zhihao Cheng
2026-10-27 6:10 ` Nan Lin
2026-10-27 8:05 ` [PATCH v5] " Nan Lin
2026-10-27 8:40 ` Zhihao Cheng
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.