Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
33 KiB
Wave-1 adversarial review — verification of the ten "how to confirm" perturbations
Every perturbation the cross-family review proposed but did not execute has now been executed.
- Tree:
~/w7/p5-verify, branchp5/verify, from the integration head4ba14fa4(the review itself read5e5bf7b9; the delta is noted per finding where it matters). - Build:
build-split=-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ONover the preamble's$COMMON. GeneratorNinja, compiler/usr/sbin/clang++,build-split/CTestTestfile.cmakepresent, 1974 ctest entries under labelunit. - Method: one perturbation at a time; rebuild; run exactly the named test(s); revert with
git checkout -- <files>;git status --porcelainempty before the next. Every perturbed run below is paired with the same test's unperturbed line, so no verdict rests on a build error. - Nothing is committed.
git statusis empty; the worktree is left in place at4ba14fa4.
Two build-harness notes, both of them the preamble's C-10 trap, both hit for real
- The first
build-splitconfigure lost$COMMONin the argument path and silently produced a GCC / Unix Makefiles tree that then failed to compile.CTestTestfile.cmakeexisted the whole time, so the preamble's check alone would not have caught it; what caught it was readingCMAKE_GENERATOR:INTERNALandCMAKE_CXX_COMPILERout ofCMakeCache.txt. Every build below runs from~/w7/p5-verify-build.sh/~/w7/p5-verify-bt.sh, i.e. a script file, never an inline command string. - One early perturbation run reported
build rc=0on a build that had emitted9 errors generated. ninja: build stopped, and the ctest that followed ran a stale binary and passed.~/w7/p5-verify-bt.shnow refuses to run any test when the build's exit status is non-zero. That result was discarded and re-run; the finding-4 numbers below are from the re-run.
Pre-existing red at the integration head — needs an integrator ruling, not in the review
Before any perturbation, on the pristine tree:
$ cd ~/w7/p5-verify/build-split && ctest -L unit -j 14
99% tests passed, 1 tests failed out of 1974
The following tests FAILED:
1947 - PipeWireCodecTest.NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot (Failed) unit
../MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp:1203: Failure
Expected equality of these values:
MGPipeCallFlagsFor(MGPWireOp::ResourceCreate) & static_cast<Uint32>(kReplySlot)
Which is: 16
0u
Which is: 0
Two landed commits disagree:
d55e3733 [Fix] (MG_Pipe): give the four acceptance calls the kReplySlot their applier entry points have always answered through—PipeCalls.def:100now hasX(ResourceCreate, MGPResourceDesc, kScreen, kReplySlot).79b78665 [Fix] (MG_Remote, Wire): ... answer a reply slot only for rows the catalogue flags kReplySlot— wroteASSERT_EQ(... & kReplySlot, 0u)for exactly that row.
ReplySlot.h's own header rules for d55e3733's side ("the four Bool acceptance entry points —
ResourceCreate, ResourceRespecify, ResourceSubData, SetTextureParams — say false (R-5)"), so the
test is the stale half. It is a catalogue-flag question either way, so it is the integrator's,
not a package's. This is the unperturbed-tree baseline for every "the unit lane is otherwise
green" statement below.
1. Empty staging segments can reject a blob that fits — CONFIRMED
Perturbation. Exactly the finding's: extend
PipeWireCodecTest.StagedBytesAreReclaimedOnlyBehindRetiredSeq so that, after its 64-byte
allocation has retired and StagedBytesInFlight() == 0, it calls StageBytes with a source of
exactly Wire2::kStageBytes bytes and asserts Offset == 0.
(~/w7/p5-verify-f1-perturb.py)
Unperturbed
1/1 Test #1944: PipeWireCodecTest.StagedBytesAreReclaimedOnlyBehindRetiredSeq ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Perturbed
=== build target=PipeWireCodecTest rc=0
1/1 Test #1944: PipeWireCodecTest.StagedBytesAreReclaimedOnlyBehindRetiredSeq ...Subprocess aborted***Exception: 0.01 sec
0% tests passed, 1 tests failed out of 1
The test's own probe, printed immediately before the call:
FINDING1: inFlight=0 capacity=262144 requesting=262144
and the FINDING1: ALLOCATED ... line that follows the call never prints. The abort is inside the
allocator, not anywhere else:
$ gdb -batch -ex run -ex bt ./MobileGL/MG_Test/Wire/PipeWireCodecTest \
--args ... --gtest_filter=PipeWireCodecTest.StagedBytesAreReclaimedOnlyBehindRetiredSeq
Program received signal SIGABRT, Aborted.
#2 0x00007ffff782567d in abort () from /usr/lib/libc.so.6
#3 0x0000555555658ccb in MobileGL::MG_Remote::Wire::PipeWireEncoder::StageAllocate(unsigned long) ()
#4 0x0000555555658e72 in MobileGL::MG_Remote::Wire::PipeWireEncoder::StageBytes(void const*, unsigned long) ()
#5 0x00005555555d63d3 in PipeWireCodecTest_StagedBytesAreReclaimedOnlyBehindRetiredSeq_Test::TestBody() ()
need == m_stageCapacity, so the first bound (PipeWireCodec.cpp:709,
if (need > m_stageCapacity)) does not fire; the abort is the second one at :734, the
Fatal{RingOverrun} whose own message reports %llu bytes still in flight — with zero in
flight.
Discriminator (the part w1-v1.md:341 gets wrong). The identical request on a fresh encoder succeeds, so the rejection is the retired head offset and nothing else:
FINDING1B: fresh encoder ALLOCATED offset=0 size=262144
[ OK ] PipeWireCodecTest.Finding1FreshEncoderTakesTheWholeCapacity (0 ms)
Arithmetic, from PipeWireCodec.cpp:720-728: head = tail = 64, offset = 64,
skip = capacity - 64, and the test is (head + skip + need) - tail <= capacity, i.e.
2*capacity - 64 <= capacity — false at every occupancy, and ReclaimStagedBytes() cannot move an
already-empty tail, so the second attempt fails identically.
- Fix shape: when
m_stageHead == m_stageTail, reset both cursors to zero before computing the wrap skip (or charge the skip against the freed suffix instead of against capacity). - Control (R-16): the extension above, kept — "red once by doing X" = X is removing the
empty-stage reset, which returns the case to today's
SIGABRTinStageAllocateatinFlight=0. - Owner: w1 (
MG_Remote/Wire/PipeWireCodec.cpp). - No integrator ruling needed — no wire struct, no segment size, no catalogue flag.
2. Default reply geometry cannot hold its advertised readback — CONFIRMED, and the E2 half the review left "unverified" is now verified and worse
(a) MaxReplyBytes() from the constants. SessionSegmentSizes::ReplyBytes = 8 MiB
(SessionRings.h:89), ReplySlotCount = kDefaultReplySlotCount = 8 (ReplySlot.h:89),
slotBytes = 8388608 / 8 = 1048576, sizeof(ReplySlotHeader) == 16 (static_assert,
ReplySlot.h:70), so MaxReplyBytes() = 1048576 - 16 = 1048560 (ReplySlot.h:150).
512×512×4 = 1048576. Short by 16 bytes.
(b) The throwaway case (not committed; ~/w7/p5-verify-f2-perturb.py) builds a default
SessionSegments + ReplySlotPool and posts 512×512×4:
FINDING2: SessionSegmentSizes::ReplyBytes=8388608 slots=8 SlotBytes=1048576 sizeof(ReplySlotHeader)=16 MaxReplyBytes=1048560
FINDING2: 512x512 RGBA8 = 1048576 bytes -> DOES NOT FIT the slot (short by 16)
FINDING2: 640x480 RGBA8 (OpenRA's declared size) = 1228800 bytes -> DOES NOT FIT the slot (short by 180240)
[ OK ] SessionTestFinding2.DefaultReplyGeometryAgainstA512x512Rgba8Answer (4 ms)
The case's EXPECT_DEATH(pool.Post(1, kReplyStatusOk, answer.data(), answer.size()), "") passes,
so Post takes the oversize-abort branch (ReplySlot.h:182) — the review's predicted branch,
not a truncation and not a silent short write.
(c) Does the OpenRA E2 retrace actually issue a full-surface ReadPixels? YES. No LFS was
fetched: tools/trace_replay/fixtures/openra.tgz is the one hydrated fixture (7,232,355 bytes on
disk), and the system apitrace 14.0-1 dumped it locally.
- The trace itself issues none.
apitrace dump --calls=0-31249 openra.traceover all 31,249 calls to the target: zero matches forReadPixels/ReadnPixels/GetTexImage. - The harness issues one, unconditionally. The snapshot the SSIM comparison needs comes from
apitrace's
getDrawBufferImage, which for an ES context forcesformat = GL_RGBA; type = GL_UNSIGNED_BYTE(3rdparty/apitrace/retrace/glstate_images.cpp:1465-1469) and then callsglReadPixels(0, 0, width, height, format, type, image->pixels)(:1492) — the whole surface, 4 bytes per pixel. - That call is routed into MobileGL, not the host GL:
tools/trace_replay/apitrace_glproc_mobilegl.cpp:126-127returnsMobileGLTraceReadPixelsforglReadPixels, which forwards toLookupSymbol("glReadPixels")in the loaded MobileGL library. - The surface is 640×480:
trace_cases.json:22-23, the trace's lastglViewport(0, 0, 640, 480), andopenra.0000031249.pngisPNG image data, 640 x 480.
So the E2 split retrace's single readback is 1,228,800 bytes, 180,240 over the slot cap —
Post aborts, not marginally but by 17%. It does not fire today only because EmitAndWait /
ApplyOne are still stubs; it fires on the commit that lands c1.
- Fix shape: size
SEG_REPLYfrom the largest scenario read that contract §2 row 23 already mandates — at these defaults, 8 slots × 2 MiB (16 MiBReplyBytes), or keep 8 MiB and drop to 4 slots, plus a client-side pre-emit check againstMaxReplyBytes()that names the geometry. - Control (R-16): a case that posts exactly
640*480*4into the default pool and requires it to succeed — "red once by doing X" = X is revertingReplyBytesto 8 MiB at 8 slots, which returns it to theEXPECT_DEATHabove. - Owner: s1 (
Transport/ReplySlot.h,Transport/SessionRings.h). - ⚠ INTEGRATOR RULING REQUIRED — this changes a SEGMENT SIZE.
SessionSegmentSizes::ReplyBytesand/orReplySlotCountare contract §2 row 23 and contract §5 numbers; and §5 has no knob for SEG_REPLY at all (MOBILEGL_IPC_RING_MBandMOBILEGL_IPC_STAGE_MBmove SEG_CMD and SEG_STAGE only — the review is right that raising them does not enlarge this pool), so either the default moves or §5 gains a row. Both are c0's file. - Also falsifies
s1-v1.md:134("1 MiB, which covers a 512×512 RGBA8ReadPixels") — by 16 bytes.
3. Blob validation accepts the wrong segment and bypasses staging audit — CONFIRMED
Perturbation. Exactly the finding's: the sampler round-trip fixture
(PipeWireCodecTest.SamplerParametersCrossByteForByteIncludingBorderColorForm) installs a mapped
SEG_REPLY view holding the SamplerParameters and the record references that view
(Parameters.Seg = kSegReply, Offset = 0, Size = sizeof(SamplerParameters)) instead of staging
them; Decoder().SetAuditPoison(true) arms rule C's mechanical control; the case then asserts
rejection. (~/w7/p5-verify-f3-perturb.py)
Unperturbed
1/1 Test #1937: PipeWireCodecTest.SamplerParametersCrossByteForByteIncludingBorderColorForm ... Passed 0.00 sec
Perturbed
=== build target=PipeWireCodecTest rc=0
1/1 Test #1937: PipeWireCodecTest.SamplerParametersCrossByteForByteIncludingBorderColorForm ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
FINDING3: seg=3 accepted=1 poisoned=0
[ OK ] PipeWireCodecTest.SamplerParametersCrossByteForByteIncludingBorderColorForm (0 ms)
seg=3 is kSegReply — a server-owned segment. accepted=1 is DecodeAndApply returning
true, so the encoder and the decoder accepted the record and the sampler applier received those
bytes. poisoned=0 with the audit on is the second half: NoteResolvedRun
(PipeWireCodec.cpp:1109) opens with if (blob.Size == 0 || blob.Seg != kSegStage) { return; },
so the run was never recorded and PoisonedStageBytes() stayed zero. RequireDeclaredBlob
(:454-467) checks only that the blob resolves inside some segment — there is no SEG_STAGE
requirement anywhere on the path, though contract §2 table 1 row 17 assigns CreateSamplerState's
bytes to SEG_STAGE and nowhere else.
- Fix shape: in
RequireDeclaredBlob, requireblob.Seg == kSegStagefor every client→server content blob (contract §2 Groups A/B/C),Fatal{ProtocolCorruption}naming the segment otherwise — which also makesNoteResolvedRun's filter unreachable rather than silently load-bearing. - Control (R-16): the perturbed fixture above, kept as its own case asserting the refusal by
message — "red once by doing X" = X is removing the
kSegStagerequirement, which returns it toseg=3 accepted=1 poisoned=0. - Owner: w1 (
MG_Remote/Wire/PipeWireCodec.cpp). - No integrator ruling needed for the minimal fix (§2 already mandates
SEG_STAGE); it becomes one only if w1 wants a per-op legal segment column generated out of the catalogue.
4. Wire MapPersistent bypasses the adoption-tier refusal — CONFIRMED (both forbidden tiers)
ConfigLoader, as asked. ConfigLoader.cpp:371 is
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2); — 0, 1 and 2 are all
accepted, exactly as contract §5 intends ("0 and 1 parse and are Fatal at use, naming P11").
Is there any tier check on the wire decode path? No — not one. AdoptTier appears in exactly
six places tree-wide, and none of them is the codec:
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:118: Bool AdoptTierIsEmulate() {
MobileGL/MG_Remote/Client/PersistentMapTracker.h:137: Bool AdoptTierIsEmulate();
MobileGL/Config.h:488: Uint32 AdoptTier = 2;
MobileGL/MG_Pipe/PipeApply.cpp:2006: MG_Remote::Client::AdoptTierIsEmulate()) {
MobileGL/ConfigLoader.cpp:371: ...
MobileGL/MG_Test/Buffer/SplitBufferTest.cpp: ... (the T0/T1 death test, on the applier helper)
PipeApply.cpp:2005-2007 is the only production caller
(if (MG_Config::Transport != Monolith && MG_Remote::Client::AdoptTierIsEmulate())), and
PipeWireCodec.cpp:1296 never reaches it — the decoder's MapPersistent arm is an unconditional
PostReply(op, seq, ReplySink::kStatusDeclined, nullptr, 0); return true;.
Perturbation. Exactly the finding's: set split transport and each forbidden tier inside
PipeWireCodecTest.KReplySlotMapPersistentIsAConstantDecline, RAII-restored.
(~/w7/p5-verify-f4-perturb.py)
Unperturbed
1/1 Test #1923: PipeWireCodecTest.KReplySlotMapPersistentIsAConstantDecline ... Passed 0.00 sec
Perturbed, MG_Config::Transport = InProcess + Ipc.AdoptTier = 0
=== build target=PipeWireCodecTest rc=0
1/1 Test #1923: PipeWireCodecTest.KReplySlotMapPersistentIsAConstantDecline ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Perturbed, same but Ipc.AdoptTier = 1
=== build target=PipeWireCodecTest rc=0
1/1 Test #1923: PipeWireCodecTest.KReplySlotMapPersistentIsAConstantDecline ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Both forbidden tiers: the successful-decline assertions
(Status == kStatusDeclined, Bytes.empty()) still pass and no fatal occurs. Contract §5's
"0 and 1 parse and are Fatal at use" is not honoured on the wire path, only on the monolith
applier path.
- Fix shape: call
MG_Remote::Client::AdoptTierIsEmulate()in the codec'sMapPersistentarm before posting DECLINED, so the split path refuses T0/T1 at the same point the applier does. - Control (R-16): a forked/death case that sets
Ipc.AdoptTier = 0and requires theMOBILEGL_IPC_ADOPT_TIER=0 names adoption tier T0, which P11 implementsdiagnostic — "red once by doing X" = X is removing that call, which returns it to the green decline above. - Owner: w1 (the call site is
MG_Remote/Wire/PipeWireCodec.cpp:1296);AdoptTierIsEmulateitself is b1's and needs no change. - No integrator ruling needed — no wire struct, no segment size, no catalogue flag.
(
PersistentMapTracker.cpp:118's own comment already files the "move it to parse time" variant for the integrator, becauseConfigLoader.cppis c0's; the fix above does not need that.)
5. The new pad-bit regression test never sends a pad bit — CONFIRMED (but the head already carries a working replacement)
Perturbation. Exactly the finding's: delete && header.kind == kRingPadRecordKind from
RingConsumer::Pop (Ring.cpp:272), leaving if ((header.flags & kRecPad) != 0) {.
Unperturbed
1/1 Test #1901: SessionTest.ARecordWearingThePadBitIsDeliveredRatherThanEatenAsAFiller ... Passed 0.00 sec
Perturbed
=== build target=SessionTest rc=0
1/1 Test #1901: SessionTest.ARecordWearingThePadBitIsDeliveredRatherThanEatenAsAFiller ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
The named control cannot go red for its own reason, exactly as claimed: RingProducer::Reserve
(Ring.cpp:178) stores header.flags = flags & ~kRecPad, so the test's Reserve(7, kRecPad, 16)
puts flags = 0 on the wire and the consumer's pad arm is never entered either way.
What the review could not know, because it read 5e5bf7b9. Head 4ba14fa4 landed
ID-43 RingTest.TheTwoFlagSpacesAreDisjointByTranslation, which stamps the header outside
Reserve. Under the same perturbation:
1/1 Test #1859: RingTest.TheTwoFlagSpacesAreDisjointByTranslation ...***Failed 0.00 sec
0% tests passed, 1 tests failed out of 1
ctest exit=8
So the defence is covered at head — by a different test than the one s1's report names.
- Fix shape: one line —
SessionTest.cpp:1036's producer must write the header itself (or call the same path ID-43's case uses) instead of going throughReserve, which strips the bit. - Control (R-16):
RingTest.TheTwoFlagSpacesAreDisjointByTranslationalready is one — "red once by doing X" = X is theRing.cpp:272deletion above, shown red here. - Owner: s1 (
MG_Test/Wire/SessionTest.cpp). Cheapest correct action is to fixs1-v1.md §8.2, which names the defective case as its kind-check regression control, to nameRingTest.TheTwoFlagSpacesAreDisjointByTranslationinstead — and fix or delete the SessionTest case. - No integrator ruling needed.
6. ABI sensitivity tests exercise a helper production no longer calls — CONFIRMED
Perturbation. Exactly the finding's: CapsCodec.cpp:472's body replaced with
Uint64 CapsAbiFingerprint() { return 1; } — every ABI input removed, a fixed nonzero returned.
Unperturbed
1/1 Test #1894: SessionTest.TheAbiFingerprintChangesWhenAnyOfItsInputsDoes ... Passed 0.00 sec
1/1 Test #1955: PipeWireCodecTest.TheAbiFingerprintIsStableWithinABuildAndNotZero ... Passed 0.00 sec
Perturbed
=== build rc=0
1/1 Test #1894: SessionTest.TheAbiFingerprintChangesWhenAnyOfItsInputsDoes ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
1/1 Test #1955: PipeWireCodecTest.TheAbiFingerprintIsStableWithinABuildAndNotZero ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
Both remain green. And so does the entire unit lane — under this perturbation
ctest -L unit -j 14 gives 99% tests passed, 1 tests failed out of 1974, the one failure being
the pre-existing NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot above. Nothing in the tree
detects that the real handshake fingerprint stopped depending on any ABI input.
The mechanism, sharper than the review states it. MixAbiFingerprint has zero production
call sites; its only callers in the whole tree are SessionTest.cpp:776-787. The two handshakes
call the other function: ServerSession.cpp:250 and ClientSession.cpp:158, both
CapsAbiFingerprint().
- Fix shape: make
CapsAbiFingerprint()beMixAbiFingerprint(sizeof…, …, GIT_COMMIT_HASH_SHORT)— one implementation, the sensitivity test then reaches production through it — or deleteMixAbiFingerprintand give the sensitivity test injectable inputs on the real function. - Control (R-16): the sensitivity case re-pointed at the production entry point — "red once by
doing X" = X is exactly this perturbation,
return 1;, which must then failSessionTest.TheAbiFingerprintChangesWhenAnyOfItsInputsDoes. - Owner: s1 (
MG_Remote/CapsCodec.cpp,Transport/Ring.cpp+SessionRings.h,MG_Test/Wire/SessionTest.cpp— all on s1's list). - No integrator ruling needed — the fingerprint is a handshake value, not a wire struct field.
7. The null-union regression test never reaches either handshake guard — CONFIRMED
Perturbation. Exactly the finding's: delete envelope->msg_as_Hello() == nullptr from
ServerSession.cpp:233 and envelope->msg_as_Welcome() == nullptr from ClientSession.cpp:198,
leaving each guard as if (envelope == nullptr || envelope->msg_type() != …) {.
Unperturbed
1/1 Test #1898: SessionTest.AVerifiableFrameCanCarryANullUnionPayload ... Passed 0.00 sec
Perturbed
=== build target=SessionTest rc=0
1/1 Test #1898: SessionTest.AVerifiableFrameCanCarryANullUnionPayload ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
And the whole unit lane with both guards gone: 99% tests passed, 1 tests failed out of 1974
— again only the pre-existing red. Nothing in the tree protects those two guards. The case
(SessionTest.cpp:931) constructs and verifies its own envelope and observes its own null member;
it never calls ServerSession::Accept or ClientSession::Start, so it proves a FlatBuffers
property and nothing about MobileGL.
- Fix shape: drive the null-union frame through
Accept/Startand requireMOBILEGL_ERR_PROTOCOL_MISMATCHplus the "not a verifiable Hello/Welcome" diagnostic — the existing case stays as the reachability premise, renamed to say so. - Control (R-16): "red once by doing X" = X is deleting either
msg_as_*() == nullptrclause, shown green here and required to be red after the fix. - Owner: s1 (
Session*,MG_Test/Wire/SessionTest.cpp). - No integrator ruling needed.
- Also falsifies
s1-v1.md:338("so the guard cannot be simplified away").
8. CI negative controls still accept unrelated failures — CONFIRMED (both halves)
CI cannot be run here, so each control's run: block was extracted to a local script with its
inputs stubbed, as instructed. The blocks are byte-for-byte the workflow's; only
RUNNER_TEMP / GITHUB_WORKSPACE / ${{ matrix.* }} and the ctest binary were substituted.
(a) E1 / E3(a) — .github/workflows/test.yml:1092, :1114
Harness ~/w7/p5-verify-f8-e1-harness.sh, stub ~/w7/p5-verify-f8-stub/ctest. Stub behaviour,
chosen to be the best case for the control: ctest -N reports a non-empty selection
(one matching test), and the control's own run fails with a reason that has nothing to do with the
knob.
### the stubbed world:
### ctest -N -> one matching test (a NON-EMPTY selection, as the finding requires)
### ctest run -> exits 1 after printing UNRELATED_CONTROL_FAILURE
1/1 Test #1: DirectGLES.Split.TriangleScenario ...***Failed
split entries that actually ran: 1
1/1 Test #1: DirectGLES.Split.TriangleScenario ...***Failed
UNRELATED_CONTROL_FAILURE: the harness aborted in setup before the knob was read
negative control E1 (MOBILEGL_IPC_VERB_BARRIER=0) turned 1 selected entries red, as it must
1/1 Test #1: DirectGLES.Split.TriangleScenario ...***Failed
UNRELATED_CONTROL_FAILURE: the harness aborted in setup before the knob was read
negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0) turned 1 selected entries red, as it must
HARNESS_RC=0 (the step would have SUCCEEDED)
HARNESS_EXIT=0
Both controls print their success message on a failure that is not theirs, and the step exits 0. Two distinct defects, both as claimed:
run_controltreats any non-zero ctest exit as "the knob is load-bearing". Thematched >= 1guard above it checks the selection is non-empty but says nothing about why the run failed.- The arming run
ctest … || true+ the JUnit counter counts a case that ran and FAILED as "ran" (armed: 1), because the python only excludes<skipped/>andstatus in ('notrun','disabled').
(b) The pull-library retrace control — .github/workflows/test.yml:1980, :1988
Harness ~/w7/p5-verify-f8-retrace-harness.sh. The identity check is exercised for real against a
compiled ELF .so defining no MG_Remote symbol, and the ctest is the real ctest in the
real build-split, with the finding's perturbation — a case/backend regex matching no tests.
--- the identity check PASSED: the stand-in defines no MG_Remote symbol
...
No tests were found!!!
Errors while running CTest
--- real ctest exit for a regex matching NO tests: 8
the pull library turned the split retrace red, as it must (ctest exit 8)
HARNESS_RC=0 (the step would have SUCCEEDED)
HARNESS_EXIT=0
--no-tests=error on an empty selection is exit 8, and the step's final
if [ "${control_rc}" -eq 0 ] accepts it. This control has no ctest -N selection guard at all
— unlike run_control — so an empty selection is the only thing it cannot distinguish from a
working transport-identity assertion. Its nm check establishes library identity and nothing about
why the replay failed, exactly as the review says.
- Fix shape: both controls must assert their own failure reason: grep the failing case's
output for the knob's / the transport assertion's named diagnostic
(
run_trace_case.cmake's transport-resolution message for the retrace control, and the BARRIER-PULLED / persistent-push wording for E1/E3(a)), and the retrace control additionally needs actest -N … | grep -c '^ *Test *#'selection guard likerun_control's. The arming counter must count passed cases, not "not skipped". - Control (R-16): the two harnesses above, kept in-repo as a control-run smoke test — "red once
by doing X" = X is exactly what is shown here (an
UNRELATED_CONTROL_FAILUREwith a non-empty selection; and a no-match regex), which must make the step exit non-zero after the fix. - Owner: t1 (
.github/workflows/*). - No integrator ruling needed.
- Also falsifies
t1-v1.md:419("so the control cannot pass for the wrong reason either").
9. gen_pipe's self-test retains the any-SystemExit defect fixed in p1 — CONFIRMED, including the exit-code-zero clause
Perturbation. Exactly the finding's: scripts/gen_pipe.py:1425's unknown-flag control
callback replaced, lambda: check_call_flags_are_known([flag_typo]) →
lambda: sys.exit("unrelated parser failure").
Unperturbed (python3 scripts/gen_pipe.py --self-test)
gen_pipe: self-test call flag that is not an MGPipeCallFlags enumerator: tripped as expected (PipeCalls.def: Canned carries flag kHasBlobb, which is not an MGPipeCallFlags enumerator (kNeedsAck,)
gen_pipe: self-test: 9 negative-control trip(s), positive control OK
RC=0
Perturbed
gen_pipe: self-test call flag that is not an MGPipeCallFlags enumerator: tripped as expected (unrelated parser failure)
gen_pipe: self-test: 9 negative-control trip(s), positive control OK
RC=0
Nine trips, positive control OK, exit 0 — the control contributed its trip for a diagnostic that has nothing to do with the guard it names, and the nine-control success condition is unchanged.
The exit-code-zero half of the claim, also true. Same line replaced with lambda: sys.exit(0):
gen_pipe: self-test call flag that is not an MGPipeCallFlags enumerator: tripped as expected (0)
gen_pipe: self-test: 9 negative-control trip(s), positive control OK
expect_trip (:1379) catches bare except SystemExit as trip and never looks at the message or
the code, so a successful exit reads as a trip.
- Fix shape: give
expect_tripa third parameter — a required substring of the guard's message — and assertstr(trip)contains it (and that the code is not 0), which is precisely whatgen_pipe_field_ownership.pyalready does. - Control (R-16): the two replacements above, as a meta-control — "red once by doing X" = X is
either
sys.exit("unrelated parser failure")orsys.exit(0)on any one control, which must make--self-testexit non-zero after the fix. - Owner: p1 (
scripts/gen_pipe*.py). - No integrator ruling needed.
10. Session death controls accept crashes without their required diagnostics — CONFIRMED (both controls)
Perturbation (the one the finding names). The backwards-watermark diagnostic-and-abort block in
AdvanceMonotonic (Ring.cpp:366-375, which Watermark::AdvanceApplied at :390 funnels into)
replaced by a bare std::abort():
REPLACED:
if (to < current) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"watermark\"} %s moved backwards, "
... );
std::abort();
}
WITH:
if (to < current) {
(void)name;
std::abort();
}
Unperturbed
1/1 Test #1905: SessionTestDeath.AWatermarkThatMovesBackwardsIsFatalRatherThanIgnored ... Passed 0.01 sec
Perturbed
=== build target=SessionTest rc=0
1/1 Test #1905: SessionTestDeath.AWatermarkThatMovesBackwardsIsFatalRatherThanIgnored ... Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
ctest exit=0
The oversized-reply half of the claim, verified separately. ReplySlotPool::Post's oversize
refusal (ReplySlot.h:182) reduced to a bare std::abort() with its whole
Fatal{ProtocolCorruption} WireLogError deleted:
=== build target=SessionTest rc=0
1/1 Test #1904: SessionTestDeath.AReplyLargerThanItsSlotIsFatalRatherThanTruncated ... Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
Both EXPECT_DEATH(..., "") regexes are empty (SessionTest.cpp:581, :805), so each accepts any
fatal termination whatsoever — a refusal path that aborts for the wrong reason, or a segfault in a
broken one, is indistinguishable from the specified protocol-fault handling.
- Fix shape: replace each
""with the diagnostic's distinctive wording —"watermark\\\" appliedSeq moved backwards"and"reply pool: Fatal\\{ProtocolCorruption\\} - a .* byte answer". - Control (R-16): "red once by doing X" = X is exactly the two bare-
std::abort()replacements shown here, which must then fail their cases. - Owner: s1 (
MG_Test/Wire/SessionTest.cpp; the two diagnostics live inTransport/Ring.cppandTransport/ReplySlot.h, also s1's). - No integrator ruling needed.
Summary
| # | finding | verdict | owner | integrator ruling? |
|---|---|---|---|---|
| 1 | Empty staging segment rejects a blob that fits | CONFIRMED | w1 | no |
| 2 | Default reply geometry cannot hold its advertised readback | CONFIRMED (+ E2 consequence now verified: 1,228,800 B needed vs 1,048,560 B cap) | s1 (+ c0) | YES — segment size |
| 3 | Blob validation accepts the wrong segment, bypasses staging audit | CONFIRMED | w1 | no |
| 4 | Wire MapPersistent bypasses the adoption-tier refusal (T0 and T1) |
CONFIRMED | w1 | no |
| 5 | Pad-bit regression test never sends a pad bit | CONFIRMED (head already carries a working replacement, RingTest.TheTwoFlagSpacesAreDisjointByTranslation) |
s1 | no |
| 6 | ABI sensitivity tests exercise a helper with zero production callers | CONFIRMED (whole unit lane blind) | s1 | no |
| 7 | Null-union regression test never reaches either handshake guard | CONFIRMED (whole unit lane blind) | s1 | no |
| 8 | CI negative controls accept unrelated failures (E1/E3(a) and pull-retrace) | CONFIRMED | t1 | no |
| 9 | gen_pipe self-test accepts any SystemExit, including code 0 |
CONFIRMED | p1 | no |
| 10 | Session death controls accept crashes without their diagnostics (both) | CONFIRMED | s1 | no |
10 CONFIRMED / 0 REFUTED / 0 PARTIAL.
Two items beyond the ten, both for the integrator:
- Finding 2 is the only fix that cannot be taken inside a package — it moves
SessionSegmentSizes::ReplyBytesand/orReplySlotCount, which are contract §2 row 23 numbers, and contract §5 has noSEG_REPLYknob to move instead. c0's file either way. PipeWireCodecTest.NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlotis RED at4ba14fa4(1973/1974 inbuild-split's unit lane), fromd55e3733givingResourceCreatethekReplySlotflag while79b78665asserts it has none. A catalogue-flag conflict between two landed commits — the integrator's, not a package's, andReplySlot.h's R-5 comment says the test is the stale half.
Provenance
Scripts and logs, all under ~/w7/ with the p5-verify- prefix:
p5-verify-build.sh (configure/build, with the CMAKE_GENERATOR + CTestTestfile.cmake checks),
p5-verify-bt.sh (build-then-test, refuses to test a failed build), p5-verify-run.sh,
p5-verify-submodlinks.sh, p5-verify-f{1,1b,2,3,4,10,10b}-perturb.py,
p5-verify-f8-e1-harness.sh, p5-verify-f8-retrace-harness.sh, p5-verify-f8-stub/ctest,
p5-verify-cfg-split.log, p5-verify-build-split.log, p5-verify-final-build.log.
Worktree left in place and clean: ~/w7/p5-verify @ 4ba14fa4, branch p5/verify,
git status --porcelain empty (with p5-verify-submodlinks.sh off). No commits. No LFS fetch.
No adb.