From ce836513494589285fd4c7ff2ca145126df54fb0 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 20:28:52 -0400 Subject: [PATCH] [MG_Remote, MG_Test, ci] (P7): a Hello asking for 64 GiB windows is welcomed with the server's own terms and its session child never reaches 64 GiB, pinned in process and on the TCP supervisor --- .../MG_Test/Wire/SessionHandshakeTest.cpp | 53 +++++++++++++++++++ scripts/ci/tcp_supervisor_smoke.py | 42 ++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index e234e26e..f51c4d60 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -261,6 +261,59 @@ TEST(SessionHandshakeTest, ForkRefusesDifferentBuildWithIdenticalWire) { CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit", ::MobileGL::Wire::DialMode::Fork, ::MobileGL::Wire::RefuseCode::BuildFingerprint); } +// PH-8. THE SERVER SIZES THE SESSION; THE CLIENT'S HELLO ONLY ASKS. +// +// Hello.linkTerms carries four byte counts a client may fill in, and nothing on the server reads +// them for sizing: Accept builds the segments from its own SetSegmentSizes and Welcome states +// those. That was stated (CONTRACT-P65 LinkTerms) and never pinned, so a refactor that "honoured +// the client's request" would have handed any peer a 64 GiB allocation per connection. Here a +// Hello asks for 64 GiB in every window and the Welcome must come back with the server's own +// 4 KiB terms and 4 KiB segments - the allocation that happened is the one Welcome describes. +// RED ONCE by making Accept size from the Hello's terms (m_sizes <- hello->linkTerms()): the +// Accept below then fails to create a 64 GiB private segment, or the EXPECT_EQs name the window +// that was echoed. The two-process half, with the child's own VmPeak, is +// TcpLane.SupervisorProtocolControls' `hello_asks_64_gib`. +TEST(SessionHandshakeTest, AHelloAskingFor64GiBIsClampedToTheServersTermsAndAllocatesNothingOfIt) { + using namespace ::MobileGL::Wire; + constexpr Uint64 kAsk = 64ull << 30; + std::unique_ptr client, server; + Transport::InProcessTransport::CreatePair(client, server); + ::flatbuffers::FlatBufferBuilder builder(512); + auto terms = CreateLinkTerms(builder, DataPlane::SharedSegments, WireForm::StructImage, kAsk, kAsk, kAsk, kAsk); + 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); + ASSERT_EQ(session.Accept(*server, &first), MOBILEGL_OK); + const auto reply = ReadHandshakeFrame(*client); + ASSERT_FALSE(reply.empty()); + ::flatbuffers::Verifier verifier(reply.data(), reply.size()); + ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier)); + const auto* welcome = GetCtrlEnvelope(reply.data())->msg_as_Welcome(); + ASSERT_NE(welcome, nullptr); + ASSERT_NE(welcome->linkTerms(), nullptr); + EXPECT_EQ(welcome->linkTerms()->cmdWindowBytes(), 4096u) << "the command window echoed the Hello"; + EXPECT_EQ(welcome->linkTerms()->stageWindowBytes(), 4096u) << "the stage window echoed the Hello"; + EXPECT_EQ(welcome->linkTerms()->eventWindowBytes(), 4096u) << "the event window echoed the Hello"; + EXPECT_EQ(welcome->linkTerms()->maxReplyBytes(), + 4096u / sizes.ReplySlotCount - sizeof(Transport::ReplySlotHeader)) << "the reply bound echoed the Hello"; + // The segments that EXIST are the server's: each announced size is a few pages at most, never + // the ask. A 64 GiB request that had been honoured would show here even if Welcome's terms + // had been rewritten afterwards. + for (const auto* segment : {welcome->cmdRing(), welcome->stageRing(), welcome->replyPool(), welcome->eventRing()}) { + ASSERT_NE(segment, nullptr); + EXPECT_LE(segment->sizeBytes(), 64u * 1024u); + } + session.Close(); +} + TEST(SessionHandshakeTest, ConnectCanRequireTheSameBuild) { ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "1"); CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit", diff --git a/scripts/ci/tcp_supervisor_smoke.py b/scripts/ci/tcp_supervisor_smoke.py index 79fd1e02..981b8654 100644 --- a/scripts/ci/tcp_supervisor_smoke.py +++ b/scripts/ci/tcp_supervisor_smoke.py @@ -76,6 +76,15 @@ def read_exact(peer, size): return result +def welcome_terms(welcome): + """The four byte counts a Welcome states, or {} when it carries no LinkTerms.""" + terms = welcome.LinkTerms() + if terms is None: + return {} + return {'maxReply': terms.MaxReplyBytes(), 'cmdWindow': terms.CmdWindowBytes(), + 'stageWindow': terms.StageWindowBytes(), 'eventWindow': terms.EventWindowBytes()} + + def receive(peer, schema): magic, size = struct.unpack('<4sI', read_exact(peer, 8)) if magic != b'MGLF' or size > 64 * 1024 * 1024: @@ -93,17 +102,26 @@ def receive(peer, schema): welcome = schema['Welcome'].Welcome() welcome.Init(table.Bytes, table.Pos) return {'welcome': welcome.ServerPid(), 'fingerprint': welcome.WireFingerprint(), - 'nonce': bytes(welcome.DataNonce(j) for j in range(welcome.DataNonceLength())).hex()} + 'nonce': bytes(welcome.DataNonce(j) for j in range(welcome.DataNonceLength())).hex(), + 'terms': welcome_terms(welcome)} raise RuntimeError(f'unexpected first control reply {kind}') -def hello(schema, flatbuffers, fingerprint=0, major=1, token=kToken, identifier=b'MGLC'): +def hello(schema, flatbuffers, fingerprint=0, major=1, token=kToken, identifier=b'MGLC', ask=0): builder = flatbuffers.Builder(512) build = builder.CreateString('intentionally-different-build-for-p65-control') auth = builder.CreateString(token) terms = schema['LinkTerms'] terms.Start(builder) terms.AddDataPlane(builder, 1) + if ask: + # PH-8: a client ASKING for windows. The server sizes the session itself; see + # `hello_asks_64_gib` below. + terms.AddWireForm(builder, 0) + terms.AddMaxReplyBytes(builder, ask) + terms.AddCmdWindowBytes(builder, ask) + terms.AddStageWindowBytes(builder, ask) + terms.AddEventWindowBytes(builder, ask) link = terms.End(builder) message = schema['Hello'] message.Start(builder) @@ -505,6 +523,26 @@ def main(): l.strip() for l in named.splitlines() if 'data connection nonce mismatch' in l)} assert process.poll() is None, 'a refused data connection killed the supervisor' + # ---- PH-8: THE SERVER SIZES THE SESSION; THE CLIENT ONLY ASKS ------------------- + # + # A Hello that asks for 64 GiB in every window is welcomed with the server's own + # terms, and the session child that answered it - named by the Welcome's serverPid - + # never had 64 GiB of address space at any point (VmPeak, read after its data + # connection bound, i.e. after every segment of the session exists). + ask = 64 << 30 + with held_session(port, schema, flatbuffers, + hello(schema, flatbuffers, fingerprint=fingerprint, ask=ask)) as (control, welcome): + assert welcome.get('welcome', 0), welcome + terms = welcome['terms'] + assert terms and all(0 < value < (1 << 30) for value in terms.values()), ( + f'the Welcome did not clamp a 64 GiB ask to the server\'s own terms: {terms}') + ready = wait_for_log(serverLog, f'pid={welcome["welcome"]} transport=spawn role=server ready') + assert ready is not None, f'the 64 GiB-asking session never came up; see {serverLog}' + status = Path(f'/proc/{welcome["welcome"]}/status').read_text() + peak = int(re.search(r'VmPeak:\s+(\d+) kB', status).group(1)) * 1024 + assert peak < ask, f'session pid={welcome["welcome"]} reached VmPeak={peak} for a 64 GiB ask' + evidence['hello_asks_64_gib'] = {'asked': ask, 'terms': terms, 'childVmPeakBytes': peak} + with supervisor(args.server, args.out / 'same-build-supervisor.log', same_build=True) as (port, _, _log): strict = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint)) assert strict.get('code') == 3, strict