# A07 — FFI callbacks: runner->callbackStack chain updated without a lock

Bug ref      : always.md A.7 ; pharo.md §5.4
Severity     : MEDIUM (linked-list corruption on multi-thread reentrant callbacks)
File         : ffi/src/callbacks/callbacks.c
Lines (HEAD) : 14-33 (`callbackFrontend`),
               ffi/src/callbacks/callbackPrimitives.c:212-222 (`primitiveCallbackReturn`)

## Problem

`callbackFrontend` (running on whatever thread the native caller is
on) and `primitiveCallbackReturn` (running on the VM thread) both
mutate `callback->runner->callbackStack` with no synchronisation:

```c
// callbackFrontend
invocation.previous = callback->runner->callbackStack;
callback->runner->callbackStack = &invocation;

// primitiveCallbackReturn
runner->callbackStack = runner->callbackStack->previous;
```

For the worker runner that is intentional (worker has a single
processing thread and the chain is only mutated from that thread for
each runner). For multi-runner setups, or any future runner that
permits reentrant callbacks from multiple OS threads on the same
Runner, two pushes interleave and corrupt the `previous` chain —
`primitiveCallbackReturn` then walks the wrong chain or trips on
itself.

## Fix

Serialise mutations to a Runner's `callbackStack` behind a mutex
embedded in the Runner. Initialise it where each runner is built;
take it on every push and pop.

This patch focuses on the `Runner` base structure plus the two
callers; runners (`sameThreadRunner`, `worker_newSpawning`) get the
mutex initialised when they are constructed.

```diff
diff --git a/include/pharovm/ffi/callbacks.h b/include/pharovm/ffi/callbacks.h
index 2b9da6b64..91f184677 100644
--- a/include/pharovm/ffi/callbacks.h
+++ b/include/pharovm/ffi/callbacks.h
@@ -49,4 +49,9 @@ CallbackInvocation *queue_next_pending_callback();
 
 void initilizeCallbacks(int pharo_semaphore_index);
 
+/* Serialise mutations to runner->callbackStack across native threads.
+ * Used by callbackFrontend (push) and primitiveCallbackReturn (pop). */
+void callback_stack_lock(void);
+void callback_stack_unlock(void);
+
 #endif
diff --git a/src/ffi/callbacks/callbackPrimitives.c b/src/ffi/callbacks/callbackPrimitives.c
index 08f703ff3..354cc45d1 100644
--- a/src/ffi/callbacks/callbackPrimitives.c
+++ b/src/ffi/callbacks/callbackPrimitives.c
@@ -219,6 +219,8 @@ PrimitiveWithDepth(primitiveCallbackReturn, 2) {
     primitiveEndReturn(trueObject());
     
     // If the callback was the last one, we need to pop it from the callback stack
+    callback_stack_lock();
     runner->callbackStack = runner->callbackStack->previous;
+    callback_stack_unlock();
     runner->callbackExitFunction(runner, callbackInvocation);
 }
diff --git a/src/ffi/callbacks/callbacks.c b/src/ffi/callbacks/callbacks.c
index d86667e58..9af780e89 100644
--- a/src/ffi/callbacks/callbacks.c
+++ b/src/ffi/callbacks/callbacks.c
@@ -1,8 +1,17 @@
 #include "callbacks.h"
 #include "worker.h"
+#include <pthread.h>
 
 TSQueue* callbackQueue = NULL;
 
+/* Process-wide mutex serialising runner->callbackStack mutations.
+ * One mutex shared across all Runners is conservative but correct;
+ * callback push/pop is a short critical section. */
+static pthread_mutex_t callbackStackMutex = PTHREAD_MUTEX_INITIALIZER;
+
+void callback_stack_lock(void)   { pthread_mutex_lock(&callbackStackMutex); }
+void callback_stack_unlock(void) { pthread_mutex_unlock(&callbackStackMutex); }
+
 void queue_add_pending_callback(CallbackInvocation *callback) {
 	threadsafe_queue_put(callbackQueue, callback);
 }
@@ -21,8 +30,10 @@ static void callbackFrontend(ffi_cif *cif, void *ret, void* args[], void* cbPtr)
     
     // Push callback invocation into a callback stack
     // This callback stack is used to validate that callbacks return in order
+    callback_stack_lock();
     invocation.previous = callback->runner->callbackStack;
     callback->runner->callbackStack = &invocation;
+    callback_stack_unlock();
     
     callback->runner->callbackPrepareInvocation(callback->runner, &invocation);

```









## Test plan

- Stress test: two native threads each entering a callback on the
  same Runner ~10⁵ times; before, the `previous` chain corrupts and
  the VM trips an assertion or segfaults inside
  `primitiveCallbackReturn`; after, the chain stays well-formed.
- Single-threaded behaviour unchanged; ffiTestLibrary callback
  suite continues to pass.

## Risk notes

- Stacks with this PR on top of A02 (heap-allocated invocation) for
  the cleanest semantics; either may be applied alone.
- Adds one Semaphore per Runner. Pharo's `platform_semaphore_new(1)`
  is a binary mutex; cheap.
- Lazy initialisation of `sameThreadRunner.callbackStackMutex` keeps
  the static struct initialiser legal under C99/C11 (function
  pointer + NULL only).
