Repository: cilium/ebpf
Pull request #682 (2022-05-24 14:07:35)

Add `RelativeOffset` field to `UprobeOptions` struct
Alternative to https://github.com/cilium/ebpf/pull/683

This PR, for a [bpftrace](https://github.com/iovisor/bpftrace) program like below:

```
#!/usr/bin/env bpftrace

uprobe:./main:"main.probeMe"+42
{
    printf("rax: %d\n", reg("ax"));
}
```

enables probing without the need of recalculating the offset of `main.probeMe` (which is already calculated and stored inside [`offsets` field](https://github.com/cilium/ebpf/blob/951bb28908d23e50fca063a2d51098ca028352bf/link/uprobe.go#L46)), adding `0x2a` and setting `Offset` field to the resulting value:

```go
up, err := ex.Uprobe("main.probeMe", objs.ExampleUprobe, &link.UprobeOptions{RelativeOffset: 0x2a})
if err != nil {
	log.Fatalf("creating uprobe: %s", err)
}
defer up.Close()
```

The PR also improves some of the comments because it's hard to understand at first glance that with the block below, the `Offset` field nullifies the offset that's calculated from the given symbol and the `Offset` is actually an **absolute offset** instead of a relative one:

https://github.com/cilium/ebpf/blob/951bb28908d23e50fca063a2d51098ca028352bf/link/uprobe.go#L255-L262


Patch:
diff --git a/btf/btf_types.go b/btf/btf_types.go
index a62b3c9..4810180 100644
--- a/btf/btf_types.go
+++ b/btf/btf_types.go
@@ -130,13 +130,22 @@ func mask(len uint32) uint32 {
 	return (1 << len) - 1
 }
 
+func readBits(value, len, shift uint32) uint32 {
+	return (value >> shift) & mask(len)
+}
+
+func writeBits(value, len, shift, new uint32) uint32 {
+	value &^= mask(len) << shift
+	value |= (new & mask(len)) << shift
+	return value
+}
+
 func (bt *btfType) info(len, shift uint32) uint32 {
-	return (bt.Info >> shift) & mask(len)
+	return readBits(bt.Info, len, shift)
 }
 
 func (bt *btfType) setInfo(value, len, shift uint32) {
-	bt.Info &^= mask(len) << shift
-	bt.Info |= (value & mask(len)) << shift
+	bt.Info = writeBits(bt.Info, len, shift, value)
 }
 
 func (bt *btfType) Kind() btfKind {
@@ -198,6 +207,50 @@ func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error {
 	return binary.Write(w, bo, rt.data)
 }
 
+// btfInt encodes additional data for integers.
+//
+//    ? ? ? ? e e e e o o o o o o o o ? ? ? ? ? ? ? ? b b b b b b b b
+//    ? = undefined
+//    e = encoding
+//    o = offset (bitfields?)
+//    b = bits (bitfields)
+type btfInt struct {
+	Raw uint32
+}
+
+const (
+	btfIntEncodingLen   = 4
+	btfIntEncodingShift = 24
+	btfIntOffsetLen     = 8
+	btfIntOffsetShift   = 16
+	btfIntBitsLen       = 8
+	btfIntBitsShift     = 0
+)
+
+func (bi btfInt) Encoding() IntEncoding {
+	return IntEncoding(readBits(bi.Raw, btfIntEncodingLen, btfIntEncodingShift))
+}
+
+func (bi *btfInt) SetEncoding(e IntEncoding) {
+	bi.Raw = writeBits(uint32(bi.Raw), btfIntEncodingLen, btfIntEncodingShift, uint32(e))
+}
+
+func (bi btfInt) Offset() Bits {
+	return Bits(readBits(bi.Raw, btfIntOffsetLen, btfIntOffsetShift))
+}
+
+func (bi *btfInt) SetOffset(offset uint32) {
+	bi.Raw = writeBits(bi.Raw, btfIntOffsetLen, btfIntOffsetShift, offset)
+}
+
+func (bi btfInt) Bits() Bits {
+	return Bits(readBits(bi.Raw, btfIntBitsLen, btfIntBitsShift))
+}
+
+func (bi *btfInt) SetBits(bits byte) {
+	bi.Raw = writeBits(bi.Raw, btfIntBitsLen, btfIntBitsShift, uint32(bits))
+}
+
 type btfArray struct {
 	Type      TypeID
 	IndexType TypeID
@@ -249,7 +302,7 @@ func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, err
 		var data interface{}
 		switch header.Kind() {
 		case kindInt:
-			data = new(uint32)
+			data = new(btfInt)
 		case kindPointer:
 		case kindArray:
 			data = new(btfArray)
@@ -288,7 +341,3 @@ func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, err
 		types = append(types, rawType{header, data})
 	}
 }
-
-func intEncoding(raw uint32) (IntEncoding, Bits, Bits) {
-	return IntEncoding((raw & 0x0f000000) >> 24), Bits(raw&0x00ff0000) >> 16, Bits(raw & 0x000000ff)
-}
diff --git a/btf/types.go b/btf/types.go
index 82e7fea..5aabd53 100644
--- a/btf/types.go
+++ b/btf/types.go
@@ -151,17 +151,22 @@ func (p *Pointer) copy() Type {
 
 // Array is an array with a fixed number of elements.
 type Array struct {
+	Index  Type
 	Type   Type
 	Nelems uint32
 }
 
 func (arr *Array) Format(fs fmt.State, verb rune) {
-	formatType(fs, verb, arr, "type=", arr.Type, "n=", arr.Nelems)
+	formatType(fs, verb, arr, "index=", arr.Index, "type=", arr.Type, "n=", arr.Nelems)
 }
 
 func (arr *Array) TypeName() string { return "" }
 
-func (arr *Array) walk(tdq *typeDeque) { tdq.push(&arr.Type) }
+func (arr *Array) walk(tdq *typeDeque) {
+	tdq.push(&arr.Index)
+	tdq.push(&arr.Type)
+}
+
 func (arr *Array) copy() Type {
 	cpy := *arr
 	return &cpy
@@ -891,11 +896,11 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 		switch raw.Kind() {
 		case kindInt:
 			size := raw.Size()
-			encoding, offset, bits := intEncoding(*raw.data.(*uint32))
-			if offset > 0 || bits.Bytes() != size {
-				legacyBitfields[id] = [2]Bits{offset, bits}
+			bi := raw.data.(*btfInt)
+			if bi.Offset() > 0 || bi.Bits().Bytes() != size {
+				legacyBitfields[id] = [2]Bits{bi.Offset(), bi.Bits()}
 			}
-			typ = &Int{name, size, encoding}
+			typ = &Int{name, raw.Size(), bi.Encoding()}
 
 		case kindPointer:
 			ptr := &Pointer{nil}
@@ -904,10 +909,8 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 
 		case kindArray:
 			btfArr := raw.data.(*btfArray)
-
-			// IndexType is unused according to btf.rst.
-			// Don't make it available right now.
-			arr := &Array{nil, btfArr.Nelems}
+			arr := &Array{nil, nil, btfArr.Nelems}
+			fixup(btfArr.IndexType, &arr.Index)
 			fixup(btfArr.Type, &arr.Type)
 			typ = arr
 
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/link/uprobe.go b/link/uprobe.go
index ca11f37..46c75b3 100644
--- a/link/uprobe.go
+++ b/link/uprobe.go
@@ -52,6 +52,10 @@ type UprobeOptions struct {
 	// Symbol offset. Must be provided in case of external symbols (shared libs).
 	// If set, overrides the offset eventually parsed from the executable.
 	Offset uint64
+	// The offset relative to given symbol. Useful when tracing an arbitrary point
+	// inside the frame of given symbol and eliminates the need of recalculating
+	// the absolute offset.
+	RelativeOffset uint64
 	// Only set the uprobe on the given process ID. Useful when tracing
 	// shared library calls or programs that have many running instances.
 	PID int
@@ -157,8 +161,14 @@ func (ex *Executable) load(f *internal.SafeELFFile) error {
 	return nil
 }
 
-func (ex *Executable) offset(symbol string) (uint64, error) {
-	if off, ok := ex.offsets[symbol]; ok {
+// offset calculates the address of a symbol in the executable.
+//
+// opts must not be nil.
+func (ex *Executable) offset(symbol string, opts *UprobeOptions) (uint64, error) {
+	var offset uint64
+	if opts.Offset > 0 {
+		offset = opts.Offset
+	} else if off, ok := ex.offsets[symbol]; ok {
 		// Symbols with location 0 from section undef are shared library calls and
 		// are relocated before the binary is executed. Dynamic linking is not
 		// implemented by the library, so mark this as unsupported for now.
@@ -169,9 +179,16 @@ func (ex *Executable) offset(symbol string) (uint64, error) {
 			return 0, fmt.Errorf("cannot resolve %s library call '%s', "+
 				"consider providing the offset via options: %w", ex.path, symbol, ErrNotSupported)
 		}
-		return off, nil
+
+		offset = off
+	} else {
+		return 0, fmt.Errorf("symbol %s: %w", symbol, ErrNoSymbol)
 	}
-	return 0, fmt.Errorf("symbol %s: %w", symbol, ErrNoSymbol)
+
+	// Always add the relative offset regardless where we got the absolute
+	// offset from.
+	offset += opts.RelativeOffset
+	return offset, nil
 }
 
 // Uprobe attaches the given eBPF program to a perf event that fires when the
@@ -186,6 +203,8 @@ func (ex *Executable) offset(symbol string) (uint64, error) {
 //
 //  up, err := ex.Uprobe("main", prog, &UprobeOptions{Offset: 0x123})
 //
+// Note: Setting the Offset field in the options supersedes the symbol's offset.
+//
 // Losing the reference to the resulting Link (up) will close the Uprobe
 // and prevent further execution of prog. The Link must be Closed during
 // program shutdown to avoid leaking system resources.
@@ -218,6 +237,8 @@ func (ex *Executable) Uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 //
 //  up, err := ex.Uretprobe("main", prog, &UprobeOptions{Offset: 0x123})
 //
+// Note: Setting the Offset field in the options supersedes the symbol's offset.
+//
 // Losing the reference to the resulting Link (up) will close the Uprobe
 // and prevent further execution of prog. The Link must be Closed during
 // program shutdown to avoid leaking system resources.
@@ -252,13 +273,9 @@ func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 		opts = &UprobeOptions{}
 	}
 
-	offset := opts.Offset
-	if offset == 0 {
-		off, err := ex.offset(symbol)
-		if err != nil {
-			return nil, err
-		}
-		offset = off
+	offset, err := ex.offset(symbol, opts)
+	if err != nil {
+		return nil, err
 	}
 
 	pid := opts.PID
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..dc2bbd8 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,46 @@ 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
+	var ctxBytes []byte
+	if opts.Context != nil {
+		ctx := new(bytes.Buffer)
+		if err := binary.Write(ctx, internal.NativeEndian, opts.Context); err != nil {
+			return 0, 0, fmt.Errorf("cannot serialize context: %v", err)
+		}
+		ctxBytes = ctx.Bytes()
 	}
 
-	// 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(ctxBytes)),
+		CtxSizeOut:  uint32(len(ctxOut)),
+		CtxIn:       sys.NewSlicePointer(ctxBytes),
+		CtxOut:      sys.NewSlicePointer(ctxOut),
+		Flags:       opts.Flags,
+		Cpu:         opts.CPU,
 	}
 
 	for {
@@ -605,28 +670,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 +724,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 +769,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/btf/core_test.go b/btf/core_test.go
index a6e7e19..136da3d 100644
--- a/btf/core_test.go
+++ b/btf/core_test.go
@@ -27,8 +27,8 @@ func TestCOREAreTypesCompatible(t *testing.T) {
 		{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
 		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
 		{&Pointer{Target: &Void{}}, &Void{}, false},
-		{&Array{Type: &Void{}}, &Array{Type: &Void{}}, true},
-		{&Array{Type: &Int{}}, &Array{Type: &Void{}}, false},
+		{&Array{Index: &Void{}, Type: &Void{}}, &Array{Index: &Void{}, Type: &Void{}}, true},
+		{&Array{Index: &Void{}, Type: &Int{}}, &Array{Index: &Void{}, Type: &Void{}}, false},
 		{&FuncProto{Return: &Int{}}, &FuncProto{Return: &Void{}}, false},
 		{
 			&FuncProto{Return: &Void{}, Params: []FuncParam{{Name: "a", Type: &Void{}}}},
diff --git a/btf/types_test.go b/btf/types_test.go
index 97e868d..2d4abba 100644
--- a/btf/types_test.go
+++ b/btf/types_test.go
@@ -360,8 +360,10 @@ func TestInflateLegacyBitfield(t *testing.T) {
 	var rawInt rawType
 	rawInt.SetKind(kindInt)
 	rawInt.SetSize(4)
-	offsetSize := uint32(offset<<16 | size)
-	rawInt.data = &offsetSize
+	var data btfInt
+	data.SetOffset(offset)
+	data.SetBits(size)
+	rawInt.data = &data
 
 	var beforeInt rawType
 	beforeInt.SetKind(kindStruct)
diff --git a/link/uprobe_test.go b/link/uprobe_test.go
index e26c8e7..973a28f 100644
--- a/link/uprobe_test.go
+++ b/link/uprobe_test.go
@@ -31,17 +31,44 @@ func TestExecutable(t *testing.T) {
 		t.Fatalf("create executable: unexpected path '%s'", bashEx.path)
 	}
 
-	_, err = bashEx.offset(bashSym)
+	_, err = bashEx.offset(bashSym, &UprobeOptions{})
 	if err != nil {
 		t.Fatalf("find offset: %v", err)
 	}
 
-	_, err = bashEx.offset("bogus")
+	_, err = bashEx.offset("bogus", &UprobeOptions{})
 	if err == nil {
 		t.Fatal("find symbol: expected error")
 	}
 }
 
+func TestExecutableOffset(t *testing.T) {
+	c := qt.New(t)
+
+	symbolOffset, err := bashEx.offset(bashSym, &UprobeOptions{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	offset, err := bashEx.offset(bashSym, &UprobeOptions{Offset: 0x1})
+	if err != nil {
+		t.Fatal(err)
+	}
+	c.Assert(offset, qt.Equals, uint64(0x1))
+
+	offset, err = bashEx.offset(bashSym, &UprobeOptions{RelativeOffset: 0x2})
+	if err != nil {
+		t.Fatal(err)
+	}
+	c.Assert(offset, qt.Equals, symbolOffset+0x2)
+
+	offset, err = bashEx.offset(bashSym, &UprobeOptions{Offset: 0x1, RelativeOffset: 0x2})
+	if err != nil {
+		t.Fatal(err)
+	}
+	c.Assert(offset, qt.Equals, uint64(0x1+0x2))
+}
+
 func TestUprobe(t *testing.T) {
 	c := qt.New(t)
 
@@ -116,7 +143,7 @@ func TestUprobeCreatePMU(t *testing.T) {
 	c := qt.New(t)
 
 	// Fetch the offset from the /bin/bash Executable already defined.
-	off, err := bashEx.offset(bashSym)
+	off, err := bashEx.offset(bashSym, &UprobeOptions{})
 	c.Assert(err, qt.IsNil)
 
 	// Prepare probe args.
@@ -148,7 +175,7 @@ func TestUprobePMUUnavailable(t *testing.T) {
 	c := qt.New(t)
 
 	// Fetch the offset from the /bin/bash Executable already defined.
-	off, err := bashEx.offset(bashSym)
+	off, err := bashEx.offset(bashSym, &UprobeOptions{})
 	c.Assert(err, qt.IsNil)
 
 	// Prepare probe args.
@@ -174,7 +201,7 @@ func TestUprobeTraceFS(t *testing.T) {
 	c := qt.New(t)
 
 	// Fetch the offset from the /bin/bash Executable already defined.
-	off, err := bashEx.offset(bashSym)
+	off, err := bashEx.offset(bashSym, &UprobeOptions{})
 	c.Assert(err, qt.IsNil)
 
 	// Prepare probe args.
@@ -223,7 +250,7 @@ func TestUprobeCreateTraceFS(t *testing.T) {
 	c := qt.New(t)
 
 	// Fetch the offset from the /bin/bash Executable already defined.
-	off, err := bashEx.offset(bashSym)
+	off, err := bashEx.offset(bashSym, &UprobeOptions{})
 	c.Assert(err, qt.IsNil)
 
 	// Sanitize the symbol in order to be used in tracefs API.
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)
 	}
 }