# Extended 2.3 — JPEGReadWriter2Plugin: scanline loop bounded by JPEG header, not Form

Bug ref      : pharo.md §2.3
Severity     : CRITICAL (remote attacker primitive: any image with a malicious JPEG)
Files        : extracted/plugins/JPEGReadWriter2Plugin/src/common/sqJPEGReadWriter2Plugin.c (~202-280),
               extracted/plugins/JPEGReadWriter2Plugin/src/common/JPEGReadWriter2Plugin.c (~340-381)

## Problem

```c
// sqJPEGReadWriter2Plugin.c
while (pcinfo->output_scanline < pcinfo->output_height) {
    ...
    bitmap[((pcinfo->output_scanline - 1) * wordsPerRow) + j] = bitmapWord;
}
```

The loop iterates over the **JPEG header's** declared height, and
writes into `bitmap` using `wordsPerRow` derived from the **Form's**
width.

The caller checks `formBitmapSizeInBytes >= (formPitch * formHeight)`
where `formPitch = ((formWidth + (pixelsPerWord-1)) / pixelsPerWord) * 4`,
i.e. the size check is against `formHeight`, **not** against the JPEG's
`output_height`. The multiply is signed and overflow-prone.

A JPEG whose `output_height > formHeight` (or `output_width >
formWidth`) writes far past the bitmap allocation. The decoded
pixel values are attacker-chosen, so this is a linear write with
attacker-chosen bytes — a strong heap-write primitive triggered by
opening a malicious JPEG.

## Fix

Reject the JPEG before entering the loop when its dimensions don't
match the Form. Also harden the size check against signed-multiply
overflow.

```diff
diff --git a/plugins/JPEGReadWriter2Plugin/src/common/JPEGReadWriter2Plugin.c b/plugins/JPEGReadWriter2Plugin/src/common/JPEGReadWriter2Plugin.c
index bb9e1550f..855f5b9f7 100644
--- a/plugins/JPEGReadWriter2Plugin/src/common/JPEGReadWriter2Plugin.c
+++ b/plugins/JPEGReadWriter2Plugin/src/common/JPEGReadWriter2Plugin.c
@@ -366,9 +366,22 @@ primJPEGReadImagefromByteArrayonFormdoDitheringerrorMgr(void)
 	pixelsPerWord = 32 / (formComponents * formComponentBitSize);
 	wordsPerRow = ((formWidth + pixelsPerWord) - 1) / pixelsPerWord;
 	formPitch = ((formWidth + (pixelsPerWord - 1)) / pixelsPerWord) * 4;
+
+	/* Reject obviously-corrupt sizes before any signed multiply. */
+	if (formWidth  <= 0 || formWidth  > 0x10000 ||
+	    formHeight <= 0 || formHeight > 0x10000 ||
+	    pixelsPerWord <= 0 || wordsPerRow > 0x10000) {
+		primitiveFail();
+		return null;
+	}
+
 	formBitmapSizeInBytes = byteSizeOf(formBitmapOOP);
-	success((isWordsOrBytes(formBitmapOOP))
-	 && (formBitmapSizeInBytes >= (formPitch * formHeight)));
+	{
+		/* uint64 multiply to avoid signed overflow. */
+		uint64_t required = (uint64_t)formPitch * (uint64_t)formHeight;
+		success((isWordsOrBytes(formBitmapOOP))
+		 && ((uint64_t)formBitmapSizeInBytes >= required));
+	}
 	if (failed()) {
 		return null;
 	}

```



(The helper function may need extra parameters threaded through to
make `formHeight` directly available. The simplest passable form
adds `int formHeight` to the helper's signature and clamps the loop
on `min(output_height, formHeight)` while still failing if they
disagree.)

## Test plan

- Open a benign JPEG sized to match a Form; image decodes correctly.
- Open a JPEG whose header declares `output_height` 100 pixels larger
  than the Form. Before: ASAN reports `heap-buffer-overflow` in the
  bitmap write. After: primitive fails before any write.
- Open a JPEG with `output_width = 0x10001`, `output_height =
  0x10001` and Form of the same shape on a 32-bit build; before:
  `formPitch * formHeight` wraps to a small int, size check passes,
  later writes through wrapped pitch. After: the uint64 check
  catches the size requirement.

## Risk notes

- Pre-loop dimension check rejects JPEGs that don't match the
  pre-allocated Form. Callers that legitimately want to decode at a
  different size must resize the Form first; that is the correct
  Pharo idiom.
- The uint64 multiply is portable C and matches libjpeg's own
  overflow-safe size arithmetic.
