package fingerprint import ( "testing" ) const asanTrace = `==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014 READ of size 4 at 0x602000000014 thread T0 #0 0x55a3b4c2d123 in vulnerable_func /home/user/project/src/parser.c:42:13 #1 0x55a3b4c2e456 in process_input /home/user/project/src/main.c:108:5 #2 0x55a3b4c2f789 in main /home/user/project/src/main.c:210:12 #3 0x7f1234567890 in __libc_start_main /build/glibc/csu/../csu/libc-start.c:308:16 #4 0x55a3b4c2a000 in _start (/home/user/project/build/app+0x2a000) ` const asanTrace2 = `==99999==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xbeefcafe0014 READ of size 4 at 0xbeefcafe0014 thread T0 #0 0xdeadbeef1234 in vulnerable_func /different/path/to/parser.c:99:13 #1 0xdeadbeef5678 in process_input /different/path/to/main.c:200:5 #2 0xdeadbeef9abc in main /different/path/to/main.c:300:12 #3 0x7fabcdef0000 in __libc_start_main /build/glibc/csu/../csu/libc-start.c:308:16 ` func TestASanParser(t *testing.T) { frames := ParseASan(asanTrace) if len(frames) == 0 { t.Fatal("expected frames from ASan trace") } if frames[0].Function != "vulnerable_func" { t.Errorf("expected function 'vulnerable_func', got %q", frames[0].Function) } if frames[0].Line != 42 { t.Errorf("expected line 42, got %d", frames[0].Line) } } func TestASanFingerprint_StableAcrossAddressesAndPaths(t *testing.T) { r1 := Compute(asanTrace) r2 := Compute(asanTrace2) if r1 == nil || r2 == nil { t.Fatal("expected non-nil results") } if r1.Fingerprint != r2.Fingerprint { t.Errorf("fingerprints should match across ASLR/path changes:\n %s\n %s", r1.Fingerprint, r2.Fingerprint) } } func TestASanFingerprint_DifferentFunctions(t *testing.T) { different := `==12345==ERROR: AddressSanitizer: heap-use-after-free #0 0x55a3b4c2d123 in other_function /home/user/project/src/parser.c:42:13 #1 0x55a3b4c2e456 in process_input /home/user/project/src/main.c:108:5 ` r1 := Compute(asanTrace) r2 := Compute(different) if r1 == nil || r2 == nil { t.Fatal("expected non-nil results") } if r1.Fingerprint == r2.Fingerprint { t.Error("fingerprints should differ for different stack traces") } } const gdbTrace = `#0 crash_here (ptr=0x0) at /home/user/src/crash.c:15 #1 0x00005555555551a0 in process_data (buf=0x7fffffffe000, len=1024) at /home/user/src/process.c:89 #2 0x0000555555555300 in main (argc=2, argv=0x7fffffffe1a8) at /home/user/src/main.c:42 #3 0x00007ffff7c29d90 in __libc_start_call_main (main=0x555555555280, argc=2, argv=0x7fffffffe1a8) at ../sysdeps/nptl/libc_start_call_main.h:58 ` func TestGDBParser(t *testing.T) { frames := ParseGDB(gdbTrace) if len(frames) == 0 { t.Fatal("expected frames from GDB trace") } if frames[0].Function != "crash_here" { t.Errorf("expected function 'crash_here', got %q", frames[0].Function) } } const zigTrace = `thread 1 panic: index out of bounds /home/user/src/parser.zig:42:13: 0x20da40 in parse (parser) /home/user/src/main.zig:108:5: 0x20e100 in main (main) /usr/lib/zig/std/start.zig:614:22: 0x20f000 in std.start.callMain (main) ` func TestZigParser(t *testing.T) { frames := ParseZig(zigTrace) if len(frames) == 0 { t.Fatal("expected frames from Zig trace") } if frames[0].Function != "parse" { t.Errorf("expected function 'parse', got %q", frames[0].Function) } if frames[0].Line != 42 { t.Errorf("expected line 42, got %d", frames[0].Line) } } func TestNormalization_StripsRuntimeFrames(t *testing.T) { r := Compute(asanTrace) if r == nil { t.Fatal("expected non-nil result") } for _, nf := range r.Normalized { if nf.Function == "__libc_start_main" || nf.Function == "_start" { t.Errorf("runtime frame should have been filtered: %q", nf.Function) } } } func TestNormalization_StripsPathsToFilename(t *testing.T) { r := Compute(asanTrace) if r == nil { t.Fatal("expected non-nil result") } for _, nf := range r.Normalized { if nf.File != "" && nf.File != "parser.c" && nf.File != "main.c" { t.Errorf("expected bare filename, got %q", nf.File) } } } func TestNormalization_MaxFrames(t *testing.T) { // Build a trace with many frames. raw := "==1==ERROR: AddressSanitizer: stack-overflow\n" for i := 0; i < 20; i++ { raw += " #" + itoa(i) + " 0xdead in func_" + itoa(i) + " /a/b/c.c:1:1\n" } r := Compute(raw) if r == nil { t.Fatal("expected non-nil result") } if len(r.Normalized) > maxFrames { t.Errorf("expected at most %d frames, got %d", maxFrames, len(r.Normalized)) } } func itoa(i int) string { return string(rune('0'+i/10)) + string(rune('0'+i%10)) } func TestNormalization_StripsCppTemplates(t *testing.T) { frames := []Frame{ {Function: "std::vector>::push_back", File: "vector.h", Index: 0}, {Function: "MyClass::process", File: "myclass.h", Index: 1}, } normalized := Normalize(frames) for _, nf := range normalized { if nf.Function == "std::vector>::push_back" { t.Error("template params should be stripped") } } } func TestGenericParser(t *testing.T) { raw := `at do_something (util.c:42) at process (main.c:108) at run (runner.c:15) ` frames := ParseGeneric(raw) if len(frames) < 2 { t.Fatalf("expected at least 2 frames, got %d", len(frames)) } if frames[0].Function != "do_something" { t.Errorf("expected 'do_something', got %q", frames[0].Function) } }