mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
P5f fr: refuse frontend identity on every registry apply
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user