* [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
* 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
* [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
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