[PATCH v2] ubifs: zero stale data beyond EOF before file size grows

Nan Lin lin.nan at h3c.com
Wed Oct 21 20:18:14 PDT 2026


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



More information about the linux-mtd mailing list