mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 08:38:30 +09:00
[Merge] (MGPipe, P5): integrate package w1, round 2 - ID-48 findings 1, 3, 4
This commit is contained in:
@@ -31,6 +31,9 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
#include <MG_Pipe/PipeApply.h>
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
// R-6's tier gate, and the ONE spelling of it (b1's file, unchanged by this package): the
|
||||||
|
// MapPersistent arm below asks it the same question MGPipeApplyMapPersistent asks.
|
||||||
|
#include <MG_Remote/Client/PersistentMapTracker.h>
|
||||||
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
@@ -457,6 +460,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)",
|
||||||
@@ -717,6 +750,12 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||||
|
// THE WRAP SKIP MAY ONLY BE CHARGED AGAINST BYTES THAT ARE STILL IN FLIGHT. When
|
||||||
|
// there are none the allocator starts over at offset zero, so a blob the segment
|
||||||
|
// can hold whole is never refused (see RebaseEmptyStage). On attempt 1 this runs
|
||||||
|
// AFTER ReclaimStagedBytes, which is the case the finding describes: 8 MiB
|
||||||
|
// allocated, then retired, then a 28 MiB request that used to abort.
|
||||||
|
RebaseEmptyStage();
|
||||||
const Uint64 offset = m_stageHead % m_stageCapacity;
|
const Uint64 offset = m_stageHead % m_stageCapacity;
|
||||||
// A run is always contiguous: one that would straddle the end skips the remainder,
|
// A run is always contiguous: one that would straddle the end skips the remainder,
|
||||||
// exactly as the ring's wrap pad does, and the skipped bytes are reclaimed with
|
// exactly as the ring's wrap pad does, and the skipped bytes are reclaimed with
|
||||||
@@ -936,6 +975,32 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
return m_emitSeq;
|
return m_emitSeq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// THE EMPTY-STAGE REBASE. Head and tail are monotonic byte counts, so "every staged byte
|
||||||
|
// has retired" reads head == tail, NOT head == tail == 0, and `head % capacity` is left
|
||||||
|
// wherever the last run ended. Charging a wrap skip against that offset then costs the
|
||||||
|
// unused suffix a second time: with head == tail == 64 in a 256 KiB stage, the allocator's
|
||||||
|
// test became `2*capacity - 64 <= capacity`, which is false at EVERY occupancy, so a blob
|
||||||
|
// that fits the segment whole was refused with `Fatal{RingOverrun, "SEG_STAGE"}` - whose
|
||||||
|
// own message then reported `0 bytes still in flight`. ReclaimStagedBytes cannot help,
|
||||||
|
// because an already-empty tail has nothing left to move.
|
||||||
|
//
|
||||||
|
// THE MARK QUEUE COMES WITH IT. A mark holds the ABSOLUTE head cursor it was pushed at and
|
||||||
|
// ReclaimStagedBytes assigns that value straight to m_stageTail. Every mark not yet
|
||||||
|
// consumed has StageCursor <= head == tail - the head is monotonic and marks are pushed in
|
||||||
|
// order - so each of them names a region that is already reclaimed and zero is the
|
||||||
|
// truthful rebasing of it. Without that, one reclaim after a rebase would put the tail
|
||||||
|
// AHEAD of the head and StagedBytesInFlight() would underflow to about 2^64.
|
||||||
|
void PipeWireEncoder::RebaseEmptyStage() {
|
||||||
|
if (m_stageHead != m_stageTail || m_stageHead == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_stageHead = 0;
|
||||||
|
m_stageTail = 0;
|
||||||
|
for (SizeT i = 0; i < m_stageMarks.size(); ++i) {
|
||||||
|
m_stageMarks[i].StageCursor = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PipeWireEncoder::ReclaimStagedBytes() {
|
void PipeWireEncoder::ReclaimStagedBytes() {
|
||||||
if (m_control == nullptr) {
|
if (m_control == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -1083,8 +1148,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
|
||||||
@@ -1303,6 +1382,19 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
//
|
//
|
||||||
// DECLINED is a real answer, not a failure: the three frontend sites already
|
// DECLINED is a real answer, not a failure: the three frontend sites already
|
||||||
// tolerate it (BufferObject.cpp:238, :603-606, :657-660).
|
// tolerate it (BufferObject.cpp:238, :603-606, :657-660).
|
||||||
|
//
|
||||||
|
// THE TIER IS CONSULTED HERE, AND IT IS THE SAME CONJUNCTION THE MONOLITH APPLIER
|
||||||
|
// USES (PipeApply.cpp's `Transport != Monolith && AdoptTierIsEmulate()`). The arm
|
||||||
|
// used to decline UNCONDITIONALLY and AdoptTier had no reference anywhere on the
|
||||||
|
// codec path, so MOBILEGL_IPC_ADOPT_TIER=0 and =1 - which contract §5 promises
|
||||||
|
// "parse and are Fatal at use, naming P11" - decoded as an ordinary DECLINED and
|
||||||
|
// the operator got a run that looked like a working T0. AdoptTierIsEmulate returns
|
||||||
|
// true at T2 and ABORTS at T0/T1 on its own named diagnostic, so the return value
|
||||||
|
// is deliberately not a branch: P5 declines at every tier it survives (R-6), and
|
||||||
|
// the two forbidden ones never get this far.
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||||
|
(void)MG_Remote::Client::AdoptTierIsEmulate();
|
||||||
|
}
|
||||||
PostReply(op, seq, ReplySink::kStatusDeclined, nullptr, 0);
|
PostReply(op, seq, ReplySink::kStatusDeclined, nullptr, 0);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,17 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
// RingControl - see ReclaimStagedBytes above.
|
// RingControl - see ReclaimStagedBytes above.
|
||||||
Uint8* StageAllocate(Uint64 size);
|
Uint8* StageAllocate(Uint64 size);
|
||||||
|
|
||||||
|
// AN EMPTY STAGE STARTS OVER AT ZERO, so that the wrap skip is only ever charged
|
||||||
|
// against bytes that are really still in flight. Head and tail are monotonic, so once
|
||||||
|
// everything has retired they are EQUAL BUT NOT ZERO, and `head % capacity` is
|
||||||
|
// wherever the last run happened to end - a wrap skip charged against that offset
|
||||||
|
// costs the suffix a second time and refused a blob the whole segment could hold, with
|
||||||
|
// a message that reported zero bytes in flight while it did so. Rebasing also rewrites
|
||||||
|
// the marks still held: a mark stores an ABSOLUTE head cursor and a later reclaim
|
||||||
|
// assigns it to m_stageTail, so leaving a stale one behind would drive the tail past
|
||||||
|
// the head and underflow StagedBytesInFlight().
|
||||||
|
void RebaseEmptyStage();
|
||||||
|
|
||||||
Transport::RingControl* m_control = nullptr;
|
Transport::RingControl* m_control = nullptr;
|
||||||
Transport::RingProducer* m_cmd = nullptr;
|
Transport::RingProducer* m_cmd = nullptr;
|
||||||
Transport::RingProducer* m_stage = nullptr;
|
Transport::RingProducer* m_stage = nullptr;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
|
|
||||||
|
// MG_Config::Transport and MG_Config::Ipc.AdoptTier: the two knobs R-6's tier gate reads.
|
||||||
|
#include <Config.h>
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
#include <MG_Pipe/PipeApply.h>
|
#include <MG_Pipe/PipeApply.h>
|
||||||
@@ -516,6 +518,37 @@ TEST_F(PipeWireCodecTest, KReplySlotMapPersistentIsAConstantDecline) {
|
|||||||
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, TierTwoUnderSplitTransportStillDeclinesRatherThanRefusing) {
|
||||||
|
// The POSITIVE half of the two AdoptTier death cases below. Without it, those two could
|
||||||
|
// be satisfied by an arm that aborted on every tier, which is the opposite mistake to the
|
||||||
|
// one wave1-codex-verify.md §4 found. T2 is the only tier P5 implements and R-6 says the
|
||||||
|
// answer there is DECLINED - a real answer, not a failure - even when the transport is
|
||||||
|
// the split one that makes the tier question live at all.
|
||||||
|
const MG_Config::TransportMode savedTransport = MG_Config::Transport;
|
||||||
|
const Uint32 savedTier = MG_Config::Ipc.AdoptTier;
|
||||||
|
struct Restore {
|
||||||
|
MG_Config::TransportMode T;
|
||||||
|
Uint32 A;
|
||||||
|
~Restore() {
|
||||||
|
MG_Config::Transport = T;
|
||||||
|
MG_Config::Ipc.AdoptTier = A;
|
||||||
|
}
|
||||||
|
} restore{savedTransport, savedTier};
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
|
MG_Config::Ipc.AdoptTier = 2u;
|
||||||
|
|
||||||
|
Wire2 wire;
|
||||||
|
const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::MapPersistent, &handle, sizeof(handle)),
|
||||||
|
kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
ASSERT_EQ(wire.Answers().All.size(), 1u);
|
||||||
|
EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined);
|
||||||
|
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
|
TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
|
||||||
// Contract table 1 row 19b. Without the carrier every per-level glTexImage*D would take
|
// Contract table 1 row 19b. Without the carrier every per-level glTexImage*D would take
|
||||||
// the whole-resource arm on the far side and eat the other levels' pending uploads, so
|
// the whole-resource arm on the far side and eat the other levels' pending uploads, so
|
||||||
@@ -1135,6 +1168,43 @@ TEST_F(PipeWireCodecTest, StagedBytesAreReclaimedOnlyBehindRetiredSeq) {
|
|||||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
wire.Encoder().ReclaimStagedBytes();
|
wire.Encoder().ReclaimStagedBytes();
|
||||||
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), 0u);
|
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), 0u);
|
||||||
|
|
||||||
|
// ---- AND AN EMPTY STAGE TAKES THE WHOLE SEGMENT ---------------------------------
|
||||||
|
// The verifier's extension of this case, kept (wave1-codex-verify.md §1). The 64-byte
|
||||||
|
// run has retired and in-flight bytes are ZERO, so every byte of SEG_STAGE is free -
|
||||||
|
// but head and tail are monotonic and both sit at 64, so `head % capacity` is 64 and the
|
||||||
|
// allocator used to charge a `capacity - 64` wrap skip against a capacity that had
|
||||||
|
// nothing in it. The test then read `2*capacity - 64 <= capacity`, false at every
|
||||||
|
// occupancy, and a blob the segment holds WHOLE aborted with
|
||||||
|
// `Fatal{RingOverrun, "SEG_STAGE"} ... with 0 bytes still in flight`.
|
||||||
|
//
|
||||||
|
// I made it red once, by doing X: X = deleting the `RebaseEmptyStage()` call at the top
|
||||||
|
// of StageAllocate's attempt loop (PipeWireCodec.cpp). The case then dies with SIGABRT
|
||||||
|
// inside PipeWireEncoder::StageAllocate on that message, exactly as the verifier
|
||||||
|
// recorded it.
|
||||||
|
//
|
||||||
|
// THE EXACT MAXIMUM. `need = Align8(size)` and the first bound is `need > capacity`, so
|
||||||
|
// an empty stage takes a blob of exactly the capacity the encoder adopted - here
|
||||||
|
// Wire2::kStageBytes, and in a real session the whole SEG_STAGE view, i.e.
|
||||||
|
// MOBILEGL_IPC_STAGE_MB (32 MiB by default; SessionRings.h keeps SEG_STAGE un-ringed and
|
||||||
|
// un-rounded, so there is no control page to subtract).
|
||||||
|
const Uint64 maxRecordBefore = wire.Encoder().MaxRecordBytesSeen();
|
||||||
|
std::vector<std::uint8_t> whole(Wire2::kStageBytes, 0x5A);
|
||||||
|
const MGPBlobRef full = wire.Encoder().StageBytes(whole.data(), whole.size());
|
||||||
|
EXPECT_EQ(full.Offset, 0u) << "an empty stage must hand a whole-capacity blob offset zero";
|
||||||
|
EXPECT_EQ(full.Size, Wire2::kStageBytes);
|
||||||
|
EXPECT_EQ(full.Seg, static_cast<Uint32>(kSegStage));
|
||||||
|
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), Wire2::kStageBytes);
|
||||||
|
const void* back = wire.Segments().Resolve(full.Seg, full.Offset, full.Size);
|
||||||
|
ASSERT_NE(back, nullptr);
|
||||||
|
EXPECT_EQ(back, wire.StageBase());
|
||||||
|
|
||||||
|
// R-10's max-record counter DOES NOT SEE IT, and that is the point of R-10's carrier
|
||||||
|
// rule: EncodeRecord feeds m_maxRecordBytes from `layout.TotalBytes` - header + payload +
|
||||||
|
// tails, all of it SEG_CMD - while the blob leaves only {Seg, Offset, Size} in the
|
||||||
|
// record. A quarter-megabyte of staging moved the counter by zero bytes. SEG_STAGE has
|
||||||
|
// its own bound and its own named Fatal, and MaxRecordBytesSeen() is not it.
|
||||||
|
EXPECT_EQ(wire.Encoder().MaxRecordBytesSeen(), maxRecordBefore);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- M2 / M3: SEG_STAGE's cursors and the mark queue --------------------------------------
|
// ---- M2 / M3: SEG_STAGE's cursors and the mark queue --------------------------------------
|
||||||
@@ -1463,6 +1533,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.
|
||||||
@@ -1718,6 +1851,59 @@ TEST_F(PipeWireCodecTest, ASecondProcessResolverIsFatalRatherThanASilentRace) {
|
|||||||
EXPECT_NE(r.Log.find("already installed"), std::string::npos) << r.Log;
|
EXPECT_NE(r.Log.find("already installed"), std::string::npos) << r.Log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- R-6 / contract §5: the two forbidden adoption tiers die ON THE WIRE PATH TOO --------
|
||||||
|
//
|
||||||
|
// wave1-codex-verify.md §4: `AdoptTier` had ZERO references anywhere on the codec path, so
|
||||||
|
// MOBILEGL_IPC_ADOPT_TIER=0 and =1 - which contract §5 promises "parse and are Fatal at use,
|
||||||
|
// naming P11" - decoded as an ordinary DECLINED. The verifier set each forbidden tier inside
|
||||||
|
// KReplySlotMapPersistentIsAConstantDecline and watched its successful-decline assertions
|
||||||
|
// still pass, on BOTH tiers.
|
||||||
|
//
|
||||||
|
// THESE ARE FORKED, NOT EXPECT_DEATH, for the reason at the top of this file - and forking is
|
||||||
|
// what lets the case REQUIRE THE DIAGNOSTIC rather than any abort: r.Log is searched for the
|
||||||
|
// exact sentence AdoptTierIsEmulate prints. ID-46 finding 10 is an empty death regex; the
|
||||||
|
// EXPECT_NE lines below are the opposite of that, and a crash for any other reason fails the
|
||||||
|
// case on the log it prints.
|
||||||
|
//
|
||||||
|
// I made both red once, by doing X: X = restoring the unconditional decline in
|
||||||
|
// PipeWireCodec.cpp's MapPersistent arm (deleting the AdoptTierIsEmulate call). Both children
|
||||||
|
// then exit 0 having posted a clean DECLINED, and both cases fail on DiedOfAbort.
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, AdoptTierZeroIsFatalOnTheWirePathAndNamesP11) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
// The child dies; nothing needs restoring. The transport half is the same conjunction
|
||||||
|
// MGPipeApplyMapPersistent uses - a monolith TRANSPORT mints like push (ID-42) and is
|
||||||
|
// not the arm this record can arrive on.
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
|
MG_Config::Ipc.AdoptTier = 0u;
|
||||||
|
Wire2 wire;
|
||||||
|
const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer);
|
||||||
|
(void)wire.Encoder().EncodeRecord(MGPWireOp::MapPersistent, &handle, sizeof(handle));
|
||||||
|
bool applied = false;
|
||||||
|
(void)wire.PumpOne(&applied);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("MOBILEGL_IPC_ADOPT_TIER=0 names adoption tier T0, which P11 implements"),
|
||||||
|
std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, AdoptTierOneIsFatalOnTheWirePathAndNamesP11) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
|
MG_Config::Ipc.AdoptTier = 1u;
|
||||||
|
Wire2 wire;
|
||||||
|
const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer);
|
||||||
|
(void)wire.Encoder().EncodeRecord(MGPWireOp::MapPersistent, &handle, sizeof(handle));
|
||||||
|
bool applied = false;
|
||||||
|
(void)wire.PumpOne(&applied);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("MOBILEGL_IPC_ADOPT_TIER=1 names adoption tier T1, which P11 implements"),
|
||||||
|
std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
TEST_F(PipeWireCodecTest, TheFatalArmsNeedFork) {
|
TEST_F(PipeWireCodecTest, TheFatalArmsNeedFork) {
|
||||||
|
|||||||
Reference in New Issue
Block a user