From eeaf14c1f7e78ddaf6456653ebf054eb4d6fc6f3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:35:35 -0400 Subject: [PATCH] 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