diff --git a/MobileGL/MG_Remote/CapsCodec.cpp b/MobileGL/MG_Remote/CapsCodec.cpp index c1763569..0db7d3c3 100644 --- a/MobileGL/MG_Remote/CapsCodec.cpp +++ b/MobileGL/MG_Remote/CapsCodec.cpp @@ -476,6 +476,7 @@ namespace MobileGL::MG_Remote { inputs.ProgramArtifactsCodecVersion = MG_State::GLState::kProgramArtifactsCodecVersion; inputs.ProgramArtifactsSchema = MG_State::GLState::ProgramArtifactsSchemaFingerprint(); inputs.OpCount = static_cast(MG_Pipe::MGPWireOp::kOpCount); + inputs.ControlSchemaRevision = MOBILEGL_PROTOCOL_CONTROL_REVISION; inputs.AbiVersion = MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR); inputs.PointerBits = sizeof(void*) * 8; const Uint32 endian = 1; diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 81154114..b98fcaf4 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -777,8 +777,11 @@ namespace MobileGL::MG_Remote::Client { m_connectDial = true; const auto endpoint = tcp ? configured : configured.substr(5); std::unique_ptr socket; - const auto connected = Transport::SocketTransport::ConnectTo(endpoint, kSpawnConnectTimeoutMs, socket); + // PH-7 (4): on TCP, the control connection only. The data connection is opened after + // Welcome, with the nonce Welcome carries - never paired by arrival order. + const auto connected = Transport::SocketTransport::ConnectControl(endpoint, kSpawnConnectTimeoutMs, socket); if (connected != MOBILEGL_OK) return connected; + m_dataEndpoint = tcp ? endpoint : std::string(); const auto started = StartOverSocket(std::move(socket)); if (started == MOBILEGL_OK) { MGLOG_I("MG_Remote client: control=%s data=%s server=%s pid=%u dial=connect", @@ -890,6 +893,7 @@ namespace MobileGL::MG_Remote::Client { // every lane (CONTRACT-P6 4.4). Transport::SessionSegmentSizes negotiatedSizes; bool stream = false; + std::vector dataBind; Uint64 announced[4] = {0, 0, 0, 0}; const Uint32 replySlotCount = Transport::kDefaultReplySlotCount; { @@ -949,6 +953,19 @@ namespace MobileGL::MG_Remote::Client { return result; } stream = terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream; + if (stream) { + // PH-7 (4). A Stream Welcome without a nonce of exactly the minted width names no + // data connection this client could open; refused rather than guessed at. + const auto* nonce = welcome->dataNonce(); + if (nonce == nullptr || nonce->size() != Transport::kDataNonceBytes || m_dataEndpoint.empty()) { + const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::LinkTerms, + "stream Welcome carries no data nonce", Transport::kDataNonceBytes, + nonce == nullptr ? 0 : nonce->size()); + Stop(); + return result; + } + dataBind = EncodeDataBind(nonce->data(), nonce->size()); + } negotiatedSizes.CmdRingBytes = terms->cmdWindowBytes(); negotiatedSizes.StageBytes = terms->stageWindowBytes(); negotiatedSizes.EventRingBytes = terms->eventWindowBytes(); @@ -969,7 +986,16 @@ namespace MobileGL::MG_Remote::Client { } if (stream) { - const auto attached = AttachStreamLink(m_socketTransport->TakeDataFd(), negotiatedSizes); + int dataFd = -1; + const auto opened = Transport::SocketTransport::ConnectDataConnection( + m_dataEndpoint, kSpawnConnectTimeoutMs, MobileGLByteSpan{dataBind.data(), dataBind.size()}, &dataFd); + if (opened != MOBILEGL_OK) { + MGLOG_E("MG_Remote client: could not open the data connection to %s (rc=%d)", + m_dataEndpoint.c_str(), static_cast(opened)); + Stop(); + return opened; + } + const auto attached = AttachStreamLink(dataFd, negotiatedSizes); if (attached != MOBILEGL_OK) { Stop(); return attached; } m_controlInbox = std::make_unique(*m_transport); Server::ServerLoopInstance().SetRemoteControlSink(&RemoteControlSinkThunk, this); diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index edb5e51d..01ab3da4 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -450,6 +450,9 @@ namespace MobileGL::MG_Remote::Client { Server::LaunchedServer m_spawned; std::unique_ptr m_socketTransport; + // PH-7 (4): the `tcp://` endpoint the control connection went to, for the data + // connection opened after Welcome. Empty on every other transport. + std::string m_dataEndpoint; std::unique_ptr m_socketSelfBell; std::unique_ptr m_socketPeerBell; diff --git a/MobileGL/MG_Remote/Handshake.h b/MobileGL/MG_Remote/Handshake.h index 8e33064c..4335dfdc 100644 --- a/MobileGL/MG_Remote/Handshake.h +++ b/MobileGL/MG_Remote/Handshake.h @@ -6,8 +6,10 @@ #include "Transport/ITransport.h" #include #include +#include #include #include +#include namespace MobileGL::MG_Remote { inline MobileGLResult RefuseHandshake(Transport::ITransport& transport, @@ -92,4 +94,35 @@ namespace MobileGL::MG_Remote { BuildFingerprint(), stamp == nullptr ? "" : stamp); return MOBILEGL_OK; } + + // PH-7 (4), ID-P7-3. THE DATA CONNECTION'S FIRST FRAME, both directions of it. + // + // A TCP data connection is opened after Welcome and names its session by handing back + // `Welcome.dataNonce` in a DataBind, framed exactly like a control message so the server can + // tell it from a Hello with one read. The nonce is COMPARED in exactly one place, + // ServerSession::BindDataConnection, in constant time; these two only build and parse. + inline std::vector EncodeDataBind(const std::uint8_t* nonce, std::size_t size) { + ::flatbuffers::FlatBufferBuilder builder(64); + auto bind = ::MobileGL::Wire::CreateDataBind(builder, builder.CreateVector(nonce, size)); + auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::DataBind, + bind.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + return std::vector(builder.GetBufferPointer(), + builder.GetBufferPointer() + builder.GetSize()); + } + + // True when `frame` is a verifiable CtrlEnvelope carrying a DataBind whose nonce is exactly + // kDataNonceBytes; the nonce is copied out. Anything else - a Hello, a different schema, a + // nonce of another width - is false, and the caller refuses the connection by name. + inline bool DecodeDataBind(const std::vector& frame, + std::uint8_t (&nonce)[Transport::kDataNonceBytes]) { + if (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_DataBind(); + if (bind == nullptr || bind->nonce() == nullptr || bind->nonce()->size() != Transport::kDataNonceBytes) + return false; + std::memcpy(nonce, bind->nonce()->data(), Transport::kDataNonceBytes); + return true; + } } // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index 86e387a2..296ac651 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -47,6 +47,9 @@ struct HelloBuilder; struct Welcome; struct WelcomeBuilder; +struct DataBind; +struct DataBindBuilder; + struct CapsSnapshot; struct CapsSnapshotBuilder; @@ -502,11 +505,12 @@ enum class CtrlMsg : uint8_t { LogLine = 10, Refuse = 11, LogFlush = 12, + DataBind = 13, MIN = NONE, - MAX = LogFlush + MAX = DataBind }; -inline const CtrlMsg (&EnumValuesCtrlMsg())[13] { +inline const CtrlMsg (&EnumValuesCtrlMsg())[14] { static const CtrlMsg values[] = { CtrlMsg::NONE, CtrlMsg::Hello, @@ -520,13 +524,14 @@ inline const CtrlMsg (&EnumValuesCtrlMsg())[13] { CtrlMsg::Fatal, CtrlMsg::LogLine, CtrlMsg::Refuse, - CtrlMsg::LogFlush + CtrlMsg::LogFlush, + CtrlMsg::DataBind }; return values; } inline const char * const *EnumNamesCtrlMsg() { - static const char * const names[14] = { + static const char * const names[15] = { "NONE", "Hello", "Welcome", @@ -540,13 +545,14 @@ inline const char * const *EnumNamesCtrlMsg() { "LogLine", "Refuse", "LogFlush", + "DataBind", nullptr }; return names; } inline const char *EnumNameCtrlMsg(CtrlMsg e) { - if (::flatbuffers::IsOutRange(e, CtrlMsg::NONE, CtrlMsg::LogFlush)) return ""; + if (::flatbuffers::IsOutRange(e, CtrlMsg::NONE, CtrlMsg::DataBind)) return ""; const size_t index = static_cast(e); return EnumNamesCtrlMsg()[index]; } @@ -603,6 +609,10 @@ template<> struct CtrlMsgTraits { static const CtrlMsg enum_value = CtrlMsg::LogFlush; }; +template<> struct CtrlMsgTraits { + static const CtrlMsg enum_value = CtrlMsg::DataBind; +}; + template bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate &verifier, const void *obj, CtrlMsg type); template @@ -1164,7 +1174,8 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_ABIFINGERPRINT = 20, VT_WIREFINGERPRINT = 22, VT_LINKTERMS = 24, - VT_BACKENDTYPE = 26 + VT_BACKENDTYPE = 26, + VT_DATANONCE = 28 }; uint32_t abiMajor() const { return GetField(VT_ABIMAJOR, 0); @@ -1202,6 +1213,9 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint32_t backendType() const { return GetField(VT_BACKENDTYPE, 0); } + const ::flatbuffers::Vector *dataNonce() const { + return GetPointer *>(VT_DATANONCE); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1223,6 +1237,8 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyOffset(verifier, VT_LINKTERMS) && verifier.VerifyTable(linkTerms()) && VerifyField(verifier, VT_BACKENDTYPE, 4) && + VerifyOffset(verifier, VT_DATANONCE) && + verifier.VerifyVector(dataNonce()) && verifier.EndTable(); } }; @@ -1267,6 +1283,9 @@ struct WelcomeBuilder { void add_backendType(uint32_t backendType) { fbb_.AddElement(Welcome::VT_BACKENDTYPE, backendType, 0); } + void add_dataNonce(::flatbuffers::Offset<::flatbuffers::Vector> dataNonce) { + fbb_.AddOffset(Welcome::VT_DATANONCE, dataNonce); + } explicit WelcomeBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -1291,10 +1310,12 @@ inline ::flatbuffers::Offset CreateWelcome( uint64_t abiFingerprint = 0, uint64_t wireFingerprint = 0, ::flatbuffers::Offset linkTerms = 0, - uint32_t backendType = 0) { + uint32_t backendType = 0, + ::flatbuffers::Offset<::flatbuffers::Vector> dataNonce = 0) { WelcomeBuilder builder_(_fbb); builder_.add_wireFingerprint(wireFingerprint); builder_.add_abiFingerprint(abiFingerprint); + builder_.add_dataNonce(dataNonce); builder_.add_backendType(backendType); builder_.add_linkTerms(linkTerms); builder_.add_buildFingerprint(buildFingerprint); @@ -1326,8 +1347,10 @@ inline ::flatbuffers::Offset CreateWelcomeDirect( uint64_t abiFingerprint = 0, uint64_t wireFingerprint = 0, ::flatbuffers::Offset linkTerms = 0, - uint32_t backendType = 0) { + uint32_t backendType = 0, + const std::vector *dataNonce = nullptr) { auto buildFingerprint__ = buildFingerprint ? _fbb.CreateString(buildFingerprint) : 0; + auto dataNonce__ = dataNonce ? _fbb.CreateVector(*dataNonce) : 0; return MobileGL::Wire::CreateWelcome( _fbb, abiMajor, @@ -1341,7 +1364,66 @@ inline ::flatbuffers::Offset CreateWelcomeDirect( abiFingerprint, wireFingerprint, linkTerms, - backendType); + backendType, + dataNonce__); +} + +struct DataBind FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef DataBindBuilder Builder; + struct Traits; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NONCE = 4 + }; + const ::flatbuffers::Vector *nonce() const { + return GetPointer *>(VT_NONCE); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NONCE) && + verifier.VerifyVector(nonce()) && + verifier.EndTable(); + } +}; + +struct DataBindBuilder { + typedef DataBind Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_nonce(::flatbuffers::Offset<::flatbuffers::Vector> nonce) { + fbb_.AddOffset(DataBind::VT_NONCE, nonce); + } + explicit DataBindBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateDataBind( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::Vector> nonce = 0) { + DataBindBuilder builder_(_fbb); + builder_.add_nonce(nonce); + return builder_.Finish(); +} + +struct DataBind::Traits { + using type = DataBind; + static auto constexpr Create = CreateDataBind; +}; + +inline ::flatbuffers::Offset CreateDataBindDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const std::vector *nonce = nullptr) { + auto nonce__ = nonce ? _fbb.CreateVector(*nonce) : 0; + return MobileGL::Wire::CreateDataBind( + _fbb, + nonce__); } struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { @@ -2188,6 +2270,9 @@ struct CtrlEnvelope FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const MobileGL::Wire::LogFlush *msg_as_LogFlush() const { return msg_type() == MobileGL::Wire::CtrlMsg::LogFlush ? static_cast(msg()) : nullptr; } + const MobileGL::Wire::DataBind *msg_as_DataBind() const { + return msg_type() == MobileGL::Wire::CtrlMsg::DataBind ? static_cast(msg()) : nullptr; + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -2246,6 +2331,10 @@ template<> inline const MobileGL::Wire::LogFlush *CtrlEnvelope::msg_as inline const MobileGL::Wire::DataBind *CtrlEnvelope::msg_as() const { + return msg_as_DataBind(); +} + struct CtrlEnvelopeBuilder { typedef CtrlEnvelope Table; ::flatbuffers::FlatBufferBuilder &fbb_; @@ -2336,6 +2425,10 @@ inline bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate &verifier, const vo auto ptr = reinterpret_cast(obj); return verifier.VerifyTable(ptr); } + case CtrlMsg::DataBind: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } default: return true; } } diff --git a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h index b3555c31..e790d040 100644 --- a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h +++ b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h @@ -51,6 +51,14 @@ extern "C" { #define MOBILEGL_PROTOCOL_ABI_MAJOR 1 #define MOBILEGL_PROTOCOL_ABI_MINOR 0 +// The CONTROL schema's revision (protocol.fbs), mixed into wireFingerprint. The fingerprint's +// other inputs are struct sizes and codec digests of the DATA plane; nothing moved it when a +// control message changed shape, so a peer built before a control-schema change agreed with one +// built after it and the difference surfaced as whatever the first misread field did. Bump this +// with every protocol.fbs change a peer must agree on. +// 1 P7 wave 2-F, PH-7 (4): Welcome.dataNonce and the DataBind message. +#define MOBILEGL_PROTOCOL_CONTROL_REVISION 1 + #define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor)) #define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16) #define MOBILEGL_ABI_MINOR_OF(version) ((uint32_t)(version) & 0xFFFFu) diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index b37bd6c7..6c0f80a5 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -126,6 +126,20 @@ table Welcome { wireFingerprint: ulong; linkTerms: LinkTerms; backendType: uint; + // PH-7 (4), ID-P7-3. 128 bits from the server's CSPRNG, minted after the Hello authenticated + // and only on a Stream data plane. The client opens its data connection AFTER this Welcome + // and presents the value as that connection's first frame (DataBind); the server binds the + // connection to this session by the value, not by which connection arrived second. Absent on + // SharedSegments, whose descriptors travel over the control connection's own aux socket. + dataNonce: [ubyte]; +} + +// PH-7 (4). The FIRST and only control-framed message on a TCP data connection: the Welcome's +// dataNonce, handed back. Everything after it on that connection is StreamLink's own framing. +// A connection whose first frame is not this, or whose nonce is not the live session's, is +// refused by name (`Refuse{Authentication}`) and never becomes a data plane. +table DataBind { + nonce: [ubyte]; } // --------------------------------------------------------------------------- @@ -348,6 +362,7 @@ union CtrlMsg { LogLine, Refuse, LogFlush, + DataBind, } table CtrlEnvelope { diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index 611d58bc..76cd7674 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -1,6 +1,7 @@ // MobileGL - separate-process server and TCP session supervisor. // SPDX-License-Identifier: LGPL-3.0-only #include "../Transport/Doorbell.h" +#include "../Transport/FdPassing.h" #include "../Transport/SocketTransport.h" #include "../Transport/WireLog.h" #include "../Protocol/SurfaceOpCodec.h" @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -59,6 +63,103 @@ void RefuseBusy(SocketTransport& transport, const char* detail) { Refuse(transport, Protocol::RefuseCode::Busy, detail); } +// PH-7 (4), ID-P7-3. THE DATA CONNECTION IS ROUTED BY WHAT IT SAYS, NOT BY WHEN IT CAME. +// +// On TCP the supervisor accepts ONE connection at a time. While no session is live, a +// connection is a session's control connection and goes to a fresh child. While one is, the +// connection's first frame decides: a DataBind is the live session's data connection and its +// descriptor goes to that child over the hand-off socketpair, with the nonce it presented as +// the sideband; anything else is a second client and is refused Busy exactly as before. The +// supervisor never judges the nonce - ServerSession::BindDataConnection compares it, once, in +// the child that minted it. This replaces AcceptPair's rule that the second connection to +// arrive inside the window IS the data plane: over a Windows `adb forward` the two arrived +// reordered, the data connection was read as control, and every child exited 67. +constexpr std::uint32_t kFirstFrameWaitMs = 2000; +constexpr std::uint64_t kFirstFrameMaxBytes = 1024 * 1024; + +void RouteWhileBusy(SocketTransport& connection, int handoff) { + std::vector frame; + const int fd = connection.StreamFd(); + const auto read = SocketTransport::ReceiveOneFrame(fd, kFirstFrameWaitMs, kFirstFrameMaxBytes, &frame); + std::uint8_t nonce[kDataNonceBytes] = {}; + if (read == MOBILEGL_OK && DecodeDataBind(frame, nonce)) { + if (handoff >= 0 && FdPassing::SendFd(handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)}) == MOBILEGL_OK) { + // The child holds its own descriptor for this connection now; ours goes without a + // shutdown(2), which would disconnect the child's too. + connection.CloseLocalCopy(); + return; + } + Refuse(connection, Protocol::RefuseCode::Authentication, + "data connection could not be handed to the live session"); + return; + } + // The frame (if any) is consumed, so closing cannot RST the refusal away - the reason + // RefuseBusy reads the Hello first. + Refuse(connection, Protocol::RefuseCode::Busy, "one session is already active"); +} + +// True when the control peer has closed (EOF or error); false while it is open, readable or not. +bool ControlPeerGone(int controlFd) { + char byte = 0; + const auto peeked = ::recv(controlFd, &byte, 1, MSG_PEEK | MSG_DONTWAIT); + return peeked == 0 || (peeked < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR); +} + +// The session child's source: descriptors the supervisor forwarded, each with the nonce its +// DataBind presented. Watching the control connection too, so a client that went away after +// Welcome ends the wait at once instead of holding the supervisor Busy for the whole deadline. +Server::ServerSession::DataConnectionSource HandoffSource(int handoff, int controlFd) { + return [handoff, controlFd](std::uint32_t timeoutMs, int* outFd, std::uint8_t* outNonce) -> MobileGLResult { + pollfd fds[2] = {{handoff, POLLIN, 0}, {controlFd, POLLIN, 0}}; + // A control connection with bytes waiting is not a hang-up and must not spin this loop: + // after the first look it is only asked again once per slice. + const int rc = ::poll(fds, 2, static_cast(std::min(timeoutMs, 50))); + if (rc < 0) return errno == EINTR ? MOBILEGL_ERR_TIMEOUT : MOBILEGL_ERR_TRANSPORT_CLOSED; + if ((fds[1].revents & (POLLIN | POLLHUP | POLLERR)) != 0 && ControlPeerGone(controlFd)) + return MOBILEGL_ERR_TRANSPORT_CLOSED; + if ((fds[0].revents & (POLLIN | POLLHUP | POLLERR)) == 0) { + if ((fds[1].revents & POLLIN) != 0) { + pollfd only{handoff, POLLIN, 0}; + if (::poll(&only, 1, static_cast(std::min(timeoutMs, 50))) <= 0) + return MOBILEGL_ERR_TIMEOUT; + } else { + return MOBILEGL_ERR_TIMEOUT; + } + } + std::uint8_t sideband[FdPassing::kMaxSidebandBytes] = {}; + std::uint64_t size = 0; + const auto received = FdPassing::ReceiveFd(handoff, outFd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, + &size, 0); + if (received != MOBILEGL_OK) return received; + // The supervisor always sends exactly the presented nonce; a short sideband compares as + // zeros, which no minted nonce is going to equal by accident. + std::memcpy(outNonce, sideband, kDataNonceBytes); + return MOBILEGL_OK; + }; +} + +// The single-session (no --serve) source: this process still owns the listener, so it accepts +// the data connection itself and reads its DataBind the same way the supervisor would. +Server::ServerSession::DataConnectionSource ListenerSource(int listener, int controlFd) { + return [listener, controlFd](std::uint32_t timeoutMs, int* outFd, std::uint8_t* outNonce) -> MobileGLResult { + if (ControlPeerGone(controlFd)) return MOBILEGL_ERR_TRANSPORT_CLOSED; + int fd = -1; + const auto accepted = SocketTransport::AcceptOne(listener, std::min(timeoutMs, 250), &fd); + if (accepted != MOBILEGL_OK) return accepted; + std::vector frame; + std::uint8_t presented[kDataNonceBytes] = {}; + const auto read = SocketTransport::ReceiveOneFrame(fd, kFirstFrameWaitMs, kFirstFrameMaxBytes, &frame); + if (read != MOBILEGL_OK || !DecodeDataBind(frame, presented)) { + SocketTransport stray(fd, -1, TransportRole::Server); + Refuse(stray, Protocol::RefuseCode::Authentication, "data connection's first frame is not a DataBind"); + return MOBILEGL_ERR_TIMEOUT; + } + std::memcpy(outNonce, presented, kDataNonceBytes); + *outFd = fd; + return MOBILEGL_OK; + }; +} + struct FlushAck { ITransport* transport; std::uint64_t seq; }; void SendLogAck(void* pointer) { auto& ack = *static_cast(pointer); @@ -70,7 +171,10 @@ void SendLogAck(void* pointer) { } // Called only after fork: the supervisor never creates a backend or EGL context. -[[noreturn]] void RunSession(std::unique_ptr control) { +// `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. +[[noreturn]] void RunSession(std::unique_ptr control, + Server::ServerSession::DataConnectionSource dataSource) { const int selfPid = static_cast(::getpid()); const bool tcp = control->IsTcp(); std::vector helloFrame; @@ -81,6 +185,15 @@ void SendLogAck(void* pointer) { if (control->ReceiveFrame({helloFrame.data(), helloFrame.size()}, &bytes, 0) != MOBILEGL_OK) ::_exit(67); flatbuffers::Verifier verifier(helloFrame.data(), helloFrame.size()); if (!Protocol::VerifyCtrlEnvelopeBuffer(verifier)) ::_exit(67); + // PH-7 (4). A DataBind with no live session to join: the supervisor routes a data connection + // to a child only while one is live, so this is a stale connection (its session already + // ended) or one that never had a session. Refused by name, and the child is a clean exit - + // it is a refusal, not a fault. + if (Protocol::GetCtrlEnvelope(helloFrame.data())->msg_type() == Protocol::CtrlMsg::DataBind) { + Refuse(*control, Protocol::RefuseCode::Authentication, "data connection names no live session"); + std::fflush(nullptr); + ::_exit(0); + } const auto* hello = Protocol::GetCtrlEnvelope(helloFrame.data())->msg_as_Hello(); if (!hello) ::_exit(67); // Reject incompatible wire before backend bring-up can obscure the cause. @@ -122,11 +235,14 @@ void SendLogAck(void* pointer) { peerBell = std::make_unique(-1, clientBell[1], 1, true); session.SetExternalDoorbells(selfBell.get(), peerBell.get()); } + if (tcp) session.SetDataConnectionSource(std::move(dataSource)); const auto accepted = session.Accept(*control, &helloFrame); if (accepted != MOBILEGL_OK) { WireLogError("MG_Remote server: pid=%d Accept failed (rc=%d)", selfPid, static_cast(accepted)); std::fflush(nullptr); - ::_exit(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH ? 0 : 67); + // A refusal and a peer that went away before its data connection bound (PH-7 (4): the + // client closed after Welcome) are outcomes, not faults; the supervisor counts only 67. + ::_exit(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH || accepted == MOBILEGL_ERR_TRANSPORT_CLOSED ? 0 : 67); } if (!tcp) { @@ -358,34 +474,74 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a WireLogError("MG_Remote server: pid=%d listening on %s", static_cast(::getpid()), endpoint.c_str()); pid_t active = -1; unsigned long sessionsFaulted = 0; + const bool tcpEndpoint = endpoint.compare(0, 6, "tcp://") == 0; + // The supervisor's end of the live child's data-connection hand-off (PH-7 (4)); -1 while no + // session is live. It goes with the child: a reaped session can be handed nothing. + int handoff = -1; + const auto reap = [&]() { + const bool reaped = ReapActiveSession(active, sessionsFaulted); + if (active <= 0 && handoff >= 0) { ::close(handoff); handoff = -1; } + return reaped; + }; for (;;) { - ReapActiveSession(active, sessionsFaulted); + reap(); std::unique_ptr control; - const auto accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control); + MobileGLResult accepted = MOBILEGL_ERR_UNSUPPORTED; + if (tcpEndpoint) { + int fd = -1; + accepted = SocketTransport::AcceptOne(listener, serve ? 250 : 30000, &fd); + if (accepted == MOBILEGL_OK) control = std::make_unique(fd, -1, TransportRole::Server); + } else { + accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control); + } if (accepted == MOBILEGL_ERR_TIMEOUT && serve) continue; if (accepted != MOBILEGL_OK) { LogSupervisorSummary(sessionsFaulted); ::close(listener); return 73; } if (!serve) { + if (tcpEndpoint) { + // The listener stays open: this session's data connection has yet to arrive on it. + const int controlFd = control->StreamFd(); + RunSession(std::move(control), ListenerSource(listener, controlFd)); + } ::close(listener); - if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str()); - RunSession(std::move(control)); + if (endpoint[0] != '@') ::unlink(endpoint.c_str()); + RunSession(std::move(control), {}); } - ReapActiveSession(active, sessionsFaulted); + reap(); // exit_group closes files just before waitpid can observe the exit. // Allow that small scheduling window; a live peer remains Busy. const auto reapDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(50); while (active > 0 && std::chrono::steady_clock::now() < reapDeadline) { - if (ReapActiveSession(active, sessionsFaulted)) break; + if (reap()) break; ::usleep(1000); } if (active > 0) { - RefuseBusy(*control, "one session is already active"); + if (tcpEndpoint) RouteWhileBusy(*control, handoff); + else RefuseBusy(*control, "one session is already active"); + continue; + } + int pair[2] = {-1, -1}; + if (tcpEndpoint && FdPassing::CreateSocketPair(pair) != MOBILEGL_OK) { + RefuseBusy(*control, "could not create the data-connection hand-off"); continue; } std::fflush(nullptr); const pid_t child = ::fork(); - if (child == 0) { ::close(listener); RunSession(std::move(control)); } - if (child < 0) { RefuseBusy(*control, "could not create session child"); continue; } + if (child == 0) { + ::close(listener); + if (pair[0] >= 0) ::close(pair[0]); + const int controlFd = control->StreamFd(); + RunSession(std::move(control), tcpEndpoint ? HandoffSource(pair[1], controlFd) + : Server::ServerSession::DataConnectionSource{}); + } + if (child < 0) { + if (pair[0] >= 0) ::close(pair[0]); + if (pair[1] >= 0) ::close(pair[1]); + RefuseBusy(*control, "could not create session child"); + continue; + } active = child; + if (pair[1] >= 0) ::close(pair[1]); + handoff = pair[0]; control->CloseLocalCopy(); } LogSupervisorSummary(sessionsFaulted); diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 50a76f49..b5f3c444 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -9,6 +9,7 @@ // P5 package s1: the server half of a session. #include "../Transport/SocketTransport.h" +#include "../Transport/StreamLink.h" #include "ServerSession.h" #include @@ -558,9 +559,26 @@ namespace MobileGL::MG_Remote::Server { // ---- 3. the four segments, both control pages, the rings. MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED; + // PH-7 (4), ID-P7-3. Minted HERE - after the token was checked, before a byte of Welcome + // exists - and bound after Welcome by BindDataConnection. The link is attached without + // its descriptor: Welcome has to announce the sizes of memory the connection it names + // will fill. + Uint8 dataNonce[Transport::kDataNonceBytes] = {}; + Transport::StreamLink* pendingStream = nullptr; if (stream) { auto* socket = dynamic_cast(&transport); - if (socket && socket->IsTcp()) created = AttachStreamLink(socket->TakeDataFd(), m_sizes); + if (socket == nullptr || !socket->IsTcp() || !m_dataSource) + return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms, + "stream needs a TCP control connection and a data-connection source"); + created = Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce)); + if (created == MOBILEGL_OK) { + auto link = std::make_unique(); + created = link->AttachOwnedDeferred(m_sizes, Transport::TransportRoleTag::ServerConsumer); + if (created == MOBILEGL_OK) { + pendingStream = link.get(); + AttachDataLink(std::move(link)); + } + } } else { created = m_link->Memory().Create(m_sizes, Transport::MemoryRole::Server); } @@ -597,6 +615,9 @@ namespace MobileGL::MG_Remote::Server { auto reply = segmentRef(3, ::MobileGL::Wire::SegmentKind::Reply, Slot::Reply); auto event = segmentRef(4, ::MobileGL::Wire::SegmentKind::Event, Slot::Event); auto stamp = builder.CreateString(BuildFingerprint()); + const auto nonce = pendingStream != nullptr + ? builder.CreateVector(dataNonce, sizeof(dataNonce)) + : ::flatbuffers::Offset<::flatbuffers::Vector>(); const Uint64 maxReply = m_sizes.ReplyBytes / m_sizes.ReplySlotCount - sizeof(Transport::ReplySlotHeader); auto negotiated = ::MobileGL::Wire::CreateLinkTerms(builder, terms->dataPlane(), ::MobileGL::Wire::WireForm::StructImage, maxReply, m_link->Memory().CmdRingCapacity(), @@ -612,7 +633,8 @@ namespace MobileGL::MG_Remote::Server { // equal by construction. static_cast(SelfProcessId()), cmd, stage, reply, event, stamp, ourFingerprint, ourFingerprint, negotiated, - m_backend == nullptr ? hello->backendType() : static_cast(m_backend->GetBackendType())); + m_backend == nullptr ? hello->backendType() : static_cast(m_backend->GetBackendType()), + nonce); auto root = ::MobileGL::Wire::CreateCtrlEnvelope( builder, ::MobileGL::Wire::CtrlMsg::Welcome, welcome.Union()); ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root); @@ -622,6 +644,13 @@ namespace MobileGL::MG_Remote::Server { return sent; } } + if (pendingStream != nullptr) { + const MobileGLResult bound = BindDataConnection(transport, *pendingStream, dataNonce); + if (bound != MOBILEGL_OK) { + Close(); + return bound; + } + } // ---- 5. the segment table and the ONE process-wide resolver. // @@ -732,6 +761,52 @@ namespace MobileGL::MG_Remote::Server { return SendEnvelope(*m_transport, builder); } + // PH-7 (4), ID-P7-3. THE ONE PLACE A DATA CONNECTION IS BOUND TO A SESSION. + // + // Every connection the source offers is compared against this session's nonce, in constant + // time. A match becomes the Stream data plane. A mismatch is refused BY NAME - to the log, + // and to the peer on that same connection - and the wait goes on, so a stale connection + // left over from an earlier session, or one raced in by somebody who can reach the port, + // cannot end the session it failed to join. What ends the wait is the deadline or the + // control peer going away; the deadline is refused on the control connection, where the + // client is listening. + MobileGLResult ServerSession::BindDataConnection(Transport::ITransport& control, + Transport::StreamLink& link, const Uint8* nonce) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kHandshakeTimeoutMs); + Uint32 refused = 0; + for (;;) { + const auto left = std::chrono::duration_cast( + deadline - std::chrono::steady_clock::now()).count(); + if (left <= 0) { + MGLOG_E("MG_Remote server: no data connection presented this session's nonce within " + "%u ms (%u refused)", kHandshakeTimeoutMs, refused); + return RefuseHandshake(control, ::MobileGL::Wire::RefuseCode::Authentication, + "no data connection presented the session nonce"); + } + int fd = -1; + Uint8 presented[Transport::kDataNonceBytes] = {}; + const MobileGLResult offered = m_dataSource(static_cast(left), &fd, presented); + if (offered == MOBILEGL_ERR_TIMEOUT) continue; + if (offered != MOBILEGL_OK) { + MGLOG_E("MG_Remote server: waiting for the data connection ended (rc=%d, %u refused)", + static_cast(offered), refused); + return offered; + } + if (Transport::ConstantTimeNonceMatch(nonce, presented)) { + const MobileGLResult bound = link.BindDataFd(fd); +#if !defined(_WIN32) + if (bound != MOBILEGL_OK) ::close(fd); +#endif + return bound; + } + ++refused; + // The stray's transport owns `fd` and closes it on the way out of this scope. + Transport::SocketTransport stray(fd, -1, Transport::TransportRole::Server); + (void)RefuseHandshake(stray, ::MobileGL::Wire::RefuseCode::Authentication, + "data connection nonce mismatch"); + } + } + MobileGLResult ServerSession::AttachStreamLink(int fd, const Transport::SessionSegmentSizes& sizes) { std::unique_ptr link; const auto attached = Transport::CreateStreamLink(fd, sizes, Transport::TransportRoleTag::ServerConsumer, link); diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index 777b0842..715de479 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -42,6 +42,8 @@ #pragma once #include "../Transport/ILink.h" #include +#include +#include #include #include @@ -56,6 +58,10 @@ #include "../Wire/PipeWireCodec.h" #include "PipeApplier.h" +namespace MobileGL::MG_Remote::Transport { + class StreamLink; +} + namespace MobileGL::MG_Remote::Server { class ServerSession { @@ -76,6 +82,20 @@ namespace MobileGL::MG_Remote::Server { MobileGLResult Accept(Transport::ITransport& transport, const std::vector* firstFrame = nullptr); + // PH-7 (4), ID-P7-3. WHERE A TCP SESSION'S DATA CONNECTION COMES FROM. + // + // A Stream data plane is not handed to Accept with the control connection any more: the + // client opens it after Welcome and names this session by presenting Welcome.dataNonce + // as its first frame (DataBind). Whoever owns the listener - ServerMain's supervisor, + // which forwards the descriptor over a socketpair, or the single-session loop, which + // accepts it directly - reads that frame and offers the connection here with the nonce + // it presented. Accept compares, binds the one that matches, and refuses every other by + // name. MOBILEGL_ERR_TIMEOUT from the source means "nothing yet"; anything else ends + // the wait (TRANSPORT_CLOSED when the control peer went away while it waited). + using DataConnectionSource = + std::function; + void SetDataConnectionSource(DataConnectionSource source) { m_dataSource = std::move(source); } + // Re-publishes the whole snapshot. R-12: a SECOND arrival IS the invalidation signal, // which is how DirectGLES - which has no OnCapsInvalidated producer - tells the client // its InitCapabilities re-ran, without any dev-shaped backend edit. It is also the @@ -207,6 +227,10 @@ namespace MobileGL::MG_Remote::Server { void LogMemory(const char* phase) const; private: + MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::StreamLink& link, + const Uint8* nonce); + + DataConnectionSource m_dataSource; std::unique_ptr m_link; // sm: non-owning; the spawn entry point owns the SocketDoorbells. Transport::Doorbell* m_externalConsumerBell = nullptr; diff --git a/MobileGL/MG_Remote/Transport/AuthToken.h b/MobileGL/MG_Remote/Transport/AuthToken.h index 301daa92..cb594f9e 100644 --- a/MobileGL/MG_Remote/Transport/AuthToken.h +++ b/MobileGL/MG_Remote/Transport/AuthToken.h @@ -87,4 +87,19 @@ namespace MobileGL::MG_Remote::Transport { return difference == 0; } + // PH-7 (4), ID-P7-3. The width of `Welcome.dataNonce`: 128 bits from the CSPRNG, the same + // floor as the token for the same reason. + inline constexpr std::size_t kDataNonceBytes = 16; + + // The nonce comparison. Both operands are exactly kDataNonceBytes by the time this is asked + // (a presented nonce of any other length is refused before it gets here), so the loop has + // one length and no early exit. + inline bool ConstantTimeNonceMatch(const unsigned char* expected, const unsigned char* presented) { + if (expected == nullptr || presented == nullptr) return false; + unsigned difference = 0; + for (std::size_t index = 0; index < kDataNonceBytes; ++index) + difference |= static_cast(expected[index] ^ presented[index]); + return difference == 0; + } + } // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Transport/Ring.cpp b/MobileGL/MG_Remote/Transport/Ring.cpp index c7aaa76f..d9f52243 100644 --- a/MobileGL/MG_Remote/Transport/Ring.cpp +++ b/MobileGL/MG_Remote/Transport/Ring.cpp @@ -696,6 +696,7 @@ namespace MobileGL::MG_Remote::Transport { mix(inputs.ProgramArtifactsCodecVersion); mix(inputs.ProgramArtifactsSchema); mix(inputs.OpCount); + mix(inputs.ControlSchemaRevision); mix(inputs.AbiVersion); mix(inputs.PointerBits); mix(inputs.LittleEndian); diff --git a/MobileGL/MG_Remote/Transport/SessionRings.h b/MobileGL/MG_Remote/Transport/SessionRings.h index 68bf4db1..970ccbb5 100644 --- a/MobileGL/MG_Remote/Transport/SessionRings.h +++ b/MobileGL/MG_Remote/Transport/SessionRings.h @@ -573,6 +573,8 @@ namespace MobileGL::MG_Remote::Transport { std::uint64_t ProgramArtifactsCodecVersion = 0; std::uint64_t ProgramArtifactsSchema = 0; std::uint64_t OpCount = 0; + // MOBILEGL_PROTOCOL_CONTROL_REVISION (mg_protocol_base.h): the control schema's shape. + std::uint64_t ControlSchemaRevision = 0; std::uint32_t AbiVersion = 0; std::uint32_t PointerBits = 0; std::uint32_t LittleEndian = 0; diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.cpp b/MobileGL/MG_Remote/Transport/SocketTransport.cpp index 3a589d39..6995c06f 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.cpp +++ b/MobileGL/MG_Remote/Transport/SocketTransport.cpp @@ -34,6 +34,9 @@ # include # include # include +# if defined(__linux__) +# include +# endif #else # define MOBILEGL_SOCKET_TRANSPORT_POSIX 0 #endif @@ -437,6 +440,126 @@ namespace MobileGL::MG_Remote::Transport { return MOBILEGL_OK; } + MobileGLResult SocketTransport::ConnectControl(const std::string& path, std::uint32_t timeoutMs, + std::unique_ptr& outClient) { + if (!TcpName(path)) return ConnectTo(path, timeoutMs, outClient); + outClient.reset(); + int control = -1; + const MobileGLResult connected = ConnectOne(path, timeoutMs, &control); + if (connected != MOBILEGL_OK) return connected; + outClient = std::make_unique(control, -1, TransportRole::Client); + return MOBILEGL_OK; + } + + MobileGLResult SocketTransport::ConnectDataConnection(const std::string& path, std::uint32_t timeoutMs, + MobileGLByteSpan firstFrame, int* outFd) { + if (outFd == nullptr || !TcpName(path)) return MOBILEGL_ERR_INVALID_ARGUMENT; + *outFd = -1; + std::vector 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(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; + } + + MobileGLResult SocketTransport::AcceptOne(int listenFd, std::uint32_t timeoutMs, int* outFd) { + if (listenFd < 0 || outFd == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT; + *outFd = -1; + for (;;) { + const int ready = WaitReadable(listenFd, timeoutMs); + if (ready == 0) return MOBILEGL_ERR_TIMEOUT; + 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. + if (errno == EINTR || errno == ECONNABORTED) continue; + WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(errno)); + return MOBILEGL_ERR_TRANSPORT_CLOSED; + } + ::fcntl(fd, F_SETFD, FD_CLOEXEC); + if (TcpSocket(fd) && !ConfigureTcp(fd)) { + ::close(fd); + continue; + } + *outFd = fd; + return MOBILEGL_OK; + } + } + + MobileGLResult SocketTransport::ReceiveOneFrame(int fd, std::uint32_t timeoutMs, std::uint64_t maxBytes, + std::vector* outPayload) { + if (fd < 0 || outPayload == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT; + outPayload->clear(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + const auto readExactly = [&](std::uint8_t* into, std::size_t size) -> MobileGLResult { + while (size != 0) { + const auto left = std::chrono::duration_cast( + deadline - std::chrono::steady_clock::now()).count(); + if (left <= 0) return MOBILEGL_ERR_TIMEOUT; + const int ready = WaitReadable(fd, static_cast(left)); + if (ready == 0) return MOBILEGL_ERR_TIMEOUT; + if (ready < 0) return MOBILEGL_ERR_TRANSPORT_CLOSED; + const ssize_t n = ::recv(fd, into, size, 0); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return MOBILEGL_ERR_TRANSPORT_CLOSED; + into += n; + size -= static_cast(n); + } + return MOBILEGL_OK; + }; + std::uint8_t header[kFrameHeaderSize]; + const MobileGLResult headerRead = readExactly(header, sizeof(header)); + if (headerRead != MOBILEGL_OK) return headerRead; + std::uint32_t magic = 0, length = 0; + std::memcpy(&magic, header, sizeof(magic)); + std::memcpy(&length, header + 4, sizeof(length)); + if (magic != kFrameMagic || length > maxBytes) return MOBILEGL_ERR_PROTOCOL_MISMATCH; + outPayload->resize(length); + const MobileGLResult payloadRead = readExactly(outPayload->data(), length); + if (payloadRead != MOBILEGL_OK) outPayload->clear(); + return payloadRead; + } + + MobileGLResult SocketTransport::MintNonce(std::uint8_t* out, std::size_t size) { + if (out == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT; + std::size_t filled = 0; +#if defined(__linux__) && defined(SYS_getrandom) + while (filled < size) { + const long n = ::syscall(SYS_getrandom, out + filled, size - filled, 0); + if (n > 0) { filled += static_cast(n); continue; } + if (n < 0 && errno == EINTR) continue; + break; // ENOSYS on an old kernel: the device file below is the same pool. + } +#endif + if (filled < size) { + const int source = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC); + while (source >= 0 && filled < size) { + const ssize_t n = ::read(source, out + filled, size - filled); + if (n > 0) { filled += static_cast(n); continue; } + if (n < 0 && errno == EINTR) continue; + break; + } + if (source >= 0) ::close(source); + } + if (filled == size) return MOBILEGL_OK; + WireLogError("MG_Remote SocketTransport: the kernel CSPRNG gave %zu of %zu bytes; a data " + "nonce is not minted from anything weaker", filled, size); + return MOBILEGL_ERR_UNSUPPORTED; + } + 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 65f73fbb..bc433c8f 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.h +++ b/MobileGL/MG_Remote/Transport/SocketTransport.h @@ -100,6 +100,39 @@ namespace MobileGL::MG_Remote::Transport { static MobileGLResult ConnectTo(const std::string& path, std::uint32_t timeoutMs, std::unique_ptr& 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 + // 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. + + // Connects the control connection only on `tcp://`; identical to ConnectTo otherwise. + static MobileGLResult ConnectControl(const std::string& path, std::uint32_t timeoutMs, + std::unique_ptr& outClient); + // Opens one more connection to `path` and writes `firstFrame` (a CtrlEnvelope carrying + // DataBind, framed here) as its first bytes. *outFd is the caller's to attach a + // 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. + static MobileGLResult AcceptOne(int listenFd, std::uint32_t timeoutMs, int* outFd); + // Reads EXACTLY one framed message from a raw socket - the header, then the payload, + // and not one byte more. A DataBind is followed on the same connection by StreamLink's + // own frames, which a buffered reader would swallow. PROTOCOL_MISMATCH for a bad magic + // or a payload over `maxBytes`, TIMEOUT when the frame did not complete in time. + static MobileGLResult ReceiveOneFrame(int fd, std::uint32_t timeoutMs, std::uint64_t maxBytes, + std::vector* outPayload); + // `size` bytes from the kernel CSPRNG. Never a PRNG fallback: an error is an error. + static MobileGLResult MintNonce(std::uint8_t* out, std::size_t size); + // 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_Remote/Transport/StreamLink.cpp b/MobileGL/MG_Remote/Transport/StreamLink.cpp index ea0d30fa..1d77a0ed 100644 --- a/MobileGL/MG_Remote/Transport/StreamLink.cpp +++ b/MobileGL/MG_Remote/Transport/StreamLink.cpp @@ -330,21 +330,53 @@ namespace MobileGL::MG_Remote::Transport { (void)role; return MOBILEGL_ERR_UNSUPPORTED; #else - if (fd < 0 || !mirrors.Valid() || !mirrors.ReplySlotCount() || + if (fd < 0) return MOBILEGL_ERR_INVALID_ARGUMENT; + const auto prepared = Prepare(mirrors, role); + if (prepared != MOBILEGL_OK) return prepared; + return BindDataFd(fd); +#endif + } + MobileGLResult StreamLink::Prepare(SessionSegments& mirrors, TransportRoleTag role) { + if (!mirrors.Valid() || !mirrors.ReplySlotCount() || mirrors.ReplyBytes() / mirrors.ReplySlotCount() <= sizeof(ReplySlotHeader)) return MOBILEGL_ERR_INVALID_ARGUMENT; Detach(); m_impl.reset(new Impl); auto& x = *m_impl; - x.fd = fd; x.memory = &mirrors; x.role = role; x.records = {mirrors.CmdRingBase(), mirrors.CmdRingCapacity(), mirrors.CmdRingCapacity() - 1, 0}; x.recordCursor = {mirrors.CmdRingBase(), mirrors.CmdRingCapacity(), mirrors.CmdRingCapacity() - 1, 0}; x.events = {mirrors.EventRingBase(), mirrors.EventRingCapacity(), mirrors.EventRingCapacity() - 1, 0}; x.eventCursor = {mirrors.EventRingBase(), mirrors.EventRingCapacity(), mirrors.EventRingCapacity() - 1, 0}; + return MOBILEGL_OK; + } + MobileGLResult StreamLink::BindDataFd(int fd) { +#if defined(_WIN32) + (void)fd; + return MOBILEGL_ERR_UNSUPPORTED; +#else + auto& x = *m_impl; + // Once, onto a prepared link: a second descriptor would be a second reader on one ring. + if (fd < 0 || !x.memory || x.fd >= 0 || x.reader.joinable()) return MOBILEGL_ERR_INVALID_ARGUMENT; + x.fd = fd; x.reader = std::thread([&x] { x.Loop(); }); return MOBILEGL_OK; +#endif + } + MobileGLResult StreamLink::AttachOwnedDeferred(const SessionSegmentSizes& sizes, TransportRoleTag role) { +#if defined(_WIN32) + (void)sizes; + (void)role; + return MOBILEGL_ERR_UNSUPPORTED; +#else + auto owned = std::make_unique(); + const auto result = owned->Memory().CreatePrivate( + sizes, role == TransportRoleTag::ClientProducer ? MemoryRole::Client : MemoryRole::Server); + if (result != MOBILEGL_OK) return result; + const auto prepared = Prepare(owned->Memory(), role); + if (prepared == MOBILEGL_OK) m_impl->owned = std::move(owned); + return prepared; #endif } MobileGLResult StreamLink::AttachOwned(int fd, const SessionSegmentSizes& sizes, TransportRoleTag role) { diff --git a/MobileGL/MG_Remote/Transport/StreamLink.h b/MobileGL/MG_Remote/Transport/StreamLink.h index d17d3428..143423f3 100644 --- a/MobileGL/MG_Remote/Transport/StreamLink.h +++ b/MobileGL/MG_Remote/Transport/StreamLink.h @@ -13,6 +13,14 @@ namespace MobileGL::MG_Remote::Transport { ~StreamLink() override; MobileGLResult Attach(int dataFd, SessionSegments& mirrors, TransportRoleTag role); MobileGLResult AttachOwned(int dataFd, const struct SessionSegmentSizes& sizes, TransportRoleTag role); + // PH-7 (4), ID-P7-3. The SERVER's half of a nonce-bound data connection, in two steps. + // The owned memory has to exist before the connection does - Welcome announces its + // sizes, and Welcome is what carries the nonce the client's data connection presents - + // so the link is attached WITHOUT a descriptor and without its reader thread, and + // BindDataFd starts both once the connection has proved which session it belongs to. + // Until then every write answers TRANSPORT_CLOSED (there is no fd) rather than blocking. + MobileGLResult AttachOwnedDeferred(const struct SessionSegmentSizes& sizes, TransportRoleTag role); + MobileGLResult BindDataFd(int dataFd); SessionSegments& Memory() override; void InitializeEndpoints() override; RingProducer& CommandsOut() override; @@ -44,6 +52,7 @@ namespace MobileGL::MG_Remote::Transport { TransportRoleTag Role() const override; private: + MobileGLResult Prepare(SessionSegments& mirrors, TransportRoleTag role); struct Impl; std::unique_ptr m_impl; }; diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index f27f02db..e234e26e 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -130,6 +130,7 @@ TEST(SessionHandshakeTest, TheAbiFingerprintChangesWhenAnyOfItsInputsDoes) { EXPECT_EQ(inputs.ProgramArtifactsCodecVersion, MG_State::GLState::kProgramArtifactsCodecVersion); EXPECT_EQ(inputs.ProgramArtifactsSchema, MG_State::GLState::ProgramArtifactsSchemaFingerprint()); EXPECT_EQ(inputs.OpCount, static_cast(MG_Pipe::MGPWireOp::kOpCount)); + EXPECT_EQ(inputs.ControlSchemaRevision, static_cast(MOBILEGL_PROTOCOL_CONTROL_REVISION)); EXPECT_EQ(inputs.PointerBits, sizeof(void*) * 8); const auto perturbed = [&](auto mutate) { auto copy = inputs; @@ -148,6 +149,7 @@ TEST(SessionHandshakeTest, TheAbiFingerprintChangesWhenAnyOfItsInputsDoes) { EXPECT_NE(production, perturbed([](auto& i) { ++i.ProgramArtifactsCodecVersion; })) << "ProgramArtifactsCodecVersion"; EXPECT_NE(production, perturbed([](auto& i) { ++i.ProgramArtifactsSchema; })) << "ProgramArtifactsSchema"; EXPECT_NE(production, perturbed([](auto& i) { ++i.OpCount; })) << "OpCount"; + EXPECT_NE(production, perturbed([](auto& i) { ++i.ControlSchemaRevision; })) << "ControlSchemaRevision"; EXPECT_NE(production, perturbed([](auto& i) { ++i.AbiVersion; })) << "AbiVersion"; EXPECT_NE(production, perturbed([](auto& i) { ++i.PointerBits; })) << "PointerBits"; EXPECT_NE(production, perturbed([](auto& i) { ++i.LittleEndian; })) << "LittleEndian"; diff --git a/MobileGL/MG_Test/Wire/SocketTransportTest.cpp b/MobileGL/MG_Test/Wire/SocketTransportTest.cpp index 22e5e12d..fe110796 100644 --- a/MobileGL/MG_Test/Wire/SocketTransportTest.cpp +++ b/MobileGL/MG_Test/Wire/SocketTransportTest.cpp @@ -35,6 +35,7 @@ // `BufferTooSmallKeepsTheMessage` matters more here: the message it must keep // is one this side already reassembled out of bytes nobody else can re-deliver. +#include // PH-7 (4): EncodeDataBind / DecodeDataBind #include // RefuseCode, for the word below #include // PH-7 (3): the minimum and the comparison #include // kWaitForever @@ -181,6 +182,75 @@ TEST(SocketTransportTest, TheTokenComparisonAnswersLengthAndPrefixTheSameWay) { EXPECT_FALSE(ConstantTimeTokenMatch(nullptr, "anything", 8)); } +// PH-7 (4), ID-P7-3. THE DATA CONNECTION'S FIRST FRAME IS READ EXACTLY, AND ONLY A DataBind +// OF THE MINTED WIDTH IS ONE. +// +// A client writes its DataBind and then, on the same socket, StreamLink's own frames - possibly +// in the same segment. The server hands that descriptor to a StreamLink reader after reading +// the DataBind, so a reader that buffered ahead would eat the first command bytes and the +// session would die of "invalid data frame header" one frame later. ReceiveOneFrame reads the +// header and then exactly the payload; the bytes behind it are still on the socket afterwards. +// End to end, over a real loopback TCP listener, through the two helpers ServerMain and the +// client use. The two-process half (reordered arrival, stale nonce, refused by name) is +// TcpLane.SupervisorProtocolControls. +TEST(SocketTransportTest, TheDataBindIsReadExactlyAndTheStreamBehindItIsLeftOnTheSocket) { + int reservation = ::socket(AF_INET, SOCK_STREAM, 0); + ASSERT_GE(reservation, 0); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ASSERT_EQ(::bind(reservation, reinterpret_cast(&address), sizeof(address)), 0); + socklen_t addressSize = sizeof(address); + ASSERT_EQ(::getsockname(reservation, reinterpret_cast(&address), &addressSize), 0); + const auto endpoint = std::string("tcp://127.0.0.1:") + std::to_string(ntohs(address.sin_port)); + ::close(reservation); + int listener = -1; + ASSERT_EQ(SocketTransport::Listen(endpoint, &listener), MOBILEGL_OK); + + std::uint8_t nonce[kDataNonceBytes]; + ASSERT_EQ(SocketTransport::MintNonce(nonce, sizeof(nonce)), MOBILEGL_OK); + const auto bind = MobileGL::MG_Remote::EncodeDataBind(nonce, sizeof(nonce)); + int client = -1; + ASSERT_EQ(SocketTransport::ConnectDataConnection(endpoint, 1000, MobileGLByteSpan{bind.data(), bind.size()}, + &client), MOBILEGL_OK); + const char tail[] = "MGLD-the-first-stream-bytes"; + ASSERT_EQ(::send(client, tail, sizeof(tail), MSG_NOSIGNAL), static_cast(sizeof(tail))); + + int accepted = -1; + ASSERT_EQ(SocketTransport::AcceptOne(listener, 1000, &accepted), MOBILEGL_OK); + std::vector frame; + ASSERT_EQ(SocketTransport::ReceiveOneFrame(accepted, 1000, 1024, &frame), MOBILEGL_OK); + EXPECT_EQ(frame, bind); + std::uint8_t presented[kDataNonceBytes] = {}; + ASSERT_TRUE(MobileGL::MG_Remote::DecodeDataBind(frame, presented)); + EXPECT_TRUE(ConstantTimeNonceMatch(nonce, presented)); + char after[sizeof(tail)] = {}; + EXPECT_EQ(::recv(accepted, after, sizeof(after), MSG_WAITALL), static_cast(sizeof(tail))); + EXPECT_EQ(std::memcmp(after, tail, sizeof(tail)), 0); + + // Only a DataBind of the minted width is one: a shorter nonce, and a frame that is not a + // DataBind at all, both decode as "no", which the callers refuse by name. + const auto shortBind = MobileGL::MG_Remote::EncodeDataBind(nonce, sizeof(nonce) - 1); + EXPECT_FALSE(MobileGL::MG_Remote::DecodeDataBind(shortBind, presented)); + std::vector notABind(bind.size(), 0xA5); + EXPECT_FALSE(MobileGL::MG_Remote::DecodeDataBind(notABind, presented)); + // One byte of difference anywhere is a mismatch. + presented[kDataNonceBytes - 1] ^= 1; + EXPECT_FALSE(ConstantTimeNonceMatch(nonce, presented)); + + // A frame over the caller's bound is refused before its payload is read. + int second = -1; + ASSERT_EQ(SocketTransport::ConnectDataConnection(endpoint, 1000, MobileGLByteSpan{bind.data(), bind.size()}, + &second), MOBILEGL_OK); + int big = -1; + ASSERT_EQ(SocketTransport::AcceptOne(listener, 1000, &big), MOBILEGL_OK); + EXPECT_EQ(SocketTransport::ReceiveOneFrame(big, 1000, bind.size() - 1, &frame), MOBILEGL_ERR_PROTOCOL_MISMATCH); + // And a connection that says nothing times out rather than blocking. + EXPECT_EQ(SocketTransport::AcceptOne(listener, 50, &big), MOBILEGL_ERR_TIMEOUT); + for (const int fd : {client, accepted, second, big, listener}) + if (fd >= 0) ::close(fd); +} + namespace { MobileGLByteSpan Span(const std::string& text) { diff --git a/scripts/ci/tcp_supervisor_smoke.py b/scripts/ci/tcp_supervisor_smoke.py index 4931f379..79fd1e02 100644 --- a/scripts/ci/tcp_supervisor_smoke.py +++ b/scripts/ci/tcp_supervisor_smoke.py @@ -92,7 +92,8 @@ def receive(peer, schema): if kind == schema['CtrlMsg'].CtrlMsg.Welcome: welcome = schema['Welcome'].Welcome() welcome.Init(table.Bytes, table.Pos) - return {'welcome': welcome.ServerPid(), 'fingerprint': welcome.WireFingerprint()} + return {'welcome': welcome.ServerPid(), 'fingerprint': welcome.WireFingerprint(), + 'nonce': bytes(welcome.DataNonce(j) for j in range(welcome.DataNonceLength())).hex()} raise RuntimeError(f'unexpected first control reply {kind}') @@ -127,6 +128,30 @@ def hello(schema, flatbuffers, fingerprint=0, major=1, token=kToken, identifier= return struct.pack('<4sI', b'MGLF', len(payload)) + payload +def data_bind(schema, flatbuffers, nonce): + """PH-7 (4): the framed DataBind a client writes as its data connection's first bytes.""" + builder = flatbuffers.Builder(64) + vector = builder.CreateByteVector(nonce) + message = schema['DataBind'] + message.Start(builder) + message.AddNonce(builder, vector) + body = message.End(builder) + envelope = schema['CtrlEnvelope'] + envelope.Start(builder) + envelope.AddMsgType(builder, schema['CtrlMsg'].CtrlMsg.DataBind) + envelope.AddMsg(builder, body) + builder.Finish(envelope.End(builder), b'MGLC') + payload = bytes(builder.Output()) + return struct.pack('<4sI', b'MGLF', len(payload)) + payload + + +def open_data(port, schema, flatbuffers, nonce): + """A data connection that has presented `nonce`. Left open: the server owns what happens next.""" + data = socket.create_connection(('127.0.0.1', port), timeout=3) + data.sendall(data_bind(schema, flatbuffers, nonce)) + return data + + def supervisor_environment(log, token, same_build=False): """The env a supervisor is started with, minus everything ServerMain's scrub catches.""" env = dict(os.environ, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no', @@ -200,35 +225,39 @@ def supervisor(server, log, same_build=False): @contextmanager -def held_session(port, schema, message): +def held_session(port, schema, flatbuffers, message): """A welcomed session whose sockets STAY OPEN, so its child is still alive to be killed. - `exchange` closes the pair before returning, which is what every other control in this file - wants and is exactly wrong for the reap case: a child that has already exited cannot be made - to fault. Busy is retried here for the same reason it is there - a previous child may still - be reaching _exit. + `exchange` closes the connection before returning, which is what every other control in this + file wants and is exactly wrong for the reap case: a child that has already exited cannot be + made to fault. Busy is retried here for the same reason it is there - a previous child may + still be reaching _exit. + + PH-7 (4): the data connection is opened AFTER the Welcome and presents its nonce, which is + what gets the child out of the handshake and into its control loop. """ control = None data = None reply = None for _ in range(30): control = socket.create_connection(('127.0.0.1', port), timeout=3) - data = socket.create_connection(('127.0.0.1', port), timeout=3) control.sendall(message) reply = receive(control, schema) if reply.get('code') != 7: break control.close() - data.close() control = None time.sleep(.05) if control is None: raise RuntimeError('supervisor remained Busy while a held session was wanted') + if reply.get('welcome', 0): + data = open_data(port, schema, flatbuffers, bytes.fromhex(reply['nonce'])) try: yield control, reply finally: control.close() - data.close() + if data is not None: + data.close() def last_faulted(path): @@ -254,6 +283,10 @@ def wait_for_log(path, needle, seconds=15): @contextmanager def pair(port): + """A session's CONTROL connection. The name is historical: until PH-7 (4) a TCP session was + two connections paired by arrival order, and this opened both. The data connection is now + opened after Welcome with the nonce it carries (open_data), so a control that stops at the + handshake - every one but held_session's - never opens one.""" deadline = time.monotonic() + 5 while True: try: @@ -264,11 +297,7 @@ def pair(port): raise time.sleep(.02) try: - data = socket.create_connection(('127.0.0.1', port), timeout=3) - try: - yield control - finally: - data.close() + yield control finally: control.close() @@ -308,7 +337,7 @@ def main(): sys.path[:0] = [directory, str(ROOT / '3rdparty/flatbuffers/python')] import flatbuffers schema = {name: importlib.import_module('MobileGL.Wire.' + name) - for name in ('CtrlEnvelope', 'CtrlMsg', 'Hello', 'LinkTerms', 'Refuse', 'Welcome')} + for name in ('CtrlEnvelope', 'CtrlMsg', 'DataBind', 'Hello', 'LinkTerms', 'Refuse', 'Welcome')} evidence = {} # ---- PH-7 (2)(3), ID-P7-3: THE TWO LISTEN REFUSALS ------------------------------- @@ -364,7 +393,7 @@ def main(): # a frame of a DIFFERENT schema that happened to verify was read here as a # CtrlEnvelope and its union tag believed. This is the cheap half of Ph's fuzz arm 1; # the arm itself (five malformed shapes, its own ctest lane) is scheduled after P7. - with held_session(port, schema, + with held_session(port, schema, flatbuffers, hello(schema, flatbuffers, fingerprint=fingerprint)) as (control, welcome): assert welcome.get('welcome', 0), welcome control.sendall(hello(schema, flatbuffers, fingerprint=fingerprint, @@ -390,7 +419,7 @@ def main(): # the status, so the log carried no line at all for this event and the assertion # below could not have been written. held = hello(schema, flatbuffers, fingerprint=fingerprint) - with held_session(port, schema, held) as (control, welcome): + with held_session(port, schema, flatbuffers, held) as (control, welcome): faulted = welcome.get('welcome', 0) assert faulted and faulted != os.getpid(), welcome # The count is a supervisor LIFETIME figure and the controls above have already @@ -418,6 +447,64 @@ def main(): after = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint)) assert after.get('welcome', 0) and after['welcome'] != faulted, after evidence['connect_after_fault'] = after + + # ---- PH-7 (4), ID-P7-3: THE DATA CONNECTION IS BOUND BY NONCE --------------------- + # + # (a) THE FIELD FAILURE. Over a Windows `adb forward` the two connections of one + # session arrived reordered, the server's AcceptPair read the data connection as + # control, and every child exited 67. Reproduced here without adb: the data + # connection (a DataBind with a nonce no live session minted) is presented FIRST, + # the control connection second. It is refused by name, as a clean exit, and the + # control connection behind it is welcomed. + stray = open_data(port, schema, flatbuffers, os.urandom(16)) + try: + refused = receive(stray, schema) + finally: + stray.close() + assert refused.get('code') == 4 and refused.get('detail') == 'data connection names no live session', refused + reordered = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint)) + assert reordered.get('welcome', 0), reordered + assert len(bytes.fromhex(reordered['nonce'])) == 16, reordered + evidence['data_connection_first'] = refused + + # (b) A STALE NONCE while a session is live: a data connection from an earlier session + # (or raced in by anybody who can reach the port) is refused by name ON THAT + # CONNECTION, and the session it failed to join still binds its own and comes up. + held = hello(schema, flatbuffers, fingerprint=fingerprint) + for _ in range(30): + control = socket.create_connection(('127.0.0.1', port), timeout=3) + control.sendall(held) + welcome = receive(control, schema) + if welcome.get('code') != 7: + break + control.close() + time.sleep(.05) + try: + assert welcome.get('welcome', 0) and len(bytes.fromhex(welcome['nonce'])) == 16, welcome + nonce = bytes.fromhex(welcome['nonce']) + stale = bytes(b ^ 0xFF for b in nonce) + wrong = open_data(port, schema, flatbuffers, stale) + try: + mismatch = receive(wrong, schema) + finally: + wrong.close() + assert mismatch.get('code') == 4 and mismatch.get('detail') == 'data connection nonce mismatch', mismatch + right = open_data(port, schema, flatbuffers, nonce) + try: + ready = wait_for_log(serverLog, f'pid={welcome["welcome"]} transport=spawn role=server ready') + assert ready is not None, ( + f'session pid={welcome["welcome"]} did not come up on its own data connection ' + f'after a stale one was refused; see {serverLog}') + finally: + right.close() + finally: + control.close() + named = wait_for_log(serverLog, 'Refuse{Authentication} data connection nonce mismatch') + assert named is not None, f'the stale data connection was not refused by name in {serverLog}' + evidence['stale_nonce'] = {'refusal': mismatch, 'line': next( + l.strip() for l in named.splitlines() if 'data connection nonce mismatch' in l)} + assert process.poll() is None, 'a refused data connection killed the supervisor' + with supervisor(args.server, args.out / 'same-build-supervisor.log', same_build=True) as (port, _, _log): strict = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint)) assert strict.get('code') == 3, strict