CVE-2026-33636 AI-generated analysis
platform/external/libpng, ARM NEON palette expansion (arm/palette_neon_intrinsics.c)
Restricts the NEON palette-expansion loops in external/libpng arm/palette_neon_intrinsics.c to full chunks only (loop condition i + pixels_per_chunk <= row_width), drops the post-loop count correction, and undoes the *ddp pre-adjustment before handing the remaining pixels to the scalar fallback.
Root cause
png_do_expand_palette_rgba8_neon and png_do_expand_palette_rgb8_neon iterate `for (i = 0; i < row_width; i += pixels_per_chunk)`. Each iteration loads palette entries indexed by source bytes at sp and stores a full vector (vst1q_u32 of a uint32x4_t for rgba8, i.e. 4 pixels; vst3_u8 for rgb8, 8 pixels per the 'Seeking this back by 8 pixels x 3 bytes' comment) at dp. When row_width is not a multiple of the chunk size, the last iteration still runs with less than a full chunk left. Per the commit message, this allows out-of-bounds reads and writes. The post-loop `if (i != row_width) i -= pixels_per_chunk;` only corrected the returned pixel count after the loop had finished. In addition, *ddp is pre-adjusted at entry by (pixels_per_chunk * element size - 1), and that offset was not undone before the pointer was handed back for the scalar path.
How the fix works
- arm/palette_neon_intrinsics.c:png_do_expand_palette_rgba8_neon: the loop condition changes from `i < row_width` to `i + pixels_per_chunk <= row_width`, so the NEON body only runs while a full chunk remains. The trailing partial chunk is left to the scalar code.
- arm/palette_neon_intrinsics.c:png_do_expand_palette_rgba8_neon: the `if (i != row_width) i -= pixels_per_chunk;` adjustment is removed because i no longer overshoots row_width.
- arm/palette_neon_intrinsics.c:png_do_expand_palette_rgba8_neon: before the final `*ssp = *ssp - i; *ddp = *ddp - (i << 2);`, the entry pre-adjustment is reverted with `*ddp = *ddp + (pixels_per_chunk * 4 - 1);`. Per the added comment, this makes the dp handed to the scalar fallback in pngrtran.c point to the correct position.
- arm/palette_neon_intrinsics.c:png_do_expand_palette_rgb8_neon: the same changes are made for the RGB path. The loop condition becomes `i + pixels_per_chunk <= row_width`, the post-loop adjustment is removed, and `*ddp = *ddp + (pixels_per_chunk * 3 - 1);` is added before `*ssp = *ssp - i; *ddp = *ddp - ((i << 1) + i);`.
- The return value i is still the number of pixels processed by NEON, and is now always a multiple of pixels_per_chunk.
Completeness
- Both NEON palette expansion functions in the diff (rgba8 and rgb8) are fixed with the same pattern.
- The fix relies on the scalar fallback in pngrtran.c to process the remaining row_width - i pixels from the handed-off *ssp/*ddp. The pngrtran.c side is not part of the diff and is not shown in the material, so its correctness with the new handoff pointer cannot be checked here.
- png_riffle_palette_neon in the same file is unchanged by the diff. Only its first lines are shown.
Behavior changes
- For rows whose width is not a multiple of the chunk size, the trailing pixels are now left to the scalar code instead of being processed by a final NEON iteration.
- The *ddp value handed back to the caller no longer carries the (pixels_per_chunk * element size - 1) pre-adjustment.
- The diff adds no new error codes and makes no API changes.
Per major
- Android 14: same fix: commit 843457e59be7 in android-security-14.0.0_r29. Identical diff to arm/palette_neon_intrinsics.c (Change-Id I56e19b3e1a226de21e91e400124e36021357689a). 7/8 lines of the patched region are present at that tag.
- Android 15: same fix: commit fa7e33a0a0cc in android-security-15.0.0_r17. Identical diff with the same blob ids (92c7d6f9f..bdd15849d). 6/8 lines of the patched region are present at that tag.
- Android 16: same fix: commit 0b9147198ed6 in android-security-16.0.0_r8. Identical diff with the same blob ids as 14/15. 6/8 lines of the patched region are present at that tag.
- Android 17: same fix with shifted offsets: commit 7d98050ef8d9 in android-security-17.0.0_r1. The file base differs (3068e9b6e, no leading blank line), so hunk offsets move by one line. The code changes are identical. 6/8 lines of the patched region are present at that tag.
Unknowns
- The pngrtran.c scalar fallback that consumes the handed-off pointers is not in the material. It is therefore not shown whether the old, un-reverted *ddp offset caused incorrect access there or was compensated for by the caller.
- The material does not say which Android process or context the bulletin's Critical EoP rating refers to.
- The content of the two internal Cherrypick-From commits is not shown.
- Whether png_riffle_palette_neon or other code in the file has a similar loop pattern is not shown.