Files
deathwatch/process_linux_test.go
emmatherockandClaude Sonnet 5 356df6cbd5 Fix Linux module-bounds detection: extend through Wine's anonymous PE mapping
Running the Linux build against a live game showed it stuck forever on
"scanning signatures": scanMaps only collected mappings whose file matched
eldenring.exe, but this Proton build backs just the 4 KB PE header with the
real path and maps the rest of the module (~94 MB of .text/.rdata/.data,
where every AOB signature lives) as one anonymous mapping with no path.
findModuleBase was handing the scanner the header alone.

moduleSpan (the matching logic, now pulled out of scanMaps as a pure
function for process_linux_test.go) extends the span through contiguous
anonymous mappings that follow the last named one, and stops at the first
mapping with its own path so it can't merge in an unrelated module.
Confirmed against the live process: all three signatures resolve, PlayerIns
confirms, and /deaths reports real numbers end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 01:54:52 -03:00

81 lines
3.3 KiB
Go

//go:build linux
package main
import "testing"
// TestModuleSpanExtendsThroughAnonymousWineMapping is a regression test for
// what running against a real Proton build surfaced: Wine's PE loader maps
// only the PE header as a real file-backed mapping, and the rest of the
// module — .text/.rdata/.data, everything the AOB signatures live in —
// comes right after as one large anonymous mapping with no path. Without
// the extension, findModuleBase would hand the scanner a few KB of PE
// header and nothing else, and every signature scan would fail forever.
func TestModuleSpanExtendsThroughAnonymousWineMapping(t *testing.T) {
lines := []mapLine{
{start: 0x10000, end: 0x12000, path: ""}, // unrelated anonymous mapping before it
{start: 0x140000000, end: 0x140001000, path: "/games/ELDEN RING/Game/eldenring.exe"}, // PE header, file-backed
{start: 0x140001000, end: 0x145e0e000, path: ""}, // the actual module body, anonymous
{start: 0x555591e6c000, end: 0x5555975f4000, path: "[heap]"},
}
base, end, path, ok := moduleSpan(lines, "eldenring.exe")
if !ok {
t.Fatal("expected a match")
}
if base != 0x140000000 {
t.Errorf("base = 0x%X, want 0x140000000", base)
}
if end != 0x145e0e000 {
t.Errorf("end = 0x%X, want 0x145e0e000 (the header alone would give 0x140001000)", end)
}
if path != "/games/ELDEN RING/Game/eldenring.exe" {
t.Errorf("path = %q, want the header's own path", path)
}
}
// TestModuleSpanDoesNotSwallowAFollowingNamedMapping guards the boundary
// of the fix above: the extension must stop at the first mapping that has
// its own path, even if it's perfectly contiguous, so it can never merge
// a genuinely different module into the span.
func TestModuleSpanDoesNotSwallowAFollowingNamedMapping(t *testing.T) {
lines := []mapLine{
{start: 0x140000000, end: 0x140001000, path: "/games/ELDEN RING/Game/eldenring.exe"},
{start: 0x140001000, end: 0x140003000, path: ""}, // module body, anonymous
{start: 0x140003000, end: 0x140010000, path: "/games/ELDEN RING/Game/d3d12.dll"}, // a different, unrelated module
}
_, end, _, ok := moduleSpan(lines, "eldenring.exe")
if !ok {
t.Fatal("expected a match")
}
if end != 0x140003000 {
t.Errorf("end = 0x%X, want 0x140003000 (must stop before d3d12.dll)", end)
}
}
// TestModuleSpanCoversMultipleNamedSegments keeps the ELF-style layout
// (several file-backed segments sharing the module's own name) working
// too, in case a future Wine/Proton build maps it that way instead.
func TestModuleSpanCoversMultipleNamedSegments(t *testing.T) {
lines := []mapLine{
{start: 0x140000000, end: 0x140001000, path: "/games/ELDEN RING/Game/eldenring.exe"},
{start: 0x140001000, end: 0x140002000, path: "/games/ELDEN RING/Game/eldenring.exe"},
{start: 0x140002000, end: 0x140003000, path: "/games/ELDEN RING/Game/eldenring.exe"},
}
base, end, _, ok := moduleSpan(lines, "eldenring.exe")
if !ok || base != 0x140000000 || end != 0x140003000 {
t.Fatalf("got base=0x%X end=0x%X ok=%v, want 0x140000000-0x140003000", base, end, ok)
}
}
func TestModuleSpanNotFound(t *testing.T) {
lines := []mapLine{
{start: 0x1000, end: 0x2000, path: "/games/somethingelse.exe"},
}
if _, _, _, ok := moduleSpan(lines, "eldenring.exe"); ok {
t.Fatal("expected no match")
}
}