diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java index 64d09c102..8cd467e48 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java @@ -13,7 +13,7 @@ *

Format: *

  *   [4 bytes: magic "CL4J"]
- *   [4 bytes: version (2)]
+ *   [4 bytes: version (3)]
  *   [modified UTF-8: target triple]
  *   [4 bytes: image length]
  *   [N bytes: image]
@@ -30,7 +30,7 @@
 public final class NativeCodeSerializer {
 
     private static final int MAGIC = 0x434C344A; // "CL4J"
-    private static final int VERSION = 2;
+    private static final int VERSION = 3;
 
     private NativeCodeSerializer() {}
 
diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
index 420362c07..9209feca8 100644
--- a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
@@ -19,6 +19,7 @@
  *  56     i64    memmovePtr        Pointer to libc memmove
  *  64     i64    memsetPtr         Pointer to libc memset
  *  72     i64    interruptFlag     Non-zero = interrupt requested
+ *  80     i64    memoryPagesPtr    Shared memory: pointer to its current page count (i32)
  * 200     i64    globalsPtr        Pointer to globals buffer
  * 208     i64    memGrowPtr        Upcall stub for memory.grow
  * 216     i32    memoryPages       Current memory page count
@@ -52,6 +53,7 @@ private CtxBuffer() {}
     public static final int MEMMOVE_PTR = 56;
     public static final int MEMSET_PTR = 64;
     public static final int INTERRUPT_FLAG = 72;
+    public static final int MEMORY_PAGES_PTR = 80;
 
     public static final int GLOBALS_PTR = 200;
     public static final int MEM_GROW_PTR = 208;
diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeAtomics.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeAtomics.java
new file mode 100644
index 000000000..e68cd4d6a
--- /dev/null
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeAtomics.java
@@ -0,0 +1,215 @@
+package run.endive.redline.experimental.api.internal;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/** Hardware atomics on a native memory's mapping, via a direct buffer; callers check bounds. */
+public final class NativeAtomics {
+
+    private static final VarHandle INT =
+            MethodHandles.byteBufferViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN);
+    private static final VarHandle LONG =
+            MethodHandles.byteBufferViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN);
+
+    private static final int ADD = 0;
+    private static final int AND = 1;
+    private static final int OR = 2;
+    private static final int XOR = 3;
+    private static final int XCHG = 4;
+
+    private final ByteBuffer buffer;
+
+    public NativeAtomics(ByteBuffer buffer) {
+        this.buffer = buffer;
+    }
+
+    public int readInt(int addr) {
+        return (int) INT.getVolatile(buffer, addr);
+    }
+
+    public long readLong(int addr) {
+        return (long) LONG.getVolatile(buffer, addr);
+    }
+
+    public short readShort(int addr) {
+        return (short) narrowGet(addr, 16);
+    }
+
+    public byte readByte(int addr) {
+        return (byte) narrowGet(addr, 8);
+    }
+
+    public void writeInt(int addr, int value) {
+        INT.setVolatile(buffer, addr, value);
+    }
+
+    public void writeLong(int addr, long value) {
+        LONG.setVolatile(buffer, addr, value);
+    }
+
+    public void writeShort(int addr, short value) {
+        narrowUpdate(addr, 16, XCHG, value);
+    }
+
+    public void writeByte(int addr, byte value) {
+        narrowUpdate(addr, 8, XCHG, value);
+    }
+
+    public int addInt(int addr, int delta) {
+        return (int) INT.getAndAdd(buffer, addr, delta);
+    }
+
+    public int andInt(int addr, int mask) {
+        return (int) INT.getAndBitwiseAnd(buffer, addr, mask);
+    }
+
+    public int orInt(int addr, int mask) {
+        return (int) INT.getAndBitwiseOr(buffer, addr, mask);
+    }
+
+    public int xorInt(int addr, int mask) {
+        return (int) INT.getAndBitwiseXor(buffer, addr, mask);
+    }
+
+    public int xchgInt(int addr, int value) {
+        return (int) INT.getAndSet(buffer, addr, value);
+    }
+
+    public int cmpxchgInt(int addr, int expected, int replacement) {
+        return (int) INT.compareAndExchange(buffer, addr, expected, replacement);
+    }
+
+    public long addLong(int addr, long delta) {
+        return (long) LONG.getAndAdd(buffer, addr, delta);
+    }
+
+    public long andLong(int addr, long mask) {
+        return (long) LONG.getAndBitwiseAnd(buffer, addr, mask);
+    }
+
+    public long orLong(int addr, long mask) {
+        return (long) LONG.getAndBitwiseOr(buffer, addr, mask);
+    }
+
+    public long xorLong(int addr, long mask) {
+        return (long) LONG.getAndBitwiseXor(buffer, addr, mask);
+    }
+
+    public long xchgLong(int addr, long value) {
+        return (long) LONG.getAndSet(buffer, addr, value);
+    }
+
+    public long cmpxchgLong(int addr, long expected, long replacement) {
+        return (long) LONG.compareAndExchange(buffer, addr, expected, replacement);
+    }
+
+    public short addShort(int addr, short delta) {
+        return (short) narrowUpdate(addr, 16, ADD, delta);
+    }
+
+    public short andShort(int addr, short mask) {
+        return (short) narrowUpdate(addr, 16, AND, mask);
+    }
+
+    public short orShort(int addr, short mask) {
+        return (short) narrowUpdate(addr, 16, OR, mask);
+    }
+
+    public short xorShort(int addr, short mask) {
+        return (short) narrowUpdate(addr, 16, XOR, mask);
+    }
+
+    public short xchgShort(int addr, short value) {
+        return (short) narrowUpdate(addr, 16, XCHG, value);
+    }
+
+    public short cmpxchgShort(int addr, short expected, short replacement) {
+        return (short) narrowCmpxchg(addr, 16, expected, replacement);
+    }
+
+    public byte addByte(int addr, byte delta) {
+        return (byte) narrowUpdate(addr, 8, ADD, delta);
+    }
+
+    public byte andByte(int addr, byte mask) {
+        return (byte) narrowUpdate(addr, 8, AND, mask);
+    }
+
+    public byte orByte(int addr, byte mask) {
+        return (byte) narrowUpdate(addr, 8, OR, mask);
+    }
+
+    public byte xorByte(int addr, byte mask) {
+        return (byte) narrowUpdate(addr, 8, XOR, mask);
+    }
+
+    public byte xchgByte(int addr, byte value) {
+        return (byte) narrowUpdate(addr, 8, XCHG, value);
+    }
+
+    public byte cmpxchgByte(int addr, byte expected, byte replacement) {
+        return (byte) narrowCmpxchg(addr, 8, expected, replacement);
+    }
+
+    private int narrowGet(int addr, int bits) {
+        int shift = shift(addr, bits);
+        return ((int) INT.getVolatile(buffer, addr & ~3) >>> shift) & ((1 << bits) - 1);
+    }
+
+    // Byte and short have no atomic VarHandle modes: CAS the enclosing aligned int.
+    private int narrowUpdate(int addr, int bits, int op, int operand) {
+        int base = addr & ~3;
+        int shift = shift(addr, bits);
+        int mask = ((1 << bits) - 1) << shift;
+        while (true) {
+            int word = (int) INT.getVolatile(buffer, base);
+            int old = (word & mask) >>> shift;
+            int updated = (word & ~mask) | ((apply(op, old, operand) << shift) & mask);
+            if (INT.compareAndSet(buffer, base, word, updated)) {
+                return old;
+            }
+        }
+    }
+
+    private int narrowCmpxchg(int addr, int bits, int expected, int replacement) {
+        int base = addr & ~3;
+        int shift = shift(addr, bits);
+        int valueMask = (1 << bits) - 1;
+        int mask = valueMask << shift;
+        while (true) {
+            int word = (int) INT.getVolatile(buffer, base);
+            int old = (word & mask) >>> shift;
+            if (old != (expected & valueMask)) {
+                return old;
+            }
+            int updated = (word & ~mask) | ((replacement & valueMask) << shift);
+            if (INT.compareAndSet(buffer, base, word, updated)) {
+                return old;
+            }
+        }
+    }
+
+    private static int shift(int addr, int bits) {
+        if ((addr & (bits / 8 - 1)) != 0) {
+            throw new IllegalStateException("Misaligned atomic access at address " + addr);
+        }
+        return (addr & 3) * 8;
+    }
+
+    private static int apply(int op, int old, int operand) {
+        switch (op) {
+            case ADD:
+                return old + operand;
+            case AND:
+                return old & operand;
+            case OR:
+                return old | operand;
+            case XOR:
+                return old ^ operand;
+            default:
+                return operand;
+        }
+    }
+}
diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeWaiters.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeWaiters.java
new file mode 100644
index 000000000..9539f8953
--- /dev/null
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/NativeWaiters.java
@@ -0,0 +1,100 @@
+package run.endive.redline.experimental.api.internal;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BooleanSupplier;
+import run.endive.runtime.WasmInterruptedException;
+import run.endive.wasm.WasmEngineException;
+
+/** memory.atomic.wait and notify for a native memory, as ByteArrayMemory implements them. */
+public final class NativeWaiters {
+
+    // Guarded by its own monitor; 0 <= pendingWakeups <= waiterCount
+    private static final class WaitState {
+        int waiterCount;
+        int pendingWakeups;
+        // a waiter only consumes a wakeup minted after it registered
+        long generation;
+    }
+
+    private final Map waitStates = new ConcurrentHashMap<>();
+    private final boolean shared;
+
+    public NativeWaiters(boolean shared) {
+        this.shared = shared;
+    }
+
+    public Object monitor(int address) {
+        return waitStates.computeIfAbsent(address, k -> new WaitState());
+    }
+
+    /** Waits while {@code condition} holds: 0 woken, 1 not-equal, 2 timed out. */
+    public int waitOn(int address, BooleanSupplier condition, long timeout) {
+        if (!shared) {
+            throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported.");
+        }
+
+        long deadline = (timeout < 0) ? Long.MAX_VALUE : System.nanoTime() + timeout;
+        WaitState state = waitStates.computeIfAbsent(address, k -> new WaitState());
+
+        synchronized (state) {
+            if (!condition.getAsBoolean()) {
+                return 1;
+            }
+
+            state.waiterCount++;
+            final long arrivalGeneration = state.generation;
+            try {
+                while (state.pendingWakeups == 0 || state.generation == arrivalGeneration) {
+                    long remaining = deadline - System.nanoTime();
+                    if (remaining <= 0) {
+                        return 2;
+                    }
+                    long millis = Math.max(remaining / 1_000_000L, 0);
+                    int nanos = Math.max((int) (remaining % 1_000_000L), 0);
+                    try {
+                        state.wait(millis, nanos);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new WasmInterruptedException("Thread interrupted");
+                    }
+                }
+                return 0;
+            } finally {
+                // Only a wakeup minted while this thread waited is its to consume.
+                if (state.pendingWakeups > 0 && state.generation != arrivalGeneration) {
+                    state.pendingWakeups--;
+                }
+                state.waiterCount--;
+                assert (0 <= state.pendingWakeups);
+                assert (state.pendingWakeups <= state.waiterCount);
+            }
+        }
+    }
+
+    /** Wakes up to {@code maxThreads} waiters (all if negative); returns how many. */
+    public int notify(int address, int maxThreads) {
+        if (!shared) {
+            return 0;
+        }
+
+        WaitState state = waitStates.get(address);
+        if (state == null) {
+            return 0;
+        }
+
+        synchronized (state) {
+            int actualWaiters = state.waiterCount - state.pendingWakeups;
+            if (actualWaiters == 0) {
+                return 0;
+            }
+            int toWake = maxThreads < 0 ? actualWaiters : Math.min(actualWaiters, maxThreads);
+            // waiters arriving after this notify wait for the next one
+            state.pendingWakeups += toWake;
+            state.generation++;
+            assert (state.pendingWakeups <= state.waiterCount);
+            state.notifyAll();
+            return toWake;
+        }
+    }
+}
diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
index 3c10e0eac..48b524b04 100644
--- a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
+++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
@@ -25,6 +25,7 @@ final class EmitContext {
     final Map sigRefCache;
     final boolean multiReturn;
     final int[] canonicalTypeMap;
+    final boolean sharedMemory;
 
     EmitContext(
             CraneliftBridge bridge,
@@ -37,7 +38,8 @@ final class EmitContext {
             int ctxPtrVar,
             Map sigRefCache,
             boolean multiReturn,
-            int[] canonicalTypeMap) {
+            int[] canonicalTypeMap,
+            boolean sharedMemory) {
         this.bridge = bridge;
         this.valueStack = valueStack;
         this.module = module;
@@ -49,6 +51,19 @@ final class EmitContext {
         this.sigRefCache = sigRefCache;
         this.multiReturn = multiReturn;
         this.canonicalTypeMap = canonicalTypeMap;
+        this.sharedMemory = sharedMemory;
+    }
+
+    /** The current page count; a shared memory's lives with the memory, as others may grow it. */
+    int emitLoadMemoryPages() {
+        var b = bridge.exports();
+        int zero = b.emitIconst32(0);
+        int ctxPtr = b.useVar(ctxPtrVar);
+        if (!sharedMemory) {
+            return b.emitLoadI32(ctxPtr, zero, CtxBuffer.MEMORY_PAGES);
+        }
+        int pagesPtr = b.emitLoadI64(ctxPtr, zero, CtxBuffer.MEMORY_PAGES_PTR);
+        return b.emitLoadI32(pagesPtr, zero, 0);
     }
 
     // --- Helpers used by emitters ---
diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java
index 157f9324d..8c8a08267 100644
--- a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java
+++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java
@@ -24,6 +24,7 @@
 import run.endive.wasm.types.ExternalType;
 import run.endive.wasm.types.FunctionImport;
 import run.endive.wasm.types.FunctionType;
+import run.endive.wasm.types.MemoryImport;
 import run.endive.wasm.types.OpCode;
 import run.endive.wasm.types.ValType;
 
@@ -41,6 +42,7 @@ public final class NativeCompiler {
     private final WasmModule module;
     private final int numImports;
     private final int[] canonicalTypeMap;
+    private final boolean sharedMemory;
 
     private NativeCompiler(CraneliftBridge bridge, String triple, WasmModule module) {
         this.bridge = bridge;
@@ -52,6 +54,20 @@ private NativeCompiler(CraneliftBridge bridge, String triple, WasmModule module)
                                 .filter(i -> i.importType() == ExternalType.FUNCTION)
                                 .count();
         this.canonicalTypeMap = TypeMapUtils.buildCanonicalTypeMap(module);
+        this.sharedMemory = isMemoryShared(module);
+    }
+
+    // Memory 0 is the first memory import, or else the first defined memory
+    private static boolean isMemoryShared(WasmModule module) {
+        for (int i = 0; i < module.importSection().importCount(); i++) {
+            var imp = module.importSection().getImport(i);
+            if (imp.importType() == ExternalType.MEMORY) {
+                return ((MemoryImport) imp).limits().shared();
+            }
+        }
+        return module.memorySection()
+                .map(ms -> ms.memoryCount() > 0 && ms.getMemory(0).limits().shared())
+                .orElse(false);
     }
 
     // --- Control frame ---
@@ -434,7 +450,8 @@ private byte[] compileFunction(int bodyIndex) {
                         ctxPtrVar,
                         new HashMap<>(),
                         multiReturn,
-                        canonicalTypeMap);
+                        canonicalTypeMap,
+                        sharedMemory);
 
         // --- Emission loop ---
         Deque controlStack = new ArrayDeque<>();
diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
index 9e336a93f..aadda55be 100644
--- a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
+++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
@@ -452,14 +452,7 @@ private static void emitBoundsCheck(EmitContext ctx, int addr, int offset, int a
                                                 (int) (endOffset >>> 32)));
 
         // Load memory size in bytes: memPages * 65536
-        int zero = ctx.bridge.exports().emitIconst32(0);
-        int memPages =
-                ctx.bridge
-                        .exports()
-                        .emitLoadI32(
-                                ctx.bridge.exports().useVar(ctx.ctxPtrVar),
-                                zero,
-                                CtxBuffer.MEMORY_PAGES);
+        int memPages = ctx.emitLoadMemoryPages();
         int memPages64 = ctx.bridge.exports().emitUextendI64(memPages);
         int memSize =
                 ctx.bridge.exports().emitIshl(memPages64, ctx.bridge.exports().emitIconst64(16, 0));
@@ -636,15 +629,7 @@ static void emitGlobalSet(EmitContext ctx, AnnotatedInstruction ins) {
     // --- Memory operations ---
 
     static void emitMemorySize(EmitContext ctx) {
-        int zero = ctx.bridge.exports().emitIconst32(0);
-        int pages =
-                ctx.bridge
-                        .exports()
-                        .emitLoadI32(
-                                ctx.bridge.exports().useVar(ctx.ctxPtrVar),
-                                zero,
-                                CtxBuffer.MEMORY_PAGES);
-        ctx.valueStack.push(pages);
+        ctx.valueStack.push(ctx.emitLoadMemoryPages());
     }
 
     static void emitMemoryGrow(EmitContext ctx) {
@@ -667,6 +652,7 @@ static void emitMemoryGrow(EmitContext ctx) {
         int growSig = ctx.getOrCreateTrampolineSigRef();
         ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.ctxPtrVar));
         int rawResult = ctx.bridge.exports().emitCallIndirect(growSig, memGrowPtr);
+        emitTrapCheck(ctx);
         int result = ctx.bridge.exports().emitIreduceI32(rawResult);
         ctx.valueStack.push(result);
         int newMemBase =
@@ -1257,6 +1243,7 @@ static void emitTableGrow(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, tableOpsPtr);
+        emitTrapCheck(ctx);
 
         b.emitJumpWithArg(mergeBlock, oldSize);
 
@@ -1304,6 +1291,7 @@ static void emitTableFill(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     static void emitTableCopy(EmitContext ctx, AnnotatedInstruction ins) {
@@ -1350,6 +1338,7 @@ static void emitTableCopy(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     static void emitTableInit(EmitContext ctx, AnnotatedInstruction ins) {
@@ -1380,6 +1369,7 @@ static void emitTableInit(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     static void emitElemDrop(EmitContext ctx, AnnotatedInstruction ins) {
@@ -1401,6 +1391,7 @@ static void emitElemDrop(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     // --- Bulk memory operations (inline native memmove/memset) ---
@@ -1415,8 +1406,7 @@ private static void emitBulkBoundsCheck(EmitContext ctx, int addr, int size) {
         int size64 = b.emitUextendI64(size);
         int end = b.emitIadd(addr64, size64);
 
-        int zero = b.emitIconst32(0);
-        int memPages = b.emitLoadI32(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.MEMORY_PAGES);
+        int memPages = ctx.emitLoadMemoryPages();
         int memPages64 = b.emitUextendI64(memPages);
         int memSize = b.emitIshl(memPages64, b.emitIconst64(16, 0));
 
@@ -1504,6 +1494,7 @@ static void emitMemoryInit(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     static void emitDataDrop(EmitContext ctx, AnnotatedInstruction ins) {
@@ -1524,6 +1515,7 @@ static void emitDataDrop(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
     }
 
     // --- Atomic operations ---
@@ -1766,6 +1758,7 @@ static void emitAtomicWait32(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
         ctx.valueStack.push(b.emitIreduceI32(rawResult));
     }
 
@@ -1796,6 +1789,7 @@ static void emitAtomicWait64(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
         ctx.valueStack.push(b.emitIreduceI32(rawResult));
     }
 
@@ -1824,6 +1818,7 @@ static void emitAtomicNotify(EmitContext ctx, AnnotatedInstruction ins) {
         int trampolineSig = ctx.getOrCreateTrampolineSigRef();
         b.pushCallArg(b.useVar(ctx.ctxPtrVar));
         int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr);
+        emitTrapCheck(ctx);
         ctx.valueStack.push(b.emitIreduceI32(rawResult));
     }
 
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/GuestSeesGrowthTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/GuestSeesGrowthTest.java
new file mode 100644
index 000000000..78c4d06ed
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/GuestSeesGrowthTest.java
@@ -0,0 +1,121 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.HostFunction;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Instance;
+import run.endive.runtime.Memory;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.FunctionType;
+import run.endive.wasm.types.MemoryLimits;
+
+/** Compiled code sees growth by another instance sharing the memory, or by a host import. */
+public class GuestSeesGrowthTest {
+
+    private static final int PAGE = 65536;
+
+    private static final WasmModule SHARED = parse("shared-memory-grow");
+    private static final NativeCode SHARED_CODE = compile(SHARED);
+    private static final WasmModule HOST_GROW = parse("host-grow");
+    private static final NativeCode HOST_GROW_CODE = compile(HOST_GROW);
+
+    private static final ExecutorService POOL = Executors.newCachedThreadPool();
+
+    @AfterAll
+    public static void shutdown() {
+        POOL.shutdownNow();
+    }
+
+    @Test
+    public void aRunningInstanceReadsThePageAnotherInstanceGrew() throws Exception {
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+        try (var waiter = shared(memory);
+                var grower = shared(memory)) {
+            var read = POOL.submit(() -> waiter.export("waitThenRead").apply(0, PAGE + 16)[0]);
+            Thread.sleep(50);
+            grower.export("growAndPublish").apply(0, PAGE + 16, 42);
+            assertEquals(42L, read.get(30, TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    public void aRunningInstanceSeesTheSizeAnotherInstanceGrew() throws Exception {
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+        try (var waiter = shared(memory);
+                var grower = shared(memory)) {
+            var size = POOL.submit(() -> waiter.export("waitThenSize").apply(0)[0]);
+            Thread.sleep(50);
+            grower.export("growAndPublish").apply(0, PAGE + 16, 42);
+            assertEquals(2L, size.get(30, TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    public void theGuestReadsThePageAHostImportGrew() {
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2));
+        try (var instance = hostGrow(memory, 77)) {
+            assertEquals(77L, instance.export("growThenRead").apply(PAGE + 8)[0]);
+        }
+    }
+
+    @Test
+    public void theGuestSeesTheSizeAHostImportGrew() {
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2));
+        try (var instance = hostGrow(memory, 77)) {
+            assertEquals(2L, instance.export("growThenSize").apply()[0]);
+        }
+    }
+
+    private static Instance shared(Memory memory) {
+        return JffiNativeMachineFactory.builder(SHARED)
+                .withPrecompiledCode(SHARED_CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .build())
+                .build();
+    }
+
+    private static Instance hostGrow(Memory memory, int value) {
+        var grow =
+                new HostFunction(
+                        "env",
+                        "grow",
+                        FunctionType.of(List.of(), List.of()),
+                        (instance, args) -> {
+                            memory.grow(1);
+                            memory.writeI32(PAGE + 8, value);
+                            return null;
+                        });
+        return JffiNativeMachineFactory.builder(HOST_GROW)
+                .withPrecompiledCode(HOST_GROW_CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .addFunction(grow)
+                                .build())
+                .build();
+    }
+
+    private static WasmModule parse(String name) {
+        return Parser.parse(CorpusResources.getResource("compiled/" + name + ".wat.wasm"));
+    }
+
+    private static NativeCode compile(WasmModule module) {
+        return NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), module);
+    }
+}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java
index b567d70aa..5c07372dc 100644
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java
@@ -1,10 +1,13 @@
 package run.endive.redline.experimental.runner.jffi.internal;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import org.junit.jupiter.api.Test;
 import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.Memory;
 import run.endive.runtime.WasmRuntimeException;
+import run.endive.wasm.UninstantiableException;
 import run.endive.wasm.types.MemoryLimits;
 
 /**
@@ -41,4 +44,31 @@ public void insideTheMemoryIsUntouched() {
         memory.writeI32(PAGE - 4, 0x11223344);
         org.junit.jupiter.api.Assertions.assertEquals(0x11223344, memory.readInt(PAGE - 4));
     }
+
+    @Test
+    public void wideAccessesAreLittleEndianAtOddOffsets() {
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 1));
+        for (int i = 0; i < 8; i++) {
+            memory.writeByte(101 + i, (byte) (i + 1));
+        }
+        assertEquals((short) 0x0201, memory.readShort(101));
+        assertEquals(0x0807060504030201L, memory.readLong(101));
+
+        memory.writeShort(201, (short) 0x0201);
+        memory.writeLong(301, 0x0807060504030201L);
+        assertEquals(1, memory.read(201));
+        assertEquals(2, memory.read(202));
+        for (int i = 0; i < 8; i++) {
+            assertEquals(i + 1, memory.read(301 + i));
+        }
+    }
+
+    @Test
+    public void anInitialSizePastTheRuntimeLimitIsRejected() {
+        assertThrows(
+                UninstantiableException.class,
+                () ->
+                        JffiNativeMachineFactory.createMemory(
+                                new MemoryLimits(Memory.RUNTIME_MAX_PAGES + 1)));
+    }
 }
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryInitTrapTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryInitTrapTest.java
new file mode 100644
index 000000000..feda1d8ff
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryInitTrapTest.java
@@ -0,0 +1,54 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.Instance;
+import run.endive.runtime.WasmRuntimeException;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+
+/** A trap raised while the runner handles memory.init has to stop the guest there. */
+public class MemoryInitTrapTest {
+
+    private static final WasmModule MODULE =
+            Parser.parse(CorpusResources.getResource("compiled/memory-init-trap.wat.wasm"));
+    private static final NativeCode CODE =
+            NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), MODULE);
+
+    @Test
+    public void anOutOfBoundsInitStopsBeforeTheNextInstruction() {
+        try (var instance = instance()) {
+            assertThrows(
+                    WasmRuntimeException.class,
+                    () -> instance.export("initOutOfBoundsThenStore").apply());
+            assertEquals(0L, instance.export("read").apply(100)[0]);
+        }
+    }
+
+    @Test
+    public void anEmptyInitPastTheEndOfADroppedSegmentTraps() {
+        try (var instance = instance()) {
+            assertThrows(
+                    WasmRuntimeException.class,
+                    () -> instance.export("dropThenInitPastEnd").apply());
+        }
+    }
+
+    @Test
+    public void anEmptyInitAtTheStartOfADroppedSegmentIsAllowed() {
+        try (var instance = instance()) {
+            instance.export("dropThenInitNothing").apply();
+        }
+    }
+
+    private static Instance instance() {
+        return JffiNativeMachineFactory.builder(MODULE).withPrecompiledCode(CODE).build();
+    }
+}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeAtomicsTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeAtomicsTest.java
new file mode 100644
index 000000000..24fa7682e
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeAtomicsTest.java
@@ -0,0 +1,145 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Instance;
+import run.endive.runtime.Memory;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.MemoryLimits;
+
+/** Host atomics on a native memory have to be atomic against the guest's own. */
+public class NativeAtomicsTest {
+
+    private static final int N = 1_000_000;
+
+    private static final WasmModule MODULE =
+            Parser.parse(CorpusResources.getResource("compiled/atomic-counter.wat.wasm"));
+    private static final NativeCode CODE =
+            NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), MODULE);
+
+    private static final ExecutorService POOL = Executors.newSingleThreadExecutor();
+
+    @AfterAll
+    public static void shutdown() {
+        POOL.shutdownNow();
+    }
+
+    @Test
+    public void hostAndGuestIntAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add32", () -> memory.atomicAddInt(0, 1));
+        assertEquals(2 * N, memory.readInt(0));
+    }
+
+    @Test
+    public void hostAndGuestLongAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add64", () -> memory.atomicAddLong(16, 1L));
+        assertEquals(2L * N, memory.readLong(16));
+    }
+
+    @Test
+    public void hostAndGuestShortAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add16", () -> memory.atomicAddShort(10, (short) 1));
+        assertEquals((short) (2 * N), memory.readShort(10));
+        assertEquals(0, memory.readShort(8), "the other half of the int is untouched");
+    }
+
+    @Test
+    public void hostAndGuestByteAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add8", () -> memory.atomicAddByte(13, (byte) 1));
+        assertEquals((byte) (2 * N), memory.read(13));
+        assertEquals(0, memory.read(12), "neighbouring bytes are untouched");
+        assertEquals(0, memory.read(14));
+        assertEquals(0, memory.read(15));
+    }
+
+    @Test
+    public void narrowOperationsOnlyTouchTheirBytes() {
+        var memory = sharedMemory();
+        memory.writeI32(0, 0x44332211);
+
+        assertEquals((byte) 0x22, memory.atomicXchgByte(1, (byte) 0x99));
+        assertEquals((byte) 0x33, memory.atomicCmpxchgByte(2, (byte) 0x00, (byte) 0x77));
+        assertEquals((byte) 0x33, memory.atomicCmpxchgByte(2, (byte) 0x33, (byte) 0x77));
+        assertEquals((short) 0x9911, memory.atomicOrShort(0, (short) 0x0002));
+        assertEquals(0x44779913, memory.atomicReadInt(0));
+
+        assertEquals((short) 0x4477, memory.atomicAndShort(2, (short) 0x00FF));
+        assertEquals((short) 0x0077, memory.atomicXorShort(2, (short) 0x0101));
+        assertEquals((short) 0x0176, memory.atomicReadShort(2));
+        memory.atomicWriteByte(3, (byte) 0xAB);
+        assertEquals((byte) 0xAB, memory.atomicReadByte(3));
+        assertEquals(0xAB769913, memory.readInt(0));
+    }
+
+    @Test
+    public void wideOperationsBehaveLikeTheirScalarForms() {
+        var memory = sharedMemory();
+        memory.atomicWriteLong(8, 0x0F0F0F0F0F0F0F0FL);
+        assertEquals(0x0F0F0F0F0F0F0F0FL, memory.atomicAndLong(8, 0x00FF00FF00FF00FFL));
+        assertEquals(0x000F000F000F000FL, memory.atomicOrLong(8, 0x1000000000000000L));
+        assertEquals(0x100F000F000F000FL, memory.atomicXorLong(8, 0x100F000F000F000FL));
+        assertEquals(0L, memory.atomicCmpxchgLong(8, 0L, 5L));
+        assertEquals(5L, memory.atomicXchgLong(8, 6L));
+        assertEquals(6L, memory.atomicReadLong(8));
+
+        memory.atomicWriteInt(4, 10);
+        assertEquals(10, memory.atomicCmpxchgInt(4, 11, 12));
+        assertEquals(10, memory.atomicCmpxchgInt(4, 10, 12));
+        assertEquals(12, memory.atomicXchgInt(4, 1));
+        assertEquals(1, memory.atomicOrInt(4, 6));
+        assertEquals(7, memory.atomicAndInt(4, 5));
+        assertEquals(5, memory.atomicXorInt(4, 1));
+        assertEquals(4, memory.atomicReadInt(4));
+    }
+
+    private static Memory sharedMemory() {
+        return JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+    }
+
+    private static void race(Memory memory, String export, Runnable hostIncrement)
+            throws Exception {
+        try (var instance = instance(memory)) {
+            var start = new CyclicBarrier(2);
+            var guest =
+                    POOL.submit(
+                            () -> {
+                                start.await();
+                                instance.export(export).apply(N);
+                                return null;
+                            });
+            start.await();
+            for (int i = 0; i < N; i++) {
+                hostIncrement.run();
+            }
+            guest.get(60, TimeUnit.SECONDS);
+        }
+    }
+
+    private static Instance instance(Memory memory) {
+        return JffiNativeMachineFactory.builder(MODULE)
+                .withPrecompiledCode(CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .build())
+                .build();
+    }
+}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeMemoryConcurrencyTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeMemoryConcurrencyTest.java
new file mode 100644
index 000000000..e31032598
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeMemoryConcurrencyTest.java
@@ -0,0 +1,83 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.wasm.types.MemoryLimits;
+
+/** A shared memory is grown and read by several host threads at once. */
+public class NativeMemoryConcurrencyTest {
+
+    private static final int PAGE = 65536;
+
+    @Test
+    public void concurrentGrowsHandOutEveryPageOnce() throws Exception {
+        int threads = 8;
+        int growsPerThread = 200;
+        var memory =
+                JffiNativeMachineFactory.createMemory(
+                        new MemoryLimits(0, threads * growsPerThread, true));
+        Set handedOut = ConcurrentHashMap.newKeySet();
+        var start = new CountDownLatch(1);
+        ExecutorService pool = Executors.newFixedThreadPool(threads);
+        try {
+            var futures = new java.util.ArrayList>();
+            for (int t = 0; t < threads; t++) {
+                futures.add(
+                        pool.submit(
+                                () -> {
+                                    start.await();
+                                    for (int i = 0; i < growsPerThread; i++) {
+                                        assertTrue(handedOut.add(memory.grow(1)));
+                                    }
+                                    return null;
+                                }));
+            }
+            start.countDown();
+            for (var f : futures) {
+                f.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+        assertEquals(threads * growsPerThread, memory.pages());
+        assertEquals(threads * growsPerThread, handedOut.size());
+    }
+
+    // The reader polls with plain reads, the pattern that let the JIT keep a stale view
+    @Test
+    public void aHostReaderCanReachPagesAnotherThreadGrew() throws Exception {
+        ExecutorService pool = Executors.newSingleThreadExecutor();
+        try {
+            for (int round = 0; round < 20; round++) {
+                var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+                Future reader =
+                        pool.submit(
+                                () -> {
+                                    long deadline = System.nanoTime() + 10_000_000_000L;
+                                    while (memory.readInt(0) != 1) {
+                                        if (System.nanoTime() > deadline) {
+                                            throw new AssertionError("flag never seen");
+                                        }
+                                    }
+                                    return memory.readInt(PAGE + 16);
+                                });
+                Thread.sleep(5);
+                memory.grow(1);
+                memory.atomicWriteInt(0, 1);
+                reader.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+    }
+}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeWaitNotifyTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeWaitNotifyTest.java
new file mode 100644
index 000000000..ae690beea
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/NativeWaitNotifyTest.java
@@ -0,0 +1,93 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.WasmInterruptedException;
+import run.endive.wasm.Parser;
+import run.endive.wasm.types.MemoryLimits;
+
+public class NativeWaitNotifyTest {
+
+    // Ported from the runtime's MemoryTest (#107): a late wait can't take the parked one's wakeup
+    @Test
+    public void notifyWakesTheParkedThreadNotALateArrival() throws Exception {
+        final int addr = 0;
+        final int sentinel = 42;
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+
+        for (int round = 0; round < 200; round++) {
+            memory.atomicWriteInt(addr, sentinel);
+            var parkedReady = new CountDownLatch(1);
+            var parked =
+                    new Thread(
+                            () -> {
+                                parkedReady.countDown();
+                                memory.atomicWait(addr, sentinel, 100_000_000L);
+                            });
+            parked.setDaemon(true);
+            parked.start();
+            parkedReady.await();
+            Thread.sleep(1);
+
+            if (memory.atomicNotify(addr, 1) == 0) {
+                continue;
+            }
+            assertEquals(2, memory.atomicWait(addr, sentinel, 0L), "round " + round);
+        }
+    }
+
+    @Test
+    public void interruptingAGuestBlockedInAWaitStopsIt() throws Exception {
+        var module = Parser.parse(CorpusResources.getResource("compiled/threads-example.wat.wasm"));
+        var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+        try (var instance =
+                JffiNativeMachineFactory.builder(module)
+                        .withPrecompiledCode(
+                                NativeCompiler.compile(
+                                        RedlineTarget.detectHost().orElseThrow().triple(), module))
+                        .withImportValues(
+                                ImportValues.builder()
+                                        .addMemory(new ImportMemory("env", "memory", memory))
+                                        .build())
+                        .build()) {
+            // locked, so lockMutex parks in memory.atomic.wait32
+            memory.atomicWriteInt(0, 1);
+            var failure = new AtomicReference();
+            var blocked =
+                    new Thread(
+                            () -> {
+                                try {
+                                    instance.export("lockMutex").apply(0);
+                                } catch (Throwable t) {
+                                    failure.set(t);
+                                }
+                            });
+            blocked.setDaemon(true);
+            blocked.start();
+
+            long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
+            while (blocked.getState() != Thread.State.WAITING
+                    && blocked.getState() != Thread.State.TIMED_WAITING) {
+                assertTrue(System.nanoTime() < deadline, "never parked");
+                Thread.sleep(1);
+            }
+            blocked.interrupt();
+            blocked.join(TimeUnit.SECONDS.toMillis(30));
+
+            assertTrue(!blocked.isAlive(), "the interrupted guest kept running");
+            assertInstanceOf(WasmInterruptedException.class, failure.get());
+        }
+    }
+}
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
index 7b55a50cc..3ba3eceb3 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
@@ -415,6 +415,14 @@ private static void setClosureReturn(Closure.Buffer buf, long result, FunctionTy
         }
     }
 
+    // Host code may have grown the memory since compiled code last saw its size
+    private void refreshMemoryPages() {
+        var mem = instance.memory();
+        if (mem != null) {
+            MEM.putInt(ctxBufferAddr + CtxBuffer.MEMORY_PAGES, mem.pages());
+        }
+    }
+
     private long importDispatchDirect(int funcId) {
         try {
             int argCount = MEM.getInt(ctxBufferAddr + CtxBuffer.ARG_COUNT);
@@ -425,6 +433,7 @@ private long importDispatchDirect(int funcId) {
             if (funcId < numImports) {
                 var importFunc = instance.imports().function(funcId);
                 long[] result = importFunc.handle().apply(instance, args);
+                refreshMemoryPages();
                 if (result == null || result.length == 0) {
                     return 0L;
                 }
@@ -914,6 +923,9 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
                 if (mem instanceof JffiNativeMemory) {
                     cachedMemBase = ((JffiNativeMemory) mem).nativeAddress();
                     MEM.putLong(ctxBufferAddr + CtxBuffer.MEM_BASE_ADDR, cachedMemBase);
+                    MEM.putLong(
+                            ctxBufferAddr + CtxBuffer.MEMORY_PAGES_PTR,
+                            ((JffiNativeMemory) mem).pagesAddress());
                 } else if (mem != null) {
                     throw new WasmEngineException(foreignImportMessage("memory", mem));
                 } else {
@@ -921,11 +933,7 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
                 }
                 memBaseInitialized = true;
             }
-            // MEMORY_PAGES can change via grow() — must refresh every call
-            var mem = instance.memory();
-            if (mem != null) {
-                MEM.putInt(ctxBufferAddr + CtxBuffer.MEMORY_PAGES, mem.pages());
-            }
+            refreshMemoryPages();
 
             if (Thread.currentThread().isInterrupted()) {
                 throw new WasmInterruptedException("Thread interrupted");
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
index 5db8bc26b..bd6c0e3ee 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
@@ -9,8 +9,10 @@
 import com.kenai.jffi.MemoryIO;
 import com.kenai.jffi.PageManager;
 import com.kenai.jffi.Type;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
+import run.endive.redline.experimental.api.internal.NativeAtomics;
+import run.endive.redline.experimental.api.internal.NativeWaiters;
 import run.endive.runtime.Instance;
 import run.endive.runtime.Memory;
 import run.endive.wasm.UninstantiableException;
@@ -88,23 +90,31 @@ private static int winVirtualFree(long addr, long size, int freeType) {
         return (int) INV.invokeN3(VIRTUAL_FREE_CTX, VIRTUAL_FREE_ADDR, addr, size, freeType);
     }
 
-    private static final class WaitState {
-        int waiterCount;
-        int pendingWakeups;
-    }
+    private static final byte[] NO_DATA = new byte[0];
 
-    private final Map waitStates = new ConcurrentHashMap<>();
     private final MemoryLimits limits;
     private final long reservedAddress;
     private final int reservedOsPages;
     private final int osPerWasm; // OS pages per Wasm page
-    private int nPages;
+    private final NativeWaiters waiters;
+    private final NativeAtomics atomics;
+    // Page count compiled code reads for a shared memory, which other instances may grow
+    private long pagesCell;
+    private volatile int nPages;
     private DataSegment[] dataSegments;
 
     public JffiNativeMemory(MemoryLimits limits) {
         this.limits = limits;
-        this.nPages = limits.initialPages();
         int maxPages = Math.min(limits.maximumPages(), RUNTIME_MAX_PAGES);
+        if (limits.initialPages() > maxPages) {
+            throw new UninstantiableException(
+                    "memory of "
+                            + limits.initialPages()
+                            + " pages exceeds the limit of "
+                            + maxPages
+                            + " pages");
+        }
+        this.nPages = limits.initialPages();
         int osPageSize = (int) PM.pageSize();
         this.osPerWasm = PAGE_SIZE / osPageSize;
         this.reservedOsPages = maxPages * osPerWasm;
@@ -133,19 +143,37 @@ public JffiNativeMemory(MemoryLimits limits) {
                     throw new WasmEngineException("Failed to reserve memory pages");
                 }
                 if (nPages > 0) {
-                    PM.protectPages(
-                            reservedAddress,
-                            nPages * osPerWasm,
-                            PageManager.PROT_READ | PageManager.PROT_WRITE);
+                    try {
+                        PM.protectPages(
+                                reservedAddress,
+                                nPages * osPerWasm,
+                                PageManager.PROT_READ | PageManager.PROT_WRITE);
+                    } catch (Throwable t) {
+                        PM.freePages(reservedAddress, reservedOsPages);
+                        throw new WasmEngineException("Failed to commit initial memory pages", t);
+                    }
                 }
             }
         } else {
             this.reservedAddress = 0;
         }
+
+        this.waiters = new NativeWaiters(limits.shared());
+        this.pagesCell = MEM.allocateMemory(Integer.BYTES, true);
+        if (pagesCell == 0) {
+            freeReservation();
+            throw new WasmEngineException("Failed to allocate the memory page count");
+        }
+        MEM.putInt(pagesCell, nPages);
+        this.atomics =
+                new NativeAtomics(
+                        reservedOsPages > 0
+                                ? MEM.newDirectByteBuffer(
+                                        reservedAddress, reservedOsPages * osPageSize)
+                                : ByteBuffer.allocateDirect(0));
     }
 
-    @Override
-    public void close() {
+    private void freeReservation() {
         if (reservedOsPages > 0 && reservedAddress != 0) {
             if (IS_WINDOWS) {
                 winVirtualFree(reservedAddress, 0, MEM_RELEASE);
@@ -155,18 +183,32 @@ public void close() {
         }
     }
 
+    @Override
+    public void close() {
+        freeReservation();
+        if (pagesCell != 0) {
+            MEM.freeMemory(pagesCell);
+            pagesCell = 0;
+        }
+    }
+
     /** Get the native address of the memory buffer, for passing to native code. */
     public long nativeAddress() {
         return reservedAddress;
     }
 
+    /** Where compiled code reads the current page count of a shared memory. */
+    public long pagesAddress() {
+        return pagesCell;
+    }
+
     @Override
     public int pages() {
         return nPages;
     }
 
     @Override
-    public int grow(int size) {
+    public synchronized int grow(int size) {
         var prevPages = nPages;
         var numPages = prevPages + size;
         if (numPages > maximumPages() || numPages < prevPages) {
@@ -191,6 +233,8 @@ public int grow(int size) {
                         numPages * osPerWasm,
                         PageManager.PROT_READ | PageManager.PROT_WRITE);
             }
+            VarHandle.releaseFence();
+            MEM.putInt(pagesCell, numPages);
             this.nPages = numPages;
         } catch (Throwable t) {
             return -1;
@@ -220,87 +264,25 @@ public Object lock(int address) {
         if (!shared()) {
             return new Object();
         }
-        return waitStates.computeIfAbsent(address, k -> new WaitState());
-    }
-
-    private int waitOn(int address, java.util.function.BooleanSupplier condition, long timeout) {
-        if (!shared()) {
-            throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported.");
-        }
-
-        long deadline = (timeout < 0) ? Long.MAX_VALUE : System.nanoTime() + timeout;
-        WaitState state = waitStates.computeIfAbsent(address, k -> new WaitState());
-
-        synchronized (state) {
-            if (!condition.getAsBoolean()) {
-                return 1; // not-equal
-            }
-
-            state.waiterCount++;
-            try {
-                while (state.pendingWakeups == 0) {
-                    long remaining = deadline - System.nanoTime();
-                    if (remaining <= 0) {
-                        return 2; // timeout
-                    }
-                    long millis = Math.max(remaining / 1_000_000L, 0);
-                    int nanos = Math.max((int) (remaining % 1_000_000L), 0);
-                    try {
-                        state.wait(millis, nanos);
-                    } catch (InterruptedException ie) {
-                        Thread.currentThread().interrupt();
-                        throw new WasmEngineException("Thread interrupted");
-                    }
-                }
-                return 0; // woken
-            } finally {
-                if (state.pendingWakeups > 0) {
-                    state.pendingWakeups--;
-                }
-                state.waiterCount--;
-            }
-        }
+        return waiters.monitor(address);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int waitOn(int address, int expected, long timeout) {
-        return waitOn(address, () -> readInt(address) == expected, timeout);
+        return waiters.waitOn(address, () -> atomicReadInt(address) == expected, timeout);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int waitOn(int address, long expected, long timeout) {
-        return waitOn(address, () -> readLong(address) == expected, timeout);
+        return waiters.waitOn(address, () -> atomicReadLong(address) == expected, timeout);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int notify(int address, int maxThreads) {
-        if (!shared()) {
-            return 0;
-        }
-
-        WaitState state = waitStates.get(address);
-        if (state == null) {
-            return 0;
-        }
-
-        synchronized (state) {
-            int actualWaiters = state.waiterCount - state.pendingWakeups;
-            if (actualWaiters == 0) {
-                return 0;
-            }
-            int toWake;
-            if (maxThreads < 0) {
-                toWake = actualWaiters;
-            } else {
-                toWake = Math.min(actualWaiters, maxThreads);
-            }
-            state.pendingWakeups += toWake;
-            state.notifyAll();
-            return toWake;
-        }
+        return waiters.notify(address, maxThreads);
     }
 
     @Override
@@ -312,9 +294,9 @@ public void initialize(Instance instance, DataSegment[] dataSegments) {
         for (var s : dataSegments) {
             if (s instanceof ActiveDataSegment) {
                 var seg = (ActiveDataSegment) s;
-                var data = seg.data();
+                var data = seg.bytes();
                 var offset = (int) computeConstantValue(instance, seg.offsetInstructions())[0];
-                if (offset < 0 || offset + data.length > sizeInBytes()) {
+                if (offset < 0 || (long) offset + data.length > sizeInBytes()) {
                     throw new UninstantiableException(
                             "out of bounds memory access: offset="
                                     + offset
@@ -329,17 +311,13 @@ public void initialize(Instance instance, DataSegment[] dataSegments) {
     @Override
     public void initPassiveSegment(int segmentId, int dest, int offset, int size) {
         var seg = dataSegments[segmentId];
-        if (seg == null || seg == PassiveDataSegment.EMPTY) {
-            if (size > 0) {
-                throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
-            }
-            return;
-        }
-        write(dest, seg.data(), offset, size);
+        // a dropped segment is empty, and still bounds-checks both offsets
+        var data = (seg == null || seg == PassiveDataSegment.EMPTY) ? NO_DATA : seg.bytes();
+        write(dest, data, offset, size);
     }
 
-    private int sizeInBytes() {
-        return PAGE_SIZE * nPages;
+    private long sizeInBytes() {
+        return PAGE_SIZE * (long) nPages;
     }
 
     /**
@@ -490,4 +468,196 @@ public void drop(int segment) {
             dataSegments[segment] = PassiveDataSegment.EMPTY;
         }
     }
+
+    @Override
+    public int atomicReadInt(int addr) {
+        checkBounds(addr, 4);
+        return atomics.readInt(addr);
+    }
+
+    @Override
+    public long atomicReadLong(int addr) {
+        checkBounds(addr, 8);
+        return atomics.readLong(addr);
+    }
+
+    @Override
+    public short atomicReadShort(int addr) {
+        checkBounds(addr, 2);
+        return atomics.readShort(addr);
+    }
+
+    @Override
+    public byte atomicReadByte(int addr) {
+        checkBounds(addr, 1);
+        return atomics.readByte(addr);
+    }
+
+    @Override
+    public void atomicWriteInt(int addr, int value) {
+        checkBounds(addr, 4);
+        atomics.writeInt(addr, value);
+    }
+
+    @Override
+    public void atomicWriteLong(int addr, long value) {
+        checkBounds(addr, 8);
+        atomics.writeLong(addr, value);
+    }
+
+    @Override
+    public void atomicWriteShort(int addr, short value) {
+        checkBounds(addr, 2);
+        atomics.writeShort(addr, value);
+    }
+
+    @Override
+    public void atomicWriteByte(int addr, byte value) {
+        checkBounds(addr, 1);
+        atomics.writeByte(addr, value);
+    }
+
+    @Override
+    public int atomicAddInt(int addr, int delta) {
+        checkBounds(addr, 4);
+        return atomics.addInt(addr, delta);
+    }
+
+    @Override
+    public int atomicAndInt(int addr, int mask) {
+        checkBounds(addr, 4);
+        return atomics.andInt(addr, mask);
+    }
+
+    @Override
+    public int atomicOrInt(int addr, int mask) {
+        checkBounds(addr, 4);
+        return atomics.orInt(addr, mask);
+    }
+
+    @Override
+    public int atomicXorInt(int addr, int mask) {
+        checkBounds(addr, 4);
+        return atomics.xorInt(addr, mask);
+    }
+
+    @Override
+    public int atomicXchgInt(int addr, int value) {
+        checkBounds(addr, 4);
+        return atomics.xchgInt(addr, value);
+    }
+
+    @Override
+    public int atomicCmpxchgInt(int addr, int expected, int replacement) {
+        checkBounds(addr, 4);
+        return atomics.cmpxchgInt(addr, expected, replacement);
+    }
+
+    @Override
+    public long atomicAddLong(int addr, long delta) {
+        checkBounds(addr, 8);
+        return atomics.addLong(addr, delta);
+    }
+
+    @Override
+    public long atomicAndLong(int addr, long mask) {
+        checkBounds(addr, 8);
+        return atomics.andLong(addr, mask);
+    }
+
+    @Override
+    public long atomicOrLong(int addr, long mask) {
+        checkBounds(addr, 8);
+        return atomics.orLong(addr, mask);
+    }
+
+    @Override
+    public long atomicXorLong(int addr, long mask) {
+        checkBounds(addr, 8);
+        return atomics.xorLong(addr, mask);
+    }
+
+    @Override
+    public long atomicXchgLong(int addr, long value) {
+        checkBounds(addr, 8);
+        return atomics.xchgLong(addr, value);
+    }
+
+    @Override
+    public long atomicCmpxchgLong(int addr, long expected, long replacement) {
+        checkBounds(addr, 8);
+        return atomics.cmpxchgLong(addr, expected, replacement);
+    }
+
+    @Override
+    public short atomicAddShort(int addr, short delta) {
+        checkBounds(addr, 2);
+        return atomics.addShort(addr, delta);
+    }
+
+    @Override
+    public short atomicAndShort(int addr, short mask) {
+        checkBounds(addr, 2);
+        return atomics.andShort(addr, mask);
+    }
+
+    @Override
+    public short atomicOrShort(int addr, short mask) {
+        checkBounds(addr, 2);
+        return atomics.orShort(addr, mask);
+    }
+
+    @Override
+    public short atomicXorShort(int addr, short mask) {
+        checkBounds(addr, 2);
+        return atomics.xorShort(addr, mask);
+    }
+
+    @Override
+    public short atomicXchgShort(int addr, short value) {
+        checkBounds(addr, 2);
+        return atomics.xchgShort(addr, value);
+    }
+
+    @Override
+    public short atomicCmpxchgShort(int addr, short expected, short replacement) {
+        checkBounds(addr, 2);
+        return atomics.cmpxchgShort(addr, expected, replacement);
+    }
+
+    @Override
+    public byte atomicAddByte(int addr, byte delta) {
+        checkBounds(addr, 1);
+        return atomics.addByte(addr, delta);
+    }
+
+    @Override
+    public byte atomicAndByte(int addr, byte mask) {
+        checkBounds(addr, 1);
+        return atomics.andByte(addr, mask);
+    }
+
+    @Override
+    public byte atomicOrByte(int addr, byte mask) {
+        checkBounds(addr, 1);
+        return atomics.orByte(addr, mask);
+    }
+
+    @Override
+    public byte atomicXorByte(int addr, byte mask) {
+        checkBounds(addr, 1);
+        return atomics.xorByte(addr, mask);
+    }
+
+    @Override
+    public byte atomicXchgByte(int addr, byte value) {
+        checkBounds(addr, 1);
+        return atomics.xchgByte(addr, value);
+    }
+
+    @Override
+    public byte atomicCmpxchgByte(int addr, byte expected, byte replacement) {
+        checkBounds(addr, 1);
+        return atomics.cmpxchgByte(addr, expected, replacement);
+    }
 }
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/GuestSeesGrowthTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/GuestSeesGrowthTest.java
new file mode 100644
index 000000000..b601dc7dc
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/GuestSeesGrowthTest.java
@@ -0,0 +1,121 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.HostFunction;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Instance;
+import run.endive.runtime.Memory;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.FunctionType;
+import run.endive.wasm.types.MemoryLimits;
+
+/** Compiled code sees growth by another instance sharing the memory, or by a host import. */
+public class GuestSeesGrowthTest {
+
+    private static final int PAGE = 65536;
+
+    private static final WasmModule SHARED = parse("shared-memory-grow");
+    private static final NativeCode SHARED_CODE = compile(SHARED);
+    private static final WasmModule HOST_GROW = parse("host-grow");
+    private static final NativeCode HOST_GROW_CODE = compile(HOST_GROW);
+
+    private static final ExecutorService POOL = Executors.newCachedThreadPool();
+
+    @AfterAll
+    public static void shutdown() {
+        POOL.shutdownNow();
+    }
+
+    @Test
+    public void aRunningInstanceReadsThePageAnotherInstanceGrew() throws Exception {
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+        try (var waiter = shared(memory);
+                var grower = shared(memory)) {
+            var read = POOL.submit(() -> waiter.export("waitThenRead").apply(0, PAGE + 16)[0]);
+            Thread.sleep(50);
+            grower.export("growAndPublish").apply(0, PAGE + 16, 42);
+            assertEquals(42L, read.get(30, TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    public void aRunningInstanceSeesTheSizeAnotherInstanceGrew() throws Exception {
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+        try (var waiter = shared(memory);
+                var grower = shared(memory)) {
+            var size = POOL.submit(() -> waiter.export("waitThenSize").apply(0)[0]);
+            Thread.sleep(50);
+            grower.export("growAndPublish").apply(0, PAGE + 16, 42);
+            assertEquals(2L, size.get(30, TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    public void theGuestReadsThePageAHostImportGrew() {
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2));
+        try (var instance = hostGrow(memory, 77)) {
+            assertEquals(77L, instance.export("growThenRead").apply(PAGE + 8)[0]);
+        }
+    }
+
+    @Test
+    public void theGuestSeesTheSizeAHostImportGrew() {
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2));
+        try (var instance = hostGrow(memory, 77)) {
+            assertEquals(2L, instance.export("growThenSize").apply()[0]);
+        }
+    }
+
+    private static Instance shared(Memory memory) {
+        return NativeMachineFactory.builder(SHARED)
+                .withPrecompiledCode(SHARED_CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .build())
+                .build();
+    }
+
+    private static Instance hostGrow(Memory memory, int value) {
+        var grow =
+                new HostFunction(
+                        "env",
+                        "grow",
+                        FunctionType.of(List.of(), List.of()),
+                        (instance, args) -> {
+                            memory.grow(1);
+                            memory.writeI32(PAGE + 8, value);
+                            return null;
+                        });
+        return NativeMachineFactory.builder(HOST_GROW)
+                .withPrecompiledCode(HOST_GROW_CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .addFunction(grow)
+                                .build())
+                .build();
+    }
+
+    private static WasmModule parse(String name) {
+        return Parser.parse(CorpusResources.getResource("compiled/" + name + ".wat.wasm"));
+    }
+
+    private static NativeCode compile(WasmModule module) {
+        return NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), module);
+    }
+}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java
index 855e18d25..6febccfe9 100644
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java
@@ -1,10 +1,13 @@
 package run.endive.redline.experimental.runner.internal;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import org.junit.jupiter.api.Test;
 import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.Memory;
 import run.endive.runtime.WasmRuntimeException;
+import run.endive.wasm.UninstantiableException;
 import run.endive.wasm.types.MemoryLimits;
 
 /**
@@ -41,4 +44,31 @@ public void insideTheMemoryIsUntouched() {
         memory.writeI32(PAGE - 4, 0x11223344);
         org.junit.jupiter.api.Assertions.assertEquals(0x11223344, memory.readInt(PAGE - 4));
     }
+
+    @Test
+    public void wideAccessesAreLittleEndianAtOddOffsets() {
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 1));
+        for (int i = 0; i < 8; i++) {
+            memory.writeByte(101 + i, (byte) (i + 1));
+        }
+        assertEquals((short) 0x0201, memory.readShort(101));
+        assertEquals(0x0807060504030201L, memory.readLong(101));
+
+        memory.writeShort(201, (short) 0x0201);
+        memory.writeLong(301, 0x0807060504030201L);
+        assertEquals(1, memory.read(201));
+        assertEquals(2, memory.read(202));
+        for (int i = 0; i < 8; i++) {
+            assertEquals(i + 1, memory.read(301 + i));
+        }
+    }
+
+    @Test
+    public void anInitialSizePastTheRuntimeLimitIsRejected() {
+        assertThrows(
+                UninstantiableException.class,
+                () ->
+                        NativeMachineFactory.createMemory(
+                                new MemoryLimits(Memory.RUNTIME_MAX_PAGES + 1)));
+    }
 }
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryInitTrapTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryInitTrapTest.java
new file mode 100644
index 000000000..363ceb059
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryInitTrapTest.java
@@ -0,0 +1,54 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.Instance;
+import run.endive.runtime.WasmRuntimeException;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+
+/** A trap raised while the runner handles memory.init has to stop the guest there. */
+public class MemoryInitTrapTest {
+
+    private static final WasmModule MODULE =
+            Parser.parse(CorpusResources.getResource("compiled/memory-init-trap.wat.wasm"));
+    private static final NativeCode CODE =
+            NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), MODULE);
+
+    @Test
+    public void anOutOfBoundsInitStopsBeforeTheNextInstruction() {
+        try (var instance = instance()) {
+            assertThrows(
+                    WasmRuntimeException.class,
+                    () -> instance.export("initOutOfBoundsThenStore").apply());
+            assertEquals(0L, instance.export("read").apply(100)[0]);
+        }
+    }
+
+    @Test
+    public void anEmptyInitPastTheEndOfADroppedSegmentTraps() {
+        try (var instance = instance()) {
+            assertThrows(
+                    WasmRuntimeException.class,
+                    () -> instance.export("dropThenInitPastEnd").apply());
+        }
+    }
+
+    @Test
+    public void anEmptyInitAtTheStartOfADroppedSegmentIsAllowed() {
+        try (var instance = instance()) {
+            instance.export("dropThenInitNothing").apply();
+        }
+    }
+
+    private static Instance instance() {
+        return NativeMachineFactory.builder(MODULE).withPrecompiledCode(CODE).build();
+    }
+}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeAtomicsTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeAtomicsTest.java
new file mode 100644
index 000000000..0b8986d8a
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeAtomicsTest.java
@@ -0,0 +1,145 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.NativeCode;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Instance;
+import run.endive.runtime.Memory;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.MemoryLimits;
+
+/** Host atomics on a native memory have to be atomic against the guest's own. */
+public class NativeAtomicsTest {
+
+    private static final int N = 1_000_000;
+
+    private static final WasmModule MODULE =
+            Parser.parse(CorpusResources.getResource("compiled/atomic-counter.wat.wasm"));
+    private static final NativeCode CODE =
+            NativeCompiler.compile(RedlineTarget.detectHost().orElseThrow().triple(), MODULE);
+
+    private static final ExecutorService POOL = Executors.newSingleThreadExecutor();
+
+    @AfterAll
+    public static void shutdown() {
+        POOL.shutdownNow();
+    }
+
+    @Test
+    public void hostAndGuestIntAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add32", () -> memory.atomicAddInt(0, 1));
+        assertEquals(2 * N, memory.readInt(0));
+    }
+
+    @Test
+    public void hostAndGuestLongAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add64", () -> memory.atomicAddLong(16, 1L));
+        assertEquals(2L * N, memory.readLong(16));
+    }
+
+    @Test
+    public void hostAndGuestShortAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add16", () -> memory.atomicAddShort(10, (short) 1));
+        assertEquals((short) (2 * N), memory.readShort(10));
+        assertEquals(0, memory.readShort(8), "the other half of the int is untouched");
+    }
+
+    @Test
+    public void hostAndGuestByteAddsDoNotLoseIncrements() throws Exception {
+        var memory = sharedMemory();
+        race(memory, "add8", () -> memory.atomicAddByte(13, (byte) 1));
+        assertEquals((byte) (2 * N), memory.read(13));
+        assertEquals(0, memory.read(12), "neighbouring bytes are untouched");
+        assertEquals(0, memory.read(14));
+        assertEquals(0, memory.read(15));
+    }
+
+    @Test
+    public void narrowOperationsOnlyTouchTheirBytes() {
+        var memory = sharedMemory();
+        memory.writeI32(0, 0x44332211);
+
+        assertEquals((byte) 0x22, memory.atomicXchgByte(1, (byte) 0x99));
+        assertEquals((byte) 0x33, memory.atomicCmpxchgByte(2, (byte) 0x00, (byte) 0x77));
+        assertEquals((byte) 0x33, memory.atomicCmpxchgByte(2, (byte) 0x33, (byte) 0x77));
+        assertEquals((short) 0x9911, memory.atomicOrShort(0, (short) 0x0002));
+        assertEquals(0x44779913, memory.atomicReadInt(0));
+
+        assertEquals((short) 0x4477, memory.atomicAndShort(2, (short) 0x00FF));
+        assertEquals((short) 0x0077, memory.atomicXorShort(2, (short) 0x0101));
+        assertEquals((short) 0x0176, memory.atomicReadShort(2));
+        memory.atomicWriteByte(3, (byte) 0xAB);
+        assertEquals((byte) 0xAB, memory.atomicReadByte(3));
+        assertEquals(0xAB769913, memory.readInt(0));
+    }
+
+    @Test
+    public void wideOperationsBehaveLikeTheirScalarForms() {
+        var memory = sharedMemory();
+        memory.atomicWriteLong(8, 0x0F0F0F0F0F0F0F0FL);
+        assertEquals(0x0F0F0F0F0F0F0F0FL, memory.atomicAndLong(8, 0x00FF00FF00FF00FFL));
+        assertEquals(0x000F000F000F000FL, memory.atomicOrLong(8, 0x1000000000000000L));
+        assertEquals(0x100F000F000F000FL, memory.atomicXorLong(8, 0x100F000F000F000FL));
+        assertEquals(0L, memory.atomicCmpxchgLong(8, 0L, 5L));
+        assertEquals(5L, memory.atomicXchgLong(8, 6L));
+        assertEquals(6L, memory.atomicReadLong(8));
+
+        memory.atomicWriteInt(4, 10);
+        assertEquals(10, memory.atomicCmpxchgInt(4, 11, 12));
+        assertEquals(10, memory.atomicCmpxchgInt(4, 10, 12));
+        assertEquals(12, memory.atomicXchgInt(4, 1));
+        assertEquals(1, memory.atomicOrInt(4, 6));
+        assertEquals(7, memory.atomicAndInt(4, 5));
+        assertEquals(5, memory.atomicXorInt(4, 1));
+        assertEquals(4, memory.atomicReadInt(4));
+    }
+
+    private static Memory sharedMemory() {
+        return NativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+    }
+
+    private static void race(Memory memory, String export, Runnable hostIncrement)
+            throws Exception {
+        try (var instance = instance(memory)) {
+            var start = new CyclicBarrier(2);
+            var guest =
+                    POOL.submit(
+                            () -> {
+                                start.await();
+                                instance.export(export).apply(N);
+                                return null;
+                            });
+            start.await();
+            for (int i = 0; i < N; i++) {
+                hostIncrement.run();
+            }
+            guest.get(60, TimeUnit.SECONDS);
+        }
+    }
+
+    private static Instance instance(Memory memory) {
+        return NativeMachineFactory.builder(MODULE)
+                .withPrecompiledCode(CODE)
+                .withImportValues(
+                        ImportValues.builder()
+                                .addMemory(new ImportMemory("env", "memory", memory))
+                                .build())
+                .build();
+    }
+}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeMemoryConcurrencyTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeMemoryConcurrencyTest.java
new file mode 100644
index 000000000..b1c151b0b
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeMemoryConcurrencyTest.java
@@ -0,0 +1,83 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.wasm.types.MemoryLimits;
+
+/** A shared memory is grown and read by several host threads at once. */
+public class NativeMemoryConcurrencyTest {
+
+    private static final int PAGE = 65536;
+
+    @Test
+    public void concurrentGrowsHandOutEveryPageOnce() throws Exception {
+        int threads = 8;
+        int growsPerThread = 200;
+        var memory =
+                NativeMachineFactory.createMemory(
+                        new MemoryLimits(0, threads * growsPerThread, true));
+        Set handedOut = ConcurrentHashMap.newKeySet();
+        var start = new CountDownLatch(1);
+        ExecutorService pool = Executors.newFixedThreadPool(threads);
+        try {
+            var futures = new java.util.ArrayList>();
+            for (int t = 0; t < threads; t++) {
+                futures.add(
+                        pool.submit(
+                                () -> {
+                                    start.await();
+                                    for (int i = 0; i < growsPerThread; i++) {
+                                        assertTrue(handedOut.add(memory.grow(1)));
+                                    }
+                                    return null;
+                                }));
+            }
+            start.countDown();
+            for (var f : futures) {
+                f.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+        assertEquals(threads * growsPerThread, memory.pages());
+        assertEquals(threads * growsPerThread, handedOut.size());
+    }
+
+    // The reader polls with plain reads, the pattern that let the JIT keep a stale view
+    @Test
+    public void aHostReaderCanReachPagesAnotherThreadGrew() throws Exception {
+        ExecutorService pool = Executors.newSingleThreadExecutor();
+        try {
+            for (int round = 0; round < 20; round++) {
+                var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2, true));
+                Future reader =
+                        pool.submit(
+                                () -> {
+                                    long deadline = System.nanoTime() + 10_000_000_000L;
+                                    while (memory.readInt(0) != 1) {
+                                        if (System.nanoTime() > deadline) {
+                                            throw new AssertionError("flag never seen");
+                                        }
+                                    }
+                                    return memory.readInt(PAGE + 16);
+                                });
+                Thread.sleep(5);
+                memory.grow(1);
+                memory.atomicWriteInt(0, 1);
+                reader.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+    }
+}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeWaitNotifyTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeWaitNotifyTest.java
new file mode 100644
index 000000000..973d98554
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/NativeWaitNotifyTest.java
@@ -0,0 +1,93 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.WasmInterruptedException;
+import run.endive.wasm.Parser;
+import run.endive.wasm.types.MemoryLimits;
+
+public class NativeWaitNotifyTest {
+
+    // Ported from the runtime's MemoryTest (#107): a late wait can't take the parked one's wakeup
+    @Test
+    public void notifyWakesTheParkedThreadNotALateArrival() throws Exception {
+        final int addr = 0;
+        final int sentinel = 42;
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+
+        for (int round = 0; round < 200; round++) {
+            memory.atomicWriteInt(addr, sentinel);
+            var parkedReady = new CountDownLatch(1);
+            var parked =
+                    new Thread(
+                            () -> {
+                                parkedReady.countDown();
+                                memory.atomicWait(addr, sentinel, 100_000_000L);
+                            });
+            parked.setDaemon(true);
+            parked.start();
+            parkedReady.await();
+            Thread.sleep(1);
+
+            if (memory.atomicNotify(addr, 1) == 0) {
+                continue;
+            }
+            assertEquals(2, memory.atomicWait(addr, sentinel, 0L), "round " + round);
+        }
+    }
+
+    @Test
+    public void interruptingAGuestBlockedInAWaitStopsIt() throws Exception {
+        var module = Parser.parse(CorpusResources.getResource("compiled/threads-example.wat.wasm"));
+        var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 1, true));
+        try (var instance =
+                NativeMachineFactory.builder(module)
+                        .withPrecompiledCode(
+                                NativeCompiler.compile(
+                                        RedlineTarget.detectHost().orElseThrow().triple(), module))
+                        .withImportValues(
+                                ImportValues.builder()
+                                        .addMemory(new ImportMemory("env", "memory", memory))
+                                        .build())
+                        .build()) {
+            // locked, so lockMutex parks in memory.atomic.wait32
+            memory.atomicWriteInt(0, 1);
+            var failure = new AtomicReference();
+            var blocked =
+                    new Thread(
+                            () -> {
+                                try {
+                                    instance.export("lockMutex").apply(0);
+                                } catch (Throwable t) {
+                                    failure.set(t);
+                                }
+                            });
+            blocked.setDaemon(true);
+            blocked.start();
+
+            long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
+            while (blocked.getState() != Thread.State.WAITING
+                    && blocked.getState() != Thread.State.TIMED_WAITING) {
+                assertTrue(System.nanoTime() < deadline, "never parked");
+                Thread.sleep(1);
+            }
+            blocked.interrupt();
+            blocked.join(TimeUnit.SECONDS.toMillis(30));
+
+            assertTrue(!blocked.isAlive(), "the interrupted guest kept running");
+            assertInstanceOf(WasmInterruptedException.class, failure.get());
+        }
+    }
+}
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
index 0c396e4a9..25df60753 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
@@ -454,6 +454,14 @@ private MemorySegment createImportStub(int funcId, FunctionType funcType) {
      * Dispatches a function call from native code. Reads args from ctxBuffer.
      * Called by import upcall stubs and bridge stubs for non-native functions.
      */
+    // Host code may have grown the memory since compiled code last saw its size
+    private void refreshMemoryPages() {
+        var mem = instance.memory();
+        if (mem != null) {
+            ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.MEMORY_PAGES, mem.pages());
+        }
+    }
+
     @SuppressWarnings("unused")
     private long importDispatchDirect(int funcId) {
         try {
@@ -465,6 +473,7 @@ private long importDispatchDirect(int funcId) {
             if (funcId < numImports) {
                 var importFunc = instance.imports().function(funcId);
                 long[] result = importFunc.handle().apply(instance, args);
+                refreshMemoryPages();
                 if (result == null || result.length == 0) {
                     return 0L;
                 }
@@ -929,6 +938,10 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
                             ValueLayout.JAVA_LONG,
                             CtxBuffer.MEM_BASE_ADDR,
                             cachedMemBase.address());
+                    ctxBuffer.set(
+                            ValueLayout.JAVA_LONG,
+                            CtxBuffer.MEMORY_PAGES_PTR,
+                            nativeMemory.pagesAddress().address());
                 } else if (mem != null) {
                     throw new WasmEngineException(foreignImportMessage("memory", mem));
                 } else {
@@ -936,11 +949,7 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
                 }
                 memBaseInitialized = true;
             }
-            // MEMORY_PAGES can change via grow() — must refresh every call
-            var mem = instance.memory();
-            if (mem != null) {
-                ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.MEMORY_PAGES, mem.pages());
-            }
+            refreshMemoryPages();
 
             if (Thread.currentThread().isInterrupted()) {
                 throw new WasmInterruptedException("Thread interrupted");
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
index a76c0fb78..f8b67600b 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
@@ -4,9 +4,11 @@
 
 import java.lang.foreign.MemorySegment;
 import java.lang.foreign.ValueLayout;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import run.endive.redline.experimental.api.internal.NativeAtomics;
+import run.endive.redline.experimental.api.internal.NativeWaiters;
 import run.endive.runtime.Instance;
 import run.endive.runtime.Memory;
 import run.endive.wasm.UninstantiableException;
@@ -25,44 +27,69 @@
  */
 public final class NativeMemory implements Memory, AutoCloseable {
 
-    private static final class WaitState {
-        int waiterCount;
-        int pendingWakeups;
-    }
-
-    private final Map waitStates = new ConcurrentHashMap<>();
-    private final MemoryLimits limits;
-    private final MemorySegment reserved;
-    // Build the value layouts at compile time and use known JIT value for lookups instead of building object for each call.
-    private static final ValueLayout.OfInt INT_LE =
+    // Static final so the JIT folds each layout's VarHandle; withOrder() allocates per call.
+    private static final ValueLayout.OfInt INT_UNALIGNED_LE =
             ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
-    private static final ValueLayout.OfLong LONG_LE =
+    private static final ValueLayout.OfLong LONG_UNALIGNED_LE =
             ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
-    private static final ValueLayout.OfShort SHORT_LE =
+    private static final ValueLayout.OfShort SHORT_UNALIGNED_LE =
             ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
+    private static final VarHandle PAGES_CELL = ValueLayout.JAVA_INT.varHandle();
+    private static final byte[] NO_DATA = new byte[0];
 
-    private MemorySegment segment;
-    private int nPages;
+    private final MemoryLimits limits;
+    private final MemorySegment reserved;
     private final long reservedSize;
+    private final NativeWaiters waiters;
+    private final NativeAtomics atomics;
+    // Page count compiled code reads for a shared memory, which other instances may grow
+    private MemorySegment pagesCell;
+    private volatile MemorySegment segment;
+    private volatile int nPages;
     private DataSegment[] dataSegments;
 
     public NativeMemory(MemoryLimits limits) {
         this.limits = limits;
-        this.nPages = limits.initialPages();
         int maxPages = Math.min(limits.maximumPages(), RUNTIME_MAX_PAGES);
+        if (limits.initialPages() > maxPages) {
+            throw new UninstantiableException(
+                    "memory of "
+                            + limits.initialPages()
+                            + " pages exceeds the limit of "
+                            + maxPages
+                            + " pages");
+        }
+        this.nPages = limits.initialPages();
         this.reservedSize = PAGE_SIZE * (long) maxPages;
+        this.waiters = new NativeWaiters(limits.shared());
 
+        MemorySegment mapping = MemorySegment.NULL;
         try {
-            this.reserved = PanamaExecutor.mmapNoAccess(reservedSize);
-
+            mapping = PanamaExecutor.mmapNoAccess(reservedSize);
             if (nPages > 0) {
-                PanamaExecutor.mprotectReadWrite(reserved, PAGE_SIZE * (long) nPages);
+                PanamaExecutor.mprotectReadWrite(mapping, PAGE_SIZE * (long) nPages);
             }
+            this.pagesCell = PanamaExecutor.malloc(Integer.BYTES);
         } catch (Throwable t) {
-            throw new WasmEngineException("Failed to mmap native memory", t);
+            var failure = new WasmEngineException("Failed to mmap native memory", t);
+            if (reservedSize > 0 && mapping.address() != 0) {
+                try {
+                    PanamaExecutor.munmap(mapping, reservedSize);
+                } catch (Throwable e) {
+                    failure.addSuppressed(e);
+                }
+            }
+            throw failure;
         }
 
+        this.reserved = mapping;
+        PAGES_CELL.setVolatile(pagesCell, 0L, nPages);
         this.segment = reserved.reinterpret(PAGE_SIZE * (long) nPages);
+        this.atomics =
+                new NativeAtomics(
+                        reservedSize > 0
+                                ? reserved.reinterpret(reservedSize).asByteBuffer()
+                                : ByteBuffer.allocateDirect(0));
     }
 
     @Override
@@ -74,6 +101,14 @@ public void close() {
                 throw new WasmEngineException("Failed to unmap native memory", e);
             }
         }
+        if (pagesCell.address() != 0) {
+            try {
+                PanamaExecutor.free(pagesCell);
+            } catch (Throwable e) {
+                throw new WasmEngineException("Failed to free native memory page count", e);
+            }
+            pagesCell = MemorySegment.NULL;
+        }
     }
 
     /** Get the native address of the memory buffer, for passing to native code. */
@@ -81,13 +116,18 @@ public MemorySegment nativeAddress() {
         return segment;
     }
 
+    /** Where compiled code reads the current page count of a shared memory. */
+    public MemorySegment pagesAddress() {
+        return pagesCell;
+    }
+
     @Override
     public int pages() {
         return nPages;
     }
 
     @Override
-    public int grow(int size) {
+    public synchronized int grow(int size) {
         var prevPages = nPages;
         var numPages = prevPages + size;
         if (numPages > maximumPages() || numPages < prevPages) {
@@ -97,6 +137,8 @@ public int grow(int size) {
         try {
             long newSize = PAGE_SIZE * (long) numPages;
             PanamaExecutor.mprotectReadWrite(reserved, newSize);
+            PAGES_CELL.setVolatile(pagesCell, 0L, numPages);
+            // the segment first, so a host that sees the new page count can access it
             this.segment = reserved.reinterpret(newSize);
             this.nPages = numPages;
         } catch (Throwable t) {
@@ -127,87 +169,25 @@ public Object lock(int address) {
         if (!shared()) {
             return new Object();
         }
-        return waitStates.computeIfAbsent(address, k -> new WaitState());
-    }
-
-    private int waitOn(int address, java.util.function.BooleanSupplier condition, long timeout) {
-        if (!shared()) {
-            throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported.");
-        }
-
-        long deadline = (timeout < 0) ? Long.MAX_VALUE : System.nanoTime() + timeout;
-        WaitState state = waitStates.computeIfAbsent(address, k -> new WaitState());
-
-        synchronized (state) {
-            if (!condition.getAsBoolean()) {
-                return 1; // not-equal
-            }
-
-            state.waiterCount++;
-            try {
-                while (state.pendingWakeups == 0) {
-                    long remaining = deadline - System.nanoTime();
-                    if (remaining <= 0) {
-                        return 2; // timeout
-                    }
-                    long millis = Math.max(remaining / 1_000_000L, 0);
-                    int nanos = Math.max((int) (remaining % 1_000_000L), 0);
-                    try {
-                        state.wait(millis, nanos);
-                    } catch (InterruptedException ie) {
-                        Thread.currentThread().interrupt();
-                        throw new WasmEngineException("Thread interrupted");
-                    }
-                }
-                return 0; // woken
-            } finally {
-                if (state.pendingWakeups > 0) {
-                    state.pendingWakeups--;
-                }
-                state.waiterCount--;
-            }
-        }
+        return waiters.monitor(address);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int waitOn(int address, int expected, long timeout) {
-        return waitOn(address, () -> readInt(address) == expected, timeout);
+        return waiters.waitOn(address, () -> atomicReadInt(address) == expected, timeout);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int waitOn(int address, long expected, long timeout) {
-        return waitOn(address, () -> readLong(address) == expected, timeout);
+        return waiters.waitOn(address, () -> atomicReadLong(address) == expected, timeout);
     }
 
     @SuppressWarnings("removal")
     @Override
     public int notify(int address, int maxThreads) {
-        if (!shared()) {
-            return 0;
-        }
-
-        WaitState state = waitStates.get(address);
-        if (state == null) {
-            return 0;
-        }
-
-        synchronized (state) {
-            int actualWaiters = state.waiterCount - state.pendingWakeups;
-            if (actualWaiters == 0) {
-                return 0;
-            }
-            int toWake;
-            if (maxThreads < 0) {
-                toWake = actualWaiters;
-            } else {
-                toWake = Math.min(actualWaiters, maxThreads);
-            }
-            state.pendingWakeups += toWake;
-            state.notifyAll();
-            return toWake;
-        }
+        return waiters.notify(address, maxThreads);
     }
 
     @Override
@@ -218,9 +198,9 @@ public void initialize(Instance instance, DataSegment[] dataSegments) {
         }
         for (var s : dataSegments) {
             if (s instanceof ActiveDataSegment seg) {
-                var data = seg.data();
+                var data = seg.bytes();
                 var offset = (int) computeConstantValue(instance, seg.offsetInstructions())[0];
-                if (offset < 0 || offset + data.length > sizeInBytes()) {
+                if (offset < 0 || (long) offset + data.length > sizeInBytes()) {
                     throw new UninstantiableException(
                             "out of bounds memory access: offset="
                                     + offset
@@ -235,27 +215,23 @@ public void initialize(Instance instance, DataSegment[] dataSegments) {
     @Override
     public void initPassiveSegment(int segmentId, int dest, int offset, int size) {
         var seg = dataSegments[segmentId];
-        if (seg == null || seg == run.endive.wasm.types.PassiveDataSegment.EMPTY) {
-            if (size > 0) {
-                throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
-            }
-            return;
-        }
-        write(dest, seg.data(), offset, size);
+        // a dropped segment is empty, and still bounds-checks both offsets
+        var data = (seg == null || seg == PassiveDataSegment.EMPTY) ? NO_DATA : seg.bytes();
+        write(dest, data, offset, size);
     }
 
-    private int sizeInBytes() {
-        return PAGE_SIZE * nPages;
+    private long sizeInBytes() {
+        return PAGE_SIZE * (long) nPages;
     }
 
     @Override
     public void write(int addr, byte[] data, int offset, int size) {
-        long limit = segment.byteSize();
+        var view = segment;
         if (Integer.toUnsignedLong(offset) + Integer.toUnsignedLong(size) > data.length
-                || Integer.toUnsignedLong(addr) + Integer.toUnsignedLong(size) > limit) {
+                || Integer.toUnsignedLong(addr) + Integer.toUnsignedLong(size) > view.byteSize()) {
             throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
         }
-        MemorySegment.copy(MemorySegment.ofArray(data), offset, segment, addr, size);
+        MemorySegment.copy(MemorySegment.ofArray(data), offset, view, addr, size);
     }
 
     /**
@@ -268,6 +244,13 @@ private static run.endive.runtime.WasmRuntimeException outOfBounds(int addr) {
                 "out of bounds memory access: attempted to access address: " + addr);
     }
 
+    private int checkAtomic(int addr, int size) {
+        if (Integer.toUnsignedLong(addr) + size > segment.byteSize()) {
+            throw outOfBounds(addr);
+        }
+        return addr;
+    }
+
     @Override
     public byte read(int addr) {
         try {
@@ -279,17 +262,20 @@ public byte read(int addr) {
 
     @Override
     public byte[] readBytes(int addr, int len) {
-        try {
-            return segment.asSlice(addr, len).toArray(ValueLayout.JAVA_BYTE);
-        } catch (IndexOutOfBoundsException e) {
+        var view = segment;
+        if (len < 0
+                || Integer.toUnsignedLong(addr) + Integer.toUnsignedLong(len) > view.byteSize()) {
             throw outOfBounds(addr);
         }
+        byte[] result = new byte[len];
+        MemorySegment.copy(view, ValueLayout.JAVA_BYTE, addr, result, 0, len);
+        return result;
     }
 
     @Override
     public void writeI32(int addr, int data) {
         try {
-            segment.set(INT_LE, addr, data);
+            segment.set(INT_UNALIGNED_LE, addr, data);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -298,7 +284,7 @@ public void writeI32(int addr, int data) {
     @Override
     public int readInt(int addr) {
         try {
-            return segment.get(INT_LE, addr);
+            return segment.get(INT_UNALIGNED_LE, addr);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -307,7 +293,7 @@ public int readInt(int addr) {
     @Override
     public void writeLong(int addr, long data) {
         try {
-            segment.set(LONG_LE, addr, data);
+            segment.set(LONG_UNALIGNED_LE, addr, data);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -316,7 +302,7 @@ public void writeLong(int addr, long data) {
     @Override
     public long readLong(int addr) {
         try {
-            return segment.get(LONG_LE, addr);
+            return segment.get(LONG_UNALIGNED_LE, addr);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -325,7 +311,7 @@ public long readLong(int addr) {
     @Override
     public void writeShort(int addr, short data) {
         try {
-            segment.set(SHORT_LE, addr, data);
+            segment.set(SHORT_UNALIGNED_LE, addr, data);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -334,7 +320,7 @@ public void writeShort(int addr, short data) {
     @Override
     public short readShort(int addr) {
         try {
-            return segment.get(SHORT_LE, addr);
+            return segment.get(SHORT_UNALIGNED_LE, addr);
         } catch (IndexOutOfBoundsException e) {
             throw outOfBounds(addr);
         }
@@ -391,23 +377,25 @@ public void zero() {
 
     @Override
     public void copy(int dest, int src, int size) {
-        long limit = segment.byteSize();
+        var view = segment;
+        long limit = view.byteSize();
         if (Integer.toUnsignedLong(src) + Integer.toUnsignedLong(size) > limit
                 || Integer.toUnsignedLong(dest) + Integer.toUnsignedLong(size) > limit) {
             throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
         }
-        MemorySegment.copy(segment, src, segment, dest, size);
+        MemorySegment.copy(view, src, view, dest, size);
     }
 
     @Override
     public void fill(byte value, int fromIndex, int toIndex) {
-        long limit = segment.byteSize();
+        var view = segment;
+        long limit = view.byteSize();
         if (Integer.toUnsignedLong(fromIndex) > limit
                 || Integer.toUnsignedLong(toIndex) > limit
                 || fromIndex > toIndex) {
             throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
         }
-        segment.asSlice(fromIndex, toIndex - fromIndex).fill(value);
+        view.asSlice(fromIndex, toIndex - fromIndex).fill(value);
     }
 
     @Override
@@ -416,4 +404,164 @@ public void drop(int segment) {
             dataSegments[segment] = PassiveDataSegment.EMPTY;
         }
     }
+
+    @Override
+    public int atomicReadInt(int addr) {
+        return atomics.readInt(checkAtomic(addr, 4));
+    }
+
+    @Override
+    public long atomicReadLong(int addr) {
+        return atomics.readLong(checkAtomic(addr, 8));
+    }
+
+    @Override
+    public short atomicReadShort(int addr) {
+        return atomics.readShort(checkAtomic(addr, 2));
+    }
+
+    @Override
+    public byte atomicReadByte(int addr) {
+        return atomics.readByte(checkAtomic(addr, 1));
+    }
+
+    @Override
+    public void atomicWriteInt(int addr, int value) {
+        atomics.writeInt(checkAtomic(addr, 4), value);
+    }
+
+    @Override
+    public void atomicWriteLong(int addr, long value) {
+        atomics.writeLong(checkAtomic(addr, 8), value);
+    }
+
+    @Override
+    public void atomicWriteShort(int addr, short value) {
+        atomics.writeShort(checkAtomic(addr, 2), value);
+    }
+
+    @Override
+    public void atomicWriteByte(int addr, byte value) {
+        atomics.writeByte(checkAtomic(addr, 1), value);
+    }
+
+    @Override
+    public int atomicAddInt(int addr, int delta) {
+        return atomics.addInt(checkAtomic(addr, 4), delta);
+    }
+
+    @Override
+    public int atomicAndInt(int addr, int mask) {
+        return atomics.andInt(checkAtomic(addr, 4), mask);
+    }
+
+    @Override
+    public int atomicOrInt(int addr, int mask) {
+        return atomics.orInt(checkAtomic(addr, 4), mask);
+    }
+
+    @Override
+    public int atomicXorInt(int addr, int mask) {
+        return atomics.xorInt(checkAtomic(addr, 4), mask);
+    }
+
+    @Override
+    public int atomicXchgInt(int addr, int value) {
+        return atomics.xchgInt(checkAtomic(addr, 4), value);
+    }
+
+    @Override
+    public int atomicCmpxchgInt(int addr, int expected, int replacement) {
+        return atomics.cmpxchgInt(checkAtomic(addr, 4), expected, replacement);
+    }
+
+    @Override
+    public long atomicAddLong(int addr, long delta) {
+        return atomics.addLong(checkAtomic(addr, 8), delta);
+    }
+
+    @Override
+    public long atomicAndLong(int addr, long mask) {
+        return atomics.andLong(checkAtomic(addr, 8), mask);
+    }
+
+    @Override
+    public long atomicOrLong(int addr, long mask) {
+        return atomics.orLong(checkAtomic(addr, 8), mask);
+    }
+
+    @Override
+    public long atomicXorLong(int addr, long mask) {
+        return atomics.xorLong(checkAtomic(addr, 8), mask);
+    }
+
+    @Override
+    public long atomicXchgLong(int addr, long value) {
+        return atomics.xchgLong(checkAtomic(addr, 8), value);
+    }
+
+    @Override
+    public long atomicCmpxchgLong(int addr, long expected, long replacement) {
+        return atomics.cmpxchgLong(checkAtomic(addr, 8), expected, replacement);
+    }
+
+    @Override
+    public short atomicAddShort(int addr, short delta) {
+        return atomics.addShort(checkAtomic(addr, 2), delta);
+    }
+
+    @Override
+    public short atomicAndShort(int addr, short mask) {
+        return atomics.andShort(checkAtomic(addr, 2), mask);
+    }
+
+    @Override
+    public short atomicOrShort(int addr, short mask) {
+        return atomics.orShort(checkAtomic(addr, 2), mask);
+    }
+
+    @Override
+    public short atomicXorShort(int addr, short mask) {
+        return atomics.xorShort(checkAtomic(addr, 2), mask);
+    }
+
+    @Override
+    public short atomicXchgShort(int addr, short value) {
+        return atomics.xchgShort(checkAtomic(addr, 2), value);
+    }
+
+    @Override
+    public short atomicCmpxchgShort(int addr, short expected, short replacement) {
+        return atomics.cmpxchgShort(checkAtomic(addr, 2), expected, replacement);
+    }
+
+    @Override
+    public byte atomicAddByte(int addr, byte delta) {
+        return atomics.addByte(checkAtomic(addr, 1), delta);
+    }
+
+    @Override
+    public byte atomicAndByte(int addr, byte mask) {
+        return atomics.andByte(checkAtomic(addr, 1), mask);
+    }
+
+    @Override
+    public byte atomicOrByte(int addr, byte mask) {
+        return atomics.orByte(checkAtomic(addr, 1), mask);
+    }
+
+    @Override
+    public byte atomicXorByte(int addr, byte mask) {
+        return atomics.xorByte(checkAtomic(addr, 1), mask);
+    }
+
+    @Override
+    public byte atomicXchgByte(int addr, byte value) {
+        return atomics.xchgByte(checkAtomic(addr, 1), value);
+    }
+
+    @Override
+    public byte atomicCmpxchgByte(int addr, byte expected, byte replacement) {
+        return atomics.cmpxchgByte(checkAtomic(addr, 1), expected, replacement);
+    }
 }
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
index fca696c58..451aae742 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
@@ -29,6 +29,8 @@ private PanamaExecutor() {}
     private static final java.lang.invoke.MethodHandle MMAP;
     private static final java.lang.invoke.MethodHandle MPROTECT;
     private static final java.lang.invoke.MethodHandle MUNMAP;
+    private static final java.lang.invoke.MethodHandle MALLOC;
+    private static final java.lang.invoke.MethodHandle FREE;
 
     // POSIX constants
     private static final int PROT_READ = 0x1;
@@ -60,6 +62,14 @@ private PanamaExecutor() {}
         var lookup = LINKER.defaultLookup();
         MEMMOVE_ADDR = lookup.find("memmove").orElseThrow().address();
         MEMSET_ADDR = lookup.find("memset").orElseThrow().address();
+        MALLOC =
+                LINKER.downcallHandle(
+                        lookup.find("malloc").orElseThrow(),
+                        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG));
+        FREE =
+                LINKER.downcallHandle(
+                        lookup.find("free").orElseThrow(),
+                        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
 
         try {
             if (IS_WINDOWS) {
@@ -129,6 +139,18 @@ private PanamaExecutor() {}
         }
     }
 
+    static MemorySegment malloc(long size) throws Throwable {
+        MemorySegment addr = (MemorySegment) MALLOC.invokeExact(size);
+        if (addr.address() == 0) {
+            throw new OutOfMemoryError("malloc failed for " + size + " bytes");
+        }
+        return addr.reinterpret(size);
+    }
+
+    static void free(MemorySegment addr) throws Throwable {
+        FREE.invokeExact(addr);
+    }
+
     /** Allocate a writable memory region. Must call mprotectExec before executing. */
     static MemorySegment mmapCode(long size) throws Throwable {
         if (IS_WINDOWS) {
diff --git a/wasm-corpus/src/main/resources/compiled/atomic-counter.wat.wasm b/wasm-corpus/src/main/resources/compiled/atomic-counter.wat.wasm
new file mode 100644
index 000000000..7e83ee415
Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/atomic-counter.wat.wasm differ
diff --git a/wasm-corpus/src/main/resources/compiled/host-grow.wat.wasm b/wasm-corpus/src/main/resources/compiled/host-grow.wat.wasm
new file mode 100644
index 000000000..cafeb7df2
Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/host-grow.wat.wasm differ
diff --git a/wasm-corpus/src/main/resources/compiled/memory-init-trap.wat.wasm b/wasm-corpus/src/main/resources/compiled/memory-init-trap.wat.wasm
new file mode 100644
index 000000000..c7dad3abc
Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/memory-init-trap.wat.wasm differ
diff --git a/wasm-corpus/src/main/resources/compiled/shared-memory-grow.wat.wasm b/wasm-corpus/src/main/resources/compiled/shared-memory-grow.wat.wasm
new file mode 100644
index 000000000..f431825fc
Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/shared-memory-grow.wat.wasm differ
diff --git a/wasm-corpus/src/main/resources/wat/atomic-counter.wat b/wasm-corpus/src/main/resources/wat/atomic-counter.wat
new file mode 100644
index 000000000..2cf74b478
--- /dev/null
+++ b/wasm-corpus/src/main/resources/wat/atomic-counter.wat
@@ -0,0 +1,25 @@
+;; Counters bumped with atomics, to race against host atomics on the same addresses.
+(module
+  (import "env" "memory" (memory 1 1 shared))
+
+  (func (export "add32") (param $n i32)
+    (loop $l
+      (drop (i32.atomic.rmw.add (i32.const 0) (i32.const 1)))
+      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1))))))
+
+  ;; upper half of the int at 8
+  (func (export "add16") (param $n i32)
+    (loop $l
+      (drop (i32.atomic.rmw16.add_u (i32.const 10) (i32.const 1)))
+      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1))))))
+
+  ;; second byte of the int at 12
+  (func (export "add8") (param $n i32)
+    (loop $l
+      (drop (i32.atomic.rmw8.add_u (i32.const 13) (i32.const 1)))
+      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1))))))
+
+  (func (export "add64") (param $n i32)
+    (loop $l
+      (drop (i64.atomic.rmw.add (i32.const 16) (i64.const 1)))
+      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1)))))))
diff --git a/wasm-corpus/src/main/resources/wat/host-grow.wat b/wasm-corpus/src/main/resources/wat/host-grow.wat
new file mode 100644
index 000000000..4c8c42c3d
--- /dev/null
+++ b/wasm-corpus/src/main/resources/wat/host-grow.wat
@@ -0,0 +1,12 @@
+;; A host import grows the memory, and the guest then uses the new page.
+(module
+  (import "env" "memory" (memory 1 2))
+  (import "env" "grow" (func $grow))
+
+  (func (export "growThenRead") (param $addr i32) (result i32)
+    (call $grow)
+    (i32.load (local.get $addr)))
+
+  (func (export "growThenSize") (result i32)
+    (call $grow)
+    (memory.size)))
diff --git a/wasm-corpus/src/main/resources/wat/memory-init-trap.wat b/wasm-corpus/src/main/resources/wat/memory-init-trap.wat
new file mode 100644
index 000000000..416ad60b9
--- /dev/null
+++ b/wasm-corpus/src/main/resources/wat/memory-init-trap.wat
@@ -0,0 +1,22 @@
+;; A memory.init that traps must stop execution.
+(module
+  (memory 1)
+  (data $d "hello")
+
+  ;; 6 bytes from a 5 byte segment: traps before the store
+  (func (export "initOutOfBoundsThenStore")
+    (memory.init $d (i32.const 0) (i32.const 0) (i32.const 6))
+    (i32.store (i32.const 100) (i32.const 1)))
+
+  ;; source offset 1 of a dropped (empty) segment: traps even with length 0
+  (func (export "dropThenInitPastEnd")
+    (data.drop $d)
+    (memory.init $d (i32.const 0) (i32.const 1) (i32.const 0)))
+
+  ;; offset 0 and length 0 of a dropped segment: allowed
+  (func (export "dropThenInitNothing")
+    (data.drop $d)
+    (memory.init $d (i32.const 0) (i32.const 0) (i32.const 0)))
+
+  (func (export "read") (param i32) (result i32)
+    (i32.load (local.get 0))))
diff --git a/wasm-corpus/src/main/resources/wat/shared-memory-grow.wat b/wasm-corpus/src/main/resources/wat/shared-memory-grow.wat
new file mode 100644
index 000000000..4f3be87dd
--- /dev/null
+++ b/wasm-corpus/src/main/resources/wat/shared-memory-grow.wat
@@ -0,0 +1,25 @@
+;; Two instances share this memory: one grows it while the other is already running.
+(module
+  (import "env" "memory" (memory 1 2 shared))
+
+  ;; Spins until the flag is set, then reads an address in the page grown meanwhile.
+  (func (export "waitThenRead") (param $flag i32) (param $addr i32) (result i32)
+    (block $done
+      (loop $spin
+        (br_if $done (i32.atomic.load (local.get $flag)))
+        (br $spin)))
+    (i32.load (local.get $addr)))
+
+  ;; Spins until the flag is set, then returns the memory size.
+  (func (export "waitThenSize") (param $flag i32) (result i32)
+    (block $done
+      (loop $spin
+        (br_if $done (i32.atomic.load (local.get $flag)))
+        (br $spin)))
+    (memory.size))
+
+  ;; Grows by one page, writes the value in it, then sets the flag.
+  (func (export "growAndPublish") (param $flag i32) (param $addr i32) (param $value i32)
+    (drop (memory.grow (i32.const 1)))
+    (i32.store (local.get $addr) (local.get $value))
+    (i32.atomic.store (local.get $flag) (i32.const 1))))