From 597edeebd83721c18d79f11f122bfa1322c8d79f Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 18 May 2026 20:28:40 -0700 Subject: [PATCH] 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. --- scripts/patch_jpegdec.py | 95 ++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/scripts/patch_jpegdec.py b/scripts/patch_jpegdec.py index 015b1761..89169419 100644 --- a/scripts/patch_jpegdec.py +++ b/scripts/patch_jpegdec.py @@ -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)