From 43b9c6827b851968225f7b4f271dc2548e7b9b19 Mon Sep 17 00:00:00 2001 From: Maksim Merzhanov Date: Mon, 13 Jul 2026 10:12:49 +0300 Subject: [PATCH] arena: align allocs to pointer size --- malloc.go | 4 ++ malloc_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 malloc_test.go diff --git a/malloc.go b/malloc.go index b9cba77..ca18e1d 100644 --- a/malloc.go +++ b/malloc.go @@ -45,6 +45,10 @@ func (a *arena) Alloc(length uintptr) unsafe.Pointer { if length == 0 { panic("can't allocate zero bytes") } + // Round up to pointer alignment: callers store pointer-containing + // structs into returned memory, and the Go runtime's write barrier + // throws on unaligned destinations (issue #25). + length = (length + 7) &^ 7 if length > a.remaining { a.grow() } diff --git a/malloc_test.go b/malloc_test.go new file mode 100644 index 0000000..4ce30d9 --- /dev/null +++ b/malloc_test.go @@ -0,0 +1,107 @@ +package wf + +import ( + "net/netip" + "runtime" + "testing" + "unsafe" +) + +// TestArenaAllocAligned is the deterministic regression test for issue #25: +// every arena allocation must be pointer-aligned, because callers store +// pointer-containing structs (fwpmFilter0 etc.) into the returned memory, +// and the Go runtime's write barrier throws on unaligned destinations. +func TestArenaAllocAligned(t *testing.T) { + var a arena + defer a.Dispose() + + // 17 mirrors unsafe.Sizeof(fwpV6AddrAndMask{}) — the allocation that + // breaks alignment in any rule with IPv6 prefix conditions. + for i := 0; i < 64; i++ { + p := uintptr(a.Alloc(17)) + if p%unsafe.Alignof(uintptr(0)) != 0 { + t.Fatalf("Alloc #%d returned unaligned pointer %#x", i, p) + } + } +} + +// TestToFilter0UnalignedWriteBarrier reproduces the intermittent +// "fatal error: bulkBarrierPreWrite: unaligned arguments" crash from +// issue #25 and makes it near-deterministic. +// +// Mechanism: IPv6 prefix conditions make toCondition0 allocate 17-byte +// fwpV6AddrAndMask values, after which the arena cursor is no longer +// 8-aligned; toFilter0 then stores a pointer-containing struct +// (`*ret = fwpmFilter0{...}`) at the unaligned address. The compiler-emitted +// write barrier (wbMove -> bulkBarrierPreWrite) throws only while a GC mark +// phase is active, which is why the crash is flaky in the wild. Here a +// background goroutine runs back-to-back GC cycles over a pointer-dense +// retained heap, so mark phases cover most of the wall clock and the loop +// below hits one with near certainty. +// +// On a broken arena this is a process-level fatal error, not a catchable +// panic: the test binary dies with the exact production stack. With the +// alignment fix in arena.Alloc it passes. +func TestToFilter0UnalignedWriteBarrier(t *testing.T) { + keepGCOccupied(t) + + lt := layerTypes{ + LayerALEAuthConnectV6: fieldTypes{ + FieldIPRemoteAddress: typeIP, + }, + } + rule := &Rule{ + ID: RuleID{Data1: 0xdeadbeef, Data2: 0xcafe, Data3: 0xf00d}, + Name: "issue #25 repro", + Layer: LayerALEAuthConnectV6, + Weight: 2000, + Action: ActionPermit, + } + for _, p := range []string{"::1/128", "fe80::/10", "fc00::/7", "ff00::/8"} { + rule.Conditions = append(rule.Conditions, &Match{ + Field: FieldIPRemoteAddress, + Op: MatchTypeEqual, + Value: netip.MustParsePrefix(p), + }) + } + + for i := 0; i < 10000; i++ { + var a arena + _, err := toFilter0(&a, rule, lt) + a.Dispose() + if err != nil { + t.Fatalf("toFilter0: %v", err) + } + } +} + +func keepGCOccupied(t *testing.T) { + type node struct { + next *node + pad *[64]byte + } + nodes := make([]*node, 1<<18) + for i := range nodes { + nodes[i] = &node{next: &node{}, pad: new([64]byte)} + } + runtime.KeepAlive(nodes) + + stop := make(chan struct{}) + gcDone := make(chan struct{}) + go func() { + defer close(gcDone) + for { + select { + case <-stop: + return + default: + runtime.GC() + } + } + }() + + t.Cleanup(func() { + close(stop) + <-gcDone + }) +}