diff --git a/scripts/jpegdec_patches/0001-redirect-pmcu-on-mcu-skip.patch b/scripts/jpegdec_patches/0001-redirect-pmcu-on-mcu-skip.patch new file mode 100644 index 00000000..5f046627 --- /dev/null +++ b/scripts/jpegdec_patches/0001-redirect-pmcu-on-mcu-skip.patch @@ -0,0 +1,28 @@ +From 5dff5afab0c68d0d0c4385d72e6f0c030b613960 Mon Sep 17 00:00:00 2001 +From: patch +Date: Mon, 18 May 2026 20:57:54 -0700 +Subject: [PATCH 1/2] Redirect pMCU to sMCUs[0] when iMCU < 0 (MCU_SKIP) + +--- + src/jpeg.inl | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/jpeg.inl b/src/jpeg.inl +index a60b548..26bcf6f 100644 +--- a/src/jpeg.inl ++++ b/src/jpeg.inl +@@ -1824,7 +1824,10 @@ static int JPEGDecodeMCU_P(JPEGIMAGE *pJPEG, int iMCU, int *iDCPredictor) + unsigned short *pFast; + uint32_t usHuff; // this prevents an unnecessary & 65535 for shorts + signed int iPositive, iNegative, iCoeff; +- signed short *pMCU = &pJPEG->sMCUs[iMCU & 0xffffff]; ++ // CrossPoint patch: redirect pMCU to sMCUs[0] when MCU_SKIP to avoid ++ // a wild pointer (~33 MB past sMCUs) that store-faults on AC writes. ++ signed short *pMCU = (iMCU < 0) ? pJPEG->sMCUs ++ : &pJPEG->sMCUs[iMCU & 0xffffff]; + uint32_t ulBitOff; + my_ulong ulCode, ulBits, ulTemp; // local copies to allow compiler to use register vars + uint8_t *pBuf; +-- +2.50.1 (Apple Git-155) + diff --git a/scripts/jpegdec_patches/0002-guard-dc-writes-on-mcu-skip.patch b/scripts/jpegdec_patches/0002-guard-dc-writes-on-mcu-skip.patch new file mode 100644 index 00000000..82925c64 --- /dev/null +++ b/scripts/jpegdec_patches/0002-guard-dc-writes-on-mcu-skip.patch @@ -0,0 +1,41 @@ +From f6238b54c2de34c8e29b0d371a7e902d9cf579bf Mon Sep 17 00:00:00 2001 +From: patch +Date: Mon, 18 May 2026 20:58:22 -0700 +Subject: [PATCH 2/2] Guard pMCU[0] DC writes against MCU_SKIP + +--- + src/jpeg.inl | 11 +++++++++-- + 1 file changed, 9 insertions(+), 2 deletions(-) + +diff --git a/src/jpeg.inl b/src/jpeg.inl +index 26bcf6f..1bd38b2 100644 +--- a/src/jpeg.inl ++++ b/src/jpeg.inl +@@ -1855,7 +1855,11 @@ static int JPEGDecodeMCU_P(JPEGIMAGE *pJPEG, int iMCU, int *iDCPredictor) + { + // (*iDCPredictor) |= iPositive; // in case the scan is run more than once + // pMCU[0] = *iDCPredictor; // store in MCU[0] +- pMCU[0] |= iPositive; ++ // CrossPoint patch: guard against MCU_SKIP. The pMCU ++ // redirect makes &sMCUs[0] safe to dereference, but ++ // writing here would clobber the just-decoded Y DC. ++ if (iMCU >= 0) ++ pMCU[0] |= iPositive; + } + goto mcu_done; // that's it + } +@@ -1887,7 +1891,10 @@ static int JPEGDecodeMCU_P(JPEGIMAGE *pJPEG, int iMCU, int *iDCPredictor) + ulCode <<= pJPEG->cApproxBitsLow; // successive approximation shift value + (*iDCPredictor) += ulCode; + } +- pMCU[0] = (short)*iDCPredictor; // store in MCU[0] ++ // CrossPoint patch: guard against MCU_SKIP. See note on the ++ // matching SA write above. ++ if (iMCU >= 0) ++ pMCU[0] = (short)*iDCPredictor; // store in MCU[0] + } + // Now get the other 63 AC coefficients + pFast = &pJPEG->usHuffAC[pJPEG->ucACTable * HUFF11SIZE]; +-- +2.50.1 (Apple Git-155) + diff --git a/scripts/patch_jpegdec.py b/scripts/patch_jpegdec.py index 015b1761..8f2d38b8 100644 --- a/scripts/patch_jpegdec.py +++ b/scripts/patch_jpegdec.py @@ -1,68 +1,92 @@ """ -PlatformIO pre-build script: patch JPEGDEC for MCU_SKIP wild pointer crash. +PlatformIO pre-build script: apply CrossPoint's JPEGDEC patches via `git apply`. -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. +The upstream JPEGDEC pin still has the wild-pointer + DC-write bugs in +JPEGDecodeMCU_P that surface when EIGHT_BIT_GRAYSCALE decodes a 3-component +progressive JPEG (each Y MCU drags two MCU_SKIP calls behind it for Cb/Cr). +The patches in `scripts/jpegdec_patches/` carry the fix; this script applies +each one against the libdep working tree. - Upstream commit 8628297 guarded the DC coefficient write (pMCU[0]) but not the - AC coefficient writes at indices 1-63. +Each patch's idempotency is decided by git itself: + * `git apply --check --reverse` succeeds -> already applied, skip + * `git apply --check` succeeds -> apply + * neither succeeds -> abort the build -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. - -Applied idempotently — safe to run on every build. +Patches live in `scripts/jpegdec_patches/` as one-commit-per-fix files +(see the file headers for context). Applied in lexical order. """ -Import("env") +Import("env") # noqa: F821 (SCons-injected global) import os +import subprocess +import sys + + +PATCH_DIR = os.path.join(env["PROJECT_DIR"], "scripts", "jpegdec_patches") # noqa: F821 def patch_jpegdec(env): libdeps_dir = os.path.join(env["PROJECT_DIR"], ".pio", "libdeps") if not os.path.isdir(libdeps_dir): return + patches = _patch_files() for env_dir in os.listdir(libdeps_dir): - 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) + jpeg_dir = os.path.join(libdeps_dir, env_dir, "JPEGDEC") + if not os.path.isdir(os.path.join(jpeg_dir, ".git")): + continue + for patch in patches: + _apply_one(jpeg_dir, patch) -def _apply_mcu_skip_pointer_fix(filepath): - MARKER = "// CrossPoint patch: safe pMCU for MCU_SKIP" - with open(filepath, "r") as f: - content = f.read() - - if MARKER in content: - return # already patched - - # The wild-pointer line in JPEGDecodeMCU_P: - OLD = " signed short *pMCU = &pJPEG->sMCUs[iMCU & 0xffffff];" - - NEW = ( - " " + MARKER + "\n" - " signed short *pMCU = (iMCU < 0) ? pJPEG->sMCUs\n" - " : &pJPEG->sMCUs[iMCU & 0xffffff];" - ) - - if OLD not in content: - print( - "WARNING: JPEGDEC MCU_SKIP pointer patch target not found in %s " - "— library may have been updated" % filepath +def _patch_files(): + if not os.path.isdir(PATCH_DIR): + raise RuntimeError( + "JPEGDEC patches missing -- aborting build (expected directory %s)" + % PATCH_DIR ) + patches = sorted( + os.path.join(PATCH_DIR, name) + for name in os.listdir(PATCH_DIR) + if name.endswith(".patch") + ) + if not patches: + raise RuntimeError( + "JPEGDEC patches missing -- aborting build (no .patch files in %s)" + % PATCH_DIR + ) + return patches + + +def _apply_one(jpeg_dir, patch_path): + name = os.path.basename(patch_path) + if _git_apply_succeeds(jpeg_dir, patch_path, reverse=True): return - - content = content.replace(OLD, NEW, 1) - with open(filepath, "w") as f: - f.write(content) - print("Patched JPEGDEC: safe pMCU for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath) + if not _git_apply_succeeds(jpeg_dir, patch_path, reverse=False): + # Not applied, not appliable -- the libdep source has diverged from + # what the patch expects. Don't write a half-patched file. + result = subprocess.run( + ["git", "apply", "--check", patch_path], + cwd=jpeg_dir, + capture_output=True, + text=True, + ) + sys.stderr.write( + "ERROR: JPEGDEC patch %s does not apply cleanly:\n%s%s\n" + % (name, result.stdout, result.stderr) + ) + raise SystemExit(1) + subprocess.run(["git", "apply", patch_path], cwd=jpeg_dir, check=True) + print("Applied JPEGDEC patch: %s" % name) -# Run immediately at script import time (before compilation). -patch_jpegdec(env) +def _git_apply_succeeds(jpeg_dir, patch_path, *, reverse): + cmd = ["git", "apply", "--check"] + if reverse: + cmd.append("--reverse") + cmd.append(patch_path) + return subprocess.run( + cmd, cwd=jpeg_dir, capture_output=True, text=True + ).returncode == 0 + + +patch_jpegdec(env) # noqa: F821