diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 771c351b..dfc754d5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -31,6 +31,8 @@ #include #if MOBILEGL_BUILD_DISAGGREGATED #include +// P7 wave 0: the seam WireDescriptorFatal dies through. See MG_Pipe/PipeSessionFail.h. +#include #endif #include #include @@ -65,9 +67,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { return hash; } + // P7 wave 0, the descriptor half of WireFramebuffer.inc's MagmaWireFatal - same message, + // same seam, same reason. Four of the fifteen `@P7` refusals die here. [[noreturn]] static void WireDescriptorFatal(const char* detail) { - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); - std::abort(); + MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb, + "MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); } static Bool ResolveWireRange(Uint64 offset, Uint64 declaredSize, VkDeviceSize bufferSize, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 43dd1a06..30e1eece 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -41,6 +41,9 @@ #include "MG_Util/Texture/PixelStoreProcessor.h" #include #if MOBILEGL_BUILD_DISAGGREGATED +// P7 wave 0: the seam WireFramebuffer.inc's MagmaWireFatal and WireDraw.inc's +// WireBufferLegacyFatal die through. Declared by MG_Pipe on purpose - see PipeSessionFail.h. +#include // P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on. #include #include diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index 457fd064..9a186394 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -11,9 +11,13 @@ return MagmaProgramSource(handle, records[index]); } + // Through the same seam as MagmaWireFatal, and for the same reason (P7 wave 0). The family + // stays RoleViolation: reaching the legacy buffer arm inside a Magma server is a role + // invariant the two halves must hold, not a verb the wire failed to migrate. [[noreturn]] static void WireBufferLegacyFatal() { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); - std::abort(); + MG_Pipe::MGPipeSessionFail( + MG_Pipe::MGPipeFatalFamily::RoleViolation, + "MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); } static const MG_Pipe::MGPStreamOutputBegin& WireXfbSpan() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index b00e9506..fe444d96 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -1,9 +1,16 @@ // P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here // belong to the applier; no frontend object, lifetime-id lookup or allocator is used. #if MOBILEGL_BUILD_DISAGGREGATED + // P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of + // the fifteen `@P7` refusals die (the other four are WireDescriptorFatal's), and until now + // every one of them raised its own abort: no SessionFault frame for the peer, no + // SessionFaultCount() for exit gate S8, and nothing tying the hand-typed word to + // FatalFamilies.def. MGPipeSessionFail hands the line to the hook MG_Remote installs at + // server-role init, whose arm for UnmigratedVerb is SessionFail(MGFatalFamily:: + // UnmigratedVerb, ...) - and with no hook installed it logs and aborts exactly as this did. [[noreturn]] static void MagmaWireFatal(const char* detail) { - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); - std::abort(); + MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb, + "MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); } VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage( diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 70413581..d0ebe69f 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // InstallPipeSessionFailHook, installed at step 0 below #include #include #endif @@ -159,6 +160,14 @@ namespace MobileGL::MG_Backend { Bool InitServerRoleCommon() { using namespace MobileGL::MG_Remote; + // 0. the Magma wire funnels' route to Session::Fail (P7 wave 0). FIRST, before + // CreateBackend can reach a wire arm: a hook installed after the first death it + // was meant to catch is a hook that does nothing on the only run that mattered. + // Here rather than in ServerMain because BOTH server roles pass through this + // function - the inproc role and the spawn/TCP session child - and a per-transport + // install is how one of the two ends up without it. + InstallPipeSessionFailHook(); + // 1. the SERVER role's private backend object, on the app thread, with no GL and no // EGL. The context is created and made current later, on mgl-srv-apply, when the // client's first eglMakeCurrent crosses as a blocking control request. diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 88d8bde7..51c851bf 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -33,6 +33,9 @@ #endif #if MOBILEGL_BUILD_DISAGGREGATED +// P7 wave 0: the seam the Magma wire funnels in MG_Backend die through. Declared in MG_Pipe and +// DEFINED below, so MG_Backend names no MG_Remote symbol of its own to reach Session::Fail. +#include // R-6's tier gate. One spelling, asked at the one place the decline is decided. Outside the // MOBILEGL_PIPE_VERIFY block above on purpose: the tier is a property of the BUILD, not of the // comparator, and a split build without the comparator still declines every acquisition. @@ -51,6 +54,8 @@ #include #include +#include +#include #include #include #include @@ -108,6 +113,49 @@ X(ProgramPointSize) namespace MobileGL::MG_Pipe { + +#if MOBILEGL_BUILD_DISAGGREGATED + // ---------------------------------------------------------------------------- + // THE BACKEND-SIDE SESSION-FAIL SEAM (P7 wave 0). PipeSessionFail.h holds the argument; + // this is the whole implementation. + // + // It lives in this file for the reason MGP_TRIP_WIRE_REPORT does, forty lines up: this is + // where MG_Pipe already states "the verdict of every trip wire, in one place", and a seam + // whose default is an abort belongs next to the other one rather than in a file of its own + // that a reader has to be told about. Split-only, so a pull build gains nothing (G1) and a + // push-monolith build - where the three Magma wire funnels are not compiled either - gains + // nothing to call. + namespace { + MGPipeSessionFailHook g_sessionFailHook = nullptr; + } // namespace + + void MGPipeInstallSessionFailHook(MGPipeSessionFailHook hook) { g_sessionFailHook = hook; } + + MGPipeSessionFailHook MGPipeSessionFailHookInstalled() { return g_sessionFailHook; } + + void MGPipeSessionFail(MGPipeFatalFamily family, const char* fmt, ...) { + // The same 512-byte buffer and the same fallback SessionFail and WireLogFatal use, so a + // death that travels this seam reads identically to one that did not. + char line[512]; + va_list args; + va_start(args, fmt); + const int written = std::vsnprintf(line, sizeof(line), fmt, args); + va_end(args); + if (written < 0) { + std::snprintf(line, sizeof(line), + "MGPipe: unformattable Fatal diagnostic (format=%s)", fmt); + } + if (g_sessionFailHook != nullptr) g_sessionFailHook(family, line); + // NO HOOK: exactly what the three funnels did before this seam existed - the line, then + // the abort. Reached in a server image whose role init has not run (a unit case that + // drives the applier headless) and in the client process, where there is no session to + // fail. The abort carries no `Fatal{` of its own because `line` already does; + // fatal_census.py's FUNNEL_SITES names this function for that reason. + MGLOG_F("%s", line); + std::abort(); + } +#endif + namespace { // ---------------------------------------------------------------------------- // WHICH CHUNKS EACH DERIVATION READS. diff --git a/MobileGL/MG_Pipe/PipeSessionFail.h b/MobileGL/MG_Pipe/PipeSessionFail.h new file mode 100644 index 00000000..446092fd --- /dev/null +++ b/MobileGL/MG_Pipe/PipeSessionFail.h @@ -0,0 +1,81 @@ +// MobileGL - MobileGL/MG_Pipe/PipeSessionFail.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P7 wave 0 (plan section 1.3 row `@P7`, item (0); Ph slice (2)). HOW A BACKEND DEATH REACHES +// Session::Fail WITHOUT THE BACKEND KNOWING MG_Remote EXISTS. +// +// The three Magma wire funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), +// WireDescriptorFatal (Renderer/UniformManager.cpp) and WireBufferLegacyFatal +// (Renderer/WireDraw.inc) - each logged their line and raised their OWN std::abort(). Fifteen +// `@P7` refusals and two more sites die through them, and every one of those deaths: +// +// * published NO SessionFault frame, so the peer read a bare EOF and could not name what +// ended the session (CONTRACT-P6 5.2's whole point); +// * bumped NO SessionFaultCount(), so exit gate S8's "zero faults over a good run" could +// neither confirm nor falsify them; +// * carried a HAND-TYPED family word with no path to FatalFamilies.def's projection, so +// nothing checked that `UnmigratedVerb` in the string still meant UnmigratedVerb. +// +// Calling SessionFail() from the .inc files directly would be the obvious fix and is the wrong +// one: MG_Backend's renderer would then name an MG_Remote symbol on its own account rather than +// through the two staging headers it already borrows under `#if MOBILEGL_BUILD_DISAGGREGATED`, +// and the layering that P13's module boundaries have to establish would be one more edge worse. +// +// So the shape is MGP_TRIP_WIRE_REPORT's (PipeApply.cpp:66): MG_Pipe owns the entry point, the +// DEFAULT behaviour is exactly what the site did before (same log line, same abort), and the +// richer behaviour is a function pointer that a layer above installs. MG_Remote installs +// InstallPipeSessionFailHook() at server-role init (MG_Backend/Init.cpp's InitServerRoleCommon, +// which both the inproc server role and the spawn child run), and from then on these deaths go +// through SessionFail like every other one. +// +// THE MESSAGE STRING IS PASSED VERBATIM, family word and all - the same rule FatalFunnel.h +// states. The lines these three funnels write are already counted by name in the retrace refusal +// census and in every recorded `@P7` measurement, so they are byte-identical before and after. + +#pragma once + +namespace MobileGL::MG_Pipe { + + // THE FAMILIES THAT CROSS THIS BOUNDARY, AND ONLY THOSE. Not `MGFatalFamily` cast to an + // integer: the .def's enumerators are positional, and a row inserted in the middle of it + // would silently re-point every hook call at a different family. Two words is what the three + // funnels use today, and a third one costs a row here and an arm in the adapter - which is + // the point, because that arm is where the choice gets argued. + enum class MGPipeFatalFamily : unsigned { + // The Magma wire arms' refusal to honour a verb/shape the split path does not implement. + // Deliberately the SAME word the sites already print: `Magma:` in the detail is what + // separates them from the GLES `UnmigratedVerb` deaths, and renaming the family would + // invalidate every census number recorded against it since P5b. + UnmigratedVerb, + // A call that reached an arm this build did not compile, or ran on the wrong role. + RoleViolation, + }; + + // What the layer above installs. `line` is the FULLY FORMATTED message, so the hook forwards + // it rather than re-formatting it - a second vsnprintf is a second chance to disagree about + // what the death said. A hook is expected not to return; MGPipeSessionFail aborts anyway if + // one does, because a death that keeps running is the one outcome nothing downstream handles. + using MGPipeSessionFailHook = void (*)(MGPipeFatalFamily family, const char* line); + + // Idempotent and last-writer-wins. Installed once per process at server-role init; a unit + // case installs its own and restores nullptr afterwards. + void MGPipeInstallSessionFailHook(MGPipeSessionFailHook hook); + + // The installed hook, or nullptr. A test reads it to prove installation happened without + // having to die to find out. + MGPipeSessionFailHook MGPipeSessionFailHookInstalled(); + + // Formats `fmt`, hands the line to the hook if one is installed, and otherwise does exactly + // what the three funnels used to do on their own: MGLOG_F the line and std::abort(). + [[noreturn]] +#if defined(__GNUC__) || defined(__clang__) + __attribute__((format(printf, 2, 3))) +#endif + void MGPipeSessionFail(MGPipeFatalFamily family, const char* fmt, ...); + +} // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp index 0c7e6825..e6b1eefb 100644 --- a/MobileGL/MG_Remote/FatalFunnel.cpp +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -9,6 +9,7 @@ #include #include "../Includes.h" // MGLOG_F and the umbrella; a .cpp may pull it, a Transport/ header may not +#include // the seam MG_Backend's renderer dies through #include "Client/ClientSession.h" #include "Protocol/generated/protocol_generated.h" #include "Server/ServerSession.h" @@ -33,6 +34,34 @@ namespace MobileGL::MG_Remote { return g_sessionFaultCount.load(std::memory_order_relaxed); } + namespace { + // THE PROJECTION FROM MG_Pipe's TWO-WORD BOUNDARY ENUM ONTO THE .def's VOCABULARY, as a + // switch with a named default rather than a cast. MGPipeFatalFamily is deliberately not + // MGFatalFamily's ordinals (PipeSessionFail.h says why), so this arm is the only place + // the two vocabularies meet - and adding a third word there means arguing for a row here. + // + // The line is forwarded through "%s" rather than re-used as a format: it has already been + // vsnprintf'd once, and a detail string that happened to contain a `%` would otherwise be + // read as a conversion the second time round. + [[noreturn]] void PipeSessionFailAdapter(MG_Pipe::MGPipeFatalFamily family, + const char* line) { + MGFatalFamily mapped = MGFatalFamily::ProtocolCorruption; + switch (family) { + case MG_Pipe::MGPipeFatalFamily::UnmigratedVerb: + mapped = MGFatalFamily::UnmigratedVerb; + break; + case MG_Pipe::MGPipeFatalFamily::RoleViolation: + mapped = MGFatalFamily::RoleViolation; + break; + } + SessionFail(mapped, "%s", line); + } + } // namespace + + void InstallPipeSessionFailHook() { + MG_Pipe::MGPipeInstallSessionFailHook(&PipeSessionFailAdapter); + } + void SessionFail(MGFatalFamily family, const char* fmt, ...) { // The line, formatted exactly as the site's old MGLOG_F would have. Same buffer size and // same fallback as WireLogFatal, so a death that used either path reads identically. diff --git a/MobileGL/MG_Remote/FatalFunnel.h b/MobileGL/MG_Remote/FatalFunnel.h index acc6b801..959de20d 100644 --- a/MobileGL/MG_Remote/FatalFunnel.h +++ b/MobileGL/MG_Remote/FatalFunnel.h @@ -47,4 +47,16 @@ namespace MobileGL::MG_Remote { // this is zero over a whole good run; a test reads it to prove the funnel was reached. ::std::uint64_t SessionFaultCount(); + // P7 wave 0. Points MG_Pipe's MGPipeSessionFail seam (MG_Pipe/PipeSessionFail.h) at + // SessionFail, which is what makes the three Magma wire funnels in MG_Backend's renderer + // publish a SessionFault, bump SessionFaultCount() and appear in the census like every other + // death - without MG_Backend naming a single MG_Remote symbol. + // + // Called from MG_Backend/Init.cpp's InitServerRoleCommon, which BOTH the inproc server role + // and the spawn/TCP session child run, so there is one install point rather than one per + // transport. Installing it in the client process would be wrong and is not done: the hook's + // families are the SERVER's renderer refusing a wire verb, and a client that installed it + // would publish a SessionFault for a death the server never had. + void InstallPipeSessionFailHook(); + } // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Server/StagedShadow.h b/MobileGL/MG_Remote/Server/StagedShadow.h index 0a37f6f3..7f54893e 100644 --- a/MobileGL/MG_Remote/Server/StagedShadow.h +++ b/MobileGL/MG_Remote/Server/StagedShadow.h @@ -37,6 +37,10 @@ #pragma once #include +// P7 wave 0 / Ph slice (2): RequireCovered's death goes through Session::Fail like every other +// one. Not an out-of-line helper - this header is already inside MG_Remote, so it may name +// MG_Remote's own funnel, and check_include_closure.py's four probes do not reach it. +#include #include #include @@ -103,7 +107,9 @@ namespace MobileGL::MG_Remote::Server { const auto it = m_shadows.find(key); if (it == m_shadows.end() || it->second.Bytes.data() != hostBase) return; if (CoverageHas(it->second.Covered, start, end)) return; - MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants " + // Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0). + SessionFail(MGFatalFamily::StageSnapshotTooNarrow, + "MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants " "[%zu, %zu) of a buffer whose staged coverage does not include it. Under " "split the authoritative shadow is SERVER-OWNED (rule C) and " "resource_subdata is the only way bytes reach it, so bytes outside a staged " @@ -112,7 +118,6 @@ namespace MobileGL::MG_Remote::Server { "GPU-written bytes dead (Managers.cpp:1126-1129). This is a missing record, " "not a missing widening", site, start, end); - std::abort(); } // Diagnostics the unit cases read, so that a check can assert WHAT HAPPENED rather than diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 059a5d53..0251ace1 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -101,6 +101,9 @@ // StagedShadowStore::CoverageAdd / CoverageHas: ONE spelling of the covered set, so the texture // half's runs and the buffer half's ranges cannot drift apart on what "covered" means. #include +// P7 wave 0 / Ph slice (2): RequireLevelBytes' death goes through Session::Fail. Named here +// rather than left to StagedShadow.h's copy of the same include. +#include #include #include @@ -331,7 +334,9 @@ namespace MobileGL::MG_Remote::Server { StagedShadowStore::CoverageHas(shadow->Covered, 0, shadow->Bytes.size())) { return shadow->Bytes.data(); } - MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the " + // Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0). + SessionFail(MGFatalFamily::StageSnapshotTooNarrow, + "MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the " "bytes of (uploadTarget=%u, level=%u) and the server's staged shadow has no " "COMPLETE covered run for it. Under split the authoritative shadow is " "SERVER-OWNED (rule C) and resource_subdata is the only way bytes reach it, " @@ -341,7 +346,6 @@ namespace MobileGL::MG_Remote::Server { "byte answer exists at all. Re-reading the client's shadow would be the " "cross-role access this store exists to end", site, static_cast(uploadTarget), static_cast(level)); - std::abort(); } // Diagnostics the sync path and the unit cases read, so that a check can assert WHAT diff --git a/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp b/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp index 160286ae..da55628e 100644 --- a/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp +++ b/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp @@ -8,12 +8,17 @@ // P6 `dl` (CONTRACT-P6 5.2). The Fatal-family table and its projection onto the wire FatalCode. +#include #include +#include #include #include #include +#include +#include +#include #include #include @@ -29,6 +34,27 @@ namespace { #undef X }; +#if !defined(_WIN32) + // P7 wave 0. SessionFaultCount() is the only thing the funnel leaves behind that a test can + // read, and SessionFail aborts before returning - so the counter has to be read from INSIDE + // the dying process. A SIGABRT handler turns it into the child's exit code: kFaultExitBase + + // n. "The funnel was reached exactly once" is then a value the parent asserts rather than a + // log line it has to parse, and a seam that bypassed the funnel would exit 0x60, not 0x61. + constexpr int kFaultExitBase = 0x60; + + void ReportFaultCountAndExit(int) { + std::_Exit(kFaultExitBase + static_cast(MobileGL::MG_Remote::SessionFaultCount())); + } + + // A hook of the TEST's own, to prove what the seam hands across independently of what + // SessionFail then does with it. It echoes and returns; MGPipeSessionFail's contract is that + // it aborts anyway, which is also under test here. + void EchoingHook(MobileGL::MG_Pipe::MGPipeFatalFamily family, const char* line) { + std::fprintf(stderr, "seam-saw family=%u line=%s\n", static_cast(family), line); + std::fflush(stderr); + } +#endif + } // namespace TEST(FatalFamily, TheTableAndTheEnumAgreeOnCount) { @@ -84,6 +110,75 @@ TEST(FatalFamily, TheAnchorFamiliesProjectWhereTheirMeaningSays) { EXPECT_EQ(FatalCodeForFamily(MGFatalFamily::UnmigratedVerb), FatalCode::ProtocolCorruption); } +// --------------------------------------------------------------------------------------------- +// P7 wave 0: the seam MG_Backend's three Magma wire funnels die through (MG_Pipe/ +// PipeSessionFail.h). Until this package they logged and raised their own std::abort(), so +// fifteen `@P7` refusals plus WireBufferLegacyFatal published no SessionFault to the peer, bumped +// no SessionFaultCount() and were invisible to the census gate. These three cases are the +// red-once for that: delete InstallPipeSessionFailHook()'s call in InitServerRoleCommon and the +// first two go red on the exit code, point a funnel back at MGLOG_F + abort and all three do. + +TEST(FatalFunnelSeam, InstallingTheHookPointsTheSeamSomewhereOtherThanItsDefault) { + using namespace MobileGL; + ASSERT_EQ(MG_Pipe::MGPipeSessionFailHookInstalled(), nullptr) + << "the seam was already installed before this case ran; the binary's default is the " + "no-hook one and only a server-role init should change it"; + MG_Remote::InstallPipeSessionFailHook(); + EXPECT_NE(MG_Pipe::MGPipeSessionFailHookInstalled(), nullptr); + // Restored, so the two cases below start from the same state whichever order they run in. + MG_Pipe::MGPipeInstallSessionFailHook(nullptr); + EXPECT_EQ(MG_Pipe::MGPipeSessionFailHookInstalled(), nullptr); +} + +TEST(FatalFunnelSeam, TheMagmaWireVerbDeathReachesSessionFailAndBumpsTheFaultCount) { +#if defined(_WIN32) + GTEST_SKIP() << "the seam's proof forks and reads a signal, which is POSIX only - the same " + "reason MG_Test/Wire's spawn cases are POSIX only"; +#else + using namespace MobileGL; + // WireFramebuffer.inc's MagmaWireFatal, character for character - the funnel eleven of the + // fifteen `@P7` refusals die through. + EXPECT_EXIT( + { + std::signal(SIGABRT, &ReportFaultCountAndExit); + MG_Remote::InstallPipeSessionFailHook(); + MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb, + "MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", + "unit-probe"); + }, + ::testing::ExitedWithCode(kFaultExitBase + 1), + "Fatal\\{UnmigratedVerb, \"Magma:unit-probe\"\\}") + << "the Magma wire funnel did not end through SessionFail: either the line is not the " + "site's own any more, or SessionFaultCount() did not move - which is what exit gate " + "S8 reads and what every telemetry consumer of the funnel depends on"; +#endif +} + +TEST(FatalFunnelSeam, TheSeamHandsTheInstalledHookItsFamilyAndItsLineVerbatim) { +#if defined(_WIN32) + GTEST_SKIP() << "POSIX only, as above"; +#else + using namespace MobileGL; + // WireDraw.inc's WireBufferLegacyFatal, character for character. Through a hook of the test's + // own rather than MG_Remote's, so what is under test is the SEAM - that the family enum and + // the fully formatted line cross intact - and not what SessionFail then does with them. The + // hook returns; MGPipeSessionFail must still abort, which is why this is an exit assertion. + EXPECT_EXIT( + { + MG_Pipe::MGPipeInstallSessionFailHook(&EchoingHook); + MG_Pipe::MGPipeSessionFail( + MG_Pipe::MGPipeFatalFamily::RoleViolation, + "MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); + }, + ::testing::KilledBySignal(SIGABRT), + "seam-saw family=1 line=MGPipe: Fatal\\{RoleViolation, \"buffer-legacy-arm\"\\} " + "\\(Magma P7 buffer consumer\\)") + << "the seam changed what it hands across: either the family ordinal moved (which would " + "re-point FatalFunnel.cpp's adapter arm at the wrong .def row) or the line is no " + "longer the site's own string, which CONTRACT-P6 5.2 requires to stay byte-identical"; +#endif +} + TEST(FatalFamily, TheSessionFaultFrameCarriesTheFamilyAndItsCode) { // `dl` step three (5.2): the Fatal frame a dying session publishes carries the full family // WORD beside the coarse code, so a peer names which family ended the session instead of diff --git a/scripts/ci/fatal_census.py b/scripts/ci/fatal_census.py index e0f0e00e..6c163685 100644 --- a/scripts/ci/fatal_census.py +++ b/scripts/ci/fatal_census.py @@ -124,6 +124,10 @@ FUNNEL_SITES = { ("#define MGP_TRIP_WIRE_REPORT", "the poison/verify trip-wire funnel: its 30-odd callers each pass MGP_TRIP_WIRE_TAG(name), " "which IS the `Fatal{name}` marker, so the death is named by the caller"), + ("void MGPipeSessionFail(", + "the no-hook default of the backend session-fail seam (MG_Pipe/PipeSessionFail.h): the " + "line it logs is the CALLER's, family word and all, and with MG_Remote's hook installed " + "this abort is never reached at all"), ), "MobileGL/MG_Pipe/generated/PipeWire.inc": ( ("MGPipeWireProtocolFatal", @@ -138,6 +142,13 @@ FUNNEL_SITES = { # abort it has nothing to do with. kMarkerWindow = 12 +# A FUNNEL'S ANCHOR GETS A WIDER WINDOW THAN A MARKER DOES, and for the opposite reason. A marker +# window has to be tight because any `Fatal{` in it silences the abort below; an anchor window can +# be loose because the anchor is a specific line of code, argued for by name in the table above, +# and the only thing a wider window can reach is the same funnel's own body. Forty covers a funnel +# whose signature and abort are separated by its formatting and its explanation. +kFunnelAnchorWindow = 40 + MARKER = re.compile(r"Fatal\{([A-Za-z][A-Za-z0-9]*)") REFUSAL = re.compile(r"Refuse\{([A-Za-z][A-Za-z0-9]*)") ABORT = re.compile(r"\bstd::abort\(\)") @@ -145,7 +156,11 @@ WIRE_LOG_FATAL_CALL = re.compile(r"WireLogFatal\s*\(") # P6 dl: a SessionFail call must carry a Fatal{ word in its string, the same rule # WireLogFatal has - the enum and the string both name the family, and a call whose string # forgot it would let the two disagree. Checked everywhere SessionFail is called. -SESSION_FAIL_CALL = re.compile(r"SessionFail\s*\(") +# +# The lookbehind is not decoration: without it this pattern also matches `MGPipeSessionFail(`, +# and it reported MG_Pipe's own seam - whose callers carry the word exactly as SessionFail's do - +# for being a SessionFail call with no word in its DECLARATION. +SESSION_FAIL_CALL = re.compile(r"(?