#!/usr/bin/env python3 # MobileGL - scripts/ci/ph_latch_sites.py # Copyright (c) 2025-2026 MobileGL-Dev # Licensed under the GNU Lesser General Public License v3.0: # https://www.gnu.org/licenses/gpl-3.0.txt # https://www.gnu.org/licenses/lgpl-3.0.txt # SPDX-License-Identifier: LGPL-3.0-only # End of Source File Header """PH-1 (3) / Ph fuzz arm 2: the site -> raw-peer control map, checked (P7 package F2 latch). WHAT IT CHECKS. PH-1 (3) (ID-P7-1) converts the named faults whose input bytes the PEER wrote into a per-session latch (MG_Remote/FatalFunnel.h SessionLatch). Fuzz arm 2 wants one raw-peer negative control per converted site. This script keeps those two lists the same list: * it finds every latch site in the four PH-1 (3) files - each `SessionLatch(` call and each `WireProtocolLatch(` / `WireProtocolLatchAt(` call - and rebuilds the line it logs, `Fatal{Family, "word"} ...` with every printf conversion a wildcard; * it reads the row table (`kRows`) of MobileGL/MG_Test/Wire/PeerLatchTest.cpp, whose Latched rows each carry a marker the test requires to be the FIRST `Fatal{` line of the server log; * a site is COVERED when a Latched row of the same file has a marker that is a prefix of a line the site can log. Otherwise it must be argued in UNREACHABLE: which earlier check refuses the same bytes first. Anything else is red. Sites whose formats are IDENTICAL are one group (a log-reading control cannot tell them apart, and the group is covered once): the three "opcode" copies (the decoder's pre-gate is the reachable one; ApplyChecked's two are behind it) and SetVertexAttribDefaults.Count's two (the count > 32 arm is behind popcount(Mask) == Count, so only the mismatch is reachable). `--table` prints the map as Markdown. `--self-test` proves the check can go red: it drops each covering row in turn and requires its group to turn up unmapped. THE DECLINE-AND-CLOSE HALF IS NOT A SITE, so it is not a row: MECHANICS below names each check that makes a latched session decline the rest of its work and close (DrainRing's pre-pop latch check, the apply thread's own exit, the control pump's and SurfaceOpCodec's latched answers, RunSession's sliced wait, the pre-gate ahead of the applier's stamp, SessionLatch's own bookkeeping), the code that IS the check, and the cases that go red without it. The script requires both to exist. """ import argparse import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TEST = ROOT / "MobileGL" / "MG_Test" / "Wire" / "PeerLatchTest.cpp" FILES = { "PipeWireCodec.cpp": ROOT / "MobileGL" / "MG_Remote" / "Wire" / "PipeWireCodec.cpp", "PipeApplier.cpp": ROOT / "MobileGL" / "MG_Remote" / "Server" / "PipeApplier.cpp", # P13 W5: the record verbs ServerVerbSink applied moved to their own consumer, with their latches. "RecordVerbSink.cpp": ROOT / "MobileGL" / "MG_Backend" / "Record" / "RecordVerbSink.cpp", "ServerLoop.cpp": ROOT / "MobileGL" / "MG_Remote" / "Server" / "ServerLoop.cpp", "SurfaceOpCodec.cpp": ROOT / "MobileGL" / "MG_Remote" / "Protocol" / "SurfaceOpCodec.cpp", } # (file, a substring of the site's line) -> why no peer byte reaches it. The site is converted # anyway - it is on the decode path and returns like its neighbours - and the reason names the # earlier check that refuses the same bytes first. UNREACHABLE = [ ("PipeWireCodec.cpp", '"segment-resolve"', "ResolveOrFatal runs RequireDeclaredBlob first, which has already refused a run the segment " "table cannot resolve (R2BlobOutsideItsSegment); reaching it means the table moved"), ("PipeWireCodec.cpp", "the audit was asked to record", "NoteResolvedRun's one caller is ResolveOrFatal after RequireDeclaredBlob: an empty or " "non-SEG_STAGE run never arrives here"), ("PipeWireCodec.cpp", "SEG_STAGE runs in one record", "no record resolves more runs than the audit array holds (CreateShaderState's seven blobs " "are the most, and six of them must be undeclared)"), ("PipeWireCodec.cpp", '"DrawVbo.userIndices"', "MGPipeWireRecordLayout gives a user-index draw exactly one 32-byte second tail and the tail " "cross-check refuses any record that differs (CodecTailCrossCheck)"), ("PipeWireCodec.cpp", '"DrawVbo.indirect"', "as DrawVbo.userIndices: the layout fixes the indirect block's size"), ("ServerLoop.cpp", '"appliedSeq batched"', "both tallies move inside the same ApplyOne callback; no record bytes can separate them"), ("ServerLoop.cpp", '"SurfaceOp.windowBackend"', "SurfaceOpCodec maps the wire WindowKind onto a legal tag or refuses it (the three SurfaceOp " "rows) before a frame reaches the dispatch"), ("ServerLoop.cpp", '"SurfaceOp.kind"', "SurfaceOpCodec refuses an unknown wire kind (SurfaceOpUnknownKind) before the dispatch"), # P12 (D6): not a peer's byte at all - the server's OWN display window was destroyed # (ServerDisplay::Detach from the display Activity's surfaceDestroyed). Its control is # ServerLoopTest's ALostServerWindowIsReleasedOnTheApplyThreadBeforeDetachReturnsAndLatchesByName. ("ServerLoop.cpp", '"surfaceDestroyed"', "raised by the server's own display (surfaceDestroyed -> ServerDisplay::Detach), never by the " "peer's bytes; ServerLoopTest's lost-window case is its negative control"), # P9 (CONTRACT-P9.md ยง1): the pack-buffer read's decline. The reply form's twin answers # DECLINED for the same case and has no row either. ("RecordVerbSink.cpp", 'Fatal{ReadbackDeclined, "ReadPixelsToBuffer"}', "both server backends install GL.ReadPixels; no record bytes can take the slot away, so the " "arm exists only for a backend that never had one"), ] REMOTE = ROOT / "MobileGL" / "MG_Remote" WIRE_TESTS = ROOT / "MobileGL" / "MG_Test" / "Wire" # (source, what the check does, a regex that IS the check, [(test file, case name)]) - each case goes # red with the check deleted (the red-once is recorded in the package's NOTE). MECHANICS = [ # ONE check, the first statement of DrainRing's loop, immediately before every pop. It replaced # the two the F2 latch package shipped (one at the function's top, one under `++applied;`): # codex closeout finding 6 showed a latch that another thread stores after the per-record check # still let one more record be popped and applied. The one check does all three jobs - a drain # ENTERED latched pops nothing (the exit-path drain), a record that latched is the last one its # batch applies, and a latch from RunSession's control thread between two records stops the # next pop when stored before this check (it NARROWS the window to check-to-pop, not closes it; # the session ends at the next check) - and each case below goes red with it deleted. The # pattern may not leave DrainRing's body (`\n }\n` closes a member function in ServerLoop.cpp). (REMOTE / "Server" / "ServerLoop.cpp", "DrainRing: no record is popped once the session latched (checked immediately before every pop)", r"Uint64 ServerLoop::DrainRing\([^)]*\) \{(?:(?!\n \}\n).)*?for \(;;\) \{\s*if \(SessionLatched\(\)\) break;", [("ServerLoopTest.cpp", "ALatchedRecordEndsItsBatchAndTheApplyThreadLeavesWithoutAStop"), ("PeerLatchTest.cpp", "ALatchedRecordIsTheLastRecordItsBatchApplies"), ("ServerLoopTest.cpp", "ALatchFromAnotherThreadBetweenTwoRecordsStopsTheNextPop")]), (REMOTE / "Server" / "ServerLoop.cpp", "ApplyThreadMain: the apply thread leaves its loop on the latch, no Stop()", r"if \(SessionLatched\(\)\) \{\s*MGLOG_E\(\"MG_Remote server: mgl-srv-apply leaves its loop", [("ServerLoopTest.cpp", "ALatchedRecordEndsItsBatchAndTheApplyThreadLeavesWithoutAStop")]), (REMOTE / "Server" / "ServerLoop.cpp", "PumpControlRequest: a frame taken after the latch is answered, not run", r"if \(SessionLatched\(\)\) \{\s*m_controlResult = MOBILEGL_ERR_PROTOCOL_MISMATCH;", [("ServerLoopTest.cpp", "AControlFrameTakenAfterTheLatchIsAnsweredWithoutRunning")]), (REMOTE / "Protocol" / "SurfaceOpCodec.cpp", "ServerApplyWireSurfaceOp: a latched session declines a well-formed op", r"if \(SessionLatched\(\)\) return MOBILEGL_ERR_PROTOCOL_MISMATCH;", [("SurfaceControlFrameTest.cpp", "ALatchedSessionAnswersAWellFormedSurfaceOpWithoutRunningIt")]), (REMOTE / "Server" / "ServerMain.cpp", "RunSession: the control wait is sliced, so a latch closes a silent peer's session", r"ReceiveFrame\(\{buffer\.data\(\), buffer\.size\(\)\}, &size, kControlSliceMs\)", [("PeerLatchTest.cpp", "ALatchOnTheApplyThreadClosesTheSessionWhileAUnixPeerHoldsItOpen"), ("PeerLatchTest.cpp", "ALatchOnTheApplyThreadClosesTheSessionWhileATcpPeerHoldsItOpen")]), (REMOTE / "Server" / "PipeApplier.cpp", "ApplyOne: the pre-gate runs before the verb stamp and the barrier read", r"if \(!m_decoder\.AdmitOrDecline\(record\)\) return false;\s*const Bool wireSaysBarriered", [("ServerLoopTest.cpp", "ARecordShorterThanItsTypeIsLatchedBeforeTheApplierStampsIt")]), # P14 S2: the latch is the CALLING THREAD'S DOMAIN now (FatalFunnel.h), so the unarmed arm # reads the domain's own word instead of the process's - the mechanism and every case below # are unchanged, and a session's domain being unarmed is still SessionFail byte for byte. (REMOTE / "FatalFunnel.cpp", "SessionLatch: first fault wins, every fault counted, one SessionFault; unarmed it dies", r"if \(!latch\.armed\.load\(std::memory_order_acquire\)\) \{\s*SessionFail\(", [("ServerLoopTest.cpp", "AnArmedSessionLatchKeepsTheFirstFaultCountsEveryOneAndPublishesOnce"), ("ServerLoopTest.cpp", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]), ] def case_suite(test_file, case): """The gtest suite that owns `case` in `test_file`, or None when the file has no such case.""" text = (WIRE_TESTS / test_file).read_text(encoding="utf-8") found = re.search(r"\bTEST(?:_F|_P)?\(\s*(\w+)\s*,\s*" + case + r"\s*\)", text) return found.group(1) if found else None def mechanics_failures(): failures = [] for source, what, check, cases in MECHANICS: if not re.search(check, source.read_text(encoding="utf-8"), re.DOTALL): failures.append(f"MECHANICS: {source.name} no longer has the check for `{what}`") for test_file, case in cases: if case_suite(test_file, case) is None: failures.append(f"MECHANICS: {test_file} has no case {case} (the control for `{what}`)") return failures def print_mechanics(): print() print("| decline / close check | source | negative control(s) |") print("|---|---|---|") for source, what, _check, cases in MECHANICS: print(f"| {what} | {source.name} | " + ", ".join(f"`{case_suite(f, c)}.{c}` ({f})" for f, c in cases) + " |") # P13 W5: MGPipeRecordLatch is SessionLatch through the record arm's seam (MG_Pipe/PipeSessionFail.h). LATCH_CALL = re.compile(r"(? control map as Markdown") parser.add_argument("--self-test", action="store_true", help="prove every covered site can go red") args = parser.parse_args() sites = latch_sites() table = rows() mapping = build_map(sites, table) if args.table: print_table(mapping, table) print_mechanics() failures = mechanics_failures() if args.self_test: failure = self_test(sites, table) if failure: failures.append(f"self-test did not go red: {failure}") for m in unmapped(mapping): failures.append(f"{m['file']}:{m['lines'][0]} latches `{short(m['fmt'])}` and no PeerLatchTest row's " f"marker names it; add a raw-peer row, or argue it into UNREACHABLE") for file, needle, _ in UNREACHABLE: if not any(m["file"] == file and needle in m["fmt"] for m in mapping): failures.append(f"UNREACHABLE names {file} `{needle}`, which is no longer a latch site") for row in table: if row["outcome"] == "Latched" and row["name"] not in {r for m in mapping for r in m["rows"]} \ and not row["name"].startswith("D11"): failures.append(f"row {row['name']} is Latched but its marker names no latch site of {row['file']}") covered = sum(1 for m in mapping if m["rows"]) argued = sum(1 for m in mapping if not m["rows"] and m["unreachable"]) print(f"ph latch sites: {len(sites)} latch calls = {len(mapping)} distinct lines in {len(FILES)} files; " f"{covered} covered by PeerLatchTest rows ({len(table)} rows), {argued} argued unreachable, " f"{len(unmapped(mapping))} unmapped; {len(MECHANICS)} decline/close checks, each with its case(s)" + ("; self-test red on every drop" if args.self_test and not any("self-test" in f for f in failures) else "")) for message in failures: print(f"::error::{message}", file=sys.stderr) return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())