From 2abf4eaa5123bdf3c58ed148f12aaaf8b3fd0884 Mon Sep 17 00:00:00 2001 From: mefisme <4188565+mefisme@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:31:22 -0500 Subject: [PATCH 1/2] backend: trust the signature over the pinned RVA when the two disagree ae_pick_engine_fn cross-checks each signature-resolved engine leaf against a hardcoded fallback RVA and, on a mismatch, REFUSED the signature and used the RVA instead. That is backwards on the only case it can actually fire in. A mismatch means the recorded RVA no longer describes the running build -- either DOOM was patched, or the value was wrong when it was written (the four addresses corrected in 8d50f80 were exactly that). In both cases the RVA points into arbitrary code while the signature is correct, so preferring the RVA guarantees the outcome the cross-check exists to prevent. The scan is not a loose match: sig_resolve_one enforces uniqueness across the whole executable range and reports AMBIGUOUS rather than guessing, so a lone hit is a strong result. The RVA still earns its place. It makes drift VISIBLE -- the log line names the leaf and now tells you to re-derive it -- and it is still used when the scan misses ENTIRELY, the one case where there is nothing better and the caller's own guards have to carry the risk. No behavior change on the pinned build: both leaves routed through this helper resolve to their recorded RVA, so the changed branch is never reached. The fix matters on a patched build, which is precisely where the old policy was harmful. Build clean; 16/16 native tests, sig_test 59/59 unique with 0 RVA-mismatches, hooktol_test 3/3 with 0 failures; gofmt/vet/go test clean. Co-Authored-By: Claude Opus 5 --- src/backend/apply_engine.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/backend/apply_engine.c b/src/backend/apply_engine.c index b036ffa..d42cf5e 100644 --- a/src/backend/apply_engine.c +++ b/src/backend/apply_engine.c @@ -1595,18 +1595,25 @@ void sh_apply_prefab_poll_play(void) * did not exist, so it is removed rather than left in as a harmless-looking write. */ } -/* Resolve an engine leaf by SIGNATURE, with a historical raw RVA as fallback, and REFUSE a signature - * that disagrees with that RVA on this build. +/* Resolve an engine leaf by SIGNATURE, with a historical raw RVA kept only as a fallback and as a + * drift tripwire. * - * Why the cross-check rather than just trusting the scan: these leaves are called on live editor state - * (the prefab ctor runs on every stage and on the Play watchdog's slot re-init), so a signature that - * resolved to a wrong-but-valid address would corrupt silently and on a timer. Preferring the RVA on a - * mismatch makes the worst case exactly the pre-existing behaviour, never something new. + * POLICY (revised 2026-07-28): on a disagreement the SIGNATURE WINS. The stale RVA is never preferred + * over a live scan hit. * - * The point of the migration is build portability: a raw base+RVA is locked to one DOOM build and fails - * SILENTLY into arbitrary code if the game is patched, whereas a signature matches bytes wherever the - * loader put them. Logs which path was taken so a future build's drift is visible at a glance instead of - * being discovered as a crash. */ + * The earlier policy refused the signature and used the RVA, reasoning that a bad scan hit could corrupt + * live editor state silently (these leaves run on every stage and on the Play watchdog's slot re-init). + * That trade is backwards on the only case it actually fires in. A mismatch means the recorded RVA no + * longer describes this build -- either the game was patched, or the RVA was wrong when it was written + * (all four of this DB's contaminated entries were exactly that: derived from a mis-imported binary). + * In both cases the RVA points into arbitrary code while the signature is correct, so preferring the RVA + * guarantees the very outcome the check was meant to prevent. The scan is also not a loose match: + * sig_resolve_one enforces UNIQUENESS across the whole executable range and reports AMBIGUOUS rather + * than guessing, so a lone hit is a strong result. + * + * The RVA still earns its place two ways: it makes drift VISIBLE (the MISMATCH line names the leaf), and + * it is still used when the scan misses ENTIRELY -- the one case where there is nothing better, and where + * the caller's own guards have to carry the risk. */ static void *ae_pick_engine_fn(const sig_result *results, size_t n, const char *sig_name, const uint8_t *base, uint32_t fallback_rva, const char *label) { @@ -1621,10 +1628,10 @@ static void *ae_pick_engine_fn(const sig_result *results, size_t n, const char * } if (s && r) { _snprintf_s(l, sizeof l, _TRUNCATE, - "C2 sig: %s sig=%p rva=%p MISMATCH -> signature REFUSED, using rva", label, - (void *)s, (void *)r); + "C2 sig: %s sig=%p rva=%p MISMATCH -> TRUSTING SIGNATURE (recorded rva is stale " + "for this build; re-derive it)", label, (void *)s, (void *)r); backend_log(l); - return (void *)r; + return (void *)s; } if (s) { _snprintf_s(l, sizeof l, _TRUNCATE, "C2 sig: %s sig=%p (no rva to cross-check)", label, (void *)s); From 1dcebcab8d5bd09b1cad6bfb877f21f872b8153a Mon Sep 17 00:00:00 2001 From: mefisme <4188565+mefisme@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:31:23 -0500 Subject: [PATCH 2/2] backend: stop the rendernode-guard log line claiming the fault shield is off The render-node guard has been commented out since the 2026-07-05 shield-off diagnostic, and its log line still read: rendernode-guard: TEMP-DISABLED (shield-off diagnostic build) The fault shield was turned off alongside the guard for that diagnostic, was cleared by it, and was RE-ENABLED the same day -- see shield_install() at the end of bootstrap_thread. Only the guard stayed off. The wording was never updated, so every launch still reports a shield state that has not been true for weeks, and it reads as a serious regression to anyone scanning the log. Wording only. sh_palette_guard_install stays commented out exactly as it was, so guard behavior is unchanged. The surrounding comment now states plainly that the fault shield is ACTIVE and records why the guard stays off: the render-node root cause (a reclassed node-less timeline keeping the pasted command's stale render-node +0x70) was fixed at the source in ae_apply_one, which makes the guard a redundant mitigation rather than a load-bearing one. Co-Authored-By: Claude Opus 5 --- src/backend/dllmain.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/backend/dllmain.c b/src/backend/dllmain.c index ff68a5e..000131e 100644 --- a/src/backend/dllmain.c +++ b/src/backend/dllmain.c @@ -217,11 +217,22 @@ static DWORD WINAPI bootstrap_thread(LPVOID p) * empty string is copied instead of the freed one. A valid entry is untouched; nothing is freed (the stale * buffer is simply never read). No editor/sig dependency (recipe-tagged RVA off the module base). See * palette_guard.c. */ - /* TEMP-DISABLED (shield-off diagnostic 2026-07-05): the render-node guard is one of OUR injected detours - * and it WRITES into the render-node array -- disabled here to prove our own code isn't the corruptor. - * RE-ENABLE by uncommenting. */ + /* DISABLED since 2026-07-05, and deliberately left that way. The render-node guard is one of OUR + * injected detours and it WRITES into the render-node array, so it was commented out to prove our + * own code was not the corruptor. + * + * READ THIS BEFORE PANICKING AT THE LOG LINE: the FAULT-SHIELD IS NOT DISABLED. It was turned off + * alongside this guard for that same 2026-07-05 diagnostic, was cleared by it, and was RE-ENABLED + * the same day -- see shield_install() at the end of this function. This guard is the ONLY thing + * still off, and the old log wording ("shield-off diagnostic build") wrongly implied otherwise. + * + * It stays off because the render-node root cause (a reclassed node-less timeline keeping the + * pasted command's stale render-node +0x70) was fixed AT THE SOURCE in ae_apply_one, which makes + * this guard a redundant mitigation rather than a load-bearing one. RE-ENABLE by uncommenting if + * the 0xd32a39 render face is ever reproduced again. */ /* sh_palette_guard_install(g_doom_base); */ - backend_log("rendernode-guard: TEMP-DISABLED (shield-off diagnostic build)"); + backend_log("rendernode-guard: DISABLED (redundant since the ae_apply_one root fix; " + "fault-shield itself is ACTIVE)"); void *get_decls = (void *)sig_addr_by_name(results, db, "GetDeclsOfType"); /* GetDeclsOfType is resolved here and handed to the command layer below (sh_commands_install),