[Bug report] ext4: enabling casefold online leaves s_encoding unset - null-ptr-deref in utf8nlookup()

From: Guikun Yang

Date: Tue Nov 10 2026 - 00:33:52 EST


Hi,

EXT4_IOC_SET_TUNE_SB_PARAM can enable the casefold feature on a live
filesystem, but nothing rebuilds the runtime state that the feature
depends on. A lookup in an already casefolded directory then
dereferences a NULL unicode map.

## The gap

The ioctl allows CASEFOLD to be set online (fs/ext4/ioctl.c):

#define EXT4_TUNE_SET_INCOMPAT_SUPP \
(EXT4_FEATURE_INCOMPAT_EXTENTS | ... | \
EXT4_FEATURE_INCOMPAT_CASEFOLD)

and the callback only writes it into the in-core superblock copy
(ext4_sb_setparams(), fs/ext4/ioctl.c):

if (params->set_flags & EXT4_TUNE_FL_ENCODING)
es->s_encoding = cpu_to_le16(params->encoding);
...
if (params->set_flags & EXT4_TUNE_FL_EDIT_FEATURES) {
es->s_feature_incompat |=
cpu_to_le32(params->set_feature_incompat_mask);
...
}

es is sbi->s_es, so ext4_has_feature_casefold(sb) becomes true
immediately. sb->s_encoding, however, is only ever assigned in
ext4_encoding_init(), whose single call site in the tree is the mount
path (super.c:5468). The ioctl does not call it, and nothing else does
either.

After that, the inode-level check in ext4_iget() no longer rejects a
casefolded directory, the inode gets S_CASEFOLD, and a lookup in it
reaches

utf8_casefold(dir->i_sb->s_encoding, ...)

with a NULL map, i.e. the same null-ptr-deref in utf8nlookup() as
reported previously:

Oops: general protection fault, probably for non-canonical address
KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
RIP: 0010:utf8nlookup+0x38/0x890
utf8byte -> utf8_casefold -> ext4_fname_setup_ci_filename
-> ext4_fname_prepare_lookup -> ext4_lookup

## Why this path is not covered by the earlier fix

The same crash was reported by syzbot in 2023 (9cf75dc5) and was closed
by 6f861765464f ("fs: Block writes to mounted block devices"), with the
comment that this series would "ultimately be the fix for this class of
issue". That fix blocks user writes to the page cache of a mounted
device. This ioctl does not write through that path - the kernel itself
changes the feature bits, inside the fs code, with the write protection
in place. (Journal replay reaches the same state and is reported
separately as <c15-ext4-replay-casefold-null-20261103@xxxxxx>.)

The removal of the NULL checks in b81427939590 ("ext4: remove redundant
checks of s_encoding") rests on the invariant that the feature bit and
sb->s_encoding are always consistent. This path is one that breaks that
invariant.

## Reproducer

Needs CAP_SYS_ADMIN, but no crafted image:

1. mount a filesystem whose superblock does not have the casefold
feature, containing a directory whose inode carries EXT4_CASEFOLD_FL
2. GET_TUNE_SB_PARAM to read the current feature set
3. set EXT4_FEATURE_INCOMPAT_CASEFOLD in that set and SET_TUNE_SB_PARAM
4. lookup an entry in the directory

Two things to know about building it:

- set_feature_*_mask in the SET request is the complete target set, not
a delta; sending only the new bit makes clear_feature_*_mask clear
everything else and the call fails with -EOPNOTSUPP. Read the set
first, then OR in the bit.
- the ioctl writes the feature into the superblock, so the second mount
of the same image initialises the encoding normally and does not
crash. Each run needs a fresh copy of the image.

Deterministic 3/3; a control run with the ioctl skipped mounts and reads
the directory without any report. The reproducer is available on
request.

## Suggested fix

Either rebuild the runtime state when the feature is enabled online
(call the encoding setup after a successful feature change, and handle
failure by rolling the superblock back), or restore a NULL check on the
namei side so a casefolded inode with no encoding fails cleanly instead
of dereferencing a NULL map. The first is the general fix; the second
also covers the journal replay path.

Thanks,
Guikun Yang