mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[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:
@@ -457,6 +457,36 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
WireOpName(op), static_cast<unsigned long long>(blob.Size));
|
WireOpName(op), static_cast<unsigned long long>(blob.Size));
|
||||||
std::abort();
|
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) {
|
if (segments.Resolve(blob.Seg, blob.Offset, blob.Size) == nullptr) {
|
||||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu "
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu "
|
||||||
"does not lie inside that segment (R-2.3)",
|
"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) {
|
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) {
|
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])) {
|
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
|
// 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;
|
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(), ¶ms, 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) {
|
TEST_F(PipeWireCodecTest, AHalfDeclaredBlobIsFatalRatherThanReadAsAbsent) {
|
||||||
// The shape a MONOLITH emitter produces - Seg None, Offset a host address, Size 0. Reading
|
// 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.
|
// it as "absent" would silently drop the bytes of every record an unconverted emitter sent.
|
||||||
|
|||||||
Reference in New Issue
Block a user