System writes out modified portions of the last memory mapped page beyond file end if file is resized

From: Vyacheslav Kovalevsky

Date: Fri Oct 09 2026 - 10:10:14 EST


Detailed description
====================

Hello, there seems to be an issue with NILFS2:

1. Create a file, truncate it to some size that is not multiple of OS page size (which is usually 4096).
2. Open the file.
3. Create a new shared mapping (MAP_SHARED) using `mmap` from 0 to some point beyond file end (but still on the same OS page as the file end, otherwise next steps will fail).
4. Write some data beyond file end using the mapping.
5. Resize file (e.g. using `truncate`) so the file end is now after the mapping range.
6. Observe that bytes written beyond file end (using the mapping) were also written to backing file after resizing.

The last bit violates the POSIX specification, extract from `mmap` documentation at <https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html>:

> The system shall always zero-fill any partial page at the end of an object. Further, __the system shall never write out any modified portions of the last page of an object which are beyond its end__.

Linux manual <https://man7.org/linux/man-pages/man2/mmap.2.html>:

> For a file that is not a multiple of the page size, the remaining bytes in the partial page at the end of the mapping are zeroed when mapped, and __modifications to that region are not written out to the file__.

For reference: `ext4` / `btrfs` / `xfs` do not write the data to file after resizing, as

The exact same issue was recently reported for ZFS: <https://github.com/openzfs/zfs/issues/19220>


System info
===========

Linux version 7.0.0-34-generic (Ubuntu 26.04.1 LTS)
nilfs-tools version 2.2.11


How to reproduce
================

```
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define WRITE_OFF 10
#define WRITE_SIZE 10
#define WRITE_DATA "Test data!"
#define MMAP_SIZE WRITE_SIZE + WRITE_OFF
#define FILE_SIZE_INI 10
#define FILE_SIZE_END 32

int main() {
  char buffer[FILE_SIZE_END + 1] = {};
  int status;
  int fd;
  void *addr;

  status = creat("file", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
  printf("CREAT: %d\n", status);
  close(status);

  status = truncate("file", FILE_SIZE_INI);
  printf("TRUNCATE: %d\n", status);

  status = open("file", O_RDWR);
  printf("OPEN: %d\n", status);
  fd = status;

  addr = mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (addr == MAP_FAILED) {
    printf("MMAP: FAILED\n");
    return EXIT_FAILURE;
  }
  printf("MMAP: OK\n");
  close(fd);

  memcpy(((char *)addr) + WRITE_OFF, WRITE_DATA, WRITE_SIZE);

  status = munmap(addr, MMAP_SIZE);
  printf("MUNMAP: %d\n", status);

  status = truncate("file", FILE_SIZE_END);
  printf("TRUNCATE: %d\n", status);

  status = open("file", O_RDONLY);
  printf("OPEN: %d\n", status);
  fd = status;

  status = read(fd, buffer, FILE_SIZE_END);
  printf("READ: %d\n", status);
  printf("BYTES: ");
  for (int i = 0; i < status; i++) {
    printf("%02x ", buffer[i]);
  }
  printf("\n");
}

// ::Expected::
// CREAT: 3
// TRUNCATE: 0
// OPEN: 3
// MMAP: OK
// MUNMAP: 0
// TRUNCATE: 0
// OPEN: 3
// READ: 32
// BYTES: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

// ::Actual::
// CREAT: 3
// TRUNCATE: 0
// OPEN: 3
// MMAP: OK
// MUNMAP: 0
// TRUNCATE: 0
// OPEN: 3
// READ: 32
// BYTES: 00 00 00 00 00 00 00 00 00 00 54 65 73 74 20 64 61 74 61 21 00 00 00 00 00 00 00 00 00 00 00 00
```

Steps:

1. Create and mount new NILFS2 filesystem in default configuration.
2. Change directory to root of the filesystem and run the compiled test.
3. Observe that last operation returns unexpected value.