P5f fr: refuse frontend identity on every registry apply

This commit is contained in:
2026-09-19 23:35:35 -04:00
parent 4667e13be6
commit eeaf14c1f7
5 changed files with 125 additions and 108 deletions
@@ -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
+17 -50
View File
@@ -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;
+13 -27
View File
@@ -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();
}
+9 -31
View File
@@ -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();
@@ -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<MG_State::GLState::TextureObject2D>* 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<MG_State::GLState::TextureObject2D>(47);
TestTextureTable table;
table.GetOrCreate(texture) = MakeShared<FakeTwin>();
Probe probe{&table, &texture, table.HandleOf(texture.get()), operation};
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult {
auto& probe = *static_cast<Probe*>(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<FakeTwin>();
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