[MG_Remote] (P6/sm): two INDEPENDENT processes, joined by a name, sharing memory

The cross-process session is up. A server process is started on its own with an
endpoint and nothing else, it listens; a client started separately connects to
that name; Hello crosses, Welcome comes back with four SegmentRefs, six
descriptors cross by SCM_RIGHTS, and the client maps memory another process
created. ClientSession::Active() is non-null at the end of it, which is the flag
that arms the thirty-seven wire emitters.

INDEPENDENT, NOT FORKED - and that is a correction to this package's first
shape. The version committed an hour ago forked the server and handed it fds
3..6. It worked, and it was less code, but inheritance can only ever produce a
server that is a CHILD OF ITS CLIENT, which is the one arrangement the end state
cannot use: there the server is a standing application and the client is
somewhere else entirely, possibly on another kernel. CONTRACT-P6 §3.1 called
this Dial == Connect and filed it as a later shape. It is the primary one, and
fork/execve survives only as a way to START a process - every descriptor it used
to carry is gone.

What that costs and buys, concretely: the rendezvous becomes a NAME, so
SocketTransport grows Listen/AcceptPair/ConnectTo, with a bounded connect retry
that is the CLIENT's (a launcher that waited for the server to be ready would be
coupling the two again by the back door). The bells stop being inherited fds and
are created by the server and handed over with the segments - slots 4 and 5 of
six SCM_RIGHTS offers. And the client connects TWICE to the same path: an fd
offer is a sendmsg whose ancillary data rides with specific bytes, so it may not
share a socket with the framed control stream, or the frame reassembler and the
descriptor receiver race for the same bytes.

The pieces behind it:
  - SessionSegments::AdoptFromDescriptors, which is the same Adopt+Map loop
    AttachInProcess has run on every inproc session since P5. That was the point
    of writing the inproc attach as dup+Adopt+Map rather than as an alias: only
    the origin of the descriptor is new.
  - ServerSession::SetExternalDoorbells, the narrow fix for a6 row A7-1 - the
    §3.9 accessors abort for every role but InProcess, so a spawn apply thread
    died before popping its first record. lk deletes them; until then an injected
    pair is honest about where the bells come from.
  - MG_Backend::InitServerRoleForSpawn: steps 1 and 2 of the split bring-up
    extracted so the server process runs the SAME code the inproc server role
    runs. The capability bits are a statement about the server's own backend and
    a second copy would diverge the first time a backend registers a slot.
  - ClientSession::FinishStartup: everything below the attach - producer, reply
    pool, event ring, segment table, encoder, first CapsSnapshot, emitter tables -
    shared verbatim between inproc and spawn. Its one parameter is not a
    transport question: under inproc the client starts the apply thread because
    there is no server-side main; under spawn there is one, and it started the
    thread before it sent the first descriptor.

G1 CAUGHT A REAL LEAK on the way: InitServerRoleForSpawn was guarded on the
inside, so the pull build still DEFINED the symbol - 2 added, 1 removed, .text
+16 bytes. The whole function and its declaration are now inside the guard.
"It returns false there" is not the same as "it is not there", and the gate is
what said so.

Gates: ServerSpawnTest 4/4; unit suite 2344 with the same two pre-existing
environmental failures; G1 pull 0 added / 0 removed, .text 10822147 unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 10:36:14 -04:00
co-authored by Claude Opus 5
parent e156ea5f7a
commit 207c31f6db
14 changed files with 1014 additions and 301 deletions
+33 -1
View File
@@ -30,6 +30,8 @@
#include <MG_Remote/Client/BackendObject_Remote.h>
#endif
#include "ServerRole.h"
namespace MobileGL::MG_Backend {
void LogBackendInfo() {
if (!pActiveBackendObject) {
@@ -148,7 +150,13 @@ namespace MobileGL::MG_Backend {
// The single hook (ARCHITECTURE.md:29). Returns false when the split could not be
// brought up, and the caller then REFUSES TO CONTINUE rather than falling back to the
// switch below - a fallback here is "the split lane ran monolith and went green".
Bool InitSplitRoles() {
// Steps 1 and 2 of the split bring-up: the server role's private backend
// object and the two CallMask halves. EXTRACTED so the spawn server
// process runs the SAME code the inproc server role runs - a second copy
// is how the two roles end up disagreeing about a capability bit that
// only one of them ever computes. ServerMain calls this and then does
// its own Accept; InitSplitRoles calls it and then starts the client.
Bool InitServerRoleCommon() {
using namespace MobileGL::MG_Remote;
// 1. the SERVER role's private backend object, on the app thread, with no GL and no
@@ -210,6 +218,17 @@ namespace MobileGL::MG_Backend {
capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType, runAheadReady);
session.SetCapabilityBits(capBits);
session.SetBackend(loop.Backend());
return true;
}
// The single hook (ARCHITECTURE.md:29) for the INPROC shape: both roles in
// this process. The spawn shape runs InitServerRoleCommon in the child and
// the client half here.
Bool InitSplitRoles() {
using namespace MobileGL::MG_Remote;
if (!InitServerRoleCommon()) {
return false;
}
// 3. the handshake, the four segments, and - at its end - the apply thread.
const MobileGLResult started =
@@ -256,6 +275,19 @@ namespace MobileGL::MG_Backend {
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// The spawn server's entry into the SAME bring-up the inproc server role
// runs (ServerRole.h). A thin forwarder on purpose: the body stays in the
// anonymous namespace beside InitSplitRoles so the two cannot drift.
//
// THE WHOLE FUNCTION IS INSIDE THE GUARD, not just its body. A version with
// the guard inside still DEFINES the symbol in a pull build, and G1 caught
// it: 2 symbols added, 1 removed, .text +16 bytes. The pull build's identity
// is byte-for-byte, and "it returns false there" is not the same as "it is
// not there".
Bool InitServerRoleForSpawn() { return InitServerRoleCommon(); }
#endif
void Init() {
MGLOG_D("Initializing MobileGL Backend...");
+45
View File
@@ -0,0 +1,45 @@
// MobileGL - MobileGL/MG_Backend/ServerRole.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The server role's bring-up, as one entry point two shapes share.
//
// Steps 1 and 2 of Init.cpp's split bring-up - the server's private
// BackendObject, and the two CallMask halves computed from THAT backend's own
// function table. Under `inproc` InitSplitRoles calls it and then starts the
// client in the same process; under `spawn` the child's ServerMain calls it and
// then accepts a connection instead.
//
// ONE ENTRY POINT, not two, because the capability bits are a statement about
// the server's backend and only the server can compute them. A second copy in
// ServerMain would be a second answer to "does this backend own the XFB
// capture", diverging silently the first time a backend registers a slot.
#pragma once
#include <Includes.h>
// DECLARED ONLY IN A DISAGGREGATED BUILD. G1 requires the pull build's symbol
// set and .text to be byte-identical, and a definition guarded only on the
// inside still emits a symbol - measured, not assumed.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend {
// Creates the server's BackendObject and publishes the consumed-subsystem
// mask and the capability bits onto ServerSessionInstance(). Does NOT create
// an EGL context: that happens later, on the apply thread, when the client's
// first eglMakeCurrent crosses as a blocking control request.
//
// Returns false when the backend could not be created, and the caller must
// then REFUSE TO CONTINUE rather than fall back - a server process that came
// up without a backend would accept a session it can never apply.
Bool InitServerRoleForSpawn();
} // namespace MobileGL::MG_Backend
#endif // MOBILEGL_BUILD_DISAGGREGATED
+195 -7
View File
@@ -15,6 +15,7 @@
#include "../Protocol/generated/protocol_generated.h"
#include "../Server/ServerLoop.h"
#include "../Server/ServerSession.h"
#include "../Transport/FdPassing.h"
#include "../Transport/InProcessTransport.h"
#include "WireTables.h"
@@ -603,6 +604,191 @@ namespace MobileGL::MG_Remote::Client {
return attached;
}
return FinishStartup(&m_clientTransport->PeerDoorbell(),
&m_clientTransport->SelfDoorbell(),
/*startApplyThreadHere=*/true);
}
MobileGLResult ClientSession::StartOverSocket(
std::unique_ptr<Transport::SocketTransport> transport) {
if (m_started) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (transport == nullptr) {
MGLOG_E("MG_Remote client: StartOverSocket needs a connected transport");
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_socketTransport = Move(transport);
m_transport = m_socketTransport.get();
// ---- Hello. The same bytes as inproc; the fingerprint is still compared
// for equality, because under Dial == Fork the child IS the same binary and
// comparing it catches a stale MOBILEGL_IPC_SERVER_PATH (CONTRACT-P6 4.1).
const Uint64 fingerprint = CapsAbiFingerprint();
{
::flatbuffers::FlatBufferBuilder builder(512);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto hello = ::MobileGL::Wire::CreateHello(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
0u, 0u, 0, fingerprint);
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
const MobileGLResult sent = m_transport->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
if (sent != MOBILEGL_OK) {
Stop();
return sent;
}
}
// ---- Welcome, and the four announced sizes. THERE IS NO LOCAL SERVER TO
// ASK: the inproc path cross-checks Welcome against ServerSessionInstance()'s
// own mapping, which a6 showed is vacuous against a real peer. Here the wire
// values are the only values, which is what that check has to become on
// every lane (CONTRACT-P6 4.4).
Uint64 announced[4] = {0, 0, 0, 0};
const Uint32 replySlotCount = Transport::kDefaultReplySlotCount;
{
std::vector<Uint8> frame;
const MobileGLResult received =
ReceiveEnvelope(*m_transport, frame, kHandshakeTimeoutMs);
if (received != MOBILEGL_OK) {
MGLOG_E("MG_Remote client: no Welcome from the spawned server within %u ms (rc=%d)",
kHandshakeTimeoutMs, static_cast<int>(received));
Stop();
return received;
}
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Welcome ||
envelope->msg_as_Welcome() == nullptr) {
MGLOG_E("MG_Remote client: the spawned server first frame is not a verifiable "
"Welcome");
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
const ::MobileGL::Wire::Welcome* welcome = envelope->msg_as_Welcome();
const char* theirStamp = welcome->buildFingerprint() == nullptr
? nullptr
: welcome->buildFingerprint()->c_str();
if (welcome->abiFingerprint() != fingerprint) {
FatalAbiMismatch("struct shapes", fingerprint, welcome->abiFingerprint(),
theirStamp);
}
const ::MobileGL::Wire::SegmentRef* refs[4] = {welcome->cmdRing(), welcome->stageRing(),
welcome->replyPool(),
welcome->eventRing()};
for (int index = 0; index < 4; ++index) {
if (refs[index] == nullptr || refs[index]->sizeBytes() == 0) {
MGLOG_E("MG_Remote client: Welcome SegmentRef %d is missing or zero-sized",
index);
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
announced[index] = refs[index]->sizeBytes();
}
}
// ---- the four descriptors. THIS is the step inproc does not have, and the
// reason SCM_RIGHTS is in this design at all: the segments are anonymous -
// ASharedMemory_create has no filesystem name and memfd / shm_open+unlink
// are not openable either - so the descriptor IS the only key.
//
// The slot travels in the sideband rather than being inferred from arrival
// order, so a reordered or dropped offer is a NAMED mismatch instead of two
// segments quietly swapped.
// Six offers: four segments, then the two bells (slots 4 and 5).
int fds[6] = {-1, -1, -1, -1, -1, -1};
{
struct Sideband {
Uint32 slot;
Uint64 bytes;
};
const auto closeAll = [&fds]() {
for (int& open : fds) {
if (open >= 0) {
::close(open);
open = -1;
}
}
};
for (int received = 0; received < 6; ++received) {
Sideband sideband{};
Uint64 sidebandSize = 0;
int fd = -1;
std::vector<Uint8> scratch(Transport::FdPassing::kMaxSidebandBytes);
MobileGLMutableByteSpan span{scratch.data(), scratch.size()};
const MobileGLResult got =
m_transport->ReceiveFd(&fd, span, &sidebandSize, kHandshakeTimeoutMs);
if (got != MOBILEGL_OK || fd < 0) {
MGLOG_E("MG_Remote client: segment descriptor %d did not arrive (rc=%d)",
received, static_cast<int>(got));
closeAll();
Stop();
return got == MOBILEGL_OK ? MOBILEGL_ERR_PROTOCOL_MISMATCH : got;
}
if (sidebandSize != sizeof(Sideband)) {
MGLOG_E("MG_Remote client: segment offer %d carried %llu sideband bytes",
received, static_cast<unsigned long long>(sidebandSize));
::close(fd);
closeAll();
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
std::memcpy(&sideband, scratch.data(), sizeof(Sideband));
const bool isSegment = sideband.slot < 4;
if (sideband.slot >= 6 || fds[sideband.slot] >= 0 ||
(isSegment && sideband.bytes != announced[sideband.slot])) {
MGLOG_E("MG_Remote client: segment offer names slot %u with %llu bytes; "
"Welcome announced a different size or the slot is already filled",
sideband.slot, static_cast<unsigned long long>(sideband.bytes));
::close(fd);
closeAll();
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
fds[sideband.slot] = fd;
}
// ---- adopt and map. The same Adopt + Map every inproc run has
// exercised since P5; only the origin of the descriptor is new.
const MobileGLResult adopted = m_shm.AdoptFromDescriptors(
fds, announced, replySlotCount, Transport::MemoryRole::Client);
if (adopted != MOBILEGL_OK) {
closeAll();
Stop();
return adopted;
}
// The four segment descriptors now belong to m_shm. The two bells
// belong to the doorbells, which own and close them.
//
// WHICH BELL IS WHICH IS THE SESSION'S KNOWLEDGE, exactly as under
// inproc: slot 4 is the pair end whose WRITE wakes the server, slot 5
// is the one this side POLLS.
m_socketPeerBell = MakeUnique<Transport::SocketDoorbell>(fds[4], 1, true);
m_socketSelfBell = MakeUnique<Transport::SocketDoorbell>(fds[5], 1, true);
}
// The apply thread IS ALREADY RUNNING, in the other process: ServerMain
// started it before it sent the first descriptor. Starting one here would
// put a second applier on a ring that already has one.
return FinishStartup(m_socketPeerBell.get(), m_socketSelfBell.get(), false);
}
// The half of startup that is IDENTICAL for inproc and spawn: the ring
// producer, the reply pool, the event ring, the segment table, the encoder,
// the first CapsSnapshot and the emitter tables. Extracted by `sm` so the
// spawn path cannot drift from the shape P5 spent five packages settling -
// everything here is below the line where "which transport" stopped
// mattering.
//
// `startApplyThreadHere` is the one policy difference, and it is not a
// transport question: under inproc the client is what starts the server
// role, because there is no server-side main to do it. Under spawn there
// IS one, and it started the apply thread before it ever sent Welcome.
MobileGLResult ClientSession::FinishStartup(Transport::Doorbell* peerBell,
Transport::Doorbell* selfBell,
bool startApplyThreadHere) {
Transport::RingControl* control = m_shm.CmdControl();
m_cmd = Transport::RingProducer(control, m_shm.CmdRingBase(), m_shm.CmdRingCapacity(),
Transport::RingCursorSet::Cmd);
@@ -619,8 +805,7 @@ namespace MobileGL::MG_Remote::Client {
}
// PeerDoorbell() is the bell the SERVER parks on and this side rings; SelfDoorbell() is
// this side's own. Which is which is the session's knowledge, not the transport's.
m_producer.Attach(control, &m_cmd, &m_clientTransport->PeerDoorbell(),
&m_clientTransport->SelfDoorbell(), SpinUsFromConfig());
m_producer.Attach(control, &m_cmd, peerBell, selfBell, SpinUsFromConfig());
m_replies = Transport::ReplySlotPool(m_shm.ReplyBase(), m_shm.ReplyBytes(),
m_shm.ReplySlotCount());
@@ -629,7 +814,7 @@ namespace MobileGL::MG_Remote::Client {
m_shm.EventSegmentBase());
// P5e (ra, §2.6): the drain's other half. A server parked on a full SEG_EVENT is woken
// by the drain that emptied it, not by the next thing this client happens to publish.
m_events.SetServerDoorbell(&m_clientTransport->PeerDoorbell());
m_events.SetServerDoorbell(peerBell);
if (!m_replies.Valid() || !m_events.Valid()) {
Stop();
return MOBILEGL_ERR_INVALID_ARGUMENT;
@@ -701,10 +886,13 @@ namespace MobileGL::MG_Remote::Client {
// thread mgl-srv-apply, applies MOBILEGL_IPC_SERVER_AFFINITY and logs the RESOLVED
// mask. Under `inproc` the client is what starts the server role, which is why this
// call is here rather than in some server-side main.
const MobileGLResult running = Server::ServerLoopInstance().Start(server);
if (running != MOBILEGL_OK) {
Stop();
return running;
if (startApplyThreadHere) {
Server::ServerSession& server = Server::ServerSessionInstance();
const MobileGLResult running = Server::ServerLoopInstance().Start(server);
if (running != MOBILEGL_OK) {
Stop();
return running;
}
}
// ---- 9. AND ONLY NOW THE THIRTY-SEVEN WIRE EMITTERS (R-17). This is the line that
+32
View File
@@ -45,6 +45,7 @@
#include "../Transport/Ring.h"
#include "../Transport/RoleMemory.h"
#include "../Transport/SessionRings.h"
#include "../Transport/SocketTransport.h"
#include "../Wire/PipeWireCodec.h"
#include "CapsMirror.h"
@@ -79,6 +80,29 @@ namespace MobileGL::MG_Remote::Client {
// come back as MOBILEGL_ERR_PROTOCOL_MISMATCH with the guard's own line. Not a second
// way to start a session: MG_Backend::Init() calls Start(), and nothing else may call
// this with a pair it did not just create.
// P6 `sm`: the spawn client's half. `transport` is this process's end of
// the control socket to an already-running server process; the two bell
// fds are this side's ends of the two bell pairs (ServerSpawn.h).
//
// It is the SAME handshake StartOverTransportPair runs, with three
// differences and no others: the server half happens in another process
// so there is no local Accept to call, the four segments arrive as
// descriptors over SCM_RIGHTS instead of being dup'ed from a local
// owner, and the bells are sockets rather than condvars. Everything
// below the attach - producer, reply pool, event ring, segment table,
// encoder - is byte for byte the inproc path, which is the property
// that makes "P6 is a transport swap" true of THIS function even though
// it is not true of the phase.
MobileGLResult FinishStartup(Transport::Doorbell* peerBell,
Transport::Doorbell* selfBell,
bool startApplyThreadHere);
// `transport` is this process's end of a connection to an ALREADY
// RUNNING server process. The two bells arrive over it, with the four
// segments, as SCM_RIGHTS offers - nothing here is inherited, because
// the two processes were started independently.
MobileGLResult StartOverSocket(std::unique_ptr<Transport::SocketTransport> transport);
MobileGLResult StartOverTransportPair(std::unique_ptr<Transport::InProcessTransport> clientEnd,
std::unique_ptr<Transport::InProcessTransport> serverEnd);
@@ -352,6 +376,14 @@ namespace MobileGL::MG_Remote::Client {
Uint64 m_presentsSent = 0;
Uint64 m_presentCreditWaits = 0;
// sm: the spawn client owns ONE end and two bells. Held here rather
// than in the transport because which bell is "mine" is the session's
// knowledge, not the transport's - the same ruling as inproc's
// PeerDoorbell/SelfDoorbell split.
std::unique_ptr<Transport::SocketTransport> m_socketTransport;
std::unique_ptr<Transport::Doorbell> m_socketSelfBell;
std::unique_ptr<Transport::Doorbell> m_socketPeerBell;
std::unique_ptr<Transport::InProcessTransport> m_clientTransport;
std::unique_ptr<Transport::InProcessTransport> m_serverTransport;
Transport::SessionSegments m_shm;
+209 -68
View File
@@ -9,61 +9,67 @@
// The server process's entry point. Package `sm` (CONTRACT-P6.md §3).
//
// ARCHITECTURE.md:488-496 has described this symbol for three phases and a6
// found it existed nowhere in the tree: the only MobileGLServer target was the
// P0 spike stub, which links no MobileGL library at all. This is the real one.
// found it existed nowhere in the tree. This is the real one.
//
// WHAT IT IS IN THIS PACKAGE. The process, the inherited descriptors, the
// control transport and the exit discipline - and nothing above them yet. It
// accepts a control connection, answers, and exits on EOF. The session, the
// applier and the four shared segments arrive with the packages that own them;
// wiring them from here before `lk` has a seam to wire them THROUGH is how the
// call sites end up written twice.
// THE TWO PROCESSES ARE INDEPENDENT. This process is started by whoever starts
// it - a shell, a test, an Android Service - and it LISTENS. The client is
// started separately and CONNECTS. Nothing is inherited: no fds, no memory, no
// parent.
//
// WHY IT STILL EARNS ITS PLACE NOW: it is the first time two MobileGL
// processes exist and talk, and every later package needs exactly this scaffold
// to have been proven - that the image links, that execve keeps the fds, that
// the child dies when the parent goes away, and that nothing is left behind.
// That is a deliberate change from this package's first shape, which forked the
// server and handed it fds 3..6. Inheritance works and is less code, but it can
// only ever produce a server that is a CHILD OF ITS CLIENT - the one arrangement
// the end state cannot use, because there the server is a standing application
// and the client is somewhere else entirely, possibly on another kernel.
// CONTRACT-P6 §3.1 calls this Dial == Connect and had it as a later shape. It is
// the primary one.
//
// WHAT THIS PACKAGE OWNS: the process, the rendezvous, the backend, the session,
// the descriptor hand-off and the exit discipline. NOT what a control frame
// means - that is `cp` - and not the data-plane seam, which is `lk`.
//
// _exit, NOT exit (CONTRACT-P6 §12.4). It decides whether g_processTeardown is
// ever set, and therefore whether seven twin destructors take their early-out
// or call into a driver whose context is already gone. The log is flushed
// explicitly first, because _exit does not run atexit handlers and a server
// that died without saying why is the diagnostic this project refuses.
// ever set, and therefore whether seven twin destructors take their early-out or
// call into a driver whose context is already gone. The log is flushed first,
// because _exit runs no atexit handler and a server that died without saying why
// is the diagnostic this project refuses.
#include "../Transport/Doorbell.h" // kWaitForever
#include "../Transport/Doorbell.h" // kWaitForever, SocketDoorbell
#include "../Transport/FdPassing.h"
#include "../Transport/SocketTransport.h"
#include "../Transport/WireLog.h"
#include "ServerLoop.h"
#include "ServerSession.h"
#include <MG_Backend/ServerRole.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
# include <sys/socket.h>
# include <unistd.h>
#endif
namespace {
constexpr int kInheritedStreamFd = 3;
constexpr int kInheritedAuxFd = 4;
// The child must not be able to become a client. CONTRACT-P6 §3.1: this is
// The server must not be able to become a client. CONTRACT-P6 §3.1: this is
// catch (a), STRUCTURAL and separate from role selection, because forcing
// MG_Config::Transport to Monolith - what ARCHITECTURE.md:488 says - would
// flip 148 MG_Backend lines to the frontend arm in the one process with no
// frontend. Catch (b) is the parent's envp scrub, and S3 falsifies it by
// leaving the scrub out; the diagnostic below is what names WHICH fired.
// frontend. Catch (b) is the launcher's envp scrub, and the two report
// through DIFFERENT exit codes so they are distinguishable: two safeties
// that cannot be told apart are one safety.
bool DialingIsForbidden() {
const char* dial = std::getenv("MOBILEGL_IPC_DIAL");
return dial != nullptr && std::strcmp(dial, "no") == 0;
}
bool LooksLikeAClientEnvironment() {
// Anything the parent should have scrubbed. If one of these survived,
// catch (b) failed and we say so by name rather than quietly relying on
// catch (a) - two safeties that cannot be told apart are one safety.
static constexpr const char* kNames[] = {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH",
"MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"};
for (const char* name : kNames) {
@@ -74,63 +80,199 @@ namespace {
return false;
}
// The rendezvous. A NAME, because independent processes have no other way to
// find each other. No default: a default rendezvous is one that two unrelated
// runs can collide on.
std::string Endpoint(int argc, char** argv) {
if (argc > 1 && argv[1] != nullptr && argv[1][0] != '\0') {
return argv[1];
}
if (const char* fromEnv = std::getenv("MOBILEGL_IPC_ENDPOINT")) {
return fromEnv;
}
return std::string();
}
} // namespace
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) {
(void)argc;
(void)argv;
using namespace MobileGL::MG_Remote;
using namespace MobileGL::MG_Remote::Transport;
const int selfPid = static_cast<int>(::getpid());
// The arm-proof line (CONTRACT-P6 §9.5). ID-124 proved a lane can run
// monolith and pass every other gate, so each entry has to leave its own
// evidence: the pid it ran as, and the transport it ran on.
// monolith and pass every other gate, so each entry leaves its own evidence.
WireLogError("MG_Remote server: pid=%d transport=spawn role=server", selfPid);
if (!DialingIsForbidden()) {
// stderr DIRECTLY, not just WireLogError: Defines.h builds the logger
// with the console sink OFF (WireLog.h says so), so an error reaches
// exactly one place - a file whose path the parent may not know. A
// server that refused to start and could not say why is the diagnostic
// this project refuses, and ARCHITECTURE.md §15.2 already warns that the
// app process's stderr is /dev/null on Android, which is why the parent
// ALSO gets this as a non-zero exit code it can name.
std::fprintf(stderr, "MG_Remote server: pid=%d refusing to start - MOBILEGL_IPC_DIAL=no "
"was not set; anti-recursion catch (a) is missing\n", selfPid);
WireLogError("MG_Remote server: pid=%d refusing to start - MOBILEGL_IPC_DIAL=no was not "
"set. The anti-recursion catch (a) is missing, which means this process was "
"not started by SpawnServer and could try to spawn a server of its own.",
std::fprintf(stderr,
"MG_Remote server: pid=%d refusing to start - MOBILEGL_IPC_DIAL=no was not "
"set; anti-recursion catch (a) is missing\n",
selfPid);
std::fflush(nullptr);
::_exit(64);
}
if (LooksLikeAClientEnvironment()) {
// Catch (a) already stopped us; this names catch (b)'s failure so the
// two are distinguishable, which is the whole point of having two.
std::fprintf(stderr, "MG_Remote server: pid=%d the envp scrub FAILED - a client knob "
"survived into the child; catch (a) held, catch (b) did not\n",
selfPid);
WireLogError("MG_Remote server: pid=%d the envp scrub FAILED - a MOBILEGL_TRANSPORT or "
"MOBILEGL_IPC_* survived into the child. Catch (a) held; catch (b) did not.",
std::fprintf(stderr,
"MG_Remote server: pid=%d the envp scrub FAILED - a client knob survived "
"into this process; catch (a) held, catch (b) did not\n",
selfPid);
std::fflush(nullptr);
::_exit(65);
}
SocketTransport control(kInheritedStreamFd, kInheritedAuxFd, TransportRole::Server);
const std::string endpoint = Endpoint(argc, argv);
if (endpoint.empty()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d no endpoint - pass one as argv[1] or set "
"MOBILEGL_IPC_ENDPOINT\n",
selfPid);
std::fflush(nullptr);
::_exit(71);
}
// The loop this package owns: read a control frame, answer it, and exit on
// EOF. `cp` replaces the body with the real surface-op pump; the SHAPE -
// one reader, EOF ends the process, no timeout decides death - is what has
// to be right now, because CONTRACT-P6 §5.4 forbids a timeout from ever
// standing in for the death fact.
// ---- 1. listen, BEFORE the backend. The client's bounded connect retry is
// what tolerates the backend taking a while; a client that connected and then
// waited for a Welcome a failed bring-up will never send would have to learn
// about death from a timeout, which §5.4 forbids.
int listenFd = -1;
if (SocketTransport::Listen(endpoint, &listenFd) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d could not listen on \"%s\"\n", selfPid,
endpoint.c_str());
std::fflush(nullptr);
::_exit(72);
}
WireLogError("MG_Remote server: pid=%d listening on \"%s\"", selfPid, endpoint.c_str());
// ---- 2. the backend, through the SAME entry point the inproc server role
// uses (MG_Backend/ServerRole.h). No EGL yet: the context is created on the
// apply thread when the client's first eglMakeCurrent crosses.
if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d could not create a backend; refusing to accept a "
"session it could never apply\n",
selfPid);
::close(listenFd);
::unlink(endpoint.c_str());
std::fflush(nullptr);
::_exit(66);
}
// ---- 3. accept the client's TWO connections: control, then aux.
std::unique_ptr<SocketTransport> control;
if (SocketTransport::AcceptPair(listenFd, 30000, control) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d no client within 30 s\n", selfPid);
::close(listenFd);
::unlink(endpoint.c_str());
std::fflush(nullptr);
::_exit(73);
}
// One session per process (CONTRACT-P6 §12.3), so the door closes behind the
// one client that came through it. A second would collide on four process
// globals and on DirectGLES's single per-process EGL tuple.
::close(listenFd);
::unlink(endpoint.c_str());
// ---- 4. the two bells, created HERE and handed over. NOT inherited: there
// is no parent to inherit from. One socketpair per DIRECTION, because
// SocketDoorbell is "write a byte to wake the peer, poll to be woken" and a
// single pair would have each side reading the byte it wrote to the other.
int serverBell[2] = {-1, -1};
int clientBell[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 ||
::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) {
std::fprintf(stderr, "MG_Remote server: pid=%d bell socketpair failed\n", selfPid);
std::fflush(nullptr);
::_exit(74);
}
SocketDoorbell selfBell(serverBell[0], /*code=*/1, /*ownsFd=*/true);
SocketDoorbell peerBell(clientBell[0], /*code=*/1, /*ownsFd=*/true);
Server::ServerSession& session = Server::ServerSessionInstance();
session.SetExternalDoorbells(&selfBell, &peerBell);
// ---- 5. Accept: the ABI assertion, the four segments, and Welcome.
const MobileGLResult accepted = session.Accept(*control);
if (accepted != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d Accept failed (rc=%d)\n", selfPid,
static_cast<int>(accepted));
std::fflush(nullptr);
::_exit(67);
}
// ---- 6. hand SIX descriptors over. THIS IS WHY SCM_RIGHTS IS IN THIS
// DESIGN: the segments are anonymous - ASharedMemory_create has no filesystem
// name at all, and memfd / shm_open+unlink are not openable either
// (ShmSegmentPosix.cpp says so) - so the descriptor IS the only key, and this
// process is the side that created them.
//
// The slot travels in the sideband rather than being inferred from arrival
// order, so a reordered or dropped offer is a NAMED mismatch on arrival
// instead of two segments quietly swapped.
{
struct Sideband {
std::uint32_t slot;
std::uint64_t bytes;
};
using Slot = SessionSegmentSlot;
static constexpr Slot kSlots[4] = {Slot::Cmd, Slot::Stage, Slot::Reply, Slot::Event};
for (const Slot slot : kSlots) {
const std::uint32_t tag = static_cast<std::uint32_t>(slot);
const int fd = session.Shm().DescriptorFor(slot);
if (fd < 0) {
std::fprintf(stderr, "MG_Remote server: pid=%d segment %u has no descriptor\n",
selfPid, tag);
std::fflush(nullptr);
::_exit(68);
}
Sideband sideband{tag, session.Shm().AnnouncedSize(slot)};
if (control->ShareFd(fd, MobileGLByteSpan{&sideband, sizeof(sideband)}) !=
MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d could not share segment %u\n",
selfPid, tag);
std::fflush(nullptr);
::_exit(69);
}
}
// Slots 4 and 5 are the bells. The client gets the OTHER end of each pair:
// it writes to the server's bell and polls its own.
const int bellFds[2] = {serverBell[1], clientBell[1]};
for (std::uint32_t index = 0; index < 2; ++index) {
Sideband sideband{4 + index, 0};
if (control->ShareFd(bellFds[index], MobileGLByteSpan{&sideband, sizeof(sideband)}) !=
MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d could not share bell %u\n", selfPid,
index);
std::fflush(nullptr);
::_exit(69);
}
}
::close(serverBell[1]);
::close(clientBell[1]);
}
// ---- 7. the apply thread. It holds the native context for its whole life,
// which is what turns MakeCurrent's cache-invalidation storm into a one-off.
Server::ServerLoop& loop = Server::ServerLoopInstance();
if (loop.Start(session) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d the apply thread did not start\n", selfPid);
std::fflush(nullptr);
::_exit(70);
}
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready "
"(backend up, six descriptors handed over, apply thread running)",
selfPid);
// ---- 8. the control pump. EOF is the whole exit condition: no timeout may
// ever stand in for the death fact (§5.4), because a server one frame behind
// is the intended steady state and a server that is gone is a different thing.
std::vector<std::uint8_t> buffer(64 * 1024);
for (;;) {
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
const MobileGLResult result = control.ReceiveFrame(span, &size, kWaitForever);
const MobileGLResult result = control->ReceiveFrame(span, &size, kWaitForever);
if (result == MOBILEGL_ERR_TRANSPORT_CLOSED) {
WireLogError("MG_Remote server: pid=%d peer closed the control stream; exiting",
selfPid);
@@ -138,28 +280,27 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
}
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) {
buffer.resize(static_cast<std::size_t>(size));
continue; // the message is still queued: that is ITransport's contract
continue; // still queued: ITransport's contract
}
if (result != MOBILEGL_OK) {
WireLogError("MG_Remote server: pid=%d control stream failed (result=%d); exiting",
selfPid, static_cast<int>(result));
WireLogError("MG_Remote server: pid=%d control stream failed (rc=%d); exiting", selfPid,
static_cast<int>(result));
break;
}
// Package sm's stand-in for the control pump: echo the frame back with
// our pid appended, so the parent's test can prove the bytes crossed a
// PROCESS boundary and not just a socket it held both ends of.
// `cp` owns what a control frame MEANS. Until then the pump exists so the
// stream is drained and EOF is noticed; the echo carries this process's
// own pid so a test that talked to itself cannot pass.
std::string reply(reinterpret_cast<const char*>(buffer.data()),
static_cast<std::size_t>(size));
reply += "|server-pid=" + std::to_string(selfPid);
const MobileGLResult sent =
control.SendFrame(MobileGLByteSpan{reply.data(), reply.size()});
if (sent != MOBILEGL_OK) {
if (control->SendFrame(MobileGLByteSpan{reply.data(), reply.size()}) != MOBILEGL_OK) {
break;
}
}
control.Shutdown();
loop.Stop();
session.Close();
control->Shutdown();
std::fflush(nullptr);
::_exit(0);
}
@@ -706,7 +706,18 @@ namespace MobileGL::MG_Remote::Server {
// The bell the apply thread parks on. On the server endpoint of an InProcessTransport that
// is SelfDoorbell(); the client reaches the same bell through its own PeerDoorbell().
void ServerSession::SetExternalDoorbells(Transport::Doorbell* consumer,
Transport::Doorbell* producer) {
m_externalConsumerBell = consumer;
m_externalProducerBell = producer;
}
Transport::Doorbell& ServerSession::ConsumerDoorbell() {
// sm: injected first, because a spawn session's bells are sockets the
// entry point made and the transport has never heard of.
if (m_externalConsumerBell != nullptr) {
return *m_externalConsumerBell;
}
if (m_transport == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no "
"transport; Accept() has not run");
@@ -725,6 +736,9 @@ namespace MobileGL::MG_Remote::Server {
}
Transport::Doorbell& ServerSession::ProducerDoorbell() {
if (m_externalProducerBell != nullptr) {
return *m_externalProducerBell;
}
if (m_transport == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ProducerDoorbell\"} - no "
"transport; Accept() has not run");
+19
View File
@@ -82,11 +82,27 @@ namespace MobileGL::MG_Remote::Server {
ReplyPool& Replies();
// The client rings this one; the apply thread parks on it.
// P6 `sm`: the bells a spawn session parks on, injected because they do
// not come from the transport. CONTRACT-P5 §3.9 kept these accessors on
// the session precisely so this would be ONE switch in ONE file rather
// than two virtuals on every transport - and the switch it produced
// aborts for every role but InProcess, which is where a spawn apply
// thread died before popping its first record (a6 row A7-1).
//
// This is the narrow fix, not the general one: `lk` moves both bells
// behind ILink and DELETES these accessors, which is what CONTRACT-P6
// §8.1 means by retiring the ruling rather than amending it. Until then
// an injected pair is honest about where they come from, and the
// InProcess arm below is untouched.
void SetExternalDoorbells(Transport::Doorbell* consumer, Transport::Doorbell* producer);
Transport::Doorbell& ConsumerDoorbell();
// The server rings this one, but only when producerParked is set (a store to a shared
// cache line otherwise burns a big core for a whole frame on a phone).
Transport::Doorbell& ProducerDoorbell();
bool HasExternalDoorbells() const { return m_externalConsumerBell != nullptr; }
// ---- s1's additions beyond c0's signature block ---------------------------------
// The sizes to create the segments with. Must be called before Accept; after it, the
@@ -183,6 +199,9 @@ namespace MobileGL::MG_Remote::Server {
void LogMemory(const char* phase) const;
private:
// sm: non-owning; the spawn entry point owns the SocketDoorbells.
Transport::Doorbell* m_externalConsumerBell = nullptr;
Transport::Doorbell* m_externalProducerBell = nullptr;
Transport::RingConsumer m_commands;
Wire::SegmentTable m_segments;
PipeApplier m_applier;
+40 -79
View File
@@ -39,6 +39,8 @@ namespace MobileGL::MG_Remote::Server {
constexpr int kChildStreamFd = 3;
constexpr int kChildAuxFd = 4;
constexpr int kChildServerBellFd = 5;
constexpr int kChildClientBellFd = 6;
bool IsExecutableFile(const std::string& path) {
if (path.empty()) {
@@ -161,48 +163,37 @@ namespace MobileGL::MG_Remote::Server {
return children;
}
MobileGLResult SpawnServer(const std::string& imagePath, SpawnedServer* out) {
if (out == nullptr) {
MobileGLResult LaunchServer(const std::string& imagePath, const std::string& endpoint,
LaunchedServer* out) {
if (out == nullptr || endpoint.empty()) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*out = SpawnedServer{};
*out = LaunchedServer{};
const std::string image = ResolveImage(imagePath);
if (!IsExecutableFile(image)) {
// NAMED, never a fallback. ConfigLoader.cpp's own comment names the
// accident this prevents: a lane that asked for spawn, silently got
// monolith, and went green on the wrong arm.
Transport::WireLogError("MG_Remote spawn: the server image is not executable: \"%s\" - refusing. "
"Set MOBILEGL_IPC_SERVER_PATH, or place libMobileGLServer.so beside "
"libMobileGL.so. There is NO monolith fallback from here.",
image.empty() ? "<unresolved>" : image.c_str());
Transport::WireLogError(
"MG_Remote spawn: the server image is not executable: \"%s\" - refusing. Set "
"MOBILEGL_IPC_SERVER_PATH, or place libMobileGLServer.so beside libMobileGL.so. "
"There is NO monolith fallback from here.",
image.empty() ? "<unresolved>" : image.c_str());
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
int stream[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, stream) != 0) {
Transport::WireLogError("MG_Remote spawn: socketpair failed: %s", std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
int aux[2] = {-1, -1};
if (Transport::FdPassing::CreateSocketPair(aux) != MOBILEGL_OK) {
::close(stream[0]);
::close(stream[1]);
return MOBILEGL_ERR_UNSUPPORTED;
}
ScrubbedEnv env = BuildChildEnv();
std::string argv0 = image;
char* argv[] = {argv0.data(), nullptr};
std::string argv1 = endpoint;
char* argv[] = {argv0.data(), argv1.data(), nullptr};
// A close-on-exec pipe so an execve that fails reports its errno back
// instead of vanishing: the app process's stderr is /dev/null on
// Android (ARCHITECTURE.md §15.2), so a silent exec failure would look
// exactly like a server that started and then died.
// instead of vanishing: the app process's stderr is /dev/null on Android
// (ARCHITECTURE.md §15.2), so a silent exec failure would look exactly
// like a server that started and then died.
int report[2] = {-1, -1};
if (::pipe(report) != 0) {
::close(stream[0]); ::close(stream[1]);
::close(aux[0]); ::close(aux[1]);
return MOBILEGL_ERR_UNSUPPORTED;
}
::fcntl(report[1], F_SETFD, FD_CLOEXEC);
@@ -210,87 +201,57 @@ namespace MobileGL::MG_Remote::Server {
const pid_t pid = ::fork();
if (pid < 0) {
Transport::WireLogError("MG_Remote spawn: fork failed: %s", std::strerror(errno));
::close(stream[0]); ::close(stream[1]);
::close(aux[0]); ::close(aux[1]);
::close(report[0]); ::close(report[1]);
::close(report[0]);
::close(report[1]);
return MOBILEGL_ERR_UNSUPPORTED;
}
if (pid == 0) {
// ---- child: async-signal-safe calls ONLY until execve -----------
//
// dup2 both fixes the number and clears close-on-exec, which the aux
// socket has set (FdPassing.cpp:89-90). But a source fd may ALREADY
// BE the target number - in a gtest process fds 3..8 are exactly
// where a fresh socketpair lands - and dup2(n, n) is specified to do
// nothing at all, INCLUDING not clearing close-on-exec. Closing the
// source afterwards then closes the descriptor we just "installed".
// That is this function's one genuinely subtle line, so it is spelled
// out rather than written cleverly.
const int sources[2] = {stream[1], aux[1]};
const int targets[2] = {kChildStreamFd, kChildAuxFd};
for (int i = 0; i < 2; ++i) {
if (sources[i] == targets[i]) {
::fcntl(targets[i], F_SETFD, 0); // already in place: just un-CLOEXEC it
} else {
::dup2(sources[i], targets[i]);
}
}
// Close the originals, but never a descriptor that IS one of the two
// we just installed.
const int spare[5] = {stream[0], stream[1], aux[0], aux[1], report[0]};
for (int fd : spare) {
if (fd != kChildStreamFd && fd != kChildAuxFd) {
::close(fd);
}
}
// ---- child: async-signal-safe calls ONLY until execve -------------
// NOTHING IS HANDED OVER. No dup2, no fixed fd numbers, no
// close-on-exec dance - the child gets an endpoint name in argv and
// finds everything else by listening. That is the whole difference
// between a launcher and a fork-coupled pair, and it is why this
// function is now twenty lines shorter than the version that tried
// to be both.
::close(report[0]);
::execve(image.c_str(), argv, env.pointers.data());
// Only reachable when execve failed.
const int failure = errno;
const ssize_t ignored = ::write(report[1], &failure, sizeof(failure));
(void)ignored;
::_exit(127);
}
// ---- parent ----------------------------------------------------------
::close(stream[1]);
::close(aux[1]);
// ---- parent ---------------------------------------------------------
::close(report[1]);
int childErrno = 0;
const ssize_t got = ::read(report[0], &childErrno, sizeof(childErrno));
::close(report[0]);
if (got == static_cast<ssize_t>(sizeof(childErrno))) {
// The pipe only carries bytes when execve failed.
Transport::WireLogError("MG_Remote spawn: execve(\"%s\") failed in the child: %s",
image.c_str(), std::strerror(childErrno));
::close(stream[0]);
::close(aux[0]);
image.c_str(), std::strerror(childErrno));
int status = 0;
::waitpid(pid, &status, 0);
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out->pid = static_cast<int>(pid);
out->transport = std::make_unique<Transport::SocketTransport>(
stream[0], aux[0], Transport::TransportRole::Client);
Transport::WireLogError("MG_Remote spawn: server pid=%d image=\"%s\" transport=spawn "
"(env scrubbed: %d entries removed)",
out->pid, image.c_str(), env.removed);
out->endpoint = endpoint;
Transport::WireLogError("MG_Remote spawn: server pid=%d image=\"%s\" endpoint=\"%s\" "
"transport=spawn (env scrubbed: %d entries removed)",
out->pid, image.c_str(), endpoint.c_str(), env.removed);
return MOBILEGL_OK;
}
MobileGLResult ReapServer(SpawnedServer& server, std::uint32_t timeoutMs, int* outExitCode) {
MobileGLResult ReapServer(LaunchedServer& server, std::uint32_t timeoutMs,
int* outExitCode) {
if (server.pid < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Closing our end is what the child sees as EOF, and EOF is what it
// exits on. Doing it here rather than making the caller remember is the
// difference between "reap" and "reap, eventually, if you also did the
// other thing".
if (server.transport) {
server.transport->Shutdown();
}
// NOTHING TO CLOSE HERE ANY MORE. The launcher holds no descriptor
// into the child: whoever owns the CONNECTION closes it, and that EOF is
// what the child exits on. A launcher that also owned the connection is
// what made the two processes one unit.
const auto deadline = timeoutMs;
std::uint32_t waited = 0;
@@ -317,12 +278,12 @@ namespace MobileGL::MG_Remote::Server {
#else // !MOBILEGL_SERVER_SPAWN_POSIX
MobileGLResult SpawnServer(const std::string&, SpawnedServer*) {
MobileGLResult LaunchServer(const std::string&, const std::string&, LaunchedServer*) {
Transport::WireLogError("MG_Remote spawn: unsupported on this platform - P6 lands POSIX only "
"(CONTRACT-P6 §2.6); there is no fork() to build a second process from");
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ReapServer(SpawnedServer&, std::uint32_t, int*) {
MobileGLResult ReapServer(LaunchedServer&, std::uint32_t, int*) {
return MOBILEGL_ERR_UNSUPPORTED;
}
int CountOwnChildren() { return -1; }
+41 -30
View File
@@ -6,67 +6,78 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// fork + execve of the server image, and the process discipline around it.
// LAUNCHING a server process, and the process discipline around it.
// Package `sm` (CONTRACT-P6.md §3).
//
// THE TWO FDS THE CHILD INHERITS, at fixed numbers:
// fd 3 the control stream (AF_UNIX SOCK_STREAM)
// fd 4 the aux socket (AF_UNIX SOCK_DGRAM, SCM_RIGHTS)
// Fixed because the child has no other way to learn them: argv would work but
// puts a file-descriptor number in a string that anything can read out of
// /proc/<pid>/cmdline, and an env var would have to survive the scrub below.
// IT LAUNCHES; IT DOES NOT COUPLE. The two processes are independent: this
// hands the child an endpoint NAME and nothing else - no inherited descriptors,
// no shared memory, no handshake. The child listens on that name and the client
// connects to it, exactly as it would to a server someone started by hand or
// that an Android Service started minutes earlier.
//
// dup2 IS WHAT MAKES THEM INHERITABLE. FdPassing::CreateSocketPair sets
// SOCK_CLOEXEC (FdPassing.cpp:89-90), so the aux socket would vanish at execve.
// dup2 does NOT copy the close-on-exec flag - the new descriptor always has it
// clear - so duplicating onto 3 and 4 both fixes the number and clears the
// flag, in one call each.
// An earlier shape of this file forked the server and handed it fds 3..6.
// Inheritance works and is less code, but it can only ever produce a server
// that is a CHILD OF ITS CLIENT - which the end state cannot use, because there
// the server is a standing application and the client is elsewhere, possibly on
// another kernel. So fork/execve survives here only as A WAY TO START A
// PROCESS, which is all it ever needed to be, and every fd it used to carry is
// gone.
//
// ANTI-RECURSION IS TWO INDEPENDENT CATCHES, and CONTRACT-P6 §3.1 is why it is
// not the one ARCHITECTURE.md:488 describes. That rule says the child hard-sets
// Transport to Monolith; a6 measured what that does - 226 live lines test
// ANTI-RECURSION IS STILL TWO INDEPENDENT CATCHES, and CONTRACT-P6 §3.1 is why
// neither is ARCHITECTURE.md:488's. That rule says the child hard-sets
// Transport to Monolith; a6 measured that 226 live lines test
// `Transport != Monolith` to select the SERVER arm, 148 of them in MG_Backend,
// so forcing Monolith flips the whole backend to frontend glue in the one
// process that has no frontend. So:
// (a) STRUCTURAL: the child is told it must not dial, which is a separate
// axis from which role it plays;
// (b) ENVIRONMENTAL: MOBILEGL_TRANSPORT and every MOBILEGL_IPC_* are removed
// from the child's envp, so even a child that ignored (a) has nothing to
// dial with.
// S3 falsifies (b) by leaving the scrub out, and the diagnostic must name WHICH
// of the two fired or the double safety is untestable as two things.
// so obeying it flips the whole backend to frontend glue in the one process
// with no frontend. Instead:
// (a) STRUCTURAL: MOBILEGL_IPC_DIAL=no, a separate axis from which role the
// process plays;
// (b) ENVIRONMENTAL: MOBILEGL_TRANSPORT and every MOBILEGL_IPC_* removed
// from the child's envp.
// The child refuses with exit 64 for (a) and 65 for (b), so the two safeties
// are distinguishable - two that cannot be told apart are one.
#pragma once
#include "../Transport/SocketTransport.h"
#include <Includes.h>
#include "../Protocol/mg_protocol_base.h"
#include <memory>
#include <string>
namespace MobileGL::MG_Remote::Server {
struct SpawnedServer {
struct LaunchedServer {
// -1 when nothing was spawned. Non-negative means this process owes the
// child a wait(): a SpawnedServer that goes out of scope without being
// reaped leaves a zombie, which is exactly what the process-tree gate
// (CONTRACT-P6 §9.4) counts.
// -1 when nothing was launched. Non-negative means this process owes the
// child a wait(): a LaunchedServer that goes out of scope unreaped leaves
// a zombie, which is what the process-tree gate (§9.4) counts.
int pid = -1;
std::unique_ptr<Transport::SocketTransport> transport; // the PARENT's end
// The endpoint the child was told to listen on. The launcher's only
// output besides the pid: everything else the client needs, it gets by
// connecting.
std::string endpoint;
};
// Locates the server image, forks, and execs it.
// Locates the server image and starts it on `endpoint`. Returns as soon as
// the process exists; the client's bounded connect retry is what waits for
// it to finish binding (SocketTransport::ConnectTo).
//
// `imagePath` empty means "resolve it": MOBILEGL_IPC_SERVER_PATH first, then
// dladdr on our own library to find libMobileGLServer.so beside it. An
// unresolvable path is a NAMED REFUSAL and never a monolith fallback -
// ConfigLoader.cpp names that accident and this is the code that must not
// repeat it.
MobileGLResult SpawnServer(const std::string& imagePath, SpawnedServer* out);
MobileGLResult LaunchServer(const std::string& imagePath, const std::string& endpoint,
LaunchedServer* out);
// Waits for the child, up to `timeoutMs`. Returns MOBILEGL_ERR_TIMEOUT if it
// is still running, in which case the caller decides whether to escalate -
// this function never signals a child it did not have to.
MobileGLResult ReapServer(SpawnedServer& server, std::uint32_t timeoutMs, int* outExitCode);
MobileGLResult ReapServer(LaunchedServer& server, std::uint32_t timeoutMs, int* outExitCode);
// How many children this process currently has, by scanning for our own
// pid as a parent. The process-tree gate needs a number, not a promise:
@@ -159,6 +159,24 @@ namespace MobileGL::MG_Remote::Transport {
// travels in SegmentRef instead), so there it still aliases and says so.
MobileGLResult AttachInProcess(SessionSegments& owner, MemoryRole role);
// P6 `sm`: the spawn client's half. The four descriptors arrived over
// SCM_RIGHTS and this side adopts and maps them.
//
// IT IS THE SAME THREE CALLS AttachInProcess ALREADY MAKES - Adopt, then
// Map, per slot - because that was the point of writing the inproc attach
// as dup+Adopt+Map rather than as an alias (see the comment above it).
// The fstat size check inside Adopt, the alignment, the peer lifetime and
// the ledger booking have all been exercised on every inproc run since
// P5; what is new here is only where the fd came from.
//
// OWNERSHIP: on success this object owns all four descriptors and closes
// them in Close(). On failure it closes none of them - the caller still
// owns what it received and is the only one that can report which slot
// failed, so a half-consuming failure path would lose descriptors in the
// one situation where the diagnostic matters.
MobileGLResult AdoptFromDescriptors(const int fds[4], const std::uint64_t sizes[4],
std::uint32_t replySlotCount, MemoryRole role);
void Close();
bool Valid() const { return m_valid; }
@@ -373,6 +373,65 @@ namespace MobileGL::MG_Remote::Transport {
return MOBILEGL_OK;
}
MobileGLResult SessionSegments::AdoptFromDescriptors(const int fds[4],
const std::uint64_t sizes[4],
std::uint32_t replySlotCount,
MemoryRole role) {
Close();
#if defined(_WIN32)
(void)fds; (void)sizes; (void)replySlotCount; (void)role;
// ShmSegment::Adopt is POSIX-only and a Windows peer resolves the section
// by the name in SegmentRef instead. Refused BY NAME rather than silently
// producing an unmapped session (CONTRACT-P6 §2.6).
MGLOG_E("MG_Remote session: AdoptFromDescriptors is POSIX-only - P6 lands POSIX only");
return MOBILEGL_ERR_UNSUPPORTED;
#else
for (std::size_t index = 0; index < kSlotCount; ++index) {
if (fds[index] < 0 || sizes[index] == 0) {
MGLOG_E("MG_Remote session: slot %zu arrived with fd=%d size=%llu - the server's "
"SCM_RIGHTS hand-off is incomplete",
index, fds[index], static_cast<unsigned long long>(sizes[index]));
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
}
m_owns = true;
for (std::size_t index = 0; index < kSlotCount; ++index) {
// Adopt takes ownership only on success, and it fstats the descriptor
// against the announced size - which is the only bound there is on a
// peer's claim about how big a segment is (CONTRACT-P6 §4.4 notes it
// has no analogue on a link with no fd).
const MobileGLResult adopted = ShmSegment::Adopt(fds[index], sizes[index],
m_owned[index]);
if (adopted != MOBILEGL_OK) {
MGLOG_E("MG_Remote session: adopting slot %zu (fd=%d, %llu bytes) failed",
index, fds[index], static_cast<unsigned long long>(sizes[index]));
Close();
return adopted;
}
const MobileGLResult mapped = m_owned[index].Map(false);
if (mapped != MOBILEGL_OK) {
Close();
return mapped;
}
}
m_replySlotCount = replySlotCount;
// NOT DeriveViews' control-page initialisation: the SERVER created these
// and already initialised both RingControls. Re-initialising here would
// zero the owner's cursors out from under a session that is already
// running - the same reason AttachInProcess does not do it either.
DeriveViews();
if (!m_valid) {
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_role = role;
LedgerAddSegment(role, m_mappedBytes);
m_booked = true;
return MOBILEGL_OK;
#endif
}
void SessionSegments::DeriveViews() {
m_valid = false;
if (m_owns) {
@@ -13,12 +13,15 @@
#include "WireLog.h"
#include <cerrno>
#include <cstddef>
#include <cstring>
#include <memory>
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
# define MOBILEGL_SOCKET_TRANSPORT_POSIX 1
# include <poll.h>
# include <sys/stat.h>
# include <sys/un.h>
# include <sys/socket.h>
# include <sys/types.h>
# include <unistd.h>
@@ -62,6 +65,158 @@ namespace MobileGL::MG_Remote::Transport {
}
} // namespace
namespace {
// A filesystem AF_UNIX path, bounded by sun_path. Refused BY NAME when it
// does not fit rather than silently truncated - a truncated path is a
// different socket, and both sides would then "succeed" onto two
// different names.
bool FillAddress(const std::string& path, sockaddr_un* out, socklen_t* outLen) {
std::memset(out, 0, sizeof(*out));
out->sun_family = AF_UNIX;
if (path.empty() || path.size() >= sizeof(out->sun_path)) {
WireLogError("MG_Remote SocketTransport: endpoint path is empty or longer than "
"sun_path allows (%zu >= %zu): \"%s\"",
path.size(), sizeof(out->sun_path), path.c_str());
return false;
}
std::memcpy(out->sun_path, path.c_str(), path.size());
*outLen = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
return true;
}
MobileGLResult ConnectOne(const std::string& path, std::uint32_t timeoutMs, int* outFd) {
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Retry with a bound budget: the server may not have reached bind()
// yet. An exhausted budget is a NAMED refusal, never a fallback -
// CONTRACT-P6 §3.4 calls for a bounded retry and says so.
const std::uint32_t deadline = timeoutMs == 0 ? 1 : timeoutMs;
for (std::uint32_t waited = 0;; waited += 5) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
WireLogError("MG_Remote SocketTransport: socket() failed: %s",
std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
if (::connect(fd, reinterpret_cast<sockaddr*>(&address), length) == 0) {
*outFd = fd;
return MOBILEGL_OK;
}
const int failure = errno;
::close(fd);
if (waited >= deadline) {
WireLogError("MG_Remote SocketTransport: could not connect to \"%s\" within "
"%u ms: %s - REFUSING BY NAME, there is no monolith fallback "
"from here",
path.c_str(), deadline, std::strerror(failure));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
::usleep(5000);
}
}
} // namespace
MobileGLResult SocketTransport::Listen(const std::string& path, int* outListenFd) {
if (outListenFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outListenFd = -1;
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// A stale socket file from a server that died is the normal case, not the
// exceptional one, and bind() fails on it with EADDRINUSE. Unlinking first
// is what every AF_UNIX server does; the alternative is a server that
// cannot restart until someone cleans up by hand.
::unlink(path.c_str());
const int listenFd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (listenFd < 0) {
WireLogError("MG_Remote SocketTransport: socket() failed: %s", std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
if (::bind(listenFd, reinterpret_cast<sockaddr*>(&address), length) != 0) {
WireLogError("MG_Remote SocketTransport: bind(\"%s\") failed: %s", path.c_str(),
std::strerror(errno));
::close(listenFd);
return MOBILEGL_ERR_UNSUPPORTED;
}
// 0600: the rendezvous is between one user's processes. An AF_UNIX path
// is a filesystem object and its mode is the only access control there is.
::chmod(path.c_str(), 0600);
if (::listen(listenFd, 4) != 0) {
WireLogError("MG_Remote SocketTransport: listen(\"%s\") failed: %s", path.c_str(),
std::strerror(errno));
::close(listenFd);
::unlink(path.c_str());
return MOBILEGL_ERR_UNSUPPORTED;
}
*outListenFd = listenFd;
return MOBILEGL_OK;
}
MobileGLResult SocketTransport::AcceptPair(int listenFd, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outServer) {
outServer.reset();
if (listenFd < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
int accepted[2] = {-1, -1};
for (int index = 0; index < 2; ++index) {
const int ready = WaitReadable(listenFd, timeoutMs);
if (ready <= 0) {
for (int fd : accepted) {
if (fd >= 0) ::close(fd);
}
// Half a client is not a client. Both connections or neither, so
// a peer that died between the two cannot leave a session that
// looks up but has no way to receive a descriptor.
WireLogError("MG_Remote SocketTransport: only %d of 2 connections arrived within "
"%u ms", index, timeoutMs);
return ready == 0 ? MOBILEGL_ERR_TIMEOUT : MOBILEGL_ERR_TRANSPORT_CLOSED;
}
accepted[index] = ::accept(listenFd, nullptr, nullptr);
if (accepted[index] < 0) {
for (int fd : accepted) {
if (fd >= 0) ::close(fd);
}
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(errno));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
}
// First connection is control, second is aux. The order IS the protocol;
// it is stated here and in ConnectTo and nowhere else.
outServer = std::make_unique<SocketTransport>(accepted[0], accepted[1],
TransportRole::Server);
return MOBILEGL_OK;
}
MobileGLResult SocketTransport::ConnectTo(const std::string& path, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outClient) {
outClient.reset();
int control = -1;
const MobileGLResult first = ConnectOne(path, timeoutMs, &control);
if (first != MOBILEGL_OK) {
return first;
}
int aux = -1;
// The second connect has a SHORT budget: the server is provably up - we
// just connected to it - so a slow second connection means something is
// wrong rather than something is starting.
const MobileGLResult second = ConnectOne(path, 2000, &aux);
if (second != MOBILEGL_OK) {
::close(control);
return second;
}
outClient = std::make_unique<SocketTransport>(control, aux, TransportRole::Client);
return MOBILEGL_OK;
}
MobileGLResult SocketTransport::CreatePair(std::unique_ptr<SocketTransport>& outClient,
std::unique_ptr<SocketTransport>& outServer) {
outClient.reset();
@@ -48,6 +48,7 @@
#include "ITransport.h"
#include <cstdint>
#include <string>
#include <memory>
#include <mutex>
#include <vector>
@@ -63,6 +64,41 @@ namespace MobileGL::MG_Remote::Transport {
static MobileGLResult CreatePair(std::unique_ptr<SocketTransport>& outClient,
std::unique_ptr<SocketTransport>& outServer);
// ---- independent processes: listen / connect -----------------------
//
// THE TWO PROCESSES ARE NOT COUPLED BY INHERITANCE. An earlier shape of
// this package forked the server and handed it fds 3..6; that can never
// be the end state, where the server is a standing application and the
// client connects to it from somewhere else entirely - possibly from
// another kernel. So the rendezvous is a NAME, and both sides are
// started by whoever starts them.
//
// TWO CONNECTIONS, not one. SCM_RIGHTS needs a socket of its own: an fd
// offer is a sendmsg whose ancillary data rides with specific BYTES, and
// interleaving those bytes with the framed control stream would make the
// frame reassembler and the descriptor receiver race for the same bytes.
// So the client connects twice to the same listening path - first
// connection is control, second is aux - and the server accepts them in
// that order. One name, no extra configuration, and FdPassing keeps the
// dedicated socket it was written for.
//
// `path` is a filesystem AF_UNIX path. ARCHITECTURE.md §15.1 ruled
// against one for the FORK shape, where the fds were inherited and a
// path would have been a strictly larger attack surface for no gain.
// Independent processes have to name a rendezvous somehow, and a path
// with 0600 permissions in a private directory is the portable answer;
// the abstract namespace is Linux-only and TCP would be reachable off
// the machine.
static MobileGLResult Listen(const std::string& path, int* outListenFd);
// Accepts one client's TWO connections, in order.
static MobileGLResult AcceptPair(int listenFd, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outServer);
// The client's half: connects twice to `path`.
static MobileGLResult ConnectTo(const std::string& path, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outClient);
// Adopts an already-connected stream fd - what `sm` uses on each side of
// the fork, where fd 3 is the stream and fd 4 is the aux socket.
// `auxFd` may be -1: the transport then answers MOBILEGL_ERR_UNSUPPORTED
+118 -116
View File
@@ -6,40 +6,44 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Two MobileGL processes, for the first time. Package `sm`.
// Two MobileGL processes, started INDEPENDENTLY and joined by a name.
// Package `sm`.
//
// What these cases pin is the scaffold every later P6 package stands on, and
// each assertion exists because something specific would otherwise pass
// silently:
// THE SHAPE IS THE POINT. The server is launched with an endpoint and nothing
// else - no inherited descriptor, no shared memory, no handshake from its
// parent - and the client finds it by connecting to that name. Everything these
// cases prove therefore transfers unchanged to a server somebody started by
// hand, or that an Android Service started minutes earlier, which is the only
// arrangement the end state can use.
//
// the image links - a6 found mobilegl_server_main existed NOWHERE in
// the tree, only in ARCHITECTURE.md:488-496. If
// the target failed to link, everything below is
// unreachable and the failure would read as "test
// did not run".
// the fds survive execve - FdPassing::CreateSocketPair sets SOCK_CLOEXEC,
// so the aux socket WOULD vanish. dup2 onto fixed
// numbers is what clears it; if that were ever
// changed to a plain assignment the child would
// come up with fd 4 closed and the symptom would
// be a hang, not an error.
// the bytes crossed a PROCESS - the echo carries the child's own getpid(),
// so a test that accidentally talked to itself
// cannot pass. This is ID-124's lesson applied to
// the smallest possible case.
// the process tree is clean - CONTRACT-P6 §9.4: exactly one child while
// running, zero after. Counted, not promised.
// the child dies on EOF - and on EOF ONLY. §5.4 forbids a timeout from
// ever standing in for the death fact, so the
// parent closes and the child must go by itself.
// Each assertion exists because something would otherwise pass silently:
//
// the image links - a6 found mobilegl_server_main existed NOWHERE in
// the tree, only in ARCHITECTURE.md:488-496. A
// target that failed to link would make every case
// below read as "did not run".
// the bytes crossed a - the echo carries the SERVER's own getpid(), so a
// PROCESS test that accidentally talked to itself cannot
// pass. ID-124's lesson at the smallest scale.
// the process tree is clean- §9.4: exactly one child while running, zero
// after. Counted, not promised.
// the server dies on EOF - and on EOF ONLY. §5.4 forbids a timeout from ever
// standing in for the death fact, so the client
// closes and the server must go by itself.
// SCM_RIGHTS works across - the mechanism the four segments ride on, proven
// the boundary before the data plane needs it.
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Server/ServerSpawn.h>
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdlib>
#include <memory>
#include <string>
#include <thread>
#include <vector>
@@ -54,10 +58,13 @@ using namespace MobileGL::MG_Remote;
namespace {
// The image lives beside the test binary's build root. The production
// resolver (MOBILEGL_IPC_SERVER_PATH, then dladdr) is exercised separately
// below; here we want a path we control so a resolution bug cannot look
// like a spawn bug.
// A private rendezvous per case. Independent processes need a NAME, and a
// per-case one is what stops two runs colliding the way a default endpoint
// would.
std::string Endpoint(const char* label) {
return std::string("/tmp/mgl-sm-") + label + "-" + std::to_string(::getpid()) + ".sock";
}
std::string ServerImage() {
if (const char* explicitPath = std::getenv("MOBILEGL_TEST_SERVER_PATH")) {
return explicitPath;
@@ -65,6 +72,40 @@ namespace {
return "libMobileGLServer.so";
}
struct Session {
Server::LaunchedServer server;
std::unique_ptr<Transport::SocketTransport> client;
};
// 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
// coupling the two processes again by the back door.
bool Bring(const char* label, Session* out) {
const std::string endpoint = Endpoint(label);
if (Server::LaunchServer(ServerImage(), endpoint, &out->server) != MOBILEGL_OK) {
return false;
}
return Transport::SocketTransport::ConnectTo(endpoint, 10000, out->client) == MOBILEGL_OK;
}
// Closing the connection is what the server sees as EOF, and EOF is the whole
// exit condition. The launcher holds nothing to close.
void CloseAndReap(Session& session, int* outExitCode) {
if (session.client) {
session.client->Shutdown();
}
ASSERT_EQ(Server::ReapServer(session.server, 5000, outExitCode), MOBILEGL_OK);
}
// The REAL handshake, not an echo. ServerMain runs ServerSession::Accept,
// which wants a Hello and answers Welcome plus six SCM_RIGHTS offers - so a
// test that spoke anything else would only ever prove the socket works.
// Driving ClientSession is what proves the SEGMENTS crossed.
MobileGLResult Handshake(Session& session) {
return Client::ClientSessionInstance().StartOverSocket(std::move(session.client));
}
std::string Exchange(Transport::ITransport& transport, const std::string& message,
std::uint32_t timeoutMs = 4000) {
const MobileGLResult sent =
@@ -85,122 +126,83 @@ namespace {
} // namespace
TEST(ServerSpawnTest, SpawnsAServerProcessAndTalksToIt) {
TEST(ServerSpawnTest, StartsAServerProcessAndHandshakesAcrossIt) {
const int before = Server::CountOwnChildren();
ASSERT_GE(before, 0) << "/proc unreadable; the process-tree gate cannot run here";
Server::SpawnedServer server;
ASSERT_EQ(Server::SpawnServer(ServerImage(), &server), MOBILEGL_OK);
ASSERT_GT(server.pid, 0);
ASSERT_TRUE(server.transport);
EXPECT_NE(server.pid, static_cast<int>(::getpid())) << "that is this process, not a child";
// Exactly one more child while it runs - §9.4, counted.
Session session;
ASSERT_TRUE(Bring("handshake", &session));
ASSERT_GT(session.server.pid, 0);
EXPECT_NE(session.server.pid, static_cast<int>(::getpid()))
<< "that is this process, not a separate one";
EXPECT_EQ(Server::CountOwnChildren(), before + 1);
// The reply carries the CHILD's pid, computed in the child. A test that
// accidentally held both ends of one socket cannot produce this string.
const std::string reply = Exchange(*server.transport, "Hello{abiMajor=1}");
EXPECT_EQ(reply, "Hello{abiMajor=1}|server-pid=" + std::to_string(server.pid));
// THE WHOLE POINT. Hello crosses, Welcome comes back with four SegmentRefs,
// six descriptors arrive over SCM_RIGHTS, and this process maps memory that
// another process created. Nothing here was inherited.
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
EXPECT_TRUE(Client::ClientSession::Active() != nullptr)
<< "a handshake that returned OK must leave the session emit-armed";
Client::ClientSessionInstance().Stop();
int exitCode = -1;
EXPECT_EQ(Server::ReapServer(server, 4000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0) << "the child must exit cleanly on EOF, not be killed";
EXPECT_EQ(server.pid, -1);
// Nothing left behind. A zombie would still be counted here.
EXPECT_EQ(Server::CountOwnChildren(), before);
ASSERT_EQ(Server::ReapServer(session.server, 5000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0) << "the server must exit cleanly on EOF, not be killed";
EXPECT_EQ(Server::CountOwnChildren(), before) << "a zombie would still be counted here";
}
TEST(ServerSpawnTest, TheChildExitsOnEofAndOnlyOnEof) {
Server::SpawnedServer server;
ASSERT_EQ(Server::SpawnServer(ServerImage(), &server), MOBILEGL_OK);
// It must NOT exit just because it is idle: §5.4 forbids a timeout from
// standing in for the death fact, and a server that wandered off on its own
// would make "the peer is gone" unfalsifiable.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
EXPECT_EQ(Server::CountOwnChildren(), 1) << "the child exited while merely idle";
EXPECT_EQ(Exchange(*server.transport, "still-there"),
"still-there|server-pid=" + std::to_string(server.pid));
// Now close. EOF is the whole signal.
int exitCode = -1;
ASSERT_EQ(Server::ReapServer(server, 4000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0);
}
TEST(ServerSpawnTest, ManyMessagesCrossTheProcessBoundaryInOrder) {
Server::SpawnedServer server;
ASSERT_EQ(Server::SpawnServer(ServerImage(), &server), MOBILEGL_OK);
// Order across a real process boundary, with the reassembler on both sides.
for (int i = 0; i < 128; ++i) {
const std::string message = "msg-" + std::to_string(i);
EXPECT_EQ(Exchange(*server.transport, message),
message + "|server-pid=" + std::to_string(server.pid));
}
int exitCode = -1;
ASSERT_EQ(Server::ReapServer(server, 4000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0);
}
TEST(ServerSpawnTest, AnUnresolvableImageIsANamedRefusalAndSpawnsNothing) {
TEST(ServerSpawnTest, AnUnresolvableImageIsANamedRefusalAndStartsNothing) {
const int before = Server::CountOwnChildren();
// S1 (CONTRACT-P6 §9). The accident this prevents is the one
// ConfigLoader.cpp names in its own comment: a lane that asked for spawn,
// silently got monolith, and went green on the wrong arm.
Server::SpawnedServer server;
EXPECT_EQ(Server::SpawnServer("/nonexistent/libMobileGLServer.so", &server),
Server::LaunchedServer server;
EXPECT_EQ(Server::LaunchServer("/nonexistent/libMobileGLServer.so", Endpoint("missing"),
&server),
MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_EQ(server.pid, -1);
EXPECT_FALSE(server.transport) << "a refused spawn must not hand back a usable transport";
EXPECT_EQ(Server::CountOwnChildren(), before) << "a refused spawn must leave no process";
EXPECT_EQ(Server::CountOwnChildren(), before) << "a refused launch must leave no process";
}
TEST(ServerSpawnTest, TheChildsEnvironmentIsScrubbedOfEveryClientKnob) {
// Catch (b). The child refuses to start with exit 65 if any MOBILEGL_IPC_*
// or MOBILEGL_TRANSPORT survived into it, so setting one here and getting a
// clean start back is the assertion. S3 falsifies it by removing the scrub.
::setenv("MOBILEGL_TRANSPORT", "spawn", 1);
::setenv("MOBILEGL_IPC_RING_MB", "64", 1);
Server::SpawnedServer server;
ASSERT_EQ(Server::SpawnServer(ServerImage(), &server), MOBILEGL_OK);
EXPECT_EQ(Exchange(*server.transport, "scrubbed"),
"scrubbed|server-pid=" + std::to_string(server.pid))
<< "the child refused to start, which means a client knob survived the scrub";
int exitCode = -1;
ASSERT_EQ(Server::ReapServer(server, 4000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0) << "exit 64 = the dial catch fired; exit 65 = the scrub leaked";
::unsetenv("MOBILEGL_TRANSPORT");
::unsetenv("MOBILEGL_IPC_RING_MB");
TEST(ServerSpawnTest, ConnectingToNothingIsANamedRefusalNotAHang) {
// The other half of S1: the client's side. A rendezvous nobody is listening
// on must be a bounded, named failure - never an unbounded wait, which is
// what a caller would experience as the whole application freezing.
std::unique_ptr<Transport::SocketTransport> client;
const auto start = std::chrono::steady_clock::now();
EXPECT_NE(Transport::SocketTransport::ConnectTo(Endpoint("nobody"), 300, client), MOBILEGL_OK);
EXPECT_FALSE(client);
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
4000);
}
TEST(ServerSpawnTest, ADescriptorCrossesToTheChildProcess) {
// The mechanism the four shared segments will ride on. Proving it now, on
// a pipe, is what stops it being first exercised on the day the data plane
// needs it - the same argument SessionRings.h makes about the attach half.
Server::SpawnedServer server;
ASSERT_EQ(Server::SpawnServer(ServerImage(), &server), MOBILEGL_OK);
TEST(ServerSpawnTest, ADescriptorCrossesBetweenTheTwoProcesses) {
// The mechanism the four shared segments ride on. Proving it now, on a pipe,
// is what stops it being first exercised on the day the data plane needs it -
// the same argument SessionRings.h makes about the attach half.
//
// NOTE it crosses the AUX connection, which is the second of the two the
// client made. An fd offer is a sendmsg whose ancillary data rides with
// specific bytes, so it may not share a socket with the framed control
// stream: the frame reassembler and the descriptor receiver would race for
// the same bytes.
Session session;
ASSERT_TRUE(Bring("scm", &session));
int pipeFds[2] = {-1, -1};
ASSERT_EQ(::pipe(pipeFds), 0);
const std::string sideband = "SegmentRef{id=1,kind=Cmd}";
EXPECT_EQ(server.transport->ShareFd(pipeFds[0],
MobileGLByteSpan{sideband.data(), sideband.size()}),
MOBILEGL_OK)
<< "SCM_RIGHTS across execve: if the aux fd lost its number or kept "
"close-on-exec, this is where it shows";
EXPECT_EQ(session.client->ShareFd(pipeFds[0],
MobileGLByteSpan{sideband.data(), sideband.size()}),
MOBILEGL_OK);
::close(pipeFds[0]);
::close(pipeFds[1]);
int exitCode = -1;
ASSERT_EQ(Server::ReapServer(server, 4000, &exitCode), MOBILEGL_OK);
CloseAndReap(session, &exitCode);
}
#endif // !_WIN32