[MG_Pipe, MG_Remote, MG_Test] (P7): bound create-render-state CSO slots

This commit is contained in:
2026-09-23 13:43:43 -04:00
parent 1997fd8878
commit 54a8e3fadb
5 changed files with 185 additions and 3 deletions
+15 -3
View File
@@ -1667,10 +1667,22 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
"create_render_state named the reserved slot 0");
if (desc.Cso.Slot >= g_applier.RenderStateCsos.size()) {
g_applier.RenderStateCsos.resize(desc.Cso.Slot + 1);
MGPipeRenderStateCsoRecord* recordAt =
RecordAt(g_applier.RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots);
if (recordAt == nullptr) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"CreateRenderState.Cso.Slot\"} - "
"create_render_state {slot=%u, gen=%u} is outside the record table bound (%u)",
desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxRenderStateCsoSlots);
#else
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
" create_render_state {slot=%u, gen=%u} is outside the record table bound (%u)",
desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxRenderStateCsoSlots);
#endif
return;
}
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[desc.Cso.Slot];
MGPipeRenderStateCsoRecord& record = *recordAt;
// NEITHER BRANCH MAY LEAVE ITS BAD CASE TO MOBILEGL_ASSERT. The slot the client is
// naming may be a RECYCLED one whose record still holds the previous occupant's 396
+3
View File
@@ -162,6 +162,9 @@ namespace MobileGL::MG_Pipe {
// bounded by kMGPipeMaxResourceSlots and each grows only to its own dense high-water mark.
inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20;
inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16;
// create_render_state records retain one 396-byte pipeline half each. Keep their
// slot-indexed table bounded independently so a peer cannot request a huge resize.
inline constexpr Uint32 kMGPipeMaxRenderStateCsoSlots = 1u << 16;
// P4a's three, and the argument is written out for each because the records differ in
// size. None is ever allocated by being named: the tables grow to the client's own dense
// high-water mark and no further, so the bound costs nothing until a record is corrupt.
+4
View File
@@ -54,6 +54,10 @@ namespace MobileGL::MG_Pipe {
UnmigratedVerb,
// A call that reached an arm this build did not compile, or ran on the wrong role.
RoleViolation,
#if MOBILEGL_BUILD_DISAGGREGATED
// A malformed peer record rejected by the server-side applier.
ProtocolCorruption,
#endif
};
// What the layer above installs. `line` is the FULLY FORMATTED message, so the hook forwards
+5
View File
@@ -53,6 +53,11 @@ namespace MobileGL::MG_Remote {
case MG_Pipe::MGPipeFatalFamily::RoleViolation:
mapped = MGFatalFamily::RoleViolation;
break;
#if MOBILEGL_BUILD_DISAGGREGATED
case MG_Pipe::MGPipeFatalFamily::ProtocolCorruption:
mapped = MGFatalFamily::ProtocolCorruption;
break;
#endif
}
SessionFail(mapped, "%s", line);
}
+158
View File
@@ -39,10 +39,20 @@
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <MG_Remote/Transport/ILink.h>
#include <MG_Remote/Transport/Ring.h>
#include <MG_Remote/Wire/PipeWireCodec.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Debug/Log.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <csignal>
#include <cstdlib>
#include <memory>
@@ -68,6 +78,56 @@ namespace {
return std::string("/tmp/mgl-sm-") + label + "-" + std::to_string(::getpid()) + ".sock";
}
struct ScopedEnvironment {
std::string name;
std::string oldValue;
bool hadOldValue = false;
ScopedEnvironment(const char* key, const std::string& value) : name(key) {
if (const char* prior = std::getenv(key)) {
oldValue = prior;
hadOldValue = true;
}
::setenv(key, value.c_str(), 1);
}
~ScopedEnvironment() {
if (hadOldValue) ::setenv(name.c_str(), oldValue.c_str(), 1);
else ::unsetenv(name.c_str());
}
};
// A peer can mutate the command-ring bytes AFTER the production encoder has
// accepted and written them. The sequence, framing size, and submitted
// watermark stay valid; only the opcode becomes unknown to the server.
// This is the seam the later D11/PH-2 controls will use for payload fields.
template <class Payload, class Mutator>
bool EncodeThenMutateHeaderAndPublish(Client::ClientSession& session, MobileGL::MG_Pipe::MGPWireOp op,
const Payload& payload, Mutator&& mutate) {
auto* link = session.DataLink();
if (link == nullptr || !link->Attached()) return false;
Wire::WireRecordLayout layout{};
if (!Wire::MGPipeWireRecordLayout(op, &payload, layout) ||
layout.PayloadBytes != sizeof(payload)) return false;
auto& encoder = session.Encoder();
const std::uint64_t seq = encoder.EncodeRecord(op, &payload, sizeof(payload));
if (seq == Wire::kInvalidSeq) return false;
auto& commands = link->CommandsOut();
const auto* arena = link->RecordArena();
const std::uint64_t head = commands.LocalHead();
if (arena == nullptr || arena->Base == nullptr || arena->Capacity == 0 ||
head < layout.TotalBytes) return false;
const std::uint64_t start = head - layout.TotalBytes;
auto* bytes = static_cast<std::uint8_t*>(arena->Base) + (start & arena->Mask);
auto* header = reinterpret_cast<Transport::RingRecordHeader*>(bytes);
if (header->kind != static_cast<std::uint16_t>(op) || header->size != layout.TotalBytes)
return false;
mutate(*header, bytes + sizeof(Transport::RingRecordHeader));
// Use the real session publisher after mutation so both the ring head
// and the producer-owned submittedSeq stay monotone and teardown knows
// the record's sequence. Only the record bytes bypass encoder checks.
session.Producer().PublishAndNotify(seq);
return link->Flush() == MOBILEGL_OK;
}
std::string ServerImage() {
if (const char* explicitPath = std::getenv("MOBILEGL_TEST_SERVER_PATH")) {
return explicitPath;
@@ -80,6 +140,22 @@ namespace {
std::unique_ptr<Transport::SocketTransport> client;
};
// Keep the negative-path case leak-free even when an ASSERT returns early.
struct ScopedSessionCleanup {
Session& session;
~ScopedSessionCleanup() {
Client::ClientSessionInstance().Stop();
if (session.client) session.client->Shutdown();
if (session.server.pid > 0) {
int ignored = -1;
if (Server::ReapServer(session.server, 50, &ignored) != MOBILEGL_OK) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &ignored);
}
}
}
};
// The two halves a real deployment does separately: start a process, then
// connect to it. The bounded connect retry is the CLIENT's, not the
// launcher's - a launcher that waited for the server to be ready would be
@@ -129,6 +205,88 @@ namespace {
} // namespace
TEST(ServerSpawnTest, ARawPeerMutatesARecordAfterTheClientEncoderAcceptedIt) {
const std::string endpoint = Endpoint("raw-peer");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("raw-peer", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
MobileGL::MG_Pipe::MGPMemoryBarrier barrier{};
barrier.Bits = 0x2000u;
barrier.ByRegion = 0;
auto& client = Client::ClientSessionInstance();
auto* link = client.DataLink();
ASSERT_NE(link, nullptr);
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::MemoryBarrier, barrier,
[](Transport::RingRecordHeader& header, std::uint8_t*) { header.kind = 0xFFFFu; }))
<< "the test peer failed to mutate and publish the accepted record bytes";
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server did not reject the mutated record promptly";
EXPECT_EQ(exitCode, -SIGABRT) << "the malformed opcode did not reach the server decoder";
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
EXPECT_NE(log.find("protocol corruption applying"), std::string::npos)
<< "the server process exited without the protocol corruption diagnostic";
EXPECT_NE(log.find("unknown opcode"), std::string::npos)
<< "the malformed opcode was not identified in the server log";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerOutOfRangeRenderStateCsoSlotGetsNamedProtocolCorruption) {
const std::string endpoint = Endpoint("bad-renderstate-slot");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-renderstate-slot", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPRenderStateDesc desc{};
desc.Cso = {7u, 1u};
desc.ChunkMask = MobileGL::MG_Pipe::MGPipeRenderStateChunkDetail::kAllPipelineHalfBits;
const auto chunkBytes = MobileGL::MG_Pipe::MGPipePipelineChunkBlobBytes(desc.ChunkMask);
std::vector<std::uint8_t> chunks(chunkBytes, 0x5A);
desc.Blob = client.Encoder().StageBytes(chunks.data(), chunks.size());
const MobileGL::Uint32 encoderAcceptedSlot = desc.Cso.Slot;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::CreateRenderState, desc,
[](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireDesc = reinterpret_cast<MobileGL::MG_Pipe::MGPRenderStateDesc*>(payloadBytes);
wireDesc->Cso.Slot = MobileGL::MG_Pipe::kMGPipeMaxRenderStateCsoSlots;
})) << "the test peer failed to mutate and publish the encoder-accepted record bytes";
EXPECT_LT(encoderAcceptedSlot, MobileGL::MG_Pipe::kMGPipeMaxRenderStateCsoSlots);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server did not reject the mutated record promptly";
EXPECT_EQ(exitCode, -SIGABRT) << "the malformed CSO slot did not reach the server applier";
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
EXPECT_NE(log.find("Fatal{ProtocolCorruption, \"CreateRenderState.Cso.Slot\"}"),
std::string::npos)
<< "the server process exited without the named protocol corruption diagnostic";
client.Stop();
}
TEST(ServerSpawnTest, StartsAServerProcessAndHandshakesAcrossIt) {
const int before = Server::CountOwnChildren();
ASSERT_GE(before, 0) << "/proc unreadable; the process-tree gate cannot run here";