From 6d86f9d9be9024c5de185738340ca8d4f0053ab6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 10:16:11 -0400 Subject: [PATCH] [Fix] (Client): share teardown refusal and gate live wire producers --- MobileGL/MG_Impl/Pipe/PipeFill.cpp | 2 +- MobileGL/MG_Remote/Client/ClientSession.cpp | 2 + MobileGL/MG_Remote/Client/EmitTables.cpp | 3 + MobileGL/MG_Remote/Client/WireTables.cpp | 18 +- MobileGL/MG_Remote/Client/WireTables.h | 5 +- MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp | 28 ++ .../MG_Test/Wire/RemoteClientControls.inc | 367 ++++++++++++++++++ MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 71 +++- MobileGL/MG_Test/Wire/c1f_redcheck.py | 159 ++++++++ 9 files changed, 641 insertions(+), 14 deletions(-) create mode 100644 MobileGL/MG_Test/Wire/RemoteClientControls.inc create mode 100644 MobileGL/MG_Test/Wire/c1f_redcheck.py diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 7a58544c..d874a0f5 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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` 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; diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 24b918aa..248dd882 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -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)", diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 39fba630..cb9c9edf 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -38,6 +38,8 @@ #include #include +#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 " diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 256e2f67..bc4459d7 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -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; diff --git a/MobileGL/MG_Remote/Client/WireTables.h b/MobileGL/MG_Remote/Client/WireTables.h index 90345dbe..4413e355 100644 --- a/MobileGL/MG_Remote/Client/WireTables.h +++ b/MobileGL/MG_Remote/Client/WireTables.h @@ -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 @@ -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. diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index b3e6c0c7..aa353029 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -13,6 +13,10 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -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. diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc new file mode 100644 index 00000000..bbd103cf --- /dev/null +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -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 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; +struct RepliesTag { + using Type = Codec::ReplySink* Codec::PipeWireDecoder::*; + friend Type PeerMember(RepliesTag); +}; +template struct PeerAccess; +struct BackendTag { + using Type = UniquePtr Srv::ServerLoop::*; + friend Type PeerMember(BackendTag); +}; +template struct PeerAccess; + +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(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(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(); + 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(1); + auto surface = reinterpret_cast(2); + auto ctx = reinterpret_cast(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(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::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(); + 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(); + 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(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(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 diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index fd2c7b33..acef2348 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -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 #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #if !defined(_WIN32) #include @@ -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(); diff --git a/MobileGL/MG_Test/Wire/c1f_redcheck.py b/MobileGL/MG_Test/Wire/c1f_redcheck.py new file mode 100644 index 00000000..602bb93b --- /dev/null +++ b/MobileGL/MG_Test/Wire/c1f_redcheck.py @@ -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())