mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
447 lines
24 KiB
C++
447 lines
24 KiB
C++
// MobileGL - MobileGL/MG_Test/Wire/SessionHandshakeTest.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
|
|
|
|
// The two handshakes, driven for real (P5 s1, wave 1.5 - ID-46 findings 6 and 7).
|
|
//
|
|
// WHY THIS SUITE EXISTS BESIDE SessionTest. SessionTest is the ring-owning suite and keeps the
|
|
// GL frontend's umbrella header out on purpose. Both of the wave-1 review's findings against it
|
|
// were the same defect seen twice: a case that observed a property of the thing it built itself
|
|
// - a null-union frame it never sent anywhere, a mixer production never called - and so could
|
|
// not go red when the production code it was named for was deleted. The cure for both is to
|
|
// start from the production entry point, and the production entry points (ServerSession::Accept,
|
|
// ClientSession::StartOverTransportPair, CapsAbiFingerprint) all reach Includes.h. So they are
|
|
// exercised here, in a target that carries the include paths and links gtest rather than
|
|
// gtest_main: each guard's refusal is asserted BY MESSAGE, and with the console sink compiled
|
|
// out (Defines.h) an MGLOG line reaches exactly one place, the log file this process names
|
|
// before anything logs - PipeWireCodecTest's main() shape.
|
|
//
|
|
// EVERY CASE BELOW CARRIES ITS RED-ONCE LINE, and each of those perturbations was run.
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "Includes.h"
|
|
|
|
#include <MG_Remote/CapsCodec.h>
|
|
#include <MG_Remote/Handshake.h>
|
|
#include <MG_Pipe/PipeWireLayout.h>
|
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
|
#include <MG_Remote/Client/ClientSession.h>
|
|
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
|
#include <MG_Remote/Server/ServerSession.h>
|
|
#include <MG_Remote/Transport/InProcessTransport.h>
|
|
#include <MG_Remote/Transport/SessionRings.h>
|
|
|
|
#if __has_include(<MGGitHash.h>)
|
|
#include <MGGitHash.h>
|
|
#define MGL_HANDSHAKE_TEST_HAS_GIT_HASH 1
|
|
#else
|
|
#define MGL_HANDSHAKE_TEST_HAS_GIT_HASH 0
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
#include <process.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_Remote;
|
|
namespace Transport = MobileGL::MG_Remote::Transport;
|
|
|
|
namespace {
|
|
|
|
std::string g_logPath;
|
|
|
|
std::string ReadLog() {
|
|
// BOTH ROLES' LOGS (P6). A death test asserts that the CHILD said something; which
|
|
// role's thread said it is not what these cases are about, and refusals raised on the
|
|
// apply thread are written under the SERVER role by construction.
|
|
return MobileGL::MG_Util::Debug::ReadRoleLogs(g_logPath.c_str());
|
|
}
|
|
|
|
long ProcessId() {
|
|
#if defined(_WIN32)
|
|
return static_cast<long>(::_getpid());
|
|
#else
|
|
return static_cast<long>(::getpid());
|
|
#endif
|
|
}
|
|
|
|
bool Contains(const std::string& haystack, const char* needle) {
|
|
return haystack.find(needle) != std::string::npos;
|
|
}
|
|
|
|
// The reviewer's 24-byte shape (SessionTest.ANullUnionFrameVerifiesWhichIsThePremiseOf-
|
|
// BothHandshakeGuards proves it verifies): the envelope's tag says `tag` and its union
|
|
// member is NULL, because FlatBuffers' Verifier::VerifyTable is `return !table ||
|
|
// table->Verify(*this)`.
|
|
std::vector<Uint8> BuildNullUnionFrame(::MobileGL::Wire::CtrlMsg tag) {
|
|
::flatbuffers::FlatBufferBuilder builder(256);
|
|
auto envelope =
|
|
::MobileGL::Wire::CreateCtrlEnvelope(builder, tag, ::flatbuffers::Offset<void>());
|
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
|
|
const Uint8* begin = builder.GetBufferPointer();
|
|
return std::vector<Uint8>(begin, begin + builder.GetSize());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The ABI fingerprint, from the production entry point (ID-46 finding 6)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// The value Hello and Welcome carry is CapsAbiFingerprint(). This case starts THERE, requires it
|
|
// to be the mixer over its own published inputs, pins those inputs to the real sizeofs and
|
|
// constants (ID-33's list: the three struct sizes, kOpCount, the protocol ABI version, the git
|
|
// stamp - plus the caps blob's extents and the two codec versions), and then perturbs every
|
|
// input by one through the same mixer and requires the answer to move. RED ONCE by replacing
|
|
// CapsAbiFingerprint()'s body with `return 1;` - the verifier's exact perturbation, which left
|
|
// the whole unit lane green before this case existed - and it fails on the first EXPECT_EQ
|
|
// below. Also red, separately, by deleting any one `mix(...)` line from MixAbiFingerprint: the
|
|
// matching EXPECT_NE names the input that stopped being mixed. Both perturbations were run.
|
|
TEST(SessionHandshakeTest, TheAbiFingerprintChangesWhenAnyOfItsInputsDoes) {
|
|
const Uint64 production = WireFingerprint();
|
|
const auto inputs = CapsAbiFingerprintInputs();
|
|
EXPECT_NE(production, 0u);
|
|
EXPECT_EQ(production, CapsAbiFingerprint());
|
|
EXPECT_EQ(production, Transport::MixAbiFingerprint(inputs));
|
|
EXPECT_EQ(inputs.DynamicParamsSize, sizeof(MG_Backend::DynamicBackendParameters));
|
|
EXPECT_EQ(inputs.CapsSize, sizeof(MG_Pipe::MGPCaps));
|
|
EXPECT_EQ(inputs.MemberLayout, MG_Pipe::kMGPipeWireMemberLayoutDigest);
|
|
EXPECT_EQ(inputs.CatalogueLayout, MG_Pipe::kMGPipeWireCatalogueDigest);
|
|
EXPECT_EQ(inputs.RenderStateLayout, MG_Pipe::WireRenderStateDigest());
|
|
EXPECT_EQ(inputs.ProgramArtifactsCodecVersion, MG_State::GLState::kProgramArtifactsCodecVersion);
|
|
EXPECT_EQ(inputs.ProgramArtifactsSchema, MG_State::GLState::ProgramArtifactsSchemaFingerprint());
|
|
EXPECT_EQ(inputs.OpCount, static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount));
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
EXPECT_EQ(inputs.ControlSchemaRevision,
|
|
(static_cast<Uint64>(MOBILEGL_PROTOCOL_CONTROL_REVISION) << 32) |
|
|
MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision);
|
|
#else
|
|
EXPECT_EQ(inputs.ControlSchemaRevision, static_cast<Uint64>(MOBILEGL_PROTOCOL_CONTROL_REVISION));
|
|
#endif
|
|
EXPECT_EQ(inputs.PointerBits, sizeof(void*) * 8);
|
|
const auto perturbed = [&](auto mutate) {
|
|
auto copy = inputs;
|
|
mutate(copy);
|
|
return Transport::MixAbiFingerprint(copy);
|
|
};
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.DynamicParamsSize; })) << "DynamicParamsSize";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.CapsSize; })) << "CapsSize";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.MemberLayout; })) << "MemberLayout";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.CatalogueLayout; })) << "CatalogueLayout";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.RenderStateLayout; })) << "RenderStateLayout";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilityTargets; })) << "FormatCapabilityTargets";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilityFormats; })) << "FormatCapabilityFormats";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilitiesCodecVersion; })) << "FormatCapabilitiesCodecVersion";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.RendererInfoCodecVersion; })) << "RendererInfoCodecVersion";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.ProgramArtifactsCodecVersion; })) << "ProgramArtifactsCodecVersion";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.ProgramArtifactsSchema; })) << "ProgramArtifactsSchema";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.OpCount; })) << "OpCount";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.ControlSchemaRevision; })) << "ControlSchemaRevision";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.AbiVersion; })) << "AbiVersion";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.PointerBits; })) << "PointerBits";
|
|
EXPECT_NE(production, perturbed([](auto& i) { ++i.LittleEndian; })) << "LittleEndian";
|
|
}
|
|
|
|
TEST(SessionHandshakeTest, SameWidthFieldReorderingChangesTheWireDigestWithoutABuildStamp) {
|
|
using namespace MG_Pipe;
|
|
std::vector<WireLayoutMember> fields(std::begin(kMGPipeWireLayoutMembers),
|
|
std::end(kMGPipeWireLayoutMembers));
|
|
// MGPRange.Offset and Size have the same width: sizeof-only checks cannot see this.
|
|
SizeT offsetIndex = 0, sizeIndex = 0;
|
|
for (SizeT i = 0; i < fields.size(); ++i) {
|
|
if (std::strcmp(fields[i].Name, "MGPRange.Offset") == 0) offsetIndex = i;
|
|
if (std::strcmp(fields[i].Name, "MGPRange.Size") == 0) sizeIndex = i;
|
|
}
|
|
ASSERT_NE(offsetIndex, sizeIndex);
|
|
ASSERT_EQ(fields[offsetIndex].Size, fields[sizeIndex].Size);
|
|
std::swap(fields[offsetIndex].Offset, fields[sizeIndex].Offset);
|
|
EXPECT_NE(WireMemberLayoutDigest(fields.data(), fields.size()), kMGPipeWireMemberLayoutDigest);
|
|
const auto before = WireFingerprint();
|
|
EXPECT_STREQ(BuildFingerprint(), MOBILEGL_BUILD_STAMP_VALUE);
|
|
EXPECT_EQ(BuildFingerprintPresent(), MOBILEGL_BUILD_STAMP_PRESENT != 0);
|
|
EXPECT_EQ(before, WireFingerprint());
|
|
}
|
|
|
|
namespace {
|
|
class ScopedHandshakeEnvironment {
|
|
public:
|
|
ScopedHandshakeEnvironment(const char* name, const char* value) : m_name(name) {
|
|
const char* previous = std::getenv(name);
|
|
m_present = previous != nullptr;
|
|
if (m_present) m_previous = previous;
|
|
Set(value);
|
|
}
|
|
~ScopedHandshakeEnvironment() { Set(m_present ? m_previous.c_str() : nullptr); }
|
|
private:
|
|
void Set(const char* value) {
|
|
#if defined(_WIN32)
|
|
::_putenv_s(m_name, value == nullptr ? "" : value);
|
|
#else
|
|
if (value == nullptr) ::unsetenv(m_name); else ::setenv(m_name, value, 1);
|
|
#endif
|
|
}
|
|
const char* m_name;
|
|
bool m_present = false;
|
|
std::string m_previous;
|
|
};
|
|
|
|
std::vector<Uint8> ReadHandshakeFrame(Transport::ITransport& transport) {
|
|
Uint64 bytes = 0;
|
|
if (transport.ReceiveFrame({nullptr, 0}, &bytes, 1000) != MOBILEGL_ERR_BUFFER_TOO_SMALL) return {};
|
|
std::vector<Uint8> frame(bytes);
|
|
if (transport.ReceiveFrame({frame.data(), frame.size()}, &bytes, 1000) != MOBILEGL_OK) return {};
|
|
return frame;
|
|
}
|
|
|
|
void CheckHandshake(Uint32 major, Uint64 wire, const char* build,
|
|
::MobileGL::Wire::DialMode dial, ::MobileGL::Wire::RefuseCode expected) {
|
|
using namespace ::MobileGL::Wire;
|
|
std::unique_ptr<Transport::InProcessTransport> client, server;
|
|
Transport::InProcessTransport::CreatePair(client, server);
|
|
::flatbuffers::FlatBufferBuilder builder(512);
|
|
auto terms = CreateLinkTerms(builder);
|
|
auto hello = CreateHelloDirect(builder, major, MOBILEGL_PROTOCOL_ABI_MINOR, build,
|
|
0, 1, nullptr, wire, wire, terms, nullptr, dial);
|
|
auto root = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
|
|
FinishCtrlEnvelopeBuffer(builder, root);
|
|
std::vector<Uint8> 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 auto result = session.Accept(*server, &first);
|
|
EXPECT_EQ(result, expected == RefuseCode::None ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH);
|
|
const auto reply = ReadHandshakeFrame(*client);
|
|
ASSERT_FALSE(reply.empty());
|
|
::flatbuffers::Verifier verifier(reply.data(), reply.size());
|
|
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
|
|
const auto* envelope = GetCtrlEnvelope(reply.data());
|
|
if (expected == RefuseCode::None) {
|
|
ASSERT_NE(envelope->msg_as_Welcome(), nullptr);
|
|
EXPECT_EQ(envelope->msg_as_Welcome()->wireFingerprint(), WireFingerprint());
|
|
ASSERT_NE(envelope->msg_as_Welcome()->linkTerms(), nullptr);
|
|
EXPECT_EQ(envelope->msg_as_Welcome()->linkTerms()->cmdWindowBytes(), 4096u);
|
|
} else {
|
|
ASSERT_NE(envelope->msg_as_Refuse(), nullptr);
|
|
EXPECT_EQ(envelope->msg_as_Refuse()->code(), expected);
|
|
EXPECT_FALSE(session.Accepted());
|
|
}
|
|
session.Close();
|
|
}
|
|
}
|
|
|
|
TEST(SessionHandshakeTest, WireMajorMismatchReturnsNamedRefuseWithoutAborting) {
|
|
CheckHandshake(99, WireFingerprint(), BuildFingerprint(), ::MobileGL::Wire::DialMode::No,
|
|
::MobileGL::Wire::RefuseCode::ProtocolVersion);
|
|
}
|
|
TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting) {
|
|
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
|
|
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
|
|
}
|
|
TEST(SessionHandshakeTest, ConnectAcceptsDifferentBuildWithIdenticalWire) {
|
|
ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "0");
|
|
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit",
|
|
::MobileGL::Wire::DialMode::Connect, ::MobileGL::Wire::RefuseCode::None);
|
|
}
|
|
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`.
|
|
//
|
|
// "Clamped" in the name is the plan's word; what Accept does is IGNORE the ask - it never reads
|
|
// the four counts for sizing - and the session is server-sized. Since the F fix round that is
|
|
// said out loud: an ask above the granted terms logs one MGLOG_W naming both sides (rule I: a
|
|
// silent difference between what a client configured and what it got is the kind this phase
|
|
// removes), and this case requires the line. RED before that fix: the line is absent.
|
|
TEST(SessionHandshakeTest, AHelloAskingFor64GiBIsClampedToTheServersTermsAndAllocatesNothingOfIt) {
|
|
using namespace ::MobileGL::Wire;
|
|
constexpr Uint64 kAsk = 64ull << 30;
|
|
std::unique_ptr<Transport::InProcessTransport> 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<Uint8> 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 std::string before = ReadLog();
|
|
ASSERT_EQ(session.Accept(*server, &first), MOBILEGL_OK);
|
|
const std::string delta = ReadLog().substr(before.size());
|
|
EXPECT_TRUE(Contains(delta, "MG_Remote server: Hello asked for windows the server does not grant"))
|
|
<< "the 64 GiB ask was ignored silently; an operator comparing the client's configured "
|
|
"windows with the server's would see no line. Log delta:\n" << delta;
|
|
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",
|
|
::MobileGL::Wire::DialMode::Connect, ::MobileGL::Wire::RefuseCode::BuildFingerprint);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The two null-union guards, driven THROUGH the handshakes (ID-46 finding 7)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// A frame whose tag says Hello and whose Hello is NULL, sent on a real InProcessTransport pair to
|
|
// a real ServerSession::Accept - a session of this case's own, not the process singleton. Accept
|
|
// must answer MOBILEGL_ERR_PROTOCOL_MISMATCH with its guard's own line, and must not have
|
|
// dereferenced the member: nothing Fatal in the log, nothing accepted. RED ONCE by deleting
|
|
// `envelope->msg_as_Hello() == nullptr` from the guard in ServerSession.cpp: the tag check
|
|
// passes, `hello` is nullptr, and `hello->buildFingerprint()` reads address 0 - the case dies
|
|
// instead of returning. That perturbation was run.
|
|
TEST(SessionHandshakeTest, ANullUnionHelloIsRefusedByAcceptRatherThanDereferenced) {
|
|
std::unique_ptr<Transport::InProcessTransport> client;
|
|
std::unique_ptr<Transport::InProcessTransport> server;
|
|
Transport::InProcessTransport::CreatePair(client, server);
|
|
ASSERT_NE(client, nullptr);
|
|
ASSERT_NE(server, nullptr);
|
|
|
|
const std::vector<Uint8> frame = BuildNullUnionFrame(::MobileGL::Wire::CtrlMsg::Hello);
|
|
ASSERT_EQ(client->SendFrame(MobileGLByteSpan{frame.data(), frame.size()}), MOBILEGL_OK);
|
|
|
|
Server::ServerSession session;
|
|
const std::string before = ReadLog();
|
|
EXPECT_EQ(session.Accept(*server), MOBILEGL_ERR_PROTOCOL_MISMATCH)
|
|
<< "a null-union Hello was not refused by the handshake";
|
|
EXPECT_FALSE(session.Accepted());
|
|
const std::string delta = ReadLog().substr(before.size());
|
|
EXPECT_TRUE(Contains(delta, "MG_Remote server: the first control frame is not a verifiable Hello"))
|
|
<< "Accept refused, but not with the guard's own line. Log delta:\n"
|
|
<< delta;
|
|
EXPECT_FALSE(Contains(delta, "Fatal{")) << "the refusal became a Fatal. Log delta:\n" << delta;
|
|
session.Close();
|
|
}
|
|
|
|
// The Welcome guard. ClientSession::Start builds its transport pair itself, so nothing could put
|
|
// a frame on the server->client direction ahead of the server's Welcome - which is why
|
|
// StartOverTransportPair, Start's second half, is public (ClientSession.h). The frame is queued
|
|
// there BEFORE Start sends Hello: the server's Accept then runs for real (segments, Welcome,
|
|
// resolver), its genuine Welcome queues behind the null-union one, the client reads the
|
|
// null-union one first and must refuse it with the guard's own line, and Stop()'s not-started
|
|
// path must have closed the server the handshake had accepted. RED ONCE by deleting
|
|
// `envelope->msg_as_Welcome() == nullptr` from the guard in ClientSession.cpp: `welcome` is then
|
|
// nullptr and `welcome->buildFingerprint()` reads address 0 - the case dies. That perturbation
|
|
// was run.
|
|
TEST(SessionHandshakeTest, ANullUnionWelcomeIsRefusedByStartRatherThanDereferenced) {
|
|
std::unique_ptr<Transport::InProcessTransport> client;
|
|
std::unique_ptr<Transport::InProcessTransport> server;
|
|
Transport::InProcessTransport::CreatePair(client, server);
|
|
ASSERT_NE(client, nullptr);
|
|
ASSERT_NE(server, nullptr);
|
|
|
|
const std::vector<Uint8> frame = BuildNullUnionFrame(::MobileGL::Wire::CtrlMsg::Welcome);
|
|
ASSERT_EQ(server->SendFrame(MobileGLByteSpan{frame.data(), frame.size()}), MOBILEGL_OK);
|
|
|
|
Client::ClientSession& session = Client::ClientSessionInstance();
|
|
Server::ServerSession& serverSession = Server::ServerSessionInstance();
|
|
ASSERT_FALSE(session.Started());
|
|
ASSERT_FALSE(serverSession.Accepted());
|
|
|
|
const std::string before = ReadLog();
|
|
EXPECT_EQ(session.StartOverTransportPair(std::move(client), std::move(server)),
|
|
MOBILEGL_ERR_PROTOCOL_MISMATCH)
|
|
<< "a null-union Welcome was not refused by the handshake";
|
|
EXPECT_FALSE(session.Started());
|
|
EXPECT_EQ(Client::ClientSession::Active(), nullptr);
|
|
// Stop()'s not-started path closes the server FIRST (ClientSession.cpp); a server left
|
|
// m_accepted would refuse every later Start in this process.
|
|
EXPECT_FALSE(serverSession.Accepted());
|
|
EXPECT_EQ(Server::ServerSession::Active(), nullptr);
|
|
|
|
const std::string delta = ReadLog().substr(before.size());
|
|
EXPECT_TRUE(Contains(delta,
|
|
"MG_Remote client: the server's first control frame is not a verifiable "
|
|
"Welcome"))
|
|
<< "Start refused, but not with the guard's own line. Log delta:\n"
|
|
<< delta;
|
|
EXPECT_FALSE(Contains(delta, "Fatal{AbiMismatch"))
|
|
<< "the null-union Welcome reached the fingerprint compare. Log delta:\n"
|
|
<< delta;
|
|
// And the server's half of the handshake DID run - the Hello it received was this
|
|
// client's real one - so the case drove Start past the point a stub would stop at.
|
|
EXPECT_TRUE(Contains(delta, "MG_Remote server: accepted with NO backend"))
|
|
<< "ServerSession::Accept never ran, so the Welcome guard was not reached the way "
|
|
"Start reaches it. Log delta:\n"
|
|
<< delta;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
|
// write, and caches the FILE*. The name carries this process's pid, because
|
|
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
|
|
namespace fs = std::filesystem;
|
|
const fs::path path = fs::temp_directory_path() /
|
|
("mobilegl-sessionhandshake-test-" + std::to_string(ProcessId()) + ".log");
|
|
std::error_code ec;
|
|
fs::remove(path, ec);
|
|
g_logPath = path.string();
|
|
#if defined(_WIN32)
|
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
|
#else
|
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
|
#endif
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
const int rc = RUN_ALL_TESTS();
|
|
fs::remove(path, ec);
|
|
return rc;
|
|
}
|