Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions malloc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
107 changes: 107 additions & 0 deletions malloc_test.go
Original file line number Diff line number Diff line change
@@ -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
})
}