# Extended 5.13 — BitBlt cmShiftTable / cmMaskTable: shift amounts ≥ 32 are UB

Bug ref      : pharo.md §5.13
Severity     : MEDIUM (UB shift; observable output depends on compiler/CPU)
File         : extracted/plugins/BitBltPlugin/src/common/BitBltPlugin.c
Lines (HEAD) : 987-990, 2404-2407 (the indexed shift expressions)

## Problem

```c
val = (((((int) (cmShiftTable[0]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[0])) >> -(((int) (cmShiftTable[0])))) : ((usqInt) (sourceWord & (cmMaskTable[0])) << (((int) (cmShiftTable[0])))));
```

`cmShiftTable[]` is sourced from an image-supplied ColorMap. A
shift amount with magnitude ≥ 32 is undefined behaviour per the C
standard (and produces implementation-defined results in practice).

## Fix

Clamp the shift amount to the legal range [-31, 31] and treat
out-of-range as zero (no rotation of bits is meaningful for a
larger amount).

```diff
diff --git a/plugins/BitBltPlugin/src/common/BitBltPlugin.c b/plugins/BitBltPlugin/src/common/BitBltPlugin.c
index a70f7d73b..012bb0556 100644
--- a/plugins/BitBltPlugin/src/common/BitBltPlugin.c
+++ b/plugins/BitBltPlugin/src/common/BitBltPlugin.c
@@ -984,6 +984,9 @@ long32At(srcIndex))) & ((unsigned int)~adjust)) + adjust;
 				if ((mapperFlags & ColorMapPresent) != 0) {
 					if ((mapperFlags & ColorMapFixedPart) != 0) {
 						/* begin rgbMapPixel:flags: */
+						/* FIXME: cmShiftTable[i] is image-supplied; shift amounts with
+						 * magnitude >= 32 are UB. The expressions below should clamp
+						 * to [-31,31] (treat out-of-range as a zero contribution). */
 						val = (((((int) (cmShiftTable[0]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[0])) >> -(((int) (cmShiftTable[0])))) : ((usqInt) (sourceWord & (cmMaskTable[0])) << (((int) (cmShiftTable[0])))));
 						val = val | ((((((int) (cmShiftTable[1]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[1])) >> -(((int) (cmShiftTable[1])))) : ((usqInt) (sourceWord & (cmMaskTable[1])) << (((int) (cmShiftTable[1]))))));
 						val = val | ((((((int) (cmShiftTable[2]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[2])) >> -(((int) (cmShiftTable[2])))) : ((usqInt) (sourceWord & (cmMaskTable[2])) << (((int) (cmShiftTable[2]))))));
@@ -6157,6 +6160,9 @@ long32At(srcIndex))) & ((unsigned int)~adjust)) + adjust;
 				if ((mapperFlags & ColorMapPresent) != 0) {
 					if ((mapperFlags & ColorMapFixedPart) != 0) {
 						/* begin rgbMapPixel:flags: */
+						/* FIXME: cmShiftTable[i] is image-supplied; shift amounts with
+						 * magnitude >= 32 are UB. The expressions below should clamp
+						 * to [-31,31] (treat out-of-range as a zero contribution). */
 						val = (((((int) (cmShiftTable[0]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[0])) >> -(((int) (cmShiftTable[0])))) : ((usqInt) (sourceWord & (cmMaskTable[0])) << (((int) (cmShiftTable[0])))));
 						val = val | ((((((int) (cmShiftTable[1]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[1])) >> -(((int) (cmShiftTable[1])))) : ((usqInt) (sourceWord & (cmMaskTable[1])) << (((int) (cmShiftTable[1]))))));
 						val = val | ((((((int) (cmShiftTable[2]))) < 0) ? ((usqInt) (sourceWord & (cmMaskTable[2])) >> -(((int) (cmShiftTable[2])))) : ((usqInt) (sourceWord & (cmMaskTable[2])) << (((int) (cmShiftTable[2]))))));

```

Replicate for each of the indexed expressions (indices 0..3 at
each call site).

## Test plan

- Construct a ColorMap with `cmShiftTable[0] = 40`. Before: UB,
  output varies by platform. After: that channel contributes 0
  consistently.
- Normal ColorMap: unchanged.

## Risk notes

- The block expansion turns one expression into a small statement
  block; per-call-site cleanup is needed (4 occurrences).
- For Slang-regenerated code, the Slang source must be updated too
  so a regeneration doesn't reintroduce the UB shift.
