Two findings that have nothing to do with Diablo II, arrived at by taking a loot filter apart.
The loot filter that runs alongside this mod reads its configuration exactly once, during
DllMain. Its parser has one call site and the chain from it terminates at process
attach. There is no watcher thread, no reload command, no menu item, no second parse anywhere in
the image. Editing the file and waiting for it to be noticed cannot work, and no amount of
patience will change that.
So the swap has to call the parser directly. That much was straightforward. What follows is the part that was not.
The first working version re-pointed the filename, called the parser, and re-applied the module it looked like it should. It logged this:
[CFG] Maphack re-applied (instance 862E14AC, found 4 modules into the key list). [CFG] re-pointed BH config 'BH.cfg' -> 'BH-Level.cfg'; parse OK, maphack re-apply OK (NO files written)
Every clause of that is true. Nothing visible changed.
Three errors were stacked underneath it, and each one alone would have been enough:
The lesson is not about loot filters. Re-applying configuration only means something against the structure that is actually read at the moment of use. Find that structure first, then work backwards to whatever rebuilds it. A log line that reports success is reporting that a function returned, which is a much smaller claim than it looks.
Calling into a third-party binary at runtime wants a guard, so that a wrong address produces a refusal instead of taking the process down. On this toolchain that turned out to have two traps, and both produce a handler that looks installed and does nothing.
__try compiles under -fms-extensions and then the assembler rejects
it — clang's i386 SEH codegen is incomplete for this target. A trivial function may slip
through; anything real does not.
The classic technique still works: link a record onto fs:[0] whose handler rewrites
the faulting context to resume at a label, and return continue execution. But
if nothing in the C source ever branches to that label, the compiler proves the block unreachable,
removes it, and folds the label's address to the literal 1:
movl $1, -36(%ebp) # recover_eip
The handler then resumes execution at address 0x00000001. Every fault becomes
fatal — the precise opposite of the intent, with no warning at compile time and nothing
visibly wrong at the call site. The guard is present, initialised, and inverted.
The fix is to keep the block reachable on paper, so that the label survives to have an address at all:
static volatile DWORD g_never = 0; ... if (g_never) goto *(void *)(uintptr_t)f.recover_eip;
The volatile read forces the test, the test keeps the indirect branch, the branch keeps the
block, and the label becomes real. One more detail earned the hard way: restore
fs:[0] inside the handler before returning, not at the recovery label — the
callee may have installed its own records on stack that is about to be discarded.
It was then tested rather than trusted: a null dereference, a deep fault after a 512-byte frame, calls to address 4 in two conventions, and a call into non-executable data. Each verified caught, stack pointer compared before and after, and a known-good call repeated afterwards to prove the guard had not eaten the process on its way past.
Every offset here is an RVA. That binary sets
DYNAMIC_BASE and relocates — it has been observed at two different load
addresses in the same week. An absolute address written down from one session is wrong in the
next, and someone would act on it.
None of it came from published source. It was disassembled from the binary as shipped, 753,664 bytes, and re-verified adversarially afterwards. That is the difference between a table copied from somewhere and a measurement, and it is the reason the findings above include the one that was wrong.