# A02 — FFI callbacks: stack-allocated CallbackInvocation registered in globals

Bug ref      : always.md A.2 ; pharo.md §2.8
Severity     : HIGH (heap-state pointing into freed stack on every callback)
File         : ffi/src/callbacks/callbacks.c
Lines (HEAD) : 14-33 (`callbackFrontend`)

## Problem

`callbackFrontend` declares `CallbackInvocation invocation;` on the
C stack and then publishes `&invocation` into two pieces of long-lived
state:

  * `callback->runner->callbackStack` (a chain of previous invocations)
  * `callbackQueue` (a thread-safe global queue consumed by the image)

The lifetime contract that keeps `&invocation` valid is implicit and
fragile: it survives only as long as `callbackFrontend`'s C frame
exists. The control flow that maintains that invariant is:

  - `primitiveCallbackReturn` pops `runner->callbackStack` (line 222)
  - then calls `runner->callbackExitFunction`, which in same-thread
    mode `sig_longjmp`s back to the `sigsetjmp` in
    `sameThreadCallbackEnter`
  - that frame unwinds through `callbackFrontend`, destroying the
    stack-allocated `invocation`

Any future change that

  - re-enters `callbackFrontend` from a different thread on the same
    Runner before the previous frame is unwound,
  - returns from the image side without consuming the queue entry
    posted by `queue_add_pending_callback(&invocation)`,
  - or alters the `sig_longjmp` cleanup path so that the pop in
    `primitiveCallbackReturn:222` is skipped,

immediately turns the published `&invocation` into a dangling pointer
that the next callback walks via `invocation->previous`.

The structural fix is to give `CallbackInvocation` its own lifetime
on the heap, owned by the frontend that allocated it.

## Fix

Heap-allocate the invocation; free it in the same function once
`callbackEnterFunction` returns. In same-thread mode the call returns
after the longjmp unwinds to the setjmp inside
`sameThreadCallbackEnter` and that function returns. In worker mode it
returns after the wait on `invocation->payload` is signalled. Both
paths reach the `free` and neither leaks.

```diff
diff --git a/src/ffi/callbacks/callbacks.c b/src/ffi/callbacks/callbacks.c
index d86667e58..0afbe6195 100644
--- a/src/ffi/callbacks/callbacks.c
+++ b/src/ffi/callbacks/callbacks.c
@@ -12,24 +12,37 @@ void initilizeCallbacks(int pharo_semaphore_index){
 }
 
 static void callbackFrontend(ffi_cif *cif, void *ret, void* args[], void* cbPtr) {
-	CallbackInvocation invocation;
 	Callback *callback = cbPtr;
+	CallbackInvocation *invocation = (CallbackInvocation*) calloc(1, sizeof(CallbackInvocation));
+	if (invocation == NULL) {
+		/* Cannot signal the image safely from here; zero the return
+		 * holder so the libffi consumer sees a deterministic value
+		 * and abort the callback. */
+		if (ret) memset(ret, 0, cif->rtype->size);
+		return;
+	}
 
-	invocation.callback = callback;
-	invocation.arguments = args;
-	invocation.returnHolder = ret;
-    
-    // Push callback invocation into a callback stack
-    // This callback stack is used to validate that callbacks return in order
-    invocation.previous = callback->runner->callbackStack;
-    callback->runner->callbackStack = &invocation;
-    
-    callback->runner->callbackPrepareInvocation(callback->runner, &invocation);
+	invocation->callback = callback;
+	invocation->arguments = args;
+	invocation->returnHolder = ret;
+
+	/* Push callback invocation into a callback stack.
+	 * This callback stack is used to validate that callbacks return in order. */
+	invocation->previous = callback->runner->callbackStack;
+	callback->runner->callbackStack = invocation;
+
+	callback->runner->callbackPrepareInvocation(callback->runner, invocation);
+
+	queue_add_pending_callback(invocation);
+
+	/* Manage callouts while waiting this callback to return.
+	 * In same-thread runners this returns after sig_longjmp unwinds back
+	 * to sameThreadCallbackEnter's setjmp; in worker runners it returns
+	 * after the semaphore wait on invocation->payload is signalled.
+	 * Both paths reach the free below. */
+	callback->runner->callbackEnterFunction(callback->runner, invocation);
 
-    queue_add_pending_callback(&invocation);
-	
-	// Manage callouts while waiting this callback to return
-	callback->runner->callbackEnterFunction(callback->runner, &invocation);
+	free(invocation);
 }
 
 Callback *callback_new(Runner* runner, ffi_type** parameters, sqInt count, ffi_type* returnType) {

```

## Test plan

- Run the FFI callback regression suite under ASAN; trigger nested
  callbacks (callback A invokes a callout that itself triggers
  callback B). ASAN must report no use-after-free either before or
  after.
- Stress test: 10⁶ callbacks across same-thread and worker runners,
  verify steady-state RSS to confirm no leak.

## Risk notes

- Existing semantics rely on `callbackEnterFunction` always returning
  before `callbackFrontend` returns. That has been true for both
  shipped runners (same-thread sigsetjmp unwind; worker semaphore
  wait). Any future runner that does not return must be expected to
  free `invocation` itself, or this PR will leak.
- Adds one calloc / free per callback. Worker callbacks already
  allocate (semaphore, task); the additional allocation is small.
- `cif` is the only piece of context available when allocation fails;
  zeroing the return holder is the same recovery existing libffi
  consumers use when a closure runs out of memory.
