Repository: cilium/ebpf
Pull request #260 (2021-03-19 08:38:24)

prog: add Flags to ProgSpec
Linux 5.10 added sleepable BPF programs.

Quoting uapi/linux/bpf.h:

"If BPF_F_SLEEPABLE is used in BPF_PROG_LOAD command, the verifier will
restrict map and helper usage for such programs. Sleepable BPF programs can
only be attached to hooks where kernel execution context allows sleeping.
Such programs are allowed to use helpers that may sleep like
bpf_copy_from_user()."

The sleepable BPF programs are used, e.g., with LSM hooks and these
are also detected using "lsm.s" ELF section. This commit adds that
capability to elf_reader too.

Signed-off-by: Mikko Ylinen <mikko.ylinen@intel.com>

mythi: > Some nits, but it seems reasonable to me otherwise.

@lmb OK! thanks for the review. I'll be OOO next week so I'll address the feedback once I'm back

mythi: looks like CI is failing to create test env (apt failures)
mythi: > I'm still on the fence whether we should already wrap this up into a `ProgFlags` 

@ti-mo I followed what is already available in `MapSpec` and thought `Flags` was justified for `ProgSpec` too.

> I'm not a fan of exposing raw bitfields, it's not user-friendly.

how this works for `MapSpec`? I can see `TestMapInfoFromProc` (info_test.go)  uses raw bitfields for `MapSpec` too.



Patch:
diff --git a/README.md b/README.md
index 7f504d3..76c3c30 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ be used in long running processes.
 * [perf](https://pkg.go.dev/github.com/cilium/ebpf/perf) allows reading from a
   `PERF_EVENT_ARRAY`
 * [cmd/bpf2go](https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go) allows
-  embedding eBPF in Go
+  compiling and embedding eBPF programs in Go code
 
 The library is maintained by [Cloudflare](https://www.cloudflare.com) and
 [Cilium](https://www.cilium.io). Feel free to
@@ -25,6 +25,14 @@ The library is maintained by [Cloudflare](https://www.cloudflare.com) and
 The package is production ready, but **the API is explicitly unstable right
 now**. Expect to update your code if you want to follow along.
 
+## Getting Started
+
+A small collection of Go and eBPF programs that serve as examples for building
+your own tools can be found under [examples/](examples/).
+
+Contributions are highly encouraged, as they highlight certain use cases of
+eBPF and the library, and help shape the future of the project.
+
 ## Requirements
 
 * A version of Go that is [supported by
diff --git a/elf_reader.go b/elf_reader.go
index f57a25d..3ae44f6 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -272,11 +272,12 @@ func (ec *elfCode) loadPrograms() (map[string]*ProgramSpec, error) {
 			return nil, fmt.Errorf("program %s: %w", funcSym.Name, err)
 		}
 
-		progType, attachType, attachTo := getProgType(sec.Name)
+		progType, attachType, progFlags, attachTo := getProgType(sec.Name)
 
 		spec := &ProgramSpec{
 			Name:          funcSym.Name,
 			Type:          progType,
+			Flags:         progFlags,
 			AttachType:    attachType,
 			AttachTo:      attachTo,
 			License:       ec.license,
@@ -831,56 +832,61 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error {
 	return nil
 }
 
-func getProgType(sectionName string) (ProgramType, AttachType, string) {
+func getProgType(sectionName string) (ProgramType, AttachType, uint32, string) {
 	types := map[string]struct {
 		progType   ProgramType
 		attachType AttachType
+		progFlags  uint32
 	}{
 		// From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/lib/bpf/libbpf.c
-		"socket":                {SocketFilter, AttachNone},
-		"seccomp":               {SocketFilter, AttachNone},
-		"kprobe/":               {Kprobe, AttachNone},
-		"uprobe/":               {Kprobe, AttachNone},
-		"kretprobe/":            {Kprobe, AttachNone},
-		"uretprobe/":            {Kprobe, AttachNone},
-		"tracepoint/":           {TracePoint, AttachNone},
-		"raw_tracepoint/":       {RawTracepoint, AttachNone},
-		"xdp":                   {XDP, AttachNone},
-		"perf_event":            {PerfEvent, AttachNone},
-		"lwt_in":                {LWTIn, AttachNone},
-		"lwt_out":               {LWTOut, AttachNone},
-		"lwt_xmit":              {LWTXmit, AttachNone},
-		"lwt_seg6local":         {LWTSeg6Local, AttachNone},
-		"sockops":               {SockOps, AttachCGroupSockOps},
-		"sk_skb/stream_parser":  {SkSKB, AttachSkSKBStreamParser},
-		"sk_skb/stream_verdict": {SkSKB, AttachSkSKBStreamParser},
-		"sk_msg":                {SkMsg, AttachSkSKBStreamVerdict},
-		"lirc_mode2":            {LircMode2, AttachLircMode2},
-		"flow_dissector":        {FlowDissector, AttachFlowDissector},
-		"iter/":                 {Tracing, AttachTraceIter},
-		"sk_lookup/":            {SkLookup, AttachSkLookup},
-		"lsm/":                  {LSM, AttachLSMMac},
-
-		"cgroup_skb/ingress": {CGroupSKB, AttachCGroupInetIngress},
-		"cgroup_skb/egress":  {CGroupSKB, AttachCGroupInetEgress},
-		"cgroup/dev":         {CGroupDevice, AttachCGroupDevice},
-		"cgroup/skb":         {CGroupSKB, AttachNone},
-		"cgroup/sock":        {CGroupSock, AttachCGroupInetSockCreate},
-		"cgroup/post_bind4":  {CGroupSock, AttachCGroupInet4PostBind},
-		"cgroup/post_bind6":  {CGroupSock, AttachCGroupInet6PostBind},
-		"cgroup/bind4":       {CGroupSockAddr, AttachCGroupInet4Bind},
-		"cgroup/bind6":       {CGroupSockAddr, AttachCGroupInet6Bind},
-		"cgroup/connect4":    {CGroupSockAddr, AttachCGroupInet4Connect},
-		"cgroup/connect6":    {CGroupSockAddr, AttachCGroupInet6Connect},
-		"cgroup/sendmsg4":    {CGroupSockAddr, AttachCGroupUDP4Sendmsg},
-		"cgroup/sendmsg6":    {CGroupSockAddr, AttachCGroupUDP6Sendmsg},
-		"cgroup/recvmsg4":    {CGroupSockAddr, AttachCGroupUDP4Recvmsg},
-		"cgroup/recvmsg6":    {CGroupSockAddr, AttachCGroupUDP6Recvmsg},
-		"cgroup/sysctl":      {CGroupSysctl, AttachCGroupSysctl},
-		"cgroup/getsockopt":  {CGroupSockopt, AttachCGroupGetsockopt},
-		"cgroup/setsockopt":  {CGroupSockopt, AttachCGroupSetsockopt},
-		"classifier":         {SchedCLS, AttachNone},
-		"action":             {SchedACT, AttachNone},
+		"socket":                {SocketFilter, AttachNone, 0},
+		"seccomp":               {SocketFilter, AttachNone, 0},
+		"kprobe/":               {Kprobe, AttachNone, 0},
+		"uprobe/":               {Kprobe, AttachNone, 0},
+		"kretprobe/":            {Kprobe, AttachNone, 0},
+		"uretprobe/":            {Kprobe, AttachNone, 0},
+		"tracepoint/":           {TracePoint, AttachNone, 0},
+		"raw_tracepoint/":       {RawTracepoint, AttachNone, 0},
+		"xdp":                   {XDP, AttachNone, 0},
+		"perf_event":            {PerfEvent, AttachNone, 0},
+		"lwt_in":                {LWTIn, AttachNone, 0},
+		"lwt_out":               {LWTOut, AttachNone, 0},
+		"lwt_xmit":              {LWTXmit, AttachNone, 0},
+		"lwt_seg6local":         {LWTSeg6Local, AttachNone, 0},
+		"sockops":               {SockOps, AttachCGroupSockOps, 0},
+		"sk_skb/stream_parser":  {SkSKB, AttachSkSKBStreamParser, 0},
+		"sk_skb/stream_verdict": {SkSKB, AttachSkSKBStreamParser, 0},
+		"sk_msg":                {SkMsg, AttachSkSKBStreamVerdict, 0},
+		"lirc_mode2":            {LircMode2, AttachLircMode2, 0},
+		"flow_dissector":        {FlowDissector, AttachFlowDissector, 0},
+		"iter/":                 {Tracing, AttachTraceIter, 0},
+		"fentry.s/":             {Tracing, AttachTraceFEntry, unix.BPF_F_SLEEPABLE},
+		"fmod_ret.s/":           {Tracing, AttachModifyReturn, unix.BPF_F_SLEEPABLE},
+		"fexit.s/":              {Tracing, AttachTraceFExit, unix.BPF_F_SLEEPABLE},
+		"sk_lookup/":            {SkLookup, AttachSkLookup, 0},
+		"lsm/":                  {LSM, AttachLSMMac, 0},
+		"lsm.s/":                {LSM, AttachLSMMac, unix.BPF_F_SLEEPABLE},
+
+		"cgroup_skb/ingress": {CGroupSKB, AttachCGroupInetIngress, 0},
+		"cgroup_skb/egress":  {CGroupSKB, AttachCGroupInetEgress, 0},
+		"cgroup/dev":         {CGroupDevice, AttachCGroupDevice, 0},
+		"cgroup/skb":         {CGroupSKB, AttachNone, 0},
+		"cgroup/sock":        {CGroupSock, AttachCGroupInetSockCreate, 0},
+		"cgroup/post_bind4":  {CGroupSock, AttachCGroupInet4PostBind, 0},
+		"cgroup/post_bind6":  {CGroupSock, AttachCGroupInet6PostBind, 0},
+		"cgroup/bind4":       {CGroupSockAddr, AttachCGroupInet4Bind, 0},
+		"cgroup/bind6":       {CGroupSockAddr, AttachCGroupInet6Bind, 0},
+		"cgroup/connect4":    {CGroupSockAddr, AttachCGroupInet4Connect, 0},
+		"cgroup/connect6":    {CGroupSockAddr, AttachCGroupInet6Connect, 0},
+		"cgroup/sendmsg4":    {CGroupSockAddr, AttachCGroupUDP4Sendmsg, 0},
+		"cgroup/sendmsg6":    {CGroupSockAddr, AttachCGroupUDP6Sendmsg, 0},
+		"cgroup/recvmsg4":    {CGroupSockAddr, AttachCGroupUDP4Recvmsg, 0},
+		"cgroup/recvmsg6":    {CGroupSockAddr, AttachCGroupUDP6Recvmsg, 0},
+		"cgroup/sysctl":      {CGroupSysctl, AttachCGroupSysctl, 0},
+		"cgroup/getsockopt":  {CGroupSockopt, AttachCGroupGetsockopt, 0},
+		"cgroup/setsockopt":  {CGroupSockopt, AttachCGroupSetsockopt, 0},
+		"classifier":         {SchedCLS, AttachNone, 0},
+		"action":             {SchedACT, AttachNone, 0},
 	}
 
 	for prefix, t := range types {
@@ -889,13 +895,13 @@ func getProgType(sectionName string) (ProgramType, AttachType, string) {
 		}
 
 		if !strings.HasSuffix(prefix, "/") {
-			return t.progType, t.attachType, ""
+			return t.progType, t.attachType, t.progFlags, ""
 		}
 
-		return t.progType, t.attachType, sectionName[len(prefix):]
+		return t.progType, t.attachType, t.progFlags, sectionName[len(prefix):]
 	}
 
-	return UnspecifiedProgram, AttachNone, ""
+	return UnspecifiedProgram, AttachNone, 0, ""
 }
 
 func (ec *elfCode) loadRelocations(sec *elf.Section, symbols []elf.Symbol) (map[uint64]elf.Symbol, error) {
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..f7ceef6
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,6 @@
+# eBPF Examples
+
+- [kprobe](kprobe/) - Attach a program to the entry or exit of an arbitrary kernel symbol (function).
+- [uprobe](uprobe/) - Like a kprobe, but for symbols in userspace binaries (e.g. `bash`).
+- [tracepoint](tracepoint/) - Attach a program to predetermined kernel tracepoints.
+- Add your use case(s) here!
diff --git a/pinning.go b/internal/pinning.go
similarity index 53%
rename from pinning.go
rename to internal/pinning.go
index 7881236..5329b43 100644
--- a/pinning.go
+++ b/internal/pinning.go
@@ -1,36 +1,38 @@
-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
-	// Object is now moved to the new pinning path.
-	if err = os.Rename(currentPath, newPath); err == nil {
+	// Renameat2 is used instead of os.Rename to disallow the new path replacing
+	// an existing path.
+	if err = unix.Renameat2(unix.AT_FDCWD, currentPath, unix.AT_FDCWD, newPath, unix.RENAME_NOREPLACE); err == nil {
+		// Object is now moved to the new pinning path.
 		return nil
 	}
 	if !os.IsNotExist(err) {
 		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 f0a9672..b101c77 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -26,8 +26,11 @@ 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_F_SLEEPABLE          = linux.BPF_F_SLEEPABLE
 	BPF_OBJ_NAME_LEN         = linux.BPF_OBJ_NAME_LEN
 	BPF_TAG_SIZE             = linux.BPF_TAG_SIZE
 	SYS_BPF                  = linux.SYS_BPF
@@ -53,6 +56,8 @@ const (
 	BPF_STATS_RUN_TIME       = linux.BPF_STATS_RUN_TIME
 	PERF_RECORD_LOST         = linux.PERF_RECORD_LOST
 	PERF_RECORD_SAMPLE       = linux.PERF_RECORD_SAMPLE
+	AT_FDCWD                 = linux.AT_FDCWD
+	RENAME_NOREPLACE         = linux.RENAME_NOREPLACE
 )
 
 // Statfs_t is a wrapper
@@ -173,6 +178,11 @@ func BytePtrFromString(s string) (*byte, error) {
 	return linux.BytePtrFromString(s)
 }
 
+// Renameat2 is a wrapper
+func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error {
+	return linux.Renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
+}
+
 func KernelRelease() (string, error) {
 	var uname Utsname
 	err := Uname(&uname)
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 9ee460e..08c8f2c 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -26,8 +26,11 @@ 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_F_SLEEPABLE          = 0
 	BPF_OBJ_NAME_LEN         = 0x10
 	BPF_TAG_SIZE             = 0x8
 	SYS_BPF                  = 321
@@ -54,6 +57,8 @@ const (
 	BPF_STATS_RUN_TIME       = 0
 	PERF_RECORD_LOST         = 2
 	PERF_RECORD_SAMPLE       = 9
+	AT_FDCWD                 = -0x2
+	RENAME_NOREPLACE         = 0x1
 )
 
 // Statfs_t is a wrapper
@@ -239,6 +244,11 @@ func BytePtrFromString(s string) (*byte, error) {
 	return nil, errNonLinux
 }
 
+// Renameat2 is a wrapper
+func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error {
+	return errNonLinux
+}
+
 func KernelRelease() (string, error) {
 	return "", errNonLinux
 }
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..de973c7 100644
--- a/link/perf_event.go
+++ b/link/perf_event.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"runtime"
 	"strconv"
 	"strings"
 	"unsafe"
@@ -78,6 +79,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
@@ -157,6 +162,8 @@ func (pe *perfEvent) attach(prog *ebpf.Program) error {
 		return fmt.Errorf("enable perf event: %s", err)
 	}
 
+	// Close the perf event when its reference is lost to avoid leaking system resources.
+	runtime.SetFinalizer(pe, (*perfEvent).Close)
 	return nil
 }
 
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/link/tracepoint.go b/link/tracepoint.go
index fe6af7f..588466b 100644
--- a/link/tracepoint.go
+++ b/link/tracepoint.go
@@ -48,6 +48,7 @@ func Tracepoint(group, name string, prog *ebpf.Program) (Link, error) {
 	}
 
 	if err := pe.attach(prog); err != nil {
+		pe.Close()
 		return nil, err
 	}
 
diff --git a/map.go b/map.go
index efd7da4..355993c 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
@@ -40,6 +41,8 @@ type MapSpec struct {
 	KeySize    uint32
 	ValueSize  uint32
 	MaxEntries uint32
+	// Flags is passed to the kernel and specifies additional map
+	// creation attributes.
 	Flags      uint32
 
 	// Automatically pin and load a map from MapOptions.PinPath.
@@ -174,23 +177,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 +236,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)
 		}
 	}
@@ -805,12 +816,13 @@ func (m *Map) Clone() (*Map, error) {
 // Pin persists the map on the BPF virtual file system past the lifetime of
 // the process that created it .
 //
-// Calling Pin on a previously pinned map will override the path.
+// Calling Pin on a previously pinned map will overwrite the path, except when
+// the new path already exists. Re-pinning across filesystems is not supported.
 // You can Clone a map to pin it to a different path.
 //
 // 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
@@ -823,7 +835,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 = ""
@@ -972,9 +984,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 4309390..36a3ea2 100644
--- a/prog.go
+++ b/prog.go
@@ -55,7 +55,9 @@ type ProgramSpec struct {
 	// depends on Type and AttachType.
 	AttachTo     string
 	Instructions asm.Instructions
-
+	// Flags is passed to the kernel and specifies additional program
+	// load attributes.
+	Flags uint32
 	// License of the program. Some helpers are only available if
 	// the license is deemed compatible with the GPL.
 	//
@@ -158,6 +160,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, btfs btfHandl
 	insCount := uint32(len(bytecode) / asm.InstructionSize)
 	attr := &bpfProgLoadAttr{
 		progType:           spec.Type,
+		progFlags:          spec.Flags,
 		expectedAttachType: spec.AttachType,
 		insCount:           insCount,
 		instructions:       internal.NewSlicePointer(bytecode),
@@ -345,9 +348,12 @@ func (p *Program) Clone() (*Program, error) {
 // Pin persists the Program on the BPF virtual file system past the lifetime of
 // the process that created it
 //
+// Calling Pin on a previously pinned program will overwrite the path, except when
+// the new path already exists. Re-pinning across filesystems is not supported.
+//
 // 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
@@ -360,7 +366,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 = ""
@@ -594,8 +600,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..1711826 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,
@@ -315,43 +314,57 @@ func TestLibBPFCompat(t *testing.T) {
 }
 
 func TestGetProgType(t *testing.T) {
-	testcases := []struct {
-		section string
-		pt      ProgramType
-		at      AttachType
-		to      string
-	}{
-		{"socket/garbage", SocketFilter, AttachNone, ""},
-		{"kprobe/func", Kprobe, AttachNone, "func"},
-		{"xdp/foo", XDP, AttachNone, ""},
-		{"cgroup_skb/ingress", CGroupSKB, AttachCGroupInetIngress, ""},
-		{"iter/bpf_map", Tracing, AttachTraceIter, "bpf_map"},
+	type progTypeTestData struct {
+		Pt ProgramType
+		At AttachType
+		Fl uint32
+		To string
 	}
 
-	for _, tc := range testcases {
-		pt, at, to := getProgType(tc.section)
-		if pt != tc.pt {
-			t.Errorf("section %s: expected type %s, got %s", tc.section, tc.pt, pt)
-		}
-
-		if at != tc.at {
-			t.Errorf("section %s: expected attach type %s, got %s", tc.section, tc.at, at)
-		}
-
-		if to != tc.to {
-			t.Errorf("section %s: expected attachment to be %q, got %q", tc.section, tc.to, to)
-		}
+	testcases := map[string]progTypeTestData{
+		"socket/garbage": {
+			Pt: SocketFilter,
+			At: AttachNone,
+			To: "",
+		},
+		"kprobe/func": {
+			Pt: Kprobe,
+			At: AttachNone,
+			To: "func",
+		},
+		"xdp/foo": {
+			Pt: XDP,
+			At: AttachNone,
+			To: "",
+		},
+		"cgroup_skb/ingress": {
+			Pt: CGroupSKB,
+			At: AttachCGroupInetIngress,
+			To: "",
+		},
+		"iter/bpf_map": {
+			Pt: Tracing,
+			At: AttachTraceIter,
+			To: "bpf_map",
+		},
+		"lsm.s/file_ioctl_sleepable": {
+			Pt: LSM,
+			At: AttachLSMMac,
+			To: "file_ioctl_sleepable",
+			Fl: unix.BPF_F_SLEEPABLE,
+		},
+		"lsm/file_ioctl": {
+			Pt: LSM,
+			At: AttachLSMMac,
+			To: "file_ioctl",
+		},
 	}
-}
 
-func tempBPFFS(tb testing.TB) string {
-	tb.Helper()
+	for section, want := range testcases {
+		pt, at, fl, to := getProgType(section)
 
-	tmp, err := ioutil.TempDir("/sys/fs/bpf", "ebpf-test")
-	if err != nil {
-		tb.Fatal(err)
+		if diff := cmp.Diff(want, progTypeTestData{Pt: pt, At: at, Fl: fl, To: to}); diff != "" {
+			t.Errorf("getProgType mismatch (-want +got):\n%s", diff)
+		}
 	}
-	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 d6db088..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()
 }
@@ -433,8 +433,32 @@ func TestMapPinWithEmptyPath(t *testing.T) {
 	c.Assert(err, qt.Not(qt.IsNil))
 }
 
+func TestMapPinFailReplace(t *testing.T) {
+	tmp := testutils.TempBPFFS(t)
+	c := qt.New(t)
+	spec := spec1.Copy()
+	spec2 := spec1.Copy()
+	spec2.Name = spec1.Name + "bar"
+
+	m, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
+	if err != nil {
+		t.Fatal("Failed to create map:", err)
+	}
+	defer m.Close()
+	m2, err := NewMapWithOptions(spec2, MapOptions{PinPath: tmp})
+	if err != nil {
+		t.Fatal("Failed to create map2:", err)
+	}
+	defer m2.Close()
+	c.Assert(m.IsPinned(), qt.Equals, true)
+	newPath := filepath.Join(tmp, spec2.Name)
+
+	c.Assert(m.Pin(newPath), qt.Not(qt.IsNil), qt.Commentf("Pin didn't"+
+		" fail new path from replacing an existing path"))
+}
+
 func TestMapUnpin(t *testing.T) {
-	tmp := tempBPFFS(t)
+	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
 	spec := spec1.Copy()
 
@@ -447,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()
 
@@ -460,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()
@@ -472,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()
@@ -480,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()
@@ -492,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()
@@ -501,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()
 
@@ -1169,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..dffa7c9 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,
@@ -551,21 +576,51 @@ func TestProgramSpecTag(t *testing.T) {
 }
 
 func TestProgramTypeLSM(t *testing.T) {
-	prog, err := NewProgram(&ProgramSpec{
-		AttachTo:   "task_getpgid",
-		AttachType: AttachLSMMac,
-		Instructions: asm.Instructions{
-			asm.LoadImm(asm.R0, 0, asm.DWord),
-			asm.Return(),
+	lsmTests := []struct {
+		attachFn    string
+		flags       uint32
+		expectedErr bool
+	}{
+		{
+			attachFn: "task_getpgid",
+		},
+		{
+			attachFn:    "task_setnice",
+			flags:       unix.BPF_F_SLEEPABLE,
+			expectedErr: true,
+		},
+		{
+			attachFn: "file_open",
+			flags:    unix.BPF_F_SLEEPABLE,
 		},
-		License: "GPL",
-		Type:    LSM,
-	})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
 	}
-	prog.Close()
+	for _, tt := range lsmTests {
+		t.Run(tt.attachFn, func(t *testing.T) {
+			prog, err := NewProgram(&ProgramSpec{
+				AttachTo:   tt.attachFn,
+				AttachType: AttachLSMMac,
+				Instructions: asm.Instructions{
+					asm.LoadImm(asm.R0, 0, asm.DWord),
+					asm.Return(),
+				},
+				License: "GPL",
+				Type:    LSM,
+				Flags:   tt.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)
+			}
+			prog.Close()
+		})
+	}
 }
 
 func createProgramArray(t *testing.T) *Map {
@@ -632,7 +687,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)
 	}