[MG_Remote, MG_Test, ci] (P7): peer-byte faults latch the session child and decline the rest; inproc keeps Fatal (PH-1 (3))

This commit is contained in:
2026-09-23 13:44:37 -04:00
parent caaedd7abe
commit d681d9b1bf
13 changed files with 576 additions and 190 deletions
+2
View File
@@ -209,6 +209,8 @@ namespace MobileGL::MG_Remote::Client {
RequireClientTablesInstalled(slot);
ClientSession* session = ClientSession::Active();
if (session == nullptr) {
// @Ph-declined (ID-P7-1): returns ClientSession& and runs in the CLIENT - no
// session to hand back, no peer bytes, and the latch is a server-session idea.
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",
+2
View File
@@ -115,6 +115,8 @@ namespace MobileGL::MG_Remote::Client {
RequireClientTablesInstalled(row);
ClientSession* session = ClientSession::Active();
if (session == nullptr) {
// @Ph-declined (ID-P7-1): returns ClientSession& and runs in the CLIENT - no
// session to hand back, no peer bytes, and the latch is a server-session idea.
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",
+104 -14
View File
@@ -18,6 +18,7 @@
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <flatbuffers/flatbuffers.h>
@@ -67,29 +68,119 @@ namespace MobileGL::MG_Remote {
MG_Pipe::MGPipeInstallSessionFailHook(&PipeSessionFailAdapter);
}
void SessionFail(MGFatalFamily family, const char* fmt, ...) {
namespace {
// 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.
void FormatFaultLine(char (&line)[512], const char* fmt, va_list args) {
const int written = std::vsnprintf(line, sizeof(line), fmt, args);
if (written < 0) {
std::snprintf(line, sizeof(line),
"MG_Remote: unformattable Fatal diagnostic (format=%s)", fmt);
}
}
// The log line and its stderr echo: 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.
void LogFaultLine(const char* line) {
MGLOG_F("%s", line);
std::fputs(line, stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
}
// The SessionFault frame to whichever peer this process has; defined below SessionFail,
// whose body it was until PH-1 (3) gave the latch a second caller.
void PublishSessionFault(MGFatalFamily family, const char* line);
// PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the
// first fault. The line is written under the mutex BEFORE the release store that
// publishes `g_latched`, so a reader that saw true reads a whole line.
std::atomic<bool> g_latchArmed{false};
std::atomic<bool> g_latched{false};
std::atomic<std::uint64_t> g_latchCount{0};
std::mutex g_latchMutex;
MGFatalFamily g_latchedFamily = MGFatalFamily::ProtocolCorruption;
char g_latchedLine[512] = {};
} // namespace
void ArmSessionLatch() { g_latchArmed.store(true, std::memory_order_release); }
bool SessionLatchArmed() { return g_latchArmed.load(std::memory_order_acquire); }
bool SessionLatched() { return g_latched.load(std::memory_order_acquire); }
MGFatalFamily SessionLatchedFamily() {
const std::lock_guard<std::mutex> lock(g_latchMutex);
return g_latchedFamily;
}
const char* SessionLatchedLine() { return g_latchedLine; }
std::uint64_t SessionLatchCount() { return g_latchCount.load(std::memory_order_relaxed); }
bool SessionLatch(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
const int written = std::vsnprintf(line, sizeof(line), fmt, args);
FormatFaultLine(line, fmt, args);
va_end(args);
if (written < 0) {
std::snprintf(line, sizeof(line),
"MG_Remote: unformattable Fatal diagnostic (format=%s)", fmt);
// UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit
// death test keep the death they had (FatalFunnel.h's block says why inproc must).
if (!g_latchArmed.load(std::memory_order_acquire)) {
SessionFail(family, "%s", line);
}
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
g_latchCount.fetch_add(1, std::memory_order_relaxed);
LogFaultLine(line);
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);
bool first = false;
{
const std::lock_guard<std::mutex> lock(g_latchMutex);
if (!g_latched.load(std::memory_order_relaxed)) {
std::snprintf(g_latchedLine, sizeof(g_latchedLine), "%s", line);
g_latchedFamily = family;
g_latched.store(true, std::memory_order_release);
first = true;
}
}
if (!first) {
// A second fault after the first one latched: the session is already declining, so
// this is logged and counted but it neither re-publishes nor replaces the cause.
MGLOG_E("MG_Remote server: SessionLatch - a further named fault after the latched one "
"(%s); it is declined with the rest of the session",
FatalFamilyName(family));
return false;
}
// ONE LINE THAT SAYS WHAT HAPPENS NEXT, so a log reader can tell a latched end from a
// crash without the supervisor's reap line.
MGLOG_E("MG_Remote server: SessionLatch{%s} - the first named fault of this session is "
"latched; every further record and control op is declined and the session "
"closes (exit %d) for the supervisor to serve the next connection (PH-1, "
"ID-P7-1)",
FatalFamilyName(family), kSessionLatchedExitCode);
PublishSessionFault(family, line);
return false;
}
void SessionFail(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
FormatFaultLine(line, fmt, args);
va_end(args);
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
LogFaultLine(line);
PublishSessionFault(family, line);
std::abort();
}
namespace {
void PublishSessionFault(MGFatalFamily family, const char* line) {
// A SessionFault frame to the peer, BEFORE the abort, so the other side's log names the
// family instead of reading a bare EOF (5.2). Best-effort by nature: this thread is about
// to die, so a send that blocks or fails changes nothing it could have changed anyway.
@@ -123,8 +214,7 @@ namespace MobileGL::MG_Remote {
(void)control->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
}
std::abort();
}
} // namespace
} // namespace MobileGL::MG_Remote
+54 -1
View File
@@ -44,9 +44,62 @@ namespace MobileGL::MG_Remote {
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.
// this is zero over a whole good run; a test reads it to prove the funnel was reached. A
// latched fault (SessionLatch below) counts here too: it ends the session just as surely.
::std::uint64_t SessionFaultCount();
// ---- PH-1 (3), ID-P7-1: THE PER-SESSION LATCH ---------------------------------------
//
// WHAT IT IS. At a site whose input bytes the PEER wrote, and which already sits in a
// function that can say "no" (Bool / MobileGLResult), a named fault no longer has to kill the
// process: SessionLatch logs the SAME line SessionFail would (family word and all, so every
// `Fatal{` grep, the census and the SessionFault frame read exactly as before), publishes the
// SessionFault to the peer, records the FIRST fault (family + line), and RETURNS false. The
// site returns failure, the decoder declines, ServerLoop::DrainRing stops applying at its
// next check, the apply thread leaves its loop, and ServerMain::RunSession closes the session
// cleanly and exits kSessionLatchedExitCode - a named, counted end the supervisor reaps and
// then serves the next connection, exactly as P6.5's fork-per-session already does for a
// crash (ID-P7-1: "下一连接照常服务" is the supervisor's, not the latch's).
//
// WHERE IT IS ARMED, AND WHY ONLY THERE. ArmSessionLatch() is called by exactly one site:
// ServerMain::RunSession, i.e. the spawn / TCP session child (with or without --serve), whose
// process IS the session. Unarmed, SessionLatch IS SessionFail - same line, same abort - and
// that is the answer everywhere else, deliberately:
// * INPROC keeps Fatal. CONTRACT-P5's R-2 arms (rules A/B/C, the four honesty arms) are
// Fatal{ProtocolCorruption} in the formal wording, and under inproc the client and the
// server share one process: a latched-but-alive server would leave the client's GL thread
// parked on a barrier in the SAME process with no EOF to wake it, and there is no
// supervisor to serve a "next connection" - the process is the session. So the latch
// changes nothing inproc and every death test stays a death test.
// * THE CLIENT never arms it: a client-side arm of a shared helper (the encoder's honesty
// pass) is a local bug, not a peer's bytes.
// The census (scripts/ci/fatal_census.py rule 2) checks SessionLatch calls the way it checks
// SessionFail calls: the string must carry its `Fatal{Word` and the word must have a .def row.
void ArmSessionLatch();
bool SessionLatchArmed();
// True once the first fault has latched. One acquire load: DrainRing asks it per record.
bool SessionLatched();
// The first latched fault - its family and its line, verbatim. Meaningful only once
// SessionLatched() is true; the line's storage lives for the process.
MGFatalFamily SessionLatchedFamily();
const char* SessionLatchedLine();
// Every fault this armed process latched or declined after the first (>= 1 once latched).
::std::uint64_t SessionLatchCount();
// The session child's exit status when its session ended on a latched fault. Distinct from
// RunSession's other codes (64-74) so the supervisor's reap line names it; nonzero, so it is
// counted in `sessionsFaulted` like every other fault.
inline constexpr int kSessionLatchedExitCode = 75;
// LATCH-OR-DIE. Armed: log + stderr echo + SessionFault (first fault only) + count, then
// return false, so a Bool site reads `return SessionLatch(...);`. Unarmed: SessionFail with
// the identical line (does not return).
bool
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
SessionLatch(MGFatalFamily family, const char* fmt, ...);
// 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
+15 -3
View File
@@ -205,28 +205,40 @@ namespace MobileGL::MG_Remote {
}
MobileGLResult ServerApplyWireSurfaceOp(const ::MobileGL::Wire::SurfaceOp& op, SurfaceControlFrame* replyOut) {
// PH-1 (3), ID-P7-1: a latched session answers no further control op. RunSession stops
// reading the control connection once it sees the latch; this covers the op it may
// already be holding.
if (SessionLatched()) return MOBILEGL_ERR_PROTOCOL_MISMATCH;
SurfaceControlFrame frame;
const SurfaceWireError error = DecodeWireSurfaceOp(op, &frame);
if (error != SurfaceWireError::None) {
// PH-1 (3): the op's bytes are the peer's. Each of the three refusals latches in an
// armed session child and the op is answered with no dispatch; unarmed they die.
if (error == SurfaceWireError::AndroidNativeWindowArrived) {
SessionFail(MGFatalFamily::UnmigratedSurface,
(void)SessionLatch(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) {
SessionFail(MGFatalFamily::UnmigratedSurface,
(void)SessionLatch(MGFatalFamily::UnmigratedSurface,
"MGPipe: Fatal{UnmigratedSurface, \"MetalLayer@P12\"} - a wire "
"SurfaceOp named a CAMetalLayer* in the CLIENT's process. "
"Real window arrival is P12");
} else {
SessionFail(MGFatalFamily::ProtocolCorruption,
(void)SessionLatch(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp\"} - a wire surface op "
"failed validation: %s (wire kind %u, window kind %u)",
SurfaceWireErrorName(error), static_cast<unsigned>(op.kind()),
static_cast<unsigned>(op.windowKind()));
}
if (replyOut != nullptr) {
*replyOut = SurfaceControlFrame{};
replyOut->seq = op.seq();
replyOut->ok = false;
}
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
const MobileGLResult rc = Server::ServerLoopInstance().RunSurfaceControlFrame(frame);
if (replyOut != nullptr) *replyOut = frame;
+30 -19
View File
@@ -341,7 +341,9 @@ 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()) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead "
// PH-1 (3): latches in an armed session child - no credit is returned for it, the
// session closes and the peer's credit wait ends on the hang-up.
return SessionLatch(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");
@@ -502,8 +504,11 @@ namespace MobileGL::MG_Remote::Server {
// slot flipped on the client ahead of its server half aborts BY NAME on the apply thread
// 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) {
SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"} (server sink: the record crossed and "
//
// PH-1 (3): the shape is the PEER's (a multi-draw record it chose to send), so in an armed
// session child the refusal latches and the verb returns false; unarmed it still dies.
static Bool ServerUnmigratedVerbLatch(const char* slot) {
return SessionLatch(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);
}
@@ -567,13 +572,16 @@ namespace MobileGL::MG_Remote::Server {
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
// (a narrowed window carries a non-zero count that is merely too small). The moment
// either set has been received, the rule binds.
void CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return;
//
// PH-1 (3): both windows are the PEER's records, so the refusals latch in an armed
// session child (false = latched, the verb declines); unarmed they still die.
Bool CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return true;
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
if (maxTouched < 0) return;
if (maxTouched < 0) return true;
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
return SessionLatch(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",
@@ -581,13 +589,14 @@ namespace MobileGL::MG_Remote::Server {
st.SamplerViewCount);
}
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
return SessionLatch(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<int>(maxTouched), required, st.SamplerStateStart,
st.SamplerStateCount);
}
return true;
}
} // namespace
#endif
@@ -596,8 +605,8 @@ namespace MobileGL::MG_Remote::Server {
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) {
if (userIndices != nullptr) {
Wire::CheckDrawUserIndices(info, ranges, *userIndices);
if (userIndices != nullptr && !Wire::CheckDrawUserIndices(info, ranges, *userIndices)) {
return false; // PH-1 (3): latched (armed session child); unarmed it died inside
}
// The witness first, before the backend is consulted, so a unit process with no
// backend object still sees the wire's fields (PipeApplier.h LastDraw).
@@ -617,7 +626,7 @@ namespace MobileGL::MG_Remote::Server {
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
// asked to resolve anything out of the windows.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo");
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo")) return false;
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false;
@@ -694,12 +703,12 @@ namespace MobileGL::MG_Remote::Server {
// ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) --------------
if (info.NumDraws != 1) {
if (userIndices != nullptr) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
: "MultiDrawElements+CLIENT_INDICES");
return ServerUnmigratedVerbLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
: "MultiDrawElements+CLIENT_INDICES");
}
if (instanced) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
: "MultiDrawElements+INSTANCED");
return ServerUnmigratedVerbLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
: "MultiDrawElements+INSTANCED");
}
const auto n = static_cast<SizeT>(info.NumDraws);
m_multiCounts.resize(n);
@@ -840,7 +849,7 @@ namespace MobileGL::MG_Remote::Server {
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid");
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid")) return false;
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
if (table == nullptr) return false;
@@ -1164,8 +1173,9 @@ namespace MobileGL::MG_Remote::Server {
// session, so the only legal sequence is 0, 1, 2, ... and the session's own count of
// 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.
// PH-1 (3): latches in an armed session child (the peer wrote the serial); dies unarmed.
if (reset.ContextSerial != m_applierResetSerial) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the "
return SessionLatch(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)",
@@ -1186,13 +1196,14 @@ namespace MobileGL::MG_Remote::Server {
// §1's zero ruling: a null handle means "the object never crossed", and the client
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
// not a no-op.
// PH-1 (3): both refusals latch in an armed session child; unarmed they die as before.
if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null "
return SessionLatch(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)");
}
if (death.Kind >= static_cast<Uint32>(MG_Pipe::MGPipeKind::KindCount)) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
"MGPipeKind",
static_cast<unsigned>(death.Kind));
}
+57 -13
View File
@@ -426,6 +426,15 @@ namespace MobileGL::MG_Remote::Server {
if (stopOrForfeit()) break;
if (signals.EventRingFull->load(std::memory_order_acquire) == 0) DrainRing();
if (stopOrForfeit()) break;
// PH-1 (3): a latched session applies nothing more, so this thread leaves the loop
// (and does not spin on a ring whose head is still ahead of a tail DrainRing will no
// longer move). The exit path below tears the backend down as for a Stop; the
// control connection's owner (ServerMain::RunSession) sees the latch and closes.
if (SessionLatched()) {
MGLOG_E("MG_Remote server: mgl-srv-apply leaves its loop - the session latched a "
"named fault and declines the rest of the ring (PH-1)");
break;
}
if (ready()) continue;
const Uint64 waits = m_parks.fetch_add(1, std::memory_order_acq_rel) + 1;
@@ -627,11 +636,18 @@ namespace MobileGL::MG_Remote::Server {
// the consumer's own m_localTail (Ring.cpp:222-232) - no mutex, no steady_clock, no
// allocation; `applied == 0` then skips the retire block below entirely. Keep it that
// way: anything added here runs ~900 times a frame doing nothing.
//
// PH-1 (3), ID-P7-1: THE LATCH CHECK, AT THE TOP AND BEFORE EVERY POP. Once a named fault
// has latched (an armed spawn / TCP session child only - unarmed this is one acquire load
// of a flag nobody sets), no further record is applied: the rest of the ring is declined
// unread, the apply thread leaves its loop, and RunSession closes the session by name.
if (SessionLatched()) return 0;
ServerSession& session = *m_session;
Transport::SessionConsumer& consumer = session.Consumer();
PipeApplier& applier = session.Applier();
Uint64 applied = 0;
for (;;) {
if (SessionLatched()) break;
bool corrupt = false;
const bool popped = consumer.ApplyOne(
[this, &applier](const Transport::RingRecordView& record) {
@@ -654,10 +670,13 @@ namespace MobileGL::MG_Remote::Server {
if (corrupt) {
// 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.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SEG_CMD record header\"} - the "
// for ever; skipping desynchronises seq, and seq IS the reply-slot id. PH-1 (3):
// the header is the peer's bytes, so an armed session child latches here and
// stops reading the ring - neither a retry nor a skip.
(void)SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SEG_CMD record header\"} - the "
"consumer refused a record header at applied seq %llu",
static_cast<unsigned long long>(consumer.AppliedSeq()));
break;
}
if (!popped) break;
++applied;
@@ -672,12 +691,15 @@ 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()) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"appliedSeq batched\"} - the "
// PH-1 (3): latched, and nothing is retired against a watermark this side no
// longer believes.
(void)SessionLatch(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<unsigned long long>(consumer.AppliedSeq()),
static_cast<unsigned long long>(applier.DecoderAppliedSeq()));
return applied;
}
// RETIRE. MANDATORY, not optional: RingProducer::FreeBytes() reclaims against
// retiredTail only, and w1's SEG_STAGE linear allocator reclaims on retiredSeq - so
@@ -917,23 +939,30 @@ namespace MobileGL::MG_Remote::Server {
// WindowHandle, so an out-of-range tag is a memory-corruption shape, not bad input -
// same discipline as the ring's Fatal{ProtocolCorruption} on a header the producer could
// not have written.
MG_Backend::WindowBackend WindowBackendFromFrameValue(Int value) {
//
// PH-1 (3): false = the refusal latched (an armed spawn / TCP session child, where the
// frame's values came off the wire through SurfaceOpCodec). Under spawn the codec has
// already mapped the wire WindowKind onto a legal tag, so this arm is defence in depth
// there; unarmed it still dies.
Bool WindowBackendFromFrameValue(Int value, MG_Backend::WindowBackend* out) {
if (value < static_cast<Int>(MG_Backend::WindowBackend::Unknown) ||
value >= static_cast<Int>(MG_Backend::WindowBackend::WindowBackendCount)) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.windowBackend\"} - a "
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.windowBackend\"} - a "
"control frame carried window backend tag %d, which names no "
"WindowBackend", value);
}
return static_cast<MG_Backend::WindowBackend>(value);
*out = static_cast<MG_Backend::WindowBackend>(value);
return true;
}
MG_Backend::WindowHandle UnpackWindowHandle(const SurfaceControlFrame& frame) {
Bool UnpackWindowHandle(const SurfaceControlFrame& frame, MG_Backend::WindowHandle* out) {
MG_Backend::WindowHandle handle;
handle.Backend = WindowBackendFromFrameValue(frame.windowBackend);
if (!WindowBackendFromFrameValue(frame.windowBackend, &handle.Backend)) return false;
handle.Handle = HandleFromToken<void*>(frame.nativeToken);
handle.Width = static_cast<Uint32>(frame.width);
handle.Height = static_cast<Uint32>(frame.height);
return handle;
*out = handle;
return true;
}
// P7 CI. THE TEST LEVER FOR THE CLIENT'S COLD-START BUDGET (ClientSession.cpp,
@@ -1003,8 +1032,13 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_OK;
}
case SurfaceControlOp::CreateWindowSurface: {
MG_Backend::WindowHandle window;
if (!UnpackWindowHandle(frame, &window)) {
frame.ok = false;
return MOBILEGL_ERR_PROTOCOL_MISMATCH; // PH-1 (3): latched
}
frame.ok = backend->CreateEGLWindowSurface(HandleFromToken<EGLSurface>(frame.surface),
UnpackWindowHandle(frame));
window);
// N-3: BackendObject_DirectGLES destroys and recreates the native context to create
// a DIFFERENT surface, so whatever tuple was bound names a dead context.
if (frame.ok) ForgetCurrentTuple();
@@ -1103,10 +1137,16 @@ namespace MobileGL::MG_Remote::Server {
ForgetCurrentTuple();
frame.ok = true;
return MOBILEGL_OK;
case SurfaceControlOp::SetWindowHandle:
backend->SetWindowHandle(UnpackWindowHandle(frame));
case SurfaceControlOp::SetWindowHandle: {
MG_Backend::WindowHandle window;
if (!UnpackWindowHandle(frame, &window)) {
frame.ok = false;
return MOBILEGL_ERR_PROTOCOL_MISMATCH; // PH-1 (3): latched
}
backend->SetWindowHandle(window);
frame.ok = true;
return MOBILEGL_OK;
}
case SurfaceControlOp::InitCapabilities:
// Inproc-only (f0-egl §4.2): on the wire the ANSWER to this op is the CapsSnapshot
// frame itself, so there is no SurfaceOp kind for it. The frame channel still has to
@@ -1142,8 +1182,12 @@ 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.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.kind\"} - the control slot held "
// PH-1 (3): latched in an armed session child (under spawn SurfaceOpCodec has already
// refused an unknown wire kind, so this is defence in depth); unarmed it dies.
frame.ok = false;
(void)SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.kind\"} - the control slot held "
"kind %d, which names no dispatchable op", static_cast<int>(frame.kind));
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
// ---------------------------------------------------------------------------------
+40 -6
View File
@@ -6,6 +6,7 @@
#include "../Transport/WireLog.h"
#include "../Protocol/SurfaceOpCodec.h"
#include "../Handshake.h"
#include "../FatalFunnel.h"
#include "ServerLoop.h"
#include "ServerSession.h"
#include "SurfaceControlFrame.h"
@@ -245,6 +246,11 @@ void SendLogAck(void* pointer) {
MobileGL::MG_ConfigLoader::Init();
MobileGL::MG_Config::Transport = MobileGL::MG_Config::TransportMode::Spawn;
MobileGL::MG_Pipe::MGPipeSetServerProcessRole(true);
// PH-1 (3), ID-P7-1: THIS PROCESS IS THE SESSION, so a named fault on the peer's bytes may end
// it cleanly instead of aborting it (FatalFunnel.h's latch block). Armed here and nowhere
// else: inproc keeps every Fatal (client and server share that process), and the supervisor
// that forked this child never applies a record.
ArmSessionLatch();
// The child bypasses MobileGL::Initialize: arm its own role counters.
MobileGL::MG_Util::PipeStats::Init();
const auto backend = static_cast<MobileGL::BackendType>(hello->backendType());
@@ -368,15 +374,26 @@ void SendLogAck(void* pointer) {
// this child would sit here, holding the supervisor's one session slot, answering every
// later client Busy. Now each quiet interval asks whether the apply thread is still there;
// when it is not (forfeit, or a dead data bell), the session takes the ordinary exit below:
// Stop, Close, _exit(0). The interval is the supervisor's own accept poll.
// Stop, Close, _exit(0). (PH-6 used the supervisor's 250 ms accept poll as the interval; the
// PH-1 note below says why the slice is now 100 ms.)
//
// ASKED AT THE TOP OF EVERY TURN, NOT ONLY WHEN A WAIT TIMES OUT (PH-6 fix round). A peer that
// forfeited the reverse channel but keeps TALKING - a LogFlush or a surface op more often than
// every 250 ms, each of which this loop answers - never let ReceiveFrame time out, so the
// question was never asked and the session, with the supervisor's one slot, lived as long as
// the peer kept chattering.
constexpr std::uint32_t kControlPollMs = 250;
//
// PH-1 (3): the same wait is also how a LATCH raised on the apply thread (the decoder, the
// applier, DrainRing) ends this loop within one slice while the peer is silent. ReceiveFrame
// keeps a partial frame in its reassembler across a timeout, so slicing loses no bytes; a
// latched session is then closed below with kSessionLatchedExitCode. The latch is asked
// BEFORE the apply thread's liveness because a latch also stops that thread (it leaves its
// loop and clears m_running), and the session must be named by its latch, not as a stopped
// apply thread. One slice serves both: the latch's 100 ms (PH-6 had 250 ms; the shorter wait
// only asks the question more often).
constexpr std::uint32_t kControlSliceMs = 100;
for (;;) {
if (SessionLatched()) break;
if (!loop.Running()) {
WireLogError("MG_Remote server: pid=%d the apply thread has stopped (%s, %llu event(s) dropped); "
"ending the session",
@@ -385,7 +402,7 @@ void SendLogAck(void* pointer) {
break;
}
std::uint64_t size = 0;
const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kControlPollMs);
const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kControlSliceMs);
if (result == MOBILEGL_ERR_TIMEOUT) continue;
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) { buffer.resize(static_cast<std::size_t>(size)); continue; }
if (result != MOBILEGL_OK) {
@@ -454,6 +471,9 @@ void SendLogAck(void* pointer) {
if (!operation) break;
Server::SurfaceControlFrame reply{};
(void)ServerApplyWireSurfaceOp(*operation, &reply);
// PH-1 (3): an op that latched (its own bytes, or a record the apply thread was on) is
// answered by the SessionFault already sent and the close below, not by a reply.
if (SessionLatched()) break;
session.FlushDataProgress();
if (session.DataLink()) reply.eventHead = session.DataLink()->EventPublishedHead();
flatbuffers::FlatBufferBuilder builder(256);
@@ -466,11 +486,22 @@ void SendLogAck(void* pointer) {
loop.Stop();
// Publish the final server window while log forwarding is still attached.
MobileGL::MG_Util::PipeStats::Shutdown();
// PH-1 (3): a latched session ends HERE, by the same orderly path as a peer's EOF - the apply
// thread has already left its loop and destroyed the backend on its own thread (loop.Stop()
// joined it), the session closes, and the exit status names the latch so the supervisor
// counts it. The first fault's line was logged (and sent as a SessionFault) when it latched.
const bool latched = SessionLatched();
if (latched) {
WireLogError("MG_Remote server: pid=%d session closed on a latched fault (%s; %llu named "
"fault(s) latched or declined); exiting %d for the supervisor to reap - PH-1",
selfPid, FatalFamilyName(SessionLatchedFamily()),
static_cast<unsigned long long>(SessionLatchCount()), kSessionLatchedExitCode);
}
session.Close();
MobileGL::MG_Util::Debug::SetLogForwarder(nullptr, nullptr);
// _exit closes control after cleanup/flush; peer EOF is the client's fence.
std::fflush(nullptr);
::_exit(0);
::_exit(latched ? kSessionLatchedExitCode : 0);
}
// P7 wave 0, Ph slice (1) (ID-P7-1). THE SUPERVISOR OBSERVES AND NAMES A SESSION'S DEATH.
@@ -498,8 +529,11 @@ bool ReapActiveSession(pid_t& active, unsigned long& sessionsFaulted) {
if (WIFEXITED(status)) {
const int code = WEXITSTATUS(status);
if (code != 0) ++sessionsFaulted;
WireLogError("MG_Remote server: session pid=%d reaped exit=%d sessionsFaulted=%lu",
reaped, code, sessionsFaulted);
// PH-1 (3): a latched session is a named, orderly end - still a fault (counted), and said
// as one so the reap line alone tells it from a crash or a refused bring-up.
WireLogError("MG_Remote server: session pid=%d reaped exit=%d%s sessionsFaulted=%lu",
reaped, code, code == kSessionLatchedExitCode ? " (latched fault)" : "",
sessionsFaulted);
} else if (WIFSIGNALED(status)) {
++sessionsFaulted;
WireLogError("MG_Remote server: session pid=%d reaped signal=%d sessionsFaulted=%lu",
@@ -404,6 +404,9 @@ namespace MobileGL::MG_Remote::Server {
ServerSession& ReverseCallbackOwner(const char* callback) {
auto* session = ServerSession::Active();
if (session == nullptr) {
// @Ph-declined (ID-P7-1): returns ServerSession& - there is no session to hand
// back and no honest "declined" reference, and the input is the backend's own
// reverse call, not a peer's bytes. Stays Fatal.
SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"%s.session-missing\"} - "
"a reverse callback has no accepted session owner", callback);
}
@@ -432,6 +435,9 @@ 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.
// @Ph-declined (ID-P7-1): the blobref is the BACKEND's (a reverse callback with
// a void signature and no caller that could act on a decline), and it rides the
// ServerSession& above; not a peer's bytes. Stays Fatal.
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 = "
@@ -997,6 +1003,8 @@ namespace MobileGL::MG_Remote::Server {
Transport::RingControl& ServerSession::Control() {
Transport::RingControl* control = m_link->Memory().CmdControl();
if (control == nullptr) {
// @Ph-declined (ID-P7-1): returns RingControl& - no honest reference exists before
// Accept() mapped SEG_CMD, and this is a call-order invariant, not a peer's bytes.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::Control\"} - the control "
"page does not exist until Accept() has mapped SEG_CMD");
}
@@ -1022,6 +1030,8 @@ namespace MobileGL::MG_Remote::Server {
return *m_externalConsumerBell;
}
if (m_transport == nullptr) {
// @Ph-declined (ID-P7-1): returns Doorbell& - no bell exists to hand back before
// Accept(); a call-order invariant, not a peer's bytes.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no "
"transport; Accept() has not run");
}
@@ -1031,6 +1041,8 @@ 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.
// @Ph-declined (ID-P7-1): returns Doorbell& for a transport role this build wires no
// pair for - a build-shape fact, not a peer's bytes, and no reference to decline with.
SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ConsumerDoorbell\"} - transport "
"role %u has no doorbell pair yet; that is P6's SocketTransport",
static_cast<unsigned>(m_transport->Role()));
@@ -1041,12 +1053,14 @@ namespace MobileGL::MG_Remote::Server {
return *m_externalProducerBell;
}
if (m_transport == nullptr) {
// @Ph-declined (ID-P7-1): returns Doorbell& - as ConsumerDoorbell above.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ProducerDoorbell\"} - no "
"transport; Accept() has not run");
}
if (m_transport->Role() == Transport::TransportRole::InProcess) {
return static_cast<Transport::InProcessTransport*>(m_transport)->PeerDoorbell();
}
// @Ph-declined (ID-P7-1): returns Doorbell& - as ConsumerDoorbell above.
SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ProducerDoorbell\"} - transport "
"role %u has no doorbell pair yet; that is P6's SocketTransport",
static_cast<unsigned>(m_transport->Role()));
+208 -119
View File
@@ -281,6 +281,17 @@ namespace MobileGL::MG_Remote::Wire {
static_assert(sizeof(kWirePayloadBytes) / sizeof(kWirePayloadBytes[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount));
// PH-1 (3): the pre-gate in DecodeAndApply (AdmitsTheGeneratedGate) asks "shorter than
// its own type" with this file's payload list, and the generated MGP_WIRE_CHECK_BOUNDS
// asks it with sizeof(MGPWireRec_X). Pinned equal row by row, so a record the pre-gate
// admits can never be one the generated gate would still abort on.
#define MGPW_REC_ROW(Name, Payload) \
static_assert(sizeof(MG_Pipe::MGPWireRec_##Name) == \
Align8(sizeof(MGPWireRecHeader) + sizeof(MG_Pipe::Payload)), \
"MGPWireRec_" #Name " and this file's payload row disagree on size");
MGPW_FOR_EACH_CALL(MGPW_REC_ROW)
#undef MGPW_REC_ROW
// The header's Size field is 32 bits by wire contract (Ring.h's kMaxRingCapacity
// comment). Every count-derived total is checked against this BEFORE it is used, so a
// corrupt Count can never wrap the arithmetic that is supposed to catch it.
@@ -389,6 +400,18 @@ namespace MobileGL::MG_Remote::Wire {
static_cast<unsigned long long>(got), static_cast<unsigned long long>(expected));
}
// PH-1 (3): the same two lines through the latch (PipeWireCodec.h says where each is used).
Bool WireProtocolLatch(const char* what, const char* detail) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what,
detail != nullptr ? detail : "");
}
Bool WireProtocolLatchAt(const char* what, Uint64 got, Uint64 expected) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu",
what, static_cast<unsigned long long>(got),
static_cast<unsigned long long>(expected));
}
// ---------------------------------------------------------------------------------
// SegmentTable
// ---------------------------------------------------------------------------------
@@ -472,22 +495,22 @@ namespace MobileGL::MG_Remote::Wire {
// R-2's honesty arms
// ---------------------------------------------------------------------------------
void CheckBlobIsHonest(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
Bool CheckBlobIsHonest(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
if (blob.Size == 0) {
if (blob.Seg != kSegNone || blob.Offset != 0) {
// 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.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} a blob with Size 0 "
return SessionLatch(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<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset));
}
return;
return true;
}
if (blob.Seg == kSegNone) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} Size=%llu with no segment "
return SessionLatch(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<unsigned long long>(blob.Size));
}
@@ -511,7 +534,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) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu "
return SessionLatch(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",
@@ -521,45 +544,47 @@ namespace MobileGL::MG_Remote::Wire {
static_cast<unsigned>(kSegStage));
}
if (segments.Resolve(blob.Seg, blob.Offset, blob.Size) == nullptr) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu "
return SessionLatch(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<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset),
static_cast<unsigned long long>(blob.Size));
}
return true;
}
void RequireDeclaredBlob(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
Bool RequireDeclaredBlob(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
if (blob.Size == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} the record's own fields say "
return SessionLatch(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));
}
CheckBlobIsHonest(op, blob, segments);
return CheckBlobIsHonest(op, blob, segments);
}
void CheckHostSpanIsHonest(const MGHostSpan& span) {
Bool CheckHostSpanIsHonest(const MGHostSpan& span) {
if (span.Ptr != nullptr) {
// Rule B. In one address space this pointer WORKS, which is the whole reason the
// rule has to be mechanical: an inproc implementation that kept using it is
// indistinguishable from a correct one until the day it is a second process.
WireProtocolFatal("host-span",
"MGHostSpan::Ptr is non-null under split; the encoder writes "
"nullptr and names SEG_STAGE (R-2.1)");
return WireProtocolLatch("host-span",
"MGHostSpan::Ptr is non-null under split; the encoder writes "
"nullptr and names SEG_STAGE (R-2.1)");
}
if (span.Size != 0 && span.Seg == kMGHostSpanSegNone) {
WireProtocolFatalAt("host-span.seg", span.Size, 0);
return WireProtocolLatchAt("host-span.seg", span.Size, 0);
}
if (span.Size == 0 && span.Seg != kMGHostSpanSegNone) {
WireProtocolFatalAt("host-span.size", span.Seg, 0);
return WireProtocolLatchAt("host-span.size", span.Seg, 0);
}
return true;
}
void CheckHostSpanIsHonest(const MGHostSpan& span, const SegmentTable& segments) {
CheckHostSpanIsHonest(span);
Bool CheckHostSpanIsHonest(const MGHostSpan& span, const SegmentTable& segments) {
if (!CheckHostSpanIsHonest(span)) return false;
if (span.Size == 0) {
// Fully absent, and the arms above already proved Seg agrees with that.
return;
return true;
}
// ARM 4, WHICH THE OTHER OVERLOAD CANNOT DO. A span naming a real segment and a run
// past the end of it used to pass every check and reach WireVerbSink::OnDrawVbo, which
@@ -569,32 +594,34 @@ 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) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"host-span\"} seg=%u offset=%llu "
return SessionLatch(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<unsigned>(span.Seg),
static_cast<unsigned long long>(span.Offset),
static_cast<unsigned long long>(span.Size));
}
return true;
}
void CheckDrawUserIndices(const MGPDrawInfo& info, const MGPDrawRange* ranges,
Bool CheckDrawUserIndices(const MGPDrawInfo& info, const MGPDrawRange* ranges,
const MGHostSpan& span) {
if ((info.Flags & kDrawHasUserIndices) == 0 || (info.Flags & kDrawIsIndirect) != 0 ||
info.NumDraws != 1 || ranges == nullptr) {
WireProtocolFatal("DrawVbo.userIndices.shape",
"a user-index span requires exactly one direct indexed range");
return WireProtocolLatch("DrawVbo.userIndices.shape",
"a user-index span requires exactly one direct indexed range");
}
if (info.IndexSize != 1 && info.IndexSize != 2 && info.IndexSize != 4) {
WireProtocolFatalAt("DrawVbo.userIndices.IndexSize", info.IndexSize, 4);
return WireProtocolLatchAt("DrawVbo.userIndices.IndexSize", info.IndexSize, 4);
}
// Client arrays are staged from their first index; Start only addresses an EBO.
if (ranges[0].Start != 0) {
WireProtocolFatalAt("DrawVbo.userIndices.Start", ranges[0].Start, 0);
return WireProtocolLatchAt("DrawVbo.userIndices.Start", ranges[0].Start, 0);
}
const Uint64 required = static_cast<Uint64>(ranges[0].Count) * info.IndexSize;
if (required > span.Size) {
WireProtocolFatalAt("DrawVbo.userIndices.extent", required, span.Size);
return WireProtocolLatchAt("DrawVbo.userIndices.extent", required, span.Size);
}
return true;
}
// ---------------------------------------------------------------------------------
@@ -625,8 +652,8 @@ namespace MobileGL::MG_Remote::Wire {
const auto& p = *static_cast<const MGPVertexBuffers*>(payload);
if (p.Count > kMGPipeMaxVertexAttribs ||
static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxVertexAttribs) {
WireProtocolFatalAt("SetVertexBuffers.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxVertexAttribs);
return WireProtocolLatchAt("SetVertexBuffers.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxVertexAttribs);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPVertexBuffer));
tails = 1;
@@ -635,8 +662,8 @@ namespace MobileGL::MG_Remote::Wire {
case MGPWireOp::SetSamplerViews: {
const auto& p = *static_cast<const MGPSamplerViews*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxTextureUnits) {
WireProtocolFatalAt("SetSamplerViews.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxTextureUnits);
return WireProtocolLatchAt("SetSamplerViews.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxTextureUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPBoundView));
tails = 1;
@@ -645,8 +672,8 @@ namespace MobileGL::MG_Remote::Wire {
case MGPWireOp::BindSamplerStates: {
const auto& p = *static_cast<const MGPSamplerStates*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxTextureUnits) {
WireProtocolFatalAt("BindSamplerStates.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxTextureUnits);
return WireProtocolLatchAt("BindSamplerStates.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxTextureUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPipeHandle));
tails = 1;
@@ -655,8 +682,8 @@ namespace MobileGL::MG_Remote::Wire {
case MGPWireOp::SetShaderImages: {
const auto& p = *static_cast<const MGPShaderImages*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxImageUnits) {
WireProtocolFatalAt("SetShaderImages.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxImageUnits);
return WireProtocolLatchAt("SetShaderImages.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxImageUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPImageView));
tails = 1;
@@ -672,14 +699,14 @@ namespace MobileGL::MG_Remote::Wire {
// is the arm that turns Count into a byte length, so a forged count has to be
// refused before it is multiplied rather than after.
if (p.Class >= kMGPipeShaderBufferClassCount) {
WireProtocolFatalAt("SetShaderBuffers.Class", p.Class, kMGPipeShaderBufferClassCount);
return WireProtocolLatchAt("SetShaderBuffers.Class", p.Class, kMGPipeShaderBufferClassCount);
}
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxBufferBindingPoints) {
WireProtocolFatalAt("SetShaderBuffers.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxBufferBindingPoints);
return WireProtocolLatchAt("SetShaderBuffers.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxBufferBindingPoints);
}
if (p.HostSpanCount != 0 && p.HostSpanCount != p.Count) {
WireProtocolFatalAt("SetShaderBuffers.HostSpanCount", p.HostSpanCount, p.Count);
return WireProtocolLatchAt("SetShaderBuffers.HostSpanCount", p.HostSpanCount, p.Count);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPBufferRange));
tail1 = TailBytesFor(p.HostSpanCount, sizeof(MGHostSpan));
@@ -709,16 +736,16 @@ namespace MobileGL::MG_Remote::Wire {
// multiplied.
const auto& p = *static_cast<const MGPProgramBindings*>(payload);
if (p.BlockBindingCount > kMGPipeMaxProgramBlockBindings) {
WireProtocolFatalAt("SetProgramBindings.BlockBindingCount", p.BlockBindingCount,
kMGPipeMaxProgramBlockBindings);
return WireProtocolLatchAt("SetProgramBindings.BlockBindingCount", p.BlockBindingCount,
kMGPipeMaxProgramBlockBindings);
}
if (p.SamplerUnitCount > kMGPipeMaxProgramSamplerUnits) {
WireProtocolFatalAt("SetProgramBindings.SamplerUnitCount", p.SamplerUnitCount,
kMGPipeMaxProgramSamplerUnits);
return WireProtocolLatchAt("SetProgramBindings.SamplerUnitCount", p.SamplerUnitCount,
kMGPipeMaxProgramSamplerUnits);
}
if (p.StorageOverrideCount > kMGPipeMaxProgramStorageOverrides) {
WireProtocolFatalAt("SetProgramBindings.StorageOverrideCount",
p.StorageOverrideCount, kMGPipeMaxProgramStorageOverrides);
return WireProtocolLatchAt("SetProgramBindings.StorageOverrideCount",
p.StorageOverrideCount, kMGPipeMaxProgramStorageOverrides);
}
tail0 = TailBytesFor(p.BlockBindingCount, sizeof(Int32));
tail1 = TailBytesFor(p.SamplerUnitCount, sizeof(MGPProgramSamplerUnit));
@@ -742,10 +769,10 @@ namespace MobileGL::MG_Remote::Wire {
// scatters attribute values onto the wrong locations.
const auto& p = *static_cast<const MGPVertexAttribDefaults*>(payload);
if (p.Count != PopCount32(p.Mask)) {
WireProtocolFatalAt("SetVertexAttribDefaults.Count", p.Count, PopCount32(p.Mask));
return WireProtocolLatchAt("SetVertexAttribDefaults.Count", p.Count, PopCount32(p.Mask));
}
if (p.Count > kMGPipeMaxVertexAttribs) {
WireProtocolFatalAt("SetVertexAttribDefaults.Count", p.Count, kMGPipeMaxVertexAttribs);
return WireProtocolLatchAt("SetVertexAttribDefaults.Count", p.Count, kMGPipeMaxVertexAttribs);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPAttribValue));
tails = 1;
@@ -768,7 +795,7 @@ namespace MobileGL::MG_Remote::Wire {
// regions, so a non-zero count is a fault rather than a tail.
const auto& p = *static_cast<const MGPSubData*>(payload);
if (p.RegionCount != 0) {
WireProtocolFatalAt("BufferSubDataResident.RegionCount", p.RegionCount, 0);
return WireProtocolLatchAt("BufferSubDataResident.RegionCount", p.RegionCount, 0);
}
break;
}
@@ -785,12 +812,12 @@ namespace MobileGL::MG_Remote::Wire {
// buffer by GL rule - and an indirect draw declares no ranges, because the server
// never reads the indirect buffer to learn a count and the client has none to send.
if (userIndices && indirect) {
WireProtocolFatal("DrawVbo.Flags",
"kDrawHasUserIndices and kDrawIsIndirect are exclusive; an "
"indirect draw's indices come from the bound element buffer");
return WireProtocolLatch("DrawVbo.Flags",
"kDrawHasUserIndices and kDrawIsIndirect are exclusive; an "
"indirect draw's indices come from the bound element buffer");
}
if (indirect && p.NumDraws != 0) {
WireProtocolFatalAt("DrawVbo.NumDraws", p.NumDraws, 0);
return WireProtocolLatchAt("DrawVbo.NumDraws", p.NumDraws, 0);
}
if (userIndices) {
tail1 = sizeof(MGHostSpan);
@@ -830,7 +857,7 @@ namespace MobileGL::MG_Remote::Wire {
out.TailCount = tails;
out.TotalBytes = Align8(out.TotalBytes);
if (out.TotalBytes > kMaxRecordBytesOnTheWire) {
WireProtocolFatalAt("record.Size", out.TotalBytes, kMaxRecordBytesOnTheWire);
return WireProtocolLatchAt("record.Size", out.TotalBytes, kMaxRecordBytesOnTheWire);
}
return true;
}
@@ -1075,7 +1102,9 @@ namespace MobileGL::MG_Remote::Wire {
MGHostSpan span{};
std::memcpy(&span, tails[1].Bytes, sizeof(span));
const auto& info = *static_cast<const MGPDrawInfo*>(payload);
CheckDrawUserIndices(info, static_cast<const MGPDrawRange*>(tails[0].Bytes), span);
// Encoder side: the latch is armed only in a server session child, so here the
// arm dies on a bad shape and its answer carries nothing to act on (PH-1 (3)).
(void)CheckDrawUserIndices(info, static_cast<const MGPDrawRange*>(tails[0].Bytes), span);
}
const Uint64 total = layout.TotalBytes;
@@ -1185,7 +1214,7 @@ namespace MobileGL::MG_Remote::Wire {
for (Uint32 i = 0; i < slots.Count; ++i) {
MGPBlobRef blob{};
std::memcpy(&blob, bytes + slots.Offset + i * sizeof(MGPBlobRef), sizeof(MGPBlobRef));
CheckBlobIsHonest(op, blob, *m_segments);
(void)CheckBlobIsHonest(op, blob, *m_segments); // encoder: unarmed, dies (PH-1 (3))
// THE ENCODER MUST NOT ACCEPT A RECORD THE DECODER FATALS ON. w1's ruling that
// CreateShaderState's modules travel inside the Reflection archive lived only
// in the decoder, so an emitter that declared a per-stage run got a valid seq
@@ -1210,7 +1239,7 @@ namespace MobileGL::MG_Remote::Wire {
std::memcpy(&span, bytes + at + i * sizeof(MGHostSpan), sizeof(MGHostSpan));
// All four arms, including the one that needs the table: a producer that
// computed an offset wrongly is caught here rather than on a peer.
CheckHostSpanIsHonest(span, *m_segments);
(void)CheckHostSpanIsHonest(span, *m_segments); // encoder: unarmed, dies (PH-1 (3))
}
}
@@ -1445,7 +1474,7 @@ namespace MobileGL::MG_Remote::Wire {
return t_activeDecoder->ApplyChecked(op, record, size);
}
void PipeWireDecoder::NoteResolvedRun(MGPWireOp op, const MGPBlobRef& blob) {
Bool PipeWireDecoder::NoteResolvedRun(MGPWireOp op, const MGPBlobRef& blob) {
// UNREACHABLE NOW, AND LOUD RATHER THAN SILENT. This used to `return`, and that made
// the audit's bookkeeping quietly optional: a record naming a non-SEG_STAGE carrier
// was applied AND recorded nothing, so PoisonedStageBytes() stayed zero and rule C's
@@ -1455,7 +1484,7 @@ 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) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the audit was asked to record a "
return SessionLatch(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<unsigned>(blob.Seg),
@@ -1466,24 +1495,28 @@ namespace MobileGL::MG_Remote::Wire {
// 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.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} more than %llu SEG_STAGE runs in "
return SessionLatch(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<unsigned long long>(sizeof(m_resolved) / sizeof(m_resolved[0])));
}
m_resolved[m_resolvedCount++] = blob;
return true;
}
// PH-1 (3): nullptr ONLY when a named fault latched (an armed session child); every caller
// returns false on it. Unarmed, each arm below still dies by name and this never returns null.
const void* PipeWireDecoder::ResolveOrFatal(MGPWireOp op, const MGPBlobRef& blob) {
RequireDeclaredBlob(op, blob, *m_segments);
if (!RequireDeclaredBlob(op, blob, *m_segments)) return nullptr;
const void* bytes = m_segments->Resolve(blob.Seg, blob.Offset, blob.Size);
if (bytes == nullptr) {
// RequireDeclaredBlob already ran the same arithmetic, so reaching here means the
// table moved under us rather than that the record is wrong. Same Fatal either
// way: there is no recovery from a segment that stopped covering its own runs.
WireProtocolFatalAt("segment-resolve", blob.Offset, blob.Size);
(void)WireProtocolLatchAt("segment-resolve", blob.Offset, blob.Size);
return nullptr;
}
NoteResolvedRun(op, blob);
if (!NoteResolvedRun(op, blob)) return nullptr;
return bytes;
}
@@ -1513,6 +1546,36 @@ namespace MobileGL::MG_Remote::Wire {
m_resolvedCount = 0;
}
namespace {
// PH-1 (3): THE GENERATED GATE'S TWO PEER-REACHABLE DEATHS, ASKED FIRST AND BY NAME, in
// an armed session child only. MGP_WIRE_CHECK_BOUNDS (MG_Pipe/generated/PipeWire.inc)
// aborts on an opcode no row names and on a record shorter than its own type or not
// 8-aligned, through MGPipeWireProtocolFatal - a line with no `Fatal{` marker, in a file
// gen_pipe.py owns (fatal_census.py's FUNNEL_SITES records that debt). Both operands are
// the ring header's `kind` and `size`, which the peer writes, so a session child asks the
// same two questions here first and a failure latches by name; whatever reaches the
// generated gate then passes it by construction (MGPW_REC_ROW pins the sizes equal).
// Unarmed - inproc, the unit cases - this admits everything and the generated gate keeps
// its old death line for line.
Bool AdmitsTheGeneratedGate(MGPWireOp op, Uint64 size) {
if (!SessionLatchArmed()) return true;
const SizeT index = static_cast<SizeT>(op);
if (index == 0 || index >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
WireRecordLayout minimum{};
(void)MGPipeWireRecordLayout(op, nullptr, minimum);
// Its own word, not the layout's "record.Size": the two refusals are different
// questions (shorter than the type vs. past the wire's 32-bit Size), and a log line
// - and PeerLatchTest's first-Fatal check - has to be able to tell them apart.
if (size < minimum.TotalBytes || (size % 8) != 0) {
return WireProtocolLatchAt("record.Minimum", size, minimum.TotalBytes);
}
return true;
}
} // namespace
Bool PipeWireDecoder::DecodeAndApply(const Transport::RingRecordView& record) {
if (!Valid()) {
WireProtocolFatal("PipeWireDecoder::DecodeAndApply", "no control page or segment table");
@@ -1533,15 +1596,18 @@ namespace MobileGL::MG_Remote::Wire {
m_resolvedCount = 0;
m_lastAcceptanceKnown = false;
PipeWireDecoder* previous = t_activeDecoder;
t_activeDecoder = this;
// The generated gate first, ALWAYS: MGPipeApplyWireRecord owns the per-opcode
// `size >= sizeof(MGPWireRec_X)` check, because that is the half that follows from the
// opcode alone and therefore belongs to the generator. It then calls back into
// ApplyChecked through the hook, which owns the half that needs the payload. The hook
// was installed once, at construction.
const Bool applied = MG_Pipe::MGPipeApplyWireRecord(op, base, size, size);
t_activeDecoder = previous;
Bool applied = false;
if (AdmitsTheGeneratedGate(op, size)) {
PipeWireDecoder* previous = t_activeDecoder;
t_activeDecoder = this;
// The generated gate first, ALWAYS: MGPipeApplyWireRecord owns the per-opcode
// `size >= sizeof(MGPWireRec_X)` check, because that is the half that follows from the
// opcode alone and therefore belongs to the generator. It then calls back into
// ApplyChecked through the hook, which owns the half that needs the payload. The hook
// was installed once, at construction.
applied = MG_Pipe::MGPipeApplyWireRecord(op, base, size, size);
t_activeDecoder = previous;
}
PoisonResolvedRuns();
@@ -1576,16 +1642,24 @@ namespace MobileGL::MG_Remote::Wire {
const auto* base = static_cast<const Uint8*>(record);
const void* payload = base + sizeof(MGPWireRecHeader);
// PH-1 (3), ID-P7-1: FROM HERE ON EVERY NAMED FAULT ON A PEER'S BYTES LATCHES. In an armed
// session child each arm below logs its unchanged `Fatal{ProtocolCorruption, "..."}` line
// and returns false, and so does this function: the record is declined, DrainRing sees
// the latch before the next one, and the session closes by name. Unarmed (inproc, unit
// cases) every arm still dies exactly as it did.
WireRecordLayout layout{};
if (!MGPipeWireRecordLayout(op, payload, layout)) {
WireProtocolFatalAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
const SizeT opIndex = static_cast<SizeT>(op);
if (opIndex == 0 || opIndex >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
// With the opcode known, false here means a count bound in the layout latched.
if (!MGPipeWireRecordLayout(op, payload, layout)) return false;
// THE TAIL CROSS-CHECK (BRIEF §5 w1). The generated gate proved `size >= sizeof(the
// 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) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and "
return SessionLatch(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<unsigned long long>(size),
@@ -1656,7 +1730,7 @@ namespace MobileGL::MG_Remote::Wire {
base + sizeof(MGPWireRecHeader) + offsetof(MGPCaps, FormatCapabilities) +
i * sizeof(MGPBlobRef),
sizeof(MGPBlobRef));
CheckBlobIsHonest(op, blob, *m_segments);
if (!CheckBlobIsHonest(op, blob, *m_segments)) return false;
}
return false;
@@ -1781,13 +1855,14 @@ namespace MobileGL::MG_Remote::Wire {
// chunk table computes it from the mask the record itself carries.
const Uint64 expected =
static_cast<Uint64>(MGPipePipelineChunkBlobBytes(desc.ChunkMask));
RequireDeclaredBlob(op, desc.Blob, *m_segments);
if (!RequireDeclaredBlob(op, desc.Blob, *m_segments)) return false;
if (desc.Blob.Size != expected) {
WireProtocolFatalAt("CreateRenderState.Blob", desc.Blob.Size, expected);
return WireProtocolLatchAt("CreateRenderState.Blob", desc.Blob.Size, expected);
}
chunks = ResolveOrFatal(op, desc.Blob);
} else {
CheckBlobIsHonest(op, desc.Blob, *m_segments);
if (chunks == nullptr) return false;
} else if (!CheckBlobIsHonest(op, desc.Blob, *m_segments)) {
return false;
}
MGPipeApplyCreateRenderState(desc, chunks);
return true;
@@ -1807,6 +1882,7 @@ namespace MobileGL::MG_Remote::Wire {
// (PipeApply.cpp:1990-1999) and is the model every other row copies; the decoder
// adds only what the applier cannot see, which is that the bytes exist at all.
const void* blob = ResolveOrFatal(op, desc.Blob);
if (blob == nullptr) return false;
MGPipeApplyCreateVertexElements(desc, blob);
return true;
}
@@ -1825,12 +1901,13 @@ namespace MobileGL::MG_Remote::Wire {
// and borderColorForm must survive BYTE FOR BYTE, because all three colour
// representations are always numerically populated and it is the only thing that
// says which one the backend must use (MGPipeTypes.h:425-428).
RequireDeclaredBlob(op, desc.Parameters, *m_segments);
if (!RequireDeclaredBlob(op, desc.Parameters, *m_segments)) return false;
if (desc.Parameters.Size != sizeof(MobileGL::SamplerParameters)) {
WireProtocolFatalAt("CreateSamplerState.Parameters", desc.Parameters.Size,
sizeof(MobileGL::SamplerParameters));
return WireProtocolLatchAt("CreateSamplerState.Parameters", desc.Parameters.Size,
sizeof(MobileGL::SamplerParameters));
}
const void* bytes = ResolveOrFatal(op, desc.Parameters);
if (bytes == nullptr) return false;
// Copied into a local rather than reinterpret_cast in place: the staged run is
// 8-aligned by the ring, but SamplerParameters is a frontend type and this file
// may not assume its alignment requirement is one the ring happens to satisfy.
@@ -1868,9 +1945,9 @@ namespace MobileGL::MG_Remote::Wire {
// archive - and then the archive has to stop carrying generatedSpirv, in the same
// change, or the two disagree.
for (Uint32 i = 0; i < 6; ++i) {
CheckBlobIsHonest(op, desc.Spirv[i], *m_segments);
if (!CheckBlobIsHonest(op, desc.Spirv[i], *m_segments)) return false;
if (desc.Spirv[i].Size != 0) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} "
return SessionLatch(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<unsigned>(i),
@@ -1878,6 +1955,7 @@ namespace MobileGL::MG_Remote::Wire {
}
}
const void* archiveBytes = ResolveOrFatal(op, desc.Reflection);
if (archiveBytes == nullptr) return false;
// P5e (pg), gap G-A: DECODED ONTO THE HEAP AND HANDED TO THE RECORD, not onto this
// stack. Before P5e these were two locals that died at the closing brace and the
// applier kept only the descriptor, so every reflection question the program twin
@@ -1893,10 +1971,13 @@ namespace MobileGL::MG_Remote::Wire {
if (!MG_State::GLState::DecodeProgramArchive(static_cast<const Uint8*>(archiveBytes),
static_cast<SizeT>(desc.Reflection.Size),
*archive)) {
WireProtocolFatal("CreateShaderState.Reflection",
"DecodeProgramArchive refused the archive - a truncated "
"stream, a codec version mismatch, a struct-size mismatch, or "
"a stage list that does not match the modules it frames");
// PH-5's ArchiveVector bound (ProgramArtifactsCodec.cpp) refuses here too: a
// vector count whose immediate allocation exceeds the archive's remaining bytes
// makes DecodeProgramArchive answer false.
return WireProtocolLatch("CreateShaderState.Reflection",
"DecodeProgramArchive refused the archive - a truncated "
"stream, a codec version mismatch, a struct-size mismatch, or "
"a stage list that does not match the modules it frames");
}
MGPipeApplyCreateShaderState(desc, &archive->Link, &archive->Spirv, archive);
return true;
@@ -1917,13 +1998,14 @@ namespace MobileGL::MG_Remote::Wire {
if (dyn.ChunkMask != 0) {
const Uint64 expected =
static_cast<Uint64>(MGPipeDynamicChunkBlobBytes(dyn.ChunkMask));
RequireDeclaredBlob(op, dyn.Blob, *m_segments);
if (!RequireDeclaredBlob(op, dyn.Blob, *m_segments)) return false;
if (dyn.Blob.Size != expected) {
WireProtocolFatalAt("SetDynamicState.Blob", dyn.Blob.Size, expected);
return WireProtocolLatchAt("SetDynamicState.Blob", dyn.Blob.Size, expected);
}
chunks = ResolveOrFatal(op, dyn.Blob);
} else {
CheckBlobIsHonest(op, dyn.Blob, *m_segments);
if (chunks == nullptr) return false;
} else if (!CheckBlobIsHonest(op, dyn.Blob, *m_segments)) {
return false;
}
MGPipeApplySetDynamicState(dyn, chunks);
return true;
@@ -1983,7 +2065,7 @@ namespace MobileGL::MG_Remote::Wire {
// ALL FOUR ARMS, the segment-range one included: this row and DrawVbo are
// the only two host-span carriers in the catalogue, and an out-of-segment
// span used to pass every check here.
CheckHostSpanIsHonest(span, *m_segments);
if (!CheckHostSpanIsHonest(span, *m_segments)) return false;
}
}
MGPipeApplySetShaderBuffers(*static_cast<const MGPShaderBuffers*>(payload),
@@ -2029,7 +2111,7 @@ namespace MobileGL::MG_Remote::Wire {
std::memcpy(&entry, reinterpret_cast<const Uint8*>(overrides) +
i * sizeof(MGPProgramStorageOverride),
sizeof(MGPProgramStorageOverride));
CheckHostSpanIsHonest(entry.Name, *m_segments);
if (!CheckHostSpanIsHonest(entry.Name, *m_segments)) return false;
// THE NUL IS THE CLIENT'S AND IS CHECKED, not assumed: the emitter stages
// the name with its terminator (ProgramEmit.h), and a span whose last byte
// is not 0 would make the applier's String(const char*) walk off the end of
@@ -2039,9 +2121,9 @@ namespace MobileGL::MG_Remote::Wire {
m_segments->Resolve(entry.Name.Seg, entry.Name.Offset, entry.Name.Size));
if (entry.Name.Size == 0 || name == nullptr ||
name[entry.Name.Size - 1] != '\0') {
WireProtocolFatal("SetProgramBindings.StorageOverrides.Name",
"an override name span is empty or is not "
"NUL-terminated; the applier copies it as a C string");
return WireProtocolLatch("SetProgramBindings.StorageOverrides.Name",
"an override name span is empty or is not "
"NUL-terminated; the applier copies it as a C string");
}
overrideNames.push_back(name);
}
@@ -2061,6 +2143,7 @@ namespace MobileGL::MG_Remote::Wire {
// program's GlobalUboSize, which this record does not carry). Under rule A it
// stops being inert for the first time, which is the whole point of arming Size.
const void* bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
MGPipeApplySetGlobalConstants(rec, bytes);
return true;
}
@@ -2101,12 +2184,13 @@ namespace MobileGL::MG_Remote::Wire {
// rather than ">=" is what makes a client built against an older block a loud
// mismatch instead of a silently short read of CapabilityBits.
const auto& rec = *static_cast<const MGPResidualValueState*>(payload);
RequireDeclaredBlob(op, rec.Blob, *m_segments);
if (!RequireDeclaredBlob(op, rec.Blob, *m_segments)) return false;
if (rec.Blob.Size != sizeof(ResidualValueBlock)) {
WireProtocolFatalAt("SetResidualValueState.Blob", rec.Blob.Size,
sizeof(ResidualValueBlock));
return WireProtocolLatchAt("SetResidualValueState.Blob", rec.Blob.Size,
sizeof(ResidualValueBlock));
}
const void* bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
ResidualValueBlock block{};
std::memcpy(&block, bytes, sizeof(block));
MGPipeApplySetResidualValueState(block);
@@ -2128,7 +2212,7 @@ namespace MobileGL::MG_Remote::Wire {
Uint64(rec.UnionBox.X) + rec.UnionBox.W > rec.LevelWidth ||
Uint64(rec.UnionBox.Y) + rec.UnionBox.H > rec.LevelHeight ||
Uint64(rec.UnionBox.Z) + rec.UnionBox.D > rec.LevelDepth))
WireProtocolFatal("ResourceSubData.LevelExtent", "invalid full image extent or dirty box");
return WireProtocolLatch("ResourceSubData.LevelExtent", "invalid full image extent or dirty box");
// Rule A arms both halves. The buffer half already declared a real size in
// monolith and is cross-checked at PipeApply.cpp:702; THE TEXTURE HALF DECLARED 0
// (TextureEmit.h:1265-1267, on the grounds that the byte count was "the server's
@@ -2145,7 +2229,7 @@ namespace MobileGL::MG_Remote::Wire {
const Bool boxIsWhole =
rec.UnionBox.W != 0 && rec.UnionBox.H != 0 && rec.UnionBox.D != 0;
if (!namesABuffer && !boxIsEmpty && !boxIsWhole) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ResourceSubData\"} the union box "
return SessionLatch(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);
@@ -2155,8 +2239,9 @@ namespace MobileGL::MG_Remote::Wire {
const void* bytes = nullptr;
if (carriesContent) {
bytes = ResolveOrFatal(op, rec.Blob);
} else {
CheckBlobIsHonest(op, rec.Blob, *m_segments);
if (bytes == nullptr) return false;
} else if (!CheckBlobIsHonest(op, rec.Blob, *m_segments)) {
return false;
}
noteAcceptance(MGPipeApplyResourceSubData(
rec, bytes, reinterpret_cast<const MGPSubRegion*>(tailAt(0))));
@@ -2168,10 +2253,12 @@ namespace MobileGL::MG_Remote::Wire {
// kOptional is a CAPABILITY question under split, not a null-pointer one: the
// client gates on kCapResidentSubData through the caps mirror before it emits
// (R-8). By the time a record is here, the answer was already yes.
const void* bytes = MGPipeSubDataBufferSize(rec) != 0 ? ResolveOrFatal(op, rec.Blob)
: nullptr;
if (bytes == nullptr) {
CheckBlobIsHonest(op, rec.Blob, *m_segments);
const void* bytes = nullptr;
if (MGPipeSubDataBufferSize(rec) != 0) {
bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
} else if (!CheckBlobIsHonest(op, rec.Blob, *m_segments)) {
return false;
}
MGPipeApplyBufferSubDataResident(rec, bytes);
return true;
@@ -2241,16 +2328,17 @@ namespace MobileGL::MG_Remote::Wire {
MGHostSpan span{};
if ((info.Flags & kDrawHasUserIndices) != 0) {
if (layout.TailCount != 2 || layout.TailBytes[1] != sizeof(MGHostSpan)) {
WireProtocolFatalAt("DrawVbo.userIndices", layout.TailBytes[1],
sizeof(MGHostSpan));
return WireProtocolLatchAt("DrawVbo.userIndices", layout.TailBytes[1],
sizeof(MGHostSpan));
}
std::memcpy(&span, tailAt(1), sizeof(span));
// ALL FOUR ARMS. WireVerbSink's header promises OnDrawVbo "a DECODED,
// VALIDATED argument list"; without the segment-range arm a span whose run
// left SEG_STAGE reached the sink and that promise was false. P5b's d1 is what
// arms this path (client-side index arrays staged whole, CONTRACT-P5B.md d1).
CheckHostSpanIsHonest(span, *m_segments);
CheckDrawUserIndices(info, reinterpret_cast<const MGPDrawRange*>(tailAt(0)), span);
if (!CheckHostSpanIsHonest(span, *m_segments)) return false;
if (!CheckDrawUserIndices(info, reinterpret_cast<const MGPDrawRange*>(tailAt(0)), span))
return false;
userIndices = &span;
}
// P5b d1: the indirect block, in the span's place. The layout already refused a
@@ -2259,8 +2347,8 @@ namespace MobileGL::MG_Remote::Wire {
MGPDrawIndirect indirectBlock{};
if ((info.Flags & kDrawIsIndirect) != 0) {
if (layout.TailCount != 2 || layout.TailBytes[1] != sizeof(MGPDrawIndirect)) {
WireProtocolFatalAt("DrawVbo.indirect", layout.TailBytes[1],
sizeof(MGPDrawIndirect));
return WireProtocolLatchAt("DrawVbo.indirect", layout.TailBytes[1],
sizeof(MGPDrawIndirect));
}
std::memcpy(&indirectBlock, tailAt(1), sizeof(indirectBlock));
indirect = &indirectBlock;
@@ -2326,17 +2414,18 @@ namespace MobileGL::MG_Remote::Wire {
// (rule C).
const auto& rec = *static_cast<const MGPStorageBlockBinding*>(payload);
constexpr Uint64 kMaxBlockNameBytes = 4096;
RequireDeclaredBlob(op, rec.Name, *m_segments);
if (!RequireDeclaredBlob(op, rec.Name, *m_segments)) return false;
if (rec.Name.Size > kMaxBlockNameBytes) {
WireProtocolFatalAt("SetStorageBlockBinding.Name", rec.Name.Size, kMaxBlockNameBytes);
return WireProtocolLatchAt("SetStorageBlockBinding.Name", rec.Name.Size, kMaxBlockNameBytes);
}
const void* bytes = ResolveOrFatal(op, rec.Name);
if (bytes == nullptr) return false;
char name[kMaxBlockNameBytes + 1];
std::memcpy(name, bytes, static_cast<SizeT>(rec.Name.Size));
name[rec.Name.Size] = '\0';
if (name[rec.Name.Size - 1] != '\0') {
WireProtocolFatal("SetStorageBlockBinding.Name",
"the staged block name is not NUL-terminated; Size is strlen + 1");
return WireProtocolLatch("SetStorageBlockBinding.Name",
"the staged block name is not NUL-terminated; Size is strlen + 1");
}
return m_verbs != nullptr && m_verbs->OnSetStorageBlockBinding(rec, name);
}
@@ -2386,8 +2475,8 @@ namespace MobileGL::MG_Remote::Wire {
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
WireProtocolFatalAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
}
+25 -7
View File
@@ -112,6 +112,16 @@ namespace MobileGL::MG_Remote::Wire {
[[noreturn]] void WireProtocolFatal(const char* what, const char* detail);
[[noreturn]] void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected);
// PH-1 (3), ID-P7-1: THE SAME TWO LINES, THROUGH THE LATCH. Byte-identical to the two above
// (same `Fatal{ProtocolCorruption, "<what>"}` wording), but through MG_Remote::SessionLatch:
// in the spawn / TCP session child the fault latches and these RETURN false, so the decode
// path declines the record and the session closes by name; everywhere else (inproc, the
// client, a unit case) SessionLatch is SessionFail and these do not return. Used ONLY on the
// decode path, where the bytes are the peer's - an encoder-side check is a local bug and
// keeps the [[noreturn]] spelling.
Bool WireProtocolLatch(const char* what, const char* detail);
Bool WireProtocolLatchAt(const char* what, Uint64 got, Uint64 expected);
// R-2.3 arms 1-4 over one record's blobref. Split only; a monolith emission is exempt by
// construction because it never reaches this layer.
//
@@ -128,11 +138,15 @@ namespace MobileGL::MG_Remote::Wire {
// destination range, GlobalUboSize, the stage mask), which this signature does not see.
// RequireDeclaredBlob below is arm 2, and the decoder's per-op arm calls it exactly where
// the payload says content is implied.
void CheckBlobIsHonest(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
//
// PH-1 (3): EVERY HONESTY ARM NOW ANSWERS. `true` = honest; `false` = a named fault latched
// (armed session child only - unarmed, the arm dies exactly as before and never returns
// false). A decode-path caller returns on false; an encoder-side caller may discard it.
Bool CheckBlobIsHonest(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
const SegmentTable& segments);
// R-2.3 arm 2: the record's other fields say it carries content, so the blob must be
// declared. Fatal on an absent blob, then CheckBlobIsHonest on a present one.
void RequireDeclaredBlob(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
Bool RequireDeclaredBlob(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
const SegmentTable& segments);
// R-2.3 arms 1 and 3 for MGHostSpan. P5's reduced path should produce ZERO host spans
// (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are both 0 in P5, table 0), so this
@@ -142,17 +156,18 @@ namespace MobileGL::MG_Remote::Wire {
// run PAST THE END OF IT passes this function. Use the overload below on any path that has
// a table; this one exists because c0 shipped the signature and other packages compile
// against it.
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span);
Bool CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span);
// All four arms. Arm 4 is the one the signature above cannot express: a span is only
// honest if its Offset+Size actually lies inside the segment it names, and the promise
// WireVerbSink's header makes - that OnDrawVbo is handed a VALIDATED argument list - is
// false without it. Latent in P5 (nothing emits a span) and armed at P8, which is exactly
// when nobody will be reading this file.
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span, const SegmentTable& segments);
Bool CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span, const SegmentTable& segments);
// A legal segment run must also contain every index the draw will consume. Shared by
// the encoder, decoder and sink so a direct sink call cannot bypass the extent gate.
void CheckDrawUserIndices(const MG_Pipe::MGPDrawInfo& info,
// PH-1 (3): answers like the arms above (false = latched).
Bool CheckDrawUserIndices(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan& span);
@@ -199,7 +214,9 @@ namespace MobileGL::MG_Remote::Wire {
// `payload` must already be known to hold at least the op's payload struct - that is what
// MGP_WIRE_CHECK_BOUNDS proves, and this function is only ever called after it. Returns
// false for an opcode outside the catalogue; a count past its own GL bound is Fatal,
// because a decoder holding such a record has nothing safe left to do with it.
// because a decoder holding such a record has nothing safe left to do with it - and in an
// armed session child (PH-1 (3)) that Fatal latches instead and this returns false too, which
// is why the decoder asks the opcode question itself before calling.
Bool MGPipeWireRecordLayout(MG_Pipe::MGPWireOp op, const void* payload, WireRecordLayout& out);
// The catalogue's own spelling of an opcode, for a Fatal line. Out of range is "<opcode>".
@@ -722,8 +739,9 @@ namespace MobileGL::MG_Remote::Wire {
private:
Bool ApplyChecked(MG_Pipe::MGPWireOp op, const void* record, Uint64 size);
// nullptr only when the session latched a named fault (PH-1 (3)); unarmed it dies.
const void* ResolveOrFatal(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob);
void NoteResolvedRun(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob);
Bool NoteResolvedRun(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob);
void PoisonResolvedRuns();
// The ONLY way this class answers a record. Fatals if `op` carries no kReplySlot -
// see LastAcceptanceKnown() for why that is a Fatal and not a log line.
+14 -5
View File
@@ -34,6 +34,7 @@
// the boundary before the data plane needs it.
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSpawn.h>
#include <MG_Remote/Transport/Doorbell.h>
@@ -290,12 +291,20 @@ TEST(ServerSpawnTest, ARawPeerMutatesARecordAfterTheClientEncoderAcceptedIt) {
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server did not reject the mutated record promptly";
EXPECT_EQ(exitCode, -SIGABRT) << "the malformed opcode did not reach the server decoder";
// PH-1 (3): the opcode is the peer's byte, so the session child LATCHES by name (the
// decoder's pre-gate asks the generated gate's question first) and closes with
// kSessionLatchedExitCode instead of aborting in MGPipeWireProtocolFatal, whose line carries
// no `Fatal{` marker at all. Red-once: drop AdmitsTheGeneratedGate and this reads -SIGABRT and
// "protocol corruption applying <unknown opcode>" again.
EXPECT_EQ(exitCode, MobileGL::MG_Remote::kSessionLatchedExitCode)
<< "the malformed opcode did not latch the session by name";
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
EXPECT_NE(log.find("protocol corruption applying"), std::string::npos)
<< "the server process exited without the protocol corruption diagnostic";
EXPECT_NE(log.find("unknown opcode"), std::string::npos)
<< "the malformed opcode was not identified in the server log";
EXPECT_NE(log.find("Fatal{ProtocolCorruption, \"opcode\"} got=65535"), std::string::npos)
<< "the server process exited without the named opcode fault";
EXPECT_NE(log.find("SessionLatch{ProtocolCorruption}"), std::string::npos)
<< "the fault was not latched";
EXPECT_EQ(log.find("protocol corruption applying"), std::string::npos)
<< "the record reached the generated gate's unnamed death";
client.Stop();
}
+11 -3
View File
@@ -67,8 +67,9 @@ THE FOUR RULES, spelled out rather than inferred:
1. an abort's `Fatal{Word` marker is within kMarkerWindow lines above it, OR the abort is
exempted by FUNNEL_FILES / FUNNEL_SITES;
2. every WireLogFatal / SessionFail call carries a `Fatal{` in its format string - a funnel that
accepted an unmarked string would launder exactly what rule 1 refuses;
2. every WireLogFatal / SessionFail / SessionLatch call carries a `Fatal{` in its format string -
a funnel that accepted an unmarked string would launder exactly what rule 1 refuses (PH-1 (3)
added SessionLatch, the per-session latch-or-die twin of SessionFail);
3. every family word found in the perimeter has a row in FatalFamilies.def. a6's finding was a
30-word vocabulary against a 7-value wire enum; the .def is the projection that bounds the
divergence, and a word with no row projects onto nothing at all;
@@ -173,6 +174,12 @@ WIRE_LOG_FATAL_CALL = re.compile(r"WireLogFatal\s*\(")
# 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"(?<![A-Za-z0-9_])SessionFail\s*\(")
# PH-1 (3), ID-P7-1: the per-session latch (FatalFunnel.h SessionLatch) is SessionFail's
# latch-or-die twin - unarmed it IS SessionFail, armed it logs the same line and returns - so its
# calls are held to the same rule: the string carries its `Fatal{Word`, and rule 3 then holds the
# word to FatalFamilies.def. Without this, converting a site from SessionFail to SessionLatch
# would quietly take it out of the census.
SESSION_LATCH_CALL = re.compile(r"(?<![A-Za-z0-9_])SessionLatch\s*\(")
FAMILY_ROW = re.compile(r"^\s*X\(([A-Za-z][A-Za-z0-9]*)\s*,", re.MULTILINE)
REFUSE_ENUM = re.compile(r"enum\s+RefuseCode\s*:[^{]*\{(.*?)\}", re.DOTALL)
ENUM_VALUE = re.compile(r"^\s*([A-Za-z][A-Za-z0-9]*)\s*=", re.MULTILINE)
@@ -302,7 +309,8 @@ def census():
# 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) or SESSION_FAIL_CALL.search(line)):
if not (WIRE_LOG_FATAL_CALL.search(line) or SESSION_FAIL_CALL.search(line) or
SESSION_LATCH_CALL.search(line)):
continue
window = "\n".join(lines[index:index + kMarkerWindow])
if not MARKER.search(window):