[MG_Remote, MG_Test] (P7): bound readback scratch by server reply limit

This commit is contained in:
2026-09-23 13:43:44 -04:00
parent 54a8e3fadb
commit 2bcec27ac2
3 changed files with 100 additions and 47 deletions
+35 -41
View File
@@ -30,6 +30,7 @@
#include <cstdlib>
#include <cstring>
#include <limits>
namespace MobileGL::MG_Remote::Server {
@@ -396,50 +397,42 @@ namespace MobileGL::MG_Remote::Server {
"and a client blocked on seq %llu would never be answered",
static_cast<unsigned long long>(seq));
}
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
if (table == nullptr || table->GL.ReadPixels == nullptr) {
// DECLINED IS A REAL ANSWER (table 0's slot-header row) and it is the RIGHT one
// here: the client is parked on this seq inside the verb barrier, so returning
// false without posting would convert "not implemented" into "never returns".
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
return false;
}
if (info.DstSize == 0) {
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
return false;
}
// ID-49: THE REPLY CROSSES TIGHT AND PACK STATE NEVER CROSSES FOR A READ. The server reads
// with NEUTRAL pack state - ROW_LENGTH 0, SKIP_ROWS/PIXELS/IMAGES 0, ALIGNMENT 1 - into a
// w*h*bytesPerPixel extent that IS the reply payload, and restores the pack state
// afterwards; the CLIENT scatters those tight rows into the application's pointer per its
// own GL_PACK_* state (c1's half). Reading with the client's pack state HERE was the
// codex-1 blocker: the backend's ReadPixels honours ROW_LENGTH/SKIP_* and writes PAST a
// DstSize the client sized without the initial skip (a 4x3 RGBA8 read with ROW_LENGTH=8,
// SKIP_ROWS=1, SKIP_PIXELS=2 allocates 80 and lands its last write at 120), which is the
// two DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs the census omitted. THE
// DstSize FORMULA BOTH SIDES AGREE ON: w * h * bytesPerPixel. A non-default server-visible
// pack state can no longer change either the reply's size or its bytes.
const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type)));
// Tight size = w*h*bpp, the whole of ID-49's formula. If this build cannot size the
// (format, type) pair (bpp == 0) it trusts the client's DstSize - a neutral read still
// cannot overflow it via row length or skips, and an unsizeable pair is c1's
// Fatal{UnsizedReadback} at emission, not this side's.
const Uint64 tight = bytesPerPixel != 0
? static_cast<Uint64>(info.Box.W) * static_cast<Uint64>(info.Box.H) *
static_cast<Uint64>(bytesPerPixel)
: info.DstSize;
if (bytesPerPixel != 0 && tight != info.DstSize) {
// Both halves compute w*h*bpp under ID-49, so a disagreement is the two sides
// disagreeing about the frame. Read (and post) the tight extent this side owns rather
// than the client's number, so a wrong DstSize can never make this a short read into
// uninitialised scratch.
MGLOG_E_ONCE("MG_Remote server: read_pixels DstSize %llu != tight w*h*bpp %llu "
"(%ux%u, bpp %zu); reading the tight extent (ID-49)",
static_cast<unsigned long long>(info.DstSize),
static_cast<unsigned long long>(tight), info.Box.W, info.Box.H,
bytesPerPixel);
// The server's LinkTerms.maxReplyBytes is copied from its attached ILink capabilities.
// Reject malformed shapes and oversized replies before growing m_readbackScratch. The
// client-side guard is not a trust boundary: a peer can write this record directly.
const auto rejectReadback = [&] {
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
return false;
};
if (bytesPerPixel == 0 || info.Box.W == 0 || info.Box.H == 0 ||
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
info.DstSize == 0 || m_maxReplyBytes == 0) {
return rejectReadback();
}
const Uint64 width = info.Box.W;
const Uint64 height = info.Box.H;
const Uint64 bpp = static_cast<Uint64>(bytesPerPixel);
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
if (width > kUint64Max / height) return rejectReadback();
const Uint64 pixels = width * height;
if (pixels > kUint64Max / bpp) return rejectReadback();
const Uint64 tight = pixels * bpp;
if (tight != info.DstSize || tight > m_maxReplyBytes ||
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
return rejectReadback();
}
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
if (table == nullptr || table->GL.ReadPixels == nullptr) {
// A well-formed call can still be unsupported by this server; return the
// established decline status after the peer-controlled size has been checked.
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
return false;
}
if (tight > m_readbackScratch.size()) {
m_readbackScratch.resize(static_cast<SizeT>(tight));
@@ -1200,6 +1193,7 @@ namespace MobileGL::MG_Remote::Server {
if (!link || !link->Attached() || !m_segments)
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, PipeApplier::Attach missing link}");
m_verbs.SetBackend(backend);
m_verbs.SetMaxReplyBytes(link->Capabilities().MaxReplyBytes);
m_decoder = Wire::PipeWireDecoder(link, m_segments, m_replies);
m_decoder.SetVerbSink(&m_verbs);
MG_Pipe::MGPipeServerBlockNoteIdentity(); m_attached = true;
+3
View File
@@ -136,6 +136,7 @@ namespace MobileGL::MG_Remote::Server {
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
// verb that arrives before then declines by name rather than dereferencing.
void SetBackend(MG_Backend::BackendObject* backend);
void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; }
Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override;
Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) override;
@@ -328,6 +329,8 @@ namespace MobileGL::MG_Remote::Server {
// ReadPixels writes into a caller buffer, so one staging vector per session sits
// between them. Grown, never shrunk, and never handed out past the call.
Vector<Uint8> m_readbackScratch;
// Server-declared LinkTerms.maxReplyBytes, copied from the attached server link.
Uint64 m_maxReplyBytes = 0;
// P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges
// per record (rule C: bounded by NumDraws, owned here, never handed out past the call),
// and the last-record witness above.
+62 -6
View File
@@ -95,13 +95,14 @@ namespace {
}
};
// A peer can mutate the command-ring bytes AFTER the production encoder has
// accepted and written them. The sequence, framing size, and submitted
// watermark stay valid; only the opcode becomes unknown to the server.
// This is the seam the later D11/PH-2 controls will use for payload fields.
// A peer can mutate command-ring record bytes AFTER the production encoder
// accepts and writes them. Sequence, framing size, and submitted watermark stay
// valid; selected payload fields bypass the encoder's client-side checks.
template <class Payload, class Mutator>
bool EncodeThenMutateHeaderAndPublish(Client::ClientSession& session, MobileGL::MG_Pipe::MGPWireOp op,
const Payload& payload, Mutator&& mutate) {
const Payload& payload, Mutator&& mutate,
std::uint64_t* outSeq = nullptr) {
if (outSeq != nullptr) *outSeq = Wire::kInvalidSeq;
auto* link = session.DataLink();
if (link == nullptr || !link->Attached()) return false;
Wire::WireRecordLayout layout{};
@@ -125,7 +126,9 @@ namespace {
// and the producer-owned submittedSeq stay monotone and teardown knows
// the record's sequence. Only the record bytes bypass encoder checks.
session.Producer().PublishAndNotify(seq);
return link->Flush() == MOBILEGL_OK;
if (link->Flush() != MOBILEGL_OK) return false;
if (outSeq != nullptr) *outSeq = seq;
return true;
}
std::string ServerImage() {
@@ -287,6 +290,59 @@ TEST(ServerSpawnTest, RawPeerOutOfRangeRenderStateCsoSlotGetsNamedProtocolCorrup
client.Stop();
}
TEST(ServerSpawnTest, RawPeerOversizedReadbackGetsErrorAndSessionContinues) {
const std::string endpoint = Endpoint("bad-readback-size");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-readback-size", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPReadbackInfo info{};
info.Res = MobileGL::MG_Pipe::kMGPipeNullHandle;
info.Box = MobileGL::MG_Pipe::MGPBox{0, 0, 0, 1, 1, 1};
info.Format = GL_RGBA;
info.Type = GL_UNSIGNED_BYTE;
info.DstSize = 4;
const std::uint64_t maxReplyBytes = client.MaxReplyBytes();
ASSERT_GT(maxReplyBytes, 0u);
std::uint64_t readSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ReadPixels, info,
[maxReplyBytes](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireInfo = reinterpret_cast<MobileGL::MG_Pipe::MGPReadbackInfo*>(payloadBytes);
const auto width = static_cast<MobileGL::Uint32>(maxReplyBytes / 4 + 1);
wireInfo->Box.W = width;
wireInfo->Box.H = 1;
wireInfo->DstSize = static_cast<std::uint64_t>(width) * 4;
}, &readSeq)) << "the peer failed to mutate and publish the encoder-accepted readback";
ASSERT_NE(readSeq, Wire::kInvalidSeq);
ASSERT_EQ(client.WaitForApplied(readSeq, 5000), Transport::SessionWait::Reached);
MobileGL::Int32 status = Wire::ReplySink::kStatusDeclined;
std::uint64_t replySize = 99;
ASSERT_TRUE(client.ReadReply(readSeq, nullptr, 0, &status, &replySize));
EXPECT_EQ(status, Wire::ReplySink::kStatusError);
EXPECT_EQ(replySize, 0u);
// A valid follow-up record proves this refusal answered the slot without ending the
// server session or stranding the command consumer.
MobileGL::MG_Pipe::MGPMemoryBarrier barrier{};
barrier.Bits = 0x2000u;
const auto followupSeq = client.EmitAndWait(
MobileGL::MG_Pipe::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier),
nullptr, 0, nullptr, 0, nullptr);
ASSERT_NE(followupSeq, Wire::kInvalidSeq);
EXPECT_EQ(client.WaitForApplied(followupSeq, 5000), Transport::SessionWait::Reached);
Client::ClientSessionInstance().Stop();
int exitCode = -1;
ASSERT_EQ(Server::ReapServer(session.server, 5000, &exitCode), MOBILEGL_OK);
EXPECT_EQ(exitCode, 0);
}
TEST(ServerSpawnTest, StartsAServerProcessAndHandshakesAcrossIt) {
const int before = Server::CountOwnChildren();
ASSERT_GE(before, 0) << "/proc unreadable; the process-tree gate cannot run here";