# Extended 2.7 — FFI primitiveCopyFromTo: image-controlled memcpy size

Bug ref      : pharo.md §2.7
Severity     : CRITICAL (image-supplied size cast to size_t → heap obliteration)
File         : ffi/src/primitiveUtils.c
Lines (HEAD) : 14-41 (`primitiveCopyFromTo`)

## Problem

```c
PrimitiveWithDepth(primitiveCopyFromTo, 1){
	sqInt from, to, size;
	void* fromAddress;
	void* toAddress;

	size = stackIntegerValue(0);
	checkFailed();

	to = stackObjectValue(1);
	checkFailed();

	from = stackObjectValue(2);
	checkFailed();

	fromAddress = getAddressFromExternalAddressOrByteArray(from);
	checkFailed();

	toAddress = getAddressFromExternalAddressOrByteArray(to);
	checkFailed();

	memcpy(toAddress, fromAddress, size);

	primitiveEnd();
}
```

`size` is a signed `sqInt` from the image, cast to `size_t` by the
memcpy prototype. A negative or out-of-band `size` becomes `SIZE_MAX`-ish
and obliterates the heap. There is no bound against either buffer's
capacity even for legitimate positive sizes.

## Fix

Validate `size >= 0` and `size <= min(byteSizeOf(from), byteSizeOf(to))`
for ByteArray arguments; ExternalAddress arguments don't carry a
size in Pharo so the helper that resolves them must also yield the
upper bound (or the primitive should refuse to copy into/out of a
raw ExternalAddress beyond a small documented cap).

```diff
diff --git a/src/ffi/primitiveUtils.c b/src/ffi/primitiveUtils.c
index ad433c87e..02be814ea 100644
--- a/src/ffi/primitiveUtils.c
+++ b/src/ffi/primitiveUtils.c
@@ -22,12 +22,36 @@ PrimitiveWithDepth(primitiveCopyFromTo, 1){
 	size = stackIntegerValue(0);
 	checkFailed();
 
+	/* Reject negative sizes before the size_t cast obliterates the heap. */
+	if (size < 0) {
+		primitiveFailFor(PrimErrBadArgument);
+		return;
+	}
+
 	to = stackObjectValue(1);
 	checkFailed();
 
 	from = stackObjectValue(2);
 	checkFailed();
 
+	/* Bound against either ByteArray's capacity. ExternalAddress carries
+	 * no length on the image side, so for those we apply a conservative
+	 * 64 MiB cap; legitimate larger copies must use a chunked API. */
+	if (isKindOfClass(from, classByteArray())) {
+		if (size > byteSizeOf(from)) {
+			primitiveFailFor(PrimErrBadIndex);
+			return;
+		}
+	} else if (size > (sqInt)(64 * 1024 * 1024)) {
+		primitiveFailFor(PrimErrBadArgument);
+		return;
+	}
+	if (isKindOfClass(to, classByteArray())) {
+		if (size > byteSizeOf(to)) {
+			primitiveFailFor(PrimErrBadIndex);
+			return;
+		}
+	}
 
 	fromAddress = getAddressFromExternalAddressOrByteArray(from);
 	checkFailed();
@@ -35,7 +59,7 @@ PrimitiveWithDepth(primitiveCopyFromTo, 1){
 	toAddress = getAddressFromExternalAddressOrByteArray(to);
 	checkFailed();
 
-	memcpy(toAddress, fromAddress, size);
+	memcpy(toAddress, fromAddress, (size_t)size);
 
 	primitiveEnd();
 }

```

## Test plan

- `primitiveCopyFromTo` between two ByteArrays of size 16 with
  size=16: copies 16 bytes.
- Same with size=17: primitive fails with `PrimErrBadIndex` (not
  `PrimErrBadArgument`, which is reserved for malformed inputs).
- Same with size=-1: primitive fails with `PrimErrBadArgument`.
- ExternalAddress→ExternalAddress with size = 1 GiB: primitive
  fails (exceeds the 64 MiB cap).

## Risk notes

- The 64 MiB cap for ExternalAddress→anywhere copies is
  conservative; users with legitimate need for larger copies should
  use chunked calls or add a separate primitive that takes an
  ExternalAddress-with-length sentinel.
- Casting `size` to `size_t` is now safe because we have
  established `size >= 0`.
- Existing in-tree callers that use `primitiveCopyFromTo` with
  correct sizes are unaffected.
