diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index b2c3299c..71937ee1 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -30,6 +30,8 @@ #include #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..."); diff --git a/MobileGL/MG_Backend/ServerRole.h b/MobileGL/MG_Backend/ServerRole.h new file mode 100644 index 00000000..3d255f8e --- /dev/null +++ b/MobileGL/MG_Backend/ServerRole.h @@ -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 + +// 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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 605abd64..d54b8bd6 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -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) { + 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 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(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 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(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(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(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(fds[4], 1, true); + m_socketSelfBell = MakeUnique(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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 63ac69e0..a3ad6e4b 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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); + MobileGLResult StartOverTransportPair(std::unique_ptr clientEnd, std::unique_ptr 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 m_socketTransport; + std::unique_ptr m_socketSelfBell; + std::unique_ptr m_socketPeerBell; + std::unique_ptr m_clientTransport; std::unique_ptr m_serverTransport; Transport::SessionSegments m_shm; diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index 6bbd1481..9300e90e 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -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 #include #include #include +#include #include #include #if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__) +# include # include #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(::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 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(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(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 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(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(result)); + WireLogError("MG_Remote server: pid=%d control stream failed (rc=%d); exiting", selfPid, + static_cast(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(buffer.data()), static_cast(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); } diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index a26e1cbe..be5a3560 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -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"); diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index 3c3b4b66..4aeabd87 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -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; diff --git a/MobileGL/MG_Remote/Server/ServerSpawn.cpp b/MobileGL/MG_Remote/Server/ServerSpawn.cpp index 4c6321f1..92008a27 100644 --- a/MobileGL/MG_Remote/Server/ServerSpawn.cpp +++ b/MobileGL/MG_Remote/Server/ServerSpawn.cpp @@ -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() ? "" : 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() ? "" : 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(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(pid); - out->transport = std::make_unique( - 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; } diff --git a/MobileGL/MG_Remote/Server/ServerSpawn.h b/MobileGL/MG_Remote/Server/ServerSpawn.h index 38dad6e6..586b1af7 100644 --- a/MobileGL/MG_Remote/Server/ServerSpawn.h +++ b/MobileGL/MG_Remote/Server/ServerSpawn.h @@ -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//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 + +#include "../Protocol/mg_protocol_base.h" #include #include 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; // 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: diff --git a/MobileGL/MG_Remote/Transport/SessionRings.h b/MobileGL/MG_Remote/Transport/SessionRings.h index bcb445ff..441f600f 100644 --- a/MobileGL/MG_Remote/Transport/SessionRings.h +++ b/MobileGL/MG_Remote/Transport/SessionRings.h @@ -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; } diff --git a/MobileGL/MG_Remote/Transport/ShmSegment.cpp b/MobileGL/MG_Remote/Transport/ShmSegment.cpp index 16f5bdcf..356009d8 100644 --- a/MobileGL/MG_Remote/Transport/ShmSegment.cpp +++ b/MobileGL/MG_Remote/Transport/ShmSegment.cpp @@ -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(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(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) { diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.cpp b/MobileGL/MG_Remote/Transport/SocketTransport.cpp index 31abe6d6..e7d277b7 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.cpp +++ b/MobileGL/MG_Remote/Transport/SocketTransport.cpp @@ -13,12 +13,15 @@ #include "WireLog.h" #include +#include #include #include #if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__) # define MOBILEGL_SOCKET_TRANSPORT_POSIX 1 # include +# include +# include # include # include # include @@ -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(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(&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(&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& 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(accepted[0], accepted[1], + TransportRole::Server); + return MOBILEGL_OK; + } + + MobileGLResult SocketTransport::ConnectTo(const std::string& path, std::uint32_t timeoutMs, + std::unique_ptr& 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(control, aux, TransportRole::Client); + return MOBILEGL_OK; + } + MobileGLResult SocketTransport::CreatePair(std::unique_ptr& outClient, std::unique_ptr& outServer) { outClient.reset(); diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.h b/MobileGL/MG_Remote/Transport/SocketTransport.h index 6389afc1..4f648311 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.h +++ b/MobileGL/MG_Remote/Transport/SocketTransport.h @@ -48,6 +48,7 @@ #include "ITransport.h" #include +#include #include #include #include @@ -63,6 +64,41 @@ namespace MobileGL::MG_Remote::Transport { static MobileGLResult CreatePair(std::unique_ptr& outClient, std::unique_ptr& 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& outServer); + + // The client's half: connects twice to `path`. + static MobileGLResult ConnectTo(const std::string& path, std::uint32_t timeoutMs, + std::unique_ptr& 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 diff --git a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp index 3aa47776..6575b362 100644 --- a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp @@ -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 #include #include +#include +#include #include #include #include +#include #include #include #include @@ -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 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(::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(::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 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::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