[BUG] erofs: LZMA stream state NULL deref after failed dict grow

From: Qingyu Zhang

Date: Tue Sep 01 2026 - 22:05:06 EST


Hello,

z_erofs_load_lzma_config() can leave strm->state == NULL while still
publishing z_erofs_lzma_max_dictsize. The next mount of the same
image skips realloc and decompress NULL-derefs.

Type: null-pointer dereference

* Summary

for (strm = head; strm; strm = strm->next) {
if (strm->state)
xz_dec_microlzma_end(strm->state);
strm->state = xz_dec_microlzma_alloc(..., dict_size);
if (!strm->state)
err = -ENOMEM;
}
...
z_erofs_lzma_max_dictsize = dict_size; /* even if err */

On alloc failure the old state is already gone. The next mount sees
max_dictsize >= dict_size and returns 0 without retrying. Then:

xz_dec_microlzma_reset(strm->state, ...); /* state is NULL */

* Affected

622ceaddb764. Needs CONFIG_EROFS_FS_ZIP_LZMA, a microlzma image,
and a failed xz_dec_microlzma_alloc (fault injection or memory
pressure). KASAN recommended.

* Reproduction

1. mkfs.erofs -z lzma,dictsize=4096 lzma.img dir/
2. Force xz_dec_microlzma_alloc() to fail on first mount
(poc/fail_lzma.c: kprobe sets dict_size=0).
3. First mount returns -ENOMEM.
4. Disarm the probe, mount the same image again (succeeds via the
max_dictsize early-out).
5. cat /mnt/payload

KASAN: null-ptr-deref in xz_dec_microlzma_reset from
z_erofs_lzma_decompress.

PoC: poc/run.sh. Use lzma_streams=1 to keep the pool to one stream.

* Expected

Failed grow keeps the previous xz state (or retries next mount) and
does not bump z_erofs_lzma_max_dictsize.

* Actual

NULL state + published max_dictsize -> NPD on the next read.

Please consider the suggested patch.

Thanks.

Suggested patch
```
diff --git a/fs/erofs/decompressor_lzma.c b/fs/erofs/decompressor_lzma.c
index 6b0cdb446c6a..3719c6f83f8a 100644
--- a/fs/erofs/decompressor_lzma.c
+++ b/fs/erofs/decompressor_lzma.c
@@ -128,11 +128,16 @@ static int z_erofs_load_lzma_config(struct
super_block *sb,
err = 0;
/* 2. walk each isolated stream and grow max dict_size if needed */
for (strm = head; strm; strm = strm->next) {
+ struct xz_dec_microlzma *state;
+
+ state = xz_dec_microlzma_alloc(XZ_PREALLOC, dict_size);
+ if (!state) {
+ err = -ENOMEM;
+ continue;
+ }
if (strm->state)
xz_dec_microlzma_end(strm->state);
- strm->state = xz_dec_microlzma_alloc(XZ_PREALLOC, dict_size);
- if (!strm->state)
- err = -ENOMEM;
+ strm->state = state;
}

/* 3. push back all to the global list and update max dict_size */
@@ -142,7 +147,8 @@ static int z_erofs_load_lzma_config(struct super_block *sb,
spin_unlock(&z_erofs_lzma_lock);
wake_up_all(&z_erofs_lzma_wq);

- z_erofs_lzma_max_dictsize = dict_size;
+ if (!err)
+ z_erofs_lzma_max_dictsize = dict_size;
mutex_unlock(&lzma_resize_mutex);
return err;
}
```