[Fix] (MG_Remote, P11 PAIR): a socket client's control and aux connections pair by the nonce each presents first, not by arrival order - a readiness probe or a second client can no longer take half a session, and a client that sends its Hello first is refused by name

This commit is contained in:
2026-09-29 10:19:59 -04:00
parent 0a6af63655
commit abe29eae2d
7 changed files with 572 additions and 97 deletions
+3
View File
@@ -656,6 +656,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp
MobileGL/MG_Remote/Server/ServerMain.cpp
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
MobileGL/MG_Remote/Server/PairAcceptor.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
+291
View File
@@ -0,0 +1,291 @@
// MobileGL - MobileGL/MG_Remote/Server/PairAcceptor.cpp
// 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
// P11 PAIR: see PairAcceptor.h for what is paired, what is refused and why.
#include "PairAcceptor.h"
#if !defined(_WIN32)
#include "../Handshake.h" // AuthenticatePeerToken / ValidatePeerHandshake, for a Hello sent first
#include "../Transport/WireLog.h"
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
namespace MobileGL::MG_Remote::Server {
namespace {
namespace Protocol = ::MobileGL::Wire;
using Transport::SocketTransport;
using Transport::TransportRole;
using Step = FirstFrameAssembler::Step;
// A pause after accept(2) ran out of descriptors or memory: the connection waits in the
// backlog and the listener is fine (TcpSupervisor pauses the same way).
constexpr std::uint32_t kAcceptPauseMs = 100;
std::int64_t MillisecondsUntil(PairAcceptor::Clock::time_point when, PairAcceptor::Clock::time_point now) {
return std::chrono::duration_cast<std::chrono::milliseconds>(when - now).count();
}
// Whatever the peer already sent, read and dropped (non-blocking, bounded), so the close
// after a refusal does not become an RST that discards the refusal (ServerMain.cpp's
// DrainUnread; RefuseBusy's comment has the reason).
void DrainUnread(int fd) {
char sink[4096];
for (int rounds = 0; rounds < 256; ++rounds) {
if (::recv(fd, sink, sizeof(sink), MSG_DONTWAIT) <= 0) return;
}
}
void Refuse(SocketTransport& connection, Protocol::RefuseCode code, const char* detail) {
::flatbuffers::FlatBufferBuilder builder(256);
auto refusal = Protocol::CreateRefuseDirect(builder, code, detail);
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::Refuse, refusal.Union());
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
(void)connection.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
Transport::WireLogError("MG_Remote server: Refuse{%s} %s", Protocol::EnumNameRefuseCode(code), detail);
}
// Refuses a connection held as a bare descriptor, and closes it (with shutdown(2): here the
// descriptor is only ever the supervisor's).
void RefuseAndClose(int fd, Protocol::RefuseCode code, const char* detail) {
SocketTransport connection(fd, -1, TransportRole::Server);
DrainUnread(fd);
Refuse(connection, code, detail);
}
} // namespace
PairAcceptor::PairAcceptor(int listenFd, std::uint32_t budgetMs) : m_listener(listenFd), m_budgetMs(budgetMs) {}
PairAcceptor::~PairAcceptor() {
for (auto& waiting : m_waiting) Drop(waiting, true, kPairStopped);
for (const auto& pair : m_ready) {
RefuseAndClose(pair.first, Protocol::RefuseCode::Busy, kPairStopped);
::close(pair.second);
}
}
void PairAcceptor::CloseInForkedChild() {
for (auto& waiting : m_waiting)
if (waiting.fd >= 0) ::close(waiting.fd);
for (const auto& pair : m_ready) {
::close(pair.first);
::close(pair.second);
}
m_waiting.clear();
m_ready.clear();
}
MobileGLResult PairAcceptor::Accept(std::uint32_t timeoutMs, std::unique_ptr<SocketTransport>& outServer) {
outServer.reset();
if (m_listener < 0) return MOBILEGL_ERR_INVALID_ARGUMENT;
const auto until = Clock::now() + std::chrono::milliseconds(timeoutMs);
std::vector<pollfd> fds;
std::vector<std::size_t> polled; // index into m_waiting of fds[1..]
for (;;) {
if (!m_ready.empty()) {
const auto pair = m_ready.front();
m_ready.pop_front();
outServer = std::make_unique<SocketTransport>(pair.first, pair.second, TransportRole::Server);
return MOBILEGL_OK;
}
auto now = Clock::now();
Expire(now);
// While accepting is paused the listener sits out the poll (it stays readable and would
// spin the loop), and the pause's end is a wakeup.
const bool acceptPaused = now < m_acceptResumeAt;
fds.clear();
polled.clear();
fds.push_back({acceptPaused ? -1 : m_listener, POLLIN, 0});
std::int64_t wait = std::clamp<std::int64_t>(MillisecondsUntil(until, now), 0, 1 << 30);
for (std::size_t index = 0; index < m_waiting.size(); ++index) {
const Waiting& waiting = m_waiting[index];
wait = std::clamp<std::int64_t>(MillisecondsUntil(waiting.deadline, now) + 1, 0, wait);
// A bound connection is not read again: on control its Hello follows, and that is
// the session's to read. It only waits for its partner or its deadline.
if (waiting.bound) continue;
fds.push_back({waiting.fd, POLLIN, 0});
polled.push_back(index);
}
if (acceptPaused) wait = std::clamp<std::int64_t>(MillisecondsUntil(m_acceptResumeAt, now) + 1, 0, wait);
const int ready = ::poll(fds.data(), static_cast<nfds_t>(fds.size()), static_cast<int>(wait));
if (ready < 0 && errno != EINTR) {
Transport::WireLogError("MG_Remote server: the pairing poll failed: %s", std::strerror(errno));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (ready > 0) {
for (std::size_t slot = 0; slot < polled.size(); ++slot)
if (fds[slot + 1].revents != 0) Read(m_waiting[polled[slot]]);
Compact();
if ((fds[0].revents & (POLLIN | POLLERR | POLLHUP)) != 0) {
const MobileGLResult accepted = AcceptNew(Clock::now());
if (accepted != MOBILEGL_OK) return accepted;
}
}
if (m_ready.empty() && Clock::now() >= until) return MOBILEGL_ERR_TIMEOUT;
}
}
// Accepts everything the listener has; each connection waits in the set until it identifies.
MobileGLResult PairAcceptor::AcceptNew(Clock::time_point now) {
for (int burst = 0; burst < 64; ++burst) {
int fd = -1;
const MobileGLResult accepted = SocketTransport::AcceptOne(m_listener, 0, &fd);
if (accepted == MOBILEGL_ERR_TIMEOUT) return MOBILEGL_OK;
if (accepted == MOBILEGL_ERR_OUT_OF_MEMORY) {
m_acceptResumeAt = now + std::chrono::milliseconds(kAcceptPauseMs);
MGLOG_W("MG_Remote server: accept paused for %u ms: out of descriptors or memory with %zu "
"connections waiting to be paired",
kAcceptPauseMs, m_waiting.size());
return MOBILEGL_OK;
}
if (accepted != MOBILEGL_OK) {
Transport::WireLogError("MG_Remote server: the listener failed while pairing (rc=%d)",
static_cast<int>(accepted));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (m_waiting.size() >= kPairMaxUnpaired) {
// The oldest connection that has not said who it is goes first; with none, the oldest.
auto victim = std::find_if(m_waiting.begin(), m_waiting.end(),
[](const Waiting& waiting) { return !waiting.bound; });
if (victim == m_waiting.end()) victim = m_waiting.begin();
MGLOG_W("MG_Remote server: %zu connections are waiting to be paired; the oldest %s one is dropped",
m_waiting.size(), victim->bound ? "identified" : "unidentified");
Drop(*victim, true, kPairFull);
m_waiting.erase(victim);
}
Waiting waiting;
waiting.fd = fd;
waiting.deadline = now + std::chrono::milliseconds(m_budgetMs);
m_waiting.push_back(std::move(waiting));
}
return MOBILEGL_OK;
}
void PairAcceptor::Read(Waiting& waiting) {
switch (waiting.frame.Pump(waiting.fd)) {
case Step::NeedMore:
return;
case Step::Complete:
Judge(waiting);
return;
case Step::EndedEmpty:
// A readiness probe - a connect() and a close() - costs an accept and nothing else.
MGLOG_D("MG_Remote server: a connection closed before presenting a PairBind (a probe); dropped");
::close(waiting.fd);
waiting.fd = -1;
return;
case Step::NotAFrame:
RefuseAndClose(waiting.fd, Protocol::RefuseCode::MalformedHello, kFirstFrameNotAFrame);
waiting.fd = -1;
return;
case Step::EndedPartial:
RefuseAndClose(waiting.fd, Protocol::RefuseCode::MalformedHello, kFirstFrameTruncated);
waiting.fd = -1;
return;
}
}
// A complete first frame. A PairBind either completes a pair or waits for its partner.
void PairAcceptor::Judge(Waiting& waiting) {
std::uint8_t nonce[Transport::kPairNonceBytes] = {};
bool aux = false;
if (!Transport::DecodePairBind(waiting.frame.Payload(), nonce, &aux)) {
RefuseUnpairable(waiting);
return;
}
for (auto& other : m_waiting) {
if (&other == &waiting || other.fd < 0 || !other.bound ||
!Transport::ConstantTimeNonceMatch(other.nonce, nonce))
continue;
if (other.aux == aux) {
MGLOG_W("MG_Remote server: a second %s connection presented a pair nonce that is already waiting",
aux ? "aux" : "control");
Drop(waiting, false, kPairDuplicate);
return;
}
m_ready.emplace_back(aux ? other.fd : waiting.fd, aux ? waiting.fd : other.fd);
other.fd = -1;
waiting.fd = -1;
return;
}
waiting.bound = true;
waiting.aux = aux;
std::memcpy(waiting.nonce, nonce, sizeof(nonce));
}
// A first frame that is not a PairBind. Nothing can be paired with it, and it is told so by name.
void PairAcceptor::RefuseUnpairable(Waiting& waiting) {
const auto& payload = waiting.frame.Payload();
const int fd = waiting.fd;
waiting.fd = -1;
SocketTransport connection(fd, -1, TransportRole::Server);
DrainUnread(fd);
const FirstFrameShape shape = ClassifyFirstFrame(payload.data(), payload.size());
if (shape == FirstFrameShape::Hello) {
// A client from before PairBind (control revision < 4) writes its Hello first. It is
// asked what RunSession would ask, in RunSession's order - the token, then the wire - so
// an older client hears Refuse{WireFingerprint} exactly as a session would answer it.
const auto* hello = Protocol::GetCtrlEnvelope(payload.data())->msg_as_Hello();
if (AuthenticatePeerToken(connection, hello->token()) != MOBILEGL_OK) return;
if (ValidatePeerHandshake(connection, hello->abiMajor(), hello->abiMinor(), hello->wireFingerprint(),
hello->buildFingerprint() ? hello->buildFingerprint()->c_str() : nullptr,
hello->dialMode()) != MOBILEGL_OK)
return;
Refuse(connection, Protocol::RefuseCode::MalformedHello, kPairHelloFirst);
return;
}
if (shape == FirstFrameShape::DataBind) {
// RunSession's own words for a DataBind that reaches a listener with no session for it.
Refuse(connection, Protocol::RefuseCode::Authentication, "data connection names no live session");
return;
}
Refuse(connection, Protocol::RefuseCode::MalformedHello, FirstFrameRefusalDetail(shape));
}
// Past its deadline a connection is dropped; nothing that is still inside its budget is touched.
void PairAcceptor::Expire(Clock::time_point now) {
for (auto& waiting : m_waiting) {
if (waiting.fd < 0 || now < waiting.deadline) continue;
if (waiting.bound)
MGLOG_W("MG_Remote server: a%s connection's partner never presented its pair nonce within %u ms; "
"dropped",
waiting.aux ? "n aux" : " control", m_budgetMs);
Drop(waiting, false, waiting.bound ? kPairNoPartner : kPairNoBind);
}
Compact();
}
void PairAcceptor::Drop(Waiting& waiting, bool busy, const char* detail) {
if (waiting.fd < 0) return;
if (waiting.bound && waiting.aux) {
::shutdown(waiting.fd, SHUT_RDWR);
::close(waiting.fd);
} else {
RefuseAndClose(waiting.fd, busy ? Protocol::RefuseCode::Busy : Protocol::RefuseCode::Authentication,
detail);
}
waiting.fd = -1;
}
void PairAcceptor::Compact() {
m_waiting.erase(std::remove_if(m_waiting.begin(), m_waiting.end(),
[](const Waiting& waiting) { return waiting.fd < 0; }),
m_waiting.end());
}
} // namespace MobileGL::MG_Remote::Server
#endif
+131
View File
@@ -0,0 +1,131 @@
// MobileGL - MobileGL/MG_Remote/Server/PairAcceptor.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
// P11 PAIR. A SOCKET CLIENT'S TWO CONNECTIONS ARE PAIRED BY WHO THEY ARE, NOT BY WHEN THEY ARRIVED.
//
// SocketTransport::ConnectTo opens two connections to one listener - control, then aux (the
// SCM_RIGHTS socket; on tcp:// the data connection). The server used to take "the next two accepted
// connections, first is control" (SocketTransport::AcceptPair). That was a protocol only while
// nothing else could reach the listener, and on the device it was not (B0-CROSS-APP.md F2): the
// server app's readiness probe - one connect() and close() - became the next client's control
// connection, that session read the probe's socket and ended ("control peer closed before sending
// a first frame"), and the client got no Welcome. Two clients connecting at the same moment could
// just as well swap halves.
//
// Now each connection's first frame is a PairBind (protocol.fbs, control revision 4): the client's
// nonce, the same on both, and which half it is. This acceptor keeps every accepted connection in a
// small set until it has presented one, and hands out a control and an aux connection only when
// their nonces match. What it does with everything else:
// - a connection that closes having sent nothing (a readiness probe) is closed, quietly;
// - a first frame that is not a PairBind is refused by name. A Hello is a client from before
// PairBind: it is asked what the session would ask, in the session's order - the token, then the
// wire (Handshake.h) - so it hears Refuse{WireFingerprint} as it would from any session, and one
// that passes both is still refused, because it cannot be paired;
// - a connection that has not presented a PairBind, or whose partner has not presented the same
// nonce, within the budget is dropped - a control connection with a refusal, an aux connection
// (whose client reads descriptors there, not frames) by closing it - and nobody else's
// connection is touched;
// - past kPairMaxUnpaired connections the oldest one that has not identified itself (else the
// oldest) is dropped the same way.
//
// It never reads a byte past a PairBind (FirstFrameAssembler): on control the Hello follows it, and
// the session reads that itself; on aux the descriptors follow it.
//
// Stateful because pairs complete out of order: with A-control, B-control, B-aux, A-aux, B is
// handed out first and A-control has to still be here for the call that hands out A.
#pragma once
#if !defined(_WIN32)
#include "../Transport/PairBind.h"
#include "../Transport/SocketTransport.h"
#include "PreAuthGate.h" // FirstFrameAssembler
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <utility>
#include <vector>
namespace MobileGL::MG_Remote::Server {
// How long an accepted connection may stay unpaired: to present its PairBind, and then to be
// joined by its partner. The old AcceptPair's wait for a second connection, and ConnectTo's
// budget for opening it.
inline constexpr std::uint32_t kPairBudgetMs = 2000;
// Connections held unpaired at once. A client needs two, and the rendezvous is local.
inline constexpr std::size_t kPairMaxUnpaired = 16;
// The refusal details a peer (and the log) sees.
inline constexpr const char* kPairNoBind = "no PairBind within the pairing budget";
inline constexpr const char* kPairNoPartner =
"no aux connection presented this connection's pair nonce within the pairing budget";
inline constexpr const char* kPairHelloFirst =
"the control connection's first frame is a Hello, not a PairBind (control revision 4 pairs by nonce)";
inline constexpr const char* kPairDuplicate = "a connection with this pair nonce and role is already waiting";
inline constexpr const char* kPairFull = "too many connections are waiting to be paired";
inline constexpr const char* kPairStopped = "the server stopped accepting before this connection was paired";
class PairAcceptor {
public:
using Clock = std::chrono::steady_clock;
// `listenFd` stays the caller's. `budgetMs` is kPairBudgetMs outside the unit test.
explicit PairAcceptor(int listenFd, std::uint32_t budgetMs = kPairBudgetMs);
// Every connection still held is let go of (kPairStopped, Busy), with shutdown(2).
~PairAcceptor();
PairAcceptor(const PairAcceptor&) = delete;
PairAcceptor& operator=(const PairAcceptor&) = delete;
// One client's pair, waiting at most `timeoutMs` for it: OK with `outServer` holding the
// control connection as its stream and the aux connection; MOBILEGL_ERR_TIMEOUT when no pair
// completed in time (connections still unpaired are KEPT for the next call); and
// MOBILEGL_ERR_TRANSPORT_CLOSED when the listener itself failed.
MobileGLResult Accept(std::uint32_t timeoutMs, std::unique_ptr<Transport::SocketTransport>& outServer);
// A forked session child's copy of every held descriptor, closed WITHOUT shutdown(2) - that
// would disconnect the supervisor's copy too - and without a word: they are the supervisor's.
void CloseInForkedChild();
// Connections accepted and not yet handed out as a pair.
std::size_t Unpaired() const { return m_waiting.size(); }
private:
struct Waiting {
int fd = -1;
Clock::time_point deadline{};
FirstFrameAssembler frame;
bool bound = false; // its PairBind has arrived
bool aux = false;
std::uint8_t nonce[Transport::kPairNonceBytes] = {};
};
MobileGLResult AcceptNew(Clock::time_point now);
void Read(Waiting& waiting);
void Judge(Waiting& waiting);
void RefuseUnpairable(Waiting& waiting);
void Expire(Clock::time_point now);
// A control connection, or one that never said which it is, is refused (Busy when it was
// crowded out or the server stopped, else Authentication) and closed; an aux connection is
// closed - its client reads descriptors there, never frames.
void Drop(Waiting& waiting, bool busy, const char* detail);
void Compact();
int m_listener;
std::uint32_t m_budgetMs;
std::vector<Waiting> m_waiting; // accept order, oldest first
std::deque<std::pair<int, int>> m_ready; // {control, aux}, not yet handed out
Clock::time_point m_acceptResumeAt{};
};
} // namespace MobileGL::MG_Remote::Server
#endif
+26 -11
View File
@@ -9,6 +9,7 @@
#include "../Handshake.h"
#include "../FatalFunnel.h"
#include "InProcessServer.h"
#include "PairAcceptor.h"
#include "PreAuthGate.h"
#include "ServerDisplay.h"
#include "ServerLoop.h"
@@ -340,7 +341,7 @@ void DelaySessionHandshakeForTest() {
// Called only after fork (or, in the single-session shape, instead of it): the supervisor never
// creates a backend or EGL context. `dataSource` is set on TCP (PH-7 (4)) and empty on a unix
// endpoint, whose second connection is the SCM_RIGHTS socket AcceptPair already paired.
// endpoint, whose second connection is the SCM_RIGHTS socket PairAcceptor already paired.
// `sourceFd` is the descriptor that source reads from - the child's end of the hand-off
// socketpair under --serve, the listener itself in the single-session shape - and -1 when there
// is none; it is let go of the moment Accept returns, and on every exit before that
@@ -1308,16 +1309,17 @@ private:
};
// THE UNIX SHAPE OF THE IN-PROCESS DISPLAY SERVER (P12 D5 extended to unix endpoints). The
// endpoint is an @abstract name or a filesystem path. A client's control AND data connections
// pair by arrival order (AcceptPair) and the shm segments' descriptors cross by SCM_RIGHTS, so
// there is no DataBind to route and no pre-auth gate - both are TCP exposure machinery, and a
// unix peer is local by construction (the forked unix supervisor runs without either). Sessions
// take the TCP in-process shape's thread hand-off: one at a time, Busy for a second client, the
// latch reset between sessions, stop by g_inProcessStop.
// endpoint is an @abstract name or a filesystem path. A client's control AND aux connections
// pair by the nonce each presents first (PairAcceptor, P11 PAIR - never by arrival order) and the
// shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth
// gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked
// unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off:
// one at a time, Busy for a second client, the latch reset between sessions, stop by
// g_inProcessStop.
class UnixInProcessSupervisor {
public:
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted)
: m_listener(listener), m_sessionsFaulted(sessionsFaulted) {}
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted) {}
~UnixInProcessSupervisor() {
// Run() only returns with the session thread joined; a listener failure (73) is the
@@ -1330,7 +1332,7 @@ public:
if (g_inProcessStop.load(std::memory_order_acquire)) return StopServing();
Reap();
std::unique_ptr<SocketTransport> control;
const auto accepted = SocketTransport::AcceptPair(m_listener, 250, control);
const auto accepted = m_pairs.Accept(250, control);
if (accepted == MOBILEGL_ERR_TIMEOUT) continue;
if (accepted != MOBILEGL_OK) {
WireLogError("MG_Remote server: the in-process unix listener failed (rc=%d)",
@@ -1401,6 +1403,8 @@ private:
}
int m_listener;
// The connections that have not been paired yet live here between Accept calls.
Server::PairAcceptor m_pairs;
unsigned long& m_sessionsFaulted;
std::thread m_sessionThread;
std::atomic<bool> m_sessionDone{false};
@@ -1472,6 +1476,10 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
// From here: the unix endpoint (--serve or not), and the single-session TCP shape.
pid_t active = -1;
const auto reap = [&]() { return ReapActiveSession(active, sessionsFaulted); };
// P11 PAIR: a unix client's two connections, paired by the nonce each presents first. The
// connections not yet paired stay in it between calls.
std::unique_ptr<Server::PairAcceptor> pairs;
if (!tcpEndpoint) pairs = std::make_unique<Server::PairAcceptor>(listener);
for (;;) {
reap();
std::unique_ptr<SocketTransport> control;
@@ -1481,11 +1489,14 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
accepted = SocketTransport::AcceptOne(listener, 30000, &fd);
if (accepted == MOBILEGL_OK) control = std::make_unique<SocketTransport>(fd, -1, TransportRole::Server);
} else {
accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control);
accepted = pairs->Accept(serve ? 250 : 30000, control);
}
if (accepted == MOBILEGL_ERR_TIMEOUT && serve) continue;
if (accepted != MOBILEGL_OK) { LogSupervisorSummary(sessionsFaulted); ::close(listener); return 73; }
if (accepted != MOBILEGL_OK) { pairs.reset(); LogSupervisorSummary(sessionsFaulted); ::close(listener); return 73; }
if (!serve) {
// The process IS this one session: whatever else is waiting to be paired is let go of by
// name now, not held for the session's lifetime.
pairs.reset();
if (tcpEndpoint) {
// The listener stays open until this session's data connection has arrived on
// it; RunSession closes it the moment Accept returns. The process IS the session,
@@ -1518,6 +1529,10 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
// P12 (D8): the unix `--serve` child dies with its supervisor too.
DieWithSupervisor(supervisor);
::close(listener);
// Every other waiting connection is the supervisor's: closed here without a word, or the
// supervisor's refusal of one would not end it while this session lives (TcpSupervisor's
// fork does the same with its pending peers).
if (pairs) pairs->CloseInForkedChild();
ExitSessionProcess(RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000));
}
if (child < 0) {
+55
View File
@@ -0,0 +1,55 @@
// MobileGL - MobileGL/MG_Remote/Transport/PairBind.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
// P11 PAIR. The PairBind frame (protocol.fbs), both directions of it: SocketTransport::ConnectTo
// writes one as the first frame of each of a client's two connections, and Server/PairAcceptor
// reads them and pairs the two halves by the nonce. Here, not in Handshake.h beside the DataBind
// codec, because the transport itself writes it and Handshake.h brings the logger's umbrella
// header with it (WireLog.h says why Transport keeps that out).
#pragma once
#include "../Protocol/generated/protocol_generated.h"
#include "AuthToken.h" // kDataNonceBytes
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
namespace MobileGL::MG_Remote::Transport {
// 128 bits from the client's CSPRNG, the width of Welcome.dataNonce, so the nonce comparison
// (ConstantTimeNonceMatch) is the same one.
inline constexpr std::size_t kPairNonceBytes = kDataNonceBytes;
inline std::vector<std::uint8_t> EncodePairBind(const std::uint8_t (&nonce)[kPairNonceBytes], bool aux) {
::flatbuffers::FlatBufferBuilder builder(64);
auto bind = ::MobileGL::Wire::CreatePairBind(builder, builder.CreateVector(nonce, kPairNonceBytes), aux);
auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::PairBind,
bind.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
return std::vector<std::uint8_t>(builder.GetBufferPointer(), builder.GetBufferPointer() + builder.GetSize());
}
// True when `frame` is a verifiable CtrlEnvelope carrying a PairBind whose nonce is exactly
// kPairNonceBytes; the nonce and the role are copied out. Anything else is false.
inline bool DecodePairBind(const std::vector<std::uint8_t>& frame, std::uint8_t (&nonce)[kPairNonceBytes],
bool* outAux) {
if (outAux == nullptr || frame.size() < 8 || !::MobileGL::Wire::CtrlEnvelopeBufferHasIdentifier(frame.data()))
return false;
::flatbuffers::Verifier verifier(frame.data(), frame.size());
if (!::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) return false;
const auto* bind = ::MobileGL::Wire::GetCtrlEnvelope(frame.data())->msg_as_PairBind();
if (bind == nullptr || bind->nonce() == nullptr || bind->nonce()->size() != kPairNonceBytes) return false;
std::memcpy(nonce, bind->nonce()->data(), kPairNonceBytes);
*outAux = bind->aux();
return true;
}
} // namespace MobileGL::MG_Remote::Transport
@@ -11,6 +11,7 @@
#include "AuthToken.h"
#include "Doorbell.h" // kWaitForever
#include "FdPassing.h"
#include "PairBind.h" // P11 PAIR: ConnectTo's first frame on each connection
#include "WireLog.h"
#include <cerrno>
@@ -315,6 +316,31 @@ namespace MobileGL::MG_Remote::Transport {
::usleep(5000);
}
}
// One more connection to `path` whose first bytes are `payload`, framed: a DataBind
// (ConnectDataConnection) or a PairBind (ConnectTo). `what` names the frame in the log.
MobileGLResult ConnectPresenting(const std::string& path, std::uint32_t timeoutMs, const void* payload,
std::uint64_t size, const char* what, int* outFd) {
*outFd = -1;
std::vector<std::uint8_t> framed;
const MobileGLResult appended = AppendFrame(framed, payload, size);
if (appended != MOBILEGL_OK) return appended;
int fd = -1;
const MobileGLResult connected = ConnectOne(path, timeoutMs, &fd);
if (connected != MOBILEGL_OK) return connected;
std::size_t written = 0;
while (written < framed.size()) {
const ssize_t n = ::send(fd, framed.data() + written, framed.size() - written, MSG_NOSIGNAL);
if (n > 0) { written += static_cast<std::size_t>(n); continue; }
if (n < 0 && errno == EINTR) continue;
WireLogError("MG_Remote SocketTransport: the %s could not be sent (%zu of %zu bytes): %s", what,
written, framed.size(), std::strerror(errno));
::close(fd);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
*outFd = fd;
return MOBILEGL_OK;
}
} // namespace
MobileGLResult SocketTransport::Listen(const std::string& path, int* outListenFd) {
@@ -370,68 +396,30 @@ namespace MobileGL::MG_Remote::Transport {
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 std::uint32_t budget = index == 0 ? timeoutMs : 2000;
for (;;) {
const int ready = WaitReadable(listenFd, budget);
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.
if (index != 0 || ready < 0) 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) break;
// EINTR (this process reaps SIGCHLD by construction) and ECONNABORTED
// (the peer reset between poll() and accept()) are not transport
// failures: the listener is still good. Re-poll within the budget
// rather than returning TRANSPORT_CLOSED, which the --serve
// supervisor turns into a full server exit (return 73).
if (errno == EINTR || errno == ECONNABORTED) continue;
for (int fd : accepted) {
if (fd >= 0) ::close(fd);
}
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(errno));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
::fcntl(accepted[index], F_SETFD, FD_CLOEXEC);
if (TcpSocket(accepted[index]) && !ConfigureTcp(accepted[index])) {
for (int fd : accepted) if (fd >= 0) ::close(fd);
return MOBILEGL_ERR_UNSUPPORTED;
}
}
// 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();
// P11 PAIR. Both connections present the same nonce as their first frame, each saying
// which half it is; the server pairs them by it (Server/PairAcceptor.h), so neither the
// order they arrive in nor anybody else's connection in between decides the pairing.
std::uint8_t nonce[kPairNonceBytes];
const MobileGLResult minted = MintNonce(nonce, sizeof(nonce));
if (minted != MOBILEGL_OK) return minted;
const auto controlBind = EncodePairBind(nonce, false);
int control = -1;
const MobileGLResult first = ConnectOne(path, timeoutMs, &control);
const MobileGLResult first =
ConnectPresenting(path, timeoutMs, controlBind.data(), controlBind.size(), "control connection's PairBind",
&control);
if (first != MOBILEGL_OK) {
return first;
}
const auto auxBind = EncodePairBind(nonce, true);
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);
const MobileGLResult second =
ConnectPresenting(path, 2000, auxBind.data(), auxBind.size(), "aux connection's PairBind", &aux);
if (second != MOBILEGL_OK) {
::close(control);
return second;
@@ -455,24 +443,8 @@ namespace MobileGL::MG_Remote::Transport {
MobileGLByteSpan firstFrame, int* outFd) {
if (outFd == nullptr || !TcpName(path)) return MOBILEGL_ERR_INVALID_ARGUMENT;
*outFd = -1;
std::vector<std::uint8_t> framed;
const MobileGLResult appended = AppendFrame(framed, firstFrame.data, firstFrame.size);
if (appended != MOBILEGL_OK) return appended;
int fd = -1;
const MobileGLResult connected = ConnectOne(path, timeoutMs, &fd);
if (connected != MOBILEGL_OK) return connected;
std::size_t written = 0;
while (written < framed.size()) {
const ssize_t n = ::send(fd, framed.data() + written, framed.size() - written, MSG_NOSIGNAL);
if (n > 0) { written += static_cast<std::size_t>(n); continue; }
if (n < 0 && errno == EINTR) continue;
WireLogError("MG_Remote SocketTransport: the data connection's DataBind could not be "
"sent (%zu of %zu bytes): %s", written, framed.size(), std::strerror(errno));
::close(fd);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
*outFd = fd;
return MOBILEGL_OK;
return ConnectPresenting(path, timeoutMs, firstFrame.data, firstFrame.size, "data connection's DataBind",
outFd);
}
MobileGLResult SocketTransport::AcceptOne(int listenFd, std::uint32_t timeoutMs, int* outFd) {
@@ -484,7 +456,9 @@ namespace MobileGL::MG_Remote::Transport {
if (ready < 0) return MOBILEGL_ERR_TRANSPORT_CLOSED;
const int fd = ::accept(listenFd, nullptr, nullptr);
if (fd < 0) {
// The same two non-failures AcceptPair re-polls on.
// EINTR (this process reaps SIGCHLD by construction) and ECONNABORTED (the peer
// reset between poll() and accept()) are not listener failures: re-poll rather
// than return TRANSPORT_CLOSED, which a supervisor turns into a full server exit (73).
if (errno == EINTR || errno == ECONNABORTED) continue;
const int error = errno;
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(error));
+22 -16
View File
@@ -78,10 +78,16 @@ namespace MobileGL::MG_Remote::Transport {
// 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.
// So the client connects twice to the same listening path - control, then
// aux - and each connection's first frame is a PairBind carrying one nonce
// and which half it is. One name, no extra configuration, and FdPassing
// keeps the dedicated socket it was written for.
//
// P11 PAIR: THE SERVER PAIRS THEM BY THAT NONCE, NOT BY ARRIVAL ORDER
// (Server/PairAcceptor.h). "First is control, second is aux" was a protocol
// only while nothing else could reach the listener: a readiness probe's
// lone connect() + close() became the next client's control connection, and
// two clients connecting at once could swap halves (B0-CROSS-APP.md F2).
//
// `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
@@ -92,27 +98,26 @@ namespace MobileGL::MG_Remote::Transport {
// 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`.
// The client's half: connects twice to `path`, each connection presenting a PairBind
// (the same fresh nonce; control, then aux) as its first frame. The server's half is
// Server::PairAcceptor.
static MobileGLResult ConnectTo(const std::string& path, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outClient);
// ---- PH-7 (4), ID-P7-3: a TCP data connection is BOUND, not paired --------
//
// AcceptPair/ConnectTo pair a session's two connections by ARRIVAL ORDER inside a
// window: first is control, second is data. That is a protocol only while nothing sits
// between the two processes. Over a Windows `adb forward` the two arrive reordered, the
// data connection is read as control, and every session child exits 67 waiting for a
// The server used to pair a session's two connections by ARRIVAL ORDER inside a window
// (AcceptPair): first is control, second is data. That is a protocol only while nothing
// sits between the two processes. Over a Windows `adb forward` the two arrive reordered,
// the data connection is read as control, and every session child exits 67 waiting for a
// Hello that went down the other socket - and anybody who can reach the port can race a
// connection into the gap and become somebody else's data plane. So on TCP the session
// is ONE connection until it is authenticated; the server mints `Welcome.dataNonce`,
// and the client then opens its data connection and presents the nonce as that
// connection's first frame (DataBind). The server matches the value, not the order.
// Unix endpoints keep AcceptPair: their second connection is the SCM_RIGHTS socket of a
// same-user rendezvous, not a data plane anybody else can reach.
// Unix endpoints keep their two connections from the start (the second is the SCM_RIGHTS
// socket the Welcome's descriptors cross), and since P11 PAIR they too are matched by a
// value - the client's PairBind nonce - not by order.
// Connects the control connection only on `tcp://`; identical to ConnectTo otherwise.
static MobileGLResult ConnectControl(const std::string& path, std::uint32_t timeoutMs,
@@ -122,7 +127,8 @@ namespace MobileGL::MG_Remote::Transport {
// StreamLink to.
static MobileGLResult ConnectDataConnection(const std::string& path, std::uint32_t timeoutMs,
MobileGLByteSpan firstFrame, int* outFd);
// Accepts ONE connection (TCP options applied, CLOEXEC). MOBILEGL_ERR_TIMEOUT when none.
// Accepts ONE connection (TCP options applied, CLOEXEC; EINTR and ECONNABORTED re-polled -
// the listener is still good). MOBILEGL_ERR_TIMEOUT when none.
// MOBILEGL_ERR_OUT_OF_MEMORY when accept(2) ran out of descriptors or memory (EMFILE,
// ENFILE, ENOBUFS, ENOMEM): the connection stays in the backlog and the listener is fine,
// so a caller that serves many peers can wait and try again (the TCP supervisor does);