From eeaf14c1f7e78ddaf6456653ebf054eb4d6fc6f3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:35:35 -0400 Subject: [PATCH 1/6] P5f fr: refuse frontend identity on every registry apply --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 4 + MobileGL/MG_Backend/DirectGLES/SlotTables.h | 67 ++++----------- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp | 40 +++------ MobileGL/MG_Impl/Pipe/SlotAllocator.h | 40 ++------- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 82 +++++++++++++++++++ 5 files changed, 125 insertions(+), 108 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 266d4d28..622fe7af 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -8292,6 +8292,9 @@ namespace MobileGL::MG_Backend::DirectGLES { ); } +#if !MOBILEGL_BUILD_DISAGGREGATED + // P5f (fr): no declaration, dispatch-table slot or caller remains for this legacy + // helper. Exclude it from the server-capable build; retain non-D-P bytes for G1. GLuint GetBackendProgramId(GLuint program) { if (!MGB_CTX->ValidateProgramName(program)) { MGLOG_E_ONCE("Invalid frontend program object: %u", program); @@ -8320,6 +8323,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } return backendObj->GetBackendProgramId(); } +#endif void Clear(GLbitfield mask) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index 3ae3d464..8cdca713 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -88,19 +88,11 @@ // allocator's own three entries carry, repeated here so the refusal names this surface), and // the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead. // -// P5e (id, CONTRACT-P5E §4.1 / §5.8) SETTLES WHAT HAPPENS TO THE FRONTEND-KEYED HALF: it is -// NOT deleted. The push build under Transport=monolith keeps its frontend arms token for token -// (ruling 1), which is what the MOBILEGL_PIPE_VERIFY comparator needs and what makes -// MOBILEGL_IPC_RUN_AHEAD=0 a pure wait-rule A/B on identical server code. So -// GetOrCreate(StatePtr), Find(StateObject*), HandleOf, NoteStateForHandle/StateForHandle, -// Entry::stateRef and the resolution memo all survive - as MONOLITH GLUE, each of them a NAMED -// FATAL the moment it is reached from an apply thread that is applying an UNBARRIERED record -// (§4.4: the client's wait is the only thing that makes such a read stable, and an unbarriered -// record has none). The refusal is raised either by the allocator guard, for the three members -// that call the allocator, or by MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply for the -// three that do not. There is no silent path: the phase's claim is that the twin is resolved -// from the handle the record carried, and a surface that quietly answered from a frontend -// pointer instead would make that claim untestable. +// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf, +// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport +// apply before touching a frontend object, with or without a barrier or named scope. +// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef. +// The registry guard is independent of Magma's legacy allocator debt scope. namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_PIPE_PUSH @@ -283,19 +275,10 @@ namespace MobileGL::MG_Backend::DirectGLES { EnsureProcessTeardownSentinel(); #if MOBILEGL_BUILD_DISAGGREGATED - // P5c (hd, CONTRACT-P5C §3.1): this overload MINTS - it is monolith glue, and with - // an active transport a call from the apply thread is Fatal{RoleViolation, - // "MGPipeSlots"} before the allocator is touched. Split paths call the handle - // overload below. (The check also lives at the allocator's own three entries; it - // is repeated at this entry so the refusal names this surface even if the entry - // set changes.) - // - // P5e (id, §4.4): that one call is also this member's unbarriered-apply refusal. - // The guard exempts a probe only inside a named scope AND while the record being - // applied is barriered, so reaching a MINT from an unbarriered apply aborts here - // whatever scope is open - which is what "kept only as monolith glue" has to mean - // if it is to be checkable. - MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("GetOrCreate(StatePtr)"); + // This overload mints from frontend identity and is monolith-only. Refuse + // before either the client object or allocator is touched, even when a + // legacy named scope is open and the current record is barriered. + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)"); #endif const MG_Pipe::MGPipeHandle handle = @@ -407,25 +390,12 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry->Live ? entry->Gen : 0; } - // P5c (hd): remember the frontend object a HANDLE-keyed twin was synced from. The - // minting overload sets stateRef itself; the handle overload cannot (no object - // crosses), so a caller that legitimately holds the object - the record-driven sync, - // which arrived holding it through the object-class barrier-pulled rows - notes it - // here. It is what lets a later handle-only resolution (P5c's named blit, and P5e's - // re-typed ForEachLive) reach the frontend object the twin's sync body still walks, - // without probing the client's slot allocator (T2). Same liveness rules as the minted - // stateRef: never an identity test, never read to decide the slot is dead. - // - // P5e (id): the pair is now compiled under MOBILEGL_PIPE_PUSH rather than - // MOBILEGL_BUILD_DISAGGREGATED, because ForEachLive's caller needs it in the - // push-monolith build too, where the note is simply always present. And each half is a - // NAMED FATAL from an unbarriered apply (§4.4): they answer from a frontend SharedPtr - // that only the client's wait pins, so without that wait the object they hand back may - // already be the client's next one. Neither touches the allocator, so the allocator - // guard never sees them - this is their own refusal. + // Remember/read the frontend object last used by the monolith sync path. + // Handles supplied to the server never need this weak state. Neither method + // touches the allocator, so each carries its own unconditional apply guard. void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { #if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply("NoteStateForHandle"); + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle"); #endif if (MG_Pipe::MGPipeHandleIsNull(handle)) return; Entry* const entry = EntryOrNull(handle.Slot); @@ -437,7 +407,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // this answers only "which object was this twin last synced from". StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { #if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply("StateForHandle"); + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle"); #endif if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; const Entry* const entry = EntryOrNull(handle.Slot); @@ -501,12 +471,9 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle; #if MOBILEGL_BUILD_DISAGGREGATED - // P5c (hd): same guard as the minting overload - a lifetime-id probe from the - // apply thread is Fatal{RoleViolation, "MGPipeSlots"} with an active transport. - // P5e (id, §4.4): and now also whenever the record being applied is UNBARRIERED, - // scope or no scope. This member and Find(StateObject*) below it are the two the - // per-family packages retire by passing the handle the record carried instead. - MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("HandleOf"); + // Resolve frontend identity only in the monolith path. A cache hit is + // subject to the same guard as a fresh allocator lookup. + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf"); #endif const Uint64 lifetimeId = stateObj->GetLifetimeId(); if (lifetimeId == m_memoLifetimeId) return m_memoHandle; diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index f2b74952..21cb4c0f 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -32,18 +32,9 @@ namespace MobileGL::MG_Pipe { void MGPipeRefuseAllocatorFromApplyThread(const char* entry) { if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return; - // CONTRACT-P5E §4.4 AMENDS CONTRACT-P5C §3.1 HERE, AND IT IS THE WHOLE OF WHAT id - // CHANGES ABOUT THIS FUNCTION: a named exemption is a debt the CLIENT'S WAIT pays for. - // Behind a barriered record the client is parked in WaitForApplied and its allocator - // is not moving, so a read-only probe is stale-free; behind an unbarriered one it is - // running ahead, and the same probe reads a free list and a lifetimeId -> slot map the - // client is concurrently mutating. So the two scopes below exempt nothing at all once - // the record is unbarriered - regardless of MOBILEGL_IPC_STRICT_ERRORS, because the - // value would be wrong by construction and there is no "count it" arm for that - // (CONTRACT-P5E, rule F). - // - // Every record is barriered until ra lands the wait rule, so this reads exactly as it - // did at 2fde7034 for the whole of this phase. + // P5f: frontend-keyed registry probes are no longer an exemption. The + // separate Magma allocator debt remains backend- and barrier-keyed until + // its remaining monolith glue is removed; it cannot exempt twin-table APIs. if (MGPipeApplierCurrentRecordIsBarriered()) { // Ruling 12: Magma's four P7 debts, and ONLY on a DirectVulkan server. The key is // on the backend kind rather than on the site because the scope is a class anyone @@ -53,34 +44,29 @@ namespace MobileGL::MG_Pipe { MG_Config::ActiveBackendType == BackendType::DirectVulkan) { return; } - // The G6 frontend-keyed registry family: the barriered-row sites the per-family - // packages have not carried a handle to yet (CONTRACT-P5E §4.4). - if (MGPipeFrontendKeyedRegistryScope::ActiveOnApplyThread()) return; + } MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread called " "MGPipeSlots().%s. With an active transport the client slot allocator is " "client-only memory (CONTRACT-P5C §3.1, rule E; CONTRACT-P5E §4.4): a handle " "arrives already minted in a record, and a server that resolves or mints one " "off a frontend object's lifetime id is reading memory that will not exist on " - "its side of a real split. A named exemption scope admits it only while the " + "its side of a real split. Only Magma's allocator debt scope admits it while the " "record being applied is BARRIERED (barriered=%d) and, for Magma's P7 debt, " "only on a DirectVulkan server", entry, MGPipeApplierCurrentRecordIsBarriered() ? 1 : 0); std::abort(); } - // P5e (id): the non-allocator half of the same rule. SlotTables.h's state note, its reader - // and ForEachLive's weak-reference walk read server memory KEYED BY FRONTEND IDENTITY and - // hand a frontend SharedPtr back, which is the same violation one step removed - no - // allocator call, so the guard above never sees it. - void MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply(const char* entry) { - if (!MGPipeApplierIsUnbarrieredApply()) return; + // P5f (fr): all frontend-identity registry surfaces are monolith glue, including + // the ones that do not touch the allocator. Neither a wait nor a named scope can + // make a frontend SharedPtr exist in a separate server process. + void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry) { + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return; MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread reached " - "BackendSlotTable::%s while applying an UNBARRIERED record. The frontend-keyed " - "half of the twin table is monolith glue (CONTRACT-P5E §4.1, §5.8): it answers " - "from a frontend object the server has no wait pinning, so the SharedPtr it " - "would hand back may already be the client's next object. Resolve the twin " - "from the handle the record carried instead", + "BackendSlotTable::%s. Frontend-identity registry operations are monolith-only, " + "including barriered records; resolve the twin from the record's handle instead", entry); std::abort(); } diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index 8c028ae1..7316112e 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -186,15 +186,11 @@ namespace MobileGL::MG_Pipe { // caller is never refused by it. Bool MGPipeApplierIsUnbarrieredApply(); - // The refusal for the frontend-keyed surfaces that do NOT touch the allocator and so are - // not covered by MGPipeRefuseAllocatorFromApplyThread: the state note, its reader, and - // ForEachLive's weak-reference walk (SlotTables.h). They survive P5e as MONOLITH GLUE - // (CONTRACT-P5E §5.8) and this is what keeps that claim honest - reaching one of them from - // an unbarriered apply is Fatal{RoleViolation, "MGPipeSlots"} naming the member, not a - // silently stale SharedPtr. Same name in the refusal as the allocator guard's on purpose: - // it is one surface, "server memory keyed by frontend identity", and a reader chasing the - // abort should land on the same rule either way. - void MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply(const char* entry); + // P5f (fr): frontend-object twin lookup, minting and weak-state access are refused + // on EVERY transport apply, including barriered records and both named scopes. + // The handle overloads remain server-local; the frontend overloads remain monolith + // glue. This separate guard also prevents borrowing Magma's allocator exemption. + void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry); // The NAMED EXEMPTION to the rule above (CONTRACT-P5C §3.1, as amended by CONTRACT-P5E // §4.4): the family of sites whose handle-carrying records the client does not EMIT yet. @@ -247,28 +243,10 @@ namespace MobileGL::MG_Pipe { Bool m_counted; }; - // The SECOND named exemption family (CONTRACT-P5C §5.4): the frontend-keyed twin - // registry (audit row G6). The texture / sampler-view / FBO-legacy HandleOf probes are - // how the server answers "which twin is this frontend object" while the registry is - // keyed by frontend identity - server-PRIVATE state whose rekey onto handles is - // P5e's per-family packages', not P5c's. A probe inside this scope stays a read-only, - // BARRIER-HELD debt - and P5e (id) makes the "barrier-held" half literal rather than - // documentary: the exemption now holds only while MGPipeApplierCurrentRecordIsBarriered() - // (§4.4), i.e. only while the client is actually parked behind the record being applied. - // Wrapping a NEW site in it is the greppable act of naming that debt, and an unwrapped - // probe from the apply thread is still Fatal{RoleViolation, "MGPipeSlots"}. - // - // WHAT THE CLASS IS FOR NOW, since P5e deletes most of its sites: vi/sb/pg/tx2/fb delete - // their ~20 draw-path constructions with the probes they wrap, and what survives is the - // set of BARRIERED-ROW sites (CONTRACT-P5E §4.4: DirectGLES.cpp's CopyTex / GetTexImage / - // mipmap-shape / set_storage_block_binding / detach-walk sites, the two verify arms, and - - // until vi/sb carry the handle - BufferImpl::HandleOfBuffer). Those keep P5C's semantics - // because their records are barriered and their client IS parked. - // - // Apply-thread-only depth, and m_counted, for MagmaP7AllocatorDebtScope's reasons - // above. This is the scope that pays: it is constructed at ~20 sites in DirectGLES.cpp, - // several of them inside StateBackendObjectRegistry::HandleOf on the per-draw path, and - // its ctor/dtor alone were 18.75% + 13.95% of the monolith GL thread's emutls samples. + // P5f (fr): retained as a greppable marker around monolith-only legacy registry + // calls. This scope no longer exempts transport apply from either registry or + // allocator guards, even behind a barrier. P3b/P4b can remove these markers with + // the surrounding monolith glue; adding one cannot make a new wire fallback legal. class MGPipeFrontendKeyedRegistryScope { public: MGPipeFrontendKeyedRegistryScope(); diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index d94a2edd..0f042147 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1980,6 +1980,88 @@ TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFat ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); } +// P5f (fr): barriers and old named scopes no longer admit frontend identity. +// Construct the object and table on the client so each death comes from the exact +// probed member, not a constructor's resource_create or a missing record. +TEST(RemoteGuards, BarrieredFrontendRegistryScopeCannotExemptAllocator) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void*) -> MobileGLResult { + MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); + const MG_Pipe::MGPipeFrontendKeyedRegistryScope scope; + MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +TEST(RemoteGuards, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes) { + struct Probe { + TestTextureTable* table; + SharedPtr* texture; + MGPipeHandle handle; + int operation; + }; + const char* members[] = {"HandleOf", "GetOrCreate(StatePtr)", "NoteStateForHandle", "StateForHandle"}; + for (int operation = 0; operation != 4; ++operation) { + SCOPED_TRACE(members[operation]); + const auto child = RunInChild([operation] { + StartControlSession(); + auto texture = MakeShared(47); + TestTextureTable table; + table.GetOrCreate(texture) = MakeShared(); + Probe probe{&table, &texture, table.HandleOf(texture.get()), operation}; + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { + auto& probe = *static_cast(self); + MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); + MG_Config::ActiveBackendType = BackendType::DirectVulkan; + const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope; + const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope; + switch (probe.operation) { + case 0: probe.table->HandleOf(probe.texture->get()); break; + case 1: probe.table->GetOrCreate(*probe.texture); break; + case 2: probe.table->NoteStateForHandle(probe.handle, *probe.texture); break; + case 3: probe.table->StateForHandle(probe.handle); break; + } + return MOBILEGL_OK; + }, &probe); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); + EXPECT_NE(child.Log.find(std::string{"BackendSlotTable::"} + members[operation]), std::string::npos); + } +} + +TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void*) -> MobileGLResult { + TestTextureTable table; + for (const bool barriered : {true, false}) { + MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(barriered); + const MGPipeHandle handle{71, barriered ? 1u : 2u}; + auto& twin = table.GetOrCreate(handle); + twin = MakeShared(); + twin->marker = 19; + auto* found = table.FindByHandle(handle); + if (!found || !*found || (*found)->marker != 19) _exit(81); + unsigned live = 0; + table.ForEachLive([&](MGPipeHandle actual, const auto& backend) { + if (actual.Slot != handle.Slot || actual.Gen != handle.Gen || backend->marker != 19) _exit(82); + ++live; + }); + if (live != 1 || !table.ReleaseByHandle(handle) || table.FindByHandle(handle)) _exit(83); + } + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ASSERT_TRUE(WIFEXITED(child.Status)) << child.Log; + EXPECT_EQ(WEXITSTATUS(child.Status), 0) << child.Log; +} + // THE ONE THAT PINS THE SINGLE-WRITER ARGUMENT. The scope is open on the GL thread for the // whole of the posted request; the apply thread's probe must still abort, because the exemption // belongs to the thread that entered the scope and never to another one. With the depth kept in From 4066f84d113b8fecc4fd08ab60ccc3bbc02bbc9a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:41:09 -0400 Subject: [PATCH 2/6] P5f fr: retire all apply allocator scope exemptions --- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp | 94 +--------------------- MobileGL/MG_Impl/Pipe/SlotAllocator.h | 76 +++-------------- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 81 ++++++------------- 3 files changed, 37 insertions(+), 214 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index 21cb4c0f..2c0b2a8d 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -11,9 +11,6 @@ #if MOBILEGL_BUILD_DISAGGREGATED #include -// P5e (id): the guard's exemption is keyed on whether the record being applied is BARRIERED -// (CONTRACT-P5E §4.4), and MGPipeApplierCurrentRecordIsBarriered() is where ApplyOne stamps -// that. Declarations only - this file names no applier state. #include #include #include @@ -32,28 +29,15 @@ namespace MobileGL::MG_Pipe { void MGPipeRefuseAllocatorFromApplyThread(const char* entry) { if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return; - // P5f: frontend-keyed registry probes are no longer an exemption. The - // separate Magma allocator debt remains backend- and barrier-keyed until - // its remaining monolith glue is removed; it cannot exempt twin-table APIs. - if (MGPipeApplierCurrentRecordIsBarriered()) { - // Ruling 12: Magma's four P7 debts, and ONLY on a DirectVulkan server. The key is - // on the backend kind rather than on the site because the scope is a class anyone - // can construct, and an Espryt probe borrowing Magma's exemption is exactly the - // hole P5e is closing. - if (MagmaP7AllocatorDebtScope::ActiveOnApplyThread() && - MG_Config::ActiveBackendType == BackendType::DirectVulkan) { - return; - } - - } + // P5f (fr): all named allocator exemptions are retired. A barrier can + // stabilize a client allocator but cannot make it server-owned memory. MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread called " "MGPipeSlots().%s. With an active transport the client slot allocator is " "client-only memory (CONTRACT-P5C §3.1, rule E; CONTRACT-P5E §4.4): a handle " "arrives already minted in a record, and a server that resolves or mints one " "off a frontend object's lifetime id is reading memory that will not exist on " - "its side of a real split. Only Magma's allocator debt scope admits it while the " - "record being applied is BARRIERED (barriered=%d) and, for Magma's P7 debt, " - "only on a DirectVulkan server", + "its side of a real split. No scope or backend exempts a barriered record " + "(barriered=%d)", entry, MGPipeApplierCurrentRecordIsBarriered() ? 1 : 0); std::abort(); } @@ -71,76 +55,6 @@ namespace MobileGL::MG_Pipe { std::abort(); } - namespace { - // NOT thread_local any more, and not atomic either (P5d round 3, package D). - // - // WHY THERE IS NO DATA RACE. Both depths have exactly ONE reader in the whole tree: - // MGPipeRefuseAllocatorFromApplyThread above, whose first two lines return unless the - // transport is active AND ServerLoop::OnApplyThread() is true. So the only thread whose - // depth could ever change an answer is the apply thread - and the four scope bodies - // below now increment and decrement ONLY when OnApplyThread() says so. That makes the - // apply thread the single writer and the single reader of both counters; no other - // thread touches them, and a monolith process never even reads them (the guard's first - // line returns on TransportMode::Monolith and there is no apply thread to make the - // second true). Single-writer-single-reader on one thread needs no lock, no atomic and - // no TLS. - // - // WHY IT WAS WORTH DOING. A shared-library thread_local costs an __emutls_get_address - // call per access. On the MONOLITH's GL thread that symbol is the TOP entry of the - // profile at 7.7%, and these two scopes' ctor/dtor are 32.7% of its samples - // (MGPipeFrontendKeyedRegistryScope 18.75 ctor + 13.95 dtor, the scope P5e renamed - // MagmaP7AllocatorDebtScope 8.4); on the split's apply thread emutls is 7.4%. The - // measurement predates the rename and the numbers are quoted as measured. The scope is - // constructed at ~20 - // sites in DirectGLES.cpp, several inside StateBackendObjectRegistry::HandleOf, which - // is per-draw. The cost on the GL thread is now one inlined predicate per end. - // - // The query's meaning ON THE APPLY THREAD is unchanged, which is the only meaning - // the guard reads. Off it the answer is now always false, and the query is named - // ActiveOnApplyThread() rather than Active() so that a reader who needs the other - // meaning cannot ask for it by accident: whoever wants "is a scope open on THIS - // thread" has to add it, and move the counting back, rather than read a false that - // looks like an answer. - Uint32 g_magmaP7AllocatorDebtScopeDepth = 0; - } - - MagmaP7AllocatorDebtScope::MagmaP7AllocatorDebtScope() - // Decided ONCE and remembered: the destructor must undo exactly what the constructor - // did, and re-asking the predicate would leak a count across a scope that straddled - // the apply thread's exit block. - : m_counted(MG_Remote::Server::ServerLoop::OnApplyThread()) { - if (m_counted) ++g_magmaP7AllocatorDebtScopeDepth; - } - - MagmaP7AllocatorDebtScope::~MagmaP7AllocatorDebtScope() { - if (m_counted) --g_magmaP7AllocatorDebtScopeDepth; - } - - // The DEPTH is not backend-keyed and deliberately so: the backend kind is read by the - // GUARD, once, at the moment it decides. Counting only on DirectVulkan would make the - // depth's meaning depend on a global that a test can move between the constructor and the - // destructor, which is the m_counted bug one level out. - Bool MagmaP7AllocatorDebtScope::ActiveOnApplyThread() { - return g_magmaP7AllocatorDebtScopeDepth != 0; - } - - namespace { - // Same counter, same argument, same single reader - see the block above. - Uint32 g_frontendKeyedRegistryScopeDepth = 0; - } - - MGPipeFrontendKeyedRegistryScope::MGPipeFrontendKeyedRegistryScope() - : m_counted(MG_Remote::Server::ServerLoop::OnApplyThread()) { - if (m_counted) ++g_frontendKeyedRegistryScopeDepth; - } - - MGPipeFrontendKeyedRegistryScope::~MGPipeFrontendKeyedRegistryScope() { - if (m_counted) --g_frontendKeyedRegistryScopeDepth; - } - - Bool MGPipeFrontendKeyedRegistryScope::ActiveOnApplyThread() { - return g_frontendKeyedRegistryScopeDepth != 0; - } #endif namespace { diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index 7316112e..614d1ea3 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -173,17 +173,8 @@ namespace MobileGL::MG_Pipe { // pull build's bytes do not move (G1). void MGPipeRefuseAllocatorFromApplyThread(const char* entry); - // ---- P5e (id, CONTRACT-P5E §4.4): THE CONDITION UNDER WHICH A NAMED SCOPE EXEMPTS ----- - // - // "This thread is applying a record the client is NOT parked behind." Both scopes below - // exempt a probe only while it is FALSE, because the client's wait is the whole of what - // makes a frontend read from the apply thread safe: behind a barriered record the client - // is blocked in WaitForApplied and its memory is stable; behind an unbarriered one it is - // running ahead and the same read is torn or stale BY CONSTRUCTION, which is why rule F - // has no "count it" arm and this answer does not consult MOBILEGL_IPC_STRICT_ERRORS. - // - // False in a monolith process and on any thread but the apply thread, so a GL-thread - // caller is never refused by it. + // Shared predicate for remaining field/verb guards. Registry and allocator + // guards below refuse both barriered and unbarriered transport apply. Bool MGPipeApplierIsUnbarrieredApply(); // P5f (fr): frontend-object twin lookup, minting and weak-state access are refused @@ -192,71 +183,24 @@ namespace MobileGL::MG_Pipe { // glue. This separate guard also prevents borrowing Magma's allocator exemption. void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry); - // The NAMED EXEMPTION to the rule above (CONTRACT-P5C §3.1, as amended by CONTRACT-P5E - // §4.4): the family of sites whose handle-carrying records the client does not EMIT yet. - // set_shader_buffers and set_stream_output_targets exist in the catalogue but are P4b/sb's - // to emit (SetHashSuppressor.h says so), so the buffer binding-point ensures and the - // GPU-written announcement they feed have no record handle to resolve from today. Inside - // this scope ONE read-only lifetime-id probe stays legal WHILE THE CURRENT RECORD IS - // BARRIERED; the scope is the debt's measurable, greppable form, and it retires with P7's - // server-side binding table. Every other apply-thread allocator access stays Fatal. - // - // THE DEPTH IS COUNTED ONLY ON THE APPLY THREAD (P5d round 3, package D). The counter's - // only reader is MGPipeRefuseAllocatorFromApplyThread, which returns before it looks unless - // ServerLoop::OnApplyThread() is true, so a depth kept on any OTHER thread could never - // change an answer - it was pure cost. The query is called ActiveOnApplyThread() and not - // Active() BECAUSE OF THAT (review round 3): a name that promised "is a scope open" would - // now be quietly answering "is a scope open ON THE APPLY THREAD", and the next reader to - // come along - a P4b/P7 probe deciding on the GL thread whether to emit a record - would - // read a truthful-looking false and take the wrong branch with nothing to warn it. The name - // carries the precondition so a second reader has to notice it. m_counted remembers what the constructor decided so - // the destructor undoes exactly what the constructor did; asking the predicate twice would - // leak a count for a scope that outlived the apply thread. On the GL thread (and in every - // monolith process) both ends are now one inlined predicate and a branch instead of an - // emutls call, which is where 32.7% of the monolith GL thread's __emutls_get_address - its - // top symbol at 7.7% - was going. - // - // P5e (id), ruling 12: RENAMED FROM MGPipeReverseAnnouncementScope AND KEYED ON THE - // BACKEND KIND. The rename is the finding: what is left inside it after P5e is not "the - // reverse announcement" - Espryt's half of that moved to the frontend-keyed scope with the - // rest of the registry debt - it is MAGMA'S FOUR APPLY-THREAD ALLOCATOR TOUCHES, which P7 - // retires (VulkanRenderer.cpp's two hidden-resource shutdowns and its named-blit endpoint - // resolve, plus MGPipeAnnounceBufferGpuWritten in ResourceTracker.h). Naming the scope - // after the debt rather than after one of its sites is what makes "has P7 landed yet" a - // grep; naming it after Magma is what stops a new Espryt site being wrapped in it. - // - // THE EXEMPTION IS REFUSED WHEN THE SERVER BACKEND IS NOT DirectVulkan, and that is the - // point of the key rather than a safety belt. Magma stays lockstep for the whole of P5e - // (§6.1: the DirectVulkan arm never publishes kCapRunAheadApply, so every record there is - // barriered and every probe inside this scope keeps P5C's semantics). Espryt is what P5e - // is retiring the lockstep for, so an Espryt probe must not be able to borrow Magma's - // exemption - even by wrapping itself in Magma's scope. + // P5f (fr): these historical scope names remain greppable markers around + // monolith glue until P3b/P4b remove those bodies. They hold no state and grant + // no exemption: allocator and frontend-registry access always refuse apply. class MagmaP7AllocatorDebtScope { public: - MagmaP7AllocatorDebtScope(); - ~MagmaP7AllocatorDebtScope(); + MagmaP7AllocatorDebtScope() = default; + ~MagmaP7AllocatorDebtScope() = default; MagmaP7AllocatorDebtScope(const MagmaP7AllocatorDebtScope&) = delete; MagmaP7AllocatorDebtScope& operator=(const MagmaP7AllocatorDebtScope&) = delete; - static Bool ActiveOnApplyThread(); - - private: - Bool m_counted; }; - // P5f (fr): retained as a greppable marker around monolith-only legacy registry - // calls. This scope no longer exempts transport apply from either registry or - // allocator guards, even behind a barrier. P3b/P4b can remove these markers with - // the surrounding monolith glue; adding one cannot make a new wire fallback legal. class MGPipeFrontendKeyedRegistryScope { public: - MGPipeFrontendKeyedRegistryScope(); - ~MGPipeFrontendKeyedRegistryScope(); + MGPipeFrontendKeyedRegistryScope() = default; + ~MGPipeFrontendKeyedRegistryScope() = default; MGPipeFrontendKeyedRegistryScope(const MGPipeFrontendKeyedRegistryScope&) = delete; MGPipeFrontendKeyedRegistryScope& operator=(const MGPipeFrontendKeyedRegistryScope&) = delete; - static Bool ActiveOnApplyThread(); - - private: - Bool m_counted; }; + #endif } // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 0f042147..3bd50a3e 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1898,29 +1898,9 @@ MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalBy MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent()) #undef MGL_BUFFER_GUARD_TEST -// P5d round 3 (package D): the two NAMED EXEMPTION scopes of CONTRACT-P5C §3.1 / §5.4 stopped -// keeping their depth in a thread_local and now count ONLY while ServerLoop::OnApplyThread() is -// true (SlotAllocator.cpp). The claim that makes that legal is "the apply-thread guard is the -// counter's only reader, so a depth kept on any other thread cannot change an answer" - and -// these three cases are that claim in executable form. They are the red-once for the change: -// -// (a) drop the `if (m_counted)` from the constructor (or make it unconditional again on a -// thread that is not the apply thread) and (c) goes red - a GL-thread scope would start -// exempting the apply thread, which is exactly the leak the thread_local used to prevent; -// (b) drop the increment altogether and (a) goes red - the exemption stops exempting and -// the debt's own sites (DirectGLES' HandleOf probes) abort the server; -// (c) drop the guard's `MagmaP7AllocatorDebtScope::ActiveOnApplyThread()` / -// `FrontendKeyedRegistryScope::ActiveOnApplyThread()` rows and (b) - the control - stops -// being the only aborting arm. -// -// P5e (id), CONTRACT-P5E §4.4 REPLACED (b). The case that used to sit there - -// `AnAllocatorProbeInsideAnExemptionScopeOnTheApplyThreadIsAllowed` - asserted that a scope -// exempts UNCONDITIONALLY, which is exactly the rule §4.4 retires: an exemption is a debt the -// CLIENT'S WAIT pays for, so it holds only while the record being applied is barriered. Its -// replacement below drives the same scope with the barriered flag forced false and expects the -// abort. The green half of the old case did not need a test of its own: every Espryt -// `integration-split` entry runs the frontend-keyed scope's surviving sites on the apply thread -// behind barriered records, so a guard that stopped exempting would take the whole lane down. +// Historical P5d/P5e probes remain named for compatibility. P5f retires both +// scope exemptions, including barriered records; all allocator access on apply is +// client-memory access regardless of whether that memory happens to be stable. TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); @@ -1935,24 +1915,8 @@ TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByN ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); } -// P5e (id) RED-ONCE, CONTRACT-P5E §4.4. The exemption scope is OPEN and the probe is the one -// the debt is named for - `HandleOf`, the frontend-keyed registry's own entry - and it still -// aborts, because the record being applied is unbarriered: the client is running ahead and the -// allocator's free list and lifetimeId -> slot map are moving under the read. -// -// THE HOOK IS THE APPLIER FIELD ITSELF and that is deliberate: `CurrentRecordBarriered` is what -// PipeApplier::ApplyOne stamps from MGPipeBarriered() on every record, so forcing it here drives -// the production path rather than a test-only branch beside it. Until ra lands the wait rule -// MGPipeBarriered answers true for every record, so this is the ONLY way to reach the arm - and -// the arm has to exist before ra, because ra is what makes it reachable in production. -// -// The red: put §4.4's `if (MGPipeApplierCurrentRecordIsBarriered())` back to an unconditional -// exemption in MGPipeRefuseAllocatorFromApplyThread and this case stops aborting - red by -// expectation, which is the shape a guard case fails in. -// EVERYTHING BUT THE PROBE RUNS ON THE CLIENT THREAD, which is the buffer/texture guard cases' -// shape one block down and is load-bearing here rather than tidy: a TextureObject2D's -// constructor MINTS a resource handle, and building it on the apply thread would abort at the -// mint with the same diagnostic - a case that passes for a reason other than the one it names. +// Construct on the client and run only HandleOf on apply. The false barrier stamp +// keeps the original P5e scenario; the tests below add the old barriered loophole. TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFatalEvenAtTheOldDebtSites) { struct Probe { TestTextureTable* table; @@ -1983,18 +1947,23 @@ TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFat // P5f (fr): barriers and old named scopes no longer admit frontend identity. // Construct the object and table on the client so each death comes from the exact // probed member, not a constructor's resource_create or a missing record. -TEST(RemoteGuards, BarrieredFrontendRegistryScopeCannotExemptAllocator) { - const auto child = RunInChild([] { - StartControlSession(); - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void*) -> MobileGLResult { - MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); - const MG_Pipe::MGPipeFrontendKeyedRegistryScope scope; - MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); - return MOBILEGL_OK; - }, nullptr); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) { + for (const auto backend : {BackendType::DirectGLES, BackendType::DirectVulkan}) { + const auto child = RunInChild([backend] { + StartControlSession(); + auto selectedBackend = backend; + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { + MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); + MG_Config::ActiveBackendType = *static_cast(self); + const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope; + const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope; + MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 99); + return MOBILEGL_OK; + }, &selectedBackend); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); + } } TEST(RemoteGuards, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes) { @@ -2062,11 +2031,7 @@ TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) { EXPECT_EQ(WEXITSTATUS(child.Status), 0) << child.Log; } -// THE ONE THAT PINS THE SINGLE-WRITER ARGUMENT. The scope is open on the GL thread for the -// whole of the posted request; the apply thread's probe must still abort, because the exemption -// belongs to the thread that entered the scope and never to another one. With the depth kept in -// a plain counter that every thread incremented, this case would go green - and a real -// apply-thread violation would be silently exempted for as long as any GL thread held a scope. +// A marker held by the GL thread never affects the apply guard either. TEST(RemoteGuards, AnExemptionScopeHeldOnTheGLThreadDoesNotExemptTheApplyThread) { const auto child = RunInChild([] { StartControlSession(); From 7ce19da02b0e344cf599699c8ba6aec2d052703d Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:43:32 -0400 Subject: [PATCH 3/6] P5f fr: guard client allocator singleton including bookkeeping reads --- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp | 6 ++++++ MobileGL/MG_Impl/Pipe/SlotAllocator.h | 4 ++-- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index 2c0b2a8d..906ae88c 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -322,6 +322,12 @@ namespace MobileGL::MG_Pipe { } MGPipeSlotAllocator& MGPipeSlots() { +#if MOBILEGL_BUILD_DISAGGREGATED + // Guard the client singleton itself too: Allocate/IsLive/HighWater and the + // bookkeeping accessors must not bypass the original three method guards. + // Backend-private allocator instances do not use this accessor. + MGPipeRefuseAllocatorFromApplyThread("client-singleton"); +#endif // NEVER DESTROYED, deliberately (one allocation for the life of the process). A // frontend object's destructor reaches this allocator - ~BufferObject through // MGPipeEmitResourceDestroyAndFree, ~VertexArrayObject through the death notice - and diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index 614d1ea3..b0e59384 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -166,7 +166,7 @@ namespace MobileGL::MG_Pipe { #if MOBILEGL_BUILD_DISAGGREGATED // P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a - // CLIENT-only surface. Acquire, FindByLifetimeId and Free called from the apply thread - + // CLIENT-only surface. Its singleton accessor and original three method guards reject apply - // i.e. a server that resolves or mints handles off a frontend object's lifetime id (T2), // which is memory that will not exist on its side of a real split - are // Fatal{RoleViolation, "MGPipeSlots"}. Compiled out entirely outside split builds, so the @@ -180,7 +180,7 @@ namespace MobileGL::MG_Pipe { // P5f (fr): frontend-object twin lookup, minting and weak-state access are refused // on EVERY transport apply, including barriered records and both named scopes. // The handle overloads remain server-local; the frontend overloads remain monolith - // glue. This separate guard also prevents borrowing Magma's allocator exemption. + // glue. The registry guard is independent of the allocator guard. void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry); // P5f (fr): these historical scope names remain greppable markers around diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 3bd50a3e..0e5dbf34 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1957,7 +1957,8 @@ TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) { MG_Config::ActiveBackendType = *static_cast(self); const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope; const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope; - MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 99); + // HighWater had no per-method guard in the old implementation. + (void)MGPipeSlots().HighWater(MGPipeKind::Texture); return MOBILEGL_OK; }, &selectedBackend); ClientSessionInstance().Stop(); From df5ffe037ecd2abeaf9677def6e1d2db4d0d7898 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:46:40 -0400 Subject: [PATCH 4/6] P5f fr: guard registry frontend surfaces before arm selection --- MobileGL/MG_Backend/DirectGLES/Managers.h | 24 +++++++++++++++++-- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 28 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 60ce40db..0d98cc72 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -329,6 +329,11 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif BackendPtr& GetOrCreate(const StatePtr& stateObj) { +#if MOBILEGL_BUILD_DISAGGREGATED + // Check before arm selection: disabling slot tables must not expose the + // legacy raw-pointer registry on a transport apply thread. + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.GetOrCreate(StatePtr)"); +#endif MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null"); #if MOBILEGL_PIPE_PUSH @@ -389,6 +394,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // need the twin across another registry call must copy the BackendPtr out (or keep only // the pointee, which is heap-allocated and never moves). BackendPtr* Find(StateObject* stateObj) { +#if MOBILEGL_BUILD_DISAGGREGATED + // Check before arm selection: disabling slot tables must not expose the + // legacy raw-pointer registry on a transport apply thread. + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.Find(StateObject*)"); +#endif #if MOBILEGL_PIPE_PUSH if (EsprytSlotTablesEnabled()) { return m_slotTable.Find(stateObj); @@ -409,8 +419,18 @@ namespace MobileGL::MG_Backend::DirectGLES { return const_cast(this)->Find(stateObj); } - iterator begin() { return m_entries.begin(); } - const_iterator begin() const { return m_entries.begin(); } + iterator begin() { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin"); +#endif + return m_entries.begin(); + } + const_iterator begin() const { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin"); +#endif + return m_entries.begin(); + } iterator end() { return m_entries.end(); } const_iterator end() const { return m_entries.end(); } diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 0e5dbf34..ad0028c0 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -2004,6 +2004,34 @@ TEST(RemoteGuards, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes) { } } +TEST(RemoteGuards, LegacyRegistryWrapperCannotExposeFrontendKeysOnApply) { + using Registry = MG_Backend::DirectGLES::StateBackendObjectRegistry< + MG_State::GLState::TextureObject2D, FakeTwin, MGPipeKind::Texture>; + struct Probe { Registry* registry; SharedPtr* texture; int operation; }; + for (int operation = 0; operation != 3; ++operation) { + const auto child = RunInChild([operation] { + StartControlSession(); + auto texture = MakeShared(48); + Registry registry; + Probe probe{®istry, &texture, operation}; + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { + auto& probe = *static_cast(self); + MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); + const MG_Pipe::MGPipeFrontendKeyedRegistryScope scope; + switch (probe.operation) { + case 0: probe.registry->Find(probe.texture->get()); break; + case 1: probe.registry->GetOrCreate(*probe.texture); break; + case 2: (void)probe.registry->begin(); break; + } + return MOBILEGL_OK; + }, &probe); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); + EXPECT_NE(child.Log.find("BackendSlotTable::Registry."), std::string::npos); + } +} + TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) { const auto child = RunInChild([] { StartControlSession(); From 8377e6cf57c4d393a80bb1174b1e8fc3e78fdb26 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:48:47 -0400 Subject: [PATCH 5/6] P5f fr: include concrete registry wrapper in guard test --- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index ad0028c0..7773ad01 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include From bf5ad4704b66b5ae8165d4e09636c89d580a6611 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:49:51 -0400 Subject: [PATCH 6/6] docs: record P5f fr registry audit and guard red-once --- docs/Disaggregated/notes/p5f/fr-report.md | 91 +++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/Disaggregated/notes/p5f/fr-report.md diff --git a/docs/Disaggregated/notes/p5f/fr-report.md b/docs/Disaggregated/notes/p5f/fr-report.md new file mode 100644 index 00000000..bf3c4f28 --- /dev/null +++ b/docs/Disaggregated/notes/p5f/fr-report.md @@ -0,0 +1,91 @@ +# P5f / fr — registry 身份边收口 + +> 分支 `codex/p5f-fr`,基线 `4667e13b`。工作树 `MobileGL-p5f-fr`; +> 独立 Linux 验证树 `/home/swung/w7/p5f-fr`。这是波次 3 的 fr 包, +> fe/fm 已迁移的 CopyImage 和 storage-block 接线不重复计数。 + +## 1. 结果与边界 + +所有 transport apply 对 client allocator 的访问都具名拒绝,设障、后端种类和历史 +scope 均不再豁免。`MGPipeSlots()` 本身新增守卫,覆盖旧三方法守卫之外的 +`Allocate`、`IsLive`、`HighWater` 等入口;backend 私有 allocator 不经这个 singleton。 + +`BackendSlotTable` 的 `GetOrCreate(StatePtr)`、`HandleOf`、`NoteStateForHandle`、 +`StateForHandle` 在所有 apply 上拒绝 frontend 身份;`Find(StateObject*)` 经 `HandleOf`。 +上层 `StateBackendObjectRegistry` 的 frontend lookup/mint/iterator 在臂选择之前检查, +不能靠关闭 slot-table 位退回裸地址 map。按 handle 的查找、创建、遍历、释放仍正常工作。 +拒绝为 `Fatal{RoleViolation, "MGPipeSlots"}`,日志还包含具体成员名。 + +两种历史 scope 现在是无状态标记,不持有深度、不产生豁免。P3b/P4b 可随 monolith +glue 一起删除这些标记;P5f 不删掉对应的 monolith 功能来消除 grep 结果。 + +自由函数 `DirectGLES::GetBackendProgramId(GLuint)` 全树无声明、调用者或 dispatch 槽。 +本包以 `!MOBILEGL_BUILD_DISAGGREGATED` 隔离,D-P 中不再存在;保留非 D-P 函数体, +避免仅为删除死代码改变 pull 的 `.text` 与符号。它不等于仍被大量使用的 +`BackendProgramObjectImpl::GetBackendProgramId() const` 成员。 + +## 2. 原 13 个 DirectGLES scope 的重新核对 + +这里按函数名定位,不沿用 f0 的旧行号。家族位关闭导致的 legacy 回退不视为安全的 wire +功能;新的无条件身份守卫和字段收口会拒绝它,不能用“client 已停在 barrier”解释放行。 + +| 原 scope 所在函数 | 当前 transport 路线 / 阻断点 | +|---|---| +| `SyncTextureObjectToBackend` | `OnResourceCopyRegion` 已由 fe/fm 将 `Src/Dst` 放入 `CopyImageEndpoint.TextureHandle`,GLES endpoint 走 `SyncTextureToBackendByHandle`;旧对象 overload 留 monolith。 | +| `SyncCurrentFBO` | 先走 `SyncCurrentFBOByRecord`;开启 family 后的 decline 经 `RefuseFramebufferBindingSlotRead`,不会落入对象 lookup。 | +| `ResolveGlobalConstantsRecord(program)` | transport 的调用点用 `ResolveGlobalConstantsRecordForHandle`;旧对象 overload 留 monolith。 | +| `SyncCurrentProgram(program)` | draw/dispatch 经 `ProgramHandleArm()` 选 `SyncCurrentProgramByHandle`。 | +| `BindCurrentFBO` | record 的 `Fbo` 经 `FindByHandle`;mandatory record 臂 decline 即拒绝。 | +| `ResolveAndBindUnitTextures` | `UnitTexturesByHandle()` 的 record 分支先行返回;legacy unit pass 留 monolith。 | +| `ResolveUnitSamplerBackend` | transport 在 scope 前以 `BindSamplerStates.Count` 拒绝 identity sampler 家族。 | +| `BindCurrentProgramWithResources` | `handleArm` 经句柄 stash / `ResolveProgramTwin`,对象 registry 在另一臂。 | +| `GetCurrentBackendProgram` | `ProgramHandleArm` 在旧 scope 前返回。 | +| 自由函数 `GetBackendProgramId(GLuint)` | 无调用者;本包从 D-P 编译中排除。 | +| `ScopedDetachedTextureFramebufferAttachments(texture)` | transport mipmap 用 handle 构造与 record reverse-index;对象构造及 `StateForHandle` 留 monolith。 | +| `ShaderStorageBlockBinding` | fe 已用 `VerbStorageBlockProgram` → `FindByHandle` → native program id,并先行返回;旧 GL-name 查对象路径留 monolith。 | +| `GetTexImage` | wire 槽仍在 `MGR_UNMIGRATED_TAIL_SLOTS`,`Fatal{UnmigratedVerb, "GetTexImage"}`;不能因为本包关闭 scope 就宣称 P9 功能已实现。 | + +## 3. Managers 的 11 个 scope 与普查遗漏 + +`HandleOfBuffer` 的 frontend overload 在 legacy buffer path;transport buffer consumers +用 handle 入口。`HandleOfSamplerViewForTexture` 全树没有调用者,仍是 monolith glue。 +`RequireImageBindableStorage` 的 object overload 在 legacy texture path;transport 使用 +`RequireImageBindableStorageByHandle`。`ResolveOwnRecord` 在 transport twin 已带 +`m_pushedSyncHandle`,不进入对象 lookup。 + +`SyncAttachmentObject` 的 texture/renderbuffer 两站和 FBO verify 的两站位于 object +`BackendFramebufferObject::SyncToBackend`;transport 使用 `SyncToBackendByHandle`。 +program serial 盖章和 renderbuffer record 解析位于各自 object `SyncToBackend`; +transport 的 by-handle overload 不问 frontend identity。 + +**第 11 项 sampler registration probe 不能照 f0 写成不可达。** transport 的 +`BindCurrentProgramWithResources` 在 depth-texture、普通 `sampler2D`、需要强制 nearest +的组合下实际调用 `GetRawDepthFetchSampler()`;该 helper 原来创建 `SamplerObject`, +setter 还会读 client context,随后 `BackendSamplerObject::SyncToBackend` 的无 CSO 分支 +进入这个 registration probe。此遗漏已交 fs 包改为 server native sampler,并补世代测试。 +fr 关闭所有 registry 豁免后,这个遗漏不会再静默放行。 + +同时发现 `VulkanRenderer::Initialize()` 仍无条件创建 blit/depth-mipmap 隐藏 +frontend program;相应两个 shutdown scope 不能当作不可达依据。已交 fv 包将 transport +初始化改道。原有 host Magma 用例通过不能代替完整 surface 初始化证据;本报告不声称 +另有已经核实的 headless 初始化豁免。fr 仍无条件关闭 Magma allocator 豁免。 + +## 4. 验证 + +- 行为代码 `8377e6cf` 的独立 split `RemoteClientTest` 构建成功,新增四个测试; + `RemoteGuards` 44/44 通过,完整 executable 147/147 通过(`fr-remoteclient-full.log`)。 +- `BarrieredLegacyScopesCannotExemptAllocator` 在 GLES / Vulkan 两 backend 都打开两种旧 + scope,然后实际调用 client singleton 的 `HighWater`,必须在访问前具名 abort。 +- `BarrieredFrontendRegistryMembersRefuseBothLegacyScopes` 对四个身份入口逐项检查;对象 + 和表在 client 创建,只有被测操作运行在 apply,日志核对具体成员名,避免在无关构造处死掉。 +- wrapper 测试覆盖 frontend `Find`、mint 和 iterator,核对 wrapper 自己的 marker。 +- handle 正对照在 barriered / unbarriered 两种记录下验证创建、查找、遍历、释放。 +- **真实 red-once**:临时在两个 production guard 恢复“当前记录 barriered 就放行”的旧 + 条件,构建并执行两条 barriered 测试,**2/2 真红**,失败为预期 abort 没有发生;恢复健康 + 源码并重建后守卫测试全绿。日志为 `fr-red-once.log`、`fr-restored-green.log`,临时修改未提交。 +- 非 D-P 行为改动全部由预处理隔离。pull / push、G1/G2/G14、完整 unit/GPU、strict 与 + dualblock 在 fs/fr/fv 合并树由集成者执行;本包不拿单测结果冒充整阶段门。 + +Linux 新树的子模块递归初始化遇到本地缺少 apitrace nested commit,随后复用了现有 +`p5f-int` 的依赖与 fixture 文件;未运行 `git lfs pull/checkout`,未修改参考树。 +