Skip to content

Multi-memory phase 2: per-memory MPU isolation is unreachable — --safety-bounds mpu and multi-memory have disjoint paths #1145

Description

@avrabe

[fathom (gale) — synth's own refusal names a blocker that has no tracker]

--safety-bounds mpu refuses on any multi-memory module, and the refusal names
"#406 phase 2" as the blocker. #406 is closed and I can find no open issue tracking
that phase. Filing so it exists — the refusal text is doing tracker duty right now.

Toolchain identity: varve layer 2026.09.0 (sha256:cf4dbdc2…4bbc), synth 0.60.0.

Reproduction

(module
  (memory $a 1)
  (memory $b 1)
  (data $da (memory $a) (i32.const 0) "tenantA")
  (data $db (memory $b) (i32.const 0) "tenantB")
  (func (export "read_a") (param i32) (result i32) (i32.load8_u $a (local.get 0)))
  (func (export "read_b") (param i32) (result i32) (i32.load8_u $b (local.get 0)))
  (func (export "write_a") (param i32 i32) (i32.store8 $a (local.get 0) (local.get 1)))
)
synth compile two.wasm --target cortex-m3 --all-exports --relocatable \
  --embedder-data-init --bounds-check --safety-bounds mpu
Error: multi-memory (#406): per-memory MPU isolation (--safety-bounds mpu) is not yet
realizable for a module with 2 linear memories. Programming one MPU region per memory
requires synth to emit the startup that writes MPU_RBAR/RASR — the self-contained reset
handler — but multi-memory compiles ONLY on --relocatable, where the host owns startup
and synth emits no MPU programming; the self-contained path in turn declines
multi-memory (one R11 base). Refusing rather than accepting a silent MPU no-op.
Blocked on self-contained multi-memory (#406 phase 2)

To be clear up front: the refusal is the right behaviour. Accepting the flag and
emitting no MPU programming would be a silent no-op on a memory-safety control, which
is the worst possible outcome. Nothing here is asking for that to be relaxed.

What phase 1 did ship, verified

Phase 1 works. Same module, without --safety-bounds:

INFO Memories (2 total):
INFO   memory[0]: 1 initial pages …
INFO   memory[1]: 1 initial pages …

exit 0, and the object carries a per-memory region symbol:

00000000 D __synth_wasm_data_1
00000000 T read_a      (8 bytes, inline)
00000008 T read_b      (20 bytes, 1 external relocation)

So N memories → N distinct bases, memory 1 addressed through a linker-placed symbol.
That is exactly what #406 promised and it is in the shipped toolchain.

The gap

The two halves needed for hardware isolation are on mutually exclusive paths:

multi-memory MPU programming (MPU_RBAR/RASR)
--relocatable ✅ supported ❌ host owns startup
self-contained ❌ declined (one R11 base) ✅ synth emits reset handler

There is no invocation that produces both, so per-memory MPU isolation is currently
unreachable by construction rather than merely unimplemented.

Also observed, same module: --safety-bounds software and --safety-bounds mask both
skip read_b — the memory-1 access — and then correctly fail via #952 rather than
shipping a partial object. So the second memory has no bounds-check story on any
profile today, not only the MPU one.

Why gale cares

This is the sole blocker for gale's v0.5.0 milestone (gale#138): isolation /
multi-tenancy. REQ-OS-MPU-001 requires each tenant's linear memory in a distinct
MPU-protected region, with the kill-criterion "a crafted tenant writes outside its
region without a fault" — which needs real MPU programming, not just distinct bases.

gale's artifact has been carrying blocked-synth-404 this whole time. #404 was the
design question and closed as answered on 2026-07-15 (Model A). I'm correcting gale's
side to point here instead.

What would unblock it

Any one of these; I have no stake in which:

  1. Self-contained multi-memory (the phase-2 framing in the refusal) — synth emits the
    reset handler and programs one region per memory.
  2. A relocatable path that emits the MPU programming as a callable init routine plus
    the region table (base/size per memory), leaving the host to call it from its own
    startup. This suits gale better, since gale's images are host-linked and kiln owns
    reset.
  3. Emit the region table as data only — __synth_mem_base_N / __synth_mem_size_N
    symbols — and let the embedder program the MPU itself. Weakest guarantee (the
    embedder becomes trusted for it), but it would let gale build the v0.5.0
    demonstrator and close the kill-criterion on real silicon.

Option 3 is a small surface and would unblock a downstream milestone without committing
synth to owning startup on the relocatable path. Happy to prototype against a branch.

Kill-criterion for whatever lands: a two-tenant image where tenant A writes outside its
region and the write lands in tenant B's memory instead of faulting.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions