[Fix] (MG_Remote, Wire): require every client-server content blob to declare SEG_STAGE and refuse any other carrier by name, which also puts the audit's run bookkeeping out of reach

This commit is contained in:
2026-09-16 06:41:00 -04:00
parent 1a06b40a58
commit ba6dc4f7f1
2 changed files with 108 additions and 1 deletions
+45 -1
View File
@@ -457,6 +457,36 @@ namespace MobileGL::MG_Remote::Wire {
WireOpName(op), static_cast<unsigned long long>(blob.Size));
std::abort();
}
// R-2.3's SECOND HALF: "inside SOME segment" IS NOT THE RULE. Contract table 1 gives
// every client->server content blob - groups A, B and C, all nineteen rows - the ONE
// carrier SEG_STAGE, and R-10 sends blobs there whole. Until this arm existed the only
// test was that the run resolved, so `CreateSamplerState.Parameters={Seg=SEG_REPLY,...}`
// was accepted and APPLIED: a server-owned segment, whose reuse is the reply pool's
// business and has nothing to do with stage retirement, carrying bytes the applier
// then read. It also went unpoisoned - NoteResolvedRun skipped every non-stage carrier
// - so rule C's only mechanical control read zero on exactly the record that needed it.
//
// The segment is checked BEFORE the resolve, deliberately: a forged SEG_REPLY run that
// happens to lie inside a mapped reply pool must be refused for naming the wrong
// carrier, not left to pass or fail on whether that pool is mapped at all.
//
// NOT A NEW FATAL FAMILY. The review suggested `Fatal{BlobNotStaged}`; this is
// ProtocolCorruption like every other R-2 honesty arm, because the families are the
// vocabulary the operator and the CI greps share (ProtocolCorruption, AbiMismatch,
// UnmigratedVerb, UnmigratedPipeInput, UnsetCallMask, RingOverrun) and a one-off
// seventh name would be a token nothing else in the tree recognises. The SEGMENT is in
// the message, which is what has to be greppable.
if (blob.Seg != kSegStage) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu "
"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",
WireOpName(op), static_cast<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset),
static_cast<unsigned long long>(blob.Size),
static_cast<unsigned>(kSegStage));
std::abort();
}
if (segments.Resolve(blob.Seg, blob.Offset, blob.Size) == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu "
"does not lie inside that segment (R-2.3)",
@@ -1115,8 +1145,22 @@ namespace MobileGL::MG_Remote::Wire {
}
void 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
// only mechanical control was dark on exactly the record it existed to catch. The one
// caller is ResolveOrFatal, which runs RequireDeclaredBlob first, and that now refuses
// both an undeclared blob and a non-SEG_STAGE one by name. If either ever arrives here
// 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) {
return;
MGLOG_F("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),
static_cast<unsigned long long>(blob.Size),
static_cast<unsigned>(kSegStage));
std::abort();
}
if (m_resolvedCount >= sizeof(m_resolved) / sizeof(m_resolved[0])) {
// LOUD, NOT A SILENT DROP. This array is what the 0xDD fill covers, and a poison
@@ -1500,6 +1500,69 @@ TEST_F(PipeWireCodecTest, ARunThatLeavesItsSegmentIsFatal) {
EXPECT_NE(r.Log.find("does not lie inside that segment"), std::string::npos) << r.Log;
}
TEST_F(PipeWireCodecTest, AContentBlobCarriedOutsideSegStageIsFatalAtTheDecoder) {
// R-2.3's second half, and the verifier's finding-3 fixture kept as its own case
// (wave1-codex-verify.md §3). Contract table 1 row 17 puts CreateSamplerState's bytes in
// SEG_STAGE; here they sit in a mapped SEG_REPLY - the SERVER-owned reply pool, whose
// reuse has nothing to do with stage retirement - and the record names that segment. It
// used to be ACCEPTED and APPLIED, because the only test was that the run resolved
// somewhere: the verifier's probe printed `seg=3 accepted=1 poisoned=0`.
//
// The audit is armed, so the second half of the finding is nailed down too: with the
// poison ON, the record must DIE rather than be applied with PoisonedStageBytes() left at
// zero. NoteResolvedRun used to return silently for any non-stage carrier, which made
// rule C's only mechanical control dark on exactly the record it exists to catch; it is
// now a Fatal of its own and unreachable behind this arm.
//
// I made it red once, by doing X: X = deleting the `blob.Seg != kSegStage` arm in
// CheckBlobIsHonest (PipeWireCodec.cpp). The child then exits 0 instead of aborting and
// this case fails on DiedOfAbort - the verifier's `accepted=1` state.
const ChildResult r = RunInChild([] {
Wire2 wire;
std::vector<std::uint8_t> replyBytes(4096, 0);
SamplerParameters params{};
params.borderColorForm = BorderColorForm::Int;
std::memcpy(replyBytes.data(), &params, sizeof(params));
wire.Segments().Install(kSegReply, SegmentView{replyBytes.data(), replyBytes.size()});
wire.Decoder().SetAuditPoison(true);
MGPSamplerDesc desc{};
desc.Cso = MakeHandle(88);
desc.Parameters.Seg = static_cast<Uint32>(kSegReply);
desc.Parameters.Offset = 0;
desc.Parameters.Size = sizeof(SamplerParameters);
ForgeAndDecode(wire, MGPWireOp::CreateSamplerState, &desc, sizeof(desc), nullptr, 0);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("is not SEG_STAGE"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("CreateSamplerState.blob"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("seg=3"), std::string::npos) << r.Log;
}
TEST_F(PipeWireCodecTest, TheEncoderRefusesTheNonStageCarrierTheDecoderCallsFatal) {
// THE ENCODER MUST NOT ACCEPT A RECORD THE DECODER FATALS ON - the same symmetry
// TheEncoderRefusesThePerStageSpirvRunTheDecoderCallsFatal states one arm over. Under
// `inproc` a SEG_REPLY pointer resolves, so an emitter that staged into the reply pool
// would get a valid seq here and a Fatal on a peer, which is the asymmetry EncodeRecord's
// own honesty loop exists to prevent.
//
// I made it red once, by doing X: X = deleting the `blob.Seg != kSegStage` arm in
// CheckBlobIsHonest. EncodeRecord then returns a real seq and the child exits 0.
const ChildResult r = RunInChild([] {
Wire2 wire;
std::vector<std::uint8_t> replyBytes(4096, 0);
wire.Segments().Install(kSegReply, SegmentView{replyBytes.data(), replyBytes.size()});
MGPSamplerDesc desc{};
desc.Cso = MakeHandle(88);
desc.Parameters.Seg = static_cast<Uint32>(kSegReply);
desc.Parameters.Offset = 0;
desc.Parameters.Size = sizeof(SamplerParameters);
(void)wire.Encoder().EncodeRecord(MGPWireOp::CreateSamplerState, &desc, sizeof(desc));
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("is not SEG_STAGE"), std::string::npos) << r.Log;
}
TEST_F(PipeWireCodecTest, AHalfDeclaredBlobIsFatalRatherThanReadAsAbsent) {
// The shape a MONOLITH emitter produces - Seg None, Offset a host address, Size 0. Reading
// it as "absent" would silently drop the bytes of every record an unconverted emitter sent.