Repository: cilium/ebpf
Pull request #647 (2022-04-28 22:05:11)

program: Add TestWithOptions() API
This PR adds support for a new `TestWithOptions()` API
for `Program`s. The extra options are useful for more advanced
testing.

lmb: Thank you for your PR, I'll try to do a full review tomorrow. In the meantime, please take a look at #576 which discusses adding this feature.
danobi: Pushed a commit that addresses all comments except for RunOptions/RunResult split
danobi: Not sure what's up with CI. Seems like one of the jobs got stuck (can't get the page to load)
danobi: I've cleaned up the commits and changed `ContextOut` to `interface{}`. IMO this approach balances ergonomics with reflection. Please let me know what you think.
lmb: Hi @danobi, I'll only be able to take another look at this towards Wednesday afternoon, European timezone. Sorry!

Patch:
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 012ba67..fda25c1 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -178,6 +178,7 @@ import (
 		},
 		{"FuncInfo", "bpf_func_info", nil},
 		{"LineInfo", "bpf_line_info", nil},
+		{"XdpMd", "xdp_md", nil},
 	}
 
 	sort.Slice(structs, func(i, j int) bool {
diff --git a/internal/sys/types.go b/internal/sys/types.go
index 586c5d0..ac5cc78 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -517,6 +517,15 @@ type ProgInfo struct {
 	_                    [4]byte
 }
 
+type XdpMd struct {
+	Data           uint32
+	DataEnd        uint32
+	DataMeta       uint32
+	IngressIfindex uint32
+	RxQueueIndex   uint32
+	EgressIfindex  uint32
+}
+
 type BtfGetFdByIdAttr struct{ Id uint32 }
 
 func BtfGetFdById(attr *BtfGetFdByIdAttr) (*FD, error) {
diff --git a/link/cgroup.go b/link/cgroup.go
index b3e65cf..003b063 100644
--- a/link/cgroup.go
+++ b/link/cgroup.go
@@ -56,18 +56,6 @@ func AttachCgroup(opts CgroupOptions) (Link, error) {
 	return cg, nil
 }
 
-// LoadPinnedCgroup loads a pinned cgroup from a bpffs.
-//
-// Deprecated: use LoadPinnedLink instead.
-func LoadPinnedCgroup(fileName string, opts *ebpf.LoadPinOptions) (Link, error) {
-	link, err := LoadPinnedRawLink(fileName, CgroupType, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	return &linkCgroup{*link}, nil
-}
-
 type progAttachCgroup struct {
 	cgroup     *os.File
 	current    *ebpf.Program
diff --git a/link/iter.go b/link/iter.go
index 289733e..d2b32ef 100644
--- a/link/iter.go
+++ b/link/iter.go
@@ -58,18 +58,6 @@ func AttachIter(opts IterOptions) (*Iter, error) {
 	return &Iter{RawLink{fd, ""}}, err
 }
 
-// LoadPinnedIter loads a pinned iterator from a bpffs.
-//
-// Deprecated: use LoadPinnedLink instead.
-func LoadPinnedIter(fileName string, opts *ebpf.LoadPinOptions) (*Iter, error) {
-	link, err := LoadPinnedRawLink(fileName, IterType, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	return &Iter{*link}, err
-}
-
 // Iter represents an attached bpf_iter.
 type Iter struct {
 	RawLink
diff --git a/link/link.go b/link/link.go
index bb84e9d..067d010 100644
--- a/link/link.go
+++ b/link/link.go
@@ -107,11 +107,6 @@ type Info struct {
 	extra   interface{}
 }
 
-// RawLinkInfo contains information on a raw link.
-//
-// Deprecated: use Info instead.
-type RawLinkInfo = Info
-
 type TracingInfo sys.TracingLinkInfo
 type CgroupInfo sys.CgroupLinkInfo
 type NetNsInfo sys.NetNsLinkInfo
@@ -188,36 +183,6 @@ func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
 	return &RawLink{fd, ""}, nil
 }
 
-// LoadPinnedRawLink loads a persisted link from a bpffs.
-//
-// Returns an error if the pinned link type doesn't match linkType. Pass
-// UnspecifiedType to disable this behaviour.
-//
-// Deprecated: use LoadPinnedLink instead.
-func LoadPinnedRawLink(fileName string, linkType Type, opts *ebpf.LoadPinOptions) (*RawLink, error) {
-	link, err := loadPinnedRawLink(fileName, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	if linkType == UnspecifiedType {
-		return link, nil
-	}
-
-	info, err := link.Info()
-	if err != nil {
-		link.Close()
-		return nil, fmt.Errorf("get pinned link info: %w", err)
-	}
-
-	if info.Type != linkType {
-		link.Close()
-		return nil, fmt.Errorf("link type %v doesn't match %v", info.Type, linkType)
-	}
-
-	return link, nil
-}
-
 func loadPinnedRawLink(fileName string, opts *ebpf.LoadPinOptions) (*RawLink, error) {
 	fd, err := sys.ObjGet(&sys.ObjGetAttr{
 		Pathname:  sys.NewStringPointer(fileName),
diff --git a/link/netns.go b/link/netns.go
index f49cbe4..344ecce 100644
--- a/link/netns.go
+++ b/link/netns.go
@@ -34,15 +34,3 @@ func AttachNetNs(ns int, prog *ebpf.Program) (*NetNsLink, error) {
 
 	return &NetNsLink{*link}, nil
 }
-
-// LoadPinnedNetNs loads a network namespace link from bpffs.
-//
-// Deprecated: use LoadPinnedLink instead.
-func LoadPinnedNetNs(fileName string, opts *ebpf.LoadPinOptions) (*NetNsLink, error) {
-	link, err := LoadPinnedRawLink(fileName, NetNsType, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	return &NetNsLink{*link}, nil
-}
diff --git a/link/tracing.go b/link/tracing.go
index e26bea7..6da4732 100644
--- a/link/tracing.go
+++ b/link/tracing.go
@@ -80,18 +80,6 @@ func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (
 	return &tracing{*link}, nil
 }
 
-// LoadPinnedFreplace loads a pinned iterator from a bpffs.
-//
-// Deprecated: use LoadPinnedLink instead.
-func LoadPinnedFreplace(fileName string, opts *ebpf.LoadPinOptions) (Link, error) {
-	link, err := LoadPinnedRawLink(fileName, TracingType, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	return &tracing{*link}, err
-}
-
 type TracingOptions struct {
 	// Program must be of type Tracing with attach type
 	// AttachTraceFEntry/AttachTraceFExit/AttachModifyReturn or
diff --git a/map.go b/map.go
index e6d26db..a450f3c 100644
--- a/map.go
+++ b/map.go
@@ -1418,14 +1418,3 @@ func NewMapFromID(id MapID) (*Map, error) {
 
 	return newMapFromFD(fd)
 }
-
-// ID returns the systemwide unique ID of the map.
-//
-// Deprecated: use MapInfo.ID() instead.
-func (m *Map) ID() (MapID, error) {
-	var info sys.MapInfo
-	if err := sys.ObjInfo(m.fd, &info); err != nil {
-		return MapID(0), err
-	}
-	return MapID(info.Id), nil
-}
diff --git a/prog.go b/prog.go
index 93398b2..a78a359 100644
--- a/prog.go
+++ b/prog.go
@@ -486,6 +486,28 @@ func (p *Program) Close() error {
 	return p.fd.Close()
 }
 
+// Various options for Run'ing a Program
+type RunOptions struct {
+	// Program's data input. Required field.
+	Data []byte
+	// Program's data after Program has run. Caller must allocate. Optional field.
+	DataOut []byte
+	// Program's context input. Optional field.
+	Context interface{}
+	// Program's context after Program has run. Must be a pointer or slice. Optional field.
+	ContextOut interface{}
+	// Number of times to run Program. Optional field. Defaults to 1.
+	Repeat uint32
+	// Optional flags.
+	Flags uint32
+	// CPU to run Program on. Optional field.
+	// Note not all program types support this field.
+	CPU uint32
+	// Called whenever the syscall is interrupted, and should be set to testing.B.ResetTimer
+	// or similar. Typically used during benchmarking. Optional field.
+	Reset func()
+}
+
 // Test runs the Program in the kernel with the given input and returns the
 // value returned by the eBPF program. outLen may be zero.
 //
@@ -494,11 +516,35 @@ func (p *Program) Close() error {
 //
 // This function requires at least Linux 4.12.
 func (p *Program) Test(in []byte) (uint32, []byte, error) {
-	ret, out, _, err := p.testRun(in, 1, nil)
+	// Older kernels ignore the dataSizeOut argument when copying to user space.
+	// Combined with things like bpf_xdp_adjust_head() we don't really know what the final
+	// size will be. Hence we allocate an output buffer which we hope will always be large
+	// enough, and panic if the kernel wrote past the end of the allocation.
+	// See https://patchwork.ozlabs.org/cover/1006822/
+	out := make([]byte, len(in)+outputPad)
+
+	opts := RunOptions{
+		Data:    in,
+		DataOut: out,
+		Repeat:  1,
+	}
+
+	ret, _, err := p.testRun(&opts)
 	if err != nil {
 		return ret, nil, fmt.Errorf("can't test program: %w", err)
 	}
-	return ret, out, nil
+	return ret, opts.DataOut, nil
+}
+
+// Run runs the Program in kernel with given RunOptions.
+//
+// Note: the same restrictions from Test apply.
+func (p *Program) Run(opts *RunOptions) (uint32, error) {
+	ret, _, err := p.testRun(opts)
+	if err != nil {
+		return ret, fmt.Errorf("can't test program: %w", err)
+	}
+	return ret, nil
 }
 
 // Benchmark runs the Program with the given input for a number of times
@@ -513,7 +559,17 @@ func (p *Program) Test(in []byte) (uint32, []byte, error) {
 //
 // This function requires at least Linux 4.12.
 func (p *Program) Benchmark(in []byte, repeat int, reset func()) (uint32, time.Duration, error) {
-	ret, _, total, err := p.testRun(in, repeat, reset)
+	if uint(repeat) > math.MaxUint32 {
+		return 0, 0, fmt.Errorf("repeat is too high")
+	}
+
+	opts := RunOptions{
+		Data:   in,
+		Repeat: uint32(repeat),
+		Reset:  reset,
+	}
+
+	ret, total, err := p.testRun(&opts)
 	if err != nil {
 		return ret, total, fmt.Errorf("can't benchmark program: %w", err)
 	}
@@ -565,37 +621,44 @@ var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() e
 	return err
 })
 
-func (p *Program) testRun(in []byte, repeat int, reset func()) (uint32, []byte, time.Duration, error) {
-	if uint(repeat) > math.MaxUint32 {
-		return 0, nil, 0, fmt.Errorf("repeat is too high")
+func (p *Program) testRun(opts *RunOptions) (uint32, time.Duration, error) {
+	if len(opts.Data) == 0 {
+		return 0, 0, fmt.Errorf("missing input")
 	}
 
-	if len(in) == 0 {
-		return 0, nil, 0, fmt.Errorf("missing input")
+	if uint(len(opts.Data)) > math.MaxUint32 {
+		return 0, 0, fmt.Errorf("input is too long")
 	}
 
-	if uint(len(in)) > math.MaxUint32 {
-		return 0, nil, 0, fmt.Errorf("input is too long")
+	if err := haveProgTestRun(); err != nil {
+		return 0, 0, err
 	}
 
-	if err := haveProgTestRun(); err != nil {
-		return 0, nil, 0, err
+	ctx := new(bytes.Buffer)
+	if opts.Context != nil {
+		if err := binary.Write(ctx, internal.NativeEndian, opts.Context); err != nil {
+			return 0, 0, fmt.Errorf("cannot serialize context: %v", err)
+		}
 	}
 
-	// Older kernels ignore the dataSizeOut argument when copying to user space.
-	// Combined with things like bpf_xdp_adjust_head() we don't really know what the final
-	// size will be. Hence we allocate an output buffer which we hope will always be large
-	// enough, and panic if the kernel wrote past the end of the allocation.
-	// See https://patchwork.ozlabs.org/cover/1006822/
-	out := make([]byte, len(in)+outputPad)
+	var ctxOut []byte
+	if opts.ContextOut != nil {
+		ctxOut = make([]byte, binary.Size(opts.ContextOut))
+	}
 
 	attr := sys.ProgRunAttr{
 		ProgFd:      p.fd.Uint(),
-		DataSizeIn:  uint32(len(in)),
-		DataSizeOut: uint32(len(out)),
-		DataIn:      sys.NewSlicePointer(in),
-		DataOut:     sys.NewSlicePointer(out),
-		Repeat:      uint32(repeat),
+		DataSizeIn:  uint32(len(opts.Data)),
+		DataSizeOut: uint32(len(opts.DataOut)),
+		DataIn:      sys.NewSlicePointer(opts.Data),
+		DataOut:     sys.NewSlicePointer(opts.DataOut),
+		Repeat:      uint32(opts.Repeat),
+		CtxSizeIn:   uint32(len(ctx.Bytes())),
+		CtxSizeOut:  uint32(len(ctxOut)),
+		CtxIn:       sys.NewSlicePointer(ctx.Bytes()),
+		CtxOut:      sys.NewSlicePointer(ctxOut),
+		Flags:       opts.Flags,
+		Cpu:         opts.CPU,
 	}
 
 	for {
@@ -605,28 +668,37 @@ func (p *Program) testRun(in []byte, repeat int, reset func()) (uint32, []byte,
 		}
 
 		if errors.Is(err, unix.EINTR) {
-			if reset != nil {
-				reset()
+			if opts.Reset != nil {
+				opts.Reset()
 			}
 			continue
 		}
 
 		if errors.Is(err, unix.ENOTSUPP) {
-			return 0, nil, 0, fmt.Errorf("kernel doesn't support testing program type %s: %w", p.Type(), ErrNotSupported)
+			return 0, 0, fmt.Errorf("kernel doesn't support testing program type %s: %w", p.Type(), ErrNotSupported)
 		}
 
-		return 0, nil, 0, fmt.Errorf("can't run test: %w", err)
+		return 0, 0, fmt.Errorf("can't run test: %w", err)
+	}
+
+	if opts.DataOut != nil {
+		if int(attr.DataSizeOut) > cap(opts.DataOut) {
+			// Houston, we have a problem. The program created more data than we allocated,
+			// and the kernel wrote past the end of our buffer.
+			panic("kernel wrote past end of output buffer")
+		}
+		opts.DataOut = opts.DataOut[:int(attr.DataSizeOut)]
 	}
 
-	if int(attr.DataSizeOut) > cap(out) {
-		// Houston, we have a problem. The program created more data than we allocated,
-		// and the kernel wrote past the end of our buffer.
-		panic("kernel wrote past end of output buffer")
+	if len(ctxOut) != 0 {
+		b := bytes.NewReader(ctxOut)
+		if err := binary.Read(b, internal.NativeEndian, opts.ContextOut); err != nil {
+			return 0, 0, fmt.Errorf("failed to decode ContextOut: %v", err)
+		}
 	}
-	out = out[:int(attr.DataSizeOut)]
 
 	total := time.Duration(attr.Duration) * time.Nanosecond
-	return attr.Retval, out, total, nil
+	return attr.Retval, total, nil
 }
 
 func unmarshalProgram(buf []byte) (*Program, error) {
@@ -650,45 +722,6 @@ func marshalProgram(p *Program, length int) ([]byte, error) {
 	return buf, nil
 }
 
-// Attach a Program.
-//
-// Deprecated: use link.RawAttachProgram instead.
-func (p *Program) Attach(fd int, typ AttachType, flags AttachFlags) error {
-	if fd < 0 {
-		return errors.New("invalid fd")
-	}
-
-	attr := sys.ProgAttachAttr{
-		TargetFd:    uint32(fd),
-		AttachBpfFd: p.fd.Uint(),
-		AttachType:  uint32(typ),
-		AttachFlags: uint32(flags),
-	}
-
-	return sys.ProgAttach(&attr)
-}
-
-// Detach a Program.
-//
-// Deprecated: use link.RawDetachProgram instead.
-func (p *Program) Detach(fd int, typ AttachType, flags AttachFlags) error {
-	if fd < 0 {
-		return errors.New("invalid fd")
-	}
-
-	if flags != 0 {
-		return errors.New("flags must be zero")
-	}
-
-	attr := sys.ProgAttachAttr{
-		TargetFd:    uint32(fd),
-		AttachBpfFd: p.fd.Uint(),
-		AttachType:  uint32(typ),
-	}
-
-	return sys.ProgAttach(&attr)
-}
-
 // LoadPinnedProgram loads a Program from a BPF file.
 //
 // Requires at least Linux 4.11.
@@ -734,17 +767,6 @@ func ProgramGetNextID(startID ProgramID) (ProgramID, error) {
 	return ProgramID(attr.NextId), sys.ProgGetNextId(attr)
 }
 
-// ID returns the systemwide unique ID of the program.
-//
-// Deprecated: use ProgramInfo.ID() instead.
-func (p *Program) ID() (ProgramID, error) {
-	var info sys.ProgInfo
-	if err := sys.ObjInfo(p.fd, &info); err != nil {
-		return ProgramID(0), err
-	}
-	return ProgramID(info.Id), nil
-}
-
 // BindMap binds map to the program and is only released once program is released.
 //
 // This may be used in cases where metadata should be associated with the program
diff --git a/syscalls.go b/syscalls.go
index b90ff7b..e5c270a 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	"os"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
@@ -12,11 +11,6 @@ import (
 	"github.com/cilium/ebpf/internal/unix"
 )
 
-// ErrNotExist is returned when loading a non-existing map or program.
-//
-// Deprecated: use os.ErrNotExist instead.
-var ErrNotExist = os.ErrNotExist
-
 // invalidBPFObjNameChar returns true if char may not appear in
 // a BPF object name.
 func invalidBPFObjNameChar(char rune) bool {


Test patch:
diff --git a/map_test.go b/map_test.go
index 4f38cdc..0c58ec9 100644
--- a/map_test.go
+++ b/map_test.go
@@ -1635,7 +1635,7 @@ func TestMapGetNextID(t *testing.T) {
 	for {
 		last := next
 		if next, err = MapGetNextID(last); err != nil {
-			if !errors.Is(err, ErrNotExist) {
+			if !errors.Is(err, os.ErrNotExist) {
 				t.Fatal("Expected ErrNotExist, got:", err)
 			}
 			break
@@ -1647,25 +1647,29 @@ func TestMapGetNextID(t *testing.T) {
 }
 
 func TestNewMapFromID(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.13", "bpf_map_get_fd_by_id")
-
 	hash := createHash()
 	defer hash.Close()
-	var next MapID
-	var err error
 
-	next, err = hash.ID()
+	info, err := hash.Info()
+	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
-		t.Fatal("Could not get ID of map:", err)
+		t.Fatal("Couldn't get map info:", err)
 	}
 
-	if _, err = NewMapFromID(next); err != nil {
-		t.Fatalf("Can't get map for ID %d: %v", uint32(next), err)
+	id, ok := info.ID()
+	if !ok {
+		t.Skip("Map ID not supported")
+	}
+
+	hash2, err := NewMapFromID(id)
+	if err != nil {
+		t.Fatalf("Can't get map for ID %d: %v", id, err)
 	}
+	hash2.Close()
 
 	// As there can be multiple maps, we use max(uint32) as MapID to trigger an expected error.
 	_, err = NewMapFromID(MapID(math.MaxUint32))
-	if !errors.Is(err, ErrNotExist) {
+	if !errors.Is(err, os.ErrNotExist) {
 		t.Fatal("Expected ErrNotExist, got:", err)
 	}
 }
diff --git a/prog_test.go b/prog_test.go
index cd04dba..e612c2a 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -91,6 +91,52 @@ func TestProgramRun(t *testing.T) {
 	}
 }
 
+func TestProgramRunWithOptions(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "5.15", "XDP ctx_in/ctx_out")
+
+	ins := asm.Instructions{
+		// Return XDP_ABORTED
+		asm.LoadImm(asm.R0, 0, asm.DWord),
+		asm.Return(),
+	}
+
+	prog, err := NewProgram(&ProgramSpec{
+		Name:         "test",
+		Type:         XDP,
+		Instructions: ins,
+		License:      "MIT",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	buf := make([]byte, 14)
+	xdp := sys.XdpMd{
+		Data:    0,
+		DataEnd: 14,
+	}
+	xdpOut := sys.XdpMd{}
+	opts := RunOptions{
+		Data:       buf,
+		Context:    xdp,
+		ContextOut: &xdpOut,
+	}
+	ret, err := prog.Run(&opts)
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if ret != 0 {
+		t.Error("Expected return value to be 0, got", ret)
+	}
+
+	if xdp != xdpOut {
+		t.Errorf("Expect xdp (%+v) == xdpOut (%+v)", xdp, xdpOut)
+	}
+}
+
 func TestProgramSpecFlattenOrder(t *testing.T) {
 	prog_a := ProgramSpec{Name: "prog_a"}
 	prog_b := ProgramSpec{Name: "prog_b"}
@@ -171,13 +217,18 @@ func TestProgramTestRunInterrupt(t *testing.T) {
 
 		// Block this thread in the BPF syscall, so that we can
 		// trigger EINTR by sending a signal.
-		_, _, _, err := prog.testRun(make([]byte, 14), math.MaxInt32, func() {
-			// We don't know how long finishing the
-			// test run would take, so flag that we've seen
-			// an interruption and abort the goroutine.
-			close(errs)
-			runtime.Goexit()
-		})
+		opts := RunOptions{
+			Data:   make([]byte, 14),
+			Repeat: math.MaxInt32,
+			Reset: func() {
+				// We don't know how long finishing the
+				// test run would take, so flag that we've seen
+				// an interruption and abort the goroutine.
+				close(errs)
+				runtime.Goexit()
+			},
+		}
+		_, _, err := prog.testRun(&opts)
 
 		errs <- err
 	}()
@@ -491,7 +542,7 @@ func TestProgramGetNextID(t *testing.T) {
 	last := ProgramID(0)
 	for {
 		next, err := ProgramGetNextID(last)
-		if errors.Is(err, ErrNotExist) {
+		if errors.Is(err, os.ErrNotExist) {
 			if last == 0 {
 				t.Fatal("Got ErrNotExist on the first iteration")
 			}
@@ -508,24 +559,28 @@ func TestProgramGetNextID(t *testing.T) {
 }
 
 func TestNewProgramFromID(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.13", "bpf_prog_get_fd_by_id")
-
 	prog := mustSocketFilter(t)
 
-	var next ProgramID
-
-	next, err := prog.ID()
+	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
-		t.Fatal("Could not get ID of program:", err)
+		t.Fatal("Could not get program info:", err)
+	}
+
+	id, ok := info.ID()
+	if !ok {
+		t.Skip("Program ID not supported")
 	}
 
-	if _, err = NewProgramFromID(next); err != nil {
-		t.Fatalf("Can't get FD for program ID %d: %v", uint32(next), err)
+	prog2, err := NewProgramFromID(id)
+	if err != nil {
+		t.Fatalf("Can't get FD for program ID %d: %v", id, err)
 	}
+	prog2.Close()
 
 	// As there can be multiple programs, we use max(uint32) as ProgramID to trigger an expected error.
 	_, err = NewProgramFromID(ProgramID(math.MaxUint32))
-	if !errors.Is(err, ErrNotExist) {
+	if !errors.Is(err, os.ErrNotExist) {
 		t.Fatal("Expected ErrNotExist, got:", err)
 	}
 }