# CONTRACT-P6 — the backend runs in a second process Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17), `CONTRACT-P5B.md` (the class-C slots), `CONTRACT-P5C.md` (rule E, the two named exemptions, SEG_EVENT, the guards), `CONTRACT-P5E.md` (rule F, the barriered predicate, the wait rule) and `CONTRACT-MAGMA-RUNAHEAD.md`. Where it disagrees with any of them this file is newer and wins; §10 lists every such place. Base: `feat/disaggregated @ c25a7760`. Every `file:line` was read at that commit; paths are under `MobileGL/` unless they start with `docs/`. **How to change it.** Package `c6`'s file, edited by the integrator first. A P6 package (`lk` / `hs` / `so` / `sm` / `cp` / `dl` / `st` / `t6`) that needs a row changed goes through the integrator; the packages compile against the rows below from day one. **What this file is written from.** [`a6-audit-v1.md`](../notes/p6/a6-audit-v1.md) (198 rows, seven items, each adversarially re-resolved) and [`a6-link-experiment.md`](../notes/p6/a6-link-experiment.md) (the 184 symbols). It supersedes [`P6-CONTRACT-DRAFT.md`](../notes/p6/P6-CONTRACT-DRAFT.md), which was written before its own audit and is **wrong in four places** (§10.1). The re-scoping against the eventual client-in-a-VM end state is [`P6-ENDSTATE-REVIEW.md`](../notes/p6/P6-ENDSTATE-REVIEW.md); this file takes from it only what P6 must not preclude, and §11 says what P6 explicitly does not take on. --- ## §0 What c6 is, and the rule above every row `c6` lands **this file and two headers**. It changes no behaviour, adds no caller, and moves no byte on any wire. Every rule below that describes an action names the package that implements it. A rule with no package named is a statement about the tree as it already is. **Rule G — a role may not name a process-local handle.** Rule E forbade naming the other role's *memory*; rule G extends it to *identity*. A value whose meaning comes from the owning process's kernel or loader state — `EGLDisplay`, `EGLSurface`, `EGLContext`, `ANativeWindow*`, any `void*` window handle, an fd number, a pid, a mapped address — either crosses as a **token** defined in §2, or does not cross. Descriptors cross only by a mechanism the transport **declares it supports**, and where the transport declares none they do not cross at all and the bytes are transferred instead; a transport may not invent a third answer. > The draft hard-coded `SCM_RIGHTS` inside rule G. That is one transport's implementation of one > clause, not the rule, and writing it into the rule is what would make the rule wrong the first > time a transport has no descriptor passing at all. **Rule H — a value whose meaning comes from the peer's window system never crosses. Permanently.** Not "until P12". P12 gives the *server* its own display; it does not give the client's window a way across. §6 is rule H's whole enforcement. **Rule G's teeth** are §9's arm-proof gate: a spawn lane that silently ran monolith satisfies every other gate in this file. --- ## §1 The one fact that makes c6 possible: `Spawn` is unreachable today `ConfigLoader.cpp:339-346` is the only writer of `MG_Config::Transport` for the string `spawn`, and it writes **`TransportMode::Monolith`** at `:344`, with a log line saying P5 does not implement it. Two consequences, and the second is why this file can exist without changing behaviour: 1. **`ConfigLoader`'s named refusal cannot falsify itself.** It lands on the same value a server's resting state would have, so "the child was forced to monolith" and "the parent fell back" are one state. `sm` fixes this; until then, no lane can prove it ran spawn (§9.5). 2. **Every rule in this file conditioned on `Transport == Spawn` is vacuous in c6's own tree.** It constrains a state no process can enter. That is how D1 (§3.1) is stated normatively while changing nothing, and `c6`'s gate pins the vacuity so the `ConfigLoader` change cannot land early. --- ## §2 Table 0 additions | row | encoding | invalid | consumer | package | |---|---|---|---|---| | `DisplayToken` / `SurfaceToken` / `ContextToken` | `Uint64`, minted by the client, dense from 1, never reused in a session. Server keeps token → native handle; it never sees the client's EGL values. `protocol.fbs`'s `display` / `surface` / `context` are already `ulong` and are these. **The client already mints them** (`MG_State/EGLState/Core.h`'s `EncodeHandle`), contrary to `Server/SurfaceControlFrame.h:26-29`, which says inproc bit-casts the handle — that comment is corrected by `cp`. | `0` on any op but `ReleaseCurrent` → `Fatal{ProtocolCorruption}` | the server's surface map | `cp` | | `SurfaceOpKind::InitCapabilities = 11` | **Appended.** The draft claimed `ServerInitCapabilities` needs no wire form because "`CapsSnapshot` answers it". The *answer* is a `CapsSnapshot`; the **request** has a live client caller (`Client/BackendObject_Remote.cpp:126`) and the codec **refuses to encode it** (`Protocol/SurfaceOpCodec.cpp:119-122`, `InprocOnlyOpOnTheWire`). A spawn client cannot ask. | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | the server's control pump | `cp` | | `SurfaceReply.result: int` | **Appended, id 5.** Carries the `MobileGLResult` that `ServerApplyWireSurfaceOp` already computes and drops. Without it a client cannot tell a server that died mid-op from a legitimate refusal, and therefore cannot choose between arming the device-lost latch and returning `EGL_FALSE`. | — | the client's control demultiplexer | `hs` | | `SurfaceReply.defaultFb` | **`(deprecated)`.** The encoder passes a literal `0` offset (`SurfaceOpCodec.cpp:184`) and the decoder never reads it; `kEventSurfaceChanged` is the landed carrier (P5f `fc`). The slot is **burned, never deleted** — a deleted field frees its vtable slot for the next append. | — | nobody, by construction | `hs` | > **asio was considered for the byte stream and declined (2026-09-21).** It is already vendored > and on the include path, so the question is a fair one. But spawn needs five platform > mechanisms and asio covers one: its own `socketpair` returns `operation_not_supported` on > Windows (`3rdparty/asio/include/asio/detail/impl/socket_ops.ipp:683`), it has no descriptor > passing at all, segment delivery is POSIX-only by design, and there is no `fork`. So adopting > it would not make Windows work; it would only put asio headers inside `Transport/`, which > `ITransport.h` keeps dependency-light on purpose. **Revisit at P6.5**, where > `asio::generic::stream_protocol` carries a raw (family, type, protocol) triple and therefore > gives AF_VSOCK from the same implementation (2026-09-22: the end state's link is TCP, not > AF_VSOCK - §10.3 - and the argument is the same for `asio::ip::tcp`) - and where `ITransport` lets an `AsioTransport` > land beside `SocketTransport` without touching a caller. | `LinkTerms{dataPlane, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` | Appended to `Hello`/`Welcome`. `dataPlane` has exactly **one** legal value in P6, `SharedSegments`; `wireForm` exactly one, `StructImage`. Anything else is refused **by name**. The four sizes are **stated by the SERVER**. | any other value → `Refuse{ProtocolMismatch}` | the handshake | `hs` → **not landed** (verified 2026-09-22: `protocol.fbs` has no `LinkTerms`); re-homed to P6.5 `nd`, §10.3 | | `Refuse{code, detail, peer values}` | Appended `CtrlMsg` union tag. A handshake disagreement is an **answer**, not an abort (§5.1). | — | both sides | `hs` → **not landed** (no `Refuse` in `union CtrlMsg`); re-homed to P6.5 `wf`, §10.3 | | `wireFingerprint` / `buildFingerprint` | The single `abiFingerprint` splits. §4. | mismatch → `Refuse`, never `Fatal` | the handshake | `hs` → **partially landed** (explicit build stamp, `abiMajor`/`abiMinor` checked, real pids; the split itself and the `PipeFields.def` layout digest are not); re-homed to P6.5 `wf`, §10.3 | | `FatalCode` | **Unchanged**, 7 values (`protocol.fbs:248-256`). `table Fatal` appends `family: string`. §5.2. | — | — | `dl` | All schema edits are **append-only**. A FlatBuffers table field's id is its vtable slot and an enum value **is** the wire value; retired fields become `(deprecated)`, never disappear. `CapsSnapshot`'s four deprecated fields are the worked precedent and their reasoning is in `protocol.fbs` itself. > **`hs` implementation note, from c6's review.** `flatc` drops deprecated fields from the generated > `Create*` helper. Deprecating `defaultFb` while appending `result` therefore changes > `CreateSurfaceReply`'s arity, and the existing 6-argument call at `SurfaceOpCodec.cpp:183-184` > would **silently bind its `/*defaultFb=*/0` to `result`**. `hs` converts that call site to > `SurfaceReplyBuilder` with explicit `add_*` in the same commit, so a positional mistake is > impossible rather than merely unlikely. --- ## §3 Role, Dial, and the six predicates ### 3.1 D1 — what `Transport` reads as in the server process **`TransportMode::Spawn`. The child does not force `Monolith`.** `ARCHITECTURE.md:488`'s literal instruction ("`mobilegl_server_main` hard-sets `MG_Config::Transport` to `Monolith` before reaching `Init()`") is **amended by this file**. a6 measured why: 226 live lines test `Transport != Monolith` to select the **server** arm, 148 of them in `MG_Backend`. Forcing `Monolith` flips all of them to frontend glue in the one process with no frontend. The sharpest instance is `PipeInputs::IsLive()` (`MG_Impl/Pipe/PipeFill.cpp:2135`): forced monolith, it reads `MG_State::pGLContext`, which is null there, so **58 `MGB_CTX_LIVE` guards all answer "not live"**. Anti-recursion moves to a **separate, non-inherited axis**: `MG_Config::Dial == DialMode::No` in the child. The envp scrub stays as the second, independent catch, and S3 (§9) still falsifies it — and the diagnostic must **name which of the two fired**, or the double safety is untestable as two things. Package: `sm`. ### 3.2 D1c — there are six predicates, not three a6 proposed three. Three do not cover the tree. | predicate | asks | pull-build shape | |---|---|---| | `MGPipeSplitActive()` | do records cross a role boundary at all | `#else` returns `false` | | `MGPipeServerArm()` | am I executing as the server **right now** | `#else` returns `false` | | `MGPipeShouldDial()` | should this process connect out | `#else` returns `false` | | `MGPipeSessionLive()` | is a peer session live | role-defined; `#else` `false` | | `MGPipeBlocksAreDistinct()` | do the two roles share one storage object | `#else` `false` | | `MGPipeBackendIsLocal()` | is the backend in **this** process | `#else` `true` | `ContextEpoch::Server` and `ActiveXfbState`'s role index are **`MGPipeSplitActive()`** sites, not `MGPipeServerArm()` sites: they are partition keys, and the `ServerArm` disjunct would make a partition key thread-dependent. **D14 — all six must fold away in the pull build**, and the required shape is `SlotCaps.h`'s: the non-disaggregated build gets a **different definition** under `#else`, not a runtime-false predicate. `MGPipeSplitActive()` must additionally remain exactly one load of a process global and must never be latched into a `static Bool`. G1 is 0/0/0/0 and `.text` unchanged; anything else is a c6 defect. Package: `sm`. ### 3.3 D13 — rename before introducing `MGPipeRoleSplitActive()` (`MG_Backend/MGPipe/PipeInputs.cpp:292`) means `Ipc.RoleSplitState && Transport != Monolith` — the **rehearsal knob**, default off. It is renamed to **`MGPipeRoleSplitRehearsalActive()`**, which is what it has always meant, and `MGPipeBlocksAreDistinct()` is introduced beside it as the wider predicate. a6's shape — rename, then widen the same function — is **overturned**: a widened function under a narrow name is a name that lies. Blast radius, measured at this head: **12 sites**, of which `DualBlockScenario.cpp`'s assertion *message* embeds the old name and must be updated with it. Package: `sm`. ### 3.4 D10 — the four guards that cannot arm in a server-only process | guard | today | becomes | package | |---|---|---|---| | `MGPipeApplierReset` layer 2 (`MG_Pipe/PipeApply.cpp:1353-1355`) | needs `ClientSession::Active() != nullptr`, permanently null in a server process | middle conjunct → `MGPipeSessionLive()`; third → `!MGPipeServerArm()` | `sm` | | `MGPipeServerBlockNoteIdentity` (`PipeInputs.cpp:314`) | early-returns unless the rehearsal knob is on | gate → `MGPipeBlocksAreDistinct()` | `sm` | | `CountBarrierPull`'s unconditional Fatal arm (`PipeInputs.cpp:174`) | knob-gated | rides `MGPipeBlocksAreDistinct()` | `sm` | | the two `RefuseFromApplyThread` helpers (`MG_Impl/Pipe/SlotAllocator.cpp:29-57`) | `ServerLoop::OnApplyThread()` | OR'd with a process-role fact set once by `ServerMain` | `sm` | **The one that crashes rather than merely failing to arm.** With `MGPipeServerBlockNoteIdentity` early-returning, a spawn server's `gPipeInputs.ContextIdentity()` stays `nullptr` for the life of the process, and `DirectGLES.cpp:199-213`'s fb-slot memo does a **raw pointer compare with no generation** against it — so `nullptr == nullptr` reads as a **cache hit** and the first call uses an uninitialised slot. `PipeInputs.h:878-884`'s own comment says a named Fatal was intended there. `sm` lands the gate change; `st` lands the named Fatal. --- ## §4 The handshake (`hs`) ### 4.1 One equality test answers three different questions today `AbiFingerprintInputs` (`Transport/SessionRings.h:544-566`) mixes wire properties, struct-layout properties and pure build identity into one `Uint64`. It splits: **`wireFingerprint` — always compared.** Inputs, in order: an ordered **catalogue digest** over `PipeCalls.def`'s `(opcode, payload struct name, call-flag row, WaitClass)`; a **payload layout digest** derived from `PipeFields.def` (per member: name, `offsetof`, `sizeof`); the record-header layout; both blob-codec version stamps; the format-capability table's two extents; `kOpCount`; `MOBILEGL_ABI_VERSION`; endianness; pointer width; **and** `sizeof(MGPCaps)` and `sizeof(DynamicBackendParameters)`. Those last two **stay compared** because they are wire facts, not build facts: `MGPWireRec_GetCaps` is composed from `sizeof(MGPCaps)` (`MG_Pipe/generated/PipeWire.inc`). `sizeof(GLFunctionsTable)` is **deleted** from the mix: it never crosses the wire, so adding one backend slot today invalidates every peer for no wire reason. > **Why the layout digest is not optional.** `SessionRings.h:561-565`'s own comment says the git > stamp is in the mix because *two builds of the same sizes can still disagree about a FIELD ORDER, > which no sizeof can see*. A catalogue digest over `(opcode, struct name, flags, WaitClass)` sees > neither a member reorder inside a payload struct nor a width change. So the widely-proposed "drop > the git stamp, add a catalogue hash" is **wrong as usually stated** — it removes a guard and does > not replace it. The layout digest is the replacement, and `gen_pipe.py` already CI-enforces that > `PipeFields.def` names every member, so it is a generator addition and not a toolchain. **`buildFingerprint` — compared only while `Dial == Fork`.** Under fork the child *is* the same binary, so comparing it catches a stale `MOBILEGL_IPC_SERVER_PATH` pointing at an old `libMobileGLServer.so` — a real P6 failure mode. **P6's policy is therefore strictly stronger than today's, not looser**; the end state is a policy flip, not a redesign. ### 4.2 The missing build stamp a6 found `CMakeLists.txt:188-195` takes the git hash with **no `RESULT_VARIABLE` and no fallback**, so a failed `git rev-parse` silently yields `""`. Verified live: building this worktree from WSL produces `GIT_COMMIT_HASH_SHORT ""`, because the worktree's `.git` file holds a Windows path WSL's git cannot resolve. Two builds from different commits then get **identical** fingerprints and the stamp silently stops doing its documented job. **The fix may not touch `GIT_COMMIT_HASH_SHORT`.** That macro is baked into `glGetString(GL_VERSION)` (`MG_Impl/GLImpl/Getter/GL_Getter.cpp:614`), so redefining it changes an **application-visible GL string** and the pull build's `.rodata` — a behaviour leak and a G1 break at once. Instead `MGGitHash.h.in` gains a **second** pair, `MOBILEGL_BUILD_STAMP_VALUE` and `MOBILEGL_BUILD_STAMP_PRESENT`, consumed only by the fingerprint, and the CMake step gains a `RESULT_VARIABLE`. A build with no stamp sets `PRESENT` to 0, and a `Dial == Fork` handshake whose peer reports `PRESENT == 0` is a **named refusal**: under fork the two peers are supposed to be the same binary, and without a stamp that premise cannot be checked at all. Package: `hs`. ### 4.3 Two verified asymmetries `hs` also fixes - The client checks only `abiFingerprint` and **never `abiMajor`/`abiMinor`** (`ClientSession.cpp:566-568`). - `Hello::pid` is hard-coded `0` and `Welcome::serverPid` echoes the client's, so it is always 0 (`ClientSession.cpp:519-521`, `ServerSession.cpp:532-534`). §9.5's arm-proof gate wants the child pid; `serverPid` is its natural carrier and must start carrying it. ### 4.4 Segment geometry (D-size) `MOBILEGL_IPC_RING_MB` / `STAGE_MB` are read from the **client** process's env with ceilings of 1024 / 4096 MiB (`ConfigLoader.cpp:363-364`), while the **server** creates the segments. Under spawn that is a guest environment variable sizing an allocation inside the renderer process. The four sizes become **server-stated** `LinkTerms` fields; the client's values become a *request* the server may clamp, and the clamped values are reported in `Welcome`. `ShmSegment::Adopt`'s `fstat` "do not trust the peer's declared size" check (`Transport/ShmSegmentPosix.cpp:127-133`) is today the **only** bound, and it has no analogue on a link with no fd. The compile-time ceiling that refuses an oversized announcement by name is **P6.5's** to define; P6 does not pretend to have answered it. --- ## §5 Death, refusal, and what P5e changed ### 5.1 The handshake never aborts A version or terms disagreement is a `Refuse{code, detail, peer values}` control message plus `MOBILEGL_ERR_PROTOCOL_MISMATCH` on **both** sides: the server sends `Refuse` and exits 0, the client reaps the child and refuses **by name**, never falling back to monolith. Both `FatalAbiMismatch` call sites go (`ServerSession.cpp:471-483`, `ClientSession.cpp:565-568`). The worked shape is eight lines away in the same function: `ServerSession.cpp:456-466` already returns `MOBILEGL_ERR_PROTOCOL_MISMATCH` for a malformed frame, with the correct `msg_as_Hello() != nullptr` guard. Package: `hs`. ### 5.2 `Fatal{}` keeps aborting in P6, and gains one funnel a6 censused **92 abort sites** under `MG_Remote/` carrying **30 distinct family words**, against a **7-value** wire `FatalCode`. So `Session::Fail(FatalCode, detail)` as usually proposed **cannot carry today's vocabulary in its first argument**. **D2 ruling.** The first argument is an internal `MGFatalFamily`, generated from a new `MG_Remote/FatalFamilies.def` — one row per family word, each row carrying its `FatalCode` projection. The **wire** keeps `FatalCode` unchanged; `table Fatal` appends `family: string`. The projection is a **total table**, not a switch with a `default:` arm — this build has **no `-Werror` anywhere** (verified: `CMakeLists.txt`, every `*.cmake`, both workflows), so a gate premised on `-Wswitch` failing the build is fiction. `Session::Fail` stays `[[noreturn]]` in P6 and still ends in `abort()`. Log lines, CI greps and every red-once are byte-identical. What the funnel buys, and what makes it worth `dl`'s two days: - **one** place that publishes a `SessionFault{family, detail, seq, op}` frame to the peer **before** dying, so the guest's log names which record killed the session instead of reading a bare EOF; - one telemetry point; - a CI census gate refusing any **new** bare abort outside the funnel. > **LANDED, in three steps.** (1) `FatalFamilies.def` + `FatalFamily.h` generate `MGFatalFamily`, > its total projection `FatalCodeForFamily` and `FatalFamilyName`; 32 rows, tested. (2) `SessionFail` > (`FatalFunnel.{h,cpp}`) is the funnel: the ~90 scattered `MGLOG_F("...Fatal{...}"); abort();` pairs > under `MG_Remote/` become one call each, the message string passed **verbatim** so every line and > grep is byte-identical, the family enum riding alongside for the coarse code. `SessionFaultCount()` > is the telemetry point. The census now also requires every `SessionFail`/`WireLogFatal` call's > string to carry a `Fatal{` word, and its baseline drops from 93 bare aborts to 4 (both funnels). > (3) `table Fatal` gains `family: string`; `SessionFail` publishes the frame on whichever session > control plane is active before aborting, and the client's control-reply loop recognizes it, names > the family and latches device-lost. **seq/op are NOT threaded** through the 90 sites — the message > already names the op — so the frame carries `code`, `family` and `message`, not a field always 0. > A `kill -9` bypasses `abort()` and is caught by `dl`'s hangup witness instead; the self-detected > fatal is the one that publishes. **The policy flip is not P6's.** Making `Fail` return needs a real return path invented at every site whose callers rely on `[[noreturn]]`, and a6 counted how many do. That is `Ph`'s (§11). Two sites carry **no `Fatal{` marker at all** (`Client/PersistentMapTracker.cpp:1052-1058`, `Transport/WireLog.cpp:41-50`), so "the log stays verbatim" is not true of them and the family grep cannot see them. `dl` gives each a family tag. ### 5.3 The device-lost latch does not exist, and P6 writes it `P6-CONTRACT-DRAFT §5.1` says the latch "is implemented unchanged". It is not implemented at all: `FatalCode::DeviceLost`/`ServerCrashed` have zero producers and zero consumers, `glGetGraphicsResetStatus` returns `GL_NO_ERROR` unconditionally (`MG_Impl/GLImpl/Getter/GL_Getter.cpp:2855-2861`), and `MOBILEGL_IPC_RESPAWN` / `IDLE_EXIT_S` are parsed **nowhere** (only a comment at `Config.h:466`). Today a dead server leaves the client silently `DECLINE`-ing every verb forever with `glGetError` clean. **It is a package (`dl`), not carriage.** The latch is session-scoped and set from `Doorbell::PeerHungUp()` — D5c amends "`Dead()`" here, and §5.4's block says why — **never from a timeout**. `MOBILEGL_IPC_RESPAWN=1` is a named refusal until some stage implements the re-push. **LANDED.** `ClientSession::DeviceLost()` / `LatchDeviceLost()`, armed at the verb barrier, at the forced run-ahead wait and at the control-reply wait, and read by `GetGraphicsResetStatus`, which now answers **`GL_UNKNOWN_CONTEXT_RESET`**. Not `GUILTY` or `INNOCENT`: those assign blame, and the server died for a reason that never crossed the wire — it could as easily have been a driver fault as this client's draw. The consult and its include sit wholly inside `MOBILEGL_BUILD_DISAGGREGATED`; G1 re-measured after it, **0 symbol diff and `.text` unchanged at `0xa52203`**. That measurement also caught an unrelated 16-byte drift the guards did not: P6's O_APPEND log-sink fix was compiled into the pull build too, and is now guarded for the same reason. ### 5.4 Slow, dead, and frozen are three states - A published, never-applied record is **discarded at the latch, not waited on**. - **Slow and dead are distinguished without a timeout threshold.** A server one frame behind is P5e's intended steady state; a server that is gone is `Fatal{ServerCrashed}`. `SocketDoorbell` already latches `m_dead` on EOF — take the fact from there. - **A frozen peer produces no hangup**, and a6 found the tree has no defence: the control-reply wait `m_controlDone.wait(lock, pred)` (`Server/ServerLoop.cpp:724`) has **no deadline**, while the ring side has `kBarrierTimeoutMs`. A SIGSTOP'd or freezer'd server wedges the GL thread inside `eglMakeCurrent` with no diagnostic. **D5 ruling — two waits, two answers.** (a) The server's *in-process* mailbox wait gains a bound and its expiry is `Fatal{ControlReplyTimeout, "@"}`. (b) The client's *cross-process* reply wait is bounded by `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` (default 5000) and its expiry is **not** fatal: the client consults the doorbell's death latch — dead latches device-lost, alive-but-silent is a named diagnostic. Packages: `cp` (a), `dl` (b). (P7, p7/spawnhang: "silent" is now literal — a server whose apply thread is running the op says so every 250 ms with `SurfaceProgress`, and each report restarts the client's bound, up to 120 s; a server that never took the op, or is frozen, sends none.) > **D5c — the client's self-bell CANNOT witness the server's death, and `sm` is why.** Measured, not > predicted: `minecraft-1.21.4-fabric-iris-iterationrp-in-world` kills the *server* process outright > on this machine (llvmpipe: `LLVM ERROR: Cannot select: intrinsic %llvm.x86.vcvtps2ph.256`, SIGABRT > at call 159429), and the client did **not** take the `SessionWait::ShutDown` path that already > exists and is already correct. It sat out the full 120 s and reported > `Fatal{BarrierTimeout, "ResourceRespecify"}` — the wrong diagnosis, at 120 s a case, for a server > that had been gone since the first second. Under monolith the same trace fails in 3 s. > > The cause is structural and is the price of the two-fd bell: the client parks on `clientBell[0]` > (slot 5) and rings *itself* through `clientBell[1]` (slot 6), so it holds **both ends** of that > socketpair. EOF arrives only when every writer closes, and the client is one of them — so that > descriptor can never hang up, no matter what happens to the peer. A bell that can wake itself is > a bell that cannot hear a death. > > **`dl` therefore needs a witness descriptor whose far end only the SERVER holds**, polled beside > the park fd and never read from, whose `POLLHUP`/EOF latches `Dead()`. The control socket is > already exactly that and needs no new descriptor. The fix does not belong to `sm`: holding both > ends is required for self-notification and is not the defect — assuming one descriptor could > answer both questions is. > > **LANDED.** `SocketDoorbell::SetDeathWitness(fd)`; the client hands it > `SocketTransport::StreamFd()`. The witness enters the poll set with `events = POLLRDHUP` **and > nothing else**, which is what lets it watch a socket it must never read: `POLLHUP`/`POLLERR`/ > `POLLNVAL` arrive in `revents` unrequested, so the hangup is seen while `POLLIN` never is — a > control reply queued on that descriptor can neither wake the bell nor be consumed from under > `SocketTransport`'s reassembler. Measured: `kill -9` on the server is noticed in **0.01 s** > against a 120 000 ms barrier, and with the witness removed the same test takes the full 2 s > retry budget and fails naming this paragraph. > > **`Dead()` AND `PeerHungUp()` ARE TWO FACTS, and only the second may arm the latch.** `Park` > sets `m_dead` for `POLLERR`, `POLLNVAL` and unrecognised `revents` as well — each a fault in > *this* process's descriptor — and `CondVarDoorbell::Kill()` sets it on every orderly `Stop()`. > A latch taken from `Dead()` would report a lost GPU for a local fd bug and a context reset for > every clean exit. `ADeadBellIsNotAlwaysAHungUpPeer` is the red-once for the distinction; > `AnOrderlyStopIsNotADeviceLoss` is **not** — it stays green under the collapse, because under > spawn the self bell is a `SocketDoorbell` and `Stop()` never kills one. That was found by > running the falsification rather than by reading the code. --- ## §6 The control plane (`cp`) ### 6.1 What `fc` landed, and what is left P5f package `fc` framed the twelve `Server*` forwarders as value-only `SurfaceControlFrame`s and landed the wire codec. The draft concluded `cp` is "transport only". **Three things are not transport**, and a6 found all three: 1. The `InitCapabilities` **request** has no wire form (§2). 2. `SurfaceReply` cannot carry a `MobileGLResult` (§2). 3. **Neither side of the control plane is wired to a socket.** The apply thread's `ready` predicate has exactly three terms — stop, `ControlIsPending()`, `cmdHead != LocalTail()` — and a socket is not among them (`ServerLoop.cpp:398-402`), so a `SurfaceOp` arriving on the wire is read by **nobody**. The client's pump consumes only `CapsSnapshot` and drops everything else with a WARN (`ClientSession.cpp:1425-1432`). ### 6.2 D4 — who reads the control socket A dedicated **`mgl-srv-io` thread**: `ReceiveFrame` → `DecodeWireSurfaceOp` → post into the **existing** one-slot mailbox as an ordinary non-apply-thread poster. `ServerLoop.cpp:673-727` is not changed. The io thread is additionally the server's **only** sender on the control transport: `PublishCapsSnapshot` enqueues and the io thread drains one outbound FIFO — which is what fixes the ordering hazard that `CapsSnapshot` is pushed from **inside the dispatch, on the apply thread, mid-op** (`ServerLoop.cpp:959-969`). Rejected: adding a fourth term to the apply thread's `ready` predicate. That puts frame decode on the thread holding the native context and makes every control arrival a wake of the applier. ### 6.3 D7 — `seq` The **client** mints, always, dense from 1, never reused in a session. `seq == 0` arriving on the wire is `Fatal{ProtocolCorruption, "SurfaceOp.seq"}` at the **decoder**, not at `RunSurfaceControlFrame`, whose local mint (`ServerLoop.cpp:659-662`) stays and becomes reachable only for a local, non-wire post. Correlation lives in the client's control demultiplexer; `DecodeWireSurfaceReply` keeps its void signature and stops being the place correlation was expected. ### 6.4 `ForgetCurrentTuple` The N-3 rule holds identically on the frame path: the forgetting calls are inside the dispatch (`ApplySurfaceControlFrame`) and `ServerLoopTest`'s C7/N-3 controls drive them through framed posts unchanged. No change. --- ## §7 Windows — rule H's enforcement (`cp`) ### 7.1 The blacklist is open, and the end state's first guest walks through it `WindowBackendForWireWindowKind` (`Protocol/SurfaceOpCodec.cpp:104-113`) **accepts** `WindowKind::X11 → WindowBackend::X11` and `Win32Hwnd → Win32`. `DecodeWireSurfaceOp` (`:141-179`) refuses only `AndroidNativeWindow` and `MetalLayer`. An accepted kind's `nativeToken` is copied through and `UnpackWindowHandle` (`Server/ServerLoop.cpp:875-882`) casts it to `void*` **unconditionally**, whence it reaches `eglCreateWindowSurface` in the server process. `DetectWindowBackend()` is a compile-time `#if` chain (`MG_Impl/EGLImpl/EGLImpl.cpp:59-70`), so a guest cannot avoid announcing its own window system. ### 7.2 D8 — whitelist, and in P6 the accept set for a window op is empty - `X11` → `Fatal{UnmigratedSurface, "X11@P12"}`; `Win32Hwnd` → `"Win32Hwnd@P12"`. - `None` on a **window** op → `Fatal{ProtocolCorruption, "SurfaceOp.windowKind"}`. Today it is accepted, maps to `WindowBackend::Unknown`, and the raw token is copied through anyway. - Separately and independently: `nativeToken != 0` on **any** op → `Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}`. - `Pbuffer` / `Surfaceless` are surface **shapes**, not window backends, and remain the only things P6 lands. **Do not append `WindowKind::ServerOwned` in P6.** The schema is append-only, so an enumerator appended now and re-meant later is a wire value burned; P6 has no consumer for it. P12 appends it. ### 7.3 The client emits unconditionally, and P6 must decide what that costs `Client/BackendObject_Remote.cpp:214-216` posts `SetWindowHandle` with no split-arm check, so on a real Android app the **first `eglCreateWindowSurface` aborts the server process** and the client sees only a dead peer. `cp` mirrors the refusal on the **client** side, so the diagnostic names the cause in the process that caused it. This is P6's, not P12's, because P6's device lanes run on a phone. ### 7.4 `m_windowHandle` has four stores, not one a6's precise shape, correcting the corpus (and `P6-ENDSTATE-REVIEW §2.5`): four stores (`MG_Backend/BackendObject.cpp:475`, `:365`, `:206`, `:207`), five reads, and `:283` is a **call**, not a store. `.Width`/`.Height` have **no readers**. The smallest latch point for a server-owned window is `RegisterEGLWindowSurface` (`:232-238`), and the non-obvious fourth edit is the `sameHandle` dedup key (`BackendObject_DirectGLES.cpp:962-967`), which under server ownership never matches and would rebuild the native context on every `eglCreateWindowSurface`. Recorded here for P12; P6 changes none of it. --- ## §8 The data plane (`lk` implements, `c6` declares) ### 8.1 Why a seam at all `ITransport.h:16-20` says in its own header that the hot path bypasses it: it is the **control** plane, and that scope is already correct for a socket. Below it there is **no interface of any kind** — `Ring.h`, `ReplySlot.h` and `EventRing.h` take raw `void* base + capacity`. So "swap the transport" has nothing to swap. `c6` declares `Transport/ILink.h` and `Transport/StreamLink.h`. `lk` implements `ShmLink` by moving today's `SessionSegments` / `Ring` / `ReplySlotPool` / `EventRing` behind the interface, line for line, with **zero behaviour change**. ### 8.2 The seam is a setup-time interface, not a hot-path one `ILink` hands out POD handles **once at attach** and then stays out of the way. No `ILink` virtual is called per record, per publish, or inside any spin predicate. Three grounds, and the second is decisive: 1. Two virtuals per record at the 4000 records/frame that `SEG_CMD` is sized against is ~8000 indirect calls/frame — about 0.5% of the measured 15.72 ms client frame. Affordable, but the only lane that could measure it has ±4% run-to-run dispersion, **eight times the effect**, so the gate `P6-ENDSTATE-REVIEW` demands could only ever print "within noise" — a green that proves nothing. 2. **The spin predicate forbids it outright.** `Doorbell::Wait` is a non-virtual template whose spin loop calls `ready()` up to `SpinItersPerUs() * spinUs` times; at 916 waits/frame with 99.8% resolved inside the spin, the predicate runs 10⁵–10⁶ times per frame and reads the watermarks straight off the page. That is the 31.58%-of-the-GL-thread path. A virtual there is not a rounding question. 3. The encode path already depends on four inline accessors around its single `Reserve`, and virtualising `Reserve` alone would force either four more virtuals or moving R-10's wrap counters behind the seam — and those are deliberately encoder-owned. **The gate is therefore an `objdump` call-count of the encode path, not a wall clock**: after `lk` the path must have no more out-of-line calls than today. That is falsifiable; "within noise" is not. ### 8.3 `RingControl`'s regroup is `lk`'s, and it is a wire break the day a second build exists The watermark cache line is `{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial, presentAckSerial}`; `eventRingFull`/`eventDropped` sit in a **separate** `alignas(64)` group with `serverEpoch`/`ringGeneration`/the two park flags. And `submittedSeq` is **producer-written** (`Ring.h`'s "THE FIVE WATERMARKS" block) while the other four are consumer-written — so the group has **mixed writers**, and any "single-writer progress block aliased over the watermark line" is unsound. `lk` therefore **regroups**: `submittedSeq` moves to the producer's own line, beside `cmdHead`, which is the other thing the producer writes; what is left — the four consumer-written watermarks — becomes a named `LinkProgress` member with per-field `offsetof` static_asserts. > **Correction, found while `lk` was measuring its own blast radius.** An earlier revision of this > section, and of `ILink.h`, also promoted `eventRingFull`/`eventDropped` into the watermark group > and called the result single-writer. **Both were wrong.** `eventRingFull` has two writers: the > server latches it with `store(1)` when `SEG_EVENT` fills (`Transport/EventRing.h:140`) and the > **client clears it with an `exchange(0)` on every drain** (`:200`). Promoting it would put a > per-drain RMW by the client on the same cache line as `appliedSeq` — which the server > release-stores every 64 records and the client's spin predicate reads 10⁵–10⁶ times per frame. > That is new false sharing on the hottest line in the system, and it would land in exactly the > numbers this package's gate has to hold constant. The two flags stay in the doorbell/generation > group, where mixed writers already live beside `consumerParked`/`producerParked`, and cross the > seam through a separate `EventFlags()` accessor. *Accessor*, not line: they share > `serverEpoch`'s line, which is where mixed writers already live. It is free **today** — both peers are the same binary, enforced by the fingerprint — and a wire break the day a second build exists. It is **`lk`'s and not `c6`'s** because the rename blast radius is **109 sites in production alone**, not the 39 an earlier draft assumed, and because five of them sit behind `#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)` — an Android configure compiles them out, so a missed rename is green on device and red only on a desktop split-peek build. `lk` publishes the per-file itemisation before it starts. ### 8.4 What `StreamLink` is for Every method is a named refusal, `Fatal{UnmigratedDataPlane, "StreamLink@P6.5"}`. It exists so the seam's adequacy is a **compile**, not an argument: if `ILink` cannot name what a byte stream needs, `StreamLink` cannot be declared against it. Writing it is what revealed that the seam needs `FlushProgress()`, `ResolveSpan()` and a reply pair — three methods the brief's own list (progress transmission, send window, flush) did not name. `c6`'s red-once: delete one `ILink` method `StreamLink` declares and the build goes red. ### 8.5 The purity gate (`lk`) After `lk`, the identifiers `RingControl`, `RingProducer`, `RingConsumer`, `ReplySlotPool` and `EventRingConsumer`, and any `m_shm.` expression, appear **only** inside `Transport/ShmLink.*` and the transport's own tests. Same shape as the existing A/B/C purity gates. The baseline is recomputed mechanically at `lk`'s head and published per file — the earlier draft's ratchet number was wrong by a factor of three, and a ratchet installed at the wrong number is red on day one for reasons that have nothing to do with a leak. ### 8.6 `SEG_STAGE`'s cursors are deliberately dead `Ring.h`'s "DEAD IN P5, DELIBERATELY" states it and a test pins it: `SEG_STAGE` is **not a ring**; staging is an encoder-local linear allocator reclaiming on `retiredSeq`, and all three stage cursors stay zero for the whole of P5. A stream link's send window either revives them **completely** or does not use them; the header names the middle state as *a guaranteed hang rather than a slow path*. P6.5's, recorded here so it is not rediscovered. --- ## §9 The gate Beyond the five-part gate (`docs/Disaggregated/ARCHITECTURE.md` §13.2): 1. **G1** — pull build 0/0/0/0, `.text` unchanged. Every predicate in §3.2 folds away. 2. **G2/G14** — `integration-spawn`'s case-name set identical to `integration-split`'s. 3. `integration-spawn` green at the same count as `integration-split`. 4. **Process tree, mechanical** — exactly one extra child while running, zero after; `HeadlessGL`'s fork pre-check leaves no orphan. 5. **Arm proof (ID-124)** — every spawn entry records the child pid and `transport=spawn` in its own private log. A spawn lane that fell back to monolith passes items 1–4. §1 is why this gate is not optional: today the fallback and the intended state are **the same value**. 6. **Red-once per package (R-16)**, one named pair each. 7. **Device** — Redmi `2f7cbe2e`, reboot-clean, paired A/B in one thermal window. 8. **Performance recorded, not gated**, but three numbers are mandatory: the socket doorbell's cost against `inproc`'s condvar; `SEG_STAGE` bytes/frame; and records/frame **post-chunking**. The last two are takeable on the existing inproc lane today and `MEASUREMENTS.md:342` says in as many words that no post-chunking distribution exists. > Item 8's wording must be able to **accept a negative regression without looking suspicious**: today > the client spins ~1600 times/frame on a shared cache line with 99.8% of waits resolved there, and a > socket link has no such line. Converting that into ~4 blocking reads/frame may be **cheaper**. Negative controls, each run red once: | # | knob | what it must falsify | package | |---|---|---|---| | S1 | `MOBILEGL_IPC_SERVER_PATH=/nonexistent` | named refusal, not a silent monolith fallback | `sm` | | S2 | `kill -9` the server mid-frame | the latch; the client never hangs and never keeps submitting. Red-once the **wrong implementation** too: set the latch from a timeout and the paired control "a server one frame behind is the normal state" must go red | `dl` | | S3 | child envp not scrubbed | `Dial = No` catches it **and the diagnostic names which of the two catches fired** | `sm` | | S4 | inherited P5f epoch invalidation disabled | a replaced context inherits stale synced state; a still-live XFB lifetime retains its paused span | `st` | | S5 | `WindowKind::X11` with a plausible XID reaches the server | named Fatal, **and the gate falsifies that the token reached `UnpackWindowHandle`**. Today's S5 tests only `AndroidNativeWindow` and stays green with §7.1's hole open | `cp` | | S6 | a peer offers `LinkTerms.dataPlane = StreamOnly` | refused by name, **and the server survives to serve the next connection** | `hs` | | S7 | a bare `abort()` added outside `Session::Fail` | the census gate goes red | `dl` | | S8 | — | `integration-spawn` asserts `SessionFaultCount() == 0` over a whole run: the **good-path** assertion E1/ID-122 wanted and could not express, and which S5/S6 can falsify | `t6` | --- ## §10 Amendments ### 10.1 To `P6-CONTRACT-DRAFT.md`, which this file supersedes 1. §1's "ABI fingerprint | Unchanged … same-machine, same-binary" → §4. 2. §5.1's "The latch … is implemented unchanged" → **false**; §5.3. 3. §4's "P6's remainder is TRANSPORT ONLY" → **false in three ways**; §6.1. 4. §7's "a transport swap and nothing else" → **no longer true**. This file adds `lk`, `hs` and `dl` to the package list and says so here rather than smuggling it. ### 10.2 To the earlier contracts 1. `CONTRACT-P5C.md:537-539` — EGL value framing was discharged by P5f `fc`, the unpack/render-state epoch work by P5f `fs`. P6 carries the existing frames over the socket and verifies the inherited lifecycle rules in the second process. 2. `CONTRACT-P5C.md:56` — P6's multi-context shape. **Declined**: P6 stays single-context. §11. 3. `Transport/ReplySlot.h`'s "P6 debt" for the 2 MiB reply cap and chunked readback. **Declined**: P9's. Note `P6-SPAWN-PLAN §8.5` already reassigned it and the header was never updated — two documents in the tree disagreed; this file settles it. 4. `Ring.h`'s "BATCHING MAY ONLY MAKE A WATERMARK LATE" — `appliedSeq` is excluded from lazy publication **by name**. P6 does not change the behaviour; it records that any future stream link **amends** this rule rather than inferring around it. Several designs have argued the late-never-early invariant licenses lazy `appliedSeq`; it explicitly carves `appliedSeq` out. 5. `ARCHITECTURE.md:488` — the child-forces-monolith rule. **Amended**; §3.1. 6. `ARCHITECTURE.md:9` and `MG_Backend/MGPipe/PipeInputs.h:281`, `:744`, `:957`, and `P5F-WIRE-COMPLETENESS §1.4` — "the server does not link `MG_Impl`". **False at this head.** §12. ### 10.3 Amendments of 2026-09-22 — the end state re-ruled 1. **The end state is TCP across machines, OSes and architectures**, not an AVF pVM over `AF_VSOCK`. `docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md` carries the retraction at its head; its §3 gains form **D**. Same-machine `spawn` (AF_UNIX + shared segments) stays as the local form. 2. **The transport stack has two independent axes.** The control plane (`ITransport`: `fork` inherited fds / `unix:` / `tcp://host:port`) and the data plane (`ILink`: `ShmLink` / `StreamLink`) are chosen separately, negotiated in the handshake, and may be mixed — on one machine, control over TCP with data over shared segments is a legal pair. Callers above `Transport/` see `ITransport` + `ILink` and nothing else; branching on the link kind above that line is a purity-gate failure (the `lk` grep gate, widened). Design: `ARCHITECTURE.md` §11.9. 3. **§8.2's "`ITransport` is not extended" is amended in one respect**: descriptor passing leaves the control plane and becomes the shared-segment data plane's *delivery*. `ShmLink` brings its own AF_UNIX aux rendezvous, named in `Welcome`, and `SCM_RIGHTS` travels on that and nowhere else. Rule G is unchanged: a transport that declares no descriptor passing (TCP) passes none, and the data plane that needs it must bring its own channel or not be selected. `auto` selection is decided by a **successful delivery**, never by inspecting addresses, and a fall-back to `Stream` is named in the log and the stats line. 4. **§2's three `hs` rows** (`LinkTerms`, `Refuse`, the fingerprint split) did not land in `hs`; `protocol.fbs` carries neither `LinkTerms` nor a `Refuse` member of `CtrlMsg`. They are P6.5's (`nd`, `wf`), and S6 in §9 moves with them. `CURRENT_STAGE_PROGRESS.md`'s earlier claim that S6 had been run red is withdrawn. 5. **Fixed-width wire form is no longer negotiable-later.** Two different binaries from two compilers are the end state, so the `PipeFields.def`-derived layout digest, the fixed-width rewrite of `DynamicBackendParameters` / `MGPCaps` / the `RenderStateParameters` blob, and the `buildFingerprint`-only-under-`Dial == Fork` policy are P6.5 `wf`, taken off P7. 6. **`Ph` gains pairing/authentication**: a `Hello` without a pairing token is refused on any non-loopback listener. `Ph` precedes P12's non-loopback listen. 7. **"The 511 class-C entries" in §11** is the P5-joint census (`docs/Disaggregated/notes/p6/census-classC.md`). At this head the `MGR_UNMIGRATED_*` lists are empty; `SetSwapInterval` (P10) and `DeleteTransformFeedback` (P9) are the two that remain. (2026-09-29: both are gone. P10 made the `SetSwapInterval` slot the server forwarder, so class C is empty - `CONTRACT-P10.md` §2.) --- ## §11 Not P6 Real window arrival and the `android:process=":mgl"` Service (P12); multi-context (P12); chunked readback and the reply-slot pool (P9); `DynamicBackendParameters`' fixed-width rewrite (landed by P6.5 wf); `MOBILEGL_IPC_POLL_ESCALATE` (P10); remaining monolith-only frontend-object / twin-registry glue (P3b/P4b) and the named P7 functionality debts; Windows `pipe:` / `unix:`; the 511 class-C entries. **The two-axis transport stack — control × data, TCP-capable, cross-build — is P6.5** and is the IPC track's next stage (§10.3). P9 builds its reply semantics on P6.5's message replies rather than on a slot pool, and P11's T0/T1 are refused by name on a stream data plane (landed by P11 A1 at the handshake: `Transport/AdoptTier.cpp`, `SettleAdoptTierAtHandshake`; CONTRACT-P11 §1). **Untrusted-guest hardening is `Ph`**, and it must land before any shipping server app accepts a connection from a guest it did not produce — including the `Fatal`-policy flip (§5.2), the handle-slot budget, and the caps in §12. E1's re-specification (ID-122) is **not** discharged by P6 and does not become P6's because P6 starts. --- ## §12 Recorded debt ### 12.1 The server image links `MG_Impl` in P6 (D12) **P6's server image is the whole `libMobileGL.so`** — which is what `ARCHITECTURE.md:496`'s "one shared library, two roles" always meant. The assertions that the server does not link `MG_Impl` are false and become debt with named owners. Measured: **184 symbols**, 177 of them real function calls, that the server set needs and only `MG_Impl` / `MG_State` / `MG_Remote/Client` define. Owners: **P7 101**, **P3b/P4b 14**, both backends **60**, **P6's own 6**. There is additionally **no module target to link** — `SOURCE_FILES` is one flat list feeding both library targets — so establishing module boundaries is itself named work, owed to P13. Method and full symbol list: [`a6-link-experiment.md`](../notes/p6/a6-link-experiment.md). **P6's own six** are not P7's and are `sm`/`lk`'s to clear: `Server/PipeApplier.cpp` calls `Client::ClientSession::NoteApplyThreadEnteredApplier()` / `NoteApplyThreadLeftApplier()`; `Wire/PipeWireCodec.cpp` needs `MG_State::GLState::DecodeProgramArchive` and `Client::AdoptTierIsEmulate()`; `MG_Pipe/PipeApply.cpp` needs `Client::ClientSession::Active()` and `AdoptTierIsEmulate()`. **Gate (landed, P7 wave 0 — ID-P7-7)**: `scripts/link_ratchet.py` recomputes this set from an explicit per-object partition on every `build-linux-split` run and fails on any symbol absent from `scripts/data/link_ratchet_baseline.txt` — which stands at **186** on `e8b2c4bd`, the 184 plus P6.5's `Client::ClientSession::StartSpawned()` and `MG_State::GLState::ProgramArtifactsSchemaFingerprint()`, with the three backend buckets reproducing a6's 101 / 14 / 60 exactly — while a symbol that disappears is reported rather than failed, so progress never turns the build red ([`link-ratchet.md`](../notes/p7/link-ratchet.md)). ### 12.2 D11 — five unbounded peer-driven allocations, capped, with no `try`/`catch` Every peer-declared count is bounded **before** it reaches an allocator and the bound is a named refusal. `MG_Remote` gets no `try`/`catch`: a cap that is checked is better than an exception that is caught, and the funnel (§5.2) is where the diagnostic lives. | site | today | package | |---|---|---| | `MG_Pipe/PipeApply.cpp:1563-1569` | resizes to a peer-supplied `Cso.Slot + 1` with **no slot limit**; every sibling create-family goes through `RecordAt`'s bounded path. `Slot = 0xFFFFFFFE` asks for ~1.7 TB | `dl` | | `Server/PipeApplier.cpp:416-441` | readback scratch resized from `Box.W * Box.H * bpp` **before** any size gate. 65535² RGBA8 asks for ~17 GB | `dl` | | `MG_State/.../ProgramArtifactsCodec.cpp:166-175`, `:216-223` | count bounded by remaining **bytes**, then multiplied by `sizeof(element)` | `dl` | | `Server/StagedTextureStore.h` | resize not bounded against the level's declared extent | `Ph` | | `SEG_EVENT` overflow | a guest that merely stops draining kills the host renderer in 60 s | `Ph` | ### 12.3 One guest per server process (D9) For the whole of P6, one server process serves exactly one client session, and it is a **structural fact, not a preference**: `g_Display`/`g_Context`/`g_Surface`/`g_Config` are per-process file-statics and `InitDisplayAndContext` opens with an unconditional `DestroyEGLContext()` whose last act is `eglTerminate` — a **process-wide** EGL teardown. `ServerSession::Accept`'s same-object arm (`ServerSession.cpp:425`) returns `MOBILEGL_ERR_INVALID_ARGUMENT` **with no log line at all**; `sm` gives it one. Under `Dial == Fork` a second guest is unreachable by construction, so the invariant becomes falsifiable only under `Dial == Connect`, which P6 does not land. ### 12.4 Settled by ruling rather than by experiment - **Who runs the applier in the server process**: `ServerMain` **must** drive it through `ServerLoop::Start()`, so `g_applyThreadKey` is published and the dozen `OnApplyThread()` guards keep meaning what they mean. Owner `sm`; red-once drives the applier without `Start()`. - **The server's exit primitive**: `_exit`, after an explicit log flush. It decides whether `g_processTeardown` is set and therefore whether seven twin destructors take their early-out. Owner `sm`. - **The handshake asserts the resolved subsystem mask**; a mismatch is `Refuse`, not `Fatal`. The envp scrub list does **not** include `MOBILEGL_PIPE_*`, so the two processes can resolve different masks from their own environments. Owner `hs`. - **P6 maps no segment read-only.** `ShmSegment.h:62-63`'s stated design — the peer gets `SEG_CMD`/`SEG_STAGE` read-only on the server side — **cannot be implemented as written**, and the comment is corrected rather than left to mislead. Owner `lk`. - **`DualBlockScenario`'s `serverIdentity != nullptr`** is an in-process fact, and G2/G14 requires the spawn lane to keep the case name. Its assertions are **re-homed**, not weakened: `DualBlockPeek` becomes a control-plane query answered by the server process. The declared fallback — assert `MGPipeBlocksAreDistinct()` on the client side and carry the identity half by §3.4's server-side Fatal — must be **declared here** if taken, not discovered in review. Owner `t6`.