From 3c72e5c637d2cdf12b4fa03c305f2eb57478f060 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 06:38:18 -0400 Subject: [PATCH] [Fix] (P11 A1): settle MOBILEGL_IPC_ADOPT_TIER at the handshake from the data plane - a stream refuses T0/T1 by name and runs T2 on either side, a client over shared segments dies by name before any record, the at-use check trusts the settlement; unit gates on a real tcp session and a real inproc handshake --- CMakeLists.txt | 3 + MobileGL/ConfigLoader.cpp | 4 +- .../Scenarios/CtWireScenario.cpp | 2 +- MobileGL/MG_Remote/Client/ClientSession.cpp | 10 + .../MG_Remote/Client/PersistentMapTracker.cpp | 33 ++-- .../MG_Remote/Client/PersistentMapTracker.h | 11 +- MobileGL/MG_Remote/Server/ServerSession.cpp | 6 + MobileGL/MG_Remote/Transport/AdoptTier.cpp | 70 +++++++ MobileGL/MG_Remote/Transport/AdoptTier.h | 53 ++++++ MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 9 +- MobileGL/MG_Test/Buffer/SplitBufferTest.cpp | 31 ++- MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp | 179 ++++++++++++++++++ MobileGL/MG_Test/Wire/CMakeLists.txt | 9 +- MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp | 99 ++++++---- .../MG_Test/Wire/RemoteClientControls.inc | 49 +++++ scripts/ci/fatal_census.py | 5 +- 16 files changed, 500 insertions(+), 73 deletions(-) create mode 100644 MobileGL/MG_Remote/Transport/AdoptTier.cpp create mode 100644 MobileGL/MG_Remote/Transport/AdoptTier.h create mode 100644 MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 86165f7a..9ac67cc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # Keeps MG_Util/Debug/Log.h - and through it the GL frontend's # umbrella header - out of the header-only wire code (WireLog.h). MobileGL/MG_Remote/Transport/WireLog.cpp + # P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by + # both roles (so SERVER-partition code in the link ratchet, not the client's tracker). + MobileGL/MG_Remote/Transport/AdoptTier.cpp # ---- P5: the three new directories ------------------------------ # Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and # var-tails in and out of SEG_STAGE. [w1] diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index c90a0941..0de4fa6e 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -430,8 +430,8 @@ namespace MobileGL::MG_ConfigLoader { // torn read rather than a divergence. The batch is therefore off whenever the // shadow comparer is armed. if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0; - // 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the - // point of use, which is where the "P11" in the message belongs. + // 2 is the only implemented tier (R-6). 0 and 1 parse here and are settled at the + // handshake from the data plane (P11 A1, MG_Remote/Transport/AdoptTier.h). ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2); ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1); // P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every diff --git a/MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.cpp index 97044982..e37122e3 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.cpp @@ -257,7 +257,7 @@ namespace { // RESIDENT, residency comes only from AdoptPersistentMap, and every persistent-map // acquisition declines under split by R-6 (PipeApply.cpp's MGPipeApplyMapPersistent: // "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION, ALWAYS"; T0/T1 are - // P11's and AdoptTierIsEmulate is a named Fatal for them). So the capability is wired + // unimplemented, refused by name at the handshake since P11 A1). So the capability is wired // AHEAD of its consumer on purpose, and `rsd=` on the PipeStats line is the counter that // will show the records the day P11 lands a tier that adopts. See notes/p7/magma-c.md. TEST_F(CtWireScenario, TheServerPublishesTheResidentSubDataCapabilityFromItsOwnTable) { diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 6697c695..ca917153 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -19,6 +19,7 @@ #include "../Server/ServerLoop.h" #include "../Server/ServerSession.h" #include "../Protocol/SurfaceOpCodec.h" +#include "../Transport/AdoptTier.h" #include "../Transport/FdPassing.h" #include "../Transport/InProcessTransport.h" #include "WireTables.h" @@ -790,6 +791,11 @@ namespace MobileGL::MG_Remote::Client { Stop(); return result; } + // P11 A1: the adopt tier, settled by the side that holds the knob once the link terms + // say which plane this is (inproc is shared segments by construction), before any + // record. T0/T1 over shared segments die here by name (Transport/AdoptTier.h). + (void)Transport::SettleAdoptTierAtHandshake( + terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream, Transport::AdoptTierSide::Client); // The four SegmentRefs are what a spawn client MAPS (P6). Under inproc the mapping // already exists, so what they are good for here is the cross-check that the two // sides agree about the geometry at all - which is the assertion that would @@ -1026,6 +1032,10 @@ namespace MobileGL::MG_Remote::Client { return result; } stream = terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream; + // P11 A1: as in StartOverTransportPair - the knob against the plane the link terms + // settled, before the data connection and before any record. A stream refuses T0/T1 + // by name and runs T2; shared segments die here by name (Transport/AdoptTier.h). + (void)Transport::SettleAdoptTierAtHandshake(stream, Transport::AdoptTierSide::Client); if (stream) { // PH-7 (4). A Stream Welcome without a nonce of exactly the minted width names no // data connection this client could open; refused rather than guessed at. diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp index 76925ed6..1991de80 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp @@ -8,6 +8,7 @@ #include "PersistentMapTracker.h" #include +#include #include #include @@ -1246,27 +1247,23 @@ namespace MobileGL::MG_Remote::Client { Bool AdoptTierIsEmulate() { const Uint32 tier = MG_Config::Ipc.AdoptTier; if (tier == 2) return true; - // A NAMED refusal, not a silent fall back to T2. T0 (a real cross-process shared - // mapping) and T1 (a server-side staging map) are P11's, and the reason the knob - // parses them today is that the negative control needs a spelling before the thing - // it controls exists. Falling back would make `MOBILEGL_IPC_ADOPT_TIER=0` look like - // a working T0 run and silently produce pmap bytes it must not produce. - // 0 and 1 are the two CONTRACT §5 promises - a real cross-process shared mapping and a - // server-side staging map - and they name P11. Anything else is not a tier at all, and - // saying "P11 implements it" of a 7 would be a lie the operator then repeats. Both die - // here rather than at parse, which is late: the abort lands at the first - // map_persistent, so a mis-set run gets through EGL bring-up and a frame of setup - // first. Moving it to the parse means a knob-validity rule in ConfigLoader, which is - // c0's file; filed for the integrator rather than taken here. - // `dl` (CONTRACT-P6 5.2): THE FAMILY WORD THIS SITE NEVER CARRIED. a6 found two aborts - // under MG_Remote/ with no Fatal{ marker at all, so "the log stays verbatim" was not true - // of them and no family grep could see them - this is one. (The other, WireLog.cpp's, is - // the sanctioned funnel: every one of its callers passes a Fatal{ string of its own.) + // P11 A1 (CONTRACT-P11 §1): THE VERDICT IS THE HANDSHAKE'S, NOT THIS CALL'S. Both roles + // settle the tier from the knob and the data plane before the first record + // (Transport/AdoptTier.cpp): a stream refuses T0/T1 by name and runs T2, a client over + // shared segments dies by name there, and a server over shared segments serves the T2 + // its client settled. So a settled session reaching this with 0/1 is a T2 session and + // says nothing more - the one line was the handshake's. + if (tier <= 1 && Transport::AdoptTierSettledAtHandshake()) return true; + // A map_persistent NO handshake settled (a session-free caller) is still a NAMED + // refusal rather than a silent T2: falling back would make `MOBILEGL_IPC_ADOPT_TIER=0` + // look like a working T0 run and silently produce pmap bytes it must not produce. + // Anything above 2 is not a tier at all. + // `dl` (CONTRACT-P6 5.2): the family word every death here carries. if (tier <= 1) { SessionFail(MGFatalFamily::UnimplementedAdoptTier, "MGPipe: Fatal{UnimplementedAdoptTier, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u " - "names an adoption tier P11 implements and P5 does not; P5 runs at T2 " - "(emulate) only.", + "reached a map_persistent that no handshake settled; T0 and T1 are not " + "implemented and only T2 (emulate) runs.", static_cast(tier), static_cast(tier)); } else { SessionFail(MGFatalFamily::UnimplementedAdoptTier, diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.h b/MobileGL/MG_Remote/Client/PersistentMapTracker.h index 1deb23b5..165518bf 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.h +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.h @@ -268,10 +268,13 @@ namespace MobileGL::MG_Remote::Client { // draw-path binding walks move to the client. void PushPersistentMapsBeforeVerb(); - // R-6's tier gate, and the ONE spelling of it. True for MOBILEGL_IPC_ADOPT_TIER=2, the - // only tier P5 implements; 0 (a real cross-process shared mapping) and 1 (a server-side - // staging map) parse - so the negative control has a name before the thing it controls - // exists - and are a NAMED refusal here rather than a silent fall back to T2. It is asked + // R-6's tier gate, read at use. True for MOBILEGL_IPC_ADOPT_TIER=2, the only implemented + // tier. The tiers are design/07's: T0 = the client allocates an AHardwareBuffer and the + // server imports it (P11 package B), T1 = the server exports an opaque fd (closed), T2 = + // decline and push. 0 and 1 are SETTLED AT THE HANDSHAKE (P11 A1, Transport/AdoptTier.h): + // a stream refuses them by name and runs T2, a client over shared segments dies by name + // there. So a settled session answers true here; only a map_persistent no handshake + // settled is still a named refusal. It is asked // by MGPipeApplyMapPersistent, which is where the decline is decided, so the client's // three adoption call sites keep their existing "null means declined" branch and the // map-persistent-roundtrips counter keeps counting ATTEMPTS in both arms (E3(c) asserts diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 15a6488b..258edbaf 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -15,6 +15,7 @@ #include "../CapsCodec.h" #include "../Handshake.h" #include "../Transport/LinkMetrics.h" +#include "../Transport/AdoptTier.h" #include "../Protocol/generated/protocol_generated.h" #include "../Transport/InProcessTransport.h" @@ -673,6 +674,11 @@ namespace MobileGL::MG_Remote::Server { "backend request differs from server", m_backend == nullptr ? 0 : static_cast(m_backend->GetBackendType()), hello->backendType()); + // ---- P11 A1: this side's adopt tier, from its own knob and the plane the Hello asked + // for, before a byte of Welcome. A stream refuses T0/T1 by name and serves T2; over + // shared segments the client settles it (Transport/AdoptTier.h). 0/1 never die here. + (void)Transport::SettleAdoptTierAtHandshake(stream, Transport::AdoptTierSide::Server); + // ---- 3. the four segments, both control pages, the rings. MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED; // PH-7 (4), ID-P7-3. Minted HERE - after the token was checked, before a byte of Welcome diff --git a/MobileGL/MG_Remote/Transport/AdoptTier.cpp b/MobileGL/MG_Remote/Transport/AdoptTier.cpp new file mode 100644 index 00000000..e2dfcea5 --- /dev/null +++ b/MobileGL/MG_Remote/Transport/AdoptTier.cpp @@ -0,0 +1,70 @@ +// MobileGL - MobileGL/MG_Remote/Transport/AdoptTier.cpp +// 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 + +#include "AdoptTier.h" + +#include +#include +#include + +#include + +namespace MobileGL::MG_Remote::Transport { + + namespace { + std::atomic g_settled{false}; + + const char* SideName(AdoptTierSide side) { return side == AdoptTierSide::Client ? "client" : "server"; } + } // namespace + + std::uint32_t SettleAdoptTierAtHandshake(bool streamDataPlane, AdoptTierSide side) { + const std::uint32_t knob = MG_Config::Ipc.AdoptTier; + if (knob > 2) { + // Unreachable through ConfigLoader (it admits 0..2); a direct assignment is the only + // way here, and "P11 implements it" of a 7 would be a lie. + SessionFail(MGFatalFamily::UnimplementedAdoptTier, + "MGPipe: Fatal{UnimplementedAdoptTier, \"%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u is not an " + "adoption tier; the values are 0 and 1 (not implemented) and 2 (emulate, the default)", + static_cast(knob), static_cast(knob)); + } + if (knob == 2) { + g_settled.store(true, std::memory_order_release); + return 2; + } + if (streamDataPlane) { + // ONE LINE PER SESSION, on the side that holds the knob. The tcp lane's server is a + // shared fixture that never sees the client's environment, so each side says it for + // itself; neither side dies, because T2 is the only tier a stream can carry. + MGLOG_W("MG_Remote %s: Refuse{AdoptTierOnStream, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u names a " + "shared-mapping adoption tier and this session's data plane is a stream (tcp): the two " + "sides share no memory to map, so the session runs T2 (emulate: the client keeps the " + "shadow and pushes)", + SideName(side), static_cast(knob), static_cast(knob)); + g_settled.store(true, std::memory_order_release); + return 2; + } + if (side == AdoptTierSide::Server) { + MGLOG_W("MG_Remote server: MOBILEGL_IPC_ADOPT_TIER=%u is the client's to settle on a shared-segment " + "link (a client naming T0/T1 dies by name at its own handshake); this server serves T2", + static_cast(knob)); + g_settled.store(true, std::memory_order_release); + return 2; + } + SessionFail(MGFatalFamily::UnimplementedAdoptTier, + "MGPipe: Fatal{UnimplementedAdoptTier, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u names a " + "shared-mapping adoption tier no package implements yet (T0 is P11 package B's, T1 is " + "closed); refused at the handshake over shared segments, before any record is emitted. " + "Unset it or set 2 (emulate)", + static_cast(knob), static_cast(knob)); + } + + bool AdoptTierSettledAtHandshake() { return g_settled.load(std::memory_order_acquire); } + + void ForgetAdoptTierSettlementForTest() { g_settled.store(false, std::memory_order_release); } + +} // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Transport/AdoptTier.h b/MobileGL/MG_Remote/Transport/AdoptTier.h new file mode 100644 index 00000000..9bf6c1bd --- /dev/null +++ b/MobileGL/MG_Remote/Transport/AdoptTier.h @@ -0,0 +1,53 @@ +// MobileGL - MobileGL/MG_Remote/Transport/AdoptTier.h +// 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 + +// P11 A1 (MG_Remote/CONTRACT-P11.md §1): THE PERSISTENT-MAP ADOPTION TIER IS SETTLED AT THE +// HANDSHAKE, BY THE SIDE THAT KNOWS THE DATA PLANE, BEFORE THE FIRST map_persistent. +// +// Tier names follow docs/Disaggregated/design/07: T0 = the client allocates an AHardwareBuffer +// and the server imports it; T1 = the server exports an opaque fd; T2 = the resource owner +// declines and the client keeps the shadow and pushes. Only T2 is implemented (T0 is P11 +// package B's, T1 is closed), so MOBILEGL_IPC_ADOPT_TIER=0/1 is either refused by name or +// fatal - never a silent T2, and never discovered at the first map_persistent: +// +// data plane side knob 0 / 1 +// Stream (tcp) either one line `Refuse{AdoptTierOnStream, "T"}`, then T2. Never Fatal: +// a stream has no memory the two sides share, so no tier but T2 +// can exist on it. +// SharedSegments client Fatal{UnimplementedAdoptTier, "T"} at the handshake, before any +// record is emitted. +// SharedSegments server one warning that the client settles the tier on this plane, then +// T2. (inproc and fork share the client's environment, so the client +// dies by name right after; a unix-socket server launched on its own +// serves the T2 its client settled.) +// +// In MG_Remote/Transport (the link-ratchet's SERVER partition) because both roles call it: +// ServerSession::Accept and ClientSession's two Start halves. The at-use check the server's +// applier and codec ask (Client::AdoptTierIsEmulate) reads the settlement back. + +#pragma once + +#include + +namespace MobileGL::MG_Remote::Transport { + + enum class AdoptTierSide : std::uint8_t { Client, Server }; + + // Settles this session's tier from MOBILEGL_IPC_ADOPT_TIER and the negotiated data plane, + // on `side`, and returns the tier the session runs (2 until package B). Logs at most one + // line per call; aborts (SessionFail) only on the client over shared segments. + std::uint32_t SettleAdoptTierAtHandshake(bool streamDataPlane, AdoptTierSide side); + + // True once a handshake in this process has settled the tier. The at-use check trusts a + // settled session and still refuses by name a map_persistent no handshake settled. + bool AdoptTierSettledAtHandshake(); + + // Test seam: forget the settlement, so one process can drive both answers. + void ForgetAdoptTierSettlementForTest(); + +} // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 677f95cb..72e31437 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -1840,10 +1840,11 @@ namespace MobileGL::MG_Remote::Wire { // 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. + // the operator got a run that looked like a working T0. Since P11 A1 the tier is + // settled at the handshake (Transport/AdoptTier.h): a stream refused T0/T1 there by + // name and runs T2, shared segments died there, so a settled session answers true + // and only a map_persistent no handshake settled still aborts by name. The return + // value is deliberately not a branch: every surviving tier declines (R-6). if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { (void)MG_Remote::Client::AdoptTierIsEmulate(); } diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index 29925120..3fec2322 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -30,6 +30,7 @@ #if MOBILEGL_BUILD_DISAGGREGATED #include #include +#include #endif using namespace MobileGL; @@ -84,6 +85,7 @@ namespace { // membership assertion in this file would then be about a different code path. MG_State::GLState::SetBufferBackendOps(nullptr); g_mintedPersistentBase = nullptr; + MG_Remote::Transport::ForgetAdoptTierSettlementForTest(); PersistentMapTracker::Instance().ClearForTest(); MG_State::pGLContext = Move(m_context); MG_Config::Transport = m_transport; @@ -536,14 +538,31 @@ TEST_F(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheReques EXPECT_FALSE(buffer->HasOutstandingGpuWrite()); } -// R-6's tier gate. T2 is the only tier P5 implements; the other two are a NAMED refusal and -// their spelling exists now so the P11 negative control has one. +// R-6's tier gate in P11 A1's shape (CONTRACT-P11 §1). T2 is the only implemented tier, and 0 / 1 +// are SETTLED AT THE HANDSHAKE by the data plane rather than discovered at the first +// map_persistent: a stream refuses them by name and runs T2 on either side, a client over shared +// segments dies there by name, a server over shared segments serves T2. The at-use check trusts a +// settled session and still dies by name for a map_persistent no handshake settled. The session +// halves are RemoteClientControls.AdoptTier*OverSharedSegments* and AdoptTierStreamTest. TEST_F(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) { + namespace Tier = MG_Remote::Transport; EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate()); - MG_Config::Ipc.AdoptTier = 0; - EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), ""); - MG_Config::Ipc.AdoptTier = 1; - EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), ""); + for (const Uint32 tier : {0u, 1u}) { + SCOPED_TRACE(tier); + MG_Config::Ipc.AdoptTier = tier; + Tier::ForgetAdoptTierSettlementForTest(); + EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), "UnimplementedAdoptTier.*no handshake settled"); + EXPECT_DEATH(Tier::SettleAdoptTierAtHandshake(false, Tier::AdoptTierSide::Client), + "UnimplementedAdoptTier.*refused at the handshake over shared segments"); + for (const auto side : {Tier::AdoptTierSide::Client, Tier::AdoptTierSide::Server}) { + Tier::ForgetAdoptTierSettlementForTest(); + EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(true, side), 2u); + EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate()); + } + Tier::ForgetAdoptTierSettlementForTest(); + EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(false, Tier::AdoptTierSide::Server), 2u); + EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate()); + } MG_Config::Ipc.AdoptTier = 2; } diff --git a/MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp b/MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp new file mode 100644 index 00000000..bd6ba207 --- /dev/null +++ b/MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp @@ -0,0 +1,179 @@ +// MobileGL - MobileGL/MG_Test/Wire/AdoptTierStreamTest.cpp +// 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 + +// P11 A1 (MG_Remote/CONTRACT-P11.md §1): MOBILEGL_IPC_ADOPT_TIER=0/1 ON A STREAM DATA PLANE. +// +// Real servers on loopback TCP and forked peers (P12ServerRig.h), so the session is the product's +// two-process tcp session and each side reads its OWN knob, as on the tcp lane: +// * a client that names T0 or T1 against a `--serve` supervisor comes up, says +// `Refuse{AdoptTierOnStream, "T"}` exactly once, gets DECLINED for its map_persistent and +// goes on applying records; +// * an in-process display server (the render-server Activity's shape) that names T0 says the +// same line once per session, never dies, and serves T2. +// The shared-segment half (a client naming T0/T1 dies at the handshake, before any map_persistent) +// is RemoteClientControls.AdoptTier*OverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent. +// +// RED-ONCE: delete the stream branch of Transport::SettleAdoptTierAtHandshake -> the client peers +// die at the handshake (started 0, Fatal{UnimplementedAdoptTier}) and the server case finds no +// refusal line. + +#include "P12ServerRig.h" + +#include +#include + +#include +#include +#include + +using namespace MobileGL; +using namespace P12; + +namespace { + + // A SERVER WHOSE OWN KNOB SAYS T0. Not the exec'd `--serve` supervisor: ServerSpawn scrubs + // every MOBILEGL_IPC_* but five server-owned knobs from a child it launches, so a knob set here + // would never reach it. The in-process display server (InProcessServerTest's shape - the device + // render-server Activity FCL reaches over tcp) is a process forked from this one, sets the + // knob in its own environment, and RunSession re-reads the environment for every session. + [[noreturn]] void RunInProcessServerWithAdoptTier(const std::string& endpoint, const std::string& logBase, + const char* adoptTier) { + ::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1); + ::setenv("MOBILEGL_IPC_ROLE", "server", 1); + ::setenv("MOBILEGL_IPC_DIAL", "no", 1); + ::setenv("MOBILEGL_IPC_ADOPT_TIER", adoptTier, 1); + for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", + "MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE", + "MOBILEGL_BACKEND_TYPE"}) + ::unsetenv(name); + sigset_t stop; + sigemptyset(&stop); + sigaddset(&stop, SIGTERM); + pthread_sigmask(SIG_BLOCK, &stop, nullptr); + std::atomic returned{false}; + std::thread server([&] { + (void)mobilegl_server_serve_inprocess(endpoint.c_str()); + returned.store(true); + }); + bool stopping = false; + while (!returned.load()) { + timespec slice{0, 100 * 1000 * 1000}; + if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) { + stopping = true; + mobilegl_server_stop_inprocess(); + } + } + server.join(); + std::fflush(nullptr); + ::_exit(0); + } + + // THE SERVER GETS A LOG BASE OF ITS OWN (`server->logBase`), the peers another (`peerBase`, which + // this process exports for the forks): a peer writes the lines the server forwards into + // `.server.log`, opened "w", so sharing one base truncates the server's own file under + // it (measured: NUL holes and the handshake's lines gone). + bool LaunchInProcessServerWithAdoptTier(const std::string& label, const char* adoptTier, const std::string& peerBase, + ServerProcess* server) { + server->logBase = "/tmp/mgl-p11-" + label + "-server-" + std::to_string(::getpid()) + ".log"; + ::setenv("MOBILEGL_LOG_FILE_PATH", peerBase.c_str(), 1); + Debug::TruncateRoleLogs(peerBase.c_str()); + PinHeadlessEgl(); + for (int attempt = 0; attempt < 5; ++attempt) { + const int port = FreeLoopbackPort(); + if (port <= 0) return false; + server->endpoint = "tcp://127.0.0.1:" + std::to_string(port); + Debug::TruncateRoleLogs(server->logBase.c_str()); + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid < 0) return false; + if (pid == 0) RunInProcessServerWithAdoptTier(server->endpoint, server->logBase, adoptTier); + server->pid = pid; + bool listening = false; + (void)WaitFor( + [&] { + if (server->Log().find("listening on") != std::string::npos) return listening = true; + return server->WaitExit(0) && server->pid <= 0; + }, + 10000); + if (listening) return true; + if (server->pid > 0) return false; + } + return false; + } + + // The client's own role log for this rig's base (the peers inherit it from LaunchSupervisor). + std::string ClientLog(const ServerProcess& server) { + return ReadFile(Debug::RoleLogPath(server.logBase.c_str(), Debug::LogRole::Client)); + } + + std::string RefusalLine(Uint32 tier) { + return "Refuse{AdoptTierOnStream, \"T" + std::to_string(tier) + "\"}"; + } + + // One map_persistent, then one ordinary record: the answer to the first and proof the session + // is still applying after it. + void MapPersistentThenARecord(Remote::Client::ClientSession& client, PeerReport& r) { + P::MGPHandleOnly handle{}; + handle.Kind = static_cast(P::MGPipeKind::Buffer); + Int32 status = Remote::Wire::ReplySink::kStatusError; + const Uint64 seq = + client.EmitAndWait(P::MGPWireOp::MapPersistent, &handle, sizeof(handle), nullptr, 0, nullptr, 0, &status); + r.status = seq == Remote::Wire::kInvalidSeq ? -98 : status; + P::MGPMemoryBarrier barrier{}; + barrier.Bits = 0x2000u; + r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0; + } + +} // namespace + +TEST(AdoptTierStream, AClientNamingT0OrT1IsRefusedOnceAndItsMapPersistentIsDeclined) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("adopt-client", &server)) << server.Log(); + for (Uint32 tier : {0u, 1u}) { + SCOPED_TRACE(tier); + const PeerReport r = RunPeer(server.endpoint, MapPersistentThenARecord, /*stopCleanly=*/true, 30000, 0, + nullptr, [tier] { MG_Config::Ipc.AdoptTier = tier; }); + const std::string client = ClientLog(server); + ASSERT_EQ(r.started, 1) << "the session did not come up: " << r.note << "\n" << client; + EXPECT_EQ(r.status, Remote::Wire::ReplySink::kStatusDeclined) + << "map_persistent was not DECLINED on a stream session\n" << client; + EXPECT_EQ(r.applied, 1) << "the session stopped applying after the map_persistent\n" << client; + EXPECT_EQ(Count(client, RefusalLine(tier)), 1u) + << "a stream session names T" << tier << " exactly once, at the handshake\n" << client; + EXPECT_EQ(Count(client, "Fatal{"), 0u) << client; + EXPECT_EQ(Count(server.Log(), "AdoptTierOnStream"), 0u) + << "the server's own knob is 2; the line is the client's\n" << server.Log(); + } +} + +TEST(AdoptTierStream, AServerNamingT0RefusesOncePerSessionAndServesT2) { + ServerProcess server; + const std::string peerBase = "/tmp/mgl-p11-adopt-server-peers-" + std::to_string(::getpid()) + ".log"; + ASSERT_TRUE(LaunchInProcessServerWithAdoptTier("adopt-server", "0", peerBase, &server)) << server.Log(); + // Both role files of the server's own base: which role a session thread of the in-process + // server logs under is not what this case is about. + const auto serverLog = [&server] { return Debug::ReadRoleLogs(server.logBase.c_str()); }; + for (Uint32 session = 1; session <= 2; ++session) { + SCOPED_TRACE(session); + const PeerReport r = RunPeer(server.endpoint, MapPersistentThenARecord, /*stopCleanly=*/true, 30000, 0, + nullptr, [] { MG_Config::Ipc.AdoptTier = 2u; }); + const std::string client = ReadFile(Debug::RoleLogPath(peerBase.c_str(), Debug::LogRole::Client)); + ASSERT_EQ(r.started, 1) << r.note << "\n" << serverLog(); + EXPECT_EQ(r.status, Remote::Wire::ReplySink::kStatusDeclined) << serverLog(); + EXPECT_EQ(r.applied, 1) << serverLog(); + EXPECT_EQ(Count(client, "AdoptTierOnStream"), 0u) << "the client's knob is 2\n" << client; + EXPECT_EQ(Count(serverLog(), RefusalLine(0)), session) + << "one refusal line per server session, at its handshake\n" << serverLog(); + } + EXPECT_EQ(Count(serverLog(), "Fatal{UnimplementedAdoptTier"), 0u) << serverLog(); + if (!::testing::Test::HasFailure()) { + std::error_code ec; + std::filesystem::remove(Debug::RoleLogPath(peerBase.c_str(), Debug::LogRole::Client), ec); + std::filesystem::remove(Debug::RoleLogPath(peerBase.c_str(), Debug::LogRole::Server), ec); + } +} diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index 65ecc276..f013e661 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -46,6 +46,10 @@ if (NOT WIN32) # MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers # and (the second, and D8's case) the real server image, as PeerLatchTest does. list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest) + # P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve` + # supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side + # that names it, map_persistent DECLINED, never a Fatal. Needs the server image too. + list(APPEND MOBILEGL_WIRE_TESTS AdoptTierStreamTest) endif() include(GoogleTest) @@ -63,7 +67,8 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${MGL_ROOT}/MobileGL ${MGL_ROOT}/3rdparty/flatbuffers/include ) - if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest") + if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR + test STREQUAL "AdoptTierStreamTest") # It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State # the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy # session, P12ServerRig.h, and so its include paths.) @@ -81,7 +86,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ) if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR - test STREQUAL "ServerOwnedSurfaceTest") + test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest") # It execs the real image, so it must be built first and must be told # where it is. MOBILEGL_TEST_SERVER_PATH is read by the test's # ServerImage() helper; without it the test would depend on the working diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index 7c07b2b0..f894fe0f 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -2947,31 +2948,76 @@ TEST_F(PipeWireCodecTest, ASecondProcessResolverIsFatalRatherThanASilentRace) { 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 -------- +// ---- R-6 / P11 A1 (CONTRACT-P11 §1): the codec's MapPersistent arm and the adopt tier ----- // -// 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. +// wave1-codex-verify.md §4 found `AdoptTier` had ZERO references on the codec path, so a T0/T1 +// run decoded as an ordinary DECLINED and looked like a working T0. The arm has asked the tier +// since. P11 A1 moved the VERDICT to the handshake (Transport/AdoptTier.h): a stream refuses +// T0/T1 by name there and runs T2, shared segments die there. So this arm now sees two shapes: +// * after a stream handshake settled the tier, T0/T1 DECLINE like T2, with no second refusal +// line - the two cases below that used to be "dies at use"; +// * with no handshake behind it, the arm still dies by name (forked, for the log), which keeps +// the wave-1 finding pinned: red once by deleting the arm's AdoptTierIsEmulate call - the +// child then exits 0 having posted a clean DECLINED. +// The session-level halves are AdoptTierStreamTest and RemoteClientControls.AdoptTier*. -TEST_F(PipeWireCodecTest, AdoptTierZeroIsFatalOnTheWirePathAndNamesP11) { +namespace { + void ExpectStreamSettledTierDeclinesOnTheWire(Uint32 tier) { + 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; + Transport::ForgetAdoptTierSettlementForTest(); + } + } restore{savedTransport, savedTier}; + MG_Config::Transport = MG_Config::TransportMode::InProcess; + MG_Config::Ipc.AdoptTier = tier; + Transport::ForgetAdoptTierSettlementForTest(); + const std::string before = ReadLog(); + ASSERT_EQ(Transport::SettleAdoptTierAtHandshake(true, Transport::AdoptTierSide::Server), 2u); + + Wire2 wire; + const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer); + for (int record = 0; record < 2; ++record) { + 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(), 2u); + for (const auto& answer : wire.Answers().All) { + EXPECT_EQ(answer.Status, ReplySink::kStatusDeclined); + EXPECT_TRUE(answer.Bytes.empty()); + } + const std::string delta = ReadLog().substr(before.size()); + const std::string line = "Refuse{AdoptTierOnStream, \"T" + std::to_string(tier) + "\"}"; + std::size_t count = 0; + for (auto at = delta.find(line); at != std::string::npos; at = delta.find(line, at + 1)) ++count; + EXPECT_EQ(count, 1u) << "one refusal at the handshake and none per map_persistent:\n" << delta; + EXPECT_EQ(delta.find("Fatal{"), std::string::npos) << delta; + } +} // namespace + +TEST_F(PipeWireCodecTest, AdoptTierZeroAfterAStreamHandshakeIsADeclineOnTheWirePath) { + ExpectStreamSettledTierDeclinesOnTheWire(0u); +} + +TEST_F(PipeWireCodecTest, AdoptTierOneAfterAStreamHandshakeIsADeclineOnTheWirePath) { + ExpectStreamSettledTierDeclinesOnTheWire(1u); +} + +TEST_F(PipeWireCodecTest, AnUnsettledAdoptTierStillDiesByNameOnTheWirePath) { 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; + Transport::ForgetAdoptTierSettlementForTest(); Wire2 wire; const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer); (void)wire.Encoder().EncodeRecord(MGPWireOp::MapPersistent, &handle, sizeof(handle)); @@ -2980,24 +3026,7 @@ TEST_F(PipeWireCodecTest, AdoptTierZeroIsFatalOnTheWirePathAndNamesP11) { }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; EXPECT_NE(r.Log.find("Fatal{UnimplementedAdoptTier, \"T0\"} - MOBILEGL_IPC_ADOPT_TIER=0 " - "names an adoption tier 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("Fatal{UnimplementedAdoptTier, \"T1\"} - MOBILEGL_IPC_ADOPT_TIER=1 " - "names an adoption tier P11 implements"), + "reached a map_persistent that no handshake settled"), std::string::npos) << r.Log; } diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index a9831ff3..a5472195 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -324,6 +324,55 @@ TEST(RemoteClientControls, MapPersistentErrorIsNotADecline) { ExpectNamedAbort(child, "Fatal{ReplyError, \"map_persistent\"}"); } +// P11 A1 (CONTRACT-P11 §1): MOBILEGL_IPC_ADOPT_TIER=0/1 OVER SHARED SEGMENTS DIES AT THE +// HANDSHAKE, before any record. inproc is shared segments by construction, and this is the real +// Start (the same StartOverTransportPair the lanes run). The child writes 'S' once Start returns +// and 'M' once its map_persistent went out, so "died before the first map_persistent" is a fact +// the parent reads rather than infers from the log. The stream half is AdoptTierStreamTest. +// +// RED-ONCE: move the check back to use (drop the client's SessionFail in +// Transport::SettleAdoptTierAtHandshake and make Client::AdoptTierIsEmulate abort on 0/1 again) +// -> the session comes up ('S'), the map_persistent goes out, and the child dies only when the +// server's codec asks the tier. +namespace { + void ExpectDeathAtTheSharedSegmentHandshake(Uint32 tier) { + int marks[2] = {-1, -1}; + ASSERT_EQ(::pipe(marks), 0); + const auto child = RunInChild([&marks, tier] { + ::close(marks[0]); + MG_Config::Ipc.AdoptTier = tier; + StartControlSession(); + const char started = 'S'; + (void)!::write(marks[1], &started, 1); + MGPHandleOnly handle{}; + handle.Kind = static_cast(MGPipeKind::Buffer); + (void)MGPipeRouteMapPersistent(handle, 64, nullptr); + const char sent = 'M'; + (void)!::write(marks[1], &sent, 1); + }); + ::close(marks[1]); + std::string seen; + char c = 0; + while (::read(marks[0], &c, 1) == 1) seen.push_back(c); + ::close(marks[0]); + const std::string word = "Fatal{UnimplementedAdoptTier, \"T" + std::to_string(tier) + "\"}"; + ExpectNamedAbort(child, word.c_str()); + EXPECT_EQ(seen, "") << "'S' = the session came up, 'M' = a map_persistent went out, both before " + "the death; T" << tier << " over shared segments must die at the handshake\n" + << child.Log; + EXPECT_NE(child.Log.find("refused at the handshake over shared segments"), std::string::npos) + << child.Log; + } +} // namespace + +TEST(RemoteClientControls, AdoptTierZeroOverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent) { + ExpectDeathAtTheSharedSegmentHandshake(0u); +} + +TEST(RemoteClientControls, AdoptTierOneOverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent) { + ExpectDeathAtTheSharedSegmentHandshake(1u); +} + TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) { const auto child = RunInChild([] { StartControlSession(); diff --git a/scripts/ci/fatal_census.py b/scripts/ci/fatal_census.py index ea5e9219..91a20cc6 100644 --- a/scripts/ci/fatal_census.py +++ b/scripts/ci/fatal_census.py @@ -184,7 +184,7 @@ FAMILY_ROW = re.compile(r"^\s*X\(([A-Za-z][A-Za-z0-9]*)\s*,", re.MULTILINE) REFUSE_ENUM = re.compile(r"enum\s+RefuseCode\s*:[^{]*\{(.*?)\}", re.DOTALL) ENUM_VALUE = re.compile(r"^\s*([A-Za-z][A-Za-z0-9]*)\s*=", re.MULTILINE) -# Two client-side declines that borrow the Refuse vocabulary for a refusal that never becomes a +# Local declines that borrow the Refuse vocabulary for a refusal that never becomes a # wire Refuse frame, so no peer ever reads the word and no RefuseCode can carry it. Listed rather # than renamed: inventing enumerators for them would move `wireFingerprint`, which is a protocol # change and not a wave-0 instrument's to make. Recorded for the integrator. @@ -195,6 +195,9 @@ LOCAL_REFUSAL_WORDS = { "InitialCapsSnapshot": "ClientSession.cpp:1292/:1302 - the CLIENT refusing a Welcome that carried no initial " "capabilities; the handshake has already been answered, so the decline is local", + "AdoptTierOnStream": + "Transport/AdoptTier.cpp (P11 A1) - either side refusing its OWN MOBILEGL_IPC_ADOPT_TIER=0/1 " + "on a stream data plane and running T2; the session goes on, so no Refuse frame is sent", }