From a5b967c82a47fd6bda8c7cd23f837e61b5cc320e Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Thu, 14 Jul 2022 21:58:45 +0000 Subject: [PATCH 1/4] btf: allow passing *Type to Spec.TypeByName --- btf/btf.go | 12 ++++++++++-- btf/btf_test.go | 6 ++++++ prog.go | 22 ++++++++-------------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/btf/btf.go b/btf/btf.go index a5969332a..59485abaf 100644 --- a/btf/btf.go +++ b/btf/btf.go @@ -609,6 +609,9 @@ func (s *Spec) AnyTypeByName(name string) (Type, error) { // Type exists in the Spec. If multiple candidates are found, // an error is returned. func (s *Spec) TypeByName(name string, typ interface{}) error { + typeInterface := reflect.TypeOf((*Type)(nil)).Elem() + + // typ may be **T or *Type typValue := reflect.ValueOf(typ) if typValue.Kind() != reflect.Ptr { return fmt.Errorf("%T is not a pointer", typ) @@ -620,7 +623,12 @@ func (s *Spec) TypeByName(name string, typ interface{}) error { } wanted := typPtr.Type() - if !wanted.AssignableTo(reflect.TypeOf((*Type)(nil)).Elem()) { + if wanted == typeInterface { + // This is *Type. Unwrap the value's type. + wanted = typPtr.Elem().Type() + } + + if !wanted.AssignableTo(typeInterface) { return fmt.Errorf("%T does not satisfy Type interface", typ) } @@ -643,7 +651,7 @@ func (s *Spec) TypeByName(name string, typ interface{}) error { } if candidate == nil { - return fmt.Errorf("type %s: %w", name, ErrNotFound) + return fmt.Errorf("%s %s: %w", wanted, name, ErrNotFound) } typPtr.Set(reflect.ValueOf(candidate)) diff --git a/btf/btf_test.go b/btf/btf_test.go index 05077f3f3..29328c7b8 100644 --- a/btf/btf_test.go +++ b/btf/btf_test.go @@ -130,6 +130,12 @@ func TestTypeByName(t *testing.T) { t.Fatal("multiple TypeByName calls for `iphdr` name do not return the same addresses") } + // It's valid to pass a *Type to TypeByName. + typ := Type(iphdr2) + if err := spec.TypeByName("iphdr", &typ); err != nil { + t.Fatal("Can't look up using *Type:", err) + } + // Excerpt from linux/ip.h, https://elixir.bootlin.com/linux/latest/A/ident/iphdr // // struct iphdr { diff --git a/prog.go b/prog.go index 675edc711..a6c8a034a 100644 --- a/prog.go +++ b/prog.go @@ -782,29 +782,34 @@ func findTargetInKernel(spec *btf.Spec, name string, progType ProgramType, attac var ( typeName, featureName string - isBTFTypeFunc = true + target btf.Type ) switch (match{progType, attachType}) { case match{LSM, AttachLSMMac}: typeName = "bpf_lsm_" + name featureName = name + " LSM hook" + target = (*btf.Func)(nil) case match{Tracing, AttachTraceIter}: typeName = "bpf_iter_" + name featureName = name + " iterator" + target = (*btf.Func)(nil) case match{Tracing, AttachTraceFEntry}: typeName = name featureName = fmt.Sprintf("fentry %s", name) + target = (*btf.Func)(nil) case match{Tracing, AttachTraceFExit}: typeName = name featureName = fmt.Sprintf("fexit %s", name) + target = (*btf.Func)(nil) case match{Tracing, AttachModifyReturn}: typeName = name featureName = fmt.Sprintf("fmod_ret %s", name) + target = (*btf.Func)(nil) case match{Tracing, AttachTraceRawTp}: typeName = fmt.Sprintf("btf_trace_%s", name) featureName = fmt.Sprintf("raw_tp %s", name) - isBTFTypeFunc = false + target = (*btf.Typedef)(nil) default: return 0, errUnrecognizedAttachType } @@ -814,18 +819,7 @@ func findTargetInKernel(spec *btf.Spec, name string, progType ProgramType, attac return 0, fmt.Errorf("load kernel spec: %w", err) } - var target btf.Type - if isBTFTypeFunc { - var targetFunc *btf.Func - err = spec.TypeByName(typeName, &targetFunc) - target = targetFunc - } else { - var targetTypedef *btf.Typedef - err = spec.TypeByName(typeName, &targetTypedef) - target = targetTypedef - } - - if err != nil { + if err := spec.TypeByName(typeName, &target); err != nil { if errors.Is(err, btf.ErrNotFound) { return 0, &internal.UnsupportedFeatureError{ Name: featureName, From 2b8def66cc76a75d1a5643b632d2175b668b8e7c Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Tue, 19 Jul 2022 11:48:06 +0000 Subject: [PATCH 2/4] btf: make HandleIterator own the Handle Add HandleIterator.Handle instead of taking **Handle in Next(). This allows adding a Take() function which makes it clearer how ownership is handled in code using HandleIterator. --- btf/handle.go | 47 ++++++++++++++++++++++++++++------------------ btf/handle_test.go | 44 +++++++++++++++++++++++++------------------ 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/btf/handle.go b/btf/handle.go index 128e9b35c..9c36e6909 100644 --- a/btf/handle.go +++ b/btf/handle.go @@ -71,48 +71,59 @@ func (i *HandleInfo) IsModule() bool { // HandleIterator allows enumerating BTF blobs loaded into the kernel. type HandleIterator struct { - // The ID of the last retrieved handle. Only valid after a call to Next. - ID ID - err error + // The ID of the current handle. Only valid after a call to Next. + ID ID + // The current Handle. Only valid until a call to Next. + // See Take if you want to retain the handle. + Handle *Handle + err error } -// Next retrieves a handle for the next BTF blob. -// -// [Handle.Close] is called if *handle is non-nil to avoid leaking fds. +// Next retrieves a handle for the next BTF object. // -// Returns true if another BTF blob was found. Call [HandleIterator.Err] after +// Returns true if another BTF object was found. Call [HandleIterator.Err] after // the function returns false. -func (it *HandleIterator) Next(handle **Handle) bool { - if *handle != nil { - (*handle).Close() - *handle = nil - } - +func (it *HandleIterator) Next() bool { id := it.ID for { attr := &sys.BtfGetNextIdAttr{Id: id} err := sys.BtfGetNextId(attr) if errors.Is(err, os.ErrNotExist) { // There are no more BTF objects. - return false + break } else if err != nil { it.err = fmt.Errorf("get next BTF ID: %w", err) - return false + break } id = attr.NextId - *handle, err = NewHandleFromID(id) + handle, err := NewHandleFromID(id) if errors.Is(err, os.ErrNotExist) { // Try again with the next ID. continue } else if err != nil { it.err = fmt.Errorf("retrieve handle for ID %d: %w", id, err) - return false + break } - it.ID = id + it.Handle.Close() + it.ID, it.Handle = id, handle return true } + + // No more handles or we encountered an error. + it.Handle.Close() + it.Handle = nil + return false +} + +// Take the ownership of the current handle. +// +// It's the callers responsibility to close the handle. +func (it *HandleIterator) Take() *Handle { + handle := it.Handle + it.Handle = nil + return handle } // Err returns an error if iteration failed for some reason. diff --git a/btf/handle_test.go b/btf/handle_test.go index fd6db85b8..4a00b04d3 100644 --- a/btf/handle_test.go +++ b/btf/handle_test.go @@ -14,20 +14,19 @@ func TestHandleIterator(t *testing.T) { // See https://github.com/torvalds/linux/commit/5329722057d41aebc31e391907a501feaa42f7d9 testutils.SkipOnOldKernel(t, "5.11", "vmlinux BTF ID") - var h *btf.Handle - defer h.Close() - it := new(btf.HandleIterator) - if !it.Next(&h) { + defer it.Handle.Close() + + if !it.Next() { t.Fatalf("No BTF loaded") } - if h == nil { + if it.Handle == nil { t.Fatal("Next doesn't assign handle") } prev := it.ID - for it.Next(&h) { + for it.Next() { // Iterate all loaded BTF. - if h == nil { + if it.Handle == nil { t.Fatal("Next doesn't assign handle") } if it.ID == prev { @@ -39,7 +38,7 @@ func TestHandleIterator(t *testing.T) { t.Fatal("Iteration returned an error:", err) } - if h != nil { + if it.Handle != nil { t.Fatal("Next doesn't clean up handle on last iteration") } if prev != it.ID { @@ -55,8 +54,10 @@ func TestParseModuleSplitSpec(t *testing.T) { defer module.Close() it := new(btf.HandleIterator) - for it.Next(&module) { - info, err := module.Info() + defer it.Handle.Close() + + for it.Next() { + info, err := it.Handle.Info() if err != nil { t.Fatal(err) } @@ -66,6 +67,7 @@ func TestParseModuleSplitSpec(t *testing.T) { } t.Log("Using module", info.Name) + module = it.Take() break } if err := it.Err(); err != nil { @@ -80,8 +82,10 @@ func TestParseModuleSplitSpec(t *testing.T) { defer vmlinux.Close() it = new(btf.HandleIterator) - for it.Next(&vmlinux) { - info, err := vmlinux.Info() + defer it.Handle.Close() + + for it.Next() { + info, err := it.Handle.Info() if err != nil { t.Fatal(err) } @@ -90,6 +94,7 @@ func TestParseModuleSplitSpec(t *testing.T) { continue } + vmlinux = it.Take() break } if err := it.Err(); err != nil { @@ -117,13 +122,16 @@ func TestParseModuleSplitSpec(t *testing.T) { } func ExampleHandleIterator() { - var handle *btf.Handle - // Ensure that handle is cleaned up. This is valid for nil handles as well. - defer handle.Close() - it := new(btf.HandleIterator) - for it.Next(&handle) { - fmt.Printf("Found handle with ID %d\n", it.ID) + defer it.Handle.Close() + + for it.Next() { + info, err := it.Handle.Info() + if err != nil { + panic(err) + } + + fmt.Printf("Found handle with ID %d and name %s\n", it.ID, info.Name) } if err := it.Err(); err != nil { panic(err) From efc4a24f4d6035d6d1130fd3ccae8febc2fbc343 Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Mon, 18 Jul 2022 15:12:14 +0000 Subject: [PATCH 3/4] btf: add FindHandle Add a helper that iterates all BTF objects in the kernel and returns the first one for which a user supplied callback returns true. --- btf/handle.go | 27 ++++++++++++++++++++ btf/handle_test.go | 61 ++++++++++------------------------------------ 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/btf/handle.go b/btf/handle.go index 9c36e6909..3d540e49c 100644 --- a/btf/handle.go +++ b/btf/handle.go @@ -130,3 +130,30 @@ func (it *HandleIterator) Take() *Handle { func (it *HandleIterator) Err() error { return it.err } + +// FindHandle returns the first handle for which predicate returns true. +// +// Requires CAP_SYS_ADMIN. +// +// Returns an error wrapping ErrNotFound if predicate never returns true or if +// there is no BTF loaded into the kernel. +func FindHandle(predicate func(info *HandleInfo) bool) (*Handle, error) { + it := new(HandleIterator) + defer it.Handle.Close() + + for it.Next() { + info, err := it.Handle.Info() + if err != nil { + return nil, fmt.Errorf("info for ID %d: %w", it.ID, err) + } + + if predicate(info) { + return it.Take(), nil + } + } + if err := it.Err(); err != nil { + return nil, fmt.Errorf("iterate handles: %w", err) + } + + return nil, fmt.Errorf("find handle: %w", ErrNotFound) +} diff --git a/btf/handle_test.go b/btf/handle_test.go index 4a00b04d3..d322ce5b7 100644 --- a/btf/handle_test.go +++ b/btf/handle_test.go @@ -50,60 +50,25 @@ func TestParseModuleSplitSpec(t *testing.T) { // See TestNewHandleFromID for reasoning. testutils.SkipOnOldKernel(t, "5.11", "vmlinux BTF ID") - var module *btf.Handle - defer module.Close() - - it := new(btf.HandleIterator) - defer it.Handle.Close() - - for it.Next() { - info, err := it.Handle.Info() - if err != nil { - t.Fatal(err) + module, err := btf.FindHandle(func(info *btf.HandleInfo) bool { + if info.IsModule() { + t.Log("Using module", info.Name) + return true } - - if !info.IsModule() { - continue - } - - t.Log("Using module", info.Name) - module = it.Take() - break - } - if err := it.Err(); err != nil { + return false + }) + if err != nil { t.Fatal(err) } + defer module.Close() - if module == nil { - t.Fatal("No BTF for kernel module found") - } - - var vmlinux *btf.Handle - defer vmlinux.Close() - - it = new(btf.HandleIterator) - defer it.Handle.Close() - - for it.Next() { - info, err := it.Handle.Info() - if err != nil { - t.Fatal(err) - } - - if !info.IsVmlinux() { - continue - } - - vmlinux = it.Take() - break - } - if err := it.Err(); err != nil { + vmlinux, err := btf.FindHandle(func(info *btf.HandleInfo) bool { + return info.IsVmlinux() + }) + if err != nil { t.Fatal(err) } - - if vmlinux == nil { - t.Fatal("No BTF for kernel found") - } + defer vmlinux.Close() vmlinuxSpec, err := vmlinux.Spec(nil) if err != nil { From b19a393b43f6508ec7422aaedb62bbaf9daf0acb Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Tue, 19 Jul 2022 11:54:36 +0000 Subject: [PATCH 4/4] program: support tracing of kernel modules Allow tracing functions in kernel modules via fentry, fexit, fmod_ret and tp_btf programs. The behaviour follows libbpf and is transparent to the user: if we can't find a target in vmlinux we attempt to find it in any loaded kernel module. Refactor TestProgramTypeLSM to test attaching via BTF. This removes LSM tests for BPF_F_SLEEPABLE, since they don't excercise library behaviour beyond passing ProgramSpec.Flags to the kernel. Updates #705 --- internal/cmd/gentypes/main.go | 1 + internal/sys/types.go | 2 +- prog.go | 101 +++++++++++++++++++++++++++------- prog_test.go | 99 ++++++++++++++++++++++++--------- 4 files changed, 156 insertions(+), 47 deletions(-) diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go index 73f6ba03c..376d405e0 100644 --- a/internal/cmd/gentypes/main.go +++ b/internal/cmd/gentypes/main.go @@ -285,6 +285,7 @@ import ( "fd_array", "core_relos", ), + choose(20, "attach_btf_obj_fd"), }, }, { diff --git a/internal/sys/types.go b/internal/sys/types.go index 291e3a619..a5f90e70c 100644 --- a/internal/sys/types.go +++ b/internal/sys/types.go @@ -967,7 +967,7 @@ type ProgLoadAttr struct { LineInfo Pointer LineInfoCnt uint32 AttachBtfId uint32 - AttachProgFd uint32 + AttachBtfObjFd uint32 CoreReloCnt uint32 FdArray Pointer CoreRelos Pointer diff --git a/prog.go b/prog.go index a6c8a034a..844e1f989 100644 --- a/prog.go +++ b/prog.go @@ -43,8 +43,7 @@ type ProgramOptions struct { // Controls the output buffer size for the verifier. Defaults to // DefaultVerifierLogSize. LogSize int - // Type information used for CO-RE relocations and when attaching to - // kernel functions. + // Type information used for CO-RE relocations. // // This is useful in environments where the kernel BTF is not available // (containers) or where it is in a non-standard location. Defaults to @@ -206,14 +205,12 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand attr.ProgName = sys.NewObjName(spec.Name) } - kernelTypes := opts.KernelTypes - insns := make(asm.Instructions, len(spec.Instructions)) copy(insns, spec.Instructions) var btfDisabled bool if spec.BTF != nil { - if err := applyRelocations(insns, spec.BTF, kernelTypes); err != nil { + if err := applyRelocations(insns, spec.BTF, opts.KernelTypes); err != nil { return nil, fmt.Errorf("apply CO-RE relocations: %w", err) } @@ -262,10 +259,10 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand } attr.AttachBtfId = uint32(targetID) - attr.AttachProgFd = uint32(spec.AttachTarget.FD()) + attr.AttachBtfObjFd = uint32(spec.AttachTarget.FD()) defer runtime.KeepAlive(spec.AttachTarget) } else if spec.AttachTo != "" { - targetID, err := findTargetInKernel(kernelTypes, spec.AttachTo, spec.Type, spec.AttachType) + module, targetID, err := findTargetInKernel(spec.AttachTo, spec.Type, spec.AttachType) if err != nil && !errors.Is(err, errUnrecognizedAttachType) { // We ignore errUnrecognizedAttachType since AttachTo may be non-empty // for programs that don't attach anywhere. @@ -273,6 +270,10 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand } attr.AttachBtfId = uint32(targetID) + if module != nil { + attr.AttachBtfObjFd = uint32(module.FD()) + defer module.Close() + } } logSize := DefaultVerifierLogSize @@ -770,11 +771,15 @@ var errUnrecognizedAttachType = errors.New("unrecognized attach type") // find an attach target type in the kernel. // -// spec may be nil and defaults to the canonical kernel BTF. name together with -// progType and attachType determine which type we need to attach to. +// name, progType and attachType determine which type we need to attach to. // -// Returns errUnrecognizedAttachType. -func findTargetInKernel(spec *btf.Spec, name string, progType ProgramType, attachType AttachType) (btf.TypeID, error) { +// The attach target may be in a loaded kernel module. +// In that case the returned handle will be non-nil. +// The caller is responsible for closing the handle. +// +// Returns errUnrecognizedAttachType if the combination of progType and attachType +// is not recognised. +func findTargetInKernel(name string, progType ProgramType, attachType AttachType) (*btf.Handle, btf.TypeID, error) { type match struct { p ProgramType a AttachType @@ -811,24 +816,80 @@ func findTargetInKernel(spec *btf.Spec, name string, progType ProgramType, attac featureName = fmt.Sprintf("raw_tp %s", name) target = (*btf.Typedef)(nil) default: - return 0, errUnrecognizedAttachType + return nil, 0, errUnrecognizedAttachType } - spec, err := maybeLoadKernelBTF(spec) + // maybeLoadKernelBTF may return external BTF if /sys/... is not available. + // Ideally we shouldn't use external BTF here, since we might try to use + // it for parsing kmod split BTF later on. That seems unlikely to work. + spec, err := maybeLoadKernelBTF(nil) if err != nil { - return 0, fmt.Errorf("load kernel spec: %w", err) + return nil, 0, fmt.Errorf("load kernel spec: %w", err) } - if err := spec.TypeByName(typeName, &target); err != nil { + err = spec.TypeByName(typeName, &target) + if errors.Is(err, btf.ErrNotFound) { + module, id, err := findTargetInModule(spec, typeName, target) if errors.Is(err, btf.ErrNotFound) { - return 0, &internal.UnsupportedFeatureError{ - Name: featureName, - } + return nil, 0, &internal.UnsupportedFeatureError{Name: featureName} + } + if err != nil { + return nil, 0, fmt.Errorf("find target for %s in modules: %w", featureName, err) + } + return module, id, nil + } + if err != nil { + return nil, 0, fmt.Errorf("find target for %s in vmlinux: %w", featureName, err) + } + + id, err := spec.TypeID(target) + return nil, id, err +} + +// find an attach target type in a kernel module. +// +// vmlinux must contain the kernel's types and is used to parse kmod BTF. +// +// Returns btf.ErrNotFound if the target can't be found in any module. +func findTargetInModule(vmlinux *btf.Spec, typeName string, target btf.Type) (*btf.Handle, btf.TypeID, error) { + it := new(btf.HandleIterator) + defer it.Handle.Close() + + for it.Next() { + info, err := it.Handle.Info() + if err != nil { + return nil, 0, fmt.Errorf("get info for BTF ID %d: %w", it.ID, err) + } + + if !info.IsModule() { + continue + } + + spec, err := it.Handle.Spec(vmlinux) + if err != nil { + return nil, 0, fmt.Errorf("parse types for module %s: %w", info.Name, err) + } + + err = spec.TypeByName(typeName, &target) + if errors.Is(err, btf.ErrNotFound) { + continue } - return 0, fmt.Errorf("find target for %s: %w", featureName, err) + if err != nil { + return nil, 0, fmt.Errorf("lookup type in module %s: %w", info.Name, err) + } + + id, err := spec.TypeID(target) + if err != nil { + return nil, 0, fmt.Errorf("lookup type id in module %s: %w", info.Name, err) + } + + return it.Take(), id, nil + } + if err := it.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate modules: %w", err) } - return spec.TypeID(target) + return nil, 0, btf.ErrNotFound } // find an attach target type in a program. diff --git a/prog_test.go b/prog_test.go index fff5338ed..f2006351b 100644 --- a/prog_test.go +++ b/prog_test.go @@ -644,48 +644,95 @@ func TestProgramSpecTag(t *testing.T) { } } -func TestProgramTypeLSM(t *testing.T) { - lsmTests := []struct { - attachFn string +func TestProgramAttachToKernel(t *testing.T) { + // See https://github.com/torvalds/linux/commit/290248a5b7d829871b3ea3c62578613a580a1744 + testutils.SkipOnOldKernel(t, "5.5", "attach_btf_id") + + haveTestmod := false + if !testutils.MustKernelVersion().Less(internal.Version{5, 11}) { + // See https://github.com/torvalds/linux/commit/290248a5b7d829871b3ea3c62578613a580a1744 + testmod, err := btf.FindHandle(func(info *btf.HandleInfo) bool { + return info.IsModule() && info.Name == "bpf_testmod" + }) + if err != nil && !errors.Is(err, btf.ErrNotFound) { + t.Fatal(err) + } + haveTestmod = testmod != nil + testmod.Close() + } + + tests := []struct { + attachTo string + programType ProgramType + attachType AttachType flags uint32 - expectedErr bool }{ { - attachFn: "task_getpgid", + attachTo: "task_getpgid", + programType: LSM, + attachType: AttachLSMMac, + }, + { + attachTo: "inet_dgram_connect", + programType: Tracing, + attachType: AttachTraceFEntry, + }, + { + attachTo: "inet_dgram_connect", + programType: Tracing, + attachType: AttachTraceFExit, + }, + { + attachTo: "bpf_modify_return_test", + programType: Tracing, + attachType: AttachModifyReturn, + }, + { + attachTo: "kfree_skb", + programType: Tracing, + attachType: AttachTraceRawTp, + }, + { + attachTo: "bpf_testmod_test_read", + programType: Tracing, + attachType: AttachTraceFEntry, }, { - attachFn: "task_setnice", - flags: unix.BPF_F_SLEEPABLE, - expectedErr: true, + attachTo: "bpf_testmod_test_read", + programType: Tracing, + attachType: AttachTraceFExit, }, { - attachFn: "file_open", - flags: unix.BPF_F_SLEEPABLE, + attachTo: "bpf_testmod_test_read", + programType: Tracing, + attachType: AttachModifyReturn, + }, + { + attachTo: "bpf_testmod_test_read", + programType: Tracing, + attachType: AttachTraceRawTp, }, } - for _, tt := range lsmTests { - t.Run(tt.attachFn, func(t *testing.T) { + for _, test := range tests { + name := fmt.Sprintf("%s:%s", test.attachType, test.attachTo) + t.Run(name, func(t *testing.T) { + if strings.HasPrefix(test.attachTo, "bpf_testmod_") && !haveTestmod { + t.Skip("bpf_testmod not loaded") + } + prog, err := NewProgram(&ProgramSpec{ - AttachTo: tt.attachFn, - AttachType: AttachLSMMac, + AttachTo: test.attachTo, + AttachType: test.attachType, Instructions: asm.Instructions{ asm.LoadImm(asm.R0, 0, asm.DWord), asm.Return(), }, License: "GPL", - Type: LSM, - Flags: tt.flags, + Type: test.programType, + Flags: test.flags, }) - testutils.SkipIfNotSupported(t, err) - - if tt.flags&unix.BPF_F_SLEEPABLE != 0 { - testutils.SkipOnOldKernel(t, "5.11", "BPF_F_SLEEPABLE for LSM progs") - } - if tt.expectedErr && err == nil { - t.Errorf("Test case '%s': expected error", tt.attachFn) - } - if !tt.expectedErr && err != nil { - t.Errorf("Test case '%s': expected success", tt.attachFn) + if err != nil { + t.Fatal("Can't load program:", err) } prog.Close() })