Skip to content

Commit df18967

Browse files
committed
internal: introduce FeatureMatrix
Add a map which holds multiple feature tests based on a key. Each feature test executes completely independently of other tests in the matrix, which avoids deadlocks. Also add a testutils helper which checks a FeatureMatrix using the existing helpers.
1 parent ff88e73 commit df18967

File tree

10 files changed

+116
-73
lines changed

10 files changed

+116
-73
lines changed

btf/btf.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ func marshalBTF(types interface{}, strings []byte, bo binary.ByteOrder) []byte {
689689

690690
// haveBTF attempts to load a BTF blob containing an Int. It should pass on any
691691
// kernel that supports BPF_BTF_LOAD.
692-
var haveBTF = internal.FeatureTest("BTF", "4.18", func() error {
692+
var haveBTF = internal.NewFeatureTest("BTF", "4.18", func() error {
693693
var (
694694
types struct {
695695
Integer btfType
@@ -719,7 +719,7 @@ var haveBTF = internal.FeatureTest("BTF", "4.18", func() error {
719719
// haveMapBTF attempts to load a minimal BTF blob containing a Var. It is
720720
// used as a proxy for .bss, .data and .rodata map support, which generally
721721
// come with a Var and Datasec. These were introduced in Linux 5.2.
722-
var haveMapBTF = internal.FeatureTest("Map BTF (Var/Datasec)", "5.2", func() error {
722+
var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", "5.2", func() error {
723723
if err := haveBTF(); err != nil {
724724
return err
725725
}
@@ -760,7 +760,7 @@ var haveMapBTF = internal.FeatureTest("Map BTF (Var/Datasec)", "5.2", func() err
760760
// haveProgBTF attempts to load a BTF blob containing a Func and FuncProto. It
761761
// is used as a proxy for ext_info (func_info) support, which depends on
762762
// Func(Proto) by definition.
763-
var haveProgBTF = internal.FeatureTest("Program BTF (func/line_info)", "5.0", func() error {
763+
var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", "5.0", func() error {
764764
if err := haveBTF(); err != nil {
765765
return err
766766
}
@@ -795,7 +795,7 @@ var haveProgBTF = internal.FeatureTest("Program BTF (func/line_info)", "5.0", fu
795795
return nil
796796
})
797797

798-
var haveFuncLinkage = internal.FeatureTest("BTF func linkage", "5.6", func() error {
798+
var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func() error {
799799
if err := haveProgBTF(); err != nil {
800800
return err
801801
}

internal/feature.go

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,18 @@ func (ufe *UnsupportedFeatureError) Is(target error) bool {
3131
return target == ErrNotSupported
3232
}
3333

34-
type featureTest struct {
35-
sync.RWMutex
34+
// FeatureTest caches the result of a [FeatureTestFn].
35+
//
36+
// Fields should not be modified after creation.
37+
type FeatureTest struct {
38+
// The name of the feature being detected.
39+
Name string
40+
// Version in in the form Major.Minor[.Patch].
41+
Version string
42+
// The feature test itself.
43+
Fn FeatureTestFn
44+
45+
mu sync.RWMutex
3646
successful bool
3747
result error
3848
}
@@ -47,54 +57,76 @@ type featureTest struct {
4757
// err != nil: the test couldn't be executed
4858
type FeatureTestFn func() error
4959

50-
// FeatureTest wraps a function so that it is run at most once.
51-
//
52-
// name should identify the tested feature, while version must be in the
53-
// form Major.Minor[.Patch].
60+
// NewFeatureTest is a convenient way to create a single [FeatureTest].
61+
func NewFeatureTest(name, version string, fn FeatureTestFn) func() error {
62+
return (&FeatureTest{
63+
Name: name,
64+
Version: version,
65+
Fn: fn,
66+
}).Result
67+
}
68+
69+
// Result returns the outcome of a feature test, executing it if necessary.
5470
//
55-
// Returns an error wrapping ErrNotSupported if the feature is not supported.
56-
func FeatureTest(name, version string, fn FeatureTestFn) func() error {
57-
ft := new(featureTest)
58-
return func() error {
59-
ft.RLock()
60-
if ft.successful {
61-
defer ft.RUnlock()
62-
return ft.result
63-
}
64-
ft.RUnlock()
65-
ft.Lock()
66-
defer ft.Unlock()
67-
// check one more time on the off
68-
// chance that two go routines
69-
// were able to call into the write
70-
// lock
71-
if ft.successful {
72-
return ft.result
71+
// See [FeatureTestFn] for the meaning of the returned error.
72+
func (ft *FeatureTest) Result() error {
73+
ft.mu.RLock()
74+
if ft.successful {
75+
defer ft.mu.RUnlock()
76+
return ft.result
77+
}
78+
ft.mu.RUnlock()
79+
ft.mu.Lock()
80+
defer ft.mu.Unlock()
81+
// check one more time on the off
82+
// chance that two go routines
83+
// were able to call into the write
84+
// lock
85+
if ft.successful {
86+
return ft.result
87+
}
88+
err := ft.Fn()
89+
switch {
90+
case errors.Is(err, ErrNotSupported):
91+
v, err := NewVersion(ft.Version)
92+
if err != nil {
93+
return err
7394
}
74-
err := fn()
75-
switch {
76-
case errors.Is(err, ErrNotSupported):
77-
v, err := NewVersion(version)
78-
if err != nil {
79-
return err
80-
}
81-
82-
ft.result = &UnsupportedFeatureError{
83-
MinimumVersion: v,
84-
Name: name,
85-
}
86-
fallthrough
87-
88-
case err == nil:
89-
ft.successful = true
90-
91-
default:
92-
// We couldn't execute the feature test to a point
93-
// where it could make a determination.
94-
// Don't cache the result, just return it.
95-
return fmt.Errorf("detect support for %s: %w", name, err)
95+
96+
ft.result = &UnsupportedFeatureError{
97+
MinimumVersion: v,
98+
Name: ft.Name,
9699
}
100+
fallthrough
97101

98-
return ft.result
102+
case err == nil:
103+
ft.successful = true
104+
105+
default:
106+
// We couldn't execute the feature test to a point
107+
// where it could make a determination.
108+
// Don't cache the result, just return it.
109+
return fmt.Errorf("detect support for %s: %w", ft.Name, err)
99110
}
111+
112+
return ft.result
113+
}
114+
115+
// FeatureMatrix groups multiple related feature tests into a map.
116+
//
117+
// Useful when there is a small number of discrete features.
118+
//
119+
// It must not be modified concurrently with calling [FeatureMatrix.Result].
120+
type FeatureMatrix[K comparable] map[K]*FeatureTest
121+
122+
// Result returns the outcome of the feature test for the given key.
123+
//
124+
// It's safe to call this function concurrently.
125+
func (fm FeatureMatrix[K]) Result(key K) error {
126+
ft, ok := fm[key]
127+
if !ok {
128+
return fmt.Errorf("no feature probe for %v", key)
129+
}
130+
131+
return ft.Result()
100132
}

internal/feature_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func TestFeatureTest(t *testing.T) {
1010
var called bool
1111

12-
fn := FeatureTest("foo", "1.0", func() error {
12+
fn := NewFeatureTest("foo", "1.0", func() error {
1313
called = true
1414
return nil
1515
})
@@ -27,7 +27,7 @@ func TestFeatureTest(t *testing.T) {
2727
t.Error("Unexpected negative result:", err)
2828
}
2929

30-
fn = FeatureTest("bar", "2.1.1", func() error {
30+
fn = NewFeatureTest("bar", "2.1.1", func() error {
3131
return ErrNotSupported
3232
})
3333

@@ -54,7 +54,7 @@ func TestFeatureTest(t *testing.T) {
5454
t.Error("Didn't cache an error wrapping ErrNotSupported")
5555
}
5656

57-
fn = FeatureTest("bar", "2.1.1", func() error {
57+
fn = NewFeatureTest("bar", "2.1.1", func() error {
5858
return errors.New("foo")
5959
})
6060

internal/testutils/feature.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ func MustKernelVersion() internal.Version {
1717
}
1818

1919
func CheckFeatureTest(t *testing.T, fn func() error) {
20-
t.Helper()
20+
checkFeatureTestError(t, fn())
21+
}
2122

22-
err := fn()
23+
func checkFeatureTestError(t *testing.T, err error) {
2324
if err == nil {
2425
return
2526
}
@@ -32,6 +33,16 @@ func CheckFeatureTest(t *testing.T, fn func() error) {
3233
}
3334
}
3435

36+
func CheckFeatureMatrix[K comparable](t *testing.T, fm internal.FeatureMatrix[K]) {
37+
t.Helper()
38+
39+
for key, ft := range fm {
40+
t.Run(ft.Name, func(t *testing.T) {
41+
checkFeatureTestError(t, fm.Result(key))
42+
})
43+
}
44+
}
45+
3546
func SkipIfNotSupported(tb testing.TB, err error) {
3647
tb.Helper()
3748

link/kprobe_multi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func (kml *kprobeMultiLink) Unpin() error {
138138
return fmt.Errorf("unpin kprobe_multi: %w", ErrNotSupported)
139139
}
140140

141-
var haveBPFLinkKprobeMulti = internal.FeatureTest("bpf_link_kprobe_multi", "5.18", func() error {
141+
var haveBPFLinkKprobeMulti = internal.NewFeatureTest("bpf_link_kprobe_multi", "5.18", func() error {
142142
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
143143
Name: "probe_kpm_link",
144144
Type: ebpf.Kprobe,

link/perf_event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ func readUint64FromFileOnce(format string, path ...string) (uint64, error) {
380380
//
381381
// https://elixir.bootlin.com/linux/v5.16.8/source/kernel/bpf/syscall.c#L4307
382382
// https://github.com/torvalds/linux/commit/b89fbfbb854c9afc3047e8273cc3a694650b802e
383-
var haveBPFLinkPerfEvent = internal.FeatureTest("bpf_link_perf_event", "5.15", func() error {
383+
var haveBPFLinkPerfEvent = internal.NewFeatureTest("bpf_link_perf_event", "5.15", func() error {
384384
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
385385
Name: "probe_bpf_perf_link",
386386
Type: ebpf.Kprobe,

link/syscalls.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
KprobeMultiType = sys.BPF_LINK_TYPE_KPROBE_MULTI
2727
)
2828

29-
var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() error {
29+
var haveProgAttach = internal.NewFeatureTest("BPF_PROG_ATTACH", "4.10", func() error {
3030
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
3131
Type: ebpf.CGroupSKB,
3232
License: "MIT",
@@ -46,7 +46,7 @@ var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() erro
4646
return nil
4747
})
4848

49-
var haveProgAttachReplace = internal.FeatureTest("BPF_PROG_ATTACH atomic replacement", "5.5", func() error {
49+
var haveProgAttachReplace = internal.NewFeatureTest("BPF_PROG_ATTACH atomic replacement", "5.5", func() error {
5050
if err := haveProgAttach(); err != nil {
5151
return err
5252
}
@@ -86,7 +86,7 @@ var haveProgAttachReplace = internal.FeatureTest("BPF_PROG_ATTACH atomic replace
8686
return err
8787
})
8888

89-
var haveBPFLink = internal.FeatureTest("bpf_link", "5.7", func() error {
89+
var haveBPFLink = internal.NewFeatureTest("bpf_link", "5.7", func() error {
9090
attr := sys.LinkCreateAttr{
9191
// This is a hopefully invalid file descriptor, which triggers EBADF.
9292
TargetFd: ^uint32(0),

link/uprobe.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var (
1818
uprobeRefCtrOffsetPMUPath = "/sys/bus/event_source/devices/uprobe/format/ref_ctr_offset"
1919
// elixir.bootlin.com/linux/v5.15-rc7/source/kernel/events/core.c#L9799
2020
uprobeRefCtrOffsetShift = 32
21-
haveRefCtrOffsetPMU = internal.FeatureTest("RefCtrOffsetPMU", "4.20", func() error {
21+
haveRefCtrOffsetPMU = internal.NewFeatureTest("RefCtrOffsetPMU", "4.20", func() error {
2222
_, err := os.Stat(uprobeRefCtrOffsetPMUPath)
2323
if err != nil {
2424
return internal.ErrNotSupported

prog.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ func (p *Program) Benchmark(in []byte, repeat int, reset func()) (uint32, time.D
593593
return ret, total, nil
594594
}
595595

596-
var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() error {
596+
var haveProgTestRun = internal.NewFeatureTest("BPF_PROG_TEST_RUN", "4.12", func() error {
597597
prog, err := NewProgram(&ProgramSpec{
598598
// SocketFilter does not require privileges on newer kernels.
599599
Type: SocketFilter,

syscalls.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func progLoad(insns asm.Instructions, typ ProgramType, license string) (*sys.FD,
4747
})
4848
}
4949

50-
var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() error {
50+
var haveNestedMaps = internal.NewFeatureTest("nested maps", "4.12", func() error {
5151
_, err := sys.MapCreate(&sys.MapCreateAttr{
5252
MapType: sys.MapType(ArrayOfMaps),
5353
KeySize: 4,
@@ -65,7 +65,7 @@ var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() error {
6565
return err
6666
})
6767

68-
var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps", "5.2", func() error {
68+
var haveMapMutabilityModifiers = internal.NewFeatureTest("read- and write-only maps", "5.2", func() error {
6969
// This checks BPF_F_RDONLY_PROG and BPF_F_WRONLY_PROG. Since
7070
// BPF_MAP_FREEZE appeared in 5.2 as well we don't do a separate check.
7171
m, err := sys.MapCreate(&sys.MapCreateAttr{
@@ -82,7 +82,7 @@ var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps
8282
return nil
8383
})
8484

85-
var haveMmapableMaps = internal.FeatureTest("mmapable maps", "5.5", func() error {
85+
var haveMmapableMaps = internal.NewFeatureTest("mmapable maps", "5.5", func() error {
8686
// This checks BPF_F_MMAPABLE, which appeared in 5.5 for array maps.
8787
m, err := sys.MapCreate(&sys.MapCreateAttr{
8888
MapType: sys.MapType(Array),
@@ -98,7 +98,7 @@ var haveMmapableMaps = internal.FeatureTest("mmapable maps", "5.5", func() error
9898
return nil
9999
})
100100

101-
var haveInnerMaps = internal.FeatureTest("inner maps", "5.10", func() error {
101+
var haveInnerMaps = internal.NewFeatureTest("inner maps", "5.10", func() error {
102102
// This checks BPF_F_INNER_MAP, which appeared in 5.10.
103103
m, err := sys.MapCreate(&sys.MapCreateAttr{
104104
MapType: sys.MapType(Array),
@@ -114,7 +114,7 @@ var haveInnerMaps = internal.FeatureTest("inner maps", "5.10", func() error {
114114
return nil
115115
})
116116

117-
var haveNoPreallocMaps = internal.FeatureTest("prealloc maps", "4.6", func() error {
117+
var haveNoPreallocMaps = internal.NewFeatureTest("prealloc maps", "4.6", func() error {
118118
// This checks BPF_F_NO_PREALLOC, which appeared in 4.6.
119119
m, err := sys.MapCreate(&sys.MapCreateAttr{
120120
MapType: sys.MapType(Hash),
@@ -154,7 +154,7 @@ func wrapMapError(err error) error {
154154
return err
155155
}
156156

157-
var haveObjName = internal.FeatureTest("object names", "4.15", func() error {
157+
var haveObjName = internal.NewFeatureTest("object names", "4.15", func() error {
158158
attr := sys.MapCreateAttr{
159159
MapType: sys.MapType(Array),
160160
KeySize: 4,
@@ -172,7 +172,7 @@ var haveObjName = internal.FeatureTest("object names", "4.15", func() error {
172172
return nil
173173
})
174174

175-
var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func() error {
175+
var objNameAllowsDot = internal.NewFeatureTest("dot in object names", "5.2", func() error {
176176
if err := haveObjName(); err != nil {
177177
return err
178178
}
@@ -194,7 +194,7 @@ var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func()
194194
return nil
195195
})
196196

197-
var haveBatchAPI = internal.FeatureTest("map batch api", "5.6", func() error {
197+
var haveBatchAPI = internal.NewFeatureTest("map batch api", "5.6", func() error {
198198
var maxEntries uint32 = 2
199199
attr := sys.MapCreateAttr{
200200
MapType: sys.MapType(Hash),
@@ -226,7 +226,7 @@ var haveBatchAPI = internal.FeatureTest("map batch api", "5.6", func() error {
226226
return nil
227227
})
228228

229-
var haveProbeReadKernel = internal.FeatureTest("bpf_probe_read_kernel", "5.5", func() error {
229+
var haveProbeReadKernel = internal.NewFeatureTest("bpf_probe_read_kernel", "5.5", func() error {
230230
insns := asm.Instructions{
231231
asm.Mov.Reg(asm.R1, asm.R10),
232232
asm.Add.Imm(asm.R1, -8),
@@ -244,7 +244,7 @@ var haveProbeReadKernel = internal.FeatureTest("bpf_probe_read_kernel", "5.5", f
244244
return nil
245245
})
246246

247-
var haveBPFToBPFCalls = internal.FeatureTest("bpf2bpf calls", "4.16", func() error {
247+
var haveBPFToBPFCalls = internal.NewFeatureTest("bpf2bpf calls", "4.16", func() error {
248248
insns := asm.Instructions{
249249
asm.Call.Label("prog2").WithSymbol("prog1"),
250250
asm.Return(),

0 commit comments

Comments
 (0)