diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 90513647..b873f191 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -489,6 +489,13 @@ namespace MobileGL::MG_Config { // blocks whose xxHash64 changed since the last push, instead of the whole mapped // range every verb. 0 restores the whole-range push (A/B control). Uint32 PersistentHashSuppress = 1; + // MOBILEGL_PIPE_PERSISTENT_MPROTECT (P13 W5): 1 (default) = the persistent-map push finds + // written pages through mprotect and a chained SIGSEGV handler where the platform has + // one (Linux, Android); 0 = no handler is ever installed and every persistent write map + // takes the hash scan. The switch exists for the hosts whose own SIGSEGV use the + // chained handler has not met yet - FCL is a JVM, whose implicit null checks fault on + // purpose - so a bring-up can rule the handler out without a rebuild. + Uint32 PersistentMprotect = 1; }; extern RecordArmTable RecordArm; #endif diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 20444cd9..accab628 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -722,6 +722,11 @@ namespace MobileGL::MG_ConfigLoader { // Whole-range push is the 0 arm; with it on (default) only blocks whose // xxHash64 changed since the last push are shipped. ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1); + ipc.PersistentMprotect = QueryEnvUint32("MOBILEGL_PIPE_PERSISTENT_MPROTECT", 1, 0, 1); + if (ipc.PersistentMprotect == 0) { + MGLOG_W("Config: MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 - no SIGSEGV handler is installed " + "for persistent write maps; every one takes the hash scan"); + } if (ipc.WireDeferredMb == 0) { MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the " "server never forces a sync for orphaned wire buffer stores, so a long frame " diff --git a/MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp b/MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp index 306fa03f..62a276b3 100644 --- a/MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp +++ b/MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp @@ -20,14 +20,26 @@ #include +#include + #include -#include #include #include + +// P13 W5: THE MPROTECT ARM IS LINUX AND ANDROID ONLY. It reads the faulting PC out of a Linux +// ucontext and the fault kind out of si_code; elsewhere (Windows, Apple - a protection fault +// arrives as SIGBUS there) no handler is installed and every persistent write map takes the +// hash scan, which is always correct, only slower. +#if defined(__linux__) || defined(__ANDROID__) +#define MGL_TRACKER_MPROTECT_ARM 1 +#include #include #include #include #include +#else +#define MGL_TRACKER_MPROTECT_ARM 0 +#endif namespace MobileGL::MG_Record { @@ -85,7 +97,14 @@ namespace MobileGL::MG_Record { constexpr uintptr_t kSlotSettingUp = 1; TrackedWriteMap g_trackedMaps[kMaxTrackedMaps]; +#if MGL_TRACKER_MPROTECT_ARM struct sigaction g_prevSegvAction {}; +#else + // Never called: without the arm nothing is ever protected (g_segvInstalled stays false). + constexpr int PROT_READ = 1; + constexpr int PROT_WRITE = 2; + inline int mprotect(void*, SizeT, int) { return -1; } +#endif std::atomic g_segvInstalled{false}; // Bumped by the handler for every write fault it answers, and by TrackWriteMap // for every registration (a fresh slot has EVERY page marked, which is a fault of @@ -186,6 +205,7 @@ namespace MobileGL::MG_Record { std::atomic g_declinedFaultNearestEnd{0}; std::atomic g_declinedFaultAnnounced{false}; +#if MGL_TRACKER_MPROTECT_ARM // Async-signal-safe hex, because snprintf is not on the safe list and this runs in a // handler that is about to hand the process to debuggerd. SizeT AppendHex(char* out, SizeT at, uintptr_t value) { @@ -418,6 +438,9 @@ namespace MobileGL::MG_Record { } void InstallWriteFaultHandler() { + // The operator's switch first (Config.h RecordArmTable::PersistentMprotect): off, the + // handler is never installed, so nothing of this process's SIGSEGV is ever touched. + if (MG_Config::RecordArm.PersistentMprotect == 0) return; if (!KernelPageIsTheTrackerPage()) return; Bool expected = false; if (!g_segvInstalled.compare_exchange_strong(expected, true)) return; @@ -432,6 +455,15 @@ namespace MobileGL::MG_Record { g_segvInstalled.store(false); } } +#else + void InstallWriteFaultHandler() {} +#endif + + // The arm takes a NEW map only while the handler is in and the switch allows it; a map it + // already holds keeps its slot until it is untracked. + Bool MprotectArmLive() { + return MG_Config::RecordArm.PersistentMprotect != 0 && g_segvInstalled.load(); + } void UntrackWriteMap(Uint64 lifetimeId); @@ -442,7 +474,7 @@ namespace MobileGL::MG_Record { SizeT rangeBegin, SizeT rangeEnd, SizeT shadowExtent) { if (shadow == nullptr || rangeEnd <= rangeBegin) return false; InstallWriteFaultHandler(); - if (!g_segvInstalled.load()) return false; + if (!MprotectArmLive()) return false; // WHICH WAY TO ALIGN IS A QUESTION OF OWNERSHIP, and the reason is a // process-killer: a protected page that holds FOREIGN bytes can be written by // a thread that does not run our handler - driver and runtime worker threads @@ -840,7 +872,7 @@ namespace MobileGL::MG_Record { Bool PersistentMapTracker::MprotectArmAvailableForTest() { InstallWriteFaultHandler(); - return g_segvInstalled.load(); + return MprotectArmLive(); } const TrackedWriteMap* PersistentMapTracker::TrackedSlotForTest(const BufferObject& buffer) { diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index a6e101ac..a9af5a60 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -705,6 +705,36 @@ TEST_F(SplitBufferSet, AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmen store[extent - 1] = 1; } +// P13 W5: THE MPROTECT ARM'S KILL SWITCH REACHES THE TRACKER. MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 +// (Config.h RecordArmTable::PersistentMprotect) is for hosts whose own SIGSEGV use the chained +// handler has not met - FCL is a JVM. Off, a new map is refused by the arm (the hash scan serves +// it) even after the handler went in; back on, the same registration is tracked again. +TEST_F(SplitBufferSet, TheMprotectSwitchSendsEveryNewMapToTheHashScan) { + if (!PersistentMapTracker::MprotectArmAvailableForTest()) { + GTEST_SKIP() << "no mprotect arm on this host (handler not installable, or the kernel " + "page is not the tracker's 4 KB); the hash arm serves every map here"; + } + MG_State::GLState::MapAlignedData store(2u * 4096u); + const SizeT extent = MG_State::GLState::ShadowAllocationBytesFor(store.capacity()); + constexpr Uint64 kId = ~0ull - 11u; + + const Uint32 saved = MG_Config::RecordArm.PersistentMprotect; + MG_Config::RecordArm.PersistentMprotect = 0; + EXPECT_FALSE(PersistentMapTracker::MprotectArmAvailableForTest()); + EXPECT_EQ(PersistentMapTracker::TrackForTest(kId, store.data(), 0, 2u * 4096u, extent), nullptr) + << "the switch is off and the arm still took a new map"; + MG_Config::RecordArm.PersistentMprotect = saved; + + // The positive control: the same registration, switch back on, is the arm's. + ASSERT_TRUE(PersistentMapTracker::MprotectArmAvailableForTest()); + const auto* slot = PersistentMapTracker::TrackForTest(kId, store.data(), 0, 2u * 4096u, extent); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(slot->pageCount, 2u); + PersistentMapTracker::UntrackForTest(kId); + store[0] = 1; + store[extent - 1] = 1; +} + // THE DRAIN THE EPOCH SKIP RESTS ON. A fault marks a page and moves the epoch; the next // draw's walk consumes the epoch. If that walk pushed only the buffers the draw bound, a // faulted map the draw did NOT bind would be left marked, writable and unable to move the