171 lines
5.3 KiB
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)
|
|
}
|
|
}
|