From dd74399e8a5ec7f4fae87f5948f2da751bb03049 Mon Sep 17 00:00:00 2001 From: hypnomalice <344329436+hypnomalice@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:23:28 +0000 Subject: [PATCH] Shaders: validate retained table bounds before proving stock preservation --- Engine/Resource/Test/shader_lookup_tests.cpp | 30 ++++++++++++++++++++ Engine/Resource/shader_lookup.cpp | 1 + 2 files changed, 31 insertions(+) diff --git a/Engine/Resource/Test/shader_lookup_tests.cpp b/Engine/Resource/Test/shader_lookup_tests.cpp index 2043f2ca..45c80795 100644 --- a/Engine/Resource/Test/shader_lookup_tests.cpp +++ b/Engine/Resource/Test/shader_lookup_tests.cpp @@ -2,6 +2,9 @@ // that points past the base's shader records is left out (the game reads such an index outside // its shader database). #include "Engine/Resource/shader_lookup.h" +#ifdef _WIN32 +#include +#endif #include #include #include @@ -134,6 +137,33 @@ int main() { const auto stock = preserved_lookup(false, {{1, {0}}}); const auto stockAlias = preserved_lookup(false, {{1, {0}}, {2, {0}}}); const auto extension = preserved_lookup(true, {{1, {0}}, {9, {1}}}); + for (const auto bytes : {std::size_t{0}, std::size_t{4}, std::size_t{12}, std::size_t{32}, std::size_t{544}}) { + auto truncated = extension; + truncated.resource.resize(bytes); + check(!fb::shader::preserves_stock_programs(truncated.table(), stock.table()), + "A retained table's metadata cannot authorize reads past its actual payload"); + } + auto shortMeta = extension; + shortMeta.meta.resize(8); + check(!fb::shader::preserves_stock_programs(shortMeta.table(), stock.table()), "Short retained metadata is refused"); +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto page = static_cast(system.dwPageSize); + auto* guarded = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + check(guarded != nullptr, "Allocate guarded shader-table regression buffer"); + if (guarded) { + auto* tiny = guarded + page - 12; + std::memcpy(tiny, extension.resource.data(), 12); + DWORD previous{}; + const bool protectedPage = VirtualProtect(guarded + page, page, PAGE_NOACCESS, &previous) != 0; + check(protectedPage, "Protect the page immediately after the truncated table"); + if (protectedPage) + check(!fb::shader::preserves_stock_programs({{tiny, 12}, extension.meta}, stock.table()), + "Direct graph selection refuses truncation before touching the guard page"); + VirtualFree(guarded, 0, MEM_RELEASE); + } +#endif const std::vector stockEdits{stockAlias.table()}; check(fb::shader::only_stock_aliases(stockAlias.table(), stock.table()) && !fb::shader::only_stock_aliases(extension.table(), stock.table()), diff --git a/Engine/Resource/shader_lookup.cpp b/Engine/Resource/shader_lookup.cpp index e98903ce..03656000 100644 --- a/Engine/Resource/shader_lookup.cpp +++ b/Engine/Resource/shader_lookup.cpp @@ -61,6 +61,7 @@ bool same_graph(const Table& left, const Table& right) { bool preserves_stock(const Table& retained, const Table& stock) { if (stock.resourceMeta.size() != 16) return false; try { + (void)lookup_offset(retained); (void)lookup_offset(stock); const auto stockSize = read(stock.resourceMeta, 0); const auto keptSize = read(retained.resourceMeta, 0);