Re: [PATCH 2/3] minix: convert address space operations to iomap
From: XIAO WU
Date: Sun Jun 28 2026 - 14:48:46 EST
Hi Jeremy,
I came across the Sashiko AI review of this patch series and reproduced
one of the issues it flagged -- a NULL pointer dereference when creating
a symlink on a mounted minix filesystem. The crash is deterministic and
triggers on the first symlink() call.
The Sashiko review page is at:
https://sashiko.dev/#/patchset/cover.1782422707.git.jbingham@xxxxxxxxx
> @@ -487,19 +540,36 @@ static int minix_write_begin(...)
> }
>
> static const struct address_space_operations minix_aops = {
> - .dirty_folio = block_dirty_folio,
> - .invalidate_folio = block_invalidate_folio,
> + .dirty_folio = iomap_dirty_folio,
> + .invalidate_folio = iomap_invalidate_folio,
> .read_folio = minix_read_folio,
> + .readahead = minix_readahead,
> .writepages = minix_writepages,
> + .migrate_folio = filemap_migrate_folio,
> + .bmap = minix_bmap,
> + .is_partially_uptodate = iomap_is_partially_uptodate,
> + .release_folio = iomap_release_folio,
> + .error_remove_folio = generic_error_remove_folio,
> +};
The iomap conversion removes .write_begin and .write_end from
minix_aops. They are now only present in minix_dir_aops (the new
directory-specific aops). However, minix_set_inode() still assigns
minix_aops (without write_begin) to symlink inodes:
In minix_set_inode() (unchanged by this patch):
```c
} else if (S_ISLNK(inode->i_mode)) {
inode->i_op = &minix_symlink_inode_operations;
inode_nohighmem(inode);
inode->i_mapping->a_ops = &minix_aops;
```
When a user calls symlink() on a mounted minix filesystem,
minix_symlink() calls page_symlink(), which directly dereferences
aops->write_begin:
In page_symlink() (fs/namei.c):
```c
err = aops->write_begin(NULL, mapping, 0, len-1, &folio, &fsdata);
```
Since minix_aops.write_begin is now NULL, the kernel tries to execute
code at address 0x0.
--- Reproduction ---
Kernel: 7.1.0-next-20260625-g085406171f0d #1 SMP PREEMPT(full)
Config: CONFIG_MINIX_FS=y, CONFIG_BLOCK=y, CONFIG_KASAN=y
QEMU: QEMU Standard PC (Q35 + ICH9, 2009)
The trigger is deterministic -- no fault injection or race conditions
required. Simply mount a minix filesystem and create a symlink.
--- Crash Log ---
[ 331.105013][ T9804] BUG: kernel NULL pointer dereference, address: 0000000000000000
[ 331.106761][ T9804] #PF: supervisor instruction fetch in kernel mode
[ 331.107911][ T9804] #PF: error_code(0x0010) - not-present page
[ 331.109704][ T9804] Oops: Oops: 0010 [#1] SMP KASAN NOPTI
[ 331.110760][ T9804] CPU: 1 UID: 0 PID: 9804 Comm: poc Not tainted 7.1.0-next-20260625-g085406171f0d #1 PREEMPT(full)
[ 331.111754][ T9804] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 331.112105][ T9804] RIP: 0010:0x0
[ 331.112831][ T9804] Code: Unable to access opcode bytes at 0xffffffffffffffd6.
[ 331.119688][ T9804] Call Trace:
[ 331.120018][ T9804] <TASK>
[ 331.120309][ T9804] page_symlink+0x394/0x4c0
[ 331.120772][ T9804] ? __pfx_page_symlink+0x10/0x10
[ 331.121270][ T9804] ? minix_new_inode+0x3bf/0x510
[ 331.121772][ T9804] minix_symlink+0xd8/0x180
[ 331.122717][ T9804] vfs_symlink+0x17d/0x4e0
[ 331.123171][ T9804] filename_symlinkat+0x3ab/0x4e0
[ 331.124745][ T9804] __x64_sys_symlink+0x7e/0xb0
[ 331.125200][ T9804] do_syscall_64+0x129/0x880
[ 331.125700][ T9804] entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 331.126200][ T9804] RIP: 0033:0x41c13b
[ 331.126700][ T9804] </TASK>
[ 331.127200][ T9804] Kernel panic - not syncing: Fatal exception
The RIP at 0x0 confirms a NULL function pointer dereference.
The call chain is exactly page_symlink -> minix_symlink -> vfs_symlink.
--- PoC ---
Build: gcc -o poc poc.c -static
Run: ./poc (requires root for mount/loop setup)
/*
* PoC: NULL pointer dereference in page_symlink() when creating a
* symlink on a minix filesystem.
*
* The patch removed .write_begin from minix_aops during the iomap
* conversion, but symlink inodes still use minix_aops. page_symlink()
* dereferences the NULL function pointer.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/wait.h>
#define MINIX_IMAGE "/tmp/minix_test.img"
#define MINIX_MOUNT "/tmp/minix_mount"
#define LOOP_DEV "/dev/loop0"
#define IMAGE_SIZE (1024 * 1024)
int main(void)
{
int fd;
char buf[IMAGE_SIZE];
pid_t pid;
int status;
if (geteuid() != 0) {
fprintf(stderr, "This PoC requires root privileges.\n");
return 1;
}
mkdir(MINIX_MOUNT, 0755);
/* Create and format a minix v1 filesystem image */
printf("Creating minix filesystem image...\n");
memset(buf, 0, sizeof(buf));
fd = open(MINIX_IMAGE, O_CREAT | O_RDWR | O_TRUNC, 0644);
if (fd < 0) { perror("open image"); return 1; }
write(fd, buf, sizeof(buf));
close(fd);
pid = fork();
if (pid == 0) {
execlp("mkfs.minix", "mkfs.minix", "-1", MINIX_IMAGE, NULL);
perror("mkfs.minix"); _exit(1);
}
waitpid(pid, &status, 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
fprintf(stderr, "mkfs.minix failed\n");
unlink(MINIX_IMAGE); return 1;
}
/* Set up loop device and mount */
printf("Setting up loop device and mounting...\n");
pid = fork();
if (pid == 0) {
execlp("losetup", "losetup", LOOP_DEV, MINIX_IMAGE, NULL);
_exit(1);
}
waitpid(pid, &status, 0);
if (mount(LOOP_DEV, MINIX_MOUNT, "minix", 0, NULL) < 0) {
perror("mount");
pid = fork();
if (pid == 0) { execlp("losetup", "losetup", "-d", LOOP_DEV, NULL); _exit(1); }
waitpid(pid, &status, 0);
unlink(MINIX_IMAGE); return 1;
}
/*
* TRIGGER: symlink() -> minix_symlink() -> page_symlink()
* -> aops->write_begin(NULL, ...) -> NULL deref -> RIP 0x0
*/
printf("Creating symlink to trigger NULL dereference...\n");
symlink("/this/is/a/test/symlink/target", MINIX_MOUNT "/mylink");
/* Cleanup (unreachable if kernel panicked) */
umount2(MINIX_MOUNT, MNT_DETACH);
pid = fork();
if (pid == 0) { execlp("losetup", "losetup", "-d", LOOP_DEV, NULL); _exit(1); }
waitpid(pid, &status, 0);
unlink(MINIX_IMAGE);
printf("PoC completed. Check dmesg for crash evidence.\n");
return 0;
}
I see you already have test patches via the syzbot test infrastructure
that add write_begin/write_end back to minix_aops -- great! This PoC
can serve as a concrete reproducer to verify the fix.
Thanks,
Xiao