cairn/internal/fingerprint/fingerprint_test.go

171 lines
5.3 KiB
Go

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<int, std::allocator<int>>::push_back", File: "vector.h", Index: 0},
{Function: "MyClass<Foo>::process", File: "myclass.h", Index: 1},
}
normalized := Normalize(frames)
for _, nf := range normalized {
if nf.Function == "std::vector<int, std::allocator<int>>::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)
}
}