mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user