CVE-2026-21352 AI-generated analysis
platform/external/dng_sdk (Adobe DNG SDK: JXL codec glue, opcode processing, memory blocks, safe arithmetic)
Updates external/dng_sdk from 1.7.1 build 2410 to 2471. The update adds integer-overflow checks around JXL decode/encode buffer sizing and allocations, fixes an off-by-one table index in dng_opcode_MapTable::ReplicateLastEntry, and guards several divisions by zero that occur when blackLevel is 65535.
Root cause
Several size and coordinate computations used unchecked arithmetic. In dng_jxl.cpp, jxl_image_chunk_reader and jxl_buffer_chunk_reader built a dng_pixel_buffer from size_t xpos/ypos/xsize/ysize by truncating them to int32 and adding them without overflow checks (fArea.b/r and, in the buffer reader, fArea.t/l). fRowStep was computed with SafeUint32Mult. dng_jxl_decoder::Decode computed fRowStep and the 64-bit bytesNeeded with plain multiplication, and cast the JxlDecoderImageOutBufferSize result to uint32 for host.Allocate without a range check. EncodeJXL_Common computed previewBytesNeeded with plain multiplication. jxl_memory_block added 160 to bytesNeeded without a wrap check and did not check allocator.Malloc for a null return. dng_ref_counted_block::Allocate called malloc(size + sizeof(header)) without a wrap check. In dng_misc_opcodes.cpp, dng_opcode_MapTable::ReplicateLastEntry read table[fCount] instead of table[fCount - 1]. Because fCount is validated to 1..0x10000 and the table is allocated as 0x10000 uint16 entries, fCount == 0x10000 reads one element past the allocation and smaller counts read an unwritten slot. dng_opcode_MapTable::Prepare, dng_opcode_GainMap::ProcessArea, RefVignette32 and RefBaselineMapPoly32 divided by (65535 - blackLevel) or by blackScale2, both of which are zero when blackLevel == 65535. The material does not identify which of these hunks is the one tracked as CVE-2026-21352 (the commit references two bugs, 483697751 and 483693973).
How the fix works
- source/dng_jxl.cpp:jxl_memory_block ctor: computes mallocSize = 160u + bytesNeeded into a size_t and calls ThrowOverflow() if mallocSize <= bytesNeeded (wrap). It now calls ThrowMemoryFull() if allocator.Malloc returns null, instead of passing the null pointer to DNG_ALIGN_SIMD.
- source/dng_jxl.cpp:jxl_image_chunk_reader and jxl_buffer_chunk_reader (get_color_channel_data_at handlers): reject xpos/ypos/xsize/ysize > 0x7fffffff with ThrowOverflow("coordinate too large"), compute the fArea bounds with SafeInt32Add, add DNG_REQUIRE checks that fPlanes and fArea.W() are <= 0x7fffffff, and compute fRowStep with SafeInt32Mult instead of SafeUint32Mult. The debug printf format changes to %zu.
- source/dng_jxl.cpp:EncodeJXL_Common: adds DNG_REQUIRE fArea.W() <= 0x7fffffff, computes fRowStep with SafeInt32Mult, and computes previewBytesNeeded with 3-arg SafeUint32Mult before host.Allocate.
- source/dng_jxl.cpp:dng_jxl_decoder::Decode: adds DNG_REQUIRE basicInfo.xsize/ysize <= 0x7fffffff before assigning to dng_point. buffer.fRowStep and wholeBuffer.fRowStep use SafeInt32Mult, and bytesNeeded uses the new 3-arg SafeInt64Mult. Adds DNG_REQUIRE size <= 0xffffffff before casting the JxlDecoderImageOutBufferSize result to uint32 for host.Allocate.
- source/dng_safe_arithmetic.h/.cpp: adds int64 SafeInt64Mult(int64, int64, int64), which chains the two-arg SafeInt64Mult and throws on overflow. Removes the __ANDROID__ special case that forced __USE_BUILTIN_SMULL_OVERFLOW to 0, so Android clang builds now use __builtin_smull_overflow.
- source/dng_ref_counted_block.cpp:dng_ref_counted_block::Allocate: computes size + sizeof(header) into a size_t mallocSize and calls ThrowOverflow() if mallocSize <= size, before malloc.
- source/dng_misc_opcodes.cpp:dng_opcode_MapTable::ReplicateLastEntry: reads lastEntry from table[fCount - 1] instead of table[fCount].
- source/dng_misc_opcodes.cpp:dng_opcode_MapTable::Prepare: srcScale is 0.0 when blackLevel >= 65535 instead of dividing by zero.
- source/dng_gain_map.cpp:dng_opcode_GainMap::ProcessArea, source/dng_reference.cpp:RefVignette32 and RefBaselineMapPoly32: blackScale1 is 0.0 when blackScale2 == 0.0 instead of 1.0f / 0.
- source/dng_flags.h and source/dng_tag_codes.h: build number 2410 -> 2471 and a new tcWebP enum value (non-security bookkeeping).
Completeness
- The diff converts the JXL computations it touches (fArea bounds, fRowStep, bytesNeeded, previewBytesNeeded, the output buffer size cast) to Safe* helpers or explicit range checks. Both chunk reader classes get the same treatment. The rest of those functions is not shown.
- jxl_memory_block adds the null check only on the Malloc branch (bytesNeeded > 0x80000000). The else branch relies on allocator.Allocate, whose failure behavior is not shown in the material.
- The dng_ref_counted_block::Allocate check (mallocSize <= size) can only trigger where size_t is 32-bit. On 64-bit builds, uint32 size + sizeof(header) cannot wrap.
- The MapTable fix changes only the index read. The fCount range validation (1..0x10000) in the constructor was already present and is unchanged.
- Division-by-zero guards are applied in four places (MapTable::Prepare, GainMap::ProcessArea, RefVignette32, RefBaselineMapPoly32). Whether other blackLevel-based divisions exist elsewhere in the SDK is not shown.
- The commit is a vendor drop (build 2471) that bundles fixes for two bug IDs. The material does not isolate which hunk corresponds to this CVE.
Behavior changes
- JXL images whose xsize or ysize exceeds 0x7fffffff, or whose decoder output buffer size exceeds 0xffffffff, now fail decode with a DNG_REQUIRE exception instead of being processed.
- JXL chunk callbacks with coordinates or sizes above 0x7fffffff, or whose area or row-step arithmetic overflows, now throw instead of continuing.
- Allocation-size wraparound in jxl_memory_block and dng_ref_counted_block::Allocate now throws ThrowOverflow. A null return from allocator.Malloc in jxl_memory_block now throws ThrowMemoryFull.
- MapTable opcodes now replicate the actual last entry (table[fCount-1]) rather than the slot after it. This changes output for tables with fCount < 0x10000.
- blackLevel == 65535 now yields a scale of 0.0 instead of a divide-by-zero result in GainMap, Vignette, BaselineMapPoly and MapTable processing.
- On Android clang builds, SafeInt64Mult now uses __builtin_smull_overflow instead of the SafeInt64MultSlow fallback.
- The SDK build string changes from 2410 to 2471, and tcWebP is added to the tag code enum.
Per major
- Android 14: same fix: commit 72d44e39d499 'Update dng_sdk to 1.7.1 2471' in android-security-14.0.0_r28. At android-security-14.0.0_r29, 5/8 patched-region lines are present. The context there shows the code hunks present and the build string at 2502, but the material does not say which 3 lines differ.
- Android 15: same fix: commit 7a737bb68ca6 in android-security-15.0.0_r16, with the same diff. 5/8 patched-region lines are present at android-security-15.0.0_r17; the reason is not shown.
- Android 16: same fix: commit b0d9d2a8d100 in android-security-16.0.0_r7, with the same diff. 5/8 patched-region lines are present at android-security-16.0.0_r8; the reason is not shown.
- Android 17: same fix in base: commit 9a9e97362f2d in android-17.0.0_r1. The content is the same except that the dng_reference.cpp RefBaselineMapPoly32 hunk is at line 3011 (two lines later) because the file base differs (95a6087). 5/8 patched-region lines are present at android-security-17.0.0_r1.
Unknowns
- Which specific hunk (JXL size overflow, ref-counted block overflow, MapTable off-by-one, or divide-by-zero) is the one assigned CVE-2026-21352; the commit covers two bug IDs.
- Which Android processes use dng_sdk with JXL support enabled and parse untrusted input, and therefore the concrete attack surface behind the Critical EoP rating.
- How dng_memory_allocator::Allocate behaves on failure in the non-Malloc branch of jxl_memory_block.
- Which 3 of the 8 patched-region lines are absent at the latest security tags of each major.
- Whether a build between 2410 and 2471 was ever shipped on any branch.