Repository: cilium/ebpf
Pull request #249 (2021-03-09 11:58:33)

map, program: allow passing flags when loading pinned object
BPF_OBJ_GET accepts the BPF_F_WRONLY and BPF_F_RDONLY flags. Especially
BPF_F_RDONLY is useful, since it allows unprivileged read-only access
to BPF objects. Allow passing flags when loading pinned maps and
programs.


Patch:
diff --git a/pinning.go b/internal/pinning.go
similarity index 78%
rename from pinning.go
rename to internal/pinning.go
index d6796f5..5329b43 100644
--- a/pinning.go
+++ b/internal/pinning.go
@@ -1,24 +1,23 @@
-package ebpf
+package internal
 
 import (
 	"errors"
 	"fmt"
 	"os"
 
-	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/unix"
 )
 
-func pin(currentPath, newPath string, fd *internal.FD) error {
+func Pin(currentPath, newPath string, fd *FD) error {
 	if newPath == "" {
 		return errors.New("given pinning path cannot be empty")
 	}
-	if currentPath == "" {
-		return internal.BPFObjPin(newPath, fd)
-	}
 	if currentPath == newPath {
 		return nil
 	}
+	if currentPath == "" {
+		return BPFObjPin(newPath, fd)
+	}
 	var err error
 	// Renameat2 is used instead of os.Rename to disallow the new path replacing
 	// an existing path.
@@ -30,10 +29,10 @@ func pin(currentPath, newPath string, fd *internal.FD) error {
 		return fmt.Errorf("unable to move pinned object to new path %v: %w", newPath, err)
 	}
 	// Internal state not in sync with the file system so let's fix it.
-	return internal.BPFObjPin(newPath, fd)
+	return BPFObjPin(newPath, fd)
 }
 
-func unpin(pinnedPath string) error {
+func Unpin(pinnedPath string) error {
 	if pinnedPath == "" {
 		return nil
 	}
diff --git a/internal/syscall.go b/internal/syscall.go
index c808151..0ba438d 100644
--- a/internal/syscall.go
+++ b/internal/syscall.go
@@ -140,9 +140,10 @@ func BPFObjPin(fileName string, fd *FD) error {
 }
 
 // BPFObjGet wraps BPF_OBJ_GET.
-func BPFObjGet(fileName string) (*FD, error) {
+func BPFObjGet(fileName string, flags uint32) (*FD, error) {
 	attr := bpfObjAttr{
-		fileName: NewStringPointer(fileName),
+		fileName:  NewStringPointer(fileName),
+		fileFlags: flags,
 	}
 	ptr, err := BPF(BPF_OBJ_GET, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	if err != nil {
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index d7845f8..371dff7 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -26,6 +26,8 @@ const (
 	EBADF                    = linux.EBADF
 	BPF_F_NO_PREALLOC        = linux.BPF_F_NO_PREALLOC
 	BPF_F_NUMA_NODE          = linux.BPF_F_NUMA_NODE
+	BPF_F_RDONLY             = linux.BPF_F_RDONLY
+	BPF_F_WRONLY             = linux.BPF_F_WRONLY
 	BPF_F_RDONLY_PROG        = linux.BPF_F_RDONLY_PROG
 	BPF_F_WRONLY_PROG        = linux.BPF_F_WRONLY_PROG
 	BPF_OBJ_NAME_LEN         = linux.BPF_OBJ_NAME_LEN
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index dfefd0d..f29c4e1 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -26,6 +26,8 @@ const (
 
 	BPF_F_NO_PREALLOC        = 0
 	BPF_F_NUMA_NODE          = 0
+	BPF_F_RDONLY             = 0
+	BPF_F_WRONLY             = 0
 	BPF_F_RDONLY_PROG        = 0
 	BPF_F_WRONLY_PROG        = 0
 	BPF_OBJ_NAME_LEN         = 0x10
diff --git a/link/cgroup.go b/link/cgroup.go
index 16a9439..5540bb0 100644
--- a/link/cgroup.go
+++ b/link/cgroup.go
@@ -57,13 +57,13 @@ func AttachCgroup(opts CgroupOptions) (Link, error) {
 }
 
 // LoadPinnedCgroup loads a pinned cgroup from a bpffs.
-func LoadPinnedCgroup(fileName string) (Link, error) {
-	link, err := LoadPinnedRawLink(fileName)
+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
+	return &linkCgroup{*link}, nil
 }
 
 type progAttachCgroup struct {
@@ -147,14 +147,16 @@ func (cg *progAttachCgroup) Pin(string) error {
 	return fmt.Errorf("can't pin cgroup: %w", ErrNotSupported)
 }
 
+func (cg *progAttachCgroup) Unpin() error {
+	return fmt.Errorf("can't pin cgroup: %w", ErrNotSupported)
+}
+
 type linkCgroup struct {
-	*RawLink
+	RawLink
 }
 
 var _ Link = (*linkCgroup)(nil)
 
-func (cg *linkCgroup) isLink() {}
-
 func newLinkCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program) (*linkCgroup, error) {
 	link, err := AttachRawLink(RawLinkOptions{
 		Target:  int(cgroup.Fd()),
@@ -165,5 +167,5 @@ func newLinkCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program)
 		return nil, err
 	}
 
-	return &linkCgroup{link}, err
+	return &linkCgroup{*link}, err
 }
diff --git a/link/iter.go b/link/iter.go
index 2b5f284..04d24ef 100644
--- a/link/iter.go
+++ b/link/iter.go
@@ -27,53 +27,29 @@ func AttachIter(opts IterOptions) (*Iter, error) {
 		return nil, fmt.Errorf("can't link iterator: %w", err)
 	}
 
-	return &Iter{link}, err
+	return &Iter{*link}, err
 }
 
 // LoadPinnedIter loads a pinned iterator from a bpffs.
-func LoadPinnedIter(fileName string) (*Iter, error) {
-	link, err := LoadPinnedRawLink(fileName)
+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
+	return &Iter{*link}, err
 }
 
 // Iter represents an attached bpf_iter.
 type Iter struct {
-	link *RawLink
-}
-
-var _ Link = (*Iter)(nil)
-
-func (it *Iter) isLink() {}
-
-// FD returns the underlying file descriptor.
-func (it *Iter) FD() int {
-	return it.link.FD()
-}
-
-// Close implements Link.
-func (it *Iter) Close() error {
-	return it.link.Close()
-}
-
-// Pin implements Link.
-func (it *Iter) Pin(fileName string) error {
-	return it.link.Pin(fileName)
-}
-
-// Update implements Link.
-func (it *Iter) Update(new *ebpf.Program) error {
-	return it.link.Update(new)
+	RawLink
 }
 
 // Open creates a new instance of the iterator.
 //
 // Reading from the returned reader triggers the BPF program.
 func (it *Iter) Open() (io.ReadCloser, error) {
-	linkFd, err := it.link.fd.Value()
+	linkFd, err := it.fd.Value()
 	if err != nil {
 		return nil, err
 	}
diff --git a/link/link.go b/link/link.go
index 48f1a55..16cfff4 100644
--- a/link/link.go
+++ b/link/link.go
@@ -22,6 +22,11 @@ type Link interface {
 	// May return an error wrapping ErrNotSupported.
 	Pin(string) error
 
+	// Undo a previous call to Pin.
+	//
+	// May return an error wrapping ErrNotSupported.
+	Unpin() error
+
 	// Close frees resources.
 	//
 	// The link will be broken unless it has been pinned. A link
@@ -58,7 +63,8 @@ type RawLinkInfo struct {
 // You should consider using the higher level interfaces in this
 // package instead.
 type RawLink struct {
-	fd *internal.FD
+	fd         *internal.FD
+	pinnedPath string
 }
 
 // AttachRawLink creates a raw link.
@@ -86,22 +92,21 @@ func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
 		return nil, fmt.Errorf("can't create link: %s", err)
 	}
 
-	return &RawLink{fd}, nil
+	return &RawLink{fd, ""}, nil
 }
 
 // LoadPinnedRawLink loads a persisted link from a bpffs.
-func LoadPinnedRawLink(fileName string) (*RawLink, error) {
-	return loadPinnedRawLink(fileName, UnspecifiedType)
-}
-
-func loadPinnedRawLink(fileName string, typ Type) (*RawLink, error) {
-	fd, err := internal.BPFObjGet(fileName)
+//
+// Returns an error if the pinned link type doesn't match linkType. Pass
+// UnspecifiedType to disable this behaviour.
+func LoadPinnedRawLink(fileName string, linkType Type, opts *ebpf.LoadPinOptions) (*RawLink, error) {
+	fd, err := internal.BPFObjGet(fileName, opts.Marshal())
 	if err != nil {
-		return nil, fmt.Errorf("load pinned link: %s", err)
+		return nil, fmt.Errorf("load pinned link: %w", err)
 	}
 
-	link := &RawLink{fd}
-	if typ == UnspecifiedType {
+	link := &RawLink{fd, fileName}
+	if linkType == UnspecifiedType {
 		return link, nil
 	}
 
@@ -111,9 +116,9 @@ func loadPinnedRawLink(fileName string, typ Type) (*RawLink, error) {
 		return nil, fmt.Errorf("get pinned link info: %s", err)
 	}
 
-	if info.Type != typ {
+	if info.Type != linkType {
 		link.Close()
-		return nil, fmt.Errorf("link type %v doesn't match %v", info.Type, typ)
+		return nil, fmt.Errorf("link type %v doesn't match %v", info.Type, linkType)
 	}
 
 	return link, nil
@@ -142,13 +147,23 @@ func (l *RawLink) Close() error {
 // Calling Close on a pinned Link will not break the link
 // until the pin is removed.
 func (l *RawLink) Pin(fileName string) error {
-	if err := internal.BPFObjPin(fileName, l.fd); err != nil {
-		return fmt.Errorf("can't pin link: %s", err)
+	if err := internal.Pin(l.pinnedPath, fileName, l.fd); err != nil {
+		return err
+	}
+	l.pinnedPath = fileName
+	return nil
+}
+
+// Unpin implements the Link interface.
+func (l *RawLink) Unpin() error {
+	if err := internal.Unpin(l.pinnedPath); err != nil {
+		return err
 	}
+	l.pinnedPath = ""
 	return nil
 }
 
-// Update implements Link.
+// Update implements the Link interface.
 func (l *RawLink) Update(new *ebpf.Program) error {
 	return l.UpdateArgs(RawLinkUpdateOptions{
 		New: new,
diff --git a/link/netns.go b/link/netns.go
index 3533ff0..37e5b84 100644
--- a/link/netns.go
+++ b/link/netns.go
@@ -41,8 +41,8 @@ func AttachNetNs(ns int, prog *ebpf.Program) (*NetNsLink, error) {
 }
 
 // LoadPinnedNetNs loads a network namespace link from bpffs.
-func LoadPinnedNetNs(fileName string) (*NetNsLink, error) {
-	link, err := loadPinnedRawLink(fileName, NetNsType)
+func LoadPinnedNetNs(fileName string, opts *ebpf.LoadPinOptions) (*NetNsLink, error) {
+	link, err := LoadPinnedRawLink(fileName, NetNsType, opts)
 	if err != nil {
 		return nil, err
 	}
diff --git a/link/perf_event.go b/link/perf_event.go
index b27c183..7cce416 100644
--- a/link/perf_event.go
+++ b/link/perf_event.go
@@ -78,6 +78,10 @@ func (pe *perfEvent) Pin(string) error {
 	return fmt.Errorf("pin perf event: %w", ErrNotSupported)
 }
 
+func (pe *perfEvent) Unpin() error {
+	return fmt.Errorf("unpin perf event: %w", ErrNotSupported)
+}
+
 // Since 4.15 (e87c6bc3852b "bpf: permit multiple bpf attachments for a single perf event"),
 // calling PERF_EVENT_IOC_SET_BPF appends the given program to a prog_array
 // owned by the perf event, which means multiple programs can be attached
diff --git a/link/raw_tracepoint.go b/link/raw_tracepoint.go
index 6565248..f4beb1e 100644
--- a/link/raw_tracepoint.go
+++ b/link/raw_tracepoint.go
@@ -55,3 +55,7 @@ func (rt *progAttachRawTracepoint) Update(_ *ebpf.Program) error {
 func (rt *progAttachRawTracepoint) Pin(_ string) error {
 	return fmt.Errorf("can't pin raw_tracepoint: %w", ErrNotSupported)
 }
+
+func (rt *progAttachRawTracepoint) Unpin() error {
+	return fmt.Errorf("unpin raw_tracepoint: %w", ErrNotSupported)
+}
diff --git a/map.go b/map.go
index 677ab1e..5f04984 100644
--- a/map.go
+++ b/map.go
@@ -25,7 +25,8 @@ type MapOptions struct {
 	// The base path to pin maps in if requested via PinByName.
 	// Existing maps will be re-used if they are compatible, otherwise an
 	// error is returned.
-	PinPath string
+	PinPath        string
+	LoadPinOptions LoadPinOptions
 }
 
 // MapID represents the unique ID of an eBPF map
@@ -174,23 +175,30 @@ func NewMapWithOptions(spec *MapSpec, opts MapOptions) (*Map, error) {
 	return newMapWithOptions(spec, opts, btfs)
 }
 
-func newMapWithOptions(spec *MapSpec, opts MapOptions, btfs btfHandleCache) (*Map, error) {
+func newMapWithOptions(spec *MapSpec, opts MapOptions, btfs btfHandleCache) (_ *Map, err error) {
+	closeOnError := func(c io.Closer) {
+		if err != nil {
+			c.Close()
+		}
+	}
+
 	switch spec.Pinning {
 	case PinByName:
 		if spec.Name == "" || opts.PinPath == "" {
 			return nil, fmt.Errorf("pin by name: missing Name or PinPath")
 		}
 
-		m, err := LoadPinnedMap(filepath.Join(opts.PinPath, spec.Name))
+		path := filepath.Join(opts.PinPath, spec.Name)
+		m, err := LoadPinnedMap(path, &opts.LoadPinOptions)
 		if errors.Is(err, unix.ENOENT) {
 			break
 		}
 		if err != nil {
-			return nil, fmt.Errorf("load pinned map: %s", err)
+			return nil, fmt.Errorf("load pinned map: %w", err)
 		}
+		defer closeOnError(m)
 
 		if err := spec.checkCompatibility(m); err != nil {
-			m.Close()
 			return nil, fmt.Errorf("use pinned map %s: %s", spec.Name, err)
 		}
 
@@ -226,10 +234,11 @@ func newMapWithOptions(spec *MapSpec, opts MapOptions, btfs btfHandleCache) (*Ma
 	if err != nil {
 		return nil, err
 	}
+	defer closeOnError(m)
 
 	if spec.Pinning == PinByName {
-		if err := m.Pin(filepath.Join(opts.PinPath, spec.Name)); err != nil {
-			m.Close()
+		path := filepath.Join(opts.PinPath, spec.Name)
+		if err := m.Pin(path); err != nil {
 			return nil, fmt.Errorf("pin map: %s", err)
 		}
 	}
@@ -811,7 +820,7 @@ func (m *Map) Clone() (*Map, error) {
 //
 // This requires bpffs to be mounted above fileName. See https://docs.cilium.io/en/k8s-doc/admin/#admin-mount-bpffs
 func (m *Map) Pin(fileName string) error {
-	if err := pin(m.pinnedPath, fileName, m.fd); err != nil {
+	if err := internal.Pin(m.pinnedPath, fileName, m.fd); err != nil {
 		return err
 	}
 	m.pinnedPath = fileName
@@ -824,7 +833,7 @@ func (m *Map) Pin(fileName string) error {
 //
 // Unpinning an unpinned Map returns nil.
 func (m *Map) Unpin() error {
-	if err := unpin(m.pinnedPath); err != nil {
+	if err := internal.Unpin(m.pinnedPath); err != nil {
 		return err
 	}
 	m.pinnedPath = ""
@@ -973,9 +982,9 @@ func (m *Map) unmarshalValue(value interface{}, buf []byte) error {
 	return unmarshalBytes(value, buf)
 }
 
-// LoadPinnedMap load a Map from a BPF file.
-func LoadPinnedMap(fileName string) (*Map, error) {
-	fd, err := internal.BPFObjGet(fileName)
+// LoadPinnedMap loads a Map from a BPF file.
+func LoadPinnedMap(fileName string, opts *LoadPinOptions) (*Map, error) {
+	fd, err := internal.BPFObjGet(fileName, opts.Marshal())
 	if err != nil {
 		return nil, err
 	}
diff --git a/prog.go b/prog.go
index dcb5fe1..e5d91fd 100644
--- a/prog.go
+++ b/prog.go
@@ -350,7 +350,7 @@ func (p *Program) Clone() (*Program, error) {
 //
 // This requires bpffs to be mounted above fileName. See https://docs.cilium.io/en/k8s-doc/admin/#admin-mount-bpffs
 func (p *Program) Pin(fileName string) error {
-	if err := pin(p.pinnedPath, fileName, p.fd); err != nil {
+	if err := internal.Pin(p.pinnedPath, fileName, p.fd); err != nil {
 		return err
 	}
 	p.pinnedPath = fileName
@@ -363,7 +363,7 @@ func (p *Program) Pin(fileName string) error {
 //
 // Unpinning an unpinned Program returns nil.
 func (p *Program) Unpin() error {
-	if err := unpin(p.pinnedPath); err != nil {
+	if err := internal.Unpin(p.pinnedPath); err != nil {
 		return err
 	}
 	p.pinnedPath = ""
@@ -597,8 +597,8 @@ func (p *Program) Detach(fd int, typ AttachType, flags AttachFlags) error {
 // LoadPinnedProgram loads a Program from a BPF file.
 //
 // Requires at least Linux 4.11.
-func LoadPinnedProgram(fileName string) (*Program, error) {
-	fd, err := internal.BPFObjGet(fileName)
+func LoadPinnedProgram(fileName string, opts *LoadPinOptions) (*Program, error) {
+	fd, err := internal.BPFObjGet(fileName, opts.Marshal())
 	if err != nil {
 		return nil, err
 	}
diff --git a/syscalls.go b/syscalls.go
index 1cba1d7..c530aad 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -383,7 +383,7 @@ func wrapMapError(err error) error {
 		return ErrNotSupported
 	}
 
-	return errors.New(err.Error())
+	return err
 }
 
 func bpfMapFreeze(m *internal.FD) error {
diff --git a/types.go b/types.go
index 3191ba1..f4d1a8c 100644
--- a/types.go
+++ b/types.go
@@ -1,5 +1,9 @@
 package ebpf
 
+import (
+	"github.com/cilium/ebpf/internal/unix"
+)
+
 //go:generate stringer -output types_string.go -type=MapType,ProgramType,AttachType,PinType
 
 // MapType indicates the type map structure
@@ -202,6 +206,33 @@ const (
 	PinByName
 )
 
+// LoadPinOptions control how a pinned object is loaded.
+type LoadPinOptions struct {
+	// Request a read-only or write-only object. The default is a read-write
+	// object. Only one of the flags may be set.
+	ReadOnly  bool
+	WriteOnly bool
+
+	// Raw flags for the syscall. Other fields of this struct take precedence.
+	Flags uint32
+}
+
+// Marshal returns a value suitable for BPF_OBJ_GET syscall file_flags parameter.
+func (lpo *LoadPinOptions) Marshal() uint32 {
+	if lpo == nil {
+		return 0
+	}
+
+	flags := lpo.Flags
+	if lpo.ReadOnly {
+		flags |= unix.BPF_F_RDONLY
+	}
+	if lpo.WriteOnly {
+		flags |= unix.BPF_F_WRONLY
+	}
+	return flags
+}
+
 // BatchOptions batch map operations options
 //
 // Mirrors libbpf struct bpf_map_batch_opts


Test patch:
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 9ae6c60..5178566 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -3,7 +3,6 @@ package ebpf
 import (
 	"errors"
 	"flag"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -156,7 +155,7 @@ func TestLoadCollectionSpec(t *testing.T) {
 		have.Maps["array_of_hash_map"].InnerMap = have.Maps["hash_map"]
 		coll, err := NewCollectionWithOptions(have, CollectionOptions{
 			Maps: MapOptions{
-				PinPath: tempBPFFS(t),
+				PinPath: testutils.TempBPFFS(t),
 			},
 			Programs: ProgramOptions{
 				LogLevel: 1,
@@ -343,15 +342,3 @@ func TestGetProgType(t *testing.T) {
 		}
 	}
 }
-
-func tempBPFFS(tb testing.TB) string {
-	tb.Helper()
-
-	tmp, err := ioutil.TempDir("/sys/fs/bpf", "ebpf-test")
-	if err != nil {
-		tb.Fatal(err)
-	}
-	tb.Cleanup(func() { os.RemoveAll(tmp) })
-
-	return tmp
-}
diff --git a/internal/testutils/bpffs.go b/internal/testutils/bpffs.go
new file mode 100644
index 0000000..5ed9e06
--- /dev/null
+++ b/internal/testutils/bpffs.go
@@ -0,0 +1,22 @@
+package testutils
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+// TempBPFFS creates a temporary directory on a BPF FS.
+//
+// The directory is automatically cleaned up at the end of the test run.
+func TempBPFFS(tb testing.TB) string {
+	tb.Helper()
+
+	tmp, err := ioutil.TempDir("/sys/fs/bpf", "ebpf-test")
+	if err != nil {
+		tb.Fatal("Create temporary directory on BPFFS:", err)
+	}
+	tb.Cleanup(func() { os.RemoveAll(tmp) })
+
+	return tmp
+}
diff --git a/link/cgroup_test.go b/link/cgroup_test.go
index d9358a4..f80c3ce 100644
--- a/link/cgroup_test.go
+++ b/link/cgroup_test.go
@@ -8,8 +8,7 @@ import (
 )
 
 func TestAttachCgroup(t *testing.T) {
-	cgroup, prog, cleanup := mustCgroupFixtures(t)
-	defer cleanup()
+	cgroup, prog := mustCgroupFixtures(t)
 
 	link, err := AttachCgroup(CgroupOptions{
 		Path:    cgroup.Name(),
@@ -33,8 +32,7 @@ func TestAttachCgroup(t *testing.T) {
 }
 
 func TestProgAttachCgroup(t *testing.T) {
-	cgroup, prog, cleanup := mustCgroupFixtures(t)
-	defer cleanup()
+	cgroup, prog := mustCgroupFixtures(t)
 
 	link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, 0)
 	if err != nil {
@@ -47,8 +45,7 @@ func TestProgAttachCgroup(t *testing.T) {
 }
 
 func TestProgAttachCgroupAllowMulti(t *testing.T) {
-	cgroup, prog, cleanup := mustCgroupFixtures(t)
-	defer cleanup()
+	cgroup, prog := mustCgroupFixtures(t)
 
 	link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, flagAllowMulti)
 	testutils.SkipIfNotSupported(t, err)
@@ -65,8 +62,7 @@ func TestProgAttachCgroupAllowMulti(t *testing.T) {
 }
 
 func TestLinkCgroup(t *testing.T) {
-	cgroup, prog, cleanup := mustCgroupFixtures(t)
-	defer cleanup()
+	cgroup, prog := mustCgroupFixtures(t)
 
 	link, err := newLinkCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog)
 	testutils.SkipIfNotSupported(t, err)
diff --git a/link/iter_test.go b/link/iter_test.go
index 1a7e75d..923e3c6 100644
--- a/link/iter_test.go
+++ b/link/iter_test.go
@@ -50,8 +50,8 @@ func TestIter(t *testing.T) {
 
 	testLink(t, it, testLinkOptions{
 		prog: prog,
-		loadPinned: func(s string) (Link, error) {
-			return LoadPinnedIter(s)
+		loadPinned: func(s string, opts *ebpf.LoadPinOptions) (Link, error) {
+			return LoadPinnedIter(s, opts)
 		},
 	})
 }
diff --git a/link/link_test.go b/link/link_test.go
index 10bbb63..e0151a6 100644
--- a/link/link_test.go
+++ b/link/link_test.go
@@ -3,6 +3,7 @@ package link
 import (
 	"errors"
 	"io/ioutil"
+	"math"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -11,11 +12,11 @@ import (
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal/testutils"
+	"github.com/cilium/ebpf/internal/unix"
 )
 
 func TestRawLink(t *testing.T) {
-	cgroup, prog, cleanup := mustCgroupFixtures(t)
-	defer cleanup()
+	cgroup, prog := mustCgroupFixtures(t)
 
 	link, err := AttachRawLink(RawLinkOptions{
 		Target:  int(cgroup.Fd()),
@@ -48,13 +49,43 @@ func TestRawLink(t *testing.T) {
 
 	testLink(t, link, testLinkOptions{
 		prog: prog,
-		loadPinned: func(f string) (Link, error) {
-			return LoadPinnedRawLink(f)
+		loadPinned: func(f string, opts *ebpf.LoadPinOptions) (Link, error) {
+			return LoadPinnedRawLink(f, UnspecifiedType, opts)
 		},
 	})
 }
 
-func mustCgroupFixtures(t *testing.T) (*os.File, *ebpf.Program, func()) {
+func TestRawLinkLoadPinnedWithOptions(t *testing.T) {
+	cgroup, prog := mustCgroupFixtures(t)
+
+	link, err := AttachRawLink(RawLinkOptions{
+		Target:  int(cgroup.Fd()),
+		Program: prog,
+		Attach:  ebpf.AttachCGroupInetEgress,
+	})
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal("Can't create raw link:", err)
+	}
+
+	path := filepath.Join(testutils.TempBPFFS(t), "link")
+	err = link.Pin(path)
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// It seems like the kernel ignores BPF_F_RDONLY when updating a link,
+	// so we can't test this.
+	_, err = LoadPinnedRawLink(path, UnspecifiedType, &ebpf.LoadPinOptions{
+		Flags: math.MaxUint32,
+	})
+	if !errors.Is(err, unix.EINVAL) {
+		t.Fatal("Invalid flags don't trigger an error:", err)
+	}
+}
+
+func mustCgroupFixtures(t *testing.T) (*os.File, *ebpf.Program) {
 	t.Helper()
 
 	testutils.SkipIfNotSupported(t, haveProgAttach())
@@ -72,12 +103,13 @@ func mustCgroupFixtures(t *testing.T) (*os.File, *ebpf.Program, func()) {
 		os.Remove(cgdir)
 		t.Fatal(err)
 	}
-
-	return cgroup, prog, func() {
+	t.Cleanup(func() {
 		prog.Close()
 		cgroup.Close()
 		os.Remove(cgdir)
-	}
+	})
+
+	return cgroup, prog
 }
 
 func mustCgroupEgressProgram(t *testing.T) *ebpf.Program {
@@ -100,7 +132,7 @@ func mustCgroupEgressProgram(t *testing.T) *ebpf.Program {
 
 type testLinkOptions struct {
 	prog       *ebpf.Program
-	loadPinned func(string) (Link, error)
+	loadPinned func(string, *ebpf.LoadPinOptions) (Link, error)
 }
 
 func testLink(t *testing.T, link Link, opts testLinkOptions) {
@@ -127,7 +159,7 @@ func testLink(t *testing.T, link Link, opts testLinkOptions) {
 			t.Fatalf("Can't pin %T: %s", link, err)
 		}
 
-		link2, err := opts.loadPinned(path)
+		link2, err := opts.loadPinned(path, nil)
 		if err != nil {
 			t.Fatalf("Can't load pinned %T: %s", link, err)
 		}
@@ -136,6 +168,13 @@ func testLink(t *testing.T, link Link, opts testLinkOptions) {
 		if reflect.TypeOf(link) != reflect.TypeOf(link2) {
 			t.Errorf("Loading a pinned %T returns a %T", link, link2)
 		}
+
+		_, err = opts.loadPinned(path, &ebpf.LoadPinOptions{
+			Flags: math.MaxUint32,
+		})
+		if !errors.Is(err, unix.EINVAL) {
+			t.Errorf("Loading a pinned %T doesn't respect flags", link)
+		}
 	}
 
 	t.Run("update", func(t *testing.T) {
diff --git a/link/netns_test.go b/link/netns_test.go
index d167581..4121d7d 100644
--- a/link/netns_test.go
+++ b/link/netns_test.go
@@ -12,11 +12,7 @@ import (
 func TestSkLookup(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.8", "sk_lookup program")
 
-	prog, err := createSkLookupProgram()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := mustCreateSkLookupProgram(t)
 
 	netns, err := os.Open("/proc/self/ns/net")
 	if err != nil {
@@ -36,12 +32,24 @@ func TestSkLookup(t *testing.T) {
 
 	testLink(t, link, testLinkOptions{
 		prog: prog,
-		loadPinned: func(fileName string) (Link, error) {
-			return LoadPinnedNetNs(fileName)
+		loadPinned: func(fileName string, opts *ebpf.LoadPinOptions) (Link, error) {
+			return LoadPinnedNetNs(fileName, opts)
 		},
 	})
 }
 
+func mustCreateSkLookupProgram(tb testing.TB) *ebpf.Program {
+	tb.Helper()
+
+	prog, err := createSkLookupProgram()
+	if err != nil {
+		tb.Fatal(err)
+	}
+	tb.Cleanup(func() { prog.Close() })
+
+	return prog
+}
+
 func createSkLookupProgram() (*ebpf.Program, error) {
 	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
 		Type:       ebpf.SkLookup,
diff --git a/map_test.go b/map_test.go
index c59e02a..7911cb2 100644
--- a/map_test.go
+++ b/map_test.go
@@ -309,7 +309,7 @@ func TestMapPin(t *testing.T) {
 		t.Fatal("Can't put:", err)
 	}
 
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 
 	// Issue 51: pad path out to a power of two, to avoid having a
 	// trailing zero at the end of the allocation which holds the string.
@@ -325,7 +325,7 @@ func TestMapPin(t *testing.T) {
 
 	m.Close()
 
-	m, err := LoadPinnedMap(path)
+	m, err := LoadPinnedMap(path, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -371,7 +371,7 @@ func TestNestedMapPin(t *testing.T) {
 	}
 	m.Close()
 
-	m, err = LoadPinnedMap(path)
+	m, err = LoadPinnedMap(path, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -398,7 +398,7 @@ func TestNestedMapPinNested(t *testing.T) {
 }
 
 func TestMapPinMultiple(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 
 	spec := spec1.Copy()
@@ -418,7 +418,7 @@ func TestMapPinMultiple(t *testing.T) {
 	if _, err := os.Stat(oldPath); err == nil {
 		t.Fatal("Previous pinned map path still exists:", err)
 	}
-	m2, err := LoadPinnedMap(newPath)
+	m2, err := LoadPinnedMap(newPath, nil)
 	c.Assert(err, qt.IsNil)
 	defer m2.Close()
 }
@@ -434,7 +434,7 @@ func TestMapPinWithEmptyPath(t *testing.T) {
 }
 
 func TestMapPinFailReplace(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 	spec := spec1.Copy()
 	spec2 := spec1.Copy()
@@ -458,7 +458,7 @@ func TestMapPinFailReplace(t *testing.T) {
 }
 
 func TestMapUnpin(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 	spec := spec1.Copy()
 
@@ -471,7 +471,7 @@ func TestMapUnpin(t *testing.T) {
 	pinned := m.IsPinned()
 	c.Assert(pinned, qt.Equals, true)
 	path := filepath.Join(tmp, spec.Name)
-	m2, err := LoadPinnedMap(path)
+	m2, err := LoadPinnedMap(path, nil)
 	c.Assert(err, qt.IsNil)
 	defer m2.Close()
 
@@ -484,7 +484,7 @@ func TestMapUnpin(t *testing.T) {
 }
 
 func TestMapLoadPinned(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 
 	spec := spec1.Copy()
@@ -496,7 +496,7 @@ func TestMapLoadPinned(t *testing.T) {
 	c.Assert(pinned, qt.Equals, true)
 
 	path := filepath.Join(tmp, spec.Name)
-	m2, err := LoadPinnedMap(path)
+	m2, err := LoadPinnedMap(path, nil)
 	c.Assert(err, qt.IsNil)
 	defer m2.Close()
 	pinned = m2.IsPinned()
@@ -504,7 +504,7 @@ func TestMapLoadPinned(t *testing.T) {
 }
 
 func TestMapLoadPinnedUnpin(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 
 	spec := spec1.Copy()
@@ -516,7 +516,7 @@ func TestMapLoadPinnedUnpin(t *testing.T) {
 	c.Assert(pinned, qt.Equals, true)
 
 	path := filepath.Join(tmp, spec.Name)
-	m2, err := LoadPinnedMap(path)
+	m2, err := LoadPinnedMap(path, nil)
 	c.Assert(err, qt.IsNil)
 	defer m2.Close()
 	err = m1.Unpin()
@@ -525,6 +525,85 @@ func TestMapLoadPinnedUnpin(t *testing.T) {
 	c.Assert(err, qt.IsNil)
 }
 
+func TestMapLoadPinnedWithOptions(t *testing.T) {
+	// Introduced in commit 6e71b04a8224.
+	testutils.SkipOnOldKernel(t, "4.14", "file_flags in BPF_OBJ_GET")
+
+	array := createArray(t)
+	defer array.Close()
+
+	tmp := testutils.TempBPFFS(t)
+
+	path := filepath.Join(tmp, "map")
+	if err := array.Pin(path); err != nil {
+		t.Fatal(err)
+	}
+	if err := array.Put(uint32(0), uint32(123)); err != nil {
+		t.Fatal(err)
+	}
+	array.Close()
+
+	t.Run("read-only", func(t *testing.T) {
+		array, err := LoadPinnedMap(path, &LoadPinOptions{
+			ReadOnly: true,
+		})
+		testutils.SkipIfNotSupported(t, err)
+		if err != nil {
+			t.Fatal("Can't load map:", err)
+		}
+		defer array.Close()
+
+		if err := array.Put(uint32(0), uint32(1)); !errors.Is(err, unix.EPERM) {
+			t.Fatal("Expected EPERM from Put, got", err)
+		}
+	})
+
+	t.Run("write-only", func(t *testing.T) {
+		array, err := LoadPinnedMap(path, &LoadPinOptions{
+			WriteOnly: true,
+		})
+		testutils.SkipIfNotSupported(t, err)
+		if err != nil {
+			t.Fatal("Can't load map:", err)
+		}
+		defer array.Close()
+
+		var value uint32
+		if err := array.Lookup(uint32(0), &value); !errors.Is(err, unix.EPERM) {
+			t.Fatal("Expected EPERM from Lookup, got", err)
+		}
+	})
+}
+
+func TestMapPinFlags(t *testing.T) {
+	tmp := testutils.TempBPFFS(t)
+
+	spec := &MapSpec{
+		Name:       "map",
+		Type:       Array,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 1,
+		Pinning:    PinByName,
+	}
+
+	m, err := NewMapWithOptions(spec, MapOptions{
+		PinPath: tmp,
+	})
+	qt.Assert(t, err, qt.IsNil)
+	m.Close()
+
+	_, err = NewMapWithOptions(spec, MapOptions{
+		PinPath: tmp,
+		LoadPinOptions: LoadPinOptions{
+			Flags: math.MaxUint32,
+		},
+	})
+	if !errors.Is(err, unix.EINVAL) {
+		t.Fatal("Invalid flags should trigger EINVAL:", err)
+	}
+}
+
 func createArray(t *testing.T) *Map {
 	t.Helper()
 
@@ -1193,7 +1272,7 @@ func TestNewMapFromID(t *testing.T) {
 }
 
 func TestMapPinning(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 
 	spec := &MapSpec{
diff --git a/prog_test.go b/prog_test.go
index b11ef15..578df52 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -186,7 +186,7 @@ func TestProgramPin(t *testing.T) {
 	c := qt.New(t)
 	defer prog.Close()
 
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 
 	path := filepath.Join(tmp, "program")
 	if err := prog.Pin(path); err != nil {
@@ -198,7 +198,7 @@ func TestProgramPin(t *testing.T) {
 
 	prog.Close()
 
-	prog, err := LoadPinnedProgram(path)
+	prog, err := LoadPinnedProgram(path, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal(err)
@@ -219,7 +219,7 @@ func TestProgramUnpin(t *testing.T) {
 	c := qt.New(t)
 	defer prog.Close()
 
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 
 	path := filepath.Join(tmp, "program")
 	if err := prog.Pin(path); err != nil {
@@ -237,6 +237,31 @@ func TestProgramUnpin(t *testing.T) {
 	}
 }
 
+func TestProgramLoadPinnedWithFlags(t *testing.T) {
+	// Introduced in commit 6e71b04a8224.
+	testutils.SkipOnOldKernel(t, "4.14", "file_flags in BPF_OBJ_GET")
+
+	prog := createSocketFilter(t)
+	defer prog.Close()
+
+	tmp := testutils.TempBPFFS(t)
+
+	path := filepath.Join(tmp, "program")
+	if err := prog.Pin(path); err != nil {
+		t.Fatal(err)
+	}
+
+	prog.Close()
+
+	_, err := LoadPinnedProgram(path, &LoadPinOptions{
+		Flags: math.MaxUint32,
+	})
+	testutils.SkipIfNotSupported(t, err)
+	if !errors.Is(err, unix.EINVAL) {
+		t.Fatal("Invalid flags don't trigger an error:", err)
+	}
+}
+
 func TestProgramVerifierOutputOnError(t *testing.T) {
 	_, err := NewProgram(&ProgramSpec{
 		Type: SocketFilter,
@@ -632,7 +657,7 @@ func ExampleNewProgramWithOptions() {
 
 // It's possible to read a program directly from a ProgramArray.
 func ExampleProgram_unmarshalFromMap() {
-	progArray, err := LoadPinnedMap("/path/to/map")
+	progArray, err := LoadPinnedMap("/path/to/map", nil)
 	if err != nil {
 		panic(err)
 	}