[Feature] (P13 W5 S7): MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 keeps the tracker's SIGSEGV handler out, and the mprotect arm builds only on Linux and Android.

This commit is contained in:
2026-10-06 13:17:25 -04:00
parent dd7fbbd7d1
commit 1961029043
4 changed files with 77 additions and 3 deletions
+7
View File
@@ -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
+5
View File
@@ -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 "
@@ -20,14 +20,26 @@
#include <xxhash.h>
#include <Config.h>
#include <atomic>
#include <csignal>
#include <cstdlib>
#include <cstring>
// 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 <csignal>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <ucontext.h>
#include <unistd.h>
#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<Bool> 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<uintptr_t> g_declinedFaultNearestEnd{0};
std::atomic<Bool> 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) {
@@ -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