diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index 4c1a07a0..d744b877 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -688,7 +688,22 @@ namespace MobileGL::MG_Pipe { // The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline // variable: no .cpp is needed for the definition. - inline PipeInputs gPipeInputs{}; + // + // LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for + // pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply + // leaks the global singletons to the OS instead of running destructors during static + // teardown". This block breaks that rule if it is a value, because its O-class members + // are SharedPtrs to FRONTEND objects: a VertexArrayObject that the application deleted + // while it was bound has its last reference here, and destroying this block from + // __run_exit_handlers therefore runs ~VertexArrayObject -> ~BufferObject at exit. Those + // destructors are not exit-safe and cannot be made so - they reach the client's slot + // allocator, the resource tracker, the vertex-input emitter, the applier AND, through + // MGPipeApplyResourceDestroy, the backend's own twin tables, deferred-release queue, + // buffer pool and driver entry points, every one of which is either already destroyed or + // about to be. So the reference is never dropped: nothing here can start such a chain. + // A live context releases these SharedPtrs the ordinary way, at the fill point. + // (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.) + inline PipeInputs& gPipeInputs = *new PipeInputs(); // Every field has storage or is forwarded, and nothing else. #define MGP_INPUT_COUNT_ONE(Field, Member) +1 diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index cf7602b1..54a1ecc8 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -363,8 +363,14 @@ namespace MobileGL::MG_Pipe { // COMPARE-AT-READ in every accessor (the stored value against a fresh read of the // live context at the moment the backend reads it - the arm that is real in P1: it // catches a value that changed between the verb boundary and the read). - PipeInputs g_snapshot{}; // the second arm - PipeInputs g_readScratch{}; // where the compare-at-read re-read lands + // LEAK-AT-EXIT STORAGE, for gPipeInputs' reason (MG_Backend/MGPipe/PipeInputs.h): a + // PipeInputs holds SharedPtrs to frontend objects in its O class, and these two are + // filled from the live context, so either can hold the LAST reference to a + // VertexArrayObject or a ProgramObject. Destroying them from __run_exit_handlers + // would run ~VertexArrayObject / ~BufferObject at exit, into a pipe and a backend + // that are already being torn down. References so the ~50 uses below need no edit. + PipeInputs& g_snapshot = *new PipeInputs(); // the second arm + PipeInputs& g_readScratch = *new PipeInputs(); // where the compare-at-read re-read lands // The read hook arms at the first fill (ArmVerify below), so it cannot see a read // made before that. That window is covered by the poison instead: MGP_INPUT_CHECK @@ -1163,7 +1169,12 @@ namespace MobileGL::MG_Pipe { // Widening the probe to all 29 would re-implement the derivation to check it. Bool ApplierDerivesRenderStateFields() { static const Bool answer = [] { - static PipeInputs probe; + // Leak-at-exit, for gPipeInputs' reason: a PipeInputs is never destroyed by + // an exit handler. This one only ever carries render state, but the rule is + // stated over the TYPE rather than over each instance's current contents - + // an instance that grows an O-class write later must not become the next + // exit-time chain starter. + static PipeInputs& probe = *new PipeInputs(); constexpr Uint32 kSentinel = 0x5a5a5a5au; MGPipeFillAccess::RenderStateOf(probe).ClearStencil = kSentinel; MGPipeFillAccess::ClearStencilOf(probe) = 0u;