[Bug report] ext4: journal replay overwrites the validated superblock, casefold feature bit set after s_encoding decision -> null-ptr-deref in utf8_casefold
From: Guikun Yang
Date: Tue Nov 03 2026 - 10:41:43 EST
Hi,
Mounting a crafted ext4 image with metadata_csum and a journal checksum
enabled ends in a general protection fault during a later lookup. The
chain is that journal replay overwrites the superblock after
ext4_fill_super() has already made a decision based on the previous
contents, so the two ends of the mount path disagree about the casefold
feature.
general protection fault, probably for non-canonical address
KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
RIP: 0010:utf8nlookup+0x38/0x890
Call Trace:
utf8byte+0x1c0/0x1320
utf8_casefold+0x161/0x230
ext4_fname_setup_ci_filename+0x17f/0x4d0
ext4_lookup+0x161/0x710
__lookup_slow+0x156/0x320
RBX is 0 (the unicode_map) and RDI is 0x18 (&um->tables), so
utf8_casefold() was called with a NULL sb->s_encoding.
## The chain
1. ext4_encoding_init() (super.c:5468) runs before
ext4_load_and_init_journal() (super.c:5576). With no casefold
feature in the on-disk superblock it returns early and
sb->s_encoding stays NULL.
2. ext4_check_blockref() has an exemption for the journal inode
(block_validity.c:356):
if (journal && inode == journal->j_inode)
return 0;
so the journal inode's extent can point at any free blocks without
failing the block reference walk.
3. A dirty journal is constructed in those free blocks whose single
data block is a copy of block 0 with the CASEFOLD incompatible
feature bit set. Every checksum is recomputed: the superblock
checksum, the journal superblock checksum, the block tag checksum
and the commit block checksum, plus the inode checksums of the
journal inode and the target directory. Nothing on disk is invalid.
4. On mount, jbd2 replay writes that block back to block 0 via
__getblk(journal->j_fs_dev, ...) + memcpy (recovery.c:551). The
in-memory ext4_super_block now advertises casefold, but
ext4_encoding_init() has already run and sb->s_encoding is still
NULL.
5. ext4_iget() rejects a casefold flag without the casefold feature
(inode.c:6568). The root inode is read before replay and would
fail, so the flag is placed on a subdirectory instead; its inode is
only read on first lookup, after replay has set the feature bit in
memory, and the check passes.
6. Any lookup inside that directory takes ext4_fname_setup_ci_filename
(namei.c:1370), which checks IS_CASEFOLD(dir) - an inode flag,
independent of the feature bit - and calls
utf8_casefold(dir->i_sb->s_encoding, ...) with NULL.
## Observed
Deterministic, 3/3 on a QEMU x86_64 kernel with KASAN. The trigger is
two syscalls after the image is built:
mount("/dev/vda", "/mnt", "ext4", 0, NULL);
stat("/mnt/d1/nonexistent", &st); /* GPF here */
Reproducer notes that matter for reproduction:
- The superblock must have NEEDS_RECOVERY set, otherwise
ext4_load_journal() calls jbd2_journal_wipe() before
jbd2_journal_load() and the crafted journal is discarded.
- The casefold flag cannot go on the root inode; ext4_iget() rejects
it during mount because the feature bit is not visible yet.
## The larger issue
The crash is one consequence of a more general property: replay writes
into the page cache of blocks that ext4_fill_super() has already
read, checksummed and acted on. s_checksum verifies the on-disk
contents before replay, not after, and ext4_encoding_init() is one of
several decisions made from the pre-replay superblock. Anything read
from the superblock before ext4_load_and_init_journal() and still used
after it is a candidate for the same disagreement.
The reproducer (image builder with checksum recomputation, forged
journal builder, ready-made image and crash log) is available on
request.
Tested on 7.3.0 with CONFIG_UNICODE; the ordering of
ext4_encoding_init() and ext4_load_and_init_journal() and the NULL
use in namei.c are unchanged in current mainline.
Thanks,
Guikun Yang