| name | native-crash-triage |
| description | Root-cause native Bannerlord CTDs (AccessViolation in TaleWorlds.Native.dll) via Event Log offsets, offline disassembly, and live debugger forensics. No symbols needed. |
Native Crash Triage
Names the crash site of a native CTD without symbols and drives it to a root cause. Proven
on the 2026-06-12 v1.4.6 spider campaign: three distinct native sites
(Agent_ai::set_attack_entity, the monster_usage.cpp jump map, the Die-path record
corruption) named and fixed in one day with exactly this protocol.
When to use: any 0xC0000005 / System.AccessViolationException whose stack dies in
TaleWorlds.Native.dll (or another native module). For managed TAOM bugs use /investigate
instead โ this skill is the native-side complement (and /investigate may hand off here).
Iron rule (from /investigate): no fixes without a named site + root cause. Every fix this
protocol has produced was DATA (XML) or a routing patch โ never a blind retry.
Phase 1 โ Collect (no debugger needed)
- Windows Event Log gives the faulting module + offset even after a CTD:
Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Application Error'; StartTime=(Get-Date).AddHours(-6)} |
Where-Object { $_.Message -match "Bannerlord" } |
ForEach-Object { ($_.Message -split "`n" | Select-Object -First 8) -join "`n"; "---" }
The Fault offset IS the RVA. Compare offsets across runs โ identical offset = same
site (discriminates "my fix didn't work" from "a different crash"); this is how Patch47 was
exonerated. Caveat: a crash held by a debugger never reaches WER โ no Event Log entry.
- Game-side timeline: newest
Logs/taom_debug_*.log (game bin) + newest
C:\ProgramData\Mount and Blade II Bannerlord\logs\rgl_log_*.txt โ last lines date the
crash relative to gameplay events (charge orders, scene loads).
- Map an implicated patch to its owner. If a
[BattleLoad] / [SaveLoad] / _PatchN
marker in those logs, or the last managed frame before the native transition, names a TAOM
patch, grep it in docs/reference/harmony-patch-registry.md
โ it maps the patch to its exact target method + status, so you know whether a TAOM hook sits
on the crashing path before blaming the engine. (This is where CLAUDE.md's former "Harmony
Patch Categories" table now lives.)
- Assertion dialogs (engine asserts, not AVs): the dialog is a PAUSED pre-crash state โ
before clicking anything, copy the session's
rgl_log_<pid>/watchdog_log_<pid> AND take a
full dump (Task Manager โ Create dump file, or rundll32 comsvcs.dll, MiniDump <pid> <path> full).
Very early asserts (module scan) leave a 0-byte watchdog log and NO rgl_log โ the dump is then
the only artifact. Post-Ignore Event-Log offsets name the SECONDARY (abort-path) site, not
the assert site. Never Ignore a queue/invariant assert to keep working โ state is corrupted
(2026-07-24 rglConcurrentQueue: Ignore โ permanent loading-screen hang). Editor crashes:
pass --dll pointing at the Win64_Shipping_wEditor TaleWorlds.Native.dll โ offsets
differ from the shipping client build.
Phase 2 โ Name the site (offline, fully scripted)
python tools/native_crash_triage.py --rva 0x<fault_offset>
python tools/native_crash_triage.py --ip 0x<RIP> --base 0x<module_base> --callers 2
Output: exact .pdata function bounds, annotated hexdump, every string the function
references (shipping builds keep assert/trace text โ functions frequently self-identify),
and the caller chain with each caller's strings. Hand-decode the few instructions around the
crash row (the tool shows them); the common patterns:
cmp [reg+disp], imm with reg=0 โ null + field-offset (missing data surface)
- chain-walk loop (
cmp r10d,[rax] / mov rax,[rax+8]) ending in a deref โ hash-map miss
dereferencing its end-sentinel (asserts compiled out of shipping) โ a DATA TABLE is missing
a key. Fix: make the table TOTAL (see feedback_engine_lookup_total_key_coverage memory)
- faulting address โ heap, or an index register holding float bits โ corrupted record
consumed downstream; check binding targets (phantom-animation sweep) and route around if
engine-internal (Patch47 pattern)
Phase 3 โ Live debugger forensics (when a repro is available)
- Attach mixed-mode: VS โ Debug โ Attach to Process โ select
Bannerlord.exe (NEVER
TaleWorlds.MountAndBlade.Launcher.exe) โ Code type: Managed (.NET Framework 4.x) +
Native both checked. Or set the launch profile's Debug engines to "Managed (.NET
Framework) with native" and F5.
- On break: Call Stack (copy entire โ native frames now visible), Registers
(double-click the TOP native frame first; the registers shown belong to the SELECTED frame),
Modules window (Ctrl+Alt+U) for the module base โ
RVA = RIP โ base.
- Managed probes (select a MANAGED frame first โ C# evaluation is blocked while a native
frame is selected;
$exception exists only in managed frames). The Immediate window accepts
lambdas via explicit System.Linq.Enumerable calls:
System.Linq.Enumerable.Count(System.Linq.Enumerable.Where(TaleWorlds.MountAndBlade.Mission.Current.AllAgents, a => a.Monster != null && a.Monster.StringId == "<id>"))
this.GetCurrentAction(0).GetName() // v1.4.6: GetCurrentAction, NOT GetCurrentActionValue
Corrupted/interleaved action names or act_none on a moving agent's channel 0 = poisoned
action records. Module base via probe:
...Cast<System.Diagnostics.ProcessModule>(System.Diagnostics.Process.GetCurrentProcess().Modules), m => m.ModuleName == "TaleWorlds.Native.dll")).BaseAddress.
- ASLR: module bases hold within a boot for repeated launches in practice, but ALWAYS
re-derive the base per process โ never reuse across launches.
Phase 4 โ Fix and verify
Data-table misses โ make the table total (extra rows are inert; missing keys crash). Flag-driven
AI paths โ match the proven baseline exactly (tools/audit_mount_parity.py for mounts).
Engine-internal corruption โ route around (Patch47 dismount-before-death pattern). Then a
control battle that exercises the trigger, and compare the next Event Log offset if it still
crashes โ a NEW offset is progress, not failure.
Full worked example with all three patterns: docs/features/spider.md ("The v1.4.6 engine-bump
campaign"); generalized lessons: memory feedback_engine_lookup_total_key_coverage.