* [PATCH v2 0/2] erofs-utils: lib: fix ZSTD decompression safety issues
@ 2026-03-17 4:55 Utkal Singh
2026-03-17 4:55 ` [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression Utkal Singh
2026-03-17 4:55 ` [PATCH v2 2/2] erofs-utils: lib: return error on ZSTD decompression length mismatch Utkal Singh
0 siblings, 2 replies; 5+ messages in thread
From: Utkal Singh @ 2026-03-17 4:55 UTC (permalink / raw)
To: linux-erofs; +Cc: hsiangkao, yifan.yfzhao, singhutkal015
Changes since v1:
- Added reproducer per maintainer request
This series fixes two issues in z_erofs_decompress_zstd() that can
be triggered by crafted EROFS filesystem images.
Patch 1/2 validates ZSTD frame content size against decodedlength.
Patch 2/2 fixes a missing error return on decompression length mismatch.
Utkal Singh (2):
erofs-utils: lib: validate ZSTD frame content size in decompression
erofs-utils: lib: return error on ZSTD decompression length mismatch
lib/decompress.c | 8 ++++++++
1 file changed, 8 insertions(+)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression
2026-03-17 4:55 [PATCH v2 0/2] erofs-utils: lib: fix ZSTD decompression safety issues Utkal Singh
@ 2026-03-17 4:55 ` Utkal Singh
2026-03-17 9:53 ` Gao Xiang
2026-03-17 4:55 ` [PATCH v2 2/2] erofs-utils: lib: return error on ZSTD decompression length mismatch Utkal Singh
1 sibling, 1 reply; 5+ messages in thread
From: Utkal Singh @ 2026-03-17 4:55 UTC (permalink / raw)
To: linux-erofs; +Cc: hsiangkao, yifan.yfzhao, singhutkal015
ZSTD_getFrameContentSize() reads the content size from the ZSTD
frame header in the compressed data. This is untrusted on-disk
metadata, independent from the extent map that provides
rq->decodedlength via z_erofs_map_blocks_iter().
A crafted EROFS image can set the extent map to claim a decoded
length larger than the actual ZSTD frame content size. When this
happens, a buffer of the (smaller) frame content size is allocated
and decompressed into, but the subsequent memcpy copies
rq->decodedlength bytes from it -- a potential out-of-bounds read.
Additionally, the ZSTD_getDecompressedSize() legacy fallback
returns 0 for frames without a content size field. This leads to
malloc(0) followed by out-of-bounds access on the returned pointer.
Reject frames where the reported content size is zero or smaller
than the expected decoded length.
Reproducer:
mkdir testdir
python3 -c "open('testdir/f','wb').write(b'A'*131072)"
mkfs.erofs -zzstd test.erofs testdir/
python3 -c "d=bytearray(open('test.erofs','rb').read());\
p=d.find(b'\x28\xb5\x2f\xfd');d[p+4]=0x20;d[p+5]=0x01;\
open('test.erofs','wb').write(d)"
fsck.erofs --extract=out test.erofs
# Expected: ZSTD frame content size 1 < decoded length 131072
Signed-off-by: Utkal Singh <singhutkal015@gmail.com>
---
lib/decompress.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/lib/decompress.c b/lib/decompress.c
index 3e7a173..fb81039 100644
--- a/lib/decompress.c
+++ b/lib/decompress.c
@@ -48,7 +48,14 @@ static int z_erofs_decompress_zstd(struct z_erofs_decompress_req *rq)
#else
total = ZSTD_getDecompressedSize(src + inputmargin,
rq->inputsize - inputmargin);
+ if (!total)
+ return -EFSCORRUPTED;
#endif
+ if (total < rq->decodedlength) {
+ erofs_err("ZSTD frame content size %llu < decoded length %u",
+ total, rq->decodedlength);
+ return -EFSCORRUPTED;
+ }
if (rq->decodedskip || total != rq->decodedlength) {
buff = malloc(total);
if (!buff)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 2/2] erofs-utils: lib: return error on ZSTD decompression length mismatch
2026-03-17 4:55 [PATCH v2 0/2] erofs-utils: lib: fix ZSTD decompression safety issues Utkal Singh
2026-03-17 4:55 ` [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression Utkal Singh
@ 2026-03-17 4:55 ` Utkal Singh
1 sibling, 0 replies; 5+ messages in thread
From: Utkal Singh @ 2026-03-17 4:55 UTC (permalink / raw)
To: linux-erofs; +Cc: hsiangkao, yifan.yfzhao, singhutkal015
When ZSTD_decompress() succeeds but produces a different number of
bytes than expected, the code logs an error and jumps to cleanup.
However, it does not set ret to an error code. Since ret still
holds the positive ZSTD output size, the caller treats it as
success via the 'if (ret < 0)' check in z_erofs_read_one_data(),
causing silently corrupted data to be returned.
Set ret to -EIO before jumping to cleanup, consistent with the
ZSTD_isError() error handling path above.
Signed-off-by: Utkal Singh <singhutkal015@gmail.com>
---
lib/decompress.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/decompress.c b/lib/decompress.c
index fb81039..a27881c 100644
--- a/lib/decompress.c
+++ b/lib/decompress.c
@@ -75,6 +75,7 @@ static int z_erofs_decompress_zstd(struct z_erofs_decompress_req *rq)
if (ret != (int)total) {
erofs_err("ZSTD decompress length mismatch %d, expected %d",
ret, total);
+ ret = -EIO;
goto out;
}
if (rq->decodedskip || total != rq->decodedlength)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression
2026-03-17 4:55 ` [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression Utkal Singh
@ 2026-03-17 9:53 ` Gao Xiang
2026-03-17 10:00 ` Utkal Singh
0 siblings, 1 reply; 5+ messages in thread
From: Gao Xiang @ 2026-03-17 9:53 UTC (permalink / raw)
To: Utkal Singh, linux-erofs; +Cc: yifan.yfzhao
On 2026/3/17 12:55, Utkal Singh wrote:
> ZSTD_getFrameContentSize() reads the content size from the ZSTD
> frame header in the compressed data. This is untrusted on-disk
> metadata, independent from the extent map that provides
> rq->decodedlength via z_erofs_map_blocks_iter().
>
> A crafted EROFS image can set the extent map to claim a decoded
> length larger than the actual ZSTD frame content size. When this
> happens, a buffer of the (smaller) frame content size is allocated
> and decompressed into, but the subsequent memcpy copies
> rq->decodedlength bytes from it -- a potential out-of-bounds read.
>
> Additionally, the ZSTD_getDecompressedSize() legacy fallback
> returns 0 for frames without a content size field. This leads to
> malloc(0) followed by out-of-bounds access on the returned pointer.
>
> Reject frames where the reported content size is zero or smaller
> than the expected decoded length.
>
> Reproducer:
> mkdir testdir
> python3 -c "open('testdir/f','wb').write(b'A'*131072)"
> mkfs.erofs -zzstd test.erofs testdir/
> python3 -c "d=bytearray(open('test.erofs','rb').read());\
> p=d.find(b'\x28\xb5\x2f\xfd');d[p+4]=0x20;d[p+5]=0x01;\
> open('test.erofs','wb').write(d)"
> fsck.erofs --extract=out test.erofs
> # Expected: ZSTD frame content size 1 < decoded length 131072
>
> Signed-off-by: Utkal Singh <singhutkal015@gmail.com>
> ---
> lib/decompress.c | 7 +++++++
> 1 file changed, 7 insertions(+)
>
> diff --git a/lib/decompress.c b/lib/decompress.c
> index 3e7a173..fb81039 100644
> --- a/lib/decompress.c
> +++ b/lib/decompress.c
> @@ -48,7 +48,14 @@ static int z_erofs_decompress_zstd(struct z_erofs_decompress_req *rq)
> #else
> total = ZSTD_getDecompressedSize(src + inputmargin,
> rq->inputsize - inputmargin);
> + if (!total)
> + return -EFSCORRUPTED;
hmm, that is the difference between the kernel and erofs-utils
implementation.
the kernel uses zstd streaming APIs, so it won't malloc()
a new buffer in advance, actually I think erofs-utils should
switch to streaming APIs too, in order to avoid
ZSTD_getFrameContentSize() and ZSTD_getDecompressedSize()
dependencies as you said in the commit message.
Thanks,
Gao Xiang
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression
2026-03-17 9:53 ` Gao Xiang
@ 2026-03-17 10:00 ` Utkal Singh
0 siblings, 0 replies; 5+ messages in thread
From: Utkal Singh @ 2026-03-17 10:00 UTC (permalink / raw)
To: Gao Xiang; +Cc: linux-erofs, yifan.yfzhao
[-- Attachment #1: Type: text/plain, Size: 2856 bytes --]
Thanks for the direction, Gao Xiang.
Understood — switching to ZSTD streaming APIs (ZSTD_decompressStream)
would eliminate the ZSTD_getFrameContentSize() /
ZSTD_getDecompressedSize() dependency entirely and align
erofs-utils with the kernel implementation.
I'll work on a v3 using the streaming approach.
- Utkal
On Tue, 17 Mar 2026 at 15:23, Gao Xiang <hsiangkao@linux.alibaba.com> wrote:
>
>
> On 2026/3/17 12:55, Utkal Singh wrote:
> > ZSTD_getFrameContentSize() reads the content size from the ZSTD
> > frame header in the compressed data. This is untrusted on-disk
> > metadata, independent from the extent map that provides
> > rq->decodedlength via z_erofs_map_blocks_iter().
> >
> > A crafted EROFS image can set the extent map to claim a decoded
> > length larger than the actual ZSTD frame content size. When this
> > happens, a buffer of the (smaller) frame content size is allocated
> > and decompressed into, but the subsequent memcpy copies
> > rq->decodedlength bytes from it -- a potential out-of-bounds read.
> >
> > Additionally, the ZSTD_getDecompressedSize() legacy fallback
> > returns 0 for frames without a content size field. This leads to
> > malloc(0) followed by out-of-bounds access on the returned pointer.
> >
> > Reject frames where the reported content size is zero or smaller
> > than the expected decoded length.
> >
> > Reproducer:
> > mkdir testdir
> > python3 -c "open('testdir/f','wb').write(b'A'*131072)"
> > mkfs.erofs -zzstd test.erofs testdir/
> > python3 -c "d=bytearray(open('test.erofs','rb').read());\
> > p=d.find(b'\x28\xb5\x2f\xfd');d[p+4]=0x20;d[p+5]=0x01;\
> > open('test.erofs','wb').write(d)"
> > fsck.erofs --extract=out test.erofs
> > # Expected: ZSTD frame content size 1 < decoded length 131072
> >
> > Signed-off-by: Utkal Singh <singhutkal015@gmail.com>
> > ---
> > lib/decompress.c | 7 +++++++
> > 1 file changed, 7 insertions(+)
> >
> > diff --git a/lib/decompress.c b/lib/decompress.c
> > index 3e7a173..fb81039 100644
> > --- a/lib/decompress.c
> > +++ b/lib/decompress.c
> > @@ -48,7 +48,14 @@ static int z_erofs_decompress_zstd(struct
> z_erofs_decompress_req *rq)
> > #else
> > total = ZSTD_getDecompressedSize(src + inputmargin,
> > rq->inputsize - inputmargin);
> > + if (!total)
> > + return -EFSCORRUPTED;
>
> hmm, that is the difference between the kernel and erofs-utils
> implementation.
>
> the kernel uses zstd streaming APIs, so it won't malloc()
> a new buffer in advance, actually I think erofs-utils should
> switch to streaming APIs too, in order to avoid
>
> ZSTD_getFrameContentSize() and ZSTD_getDecompressedSize()
>
> dependencies as you said in the commit message.
>
> Thanks,
> Gao Xiang
>
>
[-- Attachment #2: Type: text/html, Size: 3670 bytes --]
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-03-17 10:01 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-17 4:55 [PATCH v2 0/2] erofs-utils: lib: fix ZSTD decompression safety issues Utkal Singh
2026-03-17 4:55 ` [PATCH v2 1/2] erofs-utils: lib: validate ZSTD frame content size in decompression Utkal Singh
2026-03-17 9:53 ` Gao Xiang
2026-03-17 10:00 ` Utkal Singh
2026-03-17 4:55 ` [PATCH v2 2/2] erofs-utils: lib: return error on ZSTD decompression length mismatch Utkal Singh
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox