ANT-2026-5ETTYD84 · libjpeg-turbo/libjpeg-turbo
use-after-free low
Severity Claude high · Security research firm low (revised; sealed as high) · Maintainer -
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.
ANT-2026-5ETTYD84: use-after-free at ../jdsample.c:494:35
When a jpeg_decompress_struct is reused across two sessions, the first session allocates cinfo->upsample in JPOOL_IMAGE and jpeg_abort_decompress() frees that pool while leaving the pointer dangling. A second session started with raw_data_out=TRUE and buffered_image=TRUE skips upsampler reinitialization in master_selection (gated on !raw_data_out). Calling jpeg_crop_scanline() in DSTATE_BUFIMAGE then enters the reinit_upsampler path and jinit_upsampler() writes function pointers and integers through the stale cinfo->upsample into freed heap memory. Any subsampled JPEG suffices as input; the trigger is the API call sequence rather than crafted image data.
Target
Project: libjpeg-turbo
Commit: 9135da1d
Location: jdsample.c:494
Technical Details
ASAN: "heap-use-after-free ... WRITE of size 4 at 0x6290000021f8 ... in jinit_upsampler jdsample.c:494:35". The root cause is that jpeg_abort()/free_pool releases the JPOOL_IMAGE allocation backing cinfo->upsample without nulling the pointer, and the raw_data_out codepath in master selection never reallocates it, so jpeg_crop_scanline()'s no_alloc call into _jinit_upsampler writes to freed memory.
Crash trace (truncated — full trace in attached crash.log):
==2084==ERROR: AddressSanitizer: heap-use-after-free on address 0x6290000021f8 at pc 0x55eaee710c98 bp 0x7fff7e386870 sp 0x7fff7e386868
WRITE of size 4 at 0x6290000021f8 thread T0
#0 0x55eaee710c97 in jinit_upsampler /workspace/repo/src/wrapper/../jdsample.c:494:35
#1 0x55eaee61a430 in jpeg_crop_scanline /workspace/repo/src/wrapper/../jdapistd.c:300:5
#2 0x55eaee60bb91 in main /tmp/uaf_crop.c:115:3
freed by thread T0 here:
#0 0x55eaee5cff02 in free
#1 0x55eaee73ebb3 in jpeg_free_small /workspace/repo/src/jmemnobs.c:40:3
#2 0x55eaee73ad1a in free_pool /workspace/repo/src/jmemmgr.c:1156:5
#3 0x55eaee7a3218 in jpeg_abort /workspace/repo/src/jcomapi.c:48:5
#4 0x55eaee60d730 in jpeg_abort_decompress /workspace/repo/src/jdapimin.c:128:3
previously allocated by thread T0 here:
#0 0x55eaee5d01ae in __interceptor_malloc
#1 0x55eaee73eb83 in jpeg_get_small /workspace/repo/src/jmemnobs.c:34:18
#2 0x55eaee72f5df in alloc_small /workspace/repo/src/jmemmgr.c:318:33
#3 0x55eaee65ab01 in get_sof /workspace/repo/src/jdmarker.c:283:47
SUMMARY: AddressSanitizer: heap-use-after-free /workspace/repo/src/wrapper/../jdsample.c:494:35 in jinit_upsampler
Reproduction
Reproduce against the commit listed above as described under Technical Details.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-5ETTYD84.
Reference: ANT-2026-5ETTYD84
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ada Logics. The severity shown is the firm's current assessment; the report was sealed with the firm's earlier assessment of high.
- Verdict
- true positive
- Severity
- low (revised; sealed as high)
The change that resolved this finding.
diff --git a/ChangeLog.md b/ChangeLog.md
index d78803272..0b3614509 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -15,6 +15,19 @@ security risk, since `tj3SaveImage*()` is not exposed to arbitrary external
input data and since a caller that abused the API in the aforementioned manner
could never work properly.
+3. Hardened the libjpeg API against hypothetical applications that may
+erroneously call `jpeg_crop_scanline()` with buffered-image mode and raw data
+output enabled. `jpeg_crop_scanline()` does not work with raw data output, but
+due to an oversight, it did not throw an error if both buffered-image mode and
+raw data output were enabled. If a hypothetical application aborted a normal
+decompression operation without reading any scanlines, started a new
+decompression operation using the same libjpeg instance with buffered-image
+mode and raw data output enabled, then called `jpeg_crop_scanline()` with
+arguments that would have caused any of the component planes to be cropped to a
+width of 1 sample, `jpeg_crop_scanline()` would have used freed memory.
+However, this did not likely pose a security risk, since an application that
+abused the API in the aforementioned manner could never work properly.
+
3.1.90 (3.2 beta1)
==================
diff --git a/doc/libjpeg.txt b/doc/libjpeg.txt
index c32efee66..1cfbd579f 100644
--- a/doc/libjpeg.txt
+++ b/doc/libjpeg.txt
@@ -867,7 +867,7 @@ to jpeg*_read_scanlines() (since it will actually call jpeg*_read_scanlines().)
This function provides application programmers with the ability to decompress
only a portion of each row in the JPEG image. It must be called after
jpeg_start_decompress() and before any calls to jpeg*_read_scanlines() or
-jpeg*_skip_scanlines().
+jpeg*_skip_scanlines(). (It cannot be used with raw data output.)
If xoffset and width do not form a valid subset of the image row, then this
function will generate an error. Note that if the output image is scaled, then
diff --git a/src/jdapistd.c b/src/jdapistd.c
index 78ef7ac07..8f5f85a8d 100644
--- a/src/jdapistd.c
+++ b/src/jdapistd.c
@@ -200,7 +200,7 @@ _jpeg_crop_scanline(j_decompress_ptr cinfo, JDIMENSION *xoffset,
if (cinfo->data_precision != BITS_IN_JSAMPLE)
ERREXIT1(cinfo, JERR_BAD_PRECISION, cinfo->data_precision);
- if (cinfo->master->lossless)
+ if (cinfo->master->lossless || cinfo->raw_data_out)
ERREXIT(cinfo, JERR_NOTIMPL);
if ((cinfo->global_state != DSTATE_SCANNING &&https://github.com/libjpeg-turbo/libjpeg-turbo/commit/0ff47f0b46924ea1aaf04092ba1d0c0f83f93b10
Dates from discovery through public reveal.
- 2026-03-29 Reported to tracker
- 2026-04-09 Sent to maintainer
- 2026-05-07 Maintainer acknowledged
- 2026-06-30 Patch released
- 2026-07-08 Publicly revealed
SHA-3-512 hash:
0cc596b99dfbdc95c30143e64f6157d6dac36adb067b98c778ed3575b081d40dcb23856f9e9f1e8fe495e08c14835ef69fcc8f6bbf34c6625dc80c2204eb09ef
Committed 2026-05-07 07:53 PT
Revealed 2026-07-08 15:53 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-5ETTYD84",
"bug_class": "use-after-free",
"claude_severity": "high",
"commit_sha": "9135da1d",
"created_at": "2026-03-29T19:26:26+00:00",
"description": "When a jpeg_decompress_struct is reused across two sessions, the first session allocates cinfo->upsample in JPOOL_IMAGE and jpeg_abort_decompress() frees that pool while leaving the pointer dangling. A second session started with raw_data_out=TRUE and buffered_image=TRUE skips upsampler reinitialization in master_selection (gated on !raw_data_out). Calling jpeg_crop_scanline() in DSTATE_BUFIMAGE then enters the reinit_upsampler path and jinit_upsampler() writes function pointers and integers through the stale cinfo->upsample into freed heap memory. Any subsampled JPEG suffices as input; the trigger is the API call sequence rather than crafted image data.",
"discovered_at": null,
"location": "jdsample.c:494",
"poc_sha256": null,
"preimage_version": 1,
"project": "libjpeg-turbo",
"reproduction": null,
"technical_details": "ASAN: \"heap-use-after-free ... WRITE of size 4 at 0x6290000021f8 ... in jinit_upsampler jdsample.c:494:35\". The root cause is that jpeg_abort()/free_pool releases the JPOOL_IMAGE allocation backing cinfo->upsample without nulling the pointer, and the raw_data_out codepath in master selection never reallocates it, so jpeg_crop_scanline()'s no_alloc call into _jinit_upsampler writes to freed memory.",
"title": "use-after-free at ../jdsample.c:494:35",
"vendor_severity": "high"
}