From d17c6c225c34e7f3de99717b02ba0754edbd0279 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 11 Sep 2026 13:58:06 -0400 Subject: [PATCH] [Feat] (MG_Remote): MGPCapss two blob serializers and the ABI fingerprint - sparse format-capability tables, length-prefixed renderer strings, every decoder refusing truncation and trailing bytes --- MobileGL/MG_Remote/CapsCodec.cpp | 466 ++++++++++++++++++++++++++++++- 1 file changed, 451 insertions(+), 15 deletions(-) diff --git a/MobileGL/MG_Remote/CapsCodec.cpp b/MobileGL/MG_Remote/CapsCodec.cpp index eed88646..8151600a 100644 --- a/MobileGL/MG_Remote/CapsCodec.cpp +++ b/MobileGL/MG_Remote/CapsCodec.cpp @@ -6,11 +6,38 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header +// P5 package w1: MGPCaps's two blob serializers, the pair MGPipeTypes.h:134-136 defers to +// this phase by name ("Their serializers land with the transport (P5)"). +// +// THE FORMAT, and every part of it is a refusal rather than a guess. It is +// ProgramArtifactsCodec's shape on purpose - that codec is the tree's one worked example of a +// wire format that has survived a real transport, so copying it is cheaper than being +// original and much cheaper than being wrong: +// +// * a VERSION word first, and the two TABLE DIMENSIONS second, so a peer whose +// TextureInternalFormat enum grew is a mismatch AT READ TIME rather than a silent shear +// that reads one format's capabilities as another's; +// * length-prefixed everything, with the count checked against the bytes that REMAIN before +// a single element is reserved, so a corrupt count cannot become a four-billion-element +// resize; +// * SPARSE for all three capability tables. The dense form is +// 2 * targets * formats * 8 bytes plus the sample-count lists - half a megabyte of mostly +// zeroes, per context, per caps invalidation (R-12 makes a re-arriving snapshot the +// invalidation, so this is not a once-per-process cost). The tables are overwhelmingly +// empty, so what crosses is (index, value) pairs and the decoder Clear()s first; +// * little-endian by memcpy of fixed-width scalars, which is what every other MobileGL wire +// struct already assumes and what the ABI fingerprint below makes checkable; +// * every decoder returns FALSE on truncation, a bad version, a dimension mismatch, an +// out-of-range index or TRAILING BYTES THE FORMAT DOES NOT ACCOUNT FOR. These bytes +// arrive over a wire and the outputs are left in a defined, default state on a refusal. + #include "CapsCodec.h" +#include #include #include +#include namespace MobileGL::MG_Remote { @@ -29,28 +56,437 @@ namespace MobileGL::MG_Remote { static_assert(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a <= 0xFFFFull, "the subsystem mask no longer fits CallMask's sixteen consumer bits"); -#define MGP5_C0_STUB(what) \ - do { \ - MGLOG_F("MGPipe: Fatal{UnimplementedCapsCodec, \"%s\"} - P5 package w1 has not landed " \ - "this yet; c0 shipped the signature only", \ - what); \ - std::abort(); \ - } while (0) + namespace { - Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache&, Vector&) { - MGP5_C0_STUB("EncodeFormatCapabilities"); + // Bumped whenever the bytes change in a way a previous reader would misread. A reader + // that sees a different word REFUSES; it never tries to guess a layout. + constexpr Uint32 kFormatCapabilitiesCodecVersion = 1; + constexpr Uint32 kRendererInfoCodecVersion = 1; + + // A count is never believed before it is weighed against the bytes that are left. The + // largest legal element count in either blob is bounded by the tables' own dimensions, + // but a String's length is not, so the reader checks bytes rather than a constant. + class Writer { + public: + explicit Writer(Vector& out) : m_out(out) {} + + void Raw(const void* bytes, SizeT size) { + if (size == 0) { + return; + } + const auto* p = static_cast(bytes); + m_out.insert(m_out.end(), p, p + size); + } + void U8(Uint8 v) { Raw(&v, sizeof(v)); } + void U32(Uint32 v) { Raw(&v, sizeof(v)); } + void U64(Uint64 v) { Raw(&v, sizeof(v)); } + void I32(Int32 v) { Raw(&v, sizeof(v)); } + void Str(const String& s) { + U32(static_cast(s.size())); + Raw(s.data(), s.size()); + } + void OptStr(const Optional& s) { + U8(s.has_value() ? 1u : 0u); + if (s.has_value()) { + Str(*s); + } + } + + private: + Vector& m_out; + }; + + class Reader { + public: + Reader(const void* bytes, Uint64 size) + : m_p(static_cast(bytes)), m_left(bytes != nullptr ? size : 0) {} + + Bool Raw(void* out, Uint64 size) { + if (!m_ok || size > m_left) { + m_ok = false; + return false; + } + std::memcpy(out, m_p, static_cast(size)); + m_p += size; + m_left -= size; + return true; + } + Bool U8(Uint8& v) { return Raw(&v, sizeof(v)); } + Bool U32(Uint32& v) { return Raw(&v, sizeof(v)); } + Bool U64(Uint64& v) { return Raw(&v, sizeof(v)); } + Bool I32(Int32& v) { return Raw(&v, sizeof(v)); } + Bool Str(String& s) { + Uint32 length = 0; + if (!U32(length)) { + return false; + } + // THE CHECK THAT MATTERS: the count is weighed against the bytes that remain + // BEFORE the string is sized, so a corrupt length is a refusal rather than a + // four-gigabyte allocation. + if (length > m_left) { + m_ok = false; + return false; + } + s.assign(reinterpret_cast(m_p), static_cast(length)); + m_p += length; + m_left -= length; + return true; + } + Bool OptStr(Optional& s) { + Uint8 present = 0; + if (!U8(present)) { + return false; + } + if (present == 0) { + s.reset(); + return true; + } + String value; + if (!Str(value)) { + return false; + } + s = Move(value); + return true; + } + // How many elements of `elementBytes` could still possibly be there. The gate a + // length-prefixed array is held to before it reserves anything. + Uint64 RoomFor(Uint64 elementBytes) const { + return elementBytes == 0 ? 0 : m_left / elementBytes; + } + Bool Ok() const { return m_ok; } + Uint64 Left() const { return m_left; } + // Trailing bytes the format does not account for are a REFUSAL: they mean the + // writer and the reader disagree about the shape, and the half that was read is + // not trustworthy just because it parsed. + Bool Finished() const { return m_ok && m_left == 0; } + + private: + const Uint8* m_p = nullptr; + Uint64 m_left = 0; + Bool m_ok = true; + }; + + using MG_Backend::FormatCapabilityCache; + using MG_Backend::FormatCapabilityFlags; + + constexpr Uint64 kFormatCells = static_cast(MG_Backend::kFormatCapabilityTargetCount) * + static_cast(MG_Backend::kFormatCapabilityFormatCount); + + // FNV-1a, the same mixer the tree already uses for build-stamp style fingerprints. + constexpr Uint64 kFnvOffset = 1469598103934665603ull; + constexpr Uint64 kFnvPrime = 1099511628211ull; + + Uint64 FnvBytes(Uint64 hash, const void* bytes, SizeT size) { + const auto* p = static_cast(bytes); + for (SizeT i = 0; i < size; ++i) { + hash ^= static_cast(p[i]); + hash *= kFnvPrime; + } + return hash; + } + + Uint64 FnvU64(Uint64 hash, Uint64 value) { return FnvBytes(hash, &value, sizeof(value)); } + + } // namespace + + // --------------------------------------------------------------------------------- + // FormatCapabilityCache + // --------------------------------------------------------------------------------- + + Bool EncodeFormatCapabilities(const FormatCapabilityCache& cache, Vector& out) { + Writer w(out); + w.U32(kFormatCapabilitiesCodecVersion); + w.U32(static_cast(MG_Backend::kFormatCapabilityTargetCount)); + w.U32(static_cast(MG_Backend::kFormatCapabilityFormatCount)); + w.U32(0); // reserved, keeps the header 16 bytes and 8-aligned for the pairs below + + // The two flag tables, sparse. A cell is written only when it is non-zero, so what + // crosses is proportional to what the driver actually supports rather than to the + // square of two enum spaces. + const auto writeTable = [&](const MG_Backend::FormatCapabilityTable& table) { + Uint32 populated = 0; + for (SizeT t = 0; t < MG_Backend::kFormatCapabilityTargetCount; ++t) { + for (SizeT f = 0; f < MG_Backend::kFormatCapabilityFormatCount; ++f) { + if (table[t][f].GetRaw() != 0) { + ++populated; + } + } + } + w.U32(populated); + for (SizeT t = 0; t < MG_Backend::kFormatCapabilityTargetCount; ++t) { + for (SizeT f = 0; f < MG_Backend::kFormatCapabilityFormatCount; ++f) { + const Uint64 raw = static_cast(table[t][f].GetRaw()); + if (raw == 0) { + continue; + } + w.U32(static_cast(t * MG_Backend::kFormatCapabilityFormatCount + f)); + w.U64(raw); + } + } + }; + writeTable(cache.FullCaps); + writeTable(cache.CaveatCaps); + + // The sample-count lists: the Vector that is the reason this cannot be a memcpy. + Uint32 populated = 0; + for (SizeT t = 0; t < MG_Backend::kFormatCapabilityTargetCount; ++t) { + for (SizeT f = 0; f < MG_Backend::kFormatCapabilityFormatCount; ++f) { + if (!cache.SampleCounts[t][f].empty()) { + ++populated; + } + } + } + w.U32(populated); + for (SizeT t = 0; t < MG_Backend::kFormatCapabilityTargetCount; ++t) { + for (SizeT f = 0; f < MG_Backend::kFormatCapabilityFormatCount; ++f) { + const Vector& counts = cache.SampleCounts[t][f]; + if (counts.empty()) { + continue; + } + w.U32(static_cast(t * MG_Backend::kFormatCapabilityFormatCount + f)); + w.U32(static_cast(counts.size())); + for (const Int value : counts) { + w.I32(static_cast(value)); + } + } + } + return true; } - Bool DecodeFormatCapabilities(const void*, Uint64, MG_Backend::FormatCapabilityCache&) { - MGP5_C0_STUB("DecodeFormatCapabilities"); + Bool DecodeFormatCapabilities(const void* bytes, Uint64 size, FormatCapabilityCache& out) { + out.Clear(); + Reader r(bytes, size); + + Uint32 version = 0; + Uint32 targets = 0; + Uint32 formats = 0; + Uint32 reserved = 0; + if (!r.U32(version) || !r.U32(targets) || !r.U32(formats) || !r.U32(reserved)) { + return false; + } + if (version != kFormatCapabilitiesCodecVersion) { + MGLOG_E("MG_Remote caps: format-capability blob is version %u, this build reads %u", + version, kFormatCapabilitiesCodecVersion); + return false; + } + if (targets != MG_Backend::kFormatCapabilityTargetCount || + formats != MG_Backend::kFormatCapabilityFormatCount) { + // Not a corrupt stream: a peer whose TextureTarget or TextureInternalFormat enum + // is a different size. Reading it anyway shears every cell onto a neighbouring + // format, which is exactly the failure the ABI fingerprint exists to make loud. + MGLOG_E("MG_Remote caps: format-capability blob is %ux%u, this build is %llux%llu", + targets, formats, + static_cast(MG_Backend::kFormatCapabilityTargetCount), + static_cast(MG_Backend::kFormatCapabilityFormatCount)); + return false; + } + + const auto readTable = [&](MG_Backend::FormatCapabilityTable& table) -> Bool { + Uint32 populated = 0; + if (!r.U32(populated)) { + return false; + } + if (populated > r.RoomFor(sizeof(Uint32) + sizeof(Uint64))) { + return false; + } + for (Uint32 i = 0; i < populated; ++i) { + Uint32 index = 0; + Uint64 raw = 0; + if (!r.U32(index) || !r.U64(raw)) { + return false; + } + if (static_cast(index) >= kFormatCells) { + return false; + } + table[index / MG_Backend::kFormatCapabilityFormatCount] + [index % MG_Backend::kFormatCapabilityFormatCount] = + FormatCapabilityFlags(raw); + } + return true; + }; + if (!readTable(out.FullCaps) || !readTable(out.CaveatCaps)) { + out.Clear(); + return false; + } + + Uint32 populated = 0; + if (!r.U32(populated) || populated > r.RoomFor(2 * sizeof(Uint32))) { + out.Clear(); + return false; + } + for (Uint32 i = 0; i < populated; ++i) { + Uint32 index = 0; + Uint32 count = 0; + if (!r.U32(index) || !r.U32(count)) { + out.Clear(); + return false; + } + if (static_cast(index) >= kFormatCells || count > r.RoomFor(sizeof(Int32))) { + out.Clear(); + return false; + } + Vector& counts = out.SampleCounts[index / MG_Backend::kFormatCapabilityFormatCount] + [index % MG_Backend::kFormatCapabilityFormatCount]; + counts.resize(static_cast(count)); + for (Uint32 j = 0; j < count; ++j) { + Int32 value = 0; + if (!r.I32(value)) { + out.Clear(); + return false; + } + counts[j] = static_cast(value); + } + } + + if (!r.Finished()) { + MGLOG_E("MG_Remote caps: format-capability blob has %llu trailing bytes the format " + "does not account for", + static_cast(r.Left())); + out.Clear(); + return false; + } + return true; } - Bool EncodeRendererInfo(const RendererInfo&, Vector&) { MGP5_C0_STUB("EncodeRendererInfo"); } + // --------------------------------------------------------------------------------- + // RendererInfo + // --------------------------------------------------------------------------------- - Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); } + namespace { - Uint64 CapsAbiFingerprint() { MGP5_C0_STUB("CapsAbiFingerprint"); } + void WriteVersion(Writer& w, const Version& v) { + w.I32(static_cast(v.Major)); + w.I32(static_cast(v.Minor)); + w.I32(static_cast(v.Patch)); + w.OptStr(v.Suffix); + w.U8(v.Type.has_value() ? 1u : 0u); + w.U8(v.Type.has_value() ? static_cast(*v.Type) : 0u); + } -#undef MGP5_C0_STUB + Bool ReadVersion(Reader& r, Version& v) { + Int32 major = 0; + Int32 minor = 0; + Int32 patch = 0; + if (!r.I32(major) || !r.I32(minor) || !r.I32(patch)) { + return false; + } + v.Major = static_cast(major); + v.Minor = static_cast(minor); + v.Patch = static_cast(patch); + if (!r.OptStr(v.Suffix)) { + return false; + } + Uint8 hasType = 0; + Uint8 type = 0; + if (!r.U8(hasType) || !r.U8(type)) { + return false; + } + if (hasType != 0) { + v.Type = static_cast(type); + } else { + v.Type.reset(); + } + return true; + } + + } // namespace + + Bool EncodeRendererInfo(const RendererInfo& info, Vector& out) { + Writer w(out); + w.U32(kRendererInfoCodecVersion); + w.Str(info.RendererName); + w.Str(info.BackendName); + w.OptStr(info.ExtraVendor); + WriteVersion(w, info.RendererGLInfo.TargetGLVersion); + WriteVersion(w, info.RendererGLInfo.TargetGLSLVersion); + w.U32(static_cast(info.RendererGLInfo.Extensions.size())); + for (const GLExtension extension : info.RendererGLInfo.Extensions) { + w.U32(static_cast(extension)); + } + w.U8(info.RendererGLInfo.IsCompatibilityProfile ? 1u : 0u); + w.U8(info.StaticBackendCapability.AllowVSOnlyPrograms ? 1u : 0u); + return true; + } + + Bool DecodeRendererInfo(const void* bytes, Uint64 size, RendererInfo& out) { + out = RendererInfo{}; + Reader r(bytes, size); + + Uint32 version = 0; + if (!r.U32(version)) { + return false; + } + if (version != kRendererInfoCodecVersion) { + MGLOG_E("MG_Remote caps: renderer-info blob is version %u, this build reads %u", version, + kRendererInfoCodecVersion); + return false; + } + if (!r.Str(out.RendererName) || !r.Str(out.BackendName) || !r.OptStr(out.ExtraVendor) || + !ReadVersion(r, out.RendererGLInfo.TargetGLVersion) || + !ReadVersion(r, out.RendererGLInfo.TargetGLSLVersion)) { + out = RendererInfo{}; + return false; + } + + Uint32 extensionCount = 0; + if (!r.U32(extensionCount) || extensionCount > r.RoomFor(sizeof(Uint32))) { + out = RendererInfo{}; + return false; + } + out.RendererGLInfo.Extensions.resize(static_cast(extensionCount)); + for (Uint32 i = 0; i < extensionCount; ++i) { + Uint32 value = 0; + if (!r.U32(value)) { + out = RendererInfo{}; + return false; + } + out.RendererGLInfo.Extensions[i] = static_cast(value); + } + + Uint8 compatibility = 0; + Uint8 vsOnly = 0; + if (!r.U8(compatibility) || !r.U8(vsOnly)) { + out = RendererInfo{}; + return false; + } + out.RendererGLInfo.IsCompatibilityProfile = compatibility != 0; + out.StaticBackendCapability.AllowVSOnlyPrograms = vsOnly != 0; + + if (!r.Finished()) { + MGLOG_E("MG_Remote caps: renderer-info blob has %llu trailing bytes the format does " + "not account for", + static_cast(r.Left())); + out = RendererInfo{}; + return false; + } + return true; + } + + // --------------------------------------------------------------------------------- + // The ABI assertion the handshake carries + // --------------------------------------------------------------------------------- + + Uint64 CapsAbiFingerprint() { + // MGPCaps has only a COMPOSITIONAL size assertion (MGPipeTypes.h:145-146) because + // DynamicBackendParameters still carries SizeT and GLenum members - P0.5's fixed-width + // rewrite did not happen and P5 does not do it either (table 0's ABI row; the rewrite + // is P7's account). So the caps block's literal size IS ABI-dependent, and this + // fingerprint is what turns that from a latent hazard into a named refusal. + // + // The git stamp is in it because two builds of the same sizes can still disagree about + // a FIELD ORDER, which no sizeof can see; P6's spawn is same-machine and same-binary, + // so it inherits this unchanged rather than needing a looser rule. + Uint64 hash = kFnvOffset; + hash = FnvU64(hash, sizeof(MG_Backend::DynamicBackendParameters)); + hash = FnvU64(hash, sizeof(MG_Pipe::MGPCaps)); + hash = FnvU64(hash, sizeof(MG_Backend::GLFunctionsTable)); + hash = FnvU64(hash, static_cast(MG_Backend::kFormatCapabilityTargetCount)); + hash = FnvU64(hash, static_cast(MG_Backend::kFormatCapabilityFormatCount)); + hash = FnvU64(hash, kFormatCapabilitiesCodecVersion); + hash = FnvU64(hash, kRendererInfoCodecVersion); + hash = FnvU64(hash, static_cast(MG_Pipe::MGPWireOp::kOpCount)); + hash = FnvBytes(hash, GIT_COMMIT_HASH_SHORT, std::strlen(GIT_COMMIT_HASH_SHORT)); + return hash; + } } // namespace MobileGL::MG_Remote