diff --git a/CMakeLists.txt b/CMakeLists.txt index f1c30fe3..66ad6c65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -621,6 +621,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # emitter that returns quietly is how a split lane runs monolith and # goes green (ARCHITECTURE.md 10.3). MobileGL/MG_Remote/CapsCodec.cpp + # P6 dl: the one place a session dies - Session::Fail's funnel (Fatal-family aware, + # publishes SessionFault, one telemetry point). + MobileGL/MG_Remote/FatalFunnel.cpp MobileGL/MG_Remote/Wire/PipeWireCodec.cpp MobileGL/MG_Remote/Client/ClientSession.cpp MobileGL/MG_Remote/Client/EmitTables.cpp diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 5657a248..e045aff1 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -10,6 +10,7 @@ // reply read that sits inside it are package c1's (EmitAndWait below is still c0's stub). #include "ClientSession.h" +#include #include "../CapsCodec.h" #include "../Protocol/generated/protocol_generated.h" @@ -60,12 +61,10 @@ namespace MobileGL::MG_Remote::Client { } // namespace #define MGP5_C0_STUB(what) \ - do { \ - MGLOG_F("MGPipe: Fatal{UnimplementedClientSession, \"%s\"} - P5 packages s1/c1 have not " \ + SessionFail(MGFatalFamily::UnimplementedClientSession, \ + "MGPipe: Fatal{UnimplementedClientSession, \"%s\"} - P5 packages s1/c1 have not " \ "landed this yet; c0 shipped the signature only", \ - what); \ - std::abort(); \ - } while (0) + what) namespace { @@ -124,25 +123,23 @@ namespace MobileGL::MG_Remote::Client { // to be reachable, which is how the gap survived review. [[noreturn]] void FatalProtocolVersionMismatch(unsigned theirMajor, unsigned theirMinor, const char* theirStamp) { - MGLOG_F("MGPipe: Fatal{AbiMismatch, \"protocol version\"} ours=%u.%u theirs=%u.%u " + SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"protocol version\"} ours=%u.%u theirs=%u.%u " "ourBuild=%s theirBuild=%s - the peer speaks a different MGPipe protocol. " "Under spawn this is most often a stale MOBILEGL_IPC_SERVER_PATH pointing at " "a libMobileGLServer.so built from another tree.", static_cast(MOBILEGL_PROTOCOL_ABI_MAJOR), static_cast(MOBILEGL_PROTOCOL_ABI_MINOR), theirMajor, theirMinor, GIT_COMMIT_HASH_SHORT, theirStamp == nullptr ? "?" : theirStamp); - std::abort(); } [[noreturn]] void FatalAbiMismatch(const char* what, Uint64 ours, Uint64 theirs, const char* theirStamp) { - MGLOG_F("MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s " + SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s " "theirBuild=%s - never a downgrade: the caps block's size is ABI-dependent " "and every field past the first difference would be read at the wrong offset", what, static_cast(ours), static_cast(theirs), GIT_COMMIT_HASH_SHORT, theirStamp == nullptr ? "?" : theirStamp); - std::abort(); } // Guarded the same way ServerSession's helpers are: MG_Config::Ipc only exists @@ -326,9 +323,8 @@ namespace MobileGL::MG_Remote::Client { Bool corrupt = false; while (events.Pop(view, &corrupt)) { if (corrupt) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"event-ring\"} - the reverse " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"event-ring\"} - the reverse " "channel's record stream is corrupt"); - std::abort(); } switch (view.kind) { case Transport::kEventBufferWriteback: { @@ -337,11 +333,10 @@ namespace MobileGL::MG_Remote::Client { static_cast(view.payload); const void* bytes = static_cast(view.payload) + sizeof(*head); if (view.payloadSize - sizeof(*head) < head->Size) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"buffer-writeback\"} - the " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"buffer-writeback\"} - the " "head declares %llu inline bytes and the record carries %llu", static_cast(head->Size), static_cast(view.payloadSize - sizeof(*head))); - std::abort(); } // The blobref names SEG_EVENT and the IN-SEGMENT offset of those inline // bytes - never a host address (R-2's rule B), which is the whole reason @@ -363,11 +358,10 @@ namespace MobileGL::MG_Remote::Client { static_cast(view.payload); const Uint64 tail = view.payloadSize - sizeof(*head); if (tail / sizeof(Transport::EventRange) < head->RangeCount) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"gpu-written\"} - RangeCount " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"gpu-written\"} - RangeCount " "%u does not fit the record's %llu tail bytes", static_cast(head->RangeCount), static_cast(tail)); - std::abort(); } // EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66 // asserts it), so the tail is handed over as-is rather than copied into @@ -408,12 +402,11 @@ namespace MobileGL::MG_Remote::Client { // least one byte and that byte terminates the string. if (head->MessageBytes == 0 || tail < head->MessageBytes || message[head->MessageBytes - 1] != '\0') { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"kEventGlError\"} - " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"kEventGlError\"} - " "MessageBytes %u against a %llu-byte tail, or the terminating " "NUL is missing", static_cast(head->MessageBytes), static_cast(tail)); - std::abort(); } // The error queue is CLIENT state, written here on the GL thread. The // observation point is the next drain after the post - P9 owns the @@ -1381,11 +1374,10 @@ namespace MobileGL::MG_Remote::Client { if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError; if (replySizeOut != nullptr) *replySizeOut = 0; if (!m_started) { - MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - EmitAndWait on a session that has " + SessionFail(MGFatalFamily::NoClientSession, "MGPipe: Fatal{NoClientSession, \"%s\"} - EmitAndWait on a session that has " "not started. There is no fall-through: a record that could not be emitted " "is a verb that did not happen", Wire::WireOpName(op)); - std::abort(); } // R-1's INVARIANT, AS A RUNTIME CHECK RATHER THAN A SENTENCE - but only while the @@ -1399,11 +1391,10 @@ namespace MobileGL::MG_Remote::Client { // the fill quiescent during that apply). With the batch off, the original check // stands as written. if (MG_Config::Ipc.BatchWaits == 0 && ApplyThreadIsInsideApplier()) { - MGLOG_F("MGPipe: Fatal{BarrierViolation, \"%s\"} - the apply thread is inside the " + SessionFail(MGFatalFamily::BarrierViolation, "MGPipe: Fatal{BarrierViolation, \"%s\"} - the apply thread is inside the " "applier while the GL thread is emitting. R-1 makes at most one of them " "runnable, which is what keeps one process-wide gPipeInputs legal", Wire::WireOpName(op)); - std::abort(); } Uint64 seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount); @@ -1418,11 +1409,10 @@ namespace MobileGL::MG_Remote::Client { const BarrierWaitScope waiting; const auto wait = m_producer.WaitForCmdSpace(needed, kBarrierTimeoutMs); if (wait != Transport::SessionWait::Reached) { - MGLOG_F("MGPipe: Fatal{RetirementWaitFailed, \"SEG_CMD\"} - %s needs %llu " + SessionFail(MGFatalFamily::RetirementWaitFailed, "MGPipe: Fatal{RetirementWaitFailed, \"SEG_CMD\"} - %s needs %llu " "reclaimable bytes; the retirement wait ended on %s", Wire::WireOpName(op), static_cast(needed), wait == Transport::SessionWait::ShutDown ? "shutdown" : "timeout"); - std::abort(); } // P5e (ra, §2.6): EVERY WAIT EXIT DRAINS. The cmd-space wait is one of the two // safety nets under run-ahead (the stage bell is the other), and a client parked @@ -1432,9 +1422,8 @@ namespace MobileGL::MG_Remote::Client { DrainEventRing(m_events); seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount); if (seq == Wire::kInvalidSeq) { - MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s refused after sufficient " + SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s refused after sufficient " "space retired", Wire::WireOpName(op)); - std::abort(); } } @@ -1588,12 +1577,11 @@ namespace MobileGL::MG_Remote::Client { return seq; } if (wait != Transport::SessionWait::Reached) { - MGLOG_F("MGPipe: Fatal{BarrierTimeout, \"%s\"} - appliedSeq did not reach %llu within " + SessionFail(MGFatalFamily::BarrierTimeout, "MGPipe: Fatal{BarrierTimeout, \"%s\"} - appliedSeq did not reach %llu within " "%llu ms. A bounded wait is deliberate: a wedged CI job and a lost record look " "identical from outside, and only one of them is a bug worth finding", Wire::WireOpName(op), static_cast(seq), static_cast(waitBudgetMs)); - std::abort(); } // THE REVERSE CHANNEL IS DRAINED HERE, and here is the only place it can be: under the @@ -1620,11 +1608,10 @@ namespace MobileGL::MG_Remote::Client { Uint64 replySize = 0; Int32 status = Wire::ReplySink::kStatusError; if (!ReadReply(seq, replyOut, replyBytes, &status, &replySize)) { - MGLOG_F("MGPipe: Fatal{ReplyMissing, \"%s\"} - seq %llu carries kReplySlot and the " + SessionFail(MGFatalFamily::ReplyMissing, "MGPipe: Fatal{ReplyMissing, \"%s\"} - seq %llu carries kReplySlot and the " "server applied it, but its slot does not stamp that seq. The stamp is what " "makes a wrong-slot read detectable rather than plausible (R-3)", Wire::WireOpName(op), static_cast(seq)); - std::abort(); } if (statusOut != nullptr) *statusOut = status; if (replySizeOut != nullptr) *replySizeOut = replySize; @@ -1637,11 +1624,10 @@ namespace MobileGL::MG_Remote::Client { // (R-5). A client that treated it as a failure would re-create ID-39 from the other // side. if (replyOut != nullptr && replySize > replyBytes) { - MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"%s\"} - the answer is %llu bytes and the " + SessionFail(MGFatalFamily::ReplyTooLarge, "MGPipe: Fatal{ReplyTooLarge, \"%s\"} - the answer is %llu bytes and the " "caller offered %llu. P5 does not chunk a reply", Wire::WireOpName(op), static_cast(replySize), static_cast(replyBytes)); - std::abort(); } return seq; } @@ -1730,12 +1716,11 @@ namespace MobileGL::MG_Remote::Client { return; } if (wait != Transport::SessionWait::Reached) { - MGLOG_F("MGPipe: Fatal{BarrierTimeout, \"%s\"} - a forced run-ahead wait did not " + SessionFail(MGFatalFamily::BarrierTimeout, "MGPipe: Fatal{BarrierTimeout, \"%s\"} - a forced run-ahead wait did not " "reach appliedSeq %llu within %u ms. The forced waits of CONTRACT-P5E §2.5 " "are the points at which the client must be in step again, so a wedge here " "is a wedge in the queue and not a slow frame", why, static_cast(target), kBarrierTimeoutMs); - std::abort(); } // EVERY WAIT EXIT DRAINS (§2.6). It is what the flow-control deadlock argument rests // on: the server blocks only on a full event ring, the client blocks only on @@ -1794,13 +1779,12 @@ namespace MobileGL::MG_Remote::Client { DrainEventRing(m_events); } if (wait == Transport::SessionWait::TimedOut) { - MGLOG_F("MGPipe: Fatal{PresentCreditTimeout} - present %llu waited %u ms for " + SessionFail(MGFatalFamily::PresentCreditTimeout, "MGPipe: Fatal{PresentCreditTimeout} - present %llu waited %u ms for " "swap %llu to come back with a credit of %u. The credit, never the " "ring's bytes, is what paces a run-ahead client (CONTRACT-P5E §2.4), " "so a credit that never returns is a server that stopped swapping", static_cast(serial), kBarrierTimeoutMs, static_cast(awaited), credit); - std::abort(); } // ShutDown is teardown and returns: the present did not happen, the same answer // the barrier gives on a dead doorbell. @@ -1880,7 +1864,7 @@ namespace MobileGL::MG_Remote::Client { // is exactly "no record is being applied" and not merely "not decoding". if (isBarrieredFill && !ApplyThreadIsInsideApplier()) return; if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block - MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched " "gPipeInputs (%s) on a RUN-AHEAD session %s. With the client running ahead " "the block is the server's to read for as long as ANY record is in flight " "(CONTRACT-P5E §3), so the only legal GL-thread write is a barriered fill " @@ -1889,7 +1873,6 @@ namespace MobileGL::MG_Remote::Client { isBarrieredFill ? "in a barriered fill that did not first wait for the " "applier to catch up" : "outside a barriered fill"); - std::abort(); } // MOBILEGL_IPC_BATCH_WAITS=1 makes "the fill runs while the apply thread applies an // earlier value record" the INTENDED shape: the fill writes only fields no record @@ -1909,13 +1892,12 @@ namespace MobileGL::MG_Remote::Client { // access in a shared library, and it must stay thread-local (see InBarrierWait's note // above - a second GL thread would otherwise read the first one's wait as its own). if (InBarrierWait()) return; - MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched gPipeInputs " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched gPipeInputs " "(%s) while the apply thread was inside the applier and this thread was not in a " "barrier wait. R-1's barrier is the only thing that makes one process-wide " "gPipeInputs legal (CONTRACT-P5 table 3); a touch in this window races the " "applier's own reads of it", surface); - std::abort(); } void ClientSession::NoteApplyThreadEnteredApplier() { g_roleInsideApplier = true; diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 76a418c1..5bd989ec 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -28,6 +28,7 @@ // regression re-committed at the transport layer. #include "EmitTables.h" +#include #include "ClientSession.h" #include "GpuWritePending.h" @@ -87,8 +88,7 @@ namespace MobileGL::MG_Remote::Client { // The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the // slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is // inert in an INFO build - and INFO is what every device lane runs. - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); - std::abort(); + SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); } namespace { @@ -209,11 +209,10 @@ namespace MobileGL::MG_Remote::Client { RequireClientTablesInstalled(slot); ClientSession* session = ClientSession::Active(); if (session == nullptr) { - MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the remote emit table is " + SessionFail(MGFatalFamily::NoClientSession, "MGPipe: Fatal{NoClientSession, \"%s\"} - the remote emit table is " "installed but no ClientSession is active. A slot may not fall through " "to a driver this role does not have", slot); - std::abort(); } return *session; } @@ -836,11 +835,10 @@ namespace MobileGL::MG_Remote::Client { // NOT a guess and not a zero-length reply. A format this build cannot size is a // readback whose answer would be silently truncated, which is the one failure a // picture comparison cannot see. - MGLOG_F("MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x " + SessionFail(MGFatalFamily::UnsizedReadback, "MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x " "- the client must declare MGPReadbackInfo::DstSize and cannot size this " "pair; P5's reduced path reads RGBA/UNSIGNED_BYTE", static_cast(format), static_cast(type)); - std::abort(); } return static_cast(bytesPerPixel); } @@ -882,29 +880,25 @@ namespace MobileGL::MG_Remote::Client { // an oversize reply, so the only failures left are a wrong status or a SHORT one. void RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) { if (status == Wire::ReplySink::kStatusError) { - MGLOG_F("MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; " + SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; " "the destination is left untouched rather than filled with stale bytes"); - std::abort(); } if (status == Wire::ReplySink::kStatusDeclined) { - MGLOG_F("MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no " + SessionFail(MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no " "GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance " "row (R-5) but a blocking readback has no pixels to return, so it is a " "Fatal here rather than a buffer of stale bytes"); - std::abort(); } if (status != Wire::ReplySink::kStatusOk) { - MGLOG_F("MGPipe: Fatal{ReplyStatusInvalid, \"ReadPixels\"} - unknown reply status %d", status); - std::abort(); + SessionFail(MGFatalFamily::ReplyStatusInvalid, "MGPipe: Fatal{ReplyStatusInvalid, \"ReadPixels\"} - unknown reply status %d", status); } if (replySize != expected) { - MGLOG_F("MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK " + SessionFail(MGFatalFamily::ReadbackReplyShort, "MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK " "reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row " "23's exact extent); the missing rows would otherwise be scattered as " "whatever the destination held", static_cast(replySize), static_cast(expected)); - std::abort(); } } diff --git a/MobileGL/MG_Remote/Client/GpuWritePending.cpp b/MobileGL/MG_Remote/Client/GpuWritePending.cpp index 7ecbc1f2..947fe6f0 100644 --- a/MobileGL/MG_Remote/Client/GpuWritePending.cpp +++ b/MobileGL/MG_Remote/Client/GpuWritePending.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "GpuWritePending.h" +#include #include "ClientSession.h" // RunsAsTheServerRole: declared beside the wire emitters that need the same predicate, so this @@ -252,12 +253,11 @@ namespace MobileGL::MG_Remote::Client { // UnmigratedVerbFatal), and it is what gives the hole a red spelling before the // transport arrives. if (!buffer.HasOutstandingGpuWrite()) return; - MGLOG_F("MGPipe: Fatal{UnimplementedWritebackWait} - a ClientSession is active and buffer %u " + SessionFail(MGFatalFamily::UnimplementedWritebackWait, "MGPipe: Fatal{UnimplementedWritebackWait} - a ClientSession is active and buffer %u " "still has an outstanding GPU write after its readback was emitted. The wait is " "ClientSession::EmitAndWait's (R-3: the reply slot id IS the record seq); P5 package " "b1 landed the third state and s1/c1 own the wait itself.", buffer.GetExternalIndex()); - std::abort(); } } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp index b4099f19..5b4c6eb9 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "PersistentMapTracker.h" +#include #include #include @@ -640,9 +641,8 @@ namespace MobileGL::MG_Remote::Client { void PersistentMapTracker::PushBlocksFor(BufferObject& buffer) { if (!PushIsArmed()) return; if (OnServerRole()) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"PushBlocksFor\"} - the persistent-map " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"PushBlocksFor\"} - the persistent-map " "producer belongs to the client; the server consumes transported bytes"); - std::abort(); } PushBlocksForChecked(buffer); } @@ -856,9 +856,8 @@ namespace MobileGL::MG_Remote::Client { void PersistentMapTracker::PushAllMembers() { if (!PushIsArmed()) return; if (OnServerRole()) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"PushAllMembers\"} - the persistent-map " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"PushAllMembers\"} - the persistent-map " "producer belongs to the client; the server consumes transported bytes"); - std::abort(); } if (m_livePersistentMaps.empty()) return; // Copied out first: PushBlocksFor can erase its own entry (a member that stopped @@ -874,9 +873,8 @@ namespace MobileGL::MG_Remote::Client { void PersistentMapTracker::PushDrawConsumers() { if (!PushIsArmed()) return; if (OnServerRole()) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"PushDrawConsumers\"} - the persistent-map " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"PushDrawConsumers\"} - the persistent-map " "producer belongs to the client; the server consumes transported bytes"); - std::abort(); } if (m_livePersistentMaps.empty()) return; // THE EPOCH SKIP, AND WHAT IT RESTS ON. A page of a tracked map is marked by @@ -1052,17 +1050,18 @@ namespace MobileGL::MG_Remote::Client { // of them and no family grep could see them - this is one. (The other, WireLog.cpp's, is // the sanctioned funnel: every one of its callers passes a Fatal{ string of its own.) if (tier <= 1) { - MGLOG_F("MGPipe: Fatal{UnimplementedAdoptTier, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u " + SessionFail(MGFatalFamily::UnimplementedAdoptTier, + "MGPipe: Fatal{UnimplementedAdoptTier, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u " "names an adoption tier P11 implements and P5 does not; P5 runs at T2 " "(emulate) only.", static_cast(tier), static_cast(tier)); } else { - MGLOG_F("MGPipe: Fatal{UnimplementedAdoptTier, \"%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u is " + SessionFail(MGFatalFamily::UnimplementedAdoptTier, + "MGPipe: Fatal{UnimplementedAdoptTier, \"%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u is " "not an adoption tier; the only values are 0 and 1 (P11) and 2 (emulate, the " "P5 default).", static_cast(tier), static_cast(tier)); } - std::abort(); } } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index f94c7f8f..73563868 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -29,6 +29,7 @@ // before every `set_dynamic_state` - hundreds of times a frame, and each one a real record. #include "WireTables.h" +#include #if MOBILEGL_BUILD_DISAGGREGATED @@ -113,11 +114,10 @@ namespace MobileGL::MG_Remote::Client { RequireClientTablesInstalled(row); ClientSession* session = ClientSession::Active(); if (session == nullptr) { - MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are " + SessionFail(MGFatalFamily::NoClientSession, "MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are " "installed but no ClientSession is active. A row may not fall through to " "a driver this role does not have", row); - std::abort(); } return *session; } @@ -129,12 +129,11 @@ namespace MobileGL::MG_Remote::Client { MG_Pipe::MGPBlobRef StageRequired(ClientSession& session, const char* row, const void* bytes, Uint64 count) { if (bytes == nullptr || count == 0) { - MGLOG_F("MGPipe: Fatal{BlobMissing, \"%s\"} - the row's decoder requires a " + SessionFail(MGFatalFamily::BlobMissing, "MGPipe: Fatal{BlobMissing, \"%s\"} - the row's decoder requires a " "declared blob and the call site handed over %llu bytes at %p. Under " "monolith the companion pointer carries them; under split they have to " "be staged, and there is nothing to stage", row, static_cast(count), bytes); - std::abort(); } return session.Encoder().StageBytes(bytes, count); } @@ -409,12 +408,11 @@ namespace MobileGL::MG_Remote::Client { } ClientSession& session = RequireSession("ResourceRespecify"); if (initialBytes != nullptr) { - MGLOG_F("MGPipe: Fatal{UncarriedInitialBytes, \"resource_respecify\"} - a call " + SessionFail(MGFatalFamily::UncarriedInitialBytes, "MGPipe: Fatal{UncarriedInitialBytes, \"resource_respecify\"} - a call " "site handed initial content to a split respecify. R-13.3 rules that " "initialBytes never crosses and that the content follows as " "resource_subdata; a caller that still passes it has bytes nothing will " "carry"); - std::abort(); } // The scope rides in the descriptor's own pads (CONTRACT-P5 table 1 row 19b, LANDED) // and is written only through MGPipeSetRespecifiedLevel - three fields are one @@ -438,10 +436,9 @@ namespace MobileGL::MG_Remote::Client { // - the generated acceptance rows through MGPipeTakeReplyBool, and now these two // escapes - answers ERROR with the same named Fatal. if (status == 2) { - MGLOG_F("MGPipe: Fatal{ReplyError, \"resource_respecify\"} - the row answered " + SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"resource_respecify\"} - the row answered " "ERROR, which is not an acceptance answer; folding it into accepted or " "refused would make a transport fault look like a resource decision"); - std::abort(); } if (status == 1) ++g_declined; return status == 0; @@ -485,22 +482,20 @@ namespace MobileGL::MG_Remote::Client { // the generated acceptance rows obey: status 2 is a transport fault, and returning // nullptr for it would make it indistinguishable from R-6's legitimate decline. if (status == 2) { - MGLOG_F("MGPipe: Fatal{ReplyError, \"map_persistent\"} - the row answered ERROR, " + SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"map_persistent\"} - the row answered ERROR, " "which is not an acceptance answer; a transport fault is not a resource " "decision and may not be folded into the decline R-6 predicts"); - std::abort(); } if (status == 1) ++g_declined; // NOT "always nullptr": the answer is READ. R-6 says the server declines, and the // day it stops declining this returns what it actually said rather than what the // ruling predicted. if (status == 0) { - MGLOG_F("MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server " + SessionFail(MGFatalFamily::UnexpectedMapAccept, "MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server " "accepted a persistent map under split. R-6 makes the split answer a " "constant decline because there is no way to hand a host pointer across " "a process boundary in P5; a pointer arriving here is one this client " "cannot dereference"); - std::abort(); } return nullptr; } @@ -522,12 +517,11 @@ namespace MobileGL::MG_Remote::Client { } ClientSession& session = RequireSession("CreateShaderState"); if (link == nullptr || spirv == nullptr) { - MGLOG_F("MGPipe: Fatal{ArtefactsMissing, \"create_shader_state\"} - the record's " + SessionFail(MGFatalFamily::ArtefactsMissing, "MGPipe: Fatal{ArtefactsMissing, \"create_shader_state\"} - the record's " "two typed companions are null. Under monolith the applier reads the " "modules out of spirv->generatedSpirv; under split there is nothing to " "serialise, and emitting the record anyway would create a CSO with no " "code"); - std::abort(); } // EncodeProgramArchive APPENDS and never fails - everything it walks is owned // plain data - so an empty archive means the two structs themselves were empty, @@ -547,9 +541,8 @@ namespace MobileGL::MG_Remote::Client { static_cast(archive.size())); } if (archive.empty()) { - MGLOG_F("MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's " + SessionFail(MGFatalFamily::ArchiveEmpty, "MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's " "artefacts serialised to nothing"); - std::abort(); } MG_Pipe::MGPProgramDesc record = *desc; for (Uint32 i = 0; i < 6; ++i) record.Spirv[i] = MG_Pipe::MGPBlobRef{}; @@ -714,9 +707,8 @@ namespace MobileGL::MG_Remote::Client { void RequireClientTablesInstalled(const char* row) { if (g_clientTablesUninstalled.load(std::memory_order_acquire)) { - MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are " + SessionFail(MGFatalFamily::ClientTablesUninstalled, "MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are " "being torn down; refusing before session or ring access", row); - std::abort(); } } diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp new file mode 100644 index 00000000..a8892fe7 --- /dev/null +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -0,0 +1,62 @@ +// MobileGL - MobileGL/MG_Remote/FatalFunnel.cpp +// 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 + +#include + +#include "../Includes.h" // MGLOG_F and the umbrella; a .cpp may pull it, a Transport/ header may not + +#include +#include +#include + +namespace MobileGL::MG_Remote { + + namespace { + // One telemetry point (5.2). Bumped exactly once per death, before the abort, so a test + // that forks a child and reads it back sees the increment even though the child is gone - + // it reads its OWN process's counter for the funnel it drove, not the child's. + std::atomic g_sessionFaultCount{0}; + } // namespace + + std::uint64_t SessionFaultCount() { + return g_sessionFaultCount.load(std::memory_order_relaxed); + } + + 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. + 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), + "MG_Remote: unformattable Fatal diagnostic (format=%s)", fmt); + } + + g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed); + + MGLOG_F("%s", line); + // The stderr echo is the half a death test can see: Defines.h builds the logger with the + // console sink off, so an MGLOG line reaches only the file - and a gtest death matcher + // reads the child's stderr. Flushed because abort() does not flush stdio. + std::fputs(line, stderr); + std::fputc('\n', stderr); + std::fflush(stderr); + + // `dl` step three publishes a SessionFault{family, detail, seq, op} frame to the peer + // here, so the guest's log names which record ended the session instead of reading a bare + // EOF. The family's wire projection is FatalCodeForFamily(family); until the frame lands + // the family is still carried in the line above and counted here. + (void)family; + + std::abort(); + } + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/FatalFunnel.h b/MobileGL/MG_Remote/FatalFunnel.h new file mode 100644 index 00000000..acc6b801 --- /dev/null +++ b/MobileGL/MG_Remote/FatalFunnel.h @@ -0,0 +1,50 @@ +// MobileGL - MobileGL/MG_Remote/FatalFunnel.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 + +// P6 `dl` (CONTRACT-P6 5.2). THE ONE PLACE A SESSION DIES. +// +// Every `Fatal{...}` under MG_Remote/ used to be an MGLOG_F followed by its own std::abort(). +// a6 counted ~92 of them. Scattered, they gave the funnel's three benefits to nobody: there was +// no single place to publish a SessionFault to the peer before dying, no single telemetry point, +// and no way for a census to be sure a new death was named. This is that place. +// +// THE MESSAGE STRING IS PASSED VERBATIM, family word and all. The contract requires the log +// lines, the CI greps and every red-once to be byte-identical, so SessionFail does not compose +// the line from the family - it takes the SAME string the site used to hand MGLOG_F and forwards +// it unchanged. The family enum rides ALONGSIDE, redundantly, so the funnel knows the coarse wire +// FatalCode without parsing the string; a test asserts the enum's name appears in the string, so +// the redundancy cannot rot into disagreement. +// +// NOT in WireLog.h: that header is deliberately dependency-light (ITransport.h's rule, the +// include-graph purity gate), and this needs FatalFamily.h, which pulls the protocol header. The +// header-layer sites that must stay pure keep using WireLogFatal, whose own abort routes through +// the same publisher in WireLog.cpp. + +#pragma once + +#include + +#include + +namespace MobileGL::MG_Remote { + + // Logs the line (byte-identical to the old MGLOG_F), echoes it to stderr so a death test can + // see it, publishes a SessionFault{family, ...} to the peer if one is connected, bumps the + // telemetry counter, and aborts. `fmt` is the FULL original message, e.g. + // "MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s ...". + [[noreturn]] +#if defined(__GNUC__) || defined(__clang__) + __attribute__((format(printf, 2, 3))) +#endif + void SessionFail(MGFatalFamily family, const char* fmt, ...); + + // How many times a session has ended through the funnel this process. Exit gate S8 asserts + // this is zero over a whole good run; a test reads it to prove the funnel was reached. + ::std::uint64_t SessionFaultCount(); + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp index a3aa2847..2e362e3b 100644 --- a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp +++ b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "SurfaceOpCodec.h" +#include #include #include @@ -207,22 +208,24 @@ namespace MobileGL::MG_Remote { const SurfaceWireError error = DecodeWireSurfaceOp(op, &frame); if (error != SurfaceWireError::None) { if (error == SurfaceWireError::AndroidNativeWindowArrived) { - MGLOG_F("MGPipe: Fatal{UnmigratedSurface, \"AndroidNativeWindow@P12\"} - a wire " + SessionFail(MGFatalFamily::UnmigratedSurface, + "MGPipe: Fatal{UnmigratedSurface, \"AndroidNativeWindow@P12\"} - a wire " "SurfaceOp (%s) named an ANativeWindow*, which is a pointer into the " "CLIENT's process and means nothing here. Real window arrival is P12; " "until then the spawn surface path is pbuffer/surfaceless only", ::MobileGL::Wire::EnumNameSurfaceOpKind(op.kind())); } else if (error == SurfaceWireError::MetalLayerArrived) { - MGLOG_F("MGPipe: Fatal{UnmigratedSurface, \"MetalLayer@P12\"} - a wire " + SessionFail(MGFatalFamily::UnmigratedSurface, + "MGPipe: Fatal{UnmigratedSurface, \"MetalLayer@P12\"} - a wire " "SurfaceOp named a CAMetalLayer* in the CLIENT's process. " "Real window arrival is P12"); } else { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp\"} - a wire surface op " + SessionFail(MGFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp\"} - a wire surface op " "failed validation: %s (wire kind %u, window kind %u)", SurfaceWireErrorName(error), static_cast(op.kind()), static_cast(op.windowKind())); } - std::abort(); } const MobileGLResult rc = Server::ServerLoopInstance().RunSurfaceControlFrame(frame); if (replyOut != nullptr) *replyOut = frame; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index ecd07100..49990cee 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -9,6 +9,7 @@ // P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs. #include "PipeApplier.h" +#include #include "ServerSession.h" #include "../Transport/ReplySlot.h" @@ -326,11 +327,10 @@ namespace MobileGL::MG_Remote::Server { // is a protocol fault, because such a client is pacing on this answer and a 0 would // acknowledge a frame nobody asked about. if (present.FrameSerial == 0 && MGPipeServerPublishesRunAhead()) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead " "server was handed present serial 0. The client mints this 1-based and " "waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for " "serial 0 would release a wait that is asking about frame N"); - std::abort(); } m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents; // §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no @@ -385,11 +385,10 @@ namespace MobileGL::MG_Remote::Server { // The decoder always passes its ReplySink; a null one means the applier was built // without a reply pool, and answering nothing would leave the client's barrier // waiting for a slot that never gets stamped - a hang, not a wrong picture. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - " "the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) " "and a client blocked on seq %llu would never be answered", static_cast(seq)); - std::abort(); } const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels"); if (table == nullptr || table->GL.ReadPixels == nullptr) { @@ -482,10 +481,9 @@ namespace MobileGL::MG_Remote::Server { // rather than rendering nothing. Named "(server sink)" in the message so a log reader can // tell which half is missing. [[noreturn]] static void ServerUnmigratedVerbFatal(const char* slot) { - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"} (server sink: the record crossed and " + SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"} (server sink: the record crossed and " "ServerVerbSink has no body for it yet - CONTRACT-P5B.md names the package)", slot); - std::abort(); } // P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record @@ -553,22 +551,20 @@ namespace MobileGL::MG_Remote::Server { if (maxTouched < 0) return; const Uint32 required = static_cast(maxTouched) + 1u; if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with " "units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u " "(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops " "the sync of at least one texture this draw samples", verb, static_cast(maxTouched), required, st.SamplerViewStart, st.SamplerViewCount); - std::abort(); } if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with " "units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u " "(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves " "an earlier draw's sampler object on at least one unit", verb, static_cast(maxTouched), required, st.SamplerStateStart, st.SamplerStateCount); - std::abort(); } } } // namespace @@ -1147,13 +1143,12 @@ namespace MobileGL::MG_Remote::Server { // accepted resets IS the expected value; anything else means the two ends disagree // about how many make-current edges have crossed, which no backend answer can fix. if (reset.ContextSerial != m_applierResetSerial) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the " "record carries %llu and this session has accepted %llu reset(s); the " "serial is asserted against the session's own count, not dispatched on " "(one context per session in P5c)", static_cast(reset.ContextSerial), static_cast(m_applierResetSerial)); - std::abort(); } ++m_applierResetSerial; // THE WHOLE POINT OF THE RECORD: the reset runs HERE, on the apply thread, against @@ -1170,16 +1165,14 @@ namespace MobileGL::MG_Remote::Server { // emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption, // not a no-op. if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null " "handle never crosses: the client emits nothing for an object its own " "allocator cannot resolve (CONTRACT-P5C.md §5.2)"); - std::abort(); } if (death.Kind >= static_cast(MG_Pipe::MGPipeKind::KindCount)) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an " "MGPipeKind", static_cast(death.Kind)); - std::abort(); } // The per-kind release, keyed by the handle the record carried. A false answer is // NOT a decline: the kind's own delete opcode may already have released the twin @@ -1201,10 +1194,9 @@ namespace MobileGL::MG_Remote::Server { void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) { if (control == nullptr || m_segments == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control " "page or no segment table; ServerSession::Accept builds both before the " "apply thread starts"); - std::abort(); } m_verbs.SetBackend(backend); m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies); @@ -1228,10 +1220,9 @@ namespace MobileGL::MG_Remote::Server { Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) { if (!m_attached) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} " "- a record reached the applier with no decoder; the apply thread calls " "Attach once before its first pop"); - std::abort(); } // R-1's INVARIANT, THE SERVER'S HALF (table 3's gPipeInputs row, c1-v1 8.1). The flag // is raised for the WHOLE of this function and not only around DecodeAndApply: the diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 8f08db7d..db1dd90b 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -9,6 +9,7 @@ // P5 package v1: the apply thread, its affinity, its parking, and the EGL ownership move. #include "ServerLoop.h" +#include #include #include @@ -622,10 +623,9 @@ namespace MobileGL::MG_Remote::Server { // Ring.h's own rule: a header the producer could not have written is // Fatal{ProtocolCorruption}, never a retry. Retrying re-reads the same bytes // for ever; skipping desynchronises seq, and seq IS the reply-slot id. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SEG_CMD record header\"} - the " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SEG_CMD record header\"} - the " "consumer refused a record header at applied seq %llu", static_cast(consumer.AppliedSeq())); - std::abort(); } if (!popped) break; ++applied; @@ -636,13 +636,12 @@ namespace MobileGL::MG_Remote::Server { // and PipeWireDecoder keeps its own count; a batched publish would move one and not // the other, which a single counter could not have told apart (w1-v1 5). if (applier.DecoderAppliedSeq() != consumer.AppliedSeq()) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"appliedSeq batched\"} - the " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"appliedSeq batched\"} - the " "session's watermark is %llu and the decoder applied %llu records. P5 " "forbids batching appliedSeq (R-9): the verb barrier's waiter reads it, " "and a watermark ahead of the decoder promises work that has not run", static_cast(consumer.AppliedSeq()), static_cast(applier.DecoderAppliedSeq())); - std::abort(); } // RETIRE. MANDATORY, not optional: RingProducer::FreeBytes() reclaims against // retiredTail only, and w1's SEG_STAGE linear allocator reclaims on retiredSeq - so @@ -705,13 +704,12 @@ namespace MobileGL::MG_Remote::Server { // render correctly with the context on the wrong thread - the one outcome R-1 // exists to make impossible. Fatal by name, never a fallback; compare // CreateBackend's default: arm, which also refuses rather than substitutes. - MGLOG_F("MGPipe: Fatal{ApplyThreadNotRunning, \"EGL on the app thread\"} - a " + SessionFail(MGFatalFamily::ApplyThreadNotRunning, "MGPipe: Fatal{ApplyThreadNotRunning, \"EGL on the app thread\"} - a " "control frame reached RunSurfaceControlFrame with the server's backend " "built but no mgl-srv-apply thread running (ClientSession::Start failed " "after ServerLoop::CreateBackend). Dispatching it inline would make the " "context current on the APP thread - the split lane's whole premise. " "Refusing by name rather than falling back to monolith"); - std::abort(); } // Backend already gone (post-Stop teardown, or the pre-Init window): the dispatch's // null-backend arm would answer NOT_INITIALIZED anyway, so that is the honest result @@ -805,7 +803,7 @@ namespace MobileGL::MG_Remote::Server { // run on into the client's teardown, reading rings the client is about to unmap - // a use-after-free whose only symptom is an intermittent crash somewhere else. // Aborting here is red, immediate, and names the cause. - MGLOG_F("MGPipe: Fatal{ApplyThreadJoinTimeout} - mgl-srv-apply did not exit within " + SessionFail(MGFatalFamily::ApplyThreadJoinTimeout, "MGPipe: Fatal{ApplyThreadJoinTimeout} - mgl-srv-apply did not exit within " "%u ms of Stop(). Stop() published m_stopRequested (in the park predicate) " "BEFORE it rang, so a plain Notify should already have un-parked the thread; " "Doorbell::Kill() (Doorbell.h, CondVarDoorbell::Kill; table 3 step 2) is " @@ -814,7 +812,6 @@ namespace MobileGL::MG_Remote::Server { "lost, not slow. Aborting rather than detaching: a detached apply thread still " "owns the context and would read rings the client is about to unmap", kJoinTimeoutMs); - std::abort(); } m_thread.join(); // The identity is NOT cleared here any more: the thread clears it itself, under @@ -882,10 +879,9 @@ namespace MobileGL::MG_Remote::Server { MG_Backend::WindowBackend WindowBackendFromFrameValue(Int value) { if (value < static_cast(MG_Backend::WindowBackend::Unknown) || value >= static_cast(MG_Backend::WindowBackend::WindowBackendCount)) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.windowBackend\"} - a " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.windowBackend\"} - a " "control frame carried window backend tag %d, which names no " "WindowBackend", value); - std::abort(); } return static_cast(value); } @@ -1069,9 +1065,8 @@ namespace MobileGL::MG_Remote::Server { // A kind the switch does not know never legitimately posts (RunSurfaceControlFrame // refuses None), so reaching this line means the slot's bytes were not written by the // poster at all - the ring's Fatal{ProtocolCorruption} discipline, one channel over. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.kind\"} - the control slot held " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.kind\"} - the control slot held " "kind %d, which names no dispatchable op", static_cast(frame.kind)); - std::abort(); } // --------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 9bd84694..1e197679 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -9,6 +9,7 @@ // P5 package s1: the server half of a session. #include "ServerSession.h" +#include #include "../CapsCodec.h" #include "../Protocol/generated/protocol_generated.h" @@ -115,7 +116,7 @@ namespace MobileGL::MG_Remote::Server { // DynamicBackendParameters still carries SizeT, so a peer built from a different // tree hands over a caps block whose members are at different offsets and whose // bytes are all individually plausible. - MGLOG_F("MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s " + SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s " "theirBuild=%s - the two peers were not built from the same struct shapes, " "and there is no downgrade path: the caps block's size is ABI-dependent " "(MGPipeTypes.h:145-146) and every field past the first difference would be " @@ -123,7 +124,6 @@ namespace MobileGL::MG_Remote::Server { what, static_cast(ours), static_cast(theirs), ourStamp == nullptr ? "?" : ourStamp, theirStamp == nullptr ? "?" : theirStamp); - std::abort(); } // MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB, actually applied. @@ -164,7 +164,7 @@ namespace MobileGL::MG_Remote::Server { // SetCapabilityBits / SetConsumedSubsystems for why the one that used to be here was // the phase's marquee defect committed from the server's side. [[noreturn]] void FatalUnsetCallMask(Bool capBitsSet, Bool consumedSet) { - MGLOG_F("MGPipe: Fatal{UnsetCallMask} - %s%s%s was never set on this ServerSession, " + SessionFail(MGFatalFamily::UnsetCallMask, "MGPipe: Fatal{UnsetCallMask} - %s%s%s was never set on this ServerSession, " "and there is no default: a guessed consumer mask makes the client's R-8 " "liveness gates answer from a server-side fact the server never stated. The " "client would stop emitting whole record families, clear its dirty flags on " @@ -174,7 +174,6 @@ namespace MobileGL::MG_Remote::Server { capBitsSet ? "" : "SetCapabilityBits", (!capBitsSet && !consumedSet) ? " and " : "", consumedSet ? "" : "SetConsumedSubsystems"); - std::abort(); } // ---- P5c ev: the reverse channel's PRODUCER callbacks (CONTRACT-P5C §4.1) -------- @@ -192,12 +191,11 @@ namespace MobileGL::MG_Remote::Server { // a Reserve that returns nullptr is Fatal{EventRingOverflow}. CountDrop and the // eventRingFull latch stay built and stay unused-by-policy; P9 owns the policy. [[noreturn]] void FatalEventRingOverflow(const char* eventName, Uint64 payloadBytes) { - MGLOG_F("MGPipe: Fatal{EventRingOverflow} - the producer of %s could not reserve " + SessionFail(MGFatalFamily::EventRingOverflow, "MGPipe: Fatal{EventRingOverflow} - the producer of %s could not reserve " "%llu bytes on SEG_EVENT. P5c's events are lossless and a full ring under " "lockstep is a producer burst no measured workload has, so this is a " "defect, not a drop (CONTRACT-P5C §4.4; the drop policy is P9's)", eventName, static_cast(payloadBytes)); - std::abort(); } // ---- P5e (ra), CONTRACT-P5E §2.6: the ring stops being a Fatal and becomes a queue -- @@ -239,14 +237,13 @@ namespace MobileGL::MG_Remote::Server { // be a hang with a comment on it. const Uint64 cap = session.Events().Ring().MaxRecordBytes(); if (payloadBytes + sizeof(Transport::RingRecordHeader) > cap) { - MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s needs %llu bytes and SEG_EVENT " + SessionFail(MGFatalFamily::EventRingOverflow, "MGPipe: Fatal{EventRingOverflow} - %s needs %llu bytes and SEG_EVENT " "caps ONE record at %llu (half its capacity). Flow control cannot help: " "no amount of draining makes a record fit a ring that is too small for " "it. Raise MOBILEGL_IPC_EVENT_KB or slice the event at its producer, as " "the writeback path already does", eventName, static_cast(payloadBytes), static_cast(cap)); - std::abort(); } Transport::RingControl& control = session.Control(); @@ -261,12 +258,11 @@ namespace MobileGL::MG_Remote::Server { session.PublishEvents(); if (!drained() && !bell.Wait(control.consumerParked, drained, /*spinUs=*/0, kEventBacklogWaitMs)) { - MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s waited %u ms for the client " + SessionFail(MGFatalFamily::EventRingOverflow, "MGPipe: Fatal{EventRingOverflow} - %s waited %u ms for the client " "to drain SEG_EVENT and it never did. Under run-ahead the apply " "thread parks on this ring (CONTRACT-P5E §2.6); a client that does " "not drain is one that is not running", eventName, kEventBacklogWaitMs); - std::abort(); } slot = session.Events().Reserve(kind, payloadBytes); if (slot != nullptr) return slot; @@ -274,11 +270,10 @@ namespace MobileGL::MG_Remote::Server { // Two full rounds against a ring the record provably fits: something else is // producing into it, which on this side is impossible (one producer, the apply // thread, by construction). - MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s could not reserve %llu bytes on an " + SessionFail(MGFatalFamily::EventRingOverflow, "MGPipe: Fatal{EventRingOverflow} - %s could not reserve %llu bytes on an " "emptied SEG_EVENT. The apply thread is SEG_EVENT's only producer, so a " "drained ring that still refuses a record it fits is a corrupt cursor set", eventName, static_cast(payloadBytes)); - std::abort(); } // Accept owns the single process callback table. Its owner need not be the @@ -286,9 +281,8 @@ namespace MobileGL::MG_Remote::Server { ServerSession& ReverseCallbackOwner(const char* callback) { auto* session = ServerSession::Active(); if (session == nullptr) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"%s.session-missing\"} - " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"%s.session-missing\"} - " "a reverse callback has no accepted session owner", callback); - std::abort(); } return *session; } @@ -300,12 +294,11 @@ namespace MobileGL::MG_Remote::Server { // The backend handed over a segment-tagged blobref. The ONLY legal shape at // this boundary is the monolith one - Offset is the mapped address, valid // for this call - because the segment copy is THIS function's own job. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Seg\"} - the " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Seg\"} - the " "writeback producer was handed Seg %u; at the backend boundary the " "blobref names the backend's own mapped bytes (Seg = " "kMGHostSpanSegNone) and the copy into SEG_EVENT is the producer's", bytes.Seg); - std::abort(); } const Uint64 payloadBytes = sizeof(Transport::EventBufferWritebackHead) + bytes.Size; void* slot = ReserveEventOrBlock(session, Transport::kEventBufferWriteback, @@ -375,11 +368,10 @@ namespace MobileGL::MG_Remote::Server { template void InstallReverseCallback(Fn& entry, Fn producer) { if (entry != nullptr && entry != producer) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - a reverse " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - a reverse " "MGPipeCallbacks entry is already claimed by a different function. With " "an active transport the four reverse entries are the server " "session's producers; the client installs them under monolith only"); - std::abort(); } entry = producer; } @@ -442,9 +434,8 @@ namespace MobileGL::MG_Remote::Server { if (!g_sessionOwner.compare_exchange_strong(expectedOwner, this, std::memory_order_acq_rel, std::memory_order_acquire)) { if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT; - MGLOG_F("MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another " + SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another " "ServerSession already owns or is accepting the process reverse channel"); - std::abort(); } struct ReleaseFailedClaim { ServerSession* self; @@ -720,9 +711,8 @@ namespace MobileGL::MG_Remote::Server { Transport::RingControl& ServerSession::Control() { Transport::RingControl* control = m_shm.CmdControl(); if (control == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::Control\"} - the control " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::Control\"} - the control " "page does not exist until Accept() has mapped SEG_CMD"); - std::abort(); } return *control; } @@ -746,9 +736,8 @@ namespace MobileGL::MG_Remote::Server { return *m_externalConsumerBell; } if (m_transport == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no " "transport; Accept() has not run"); - std::abort(); } if (m_transport->Role() == Transport::TransportRole::InProcess) { return static_cast(m_transport)->SelfDoorbell(); @@ -756,10 +745,9 @@ namespace MobileGL::MG_Remote::Server { // P6: SocketTransport's pair. The accessors stay off ITransport by ruling (contract // §3.9) precisely so that this stays one switch in one file rather than two virtuals // every transport has to invent a home for. - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ConsumerDoorbell\"} - transport " + SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ConsumerDoorbell\"} - transport " "role %u has no doorbell pair yet; that is P6's SocketTransport", static_cast(m_transport->Role())); - std::abort(); } Transport::Doorbell& ServerSession::ProducerDoorbell() { @@ -767,17 +755,15 @@ namespace MobileGL::MG_Remote::Server { return *m_externalProducerBell; } if (m_transport == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ProducerDoorbell\"} - no " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ProducerDoorbell\"} - no " "transport; Accept() has not run"); - std::abort(); } if (m_transport->Role() == Transport::TransportRole::InProcess) { return static_cast(m_transport)->PeerDoorbell(); } - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ProducerDoorbell\"} - transport " + SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ProducerDoorbell\"} - transport " "role %u has no doorbell pair yet; that is P6's SocketTransport", static_cast(m_transport->Role())); - std::abort(); } Transport::SessionSegments& ServerSession::Shm() { return m_shm; } diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 708acfc4..d0b2224e 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -27,6 +27,7 @@ // someone who reads the comment and not this file. #include "PipeWireCodec.h" +#include #include #include @@ -375,14 +376,12 @@ namespace MobileGL::MG_Remote::Wire { // --------------------------------------------------------------------------------- void WireProtocolFatal(const char* what, const char* detail) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : ""); - std::abort(); + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : ""); } void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what, + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what, static_cast(got), static_cast(expected)); - std::abort(); } // --------------------------------------------------------------------------------- @@ -458,20 +457,18 @@ namespace MobileGL::MG_Remote::Wire { // The monolith shape: Seg = None, Offset = a host address, Size = 0. Legal // there, and precisely what must not cross (rule A). Reading it as "absent" // would silently drop the bytes of every record an unconverted emitter sent. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} a blob with Size 0 " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} a blob with Size 0 " "declares Seg=%u Offset=%llu; under split a blobref is either fully " "declared or all three fields zero", WireOpName(op), static_cast(blob.Seg), static_cast(blob.Offset)); - std::abort(); } return; } if (blob.Seg == kSegNone) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} Size=%llu with no segment " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} Size=%llu with no segment " "(R-2.3): a non-zero Size must name a real SEG_*", WireOpName(op), static_cast(blob.Size)); - std::abort(); } // R-2.3's SECOND HALF: "inside SOME segment" IS NOT THE RULE. Contract table 1 gives // every client->server content blob - groups A, B and C, all nineteen rows - the ONE @@ -493,7 +490,7 @@ namespace MobileGL::MG_Remote::Wire { // seventh name would be a token nothing else in the tree recognises. The SEGMENT is in // the message, which is what has to be greppable. if (blob.Seg != kSegStage) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu " "size=%llu is not SEG_STAGE(%u); every client->server content blob is " "staged whole in SEG_STAGE (contract table 1 groups A/B/C, R-10) and no " "other segment may carry one", @@ -501,24 +498,21 @@ namespace MobileGL::MG_Remote::Wire { static_cast(blob.Offset), static_cast(blob.Size), static_cast(kSegStage)); - std::abort(); } if (segments.Resolve(blob.Seg, blob.Offset, blob.Size) == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu " "does not lie inside that segment (R-2.3)", WireOpName(op), static_cast(blob.Seg), static_cast(blob.Offset), static_cast(blob.Size)); - std::abort(); } } void RequireDeclaredBlob(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) { if (blob.Size == 0) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} the record's own fields say " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} the record's own fields say " "it carries content and its blob declares none (R-2.2 / rule A)", WireOpName(op)); - std::abort(); } CheckBlobIsHonest(op, blob, segments); } @@ -554,12 +548,11 @@ namespace MobileGL::MG_Remote::Wire { // resolver, reads outside the segment. P5 emits no spans, so this was latent; P8 arms // it, which is exactly when nobody will be reading this code. if (segments.Resolve(span.Seg, span.Offset, span.Size) == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"host-span\"} seg=%u offset=%llu " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"host-span\"} seg=%u offset=%llu " "size=%llu does not lie inside that segment (R-2.3 arm 4)", static_cast(span.Seg), static_cast(span.Offset), static_cast(span.Size)); - std::abort(); } } @@ -855,14 +848,13 @@ namespace MobileGL::MG_Remote::Wire { const Uint64 need = Align8(size); if (need > m_stageCapacity) { - MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob cannot fit a " + SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob cannot fit a " "%llu byte staging segment at any occupancy; a blob is staged whole, so a " "row that cuts its content at MGPipeStageChunkBytes() never reaches this - " "raise MOBILEGL_IPC_STAGE_MB or report the record type with no cut to the " "integrator", static_cast(size), static_cast(m_stageCapacity)); - std::abort(); } bool reclaimed = false; @@ -907,15 +899,14 @@ namespace MobileGL::MG_Remote::Wire { // therefore no reverse channel to drain. if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf); if (!ready() && !m_stageRetirementBell->Wait(m_control->producerParked, ready, 0, 5000)) { - MGLOG_F("MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} producer wait " + SessionFail(MGFatalFamily::RetirementWaitFailed, "MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} producer wait " "ended before the pending allocation retired (shutdown or timeout)"); - std::abort(); } if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf); } ReclaimStagedBytes(); } - MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob does not fit a %llu " + SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob does not fit a %llu " "byte staging segment with %llu bytes still in flight (retiredSeq=%llu); a " "blob is staged whole and only the rows that cut at the stage chunk budget " "stay small - raise MOBILEGL_IPC_STAGE_MB or report that row to the integrator", @@ -924,7 +915,6 @@ namespace MobileGL::MG_Remote::Wire { static_cast(m_stageHead - m_stageTail), static_cast( m_control != nullptr ? m_control->Progress.retiredSeq.load(std::memory_order_acquire) : 0)); - std::abort(); } MGPBlobRef PipeWireEncoder::StageBytes(const void* bytes, Uint64 size) { @@ -1020,14 +1010,13 @@ namespace MobileGL::MG_Remote::Wire { if (total > m_cmd->MaxRecordBytes()) { // R-10's record bound. The content rows cut their BLOBS at the stage chunk budget // (MGPipeStageChunkBytes); a record's own bytes have no such budget and die here. - MGLOG_F("MGPipe: Fatal{RingOverrun, \"%s\"} a %llu byte record exceeds " + SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"%s\"} a %llu byte record exceeds " "RingProducer::MaxRecordBytes() == %llu (half of a %llu byte SEG_CMD); the stage chunk " "budget cuts blobs, not a record's own bytes - report the row to the " "integrator", WireOpName(op), static_cast(total), static_cast(m_cmd->MaxRecordBytes()), static_cast(m_cmd->Capacity())); - std::abort(); } // MGPipeCallFlags -> RingRecordFlags. See the file header: these are two flag spaces @@ -1131,13 +1120,12 @@ namespace MobileGL::MG_Remote::Wire { // here and a Fatal on a peer - exactly the asymmetry EncodeRecord's own // comment says it exists to prevent. if (op == MGPWireOp::CreateShaderState && i < 6 && blob.Size != 0) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} " "declares %llu bytes at the ENCODER; under split the modules travel " "inside the Reflection archive and the six per-stage runs stay " "undeclared", static_cast(i), static_cast(blob.Size)); - std::abort(); } } } @@ -1347,11 +1335,10 @@ namespace MobileGL::MG_Remote::Wire { // then fails for ever: the barrier HANGS rather than returning something wrong, which // is the harder failure to diagnose of the two. if ((MGPipeCallFlagsFor(op) & static_cast(kReplySlot)) == 0) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} the decoder tried to answer into " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the decoder tried to answer into " "a reply slot for a call the catalogue gives no kReplySlot; s1 sizes " "ReplyPool from MGPipeCallFlagsFor and reserved none", WireOpName(op)); - std::abort(); } if (m_replies != nullptr) { m_replies->PostReply(seq, status, bytes, size); @@ -1376,23 +1363,21 @@ namespace MobileGL::MG_Remote::Wire { // the audit has stopped covering the carrier, which is the same failure as no audit at // all - the reason the run-count overflow just below is a Fatal too. if (blob.Size == 0 || blob.Seg != kSegStage) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} the audit was asked to record a " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the audit was asked to record a " "resolved run with seg=%u size=%llu; only declared SEG_STAGE(%u) runs " "reach the poison fill (R-2.5)", WireOpName(op), static_cast(blob.Seg), static_cast(blob.Size), static_cast(kSegStage)); - std::abort(); } if (m_resolvedCount >= sizeof(m_resolved) / sizeof(m_resolved[0])) { // LOUD, NOT A SILENT DROP. This array is what the 0xDD fill covers, and a poison // that quietly stopped covering a run is the same failure as no poison at all - // rule C's only mechanical control going dark without a line in the log. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} more than %llu SEG_STAGE runs in " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} more than %llu SEG_STAGE runs in " "one record; the audit fill would stop covering them", WireOpName(op), static_cast(sizeof(m_resolved) / sizeof(m_resolved[0]))); - std::abort(); } m_resolved[m_resolvedCount++] = blob; } @@ -1508,7 +1493,7 @@ namespace MobileGL::MG_Remote::Wire { // record type)`; it CANNOT SEE THE TAIL, so this is the first thing that holds a // record declaring Count = 4000 while carrying 8 bytes to its own arithmetic. if (size != layout.TotalBytes) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and " "its own count fields describe %llu bytes (fixed payload %llu + tails " "%llu/%llu/%llu)", WireOpName(op), static_cast(size), @@ -1517,7 +1502,6 @@ namespace MobileGL::MG_Remote::Wire { static_cast(layout.TailBytes[0]), static_cast(layout.TailBytes[1]), static_cast(layout.TailBytes[2])); - std::abort(); } // The record's own ordinal, which is its reply-slot id (R-3). 1-based, and m_applySeq @@ -1791,12 +1775,11 @@ namespace MobileGL::MG_Remote::Wire { for (Uint32 i = 0; i < 6; ++i) { CheckBlobIsHonest(op, desc.Spirv[i], *m_segments); if (desc.Spirv[i].Size != 0) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} " "declares %llu bytes; under split the modules travel inside the " "Reflection archive and the six per-stage runs stay undeclared", static_cast(i), static_cast(desc.Spirv[i].Size)); - std::abort(); } } const void* archiveBytes = ResolveOrFatal(op, desc.Reflection); @@ -2067,11 +2050,10 @@ namespace MobileGL::MG_Remote::Wire { const Bool boxIsWhole = rec.UnionBox.W != 0 && rec.UnionBox.H != 0 && rec.UnionBox.D != 0; if (!namesABuffer && !boxIsEmpty && !boxIsWhole) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ResourceSubData\"} the union box " + SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ResourceSubData\"} the union box " "%ux%ux%u has a zero extent on some axes and not others; a box is " "either empty or whole", rec.UnionBox.W, rec.UnionBox.H, rec.UnionBox.D); - std::abort(); } const Bool carriesContent = namesABuffer ? MGPipeSubDataBufferSize(rec) != 0 : (boxIsWhole || rec.RegionCount != 0); diff --git a/scripts/ci/fatal_census.py b/scripts/ci/fatal_census.py index 5c990140..8a68bbc9 100644 --- a/scripts/ci/fatal_census.py +++ b/scripts/ci/fatal_census.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 """The Fatal census gate (CONTRACT-P6 5.2, exit gate S7). -WHAT IT REFUSES: a `std::abort()` under MG_Remote/ that carries no family word. +WHAT IT REFUSES: a `std::abort()` under MG_Remote/ that carries no family word, and a +SessionFail / WireLogFatal call whose string forgot its family. -S7 says "a bare abort() added outside Session::Fail -> the census gate goes red". Session::Fail -does not exist yet - it is the 94-site funnel 5.2 describes, and this file is its precondition -rather than its replacement. What CAN be checked today is the property that funnel exists to -guarantee and that every consumer already depends on: every death names a FAMILY, in a -`Fatal{Word...}` marker, on a line a grep can find. +S7: "a bare abort() added outside Session::Fail -> the census gate goes red". Session::Fail now +EXISTS - it is SessionFail() in FatalFunnel.cpp, and the ~90 scattered aborts route through it, +so a bare std::abort() under MG_Remote/ is now the exception this gate refuses rather than the +rule. Four aborts remain, all inside the two funnels (WireLog.cpp, FatalFunnel.cpp), and both +files are exempt because their callers carry the family word. The property this enforces is the +one every downstream consumer depends on: every death names a FAMILY, in a `Fatal{Word...}` +marker, on a line a grep can find. That is not as weak as it sounds. a6's census found the vocabulary had drifted to 30 family words against a 7-value wire FatalCode, and found two aborts carrying no marker at all - so "the @@ -44,6 +47,11 @@ BASELINE = Path(__file__).with_name("fatal_census_baseline.json") FUNNEL_FILES = { "MobileGL/MG_Remote/Transport/WireLog.cpp", "MobileGL/MG_Remote/Transport/WireLog.h", + # P6 dl: Session::Fail's funnel. Its abort is the sanctioned one every SessionFail reaches, + # exactly as WireLog.cpp's is for WireLogFatal; its own file also defines the SessionFail + # call, so it is exempt from both rules for the same reason WireLog is. + "MobileGL/MG_Remote/FatalFunnel.cpp", + "MobileGL/MG_Remote/FatalFunnel.h", } # Twelve lines: long enough for a wrapped MGLOG_F argument list (the longest in the tree runs to @@ -54,6 +62,10 @@ kMarkerWindow = 12 MARKER = re.compile(r"Fatal\{([A-Za-z][A-Za-z0-9]*)") ABORT = re.compile(r"\bstd::abort\(\)") 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*\(") def strip_comments(text): @@ -136,10 +148,10 @@ def census(): families.add(word) if rel not in FUNNEL_FILES: - # Rule 2: every WireLogFatal call carries a family word. The call may wrap, so the + # Rule 2: every WireLogFatal / SessionFail call carries a family word in its string. # window covers its continuation lines. for index, line in enumerate(lines): - if not WIRE_LOG_FATAL_CALL.search(line): + if not (WIRE_LOG_FATAL_CALL.search(line) or SESSION_FAIL_CALL.search(line)): continue window = "\n".join(lines[index:index + kMarkerWindow]) if not MARKER.search(window): diff --git a/scripts/ci/fatal_census_baseline.json b/scripts/ci/fatal_census_baseline.json index ff87912b..b8eee28a 100644 --- a/scripts/ci/fatal_census_baseline.json +++ b/scripts/ci/fatal_census_baseline.json @@ -1,5 +1,5 @@ { - "abort_sites": 93, + "abort_sites": 4, "families": [ "AbiMismatch", "ApplyThreadJoinTimeout",