[Fix] (Client): share teardown refusal and gate live wire producers

This commit is contained in:
2026-09-16 10:16:11 -04:00
parent 5682f429d0
commit 6d86f9d9be
9 changed files with 641 additions and 14 deletions
+1 -1
View File
@@ -1534,7 +1534,7 @@ namespace MobileGL::MG_Pipe {
// not consulted, and under spawn a silent no-op that leaks every one of those objects.
// The parameter type is the route's, which is byte-identical to the applier's
// (const MGPHandleOnly&, void return), so the fix is `&MGPipeRoute<Name>` at the five call
// sites; the grep gate scripts/../p5-c1 redcheck refuses any `&MGPipeApply` under MG_Impl/.
// sites; PipeCatalogue.FrontendNeverTakesAnApplierAddress checks MG_Impl in every unit lane.
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle,
void (*route)(const MGPHandleOnly&)) {
if (!MGPipeHandleIsPublished(kind, handle)) return false;
@@ -756,6 +756,8 @@ namespace MobileGL::MG_Remote::Client {
// down session, which is what the "teardown legitimately reaches here" comment
// promised would NOT happen. DECLINED is honest - "the verb did not happen" - and the
// acceptance rows already treat it as `false` / nullptr without aborting.
// ReadPixels intentionally refuses this with Fatal{ReadbackDeclined, "ReadPixels"}:
// unlike acceptance rows it cannot return successfully without complete pixels.
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusDeclined;
MGLOG_E("MG_Remote client: the barrier for %s (seq %llu) woke on a dead doorbell; the "
"server is gone and this verb did not happen (reported as DECLINED, not ERROR)",
+3
View File
@@ -38,6 +38,8 @@
#include <cstdlib>
#include <cstring>
#include "WireTables.h"
namespace MobileGL::MG_Remote::Client {
// The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is
@@ -117,6 +119,7 @@ namespace MobileGL::MG_Remote::Client {
// slot is the "split lane ran monolith and went green" shape that every gate in this
// phase exists to prevent (R-4).
ClientSession& RequireSession(const char* slot) {
RequireClientTablesInstalled(slot);
ClientSession* session = ClientSession::Active();
if (session == nullptr) {
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the remote emit table is "
+9 -9
View File
@@ -94,15 +94,7 @@ namespace MobileGL::MG_Remote::Client {
// emit into a ring being freed is a use-after-free. The monolith adapters are put
// back only as the LAST step of teardown, for the at-exit ~BufferObject deletes that
// legitimately reach a process with no session (see ReinstallMonolithAfterTeardown).
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - a routed call reached the "
"client wire tables while ClientSession::Stop was tearing the session "
"down. R-4/R-17: the applier is server-exclusive and the rings this emit "
"would use are being freed, so the call is refused by name rather than run "
"on the caller",
row);
std::abort();
}
RequireClientTablesInstalled(row);
ClientSession* session = ClientSession::Active();
if (session == nullptr) {
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are "
@@ -511,6 +503,14 @@ namespace MobileGL::MG_Remote::Client {
} // namespace
void RequireClientTablesInstalled(const char* row) {
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are "
"being torn down; refusing before session or ring access", row);
std::abort();
}
}
void InstallClientWireTables() {
using namespace MG_Pipe;
+4 -1
View File
@@ -30,7 +30,7 @@
// barrier holds (table 3), and installing from the apply thread would publish the table to
// the GL thread with no synchronisation at all.
// `Uninstall()` runs at the TOP of `Stop()`, before the rings go away, so the last thing any
// straggling GL-thread call reaches is the monolith arm rather than a dangling session.
// straggling GL-thread call reaches is a named refusal rather than a dangling session.
#pragma once
#include <Includes.h>
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Remote::Client {
// below, run only once the session's rings are freed. Idempotent.
void UninstallClientWireTables();
// Shared by routed rows and the five class-B emitters, before session/ring access.
void RequireClientTablesInstalled(const char* row);
// The LAST step of ClientSession::Stop: after the rings, segments and transport are gone,
// puts the monolith adapters back and clears the refusal flag, so the at-exit ~BufferObject
// deletes that reach a process with no session run the applier as they do under monolith.
@@ -13,6 +13,10 @@
#include <gtest/gtest.h>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <regex>
#include <sstream>
#include <iterator>
#include <limits>
#include <type_traits>
@@ -34,6 +38,30 @@
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
TEST(PipeCatalogue, FrontendNeverTakesAnApplierAddress) {
namespace fs = std::filesystem;
auto base = fs::current_path();
while (!fs::is_directory(base / "MobileGL/MG_Impl") && base != base.root_path())
base = base.parent_path();
const auto root = base / "MobileGL/MG_Impl";
ASSERT_TRUE(fs::is_directory(root)) << "frontend source unavailable: " << root;
const std::regex comments(R"(/\*[\s\S]*?\*/|//[^\n]*)");
const std::regex address(R"(&\s*MGPipeApply[A-Za-z_0-9]*)");
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file()) continue;
const auto ext = entry.path().extension();
if (ext != ".cpp" && ext != ".h" && ext != ".inc") continue;
std::ifstream input(entry.path());
ASSERT_TRUE(input.good()) << entry.path();
std::ostringstream bytes;
bytes << input.rdbuf();
const auto code = std::regex_replace(bytes.str(), comments, "");
EXPECT_FALSE(std::regex_search(code, address))
<< "FrontendApplierAddress: " << entry.path()
<< " must take the route address; routing tables live outside MG_Impl";
}
}
namespace {
// Counting expansions of the catalogue. The Class parameter is a real enumerator, so a
// per-class count is a constant expression too.
@@ -0,0 +1,367 @@
// c1f: exercise installed client producers over a live inproc session.
// Peer substitutions supply adversarial INPUTS, never client state being asserted.
#if MGTEST_HAVE_FORK
namespace {
namespace Srv = MobileGL::MG_Remote::Server;
namespace Codec = MobileGL::MG_Remote::Wire;
// Test-only member access for replacing the peer at its existing interfaces. Explicit
// instantiations may name private members; no production test knob or ABI change is needed.
template<class Tag, typename Tag::Type member> struct PeerAccess {
friend typename Tag::Type PeerMember(Tag) { return member; }
};
struct DecoderTag {
using Type = Codec::PipeWireDecoder Srv::PipeApplier::*;
friend Type PeerMember(DecoderTag);
};
template struct PeerAccess<DecoderTag, &Srv::PipeApplier::m_decoder>;
struct RepliesTag {
using Type = Codec::ReplySink* Codec::PipeWireDecoder::*;
friend Type PeerMember(RepliesTag);
};
template struct PeerAccess<RepliesTag, &Codec::PipeWireDecoder::m_replies>;
struct BackendTag {
using Type = UniquePtr<MG_Backend::BackendObject> Srv::ServerLoop::*;
friend Type PeerMember(BackendTag);
};
template struct PeerAccess<BackendTag, &Srv::ServerLoop::m_backend>;
void StartControlSession() {
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
Srv::ServerSessionInstance().SetCapabilityBits(0);
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK)
::_exit(81);
}
struct ReadPeer : Codec::WireVerbSink {
Int32 status = 0;
Uint64 size = 48;
Uint64 observed = 0;
Bool OnReadPixels(const MGPReadbackInfo& info, Uint64 seq, Codec::ReplySink* replies) override {
observed = info.DstSize;
Uint8 pixels[48];
std::memset(pixels, 0x37, sizeof(pixels));
replies->PostReply(seq, status, pixels, size);
return true;
}
void Install() {
auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<ReadPeer*>(self));
return MOBILEGL_OK;
}, this);
if (rc != MOBILEGL_OK) ::_exit(82);
}
};
struct ErrorPeer : Codec::ReplySink {
Int32 status = 2;
void PostReply(Uint64 seq, Int32, const void*, Uint64) override {
// Keep the decoded request's sequence: this is an ERROR answer, not a wrong-slot test.
Srv::ServerSessionInstance().Replies().PostReply(seq, status, nullptr, 0);
}
void Install() {
auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.*PeerMember(RepliesTag{}) = static_cast<ErrorPeer*>(self);
return MOBILEGL_OK;
}, this);
if (rc != MOBILEGL_OK) ::_exit(83);
}
};
struct CapsPeer : MG_Backend::BackendObject {
MG_Backend::DynamicBackendParameters dynamic{};
MG_Backend::GlobalBackendFunctionsTable table{};
RendererInfo renderer{};
void Initialize() override {}
Bool InitCapabilities() override { return true; }
Bool InitWindowSurface() override { return true; }
Bool InitPbufferSurface(EGLint, EGLint) override { return true; }
const RendererInfo& GetRendererInfo() const override { return renderer; }
String GetBackendAPIVersionString() const override { return "4.6"; }
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override { return table; }
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { return dynamic; }
BackendType GetBackendType() const override { return BackendType::DirectGLES; }
};
void ExpectNamedAbort(const ChildResult& child, const char* diagnostic) {
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << '\n' << child.Log;
EXPECT_NE(child.Log.find(diagnostic), std::string::npos) << child.Log;
}
void ExpectChildSuccess(const ChildResult& child) {
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< DescribeStatus(child) << '\n' << child.Log;
}
}
#define C1F_TEARDOWN(Name, Call) \
TEST(RemoteClientControls, TeardownRefuses##Name##OnALiveSession) { \
const auto child = RunInChild([] { \
StartControlSession(); \
UninstallClientWireTables(); \
RemoteEmitTable().Call; \
}); \
ExpectNamedAbort(child, "Fatal{ClientTablesUninstalled, \"" #Name "\"}"); \
}
C1F_TEARDOWN(Clear, GL.Clear(0))
C1F_TEARDOWN(DrawArrays, GL.DrawArrays(0x0004, 0, 0))
C1F_TEARDOWN(ReadPixels, GL.ReadPixels(0, 0, 0, 0, 0x1908, 0x1401, nullptr))
C1F_TEARDOWN(BlitFramebuffer, GL.BlitFramebuffer(0,0,0,0,0,0,0,0,0,0x2600))
C1F_TEARDOWN(Present, Present())
#undef C1F_TEARDOWN
TEST(RemoteClientControls, ReadPixelsPutsTheTightExtentOnTheWire) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
if (peer.observed != 48 || pixels[47] != 0x37) ::_exit(84);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ShortOkReadPixelsReplyRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.size = 32;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReadbackReplyShort, \"ReadPixels 32 < 48\"}");
}
TEST(RemoteClientControls, ErrorReadPixelsReplyRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.status = 2;
peer.size = 0;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"ReadPixels\"}");
}
TEST(RemoteClientControls, ResourceRespecifyErrorIsNotADecline) {
const auto child = RunInChild([] {
StartControlSession();
ErrorPeer peer;
peer.Install();
MGPResourceDesc desc{};
(void)MGPipeRouteResourceRespecify(desc, nullptr);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"resource_respecify\"}");
}
TEST(RemoteClientControls, MapPersistentErrorIsNotADecline) {
const auto child = RunInChild([] {
StartControlSession();
ErrorPeer peer;
peer.Install();
MGPHandleOnly handle{};
(void)MGPipeRouteMapPersistent(handle, 64, nullptr);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"map_persistent\"}");
}
TEST(RemoteClientControls, RepeatedMakeCurrentAdoptsRepublishedCapsWithoutAPumpOrPresent) {
const auto child = RunInChild([] {
StartControlSession();
if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
auto backend = MakeUnique<CapsPeer>();
backend->dynamic.MaxComputeWorkGroupCount[0] = 113;
Srv::ServerSessionInstance().SetBackend(backend.get());
Srv::ServerLoopInstance().*PeerMember(BackendTag{}) = Move(backend);
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(85);
BackendObject_Remote remote;
auto dpy = reinterpret_cast<EGLDisplay>(1);
auto surface = reinterpret_cast<EGLSurface>(2);
auto ctx = reinterpret_cast<EGLContext>(3);
if (!remote.InitializeEGLDisplay(dpy, nullptr, nullptr) ||
!remote.CreateEGLPbufferSurface(surface, 4, 4) ||
!remote.MakeEGLCurrent(dpy, surface, surface, ctx)) ::_exit(86);
const auto generation = CapsMirrorInstance().Generation();
GLint before = 0;
remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &before);
if (before != 113) ::_exit(87);
if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
static_cast<CapsPeer*>(Srv::ServerLoopInstance().Backend())->dynamic.MaxComputeWorkGroupCount[0] = 227;
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(88);
if (!remote.MakeEGLCurrent(dpy, surface, surface, ctx)) ::_exit(89);
GLint after = 0;
remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &after);
if (CapsMirrorInstance().Generation() <= generation || after != 227) ::_exit(90);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, BounceReadPixelsShortReplyRefusesBeforeScatter) {
const auto child = RunInChild([] {
StartControlSession();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_State::pGLContext->SetPixelStoreParam(PixelStoreParam::PackRowLength, 8);
ReadPeer peer;
peer.size = 32;
peer.Install();
Uint8 pixels[128]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReadbackReplyShort, \"ReadPixels 32 < 48\"}");
}
TEST(RemoteClientControls, DeleteTexturesAdvancesTheWireOrdinal) {
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
ErrorPeer replies;
replies.status = 0; // The peer accepts the resource; the client must publish and delete it.
replies.Install();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources | kMGPipeSubsystemTextureResources |
kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 1, 1, 0, 0x1908, 0x1401, nullptr);
const auto before = ClientWireRecordsEmitted();
MG_Impl::GLImpl::DeleteTextures(1, &texture);
const auto after = ClientWireRecordsEmitted();
if (before == 0 || after <= before) ::_exit(91);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, BoundPackBufferOffsetReadRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(0x88EB, buffer);
MG_Impl::GLImpl::BufferData(0x88EB, 64, nullptr, 0x88E1);
RemoteEmitTable().GL.ReadPixels(0, 0, 1, 1, 0x1908, 0x1401, reinterpret_cast<void*>(16));
});
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ReadPixels+PACK_BUFFER\"}");
}
TEST(RemoteClientControls, ServerRoleRespecifyDoesNotEmitClientInitialBytes) {
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
const auto before = ClientWireRecordsEmitted();
if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MG_State::GLState::BufferObject buffer(701);
Uint8 data[64]{};
buffer.Respecify(sizeof(data), data);
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(92);
if (ClientWireRecordsEmitted() != before) ::_exit(93);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) {
const auto child = RunInChild([] {
StartControlSession();
MGPDynamicState record{};
record.ChunkMask = 0;
const auto before = ClientWireRecordsEmitted();
gMGPipeContext.SetDynamicState(&record, nullptr, 0);
if (ClientWireRecordsEmitted() != before + 1) ::_exit(94);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ServerRoleFlushDoesNotRunTheClientSubDataFollowup) {
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
const auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
static int subdata = 0, flushes = 0;
MGPipeResourceOps ops{};
ops.Create = [](MGPipeHandle, const MGPResourceDesc&) {};
ops.Respecify = [](MGPipeHandle, const MGPResourceDesc&, const void*) {};
ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) { ++subdata; };
ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) { ++flushes; };
ops.Destroy = [](MGPipeHandle) {};
MGPipeSetResourceOps(&ops);
{
MG_State::GLState::BufferObject buffer(702);
buffer.Resize(64);
subdata = flushes = 0;
MGPipeEmitResourceFlushRange(buffer, 16, 16, 0);
if (subdata != 0 || flushes != 1) ::_exit(95);
}
MGPipeSetResourceOps(nullptr);
return MOBILEGL_OK;
}, nullptr);
if (rc != MOBILEGL_OK) ::_exit(96);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// Uses the shipped EGL/GL frontend and real DirectGLES driver. Invoked explicitly by the
// package gate because a native driver is not a prerequisite of the unit lane.
int RunPackGpuControl(bool split) {
const auto child = RunInChild([=] {
::alarm(45);
setenv("EGL_PLATFORM", "surfaceless", 1);
setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
setenv("MOBILEGL_TRANSPORT", split ? "inproc" : "monolith", 1);
MobileGL::Initialize();
namespace E = MG_Impl::EGLImpl;
namespace G = MG_Impl::GLImpl;
const auto display = E::GetDisplay(EGL_DEFAULT_DISPLAY);
if (!E::Initialize(display, nullptr, nullptr)) ::_exit(101);
E::BindAPI(EGL_OPENGL_API);
EGLConfig config{};
EGLint count = 0;
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
if (!E::ChooseConfig(display, configAttribs, &config, 1, &count) || count < 1) ::_exit(102);
const auto context = E::CreateContext(display, config, EGL_NO_CONTEXT, nullptr);
const EGLint surfaceAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE};
const auto surface = E::CreatePbufferSurface(display, config, surfaceAttribs);
if (!E::MakeCurrent(display, surface, surface, context)) ::_exit(103);
G::ClearColor(0, 1, 0, 1);
G::Clear(GL_COLOR_BUFFER_BIT);
GLuint pbo = 0;
G::GenBuffers(1, &pbo);
G::BindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
G::BufferData(GL_PIXEL_PACK_BUFFER, 64, nullptr, GL_STREAM_READ);
G::ReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast<void*>(16));
if (split) ::_exit(104); // Must have refused by name, never returned or dereferenced 16.
Uint8 pixels[4]{};
G::GetBufferSubData(GL_PIXEL_PACK_BUFFER, 16, 4, pixels);
if (pixels[0] != 0 || pixels[1] != 255 || pixels[2] != 0 || pixels[3] != 255) ::_exit(105);
std::printf("C1F_PACK_MONOLITH bytes at offset 16: %u %u %u %u\n", pixels[0], pixels[1], pixels[2], pixels[3]);
std::fflush(stdout);
});
const bool ok = split
? DiedOfAbort(child) && child.Log.find("Fatal{UnmigratedVerb, \"ReadPixels+PACK_BUFFER\"}") != std::string::npos
: WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0;
std::printf("C1F_PACK_%s: %s (%s)\n%s", split ? "INPROC" : "MONOLITH",
ok ? "PASS" : "FAIL", DescribeStatus(child).c_str(), child.Log.c_str());
return ok ? 0 : 1;
}
#endif
+68 -3
View File
@@ -6,9 +6,9 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5 package c1's suite: the 71-slot emit table, the caps mirror and R-8's liveness gates. No
// session, no transport and no thread - s1's SessionTest owns those and w1's PipeWireCodecTest
// owns the bytes.
// P5 package c1's suite: the 71-slot emit table, the caps mirror and R-8's liveness gates.
// The helper cases below are supplemented by RemoteClientControls.inc: installed producers
// over a live session, with adversarial peer replies and a repeated-make-current control.
//
// IT LINKS gtest RATHER THAN gtest_main AND CARRIES ITS OWN main(), for PipeWireCodecTest's and
// PipeInputsTest's reason: the Fatal arms report through MGLOG_F + std::abort, and MGLOG_F
@@ -38,6 +38,19 @@
#include <MG_Pipe/PipeRoute.h>
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/WireTables.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Client/BackendObject_Remote.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_Pipe/PipeApply.h>
#include <Init.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#if !defined(_WIN32)
#include <csignal>
@@ -713,6 +726,47 @@ TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMove
<< "InstallClientWireTables did not record the client-wire arm";
const SizeT movedScreen = CountDifferingCells(gMGPipeScreen, MGPipeMonolithScreen());
#define C1F_MOVED(Table, Row) EXPECT_NE(gMGPipe##Table.Row, MGPipeMonolith##Table().Row) << #Table "." #Row
C1F_MOVED(Screen, ResourceCreate);
C1F_MOVED(Screen, ResourceDestroy);
C1F_MOVED(Screen, UnmapPersistent);
C1F_MOVED(Context, CreateRenderState);
C1F_MOVED(Context, BindRenderState);
C1F_MOVED(Context, DeleteRenderState);
C1F_MOVED(Context, CreateVertexElements);
C1F_MOVED(Context, BindVertexElements);
C1F_MOVED(Context, DeleteVertexElements);
C1F_MOVED(Context, CreateSamplerState);
C1F_MOVED(Context, DeleteSamplerState);
C1F_MOVED(Context, CreateSamplerView);
C1F_MOVED(Context, DeleteSamplerView);
C1F_MOVED(Context, BindShaderState);
C1F_MOVED(Context, DeleteShaderState);
C1F_MOVED(Context, SetDrawProgram);
C1F_MOVED(Context, SetDispatchProgram);
C1F_MOVED(Context, SetDynamicState);
C1F_MOVED(Context, SetFramebufferState);
C1F_MOVED(Context, SetVertexBuffers);
C1F_MOVED(Context, SetIndexBuffer);
C1F_MOVED(Context, SetSamplerViews);
C1F_MOVED(Context, BindSamplerStates);
C1F_MOVED(Context, SetShaderImages);
C1F_MOVED(Context, SetGlobalConstants);
C1F_MOVED(Context, SetVertexAttribDefaults);
C1F_MOVED(Context, SetPixelPackState);
C1F_MOVED(Context, SetPatchState);
C1F_MOVED(Context, SetResidualValueState);
C1F_MOVED(Context, SetTextureParams);
C1F_MOVED(Context, ResourceSubData);
C1F_MOVED(Context, BufferSubDataResident);
C1F_MOVED(Context, ResourceReadback);
#undef C1F_MOVED
#define C1F_ESCAPE(Row) EXPECT_NE(gMGPipeRouteEscapes.Row, MGPipeMonolithEscapes().Row) << #Row
C1F_ESCAPE(ResourceRespecify);
C1F_ESCAPE(ResourceFlushRange);
C1F_ESCAPE(MapPersistent);
C1F_ESCAPE(CreateShaderState);
#undef C1F_ESCAPE
const SizeT movedContext = CountDifferingCells(gMGPipeContext, MGPipeMonolithContext());
EXPECT_EQ(movedScreen + movedContext, 33u)
<< "exactly the 33 generated routed rows must differ from the monolith adapters; "
@@ -792,6 +846,8 @@ TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, 0, tight)) << "an OK reply of zero bytes is not the extent";
}
#include "RemoteClientControls.inc"
int main(int argc, char** argv) {
namespace fs = std::filesystem;
const fs::path path =
@@ -803,6 +859,15 @@ int main(int argc, char** argv) {
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
#if MGTEST_HAVE_FORK
// Explicit GPU control, kept out of the CPU-only unit label. No silent skip is allowed.
if (argc == 2 && std::string(argv[1]).starts_with("--c1f-pack-gpu=")) {
const bool split = std::string(argv[1]) == "--c1f-pack-gpu=inproc";
const int rc = RunPackGpuControl(split);
fs::remove(path, ec);
return rc;
}
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""c1f mutation gates. Run from the worktree, links ON, after building split.
Every mutation must compile, fail its named test, then restore/build/pass that test.
No git operation, server source edit, or unrelated failure is accepted as red.
"""
import argparse
import pathlib
import re
import subprocess
import sys
ROOT = pathlib.Path(__file__).resolve().parents[3]
EMIT = 'MobileGL/MG_Remote/Client/EmitTables.cpp'
WIRE = 'MobileGL/MG_Remote/Client/WireTables.cpp'
FILL = 'MobileGL/MG_Impl/Pipe/PipeFill.cpp'
BACKEND = 'MobileGL/MG_Remote/Client/BackendObject_Remote.cpp'
SUITE = 'RemoteClientControls.'
def replace(old, new, count=1):
def change(src):
if src.count(old) < count:
raise RuntimeError('mutation anchor absent: ' + old)
return src.replace(old, new, count)
return change
def escape_error(src):
result, count = re.subn(r' if \(status == 2\) \{.*?\n \}', '', src,
flags=re.S)
if count != 2:
raise RuntimeError(f'expected two escape ERROR blocks, got {count}')
return result
def helper_size(src):
return replace('ReadbackBytesPerPixel(format, type);\n }',
'ReadbackBytesPerPixel(format, type) + 16;\n }')(src)
def cases():
both = 'RequireReadbackReplyComplete(status, replySize, tight);'
site = '&MGPipeRouteResourceDestroy);'
return [
('B1-source', FILL, replace(site, '&MGPipeApplyResourceDestroy);'),
['PipeCatalogue.FrontendNeverTakesAnApplierAddress'], 'PipeCatalogueTest'),
('B1-wire', FILL, replace(site, '&MGPipeApplyResourceDestroy);'),
[SUITE+'DeleteTexturesAdvancesTheWireOrdinal'], 'RemoteClientTest'),
('N2-five-verbs', EMIT, replace(' RequireClientTablesInstalled(slot);', ''),
[SUITE+'TeardownRefuses'+n+'OnALiveSession' for n in
['Clear','DrawArrays','ReadPixels','BlitFramebuffer','Present']], 'RemoteClientTest'),
('M2-production-deleted', EMIT, replace(both, '', 2),
[SUITE+'ShortOkReadPixelsReplyRefusesByName', SUITE+'BounceReadPixelsShortReplyRefusesBeforeScatter',
SUITE+'ErrorReadPixelsReplyRefusesByName'], 'RemoteClientTest'),
('M2-fast-deleted', EMIT, replace(both, ''),
[SUITE+'ShortOkReadPixelsReplyRefusesByName'], 'RemoteClientTest'),
('M2-bounce-deleted', EMIT, replace(' '+both+'\n Scatter', ' Scatter'),
[SUITE+'BounceReadPixelsShortReplyRefusesBeforeScatter'], 'RemoteClientTest'),
('M3-production-plus16', EMIT, replace('info.DstSize = tight;', 'info.DstSize = tight + 16;'),
[SUITE+'ReadPixelsPutsTheTightExtentOnTheWire'], 'RemoteClientTest'),
('M3-shared-size', EMIT, helper_size,
[SUITE+'ReadPixelsPutsTheTightExtentOnTheWire'], 'RemoteClientTest'),
('M4-escape-errors-deleted', WIRE, escape_error,
[SUITE+'ResourceRespecifyErrorIsNotADecline', SUITE+'MapPersistentErrorIsNotADecline'], 'RemoteClientTest'),
('M5-server-role-guards-deleted', FILL,
replace('!MG_Remote::Client::RunsAsTheServerRole() && ', '', 2),
[SUITE+'ServerRoleRespecifyDoesNotEmitClientInitialBytes'], 'RemoteClientTest'),
('M5-server-flush-guard-deleted', FILL,
replace('!MG_Remote::Client::RunsAsTheServerRole() && size != 0', 'size != 0'),
[SUITE+'ServerRoleFlushDoesNotRunTheClientSubDataFollowup'], 'RemoteClientTest'),
('M8-real-pack-binding', EMIT,
replace('if (MG_State::pGLContext != nullptr &&', 'if (false && MG_State::pGLContext != nullptr &&'),
[SUITE+'BoundPackBufferOffsetReadRefusesByName'], 'RemoteClientTest'),
('codex12-repeat-skip', BACKEND,
replace('if (draw != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {', 'if (false) {'),
[SUITE+'RepeatedMakeCurrentAdoptsRepublishedCapsWithoutAPumpOrPresent'], 'RemoteClientTest'),
('BlobMissing-optional-to-required', WIRE,
replace('record.Blob = StageOptional(session, blobBytes, blobByteCount);',
'record.Blob = StageRequired(session, "SetDynamicState", blobBytes, blobByteCount);'),
[SUITE+'HeaderOnlyDynamicStateCrossesWithoutBlobMissing'], 'RemoteClientTest'),
('N6-compensating-wrong-row', WIRE,
replace('gMGPipeScreen.ResourceDestroy = &Wire_ResourceDestroy;',
'gMGPipeScreen.FenceCreate = &Wire_ResourceDestroy;'),
['PipeRouting.TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMoved'], 'RemoteClientTest'),
]
def command(args):
p = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=180)
return p.returncode, p.stdout
def verdict(build_rc, run_rc, output, names):
if build_rc:
return 'HARNESS-FAIL'
if run_rc and all('[ FAILED ] '+name+' (' in output for name in names):
return 'RED'
return 'WRONG-VERDICT'
def self_test():
names = ['Own.Test']
probes = [(1, 1, '[ FAILED ] Own.Test (0 ms)', 'HARNESS-FAIL'),
(0, -11, 'UNRELATED_FAILURE\nSegmentation fault', 'WRONG-VERDICT'),
(0, 0, '[ PASSED ] 1 test.', 'WRONG-VERDICT'),
(0, 0, '[ FAILED ] Own.Test (0 ms)', 'WRONG-VERDICT'),
(0, 1, '[ FAILED ] Own.Test (0 ms)', 'RED')]
for b, rc, out, expected in probes:
assert verdict(b, rc, out, names) == expected
print('RUNNER_META 5/5: unrelated crash, all green, build failure, false status, own failure')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--self-test', action='store_true')
parser.add_argument('--case', default='')
args = parser.parse_args()
self_test()
if args.self_test:
return 0
failures = []
selected = [case for case in cases() if args.case in case[0]]
if not selected:
print('HARNESS-FAIL: no matching mutation')
return 1
for label, file, mutate, names, target in selected:
path = ROOT / file
original = path.read_bytes()
binary = ROOT / 'build-split/MobileGL/MG_Test' / ('Pipe' if target == 'PipeCatalogueTest' else 'Wire') / target
build = ['cmake', '--build', 'build-split', '-j', '24', '--target', target]
run = [str(binary), '--gtest_filter='+':'.join(names)]
print('\n=== '+label+' ===', flush=True)
try:
brc, out = command(build)
if brc:
raise RuntimeError('baseline build failed\n'+out)
rc, out = command(run)
if rc or any('[ OK ] '+name+' (' not in out for name in names):
raise RuntimeError('baseline not green\n'+out)
path.write_text(mutate(original.decode()))
brc, out = command(build)
rc = 0
if not brc:
rc, out = command(run)
result = verdict(brc, rc, out, names)
print(out, end='')
print(f'{label}: {result} build={brc} run={rc}', flush=True)
if result != 'RED':
failures.append(label)
except Exception as exc:
print('HARNESS-FAIL', exc, flush=True)
failures.append(label)
finally:
path.write_bytes(original)
brc, out = command(build)
rc, out = command(run) if brc == 0 else (brc, out)
if rc or any('[ OK ] '+name+' (' not in out for name in names):
print('RESTORE-FAIL\n'+out, flush=True)
failures.append(label+' restore')
else:
print('RESTORED GREEN: '+', '.join(names), flush=True)
print('FAILED_CONTROLS='+repr(failures), flush=True)
return bool(failures)
if __name__ == '__main__':
sys.exit(main())