From 2522f1803c5a4003c1fbec0458af77645da17382 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 17:40:56 -0400 Subject: [PATCH] [Test] (P11 B2): T0 protocol on the host - the Offer inbox (late, out of order, stale, missing) over both aux channels, the whole T0 path on a stand-in platform, A1's two handshake-Fatal cases rewritten as named fallbacks, revision 4 refused by name, AdoptTier0 lanes on the shared-segment arms and the allow switch through spawn --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 67 +++- .../Scenarios/LargeArenaAdoptionScenario.cpp | 51 +++ MobileGL/MG_Test/Buffer/SplitBufferTest.cpp | 24 +- MobileGL/MG_Test/Wire/AdoptInboxTest.cpp | 200 +++++++++++ MobileGL/MG_Test/Wire/CMakeLists.txt | 4 + MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc | 330 ++++++++++++++++++ .../MG_Test/Wire/RemoteClientControls.inc | 51 +-- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 1 + .../MG_Test/Wire/SessionHandshakeTest.cpp | 60 ++++ scripts/ci/spawn_lane_parity.py | 14 +- 10 files changed, 723 insertions(+), 79 deletions(-) create mode 100644 MobileGL/MG_Test/Wire/AdoptInboxTest.cpp create mode 100644 MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 74f1dc3f..a67d2aef 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2483,6 +2483,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ) list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}") math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1") + # P11 B2 (CONTRACT-P11 B2): THE SHARED-SEGMENT ARMS WITH MOBILEGL_IPC_ADOPT_TIER=0, which A1 made + # die at the handshake and B2 turned into an ASK. A host has no AHardwareBuffer, so the server + # refuses T0 by name at its first native bind, publishes no kCapAdoptT0, and the client falls + # back to T2 with one named line at its first map_persistent: every case green, the arena + # emulated (SplitLogPaths declares it), ASessionThatCannotRunT0FallsBackToT2OnceByName reading + # the two lines. `.AdoptT0Disallowed.` is the same on the spawn arm with the server's allow switch + # off - which also proves MOBILEGL_IPC_ALLOW_ADOPT_T0 survives ServerSpawn's environment scrub. + foreach(mglT0Arm IN ITEMS Split Spawn) + string(TOUPPER "${mglT0Arm}" mglT0ArmUpper) + string(TOLOWER "${mglT0Arm}" mglT0ArmLower) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.${mglT0Arm}.AdoptTier0." + TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" + TEST_FILTER "LargeArenaAdoptionScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-${mglT0ArmLower}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_${mglT0ArmUpper}_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer" + ) + list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}") + math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1") + endforeach() + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Spawn.AdoptT0Disallowed." + TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" + TEST_FILTER "LargeArenaAdoptionScenario.ASessionThatCannotRunT0FallsBackToT2OnceByName:LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-spawn" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPAWN_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MOBILEGL_IPC_ALLOW_ADOPT_T0=0\;MGITEST_ADOPT_T0_FALLBACK=disallowed" + ) + list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}") + math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1") # ---- P3b/P4b espryt D1 slice 1: THE ORPHANED XFB CAPTURE TARGET ------------------------- # @@ -3864,21 +3899,27 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-arena${mglItestArmLogTag}-${arenaCase}.log") endforeach() - # P11 A1 (CONTRACT-P11 §1): the tcp arm with MOBILEGL_IPC_ADOPT_TIER=0 - refused by name at - # the client's handshake, T2, the scenario green. TCP ONLY by construction (over shared - # segments the knob dies at the handshake); spawn_lane_parity.py names the tail in - # MAGMA_TCP_ONLY. + # P11 A1 + B2 (CONTRACT-P11 §1, B2): MOBILEGL_IPC_ADOPT_TIER=0 on every arm, one case set. + # On tcp a stream refuses the tier by name at the client's handshake (A1, the stream case + # reads the line); over shared segments (split, spawn) the knob ASKS T0, a host has no + # AHardwareBuffer, and the session falls back to T2 by name (B2, the fallback case reads + # both lines). Each arm's marker makes the other arm's case skip, so the three arms carry + # the same names and spawn_lane_parity.py compares them like any other lane. if(mglItestArm STREQUAL "Tcp") - foreach(arenaCase IN LISTS MGL_ITEST_ARENA_CASES ITEMS AStreamSessionRefusesTheAdoptTierOnceByName) - gtest_discover_tests(MobileGLIntegrationTest - TEST_PREFIX "DirectVulkan.Tcp.AdoptTier0." - TEST_FILTER "LargeArenaAdoptionScenario.${arenaCase}" - DISCOVERY_TIMEOUT 30 - PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} - ${mglItestArmProperties} - ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_TIER_REFUSAL=T0\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0-tcp-${arenaCase}.log") - endforeach() + set(mglItestAdoptTier0Env "MGITEST_ADOPT_TIER_REFUSAL=T0") + else() + set(mglItestAdoptTier0Env "MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer") endif() + foreach(arenaCase IN LISTS MGL_ITEST_ARENA_CASES ITEMS AStreamSessionRefusesTheAdoptTierOnceByName + ASessionThatCannotRunT0FallsBackToT2OnceByName) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.AdoptTier0." + TEST_FILTER "LargeArenaAdoptionScenario.${arenaCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;${mglItestAdoptTier0Env}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0${mglItestArmLogTag}-${arenaCase}.log") + endforeach() endforeach() endif() diff --git a/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp index 19692a33..05b1352a 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp @@ -99,6 +99,9 @@ void main() { word = 0xC0FFEEu; } // P11 A1: set (to the tier the lane names, "T0") only by the `.AdoptTier0.` tcp entries, // which also set MOBILEGL_IPC_ADOPT_TIER=0; a harness marker, never read by the library. constexpr const char* kAdoptTierRefusalMarker = "MGITEST_ADOPT_TIER_REFUSAL"; + // P11 B2: set by the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries to the reason word the + // server gives for not granting T0 ("no AHardwareBuffer" on a host, "disallowed" with its allow switch off). + constexpr const char* kAdoptT0FallbackMarker = "MGITEST_ADOPT_T0_FALLBACK"; // Draws issued against the arena inside the counted window. One definition, many draws: // "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different // numbers or the assertion cannot tell them apart. @@ -619,4 +622,52 @@ void main() { word = 0xC0FFEEu; } EXPECT_EQ(log.find("Fatal{UnimplementedAdoptTier"), std::string::npos) << log; } + // P11 B2 (MG_Remote/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT. + // MOBILEGL_IPC_ADOPT_TIER=0 over shared segments asks T0 in the Hello (A1 made it die at the + // handshake; ruling ID-P11-14 made it this). The lane's server cannot grant it - a host has no + // AHardwareBuffer, or `.AdoptT0Disallowed.` turned its allow switch off - so it refuses by name + // at its first native bind, publishes no kCapAdoptT0, and the client falls back to T2 with ONE + // named line at its first map_persistent. Never a Fatal; the other cases in the lane are green + // and the arena is emulated. Everywhere else it skips. + TEST_F(LargeArenaAdoptionScenario, ASessionThatCannotRunT0FallsBackToT2OnceByName) { + if (!Ready() || IsSkipped()) return; + const char* why = std::getenv(kAdoptT0FallbackMarker); + if (why == nullptr || why[0] == '\0') { + GTEST_SKIP() << "runs only in the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries, " + "which set MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptT0FallbackMarker; + } + if (PipeStatsWindow::LibraryLogPath().empty()) { + GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the two logs are where the " + "fallback is written"; + } + // A frame of the arena's work first: the fallback is only worth reading from a session that + // then ran on T2 - and the client decides at its first map_persistent, which SetUp's + // definition of the arena has already made. + UploadQuad(0.f, 1.f, 0.f); + DrawQuad(); + EXPECT_GT(CenterPixel()[1], 200) << "the session that fell back to T2 did not render"; + EXPECT_EQ(FirstGLError(), 0u); + Gl().EndFrame(); + + const auto countOf = [](const std::string& text, const std::string& needle) { + std::size_t n = 0; + for (auto at = text.find(needle); at != std::string::npos; at = text.find(needle, at + 1)) ++n; + return n; + }; + const std::string client = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath()); + const std::string server = PipeStatsWindow::ReadServerLogSince(PipeStatsWindow::LogMark{}); + const std::string clientLine = "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}"; + const std::string serverLine = std::string("Refuse{AdoptT0Unavailable, \"") + why + "\"}"; + RecordProperty("t0_client_fallbacks", static_cast(countOf(client, clientLine))); + RecordProperty("t0_server_refusals", static_cast(countOf(server, serverLine))); + EXPECT_EQ(countOf(client, clientLine), 1u) + << "the client names a T0 it asked for and cannot run exactly once, at its first map_persistent (" + << clientLine << "), in " << PipeStatsWindow::LibraryLogPath(); + EXPECT_EQ(countOf(server, serverLine), 1u) + << "the server names why it did not grant T0 exactly once, at its first native bind (" << serverLine + << "), in " << PipeStatsWindow::ServerLibraryLogPath(); + EXPECT_EQ(client.find("Fatal{"), std::string::npos) << client; + EXPECT_EQ(server.find("Fatal{"), std::string::npos) << server; + } + } // namespace MGITest diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index 3fec2322..1a7f5470 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -538,13 +538,14 @@ TEST_F(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheReques EXPECT_FALSE(buffer->HasOutstandingGpuWrite()); } -// 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) { +// The tier gate in P11 B2's shape (CONTRACT-P11 §1 and B2). 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; over shared segments a client with 0 ASKS T0 (0), with 1 is refused +// by name and asks T2, and a server serves by the client's ask (its own knob answers 2). None of +// them dies any more (ID-P11-14). The at-use check trusts a settled session and still dies by +// name for a map_persistent no handshake settled. The session halves are RemoteClientAdoptT0.* +// and AdoptTierStreamTest. +TEST_F(SplitBufferSet, TheAdoptTierIsSettledAtTheHandshakeAndNeverFatal) { namespace Tier = MG_Remote::Transport; EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate()); for (const Uint32 tier : {0u, 1u}) { @@ -552,8 +553,11 @@ TEST_F(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) { 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"); + Tier::ForgetAdoptTierSettlementForTest(); + EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(false, Tier::AdoptTierSide::Client), tier == 0 ? 0u : 2u); + EXPECT_EQ(Tier::AdoptTierAskFor(false), tier == 0 ? 0u : 2u); + EXPECT_EQ(Tier::AdoptTierAskFor(true), 2u); + EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate()); for (const auto side : {Tier::AdoptTierSide::Client, Tier::AdoptTierSide::Server}) { Tier::ForgetAdoptTierSettlementForTest(); EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(true, side), 2u); @@ -777,7 +781,7 @@ TEST(SplitBufferSet, BothEdgesOfAWriteMapPublishOneStateRecord) { MGL_SPLIT_ONLY TEST(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) { MGL_SPLIT_ONLY_OR_SKIP(); } TEST(SplitBufferSet, ADestroyedBufferLeavesTheSet) { MGL_SPLIT_ONLY_OR_SKIP(); } TEST(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheRequest) { MGL_SPLIT_ONLY_OR_SKIP(); } -TEST(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) { MGL_SPLIT_ONLY_OR_SKIP(); } +TEST(SplitBufferSet, TheAdoptTierIsSettledAtTheHandshakeAndNeverFatal) { MGL_SPLIT_ONLY_OR_SKIP(); } TEST(SplitBufferSet, ASubPageMapIsOneProtectedPageAndAWriteThroughItFaults) { MGL_SPLIT_ONLY_OR_SKIP(); } TEST(SplitBufferSet, AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmentAndItsEdges) { MGL_SPLIT_ONLY_OR_SKIP(); diff --git a/MobileGL/MG_Test/Wire/AdoptInboxTest.cpp b/MobileGL/MG_Test/Wire/AdoptInboxTest.cpp new file mode 100644 index 00000000..2586b7af --- /dev/null +++ b/MobileGL/MG_Test/Wire/AdoptInboxTest.cpp @@ -0,0 +1,200 @@ +// MobileGL - MobileGL/MG_Test/Wire/AdoptInboxTest.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 B2 (MG_Remote/CONTRACT-P11.md B2). Server::AdoptInbox - T0's Offers matched to their +// map_persistent records BY SEQ, never by arrival order - over both aux channels the product has: +// the in-process queue (inproc) and a real socket pair with SCM_RIGHTS (fork spawn / unix / fd:). +// +// Every case proves WHICH descriptor came back, not just that one did: each Offer's descriptor is +// a pipe whose far end carries the Offer's own seq, so a mix-up reads the wrong number. + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace MobileGL; +using namespace MobileGL::MG_Remote; +namespace T0 = MobileGL::MG_Remote::Transport::AdoptT0; + +namespace { + + struct Channel { + std::unique_ptr client; + std::unique_ptr server; + }; + + Channel MakeChannel(bool socket) { + Channel c; + if (socket) { + std::unique_ptr a, b; + EXPECT_EQ(Transport::SocketTransport::CreatePair(a, b), MOBILEGL_OK); + c.client = std::move(a); + c.server = std::move(b); + } else { + std::unique_ptr a, b; + Transport::InProcessTransport::CreatePair(a, b); + c.client = std::move(a); + c.server = std::move(b); + } + return c; + } + + // An Offer for `seq` whose descriptor is a pipe's read end; the pipe carries `seq` itself. + void SendOffer(Transport::ITransport& client, std::uint64_t seq, std::uint32_t flags = T0::kOfferHasBuffer) { + int p[2] = {-1, -1}; + ASSERT_EQ(::pipe(p), 0); + ASSERT_EQ(::write(p[1], &seq, sizeof(seq)), static_cast(sizeof(seq))); + ::close(p[1]); + T0::Offer offer{}; + offer.magic = T0::kOfferMagic; + offer.version = T0::kOfferVersion; + offer.seq = seq; + offer.size = 4096; + offer.flags = flags; + ASSERT_EQ(client.ShareFd(p[0], MobileGLByteSpan{&offer, sizeof(offer)}), MOBILEGL_OK); + ::close(p[0]); + } + + // The seq the descriptor's pipe carries, and closes it. + std::uint64_t SeqOf(int fd) { + std::uint64_t seq = ~0ull; + EXPECT_EQ(::read(fd, &seq, sizeof(seq)), static_cast(sizeof(seq))); + ::close(fd); + return seq; + } + + class AdoptInboxTest : public ::testing::TestWithParam {}; + +} // namespace + +// In order: each Take finds its own Offer. +TEST_P(AdoptInboxTest, OffersInOrderAreTakenByTheirOwnRecords) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + SendOffer(*c.client, 10); + SendOffer(*c.client, 11); + for (std::uint64_t seq : {10ull, 11ull}) { + int fd = -1; + T0::Offer offer{}; + std::string why; + ASSERT_EQ(inbox.Take(seq, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(offer.seq, seq); + EXPECT_EQ(SeqOf(fd), seq); + } + EXPECT_EQ(inbox.Kept(), 0u); +} + +// OUT OF ORDER: the Offer for the later record arrives first. It is kept for that record, the +// earlier record still gets its own, and the later one is then served from what was kept. +TEST_P(AdoptInboxTest, AnOfferForALaterRecordIsKeptForItAndDoesNotAnswerAnEarlierOne) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + SendOffer(*c.client, 21); + SendOffer(*c.client, 20); + int fd = -1; + T0::Offer offer{}; + std::string why; + ASSERT_EQ(inbox.Take(20, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(SeqOf(fd), 20u); + EXPECT_EQ(inbox.Kept(), 1u); + ASSERT_EQ(inbox.Take(21, 0, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(SeqOf(fd), 21u); + EXPECT_EQ(inbox.Kept(), 0u); +} + +// LATE: the record is being waited for when its Offer arrives. +TEST_P(AdoptInboxTest, AnOfferThatArrivesDuringTheWaitIsTaken) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + std::thread late([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + SendOffer(*c.client, 30); + }); + int fd = -1; + T0::Offer offer{}; + std::string why; + const auto start = std::chrono::steady_clock::now(); + const auto outcome = inbox.Take(30, 2000, &fd, &offer, why); + const auto waited = std::chrono::steady_clock::now() - start; + late.join(); + ASSERT_EQ(outcome, Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(SeqOf(fd), 30u); + EXPECT_GE(waited, std::chrono::milliseconds(150)); +} + +// STALE: an Offer for a record that was already answered (its wait timed out) is closed and +// counted, and does not answer the record being waited for. +TEST_P(AdoptInboxTest, AnOfferForAnEarlierRecordIsStaleAndClosed) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + SendOffer(*c.client, 40); + SendOffer(*c.client, 41); + int fd = -1; + T0::Offer offer{}; + std::string why; + ASSERT_EQ(inbox.Take(41, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(SeqOf(fd), 41u); + EXPECT_EQ(inbox.StaleOffers(), 1u); +} + +// MISSING: nothing comes - a bounded TimedOut with a reason, never a hang. +TEST_P(AdoptInboxTest, AnOfferThatNeverComesTimesOutWithinItsBound) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + int fd = -1; + T0::Offer offer{}; + std::string why; + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ(inbox.Take(50, 300, &fd, &offer, why), Server::AdoptInbox::Outcome::TimedOut); + const auto waited = std::chrono::steady_clock::now() - start; + EXPECT_EQ(fd, -1); + EXPECT_NE(why.find("no store arrived"), std::string::npos) << why; + EXPECT_LT(waited, std::chrono::milliseconds(2000)); +} + +// A descriptor whose sideband is not an Offer (another sender on the channel) is closed and +// counted, and the real Offer behind it still answers. +TEST_P(AdoptInboxTest, ADescriptorThatIsNotAnOfferIsSkipped) { + Channel c = MakeChannel(GetParam()); + Server::AdoptInbox inbox; + inbox.Attach(c.server.get()); + int p[2] = {-1, -1}; + ASSERT_EQ(::pipe(p), 0); + const std::uint32_t junk = 7; + ASSERT_EQ(c.client->ShareFd(p[0], MobileGLByteSpan{&junk, sizeof(junk)}), MOBILEGL_OK); + ::close(p[0]); + ::close(p[1]); + SendOffer(*c.client, 60); + int fd = -1; + T0::Offer offer{}; + std::string why; + ASSERT_EQ(inbox.Take(60, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why; + EXPECT_EQ(SeqOf(fd), 60u); + EXPECT_EQ(inbox.MalformedOffers(), 1u); +} + +INSTANTIATE_TEST_SUITE_P(Channels, AdoptInboxTest, ::testing::Values(false, true), + [](const ::testing::TestParamInfo& info) { + return info.param ? std::string("socket") : std::string("inprocess"); + }); diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index 4d3518e5..29803e4b 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -36,6 +36,10 @@ if (NOT WIN32) # P11 PAIR: the unix supervisors' pairing of a client's control and aux connections by the # nonce each presents first (Server/PairAcceptor), over real unix and TCP listeners. list(APPEND MOBILEGL_WIRE_TESTS PairAcceptorTest) + # P11 B2: T0's Offers matched to their map_persistent records by seq (Server/AdoptInbox) over + # the in-process queue and a real SCM_RIGHTS socket pair: in order, out of order, late, stale, + # missing (bounded), not-an-Offer. + list(APPEND MOBILEGL_WIRE_TESTS AdoptInboxTest) # P9 W3: the graded log forward - the level mapping both ways, the logger's level reaching the # forwarder, and the channel against a transport whose writes can be held (and a real socket # pair nobody reads). POSIX for the socket pair. diff --git a/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc b/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc new file mode 100644 index 00000000..9968e26d --- /dev/null +++ b/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc @@ -0,0 +1,330 @@ +// P11 B2 (MG_Remote/CONTRACT-P11.md B2): T0'S PROTOCOL ON THE HOST, over a live inproc session. +// +// A host has no AHardwareBuffer, so what these prove is the protocol, the negotiation and the +// fallback - never GPU correctness, which is the device's (B2's device gates). Two halves: +// * the real host platform: every T0 ask falls back to T2 by name (no AHB, the allow switch, +// knob 1), and never dies - A1's handshake Fatal for SharedSegments + 0/1 is gone; +// * a STAND-IN platform (a memfd for the AHB, AdoptT0::SetPlatformForTest) plus a stand-in +// importer (MGPipeResourceOps::ImportExternal): the whole T0 path runs - the grant, the caps +// bit, the client's store, the Offer on the real aux channel matched by seq, the import - and +// the store lands with the client's bytes, including when its Offer comes after its record; +// a lost Offer is a named DECLINED within the bounded wait, not a hang. +#if MGTEST_HAVE_FORK +#include +#include +#include + +#include +#include +#include + +namespace { +namespace T0 = MobileGL::MG_Remote::Transport::AdoptT0; + +constexpr Uint64 kT0Bytes = 64 * 1024; + +// ---- the stand-in AHB: a memfd, mapped shared, reference-counted ------------------------- +struct FakeAhb { + int fd = -1; + void* map = nullptr; + Uint64 size = 0; + std::atomic refs{1}; +}; +void FakeDrop(FakeAhb* f) { + if (f != nullptr && --f->refs == 0) { + if (f->map != nullptr) ::munmap(f->map, f->size); + if (f->fd >= 0) ::close(f->fd); + delete f; + } +} +bool FakeHasAhb() { return true; } +bool FakeAllocate(std::uint64_t size, const void* seed, T0::HeldStore& out, std::string& why) { + auto* f = new FakeAhb; + f->fd = static_cast(::syscall(SYS_memfd_create, "t0-fake", 1u /*MFD_CLOEXEC*/)); + if (f->fd < 0 || ::ftruncate(f->fd, static_cast(size)) != 0) { + why = "memfd"; + FakeDrop(f); + return false; + } + f->size = size; + f->map = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, f->fd, 0); + if (f->map == MAP_FAILED) { + f->map = nullptr; + why = "mmap"; + FakeDrop(f); + return false; + } + if (seed != nullptr) std::memcpy(f->map, seed, size); + out.ahb = f; + out.ptr = f->map; + out.size = size; + return true; +} +int FakeMakeHop(const T0::HeldStore* store, std::string& why) { + int pair[2] = {-1, -1}; + if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) { + why = "socketpair"; + return -1; + } + if (store != nullptr && store->ahb != nullptr) { + auto* f = static_cast(store->ahb); + const Uint64 size = f->size; + if (Transport::FdPassing::SendFd(pair[0], f->fd, MobileGLByteSpan{&size, sizeof(size)}) != MOBILEGL_OK) { + why = "send"; + ::close(pair[0]); + ::close(pair[1]); + return -1; + } + } + ::close(pair[0]); + return pair[1]; +} +void FakeReleaseHeld(T0::HeldStore& store) { + FakeDrop(static_cast(store.ahb)); + store = T0::HeldStore{}; +} +bool FakeReceive(int hopFd, std::uint64_t size, void** outAhb, std::string& why) { + *outAhb = nullptr; + int fd = -1; + Uint8 sideband[Transport::FdPassing::kMaxSidebandBytes] = {}; + std::uint64_t sidebandSize = 0; + if (Transport::FdPassing::ReceiveFd(hopFd, &fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, + &sidebandSize, 1000) != MOBILEGL_OK) { + why = "no stand-in handle on the hop"; + return false; + } + auto* f = new FakeAhb; + f->fd = fd; + f->size = size; + f->map = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (f->map == MAP_FAILED) { + f->map = nullptr; + FakeDrop(f); + why = "mmap"; + return false; + } + *outAhb = f; + return true; +} +void FakeAcquire(void* ahb) { ++static_cast(ahb)->refs; } +void FakeRelease(void* ahb) { FakeDrop(static_cast(ahb)); } + +const T0::PlatformForTest kFakePlatform{&FakeHasAhb, &FakeAllocate, &FakeMakeHop, &FakeReleaseHeld, + &FakeReceive, &FakeAcquire, &FakeRelease}; + +// ---- the stand-in importer: records what it was handed, keeps its own reference ---------- +struct FakeImports { + MGPipeHandle handle{}; + Uint64 size = 0; + Uint64 count = 0; + std::vector bytes; + void* held = nullptr; +}; +FakeImports g_fakeImports; +Bool FakeImportExternal(MGPipeHandle res, void* ahb, Uint64 size) { + auto* f = static_cast(ahb); + g_fakeImports.handle = res; + g_fakeImports.size = size; + ++g_fakeImports.count; + const auto* first = static_cast(f->map); + g_fakeImports.bytes.assign(first, first + size); + T0::AcquireImported(ahb); + g_fakeImports.held = ahb; + return true; +} +Bool FakeSelfTest(char* why, Uint64 whyBytes) { + std::snprintf(why, static_cast(whyBytes), "stand-in platform (host protocol test, no GPU)"); + return true; +} +MGPipeResourceOps MakeFakeOps() { + MGPipeResourceOps ops{}; + ops.ImportExternal = &FakeImportExternal; + ops.SelfTestExternal = &FakeSelfTest; + return ops; +} +MGPipeResourceOps g_fakeOps = MakeFakeOps(); + +// What the server's first native bind does for T0, on the apply thread like the real one: the +// grant (and its POST), then the caps that bind republishes - which the client's make-current +// adopts, as here. +void GrantAtFirstBind() { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { + Srv::ServerSessionInstance().SettleAdoptT0AtBind(); + return Srv::ServerSessionInstance().PublishCapsSnapshot(); + }, nullptr) != MOBILEGL_OK) ::_exit(120); + (void)ClientSessionInstance().PumpControlPlane(); +} + +MGPHandleOnly MakeBufferStore(Uint32 slot, Uint64 bytes) { + MGPResourceDesc desc{}; + desc.Resource = MGPipeHandle{slot, 1}; + desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer; + desc.Width = static_cast(bytes); + desc.Height = desc.Depth = 1; + if (!MGPipeRouteResourceCreate(desc)) ::_exit(121); + (void)MGPipeRouteResourceRespecify(desc, nullptr); + MGPHandleOnly handle{}; + handle.Handle = desc.Resource; + handle.Kind = static_cast(MGPipeKind::Buffer); + return handle; +} + +std::vector Pattern(Uint64 bytes, Uint8 salt) { + std::vector out(bytes); + for (Uint64 i = 0; i < bytes; ++i) out[i] = static_cast((i * 131u + salt) & 0xFF); + return out; +} + +size_t CountOf(const std::string& log, const std::string& needle) { + size_t n = 0; + for (size_t at = log.find(needle); at != std::string::npos; at = log.find(needle, at + 1)) ++n; + return n; +} + +// The grant with the stand-ins, the store, and the map_persistent: returns the adopted pointer. +void* AdoptOneStore(Uint8 salt, std::vector& seed) { + MG_Config::Ipc.AdoptTier = 0; + T0::SetPlatformForTest(&kFakePlatform); + MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); + StartControlSession(); + GrantAtFirstBind(); + seed = Pattern(kT0Bytes, salt); + const MGPHandleOnly handle = MakeBufferStore(5, kT0Bytes); + return MGPipeRouteMapPersistent(handle, kT0Bytes, seed.data()); +} +} // namespace + +// A1's "SharedSegments + 0 dies at the handshake" is GONE (ID-P11-14): knob 0 over shared segments +// ASKS T0 in the Hello, and a server that does not grant it (here: no bind ever ran, so no +// kCapAdoptT0) leaves the session on T2 with ONE named client line at the first map_persistent. +// RED-ONCE: restore the client's SessionFail in SettleAdoptTierAtHandshake -> the child dies +// before 'S'. +TEST(RemoteClientAdoptT0, KnobZeroOverSharedSegmentsAsksT0AndFallsBackToT2ByNameWithoutAGrant) { + const auto child = RunInChild([] { + MG_Config::Ipc.AdoptTier = 0; + StartControlSession(); + if (!Srv::ServerSessionInstance().AdoptT0Asked()) ::_exit(130); // the Hello asked T0 + MGPHandleOnly handle{}; + handle.Kind = static_cast(MGPipeKind::Buffer); + if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(131); + if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(132); + if (ClientSessionInstance().AdoptT0()) ::_exit(133); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}"), 1u) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} + +// Knob 1 names T1, which P11 closed: one named refusal, the Hello asks T2, never a Fatal. +TEST(RemoteClientAdoptT0, KnobOneIsRefusedByNameAndAsksT2) { + const auto child = RunInChild([] { + MG_Config::Ipc.AdoptTier = 1; + StartControlSession(); + if (Srv::ServerSessionInstance().AdoptT0Asked()) ::_exit(140); + MGPHandleOnly handle{}; + handle.Kind = static_cast(MGPipeKind::Buffer); + if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(141); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptTierClosed, \"T1\"}"), 1u) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} + +// The host's own platform: the grant refuses by name (no AHB) and the client falls back by name. +TEST(RemoteClientAdoptT0, NoAhardwareBufferOnThisPlatformIsT2AndTwoNamedLines) { + const auto child = RunInChild([] { + MG_Config::Ipc.AdoptTier = 0; + MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); // a backend WITH the members: the platform decides + StartControlSession(); + GrantAtFirstBind(); + if (Srv::ServerSessionInstance().AdoptT0()) ::_exit(150); + if (ClientSessionInstance().AdoptT0()) ::_exit(151); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no AHardwareBuffer\"}"), 1u) << child.Log; + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}"), 1u) << child.Log; +} + +// The server's allow switch off: T2 and a named line, even where T0 would otherwise work. +TEST(RemoteClientAdoptT0, TheServersAllowSwitchOffIsT2AndANamedLine) { + const auto child = RunInChild([] { + MG_Config::Ipc.AllowAdoptT0 = 0; + std::vector seed; + if (AdoptOneStore(0x11, seed) != nullptr) ::_exit(160); + if (g_fakeImports.count != 0) ::_exit(161); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"disallowed\"}"), 1u) << child.Log; + EXPECT_NE(child.Log.find("MOBILEGL_IPC_ALLOW_ADOPT_T0=0"), std::string::npos) << child.Log; +} + +// THE WHOLE PATH: granted, the caps bit read, the store allocated and seeded from the shadow, +// its Offer queued before its record, matched by seq, imported - and the importer holds exactly +// the client's bytes, the client's pointer is the store, and both arms count it. +// RED-ONCE (host): do not publish kCapAdoptT0 (ServerSession::PublishCapsSnapshot) -> the client +// never adopts, 170. +TEST(RemoteClientAdoptT0, AGrantedStoreLandsWithTheClientsBytes) { + const auto child = RunInChild([] { + std::vector seed; + void* adopted = AdoptOneStore(0x22, seed); + if (adopted == nullptr) ::_exit(170); + if (std::memcmp(adopted, seed.data(), kT0Bytes) != 0) ::_exit(171); + if (g_fakeImports.count != 1 || g_fakeImports.size != kT0Bytes) ::_exit(172); + if (g_fakeImports.handle.Slot != 5 || g_fakeImports.handle.Gen != 1) ::_exit(173); + if (g_fakeImports.bytes != seed) ::_exit(174); + // The import and the client's pointer are one store: a write through either is the other's. + static_cast(adopted)[100] = 0xA5; + if (static_cast(static_cast(g_fakeImports.held)->map)[100] != 0xA5) ::_exit(175); + if (ClientSessionInstance().T0StoresImported() != 1 || ClientSessionInstance().T0BytesImported() != kT0Bytes) + ::_exit(176); + if (Srv::ServerSessionInstance().T0StoresImported() != 1) ::_exit(177); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_NE(child.Log.find("T0 granted"), std::string::npos) << child.Log; + EXPECT_NE(child.Log.find("T0 imported store {slot=5, gen=1} - 65536 bytes"), std::string::npos) << child.Log; + EXPECT_NE(child.Log.find("T0 store {slot=5, gen=1} adopted - 65536 bytes"), std::string::npos) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} + +// ORDER: the Offer arrives 300 ms AFTER its record was published and decoded - the apply thread +// waits for it (bounded) and the store still lands with the client's bytes. +TEST(RemoteClientAdoptT0, AnOfferThatArrivesAfterItsRecordStillLands) { + const auto child = RunInChild([] { + ClientSession::SetT0OfferFaultForTest(300); + std::vector seed; + const auto start = std::chrono::steady_clock::now(); + void* adopted = AdoptOneStore(0x33, seed); + const auto waited = std::chrono::steady_clock::now() - start; + if (adopted == nullptr) ::_exit(180); + if (g_fakeImports.bytes != seed) ::_exit(181); + if (waited < std::chrono::milliseconds(300)) ::_exit(182); // it really waited for the late Offer + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(child.Log.find("AdoptT0NoStore"), std::string::npos) << child.Log; +} + +// A LOST Offer is a NAMED DECLINE within the bounded wait - never a hang: the store runs T2. +TEST(RemoteClientAdoptT0, AnOfferThatNeverArrivesIsANamedDeclineNotAHang) { + const auto child = RunInChild([] { + ClientSession::SetT0OfferFaultForTest(-1); + std::vector seed; + const auto start = std::chrono::steady_clock::now(); + void* adopted = AdoptOneStore(0x44, seed); + const auto waited = std::chrono::steady_clock::now() - start; + if (adopted != nullptr) ::_exit(190); + if (g_fakeImports.count != 0) ::_exit(191); + if (waited > std::chrono::milliseconds(T0::kOfferWaitMs + 3000)) ::_exit(192); + if (ClientSessionInstance().T0Declined() != 1) ::_exit(193); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0NoStore, \"record"), 1u) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} +#endif diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index a5472195..48c1ea58 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -324,54 +324,9 @@ 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); -} +// P11 A1's two handshake-Fatal cases (AdoptTier{Zero,One}OverSharedSegmentsDiesAtTheHandshake...) +// are rewritten for B2's fallback semantics in RemoteClientAdoptT0.inc (ID-P11-14: a T0 this +// session cannot use is T2 plus one named line, never a Fatal). TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) { const auto child = RunInChild([] { diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 2880bad1..39b2ef5c 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1721,6 +1721,7 @@ TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) { } #include "RemoteClientControls.inc" +#include "RemoteClientAdoptT0.inc" #include #include #include diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index 094d52c6..672d90de 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -261,6 +261,66 @@ TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting) CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(), ::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint); } +// P11 B2 (CONTRACT-P11 B2): CONTROL REVISION 5. A peer built at revision 4 - before +// LinkTerms.adoptTier - carries the fingerprint its revision mixed, and is refused by name +// (Refuse{WireFingerprint}), never read as a revision-5 Hello that happens to ask T2. The same shape +// PAIR's 3 -> 4 had. RED ONCE by leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 4 with the schema +// change: the two fingerprints are then equal and the Welcome goes out. +TEST(SessionHandshakeTest, ARevisionFourPeerIsRefusedByNameAtRevisionFive) { + static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 5, "B2 bumped the control revision 4 -> 5"); + auto inputs = CapsAbiFingerprintInputs(); +#if MOBILEGL_BUILD_DISAGGREGATED + inputs.ControlSchemaRevision = (static_cast(4) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision; +#else + inputs.ControlSchemaRevision = 4; +#endif + const Uint64 revisionFour = Transport::MixAbiFingerprint(inputs); + ASSERT_NE(revisionFour, WireFingerprint()); + CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFour, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect, + ::MobileGL::Wire::RefuseCode::WireFingerprint); +} + +// P11 B2: the Hello's adoptTier is an ASK of 2 or (shared segments) 0 - T1 is closed and never sent, +// and anything else is not an ask this revision knows. Refuse{LinkTerms} by name; the echo of a +// legal ask is the Welcome's. +TEST(SessionHandshakeTest, AHelloAskingAnAdoptTierThatIsNotAnAskIsRefusedByName) { + using namespace ::MobileGL::Wire; + for (const Uint8 ask : {Uint8{0}, Uint8{1}, Uint8{2}, Uint8{7}}) { + SCOPED_TRACE(static_cast(ask)); + std::unique_ptr client, server; + Transport::InProcessTransport::CreatePair(client, server); + ::flatbuffers::FlatBufferBuilder builder(512); + auto terms = CreateLinkTerms(builder, DataPlane::SharedSegments, WireForm::StructImage, 0, 0, 0, 0, ask); + auto hello = CreateHelloDirect(builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, + BuildFingerprint(), 0, 1, nullptr, WireFingerprint(), WireFingerprint(), + terms, nullptr, DialMode::No); + auto root = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union()); + FinishCtrlEnvelopeBuffer(builder, root); + std::vector first(builder.GetBufferPointer(), builder.GetBufferPointer() + builder.GetSize()); + Server::ServerSession session; + Transport::SessionSegmentSizes sizes; + sizes.CmdRingBytes = 4096; sizes.StageBytes = 4096; + sizes.ReplyBytes = 4096; sizes.EventRingBytes = 4096; + session.SetSegmentSizes(sizes); + const Bool legal = ask == 0 || ask == 2; + EXPECT_EQ(session.Accept(*server, &first), legal ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH); + const auto reply = ReadHandshakeFrame(*client); + ASSERT_FALSE(reply.empty()); + const auto* envelope = GetCtrlEnvelope(reply.data()); + if (legal) { + ASSERT_NE(envelope->msg_as_Welcome(), nullptr); + ASSERT_NE(envelope->msg_as_Welcome()->linkTerms(), nullptr); + EXPECT_EQ(envelope->msg_as_Welcome()->linkTerms()->adoptTier(), ask) << "the Welcome echoes the ask"; + EXPECT_EQ(session.AdoptT0Asked(), ask == 0); + EXPECT_FALSE(session.AdoptT0()) << "no grant before the first native bind"; + } else { + ASSERT_NE(envelope->msg_as_Refuse(), nullptr); + EXPECT_EQ(envelope->msg_as_Refuse()->code(), RefuseCode::LinkTerms); + } + session.Close(); + } +} + TEST(SessionHandshakeTest, ConnectAcceptsDifferentBuildWithIdenticalWire) { ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "0"); CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit", diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 425ed80a..cecd9c7d 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -141,14 +141,12 @@ MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", f".SplitRecording64.OversizedRecordingSplitScenario.{case}" for case in SPLIT_RECORDING_CASES) -# P11 A1: THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. `.AdoptTier0.` runs -# LargeArenaAdoptionScenario with MOBILEGL_IPC_ADOPT_TIER=0 on the CLIENT: a stream data plane -# refuses that tier by name and runs T2 (the scenario is green there), while over shared segments - -# the split and spawn arms - the same knob dies at the handshake by design (CONTRACT-P11 §1), so a -# split or spawn copy could only ever be a death. Each tail must match at least one tcp entry, or -# the exception names nothing. (The DirectGLES `.AdoptTier0.` twins need no entry: the Espryt -# CASE pattern does not parse a middle segment, so they never enter that comparison.) -MAGMA_TCP_ONLY = (".AdoptTier0.",) +# THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. Each tail must match at least +# one tcp entry, or the exception names nothing. P11 A1 put `.AdoptTier0.` here (over shared segments +# MOBILEGL_IPC_ADOPT_TIER=0 died at the handshake); P11 B2 made that knob an ASK that falls back to T2 +# by name, so the Magma `.AdoptTier0.` lane now runs on all three arms with one case set and is +# compared like any other. The mechanism stays for the next tcp-only lane. +MAGMA_TCP_ONLY = () def lane_names(build_dir, label):