fix: guard DC writes in JPEGDEC MCU_SKIP path

EIGHT_BIT_GRAYSCALE decode of a 3-component progressive JPEG calls
JPEGDecodeMCU_P with MCU_SKIP for Cb and Cr after every Y MCU. The
existing safe-pMCU patch redirects the wild pointer to &sMCUs[0] but
leaves the DC store unguarded, so each chroma skip overwrites the
just-decoded Y DC with the chroma DC predictor. Output reads sMCUs[0],
gets the trailing Cr DC (~0), and renders an all-black image.

Add `if (iMCU >= 0)` guards to the two pMCU[0] writes (main DC store
and successive-approximation update). The pointer redirect stays as the
AC wild-pointer defence; the new guards stop the silent corruption at
sMCUs[0]. The two fixes are independent and both required.
This commit is contained in:
Jeremy Klein
2026-05-19 20:05:07 +02:00
committed by jpirnay
parent ca7076ab4b
commit 597edeebd8
+76 -19
View File
@@ -1,23 +1,29 @@
"""
PlatformIO pre-build script: patch JPEGDEC for MCU_SKIP wild pointer crash.
PlatformIO pre-build script: patch JPEGDEC for safe MCU_SKIP handling.
Problem:
JPEGDecodeMCU_P computes pMCU = &sMCUs[iMCU & 0xffffff]. When iMCU is
MCU_SKIP (-8), the bitmask produces index 0xFFFFF8 (16 777 208), creating a
pointer ~33 MB past the 392-entry sMCUs array. If the progressive JPEG's
first scan includes AC coefficients (iScanEnd > 0), the AC decode loop writes
through this wild pointer and crashes with a store-access fault.
When iMCU is MCU_SKIP (-8), JPEGDecodeMCU_P computes pMCU as
&sMCUs[iMCU & 0xffffff] = &sMCUs[0xFFFFF8], a wild pointer ~33 MB past
the array. EIGHT_BIT_GRAYSCALE decoding of a 3-component progressive
JPEG calls JPEGDecodeMCU_P with MCU_SKIP twice per Y MCU (Cb then Cr),
so every MCU exercises the wild pointer.
Upstream commit 8628297 guarded the DC coefficient write (pMCU[0]) but not the
AC coefficient writes at indices 1-63.
Two patches, both required:
Fix:
Redirect pMCU to sMCUs[0] when MCU_SKIP is active. Writes to sMCUs[1..63]
are harmless: for JPEG_SCALE_EIGHTH only sMCUs[0] is read for output, and
the DC write at sMCUs[0] is already guarded by the existing `if (iMCU >= 0)`
check.
1. Redirect pMCU to &sMCUs[0] when iMCU < 0. Without this, the AC
decode loop (`pMCU[iIndex] = ...`) store-faults on any progressive
JPEG whose first scan carries AC coefficients (iScanEnd > 0).
Applied idempotently — safe to run on every build.
2. Guard the two pMCU[0] DC writes with `if (iMCU >= 0)`. Without
this, patch 1 just relocates the corruption: chroma-skip DC writes
land at sMCUs[0] and clobber the freshly-decoded Y DC, producing
all-black output for progressive JPEGs at JPEG_SCALE_EIGHTH grayscale.
The AC loop body (`pMCU[iIndex] = ...` writes and matching reads) is
not separately guarded. It is unreachable on the JPEG_SCALE_EIGHTH
DC-only first-scan path, and guarding the writes without also skipping
bit consumption would desync the bitstream for the next MCU.
Both patches are idempotent.
"""
Import("env")
@@ -32,6 +38,7 @@ def patch_jpegdec(env):
jpeg_inl = os.path.join(libdeps_dir, env_dir, "JPEGDEC", "src", "jpeg.inl")
if os.path.isfile(jpeg_inl):
_apply_mcu_skip_pointer_fix(jpeg_inl)
_apply_dc_write_guards(jpeg_inl)
def _apply_mcu_skip_pointer_fix(filepath):
@@ -40,9 +47,8 @@ def _apply_mcu_skip_pointer_fix(filepath):
content = f.read()
if MARKER in content:
return # already patched
return
# The wild-pointer line in JPEGDecodeMCU_P:
OLD = " signed short *pMCU = &pJPEG->sMCUs[iMCU & 0xffffff];"
NEW = (
@@ -54,7 +60,7 @@ def _apply_mcu_skip_pointer_fix(filepath):
if OLD not in content:
print(
"WARNING: JPEGDEC MCU_SKIP pointer patch target not found in %s "
" library may have been updated" % filepath
"-- library may have been updated" % filepath
)
return
@@ -64,5 +70,56 @@ def _apply_mcu_skip_pointer_fix(filepath):
print("Patched JPEGDEC: safe pMCU for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath)
# Run immediately at script import time (before compilation).
def _apply_dc_write_guards(filepath):
MARKER = "// CrossPoint patch: guard pMCU DC writes for MCU_SKIP"
with open(filepath, "r") as f:
content = f.read()
if MARKER in content:
return
OLD_DC = """\
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
}
// Now get the other 63 AC coefficients"""
NEW_DC = """\
""" + MARKER + """
if (iMCU >= 0)
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
}
// Now get the other 63 AC coefficients"""
OLD_SA = """\
pMCU[0] |= iPositive;
}
goto mcu_done; // that's it"""
NEW_SA = """\
if (iMCU >= 0)
pMCU[0] |= iPositive;
}
goto mcu_done; // that's it"""
if OLD_DC not in content:
print(
"WARNING: JPEGDEC DC write guard target not found in %s "
"-- library may have been updated" % filepath
)
return
content = content.replace(OLD_DC, NEW_DC, 1)
if OLD_SA in content:
content = content.replace(OLD_SA, NEW_SA, 1)
else:
print(
"WARNING: JPEGDEC successive-approximation DC guard target not found in %s "
"-- continuing without that half of the patch" % filepath
)
with open(filepath, "w") as f:
f.write(content)
print("Patched JPEGDEC: guard pMCU[0] DC writes for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath)
patch_jpegdec(env)