Repository: cilium/ebpf
Pull request #1221 (2023-11-15 15:46:13)

docs: split CONTRIBUTING.md into separate pages
Split out and update CONTRIBUTING.md so it is part of the ebpf-go.dev documentation.


Patch:
diff --git a/.github/workflows/apidiff.yml b/.github/workflows/apidiff.yml
new file mode 100644
index 0000000..c4bca90
--- /dev/null
+++ b/.github/workflows/apidiff.yml
@@ -0,0 +1,36 @@
+name: apidiff
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+
+jobs:
+  go-apidiff:
+    name: go-apidiff
+    runs-on: ubuntu-latest
+    if: github.event_name == 'pull_request'
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - uses: actions/setup-go@v4
+        with:
+          go-version-file: go.mod
+
+      - name: Run go-apidiff
+        id: apidiff
+        uses: joelanford/go-apidiff@main
+
+      - name: Create apidiff.json
+        if: always()
+        run: |
+          echo '{"id": ${{ github.event.pull_request.number }}, "semver-type": "${{ steps.apidiff.outputs.semver-type }}"}' > apidiff.json
+
+      - name: Upload apidiff.json
+        if: always()
+        uses: actions/upload-artifact@v3
+        with:
+          name: apidiff
+          path: apidiff.json
diff --git a/.github/workflows/trusted.yml b/.github/workflows/trusted.yml
new file mode 100644
index 0000000..fa28a27
--- /dev/null
+++ b/.github/workflows/trusted.yml
@@ -0,0 +1,68 @@
+on:
+  workflow_run:
+    workflows: ["apidiff"]
+    types:
+      - completed
+
+permissions:
+  pull-requests: write
+
+jobs:
+  tag-breaking-change:
+    name: Tag breaking changes
+    runs-on: ubuntu-latest
+    if: github.event.workflow_run.event == 'pull_request'
+    steps:
+      - name: 'Download artifact'
+        uses: actions/github-script@v3
+        with:
+          script: |
+            var artifacts = await github.actions.listWorkflowRunArtifacts({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               run_id: ${{github.event.workflow_run.id }},
+            });
+            var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
+              return artifact.name == "apidiff"
+            })[0];
+            var download = await github.actions.downloadArtifact({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               artifact_id: matchArtifact.id,
+               archive_format: 'zip',
+            });
+            var fs = require('fs');
+            fs.writeFileSync('${{github.workspace}}/apidiff.zip', Buffer.from(download.data));
+      - run: unzip apidiff.zip
+      - name: 'Add or remove label'
+        uses: actions/github-script@v3
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            var fs = require('fs');
+            var jsonData = JSON.parse(fs.readFileSync('apidiff.json', 'utf8'));
+
+            var issueNumber = jsonData.id;
+            var semverType = jsonData["semver-type"];
+
+            if (semverType === 'major') {
+              // Add 'breaking-change' label
+              await github.issues.addLabels({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: issueNumber,
+                labels: ['breaking-change']
+              });
+            } else {
+              // Remove 'breaking-change' label if it exists
+              try {
+                await github.issues.removeLabel({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  issue_number: issueNumber,
+                  name: 'breaking-change'
+                });
+              } catch (error) {
+                console.log('Label breaking-change not found or already removed');
+              }
+            }
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..adf991a
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,1 @@
+* @cilium/ebpf-lib-maintainers
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index bf57da9..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# How to contribute
-
-Development is on [GitHub](https://github.com/cilium/ebpf) and contributions in
-the form of pull requests and issues reporting bugs or suggesting new features
-are welcome. Please take a look at [the architecture](ARCHITECTURE.md) to get
-a better understanding for the high-level goals.
-
-## Adding a new feature
-
-1. [Join](https://ebpf.io/slack) the
-[#ebpf-go](https://cilium.slack.com/messages/ebpf-go) channel to discuss your requirements and how the feature can be implemented. The most important part is figuring out how much new exported API is necessary. **The less new API is required the easier it will be to land the feature.**
-2. (*optional*) Create a draft PR if you want to discuss the implementation or have hit a problem. It's fine if this doesn't compile or contains debug statements.
-3. Create a PR that is ready to merge. This must pass CI and have tests.
-
-### API stability
-
-The library doesn't guarantee the stability of its API at the moment.
-
-1. If possible avoid breakage by introducing new API and deprecating the old one
-   at the same time. If an API was deprecated in v0.x it can be removed in v0.x+1.
-2. Breaking API in a way that causes compilation failures is acceptable but must
-   have good reasons.
-3. Changing the semantics of the API without causing compilation failures is
-   heavily discouraged.
-
-## Running the tests
-
-Many of the tests require privileges to set resource limits and load eBPF code.
-The easiest way to obtain these is to run the tests with `sudo`.
-
-To test the current package with your local kernel you can simply run:
-```
-go test -exec sudo  ./...
-```
-
-To test the current package with a different kernel version you can use the [run-tests.sh](run-tests.sh) script.
-It requires [virtme](https://github.com/amluto/virtme) and qemu to be installed.
-
-Examples:
-
-```bash
-# Run all tests on a 5.4 kernel
-./run-tests.sh 5.4
-
-# Run a subset of tests:
-./run-tests.sh 5.4 ./link
-```
-
diff --git a/README.md b/README.md
index 81235a6..b2bcda0 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ ecosystem.
 A small collection of Go and eBPF programs that serve as examples for building
 your own tools can be found under [examples/](examples/).
 
-[Contributions](CONTRIBUTING.md) are highly encouraged, as they highlight certain use cases of
+[Contributions](https://ebpf-go.dev/contributing) are highly encouraged, as they highlight certain use cases of
 eBPF and the library, and help shape the future of the project.
 
 ## Getting Help
@@ -62,17 +62,6 @@ This library includes the following packages:
 * Linux >= 4.9. CI is run against kernel.org LTS releases. 4.4 should work but is
   not tested against.
 
-## Regenerating Testdata
-
-Run `make` in the root of this repository to rebuild testdata in all
-subpackages. This requires Docker, as it relies on a standardized build
-environment to keep the build output stable.
-
-It is possible to regenerate data using Podman by overriding the `CONTAINER_*`
-variables: `CONTAINER_ENGINE=podman CONTAINER_RUN_ARGS= make`.
-
-The toolchain image build files are kept in [testdata/docker/](testdata/docker/).
-
 ## License
 
 MIT
diff --git a/ARCHITECTURE.md b/docs/ebpf/contributing/architecture.md
similarity index 86%
rename from ARCHITECTURE.md
rename to docs/ebpf/contributing/architecture.md
index 26f555e..eb4a94b 100644
--- a/ARCHITECTURE.md
+++ b/docs/ebpf/contributing/architecture.md
@@ -1,6 +1,12 @@
 Architecture of the library
 ===
 
+The bulk of the functionality of the library split across the `ebpf`, `btf` and
+`link` packages.
+Below is a diagram how the most important types relate to each other.
+The graph is in dependecy order, so an arrow from `Links` to `Map` can be read
+as "Link depends on Map".
+
 ```mermaid
 graph RL
     Program --> ProgramSpec --> ELF
@@ -25,10 +31,10 @@ an ELF file which contains program byte code (aka BPF), but also metadata for
 maps used by the program. The metadata follows the conventions set by libbpf
 shipped with the kernel. Certain ELF sections have special meaning
 and contain structures defined by libbpf. Newer versions of clang emit
-additional metadata in [BPF Type Format](#BTF).
+additional metadata in [BPF Type Format](../btf/index.md).
 
 The library aims to be compatible with libbpf so that moving from a C toolchain
-to a Go one creates little friction. To that end, the [ELF reader](elf_reader.go)
+to a Go one creates little friction. To that end, the ELF reader
 is tested against the Linux selftests and avoids introducing custom behaviour
 if possible.
 
@@ -37,7 +43,7 @@ all of the information contained in the ELF in a form that is easy to work with
 in Go. The returned `CollectionSpec` should be deterministic: reading the same ELF
 file on different systems must produce the same output.
 As a corollary, any changes that depend on the runtime environment like the
-current kernel version must happen when creating [Objects](#Objects).
+current kernel version must happen when creating [Objects](#objects).
 
 Specifications
 ---
@@ -50,7 +56,7 @@ objects and contain everything necessary to execute the relevant `bpf(2)`
 syscalls. They refer to `btf.Spec` for type information such as `Map` key and
 value types.
 
-The [asm](asm/) package provides an assembler that can be used to generate
+The {{ godoc("asm") }} package provides an assembler that can be used to generate
 `ProgramSpec` on the fly.
 
 Objects
diff --git a/docs/ebpf/contributing/index.md b/docs/ebpf/contributing/index.md
new file mode 100644
index 0000000..516e720
--- /dev/null
+++ b/docs/ebpf/contributing/index.md
@@ -0,0 +1,94 @@
+# How to contribute
+
+Development happens on [GitHub](https://github.com/cilium/ebpf) and contributions in
+all forms are welcome. Please take a look at [the architecture](architecture.md) to get
+a better understanding of the high-level goals.
+
+## Running the tests
+
+Many of the tests require privileges to set resource limits and load eBPF code.
+The easiest way to obtain these is to run the tests with `sudo`.
+
+To test the current package with your local kernel you can simply run:
+```
+go test -exec sudo ./...
+```
+
+To test the current package with a different kernel version you can use the [run-tests.sh] script.
+It requires [virtme] and qemu to be installed.
+Unfortunately virtme is not well maintained at the moment, so we recommend installing
+a known working version:
+
+```shell-session
+pip3 install https://github.com/amluto/virtme/archive/beb85146cd91de37ae455eccb6ab67c393e6e290.zip
+```
+
+Once you have the dependencies you can run all tests on a different kernel:
+
+```shell-session
+./run-tests.sh 5.4
+```
+
+Or run a subset of tests:
+
+```shell-session
+./run-tests.sh 5.4 -run TCX ./link
+```
+
+## Regenerating testdata and source code
+
+The library includes some binary artifacts which are used for tests and some
+generated source code. Run `make` in the root of the repository to start
+this process.
+
+```shell-session
+make
+```
+
+This requires Docker, as it relies on a standardized build
+environment to keep the build output stable.
+It is possible to regenerate data using Podman by overriding the `CONTAINER_*`
+variables:
+
+```shell-session
+make CONTAINER_ENGINE=podman CONTAINER_RUN_ARGS=
+```
+
+### Updating kernel dependencies
+
+Syscall bindings and some parameters required to parse ELF sections are derived
+from upstream kernel versions. You can update them to the latest version by:
+
+1. Adjusting the `KERNEL_VERSION` variable in `Makefile`
+2. Running
+    ```shell-session
+    make update-kernel-deps
+    ```
+3. [Regenerating testdata and source code](#regenerating-testdata-and-source-code)
+
+## Project permissions
+
+If you'd like to contribute to the library more regularly, one of the
+[maintainers][ebpf-lib-maintainers] can add you to the appropriate team or mark
+you as a code owner. Please create an issue in the repository.
+
+* [ebpf-go-contributors]
+    * Have ["Triage"][permissions] role
+    * May be asked to review certain parts of code
+    * May be asked to help with certain issues
+* [ebpf-go-reviewers]
+    * Have ["Write"][permissions] role
+    * Can re-run failed CI tasks
+    * Can't merge to protected branches
+    * Can't create releases
+    * May be asked to become CODEOWNERS
+* [ebpf-lib-maintainers]
+    * Have ["Admin"][permissions] role
+    * CODEOWNERS of last resort
+
+[virtme]: https://github.com/amluto/virtme
+[run-tests.sh]: https://github.com/cilium/ebpf/blob/main/run-tests.sh
+[permissions]: https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/repository-roles-for-an-organization#permissions-for-each-role
+[ebpf-go-contributors]: https://github.com/orgs/cilium/teams/ebpf-go-contributors/members
+[ebpf-go-reviewers]: https://github.com/orgs/cilium/teams/ebpf-go-reviewers/members
+[ebpf-lib-maintainers]: https://github.com/orgs/cilium/teams/ebpf-lib-maintainers/members
\ No newline at end of file
diff --git a/docs/ebpf/contributing/new-feature.md b/docs/ebpf/contributing/new-feature.md
new file mode 100644
index 0000000..3cc2c8b
--- /dev/null
+++ b/docs/ebpf/contributing/new-feature.md
@@ -0,0 +1,30 @@
+# Adding a new feature
+
+We're very much looking for contributions which flesh out the functionality of
+the library.
+
+1. Have a look at the [architecture](architecture.md) of the library if you
+   haven't already.
+2. [Join](https://ebpf.io/slack) the
+   [#ebpf-go-dev](https://cilium.slack.com/messages/ebpf-go-dev) channel to
+   discuss your requirements and how the feature can be implemented.
+   Alternatively open a new Discussion if you prefer to not use Slack.
+   The most important part is figuring out how much new exported API is necessary.
+   **The less new API is required the easier it will be to land the feature.**
+   Also see [API stability](#api-stability).
+3. (*optional*) Create a draft PR if you want to discuss the implementation or have hit a problem. It's fine if this doesn't compile or contains debug statements.
+4. Create a PR that is ready to merge. This must pass CI and have tests.
+
+## API stability
+
+There is an emphasis on compatibility even though the library doesn't guarantee
+the stability of its API at the moment.
+
+1. If possible, avoid breakage by introducing new API and deprecating the old one
+   at the same time. If an API was deprecated in v0.x it can be removed in v0.x+1.
+   This is especially important if there is no straighforward way to convert
+   from the old to the new API.
+2. Breaking API in a way that causes compilation failures is acceptable but must
+   have good reasons.
+3. Changing the semantics of the API without causing compilation failures is
+   heavily discouraged.
diff --git a/docs/includes/glossary.md b/docs/includes/glossary.md
index 7f09e5e..da81dbc 100644
--- a/docs/includes/glossary.md
+++ b/docs/includes/glossary.md
@@ -13,3 +13,4 @@
 *[XDP]: eXpress Data Path, a high-performance eBPF-powered data path. Only has a receive hook.
 *[bpf2go]: Convenience utility to compile eBPF C using clang and generate a Go skeleton.
 *[libbpf]: A library for writing kernel- and user space BPF programs in C, developed by the upstream Linux project.
+*[qemu]: A popular virtual machine manager.
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 48d116a..1bb57a6 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -1,5 +1,5 @@
 site_name: "ebpf-go Documentation"
-site_description: Pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel. 
+site_description: Pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.
 site_author: Cilium Community
 
 # Rendered in header.
@@ -74,6 +74,10 @@ nav:
     - 'Index': bpf2go/index.md
   - 'BTF':
     - btf/index.md
+  - 'Contributing':
+    - contributing/index.md
+    - contributing/architecture.md
+    - contributing/new-feature.md
   - 'Users': users.md
   - 'Go Reference': https://pkg.go.dev/github.com/cilium/ebpf
   - 'GitHub':
@@ -145,7 +149,7 @@ markdown_extensions:
           format: !!python/name:pymdownx.superfences.fence_code_format
   # Content tabs for code snippets, checklists, etc.
   - pymdownx.tabbed:
-      alternate_style: true 
+      alternate_style: true
   # Emoji and icons like :fontawesome-brands-golang:
   - pymdownx.emoji:
       emoji_index: !!python/name:material.extensions.emoji.twemoji
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 07ff966..5e4897c 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -15,6 +15,8 @@ import (
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/sys"
+
+	"golang.org/x/exp/slices"
 )
 
 type syscallRetval int
@@ -120,6 +122,7 @@ import (
 		{"HdrStartOff", "bpf_hdr_start_off"},
 		{"RetCode", "bpf_ret_code"},
 		{"XdpAction", "xdp_action"},
+		{"TcxActionBase", "tcx_action_base"},
 	}
 
 	sort.Slice(enums, func(i, j int) bool {
@@ -319,11 +322,21 @@ import (
 		},
 		{
 			"ProgAttach", retError, "prog_attach", "BPF_PROG_ATTACH",
-			nil,
+			[]patch{
+				flattenAnon,
+				rename("target_fd", "target_fd_or_ifindex"),
+				rename("relative_fd", "relative_fd_or_id"),
+			},
 		},
 		{
 			"ProgDetach", retError, "prog_attach", "BPF_PROG_DETACH",
-			[]patch{truncateAfter("attach_type")},
+			[]patch{
+				flattenAnon,
+				rename("target_fd", "target_fd_or_ifindex"),
+				truncateAfter("expected_revision"),
+				rename("relative_fd", "relative_fd_or_id"),
+				remove("replace_bpf_fd"),
+			},
 		},
 		{
 			"ProgRun", retError, "prog_run", "BPF_PROG_TEST_RUN",
@@ -426,6 +439,17 @@ import (
 				replace(btfID, "target_btf_id"),
 			},
 		},
+		{
+			"LinkCreateTcx", retFd, "link_create", "BPF_LINK_CREATE",
+			[]patch{
+				choose(1, "target_ifindex"),
+				choose(4, "tcx"),
+				replace(enumTypes["AttachType"], "attach_type"),
+				flattenAnon,
+				flattenAnon, // flatten tcx member
+				rename("relative_fd", "relative_fd_or_id"),
+			},
+		},
 		{
 			"LinkUpdate", retError, "link_update", "BPF_LINK_UPDATE",
 			nil,
@@ -442,9 +466,11 @@ import (
 			"ProgQuery", retError, "prog_query", "BPF_PROG_QUERY",
 			[]patch{
 				replace(enumTypes["AttachType"], "attach_type"),
-				replace(pointer, "prog_ids", "prog_attach_flags", "link_ids", "link_attach_flags"),
-				choose(5, "prog_cnt"),
-				rename("prog_cnt", "prog_count"),
+				replace(pointer, "prog_ids", "prog_attach_flags"),
+				replace(pointer, "link_ids", "link_attach_flags"),
+				flattenAnon,
+				rename("prog_cnt", "count"),
+				rename("target_fd", "target_fd_or_ifindex"),
 			},
 		},
 	}
@@ -519,6 +545,9 @@ import (
 			replace(typeID, "target_btf_id")},
 		},
 		{"XDPLinkInfo", "xdp", nil},
+		{"TcxLinkInfo", "tcx", []patch{
+			replace(enumTypes["AttachType"], "attach_type"),
+		}},
 	}
 
 	sort.Slice(linkInfoExtraTypes, func(i, j int) bool {
@@ -543,6 +572,11 @@ import (
 		{"iter", "iter"},
 		{"netns", "netns"},
 		{"xdp", "xdp"},
+		{"struct_ops", "struct_ops"},
+		{"netfilter", "netfilter"},
+		{"kprobe_multi", "kprobe_multi"},
+		{"perf_event", "perf_event"},
+		{"tcx", "tcx"},
 	})
 	if err != nil {
 		return nil, fmt.Errorf("splitting linkInfo: %w", err)
@@ -550,6 +584,9 @@ import (
 
 	for _, s := range linkInfoExtraTypes {
 		t := linkInfoTypes[s.cType]
+		if t == nil {
+			return nil, fmt.Errorf("link info: no C type named %q", s.cType)
+		}
 		if err := outputPatchedStruct(gf, w, s.goType, t, s.patches); err != nil {
 			return nil, fmt.Errorf("output %q: %w", s.goType, err)
 		}
@@ -716,21 +753,27 @@ func flattenAnon(s *btf.Struct) error {
 	for i := range s.Members {
 		m := &s.Members[i]
 
-		cs, ok := m.Type.(*btf.Struct)
-		if !ok || cs.TypeName() != "" {
+		if m.Type.TypeName() != "" {
 			continue
 		}
 
-		for j := range cs.Members {
-			cs.Members[j].Offset += m.Offset
-		}
+		var newMembers []btf.Member
+		switch cs := m.Type.(type) {
+		case *btf.Struct:
+			for j := range cs.Members {
+				cs.Members[j].Offset += m.Offset
+			}
+			newMembers = cs.Members
 
-		newMembers := make([]btf.Member, 0, len(s.Members)+len(cs.Members)-1)
-		newMembers = append(newMembers, s.Members[:i]...)
-		newMembers = append(newMembers, cs.Members...)
-		newMembers = append(newMembers, s.Members[i+1:]...)
+		case *btf.Union:
+			cs.Members[0].Offset += m.Offset
+			newMembers = []btf.Member{cs.Members[0]}
 
-		s.Members = newMembers
+		default:
+			continue
+		}
+
+		s.Members = slices.Replace(s.Members, i, i+1, newMembers...)
 	}
 
 	return nil
@@ -799,3 +842,15 @@ func replaceWithBytes(members ...string) patch {
 		return nil
 	}, members...)
 }
+
+func remove(member string) patch {
+	return func(s *btf.Struct) error {
+		for i, m := range s.Members {
+			if m.Name == member {
+				s.Members = slices.Delete(s.Members, i, i+1)
+				return nil
+			}
+		}
+		return fmt.Errorf("member %q not found", member)
+	}
+}
diff --git a/internal/sys/mapflags_string.go b/internal/sys/mapflags_string.go
index c80744a..d9fe217 100644
--- a/internal/sys/mapflags_string.go
+++ b/internal/sys/mapflags_string.go
@@ -21,24 +21,28 @@ func _() {
 	_ = x[BPF_F_MMAPABLE-1024]
 	_ = x[BPF_F_PRESERVE_ELEMS-2048]
 	_ = x[BPF_F_INNER_MAP-4096]
+	_ = x[BPF_F_LINK-8192]
+	_ = x[BPF_F_PATH_FD-16384]
 }
 
-const _MapFlags_name = "BPF_F_NO_PREALLOCBPF_F_NO_COMMON_LRUBPF_F_NUMA_NODEBPF_F_RDONLYBPF_F_WRONLYBPF_F_STACK_BUILD_IDBPF_F_ZERO_SEEDBPF_F_RDONLY_PROGBPF_F_WRONLY_PROGBPF_F_CLONEBPF_F_MMAPABLEBPF_F_PRESERVE_ELEMSBPF_F_INNER_MAP"
+const _MapFlags_name = "BPF_F_NO_PREALLOCBPF_F_NO_COMMON_LRUBPF_F_NUMA_NODEBPF_F_RDONLYBPF_F_WRONLYBPF_F_STACK_BUILD_IDBPF_F_ZERO_SEEDBPF_F_RDONLY_PROGBPF_F_WRONLY_PROGBPF_F_CLONEBPF_F_MMAPABLEBPF_F_PRESERVE_ELEMSBPF_F_INNER_MAPBPF_F_LINKBPF_F_PATH_FD"
 
 var _MapFlags_map = map[MapFlags]string{
-	1:    _MapFlags_name[0:17],
-	2:    _MapFlags_name[17:36],
-	4:    _MapFlags_name[36:51],
-	8:    _MapFlags_name[51:63],
-	16:   _MapFlags_name[63:75],
-	32:   _MapFlags_name[75:95],
-	64:   _MapFlags_name[95:110],
-	128:  _MapFlags_name[110:127],
-	256:  _MapFlags_name[127:144],
-	512:  _MapFlags_name[144:155],
-	1024: _MapFlags_name[155:169],
-	2048: _MapFlags_name[169:189],
-	4096: _MapFlags_name[189:204],
+	1:     _MapFlags_name[0:17],
+	2:     _MapFlags_name[17:36],
+	4:     _MapFlags_name[36:51],
+	8:     _MapFlags_name[51:63],
+	16:    _MapFlags_name[63:75],
+	32:    _MapFlags_name[75:95],
+	64:    _MapFlags_name[95:110],
+	128:   _MapFlags_name[110:127],
+	256:   _MapFlags_name[127:144],
+	512:   _MapFlags_name[144:155],
+	1024:  _MapFlags_name[155:169],
+	2048:  _MapFlags_name[169:189],
+	4096:  _MapFlags_name[189:204],
+	8192:  _MapFlags_name[204:214],
+	16384: _MapFlags_name[214:227],
 }
 
 func (i MapFlags) String() string {
diff --git a/internal/sys/syscall.go b/internal/sys/syscall.go
index 088e82e..b1d49b8 100644
--- a/internal/sys/syscall.go
+++ b/internal/sys/syscall.go
@@ -139,6 +139,17 @@ const (
 	BPF_F_MMAPABLE
 	BPF_F_PRESERVE_ELEMS
 	BPF_F_INNER_MAP
+	BPF_F_LINK
+	BPF_F_PATH_FD
+)
+
+// Flags used by bpf_mprog.
+const (
+	BPF_F_REPLACE = 1 << (iota + 2)
+	BPF_F_BEFORE
+	BPF_F_AFTER
+	BPF_F_ID
+	BPF_F_LINK_MPROG = 1 << 13 // aka BPF_F_LINK
 )
 
 // wrappedErrno wraps syscall.Errno to prevent direct comparisons with
diff --git a/internal/sys/types.go b/internal/sys/types.go
index 51698e0..0feffae 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -462,6 +462,15 @@ const (
 	BPF_STATS_RUN_TIME StatsType = 0
 )
 
+type TcxActionBase int32
+
+const (
+	TCX_NEXT     TcxActionBase = -1
+	TCX_PASS     TcxActionBase = 0
+	TCX_DROP     TcxActionBase = 2
+	TCX_REDIRECT TcxActionBase = 7
+)
+
 type XdpAction uint32
 
 const (
@@ -719,6 +728,25 @@ func LinkCreatePerfEvent(attr *LinkCreatePerfEventAttr) (*FD, error) {
 	return NewFD(int(fd))
 }
 
+type LinkCreateTcxAttr struct {
+	ProgFd           uint32
+	TargetIfindex    uint32
+	AttachType       AttachType
+	Flags            uint32
+	RelativeFdOrId   uint32
+	_                [4]byte
+	ExpectedRevision uint64
+	_                [32]byte
+}
+
+func LinkCreateTcx(attr *LinkCreateTcxAttr) (*FD, error) {
+	fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	if err != nil {
+		return nil, err
+	}
+	return NewFD(int(fd))
+}
+
 type LinkCreateTracingAttr struct {
 	ProgFd      uint32
 	TargetFd    uint32
@@ -971,13 +999,13 @@ func ObjPin(attr *ObjPinAttr) error {
 }
 
 type ProgAttachAttr struct {
-	TargetFd         uint32
-	AttachBpfFd      uint32
-	AttachType       uint32
-	AttachFlags      uint32
-	ReplaceBpfFd     uint32
-	RelativeFd       uint32
-	ExpectedRevision uint64
+	TargetFdOrIfindex uint32
+	AttachBpfFd       uint32
+	AttachType        uint32
+	AttachFlags       uint32
+	ReplaceBpfFd      uint32
+	RelativeFdOrId    uint32
+	ExpectedRevision  uint64
 }
 
 func ProgAttach(attr *ProgAttachAttr) error {
@@ -997,9 +1025,13 @@ func ProgBindMap(attr *ProgBindMapAttr) error {
 }
 
 type ProgDetachAttr struct {
-	TargetFd    uint32
-	AttachBpfFd uint32
-	AttachType  uint32
+	TargetFdOrIfindex uint32
+	AttachBpfFd       uint32
+	AttachType        uint32
+	AttachFlags       uint32
+	_                 [4]byte
+	RelativeFdOrId    uint32
+	ExpectedRevision  uint64
 }
 
 func ProgDetach(attr *ProgDetachAttr) error {
@@ -1065,17 +1097,17 @@ func ProgLoad(attr *ProgLoadAttr) (*FD, error) {
 }
 
 type ProgQueryAttr struct {
-	TargetFd        uint32
-	AttachType      AttachType
-	QueryFlags      uint32
-	AttachFlags     uint32
-	ProgIds         Pointer
-	ProgCount       uint32
-	_               [4]byte
-	ProgAttachFlags Pointer
-	LinkIds         Pointer
-	LinkAttachFlags Pointer
-	Revision        uint64
+	TargetFdOrIfindex uint32
+	AttachType        AttachType
+	QueryFlags        uint32
+	AttachFlags       uint32
+	ProgIds           Pointer
+	Count             uint32
+	_                 [4]byte
+	ProgAttachFlags   Pointer
+	LinkIds           Pointer
+	LinkAttachFlags   Pointer
+	Revision          uint64
 }
 
 func ProgQuery(attr *ProgQueryAttr) error {
@@ -1143,6 +1175,11 @@ type RawTracepointLinkInfo struct {
 	_         [4]byte
 }
 
+type TcxLinkInfo struct {
+	Ifindex    uint32
+	AttachType AttachType
+}
+
 type TracingLinkInfo struct {
 	AttachType  AttachType
 	TargetObjId uint32
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 51ed7d0..bc63724 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -25,6 +25,7 @@ const (
 	EACCES     = linux.EACCES
 	EILSEQ     = linux.EILSEQ
 	EOPNOTSUPP = linux.EOPNOTSUPP
+	ESTALE     = linux.ESTALE
 )
 
 const (
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 1760e9e..3a0f79c 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -27,6 +27,7 @@ const (
 	EACCES
 	EILSEQ
 	EOPNOTSUPP
+	ESTALE
 )
 
 // Constants are distinct to avoid breaking switch statements.
diff --git a/link/anchor.go b/link/anchor.go
new file mode 100644
index 0000000..983296d
--- /dev/null
+++ b/link/anchor.go
@@ -0,0 +1,135 @@
+package link
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/sys"
+)
+
+const anchorFlags = sys.BPF_F_REPLACE |
+	sys.BPF_F_BEFORE |
+	sys.BPF_F_AFTER |
+	sys.BPF_F_ID |
+	sys.BPF_F_LINK_MPROG
+
+// Anchor is a reference to a link or program.
+//
+// It is used to describe where an attachment or detachment should take place
+// for link types which support multiple attachment.
+type Anchor interface {
+	// anchor returns an fd or ID and a set of flags.
+	//
+	// By default fdOrID is taken to reference a program, but BPF_F_LINK_MPROG
+	// changes this to refer to a link instead.
+	//
+	// BPF_F_BEFORE, BPF_F_AFTER, BPF_F_REPLACE modify where a link or program
+	// is attached. The default behaviour if none of these flags is specified
+	// matches BPF_F_AFTER.
+	anchor() (fdOrID, flags uint32, _ error)
+}
+
+type firstAnchor struct{}
+
+func (firstAnchor) anchor() (fdOrID, flags uint32, _ error) {
+	return 0, sys.BPF_F_BEFORE, nil
+}
+
+func First() Anchor {
+	return firstAnchor{}
+}
+
+type lastAnchor struct{}
+
+func (lastAnchor) anchor() (fdOrID, flags uint32, _ error) {
+	return 0, sys.BPF_F_AFTER, nil
+}
+
+func Last() Anchor {
+	return lastAnchor{}
+}
+
+// Before is the position just in front of target.
+func BeforeLink(target Link) Anchor {
+	return anchor{target, sys.BPF_F_BEFORE}
+}
+
+// After is the position just after target.
+func AfterLink(target Link) Anchor {
+	return anchor{target, sys.BPF_F_AFTER}
+}
+
+// Before is the position just in front of target.
+func BeforeLinkByID(target ID) Anchor {
+	return anchor{target, sys.BPF_F_BEFORE}
+}
+
+// After is the position just after target.
+func AfterLinkByID(target ID) Anchor {
+	return anchor{target, sys.BPF_F_AFTER}
+}
+
+// Before is the position just in front of target.
+func BeforeProgram(target *ebpf.Program) Anchor {
+	return anchor{target, sys.BPF_F_BEFORE}
+}
+
+// After is the position just after target.
+func AfterProgram(target *ebpf.Program) Anchor {
+	return anchor{target, sys.BPF_F_AFTER}
+}
+
+// Replace the target itself.
+func ReplaceProgram(target *ebpf.Program) Anchor {
+	return anchor{target, sys.BPF_F_REPLACE}
+}
+
+// Before is the position just in front of target.
+func BeforeProgramByID(target ebpf.ProgramID) Anchor {
+	return anchor{target, sys.BPF_F_BEFORE}
+}
+
+// After is the position just after target.
+func AfterProgramByID(target ebpf.ProgramID) Anchor {
+	return anchor{target, sys.BPF_F_AFTER}
+}
+
+// Replace the target itself.
+func ReplaceProgramByID(target ebpf.ProgramID) Anchor {
+	return anchor{target, sys.BPF_F_REPLACE}
+}
+
+type anchor struct {
+	target   any
+	position uint32
+}
+
+func (ap anchor) anchor() (fdOrID, flags uint32, _ error) {
+	var typeFlag uint32
+	switch target := ap.target.(type) {
+	case *ebpf.Program:
+		fd := target.FD()
+		if fd < 0 {
+			return 0, 0, sys.ErrClosedFd
+		}
+		fdOrID = uint32(fd)
+		typeFlag = 0
+	case ebpf.ProgramID:
+		fdOrID = uint32(target)
+		typeFlag = sys.BPF_F_ID
+	case interface{ FD() int }:
+		fd := target.FD()
+		if fd < 0 {
+			return 0, 0, sys.ErrClosedFd
+		}
+		fdOrID = uint32(fd)
+		typeFlag = sys.BPF_F_LINK_MPROG
+	case ID:
+		fdOrID = uint32(target)
+		typeFlag = sys.BPF_F_LINK_MPROG | sys.BPF_F_ID
+	default:
+		return 0, 0, fmt.Errorf("invalid target %T", ap.target)
+	}
+
+	return fdOrID, ap.position | typeFlag, nil
+}
diff --git a/link/cgroup.go b/link/cgroup.go
index 58e85fe..79f3d2b 100644
--- a/link/cgroup.go
+++ b/link/cgroup.go
@@ -143,8 +143,7 @@ func (cg *progAttachCgroup) Update(prog *ebpf.Program) error {
 		// Atomically replacing multiple programs requires at least
 		// 5.5 (commit 7dd68b3279f17921 "bpf: Support replacing cgroup-bpf
 		// program in MULTI mode")
-		args.Flags |= uint32(flagReplace)
-		args.Replace = cg.current
+		args.Anchor = ReplaceProgram(cg.current)
 	}
 
 	if err := RawAttachProgram(args); err != nil {
diff --git a/link/link.go b/link/link.go
index 36acd6e..590ea3a 100644
--- a/link/link.go
+++ b/link/link.go
@@ -98,6 +98,8 @@ func wrapRawLink(raw *RawLink) (_ Link, err error) {
 		return &kprobeMultiLink{*raw}, nil
 	case PerfEventType:
 		return nil, fmt.Errorf("recovering perf event fd: %w", ErrNotSupported)
+	case TCXType:
+		return &tcxLink{*raw}, nil
 	default:
 		return raw, nil
 	}
@@ -132,6 +134,7 @@ type TracingInfo sys.TracingLinkInfo
 type CgroupInfo sys.CgroupLinkInfo
 type NetNsInfo sys.NetNsLinkInfo
 type XDPInfo sys.XDPLinkInfo
+type TCXInfo sys.TcxLinkInfo
 
 // Tracing returns tracing type-specific link info.
 //
@@ -315,6 +318,8 @@ func (l *RawLink) Info() (*Info, error) {
 	case RawTracepointType, IterType,
 		PerfEventType, KprobeMultiType:
 		// Extra metadata not supported.
+	case TCXType:
+		extra = &TCXInfo{}
 	default:
 		return nil, fmt.Errorf("unknown link info type: %d", info.Type)
 	}
diff --git a/link/program.go b/link/program.go
index 053735a..d8a2a15 100644
--- a/link/program.go
+++ b/link/program.go
@@ -2,22 +2,27 @@ package link
 
 import (
 	"fmt"
+	"runtime"
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/internal/sys"
 )
 
 type RawAttachProgramOptions struct {
-	// File descriptor to attach to. This differs for each attach type.
+	// Target to query. This is usually a file descriptor but may refer to
+	// something else based on the attach type.
 	Target int
 	// Program to attach.
 	Program *ebpf.Program
-	// Program to replace (cgroups).
-	Replace *ebpf.Program
-	// Attach must match the attach type of Program (and Replace).
+	// Attach must match the attach type of Program.
 	Attach ebpf.AttachType
-	// Flags control the attach behaviour. This differs for each attach type.
+	// Attach relative to an anchor. Optional.
+	Anchor Anchor
+	// Flags control the attach behaviour. Specify an Anchor instead of
+	// F_LINK, F_ID, F_BEFORE, F_AFTER and F_REPLACE. Optional.
 	Flags uint32
+	// Only attach if the internal revision matches the given value.
+	ExpectedRevision uint64
 }
 
 // RawAttachProgram is a low level wrapper around BPF_PROG_ATTACH.
@@ -25,45 +30,72 @@ type RawAttachProgramOptions struct {
 // You should use one of the higher level abstractions available in this
 // package if possible.
 func RawAttachProgram(opts RawAttachProgramOptions) error {
-	var replaceFd uint32
-	if opts.Replace != nil {
-		replaceFd = uint32(opts.Replace.FD())
+	if opts.Flags&anchorFlags != 0 {
+		return fmt.Errorf("disallowed flags: use Anchor to specify attach target")
 	}
 
 	attr := sys.ProgAttachAttr{
-		TargetFd:     uint32(opts.Target),
-		AttachBpfFd:  uint32(opts.Program.FD()),
-		ReplaceBpfFd: replaceFd,
-		AttachType:   uint32(opts.Attach),
-		AttachFlags:  uint32(opts.Flags),
+		TargetFdOrIfindex: uint32(opts.Target),
+		AttachBpfFd:       uint32(opts.Program.FD()),
+		AttachType:        uint32(opts.Attach),
+		AttachFlags:       uint32(opts.Flags),
+		ExpectedRevision:  opts.ExpectedRevision,
+	}
+
+	if opts.Anchor != nil {
+		fdOrID, flags, err := opts.Anchor.anchor()
+		if err != nil {
+			return fmt.Errorf("attach program: %w", err)
+		}
+
+		if flags == sys.BPF_F_REPLACE {
+			// Ensure that replacing a program works on old kernels.
+			attr.ReplaceBpfFd = fdOrID
+		} else {
+			attr.RelativeFdOrId = fdOrID
+			attr.AttachFlags |= flags
+		}
 	}
 
 	if err := sys.ProgAttach(&attr); err != nil {
 		if haveFeatErr := haveProgAttach(); haveFeatErr != nil {
 			return haveFeatErr
 		}
-		return fmt.Errorf("can't attach program: %w", err)
+		return fmt.Errorf("attach program: %w", err)
 	}
+	runtime.KeepAlive(opts.Program)
 
 	return nil
 }
 
-type RawDetachProgramOptions struct {
-	Target  int
-	Program *ebpf.Program
-	Attach  ebpf.AttachType
-}
+type RawDetachProgramOptions RawAttachProgramOptions
 
 // RawDetachProgram is a low level wrapper around BPF_PROG_DETACH.
 //
 // You should use one of the higher level abstractions available in this
 // package if possible.
 func RawDetachProgram(opts RawDetachProgramOptions) error {
+	if opts.Flags&anchorFlags != 0 {
+		return fmt.Errorf("disallowed flags: use Anchor to specify attach target")
+	}
+
 	attr := sys.ProgDetachAttr{
-		TargetFd:    uint32(opts.Target),
-		AttachBpfFd: uint32(opts.Program.FD()),
-		AttachType:  uint32(opts.Attach),
+		TargetFdOrIfindex: uint32(opts.Target),
+		AttachBpfFd:       uint32(opts.Program.FD()),
+		AttachType:        uint32(opts.Attach),
+		ExpectedRevision:  opts.ExpectedRevision,
 	}
+
+	if opts.Anchor != nil {
+		fdOrID, flags, err := opts.Anchor.anchor()
+		if err != nil {
+			return fmt.Errorf("detach program: %w", err)
+		}
+
+		attr.RelativeFdOrId = fdOrID
+		attr.AttachFlags |= flags
+	}
+
 	if err := sys.ProgDetach(&attr); err != nil {
 		if haveFeatErr := haveProgAttach(); haveFeatErr != nil {
 			return haveFeatErr
diff --git a/link/query.go b/link/query.go
index c056565..fe534f8 100644
--- a/link/query.go
+++ b/link/query.go
@@ -2,7 +2,6 @@ package link
 
 import (
 	"fmt"
-	"os"
 	"unsafe"
 
 	"github.com/cilium/ebpf"
@@ -11,53 +10,102 @@ import (
 
 // QueryOptions defines additional parameters when querying for programs.
 type QueryOptions struct {
-	// Path can be a path to a cgroup, netns or LIRC2 device
-	Path string
+	// Target to query. This is usually a file descriptor but may refer to
+	// something else based on the attach type.
+	Target int
 	// Attach specifies the AttachType of the programs queried for
 	Attach ebpf.AttachType
 	// QueryFlags are flags for BPF_PROG_QUERY, e.g. BPF_F_QUERY_EFFECTIVE
 	QueryFlags uint32
 }
 
-// QueryPrograms retrieves ProgramIDs associated with the AttachType.
-//
-// Returns (nil, nil) if there are no programs attached to the queried kernel
-// resource. Calling QueryPrograms on a kernel missing PROG_QUERY will result in
-// ErrNotSupported.
-func QueryPrograms(opts QueryOptions) ([]ebpf.ProgramID, error) {
-	if haveProgQuery() != nil {
-		return nil, fmt.Errorf("can't query program IDs: %w", ErrNotSupported)
-	}
+// QueryResult describes which programs and links are active.
+type QueryResult struct {
+	// List of attached programs.
+	Programs []AttachedProgram
 
-	f, err := os.Open(opts.Path)
-	if err != nil {
-		return nil, fmt.Errorf("can't open file: %s", err)
-	}
-	defer f.Close()
+	// Incremented by one every time the set of attached programs changes.
+	// May be zero if not supported by the [ebpf.AttachType].
+	Revision uint64
+}
+
+// HaveLinkInfo returns true if the kernel supports querying link information
+// for a particular [ebpf.AttachType].
+func (qr *QueryResult) HaveLinkInfo() bool {
+	return qr.Revision > 0
+}
+
+type AttachedProgram struct {
+	ID     ebpf.ProgramID
+	linkID ID
+}
+
+// LinkID returns the ID associated with the program.
+//
+// Returns 0, false if the kernel doesn't support retrieving the ID or if the
+// program wasn't attached via a link. See [QueryResult.HaveLinkInfo] if you
+// need to tell the two apart.
+func (ap *AttachedProgram) LinkID() (ID, bool) {
+	return ap.linkID, ap.linkID != 0
+}
 
+// QueryPrograms retrieves a list of programs for the given AttachType.
+//
+// Returns a slice of attached programs, which may be empty.
+// revision counts how many times the set of attached programs has changed and
+// may be zero if not supported by the [ebpf.AttachType].
+// Returns ErrNotSupportd on a kernel without BPF_PROG_QUERY
+func QueryPrograms(opts QueryOptions) (*QueryResult, error) {
 	// query the number of programs to allocate correct slice size
 	attr := sys.ProgQueryAttr{
-		TargetFd:   uint32(f.Fd()),
-		AttachType: sys.AttachType(opts.Attach),
-		QueryFlags: opts.QueryFlags,
+		TargetFdOrIfindex: uint32(opts.Target),
+		AttachType:        sys.AttachType(opts.Attach),
+		QueryFlags:        opts.QueryFlags,
 	}
-	if err := sys.ProgQuery(&attr); err != nil {
-		return nil, fmt.Errorf("can't query program count: %w", err)
+	err := sys.ProgQuery(&attr)
+	if err != nil {
+		if haveFeatErr := haveProgQuery(); haveFeatErr != nil {
+			return nil, fmt.Errorf("query programs: %w", haveFeatErr)
+		}
+		return nil, fmt.Errorf("query programs: %w", err)
 	}
+	if attr.Count == 0 {
+		return &QueryResult{Revision: attr.Revision}, nil
+	}
+
+	// The minimum bpf_mprog revision is 1, so we can use the field to detect
+	// whether the attach type supports link ids.
+	haveLinkIDs := attr.Revision != 0
 
-	// return nil if no progs are attached
-	if attr.ProgCount == 0 {
-		return nil, nil
+	count := attr.Count
+	progIds := make([]ebpf.ProgramID, count)
+	attr = sys.ProgQueryAttr{
+		TargetFdOrIfindex: uint32(opts.Target),
+		AttachType:        sys.AttachType(opts.Attach),
+		QueryFlags:        opts.QueryFlags,
+		Count:             count,
+		ProgIds:           sys.NewPointer(unsafe.Pointer(&progIds[0])),
+	}
+
+	var linkIds []ID
+	if haveLinkIDs {
+		linkIds = make([]ID, count)
+		attr.LinkIds = sys.NewPointer(unsafe.Pointer(&linkIds[0]))
 	}
 
-	// we have at least one prog, so we query again
-	progIds := make([]ebpf.ProgramID, attr.ProgCount)
-	attr.ProgIds = sys.NewPointer(unsafe.Pointer(&progIds[0]))
-	attr.ProgCount = uint32(len(progIds))
 	if err := sys.ProgQuery(&attr); err != nil {
-		return nil, fmt.Errorf("can't query program IDs: %w", err)
+		return nil, fmt.Errorf("query programs: %w", err)
 	}
 
-	return progIds, nil
+	// NB: attr.Count might have changed between the two syscalls.
+	var programs []AttachedProgram
+	for i, id := range progIds[:attr.Count] {
+		ap := AttachedProgram{ID: id}
+		if haveLinkIDs {
+			ap.linkID = linkIds[i]
+		}
+		programs = append(programs, ap)
+	}
 
+	return &QueryResult{programs, attr.Revision}, nil
 }
diff --git a/link/syscalls.go b/link/syscalls.go
index 012970e..96d6c7b 100644
--- a/link/syscalls.go
+++ b/link/syscalls.go
@@ -24,6 +24,7 @@ const (
 	XDPType           = sys.BPF_LINK_TYPE_XDP
 	PerfEventType     = sys.BPF_LINK_TYPE_PERF_EVENT
 	KprobeMultiType   = sys.BPF_LINK_TYPE_KPROBE_MULTI
+	TCXType           = sys.BPF_LINK_TYPE_TCX
 )
 
 var haveProgAttach = internal.NewFeatureTest("BPF_PROG_ATTACH", "4.10", func() error {
@@ -72,10 +73,10 @@ var haveProgAttachReplace = internal.NewFeatureTest("BPF_PROG_ATTACH atomic repl
 	// present.
 	attr := sys.ProgAttachAttr{
 		// We rely on this being checked after attachFlags.
-		TargetFd:    ^uint32(0),
-		AttachBpfFd: uint32(prog.FD()),
-		AttachType:  uint32(ebpf.AttachCGroupInetIngress),
-		AttachFlags: uint32(flagReplace),
+		TargetFdOrIfindex: ^uint32(0),
+		AttachBpfFd:       uint32(prog.FD()),
+		AttachType:        uint32(ebpf.AttachCGroupInetIngress),
+		AttachFlags:       uint32(flagReplace),
 	}
 
 	err = sys.ProgAttach(&attr)
@@ -110,8 +111,8 @@ var haveProgQuery = internal.NewFeatureTest("BPF_PROG_QUERY", "4.15", func() err
 		// We rely on this being checked during the syscall.
 		// With an otherwise correct payload we expect EBADF here
 		// as an indication that the feature is present.
-		TargetFd:   ^uint32(0),
-		AttachType: sys.AttachType(ebpf.AttachCGroupInetIngress),
+		TargetFdOrIfindex: ^uint32(0),
+		AttachType:        sys.AttachType(ebpf.AttachCGroupInetIngress),
 	}
 
 	err := sys.ProgQuery(&attr)
diff --git a/link/tcx.go b/link/tcx.go
new file mode 100644
index 0000000..6989af8
--- /dev/null
+++ b/link/tcx.go
@@ -0,0 +1,68 @@
+package link
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/sys"
+)
+
+type TCXOptions struct {
+	// Index of the interface to attach to.
+	Interface int
+	// Program to attach.
+	Program *ebpf.Program
+	// One of the AttachTCX* constants.
+	Attach ebpf.AttachType
+	// Attach relative to an anchor. Optional.
+	Anchor Anchor
+	// Only attach if the expected revision matches.
+	ExpectedRevision uint64
+	// Flags control the attach behaviour. Specify an Anchor instead of
+	// F_LINK, F_ID, F_BEFORE, F_AFTER and R_REPLACE. Optional.
+	Flags uint32
+}
+
+func AttachTCX(opts TCXOptions) (Link, error) {
+	if opts.Interface < 0 {
+		return nil, fmt.Errorf("interface %d is out of bounds", opts.Interface)
+	}
+
+	if opts.Flags&anchorFlags != 0 {
+		return nil, fmt.Errorf("disallowed flags: use Anchor to specify attach target")
+	}
+
+	attr := sys.LinkCreateTcxAttr{
+		ProgFd:           uint32(opts.Program.FD()),
+		AttachType:       sys.AttachType(opts.Attach),
+		TargetIfindex:    uint32(opts.Interface),
+		ExpectedRevision: opts.ExpectedRevision,
+		Flags:            opts.Flags,
+	}
+
+	if opts.Anchor != nil {
+		fdOrID, flags, err := opts.Anchor.anchor()
+		if err != nil {
+			return nil, fmt.Errorf("attach tcx link: %w", err)
+		}
+
+		attr.RelativeFdOrId = fdOrID
+		attr.Flags |= flags
+	}
+
+	fd, err := sys.LinkCreateTcx(&attr)
+	runtime.KeepAlive(opts.Program)
+	runtime.KeepAlive(opts.Anchor)
+	if err != nil {
+		return nil, fmt.Errorf("attach tcx link: %w", err)
+	}
+
+	return &tcxLink{RawLink{fd, ""}}, nil
+}
+
+type tcxLink struct {
+	RawLink
+}
+
+var _ Link = (*tcxLink)(nil)
diff --git a/map.go b/map.go
index fec0adf..dc25245 100644
--- a/map.go
+++ b/map.go
@@ -642,11 +642,15 @@ func (m *Map) LookupBytes(key interface{}) ([]byte, error) {
 }
 
 func (m *Map) lookupPerCPU(key, valueOut any, flags MapLookupFlags) error {
+	slice, err := ensurePerCPUSlice(valueOut, int(m.valueSize))
+	if err != nil {
+		return err
+	}
 	valueBytes := make([]byte, m.fullValueSize)
 	if err := m.lookup(key, sys.NewSlicePointer(valueBytes), flags); err != nil {
 		return err
 	}
-	return unmarshalPerCPUValue(valueOut, int(m.valueSize), valueBytes)
+	return unmarshalPerCPUValue(slice, int(m.valueSize), valueBytes)
 }
 
 func (m *Map) lookup(key interface{}, valueOut sys.Pointer, flags MapLookupFlags) error {
@@ -669,11 +673,53 @@ func (m *Map) lookup(key interface{}, valueOut sys.Pointer, flags MapLookupFlags
 }
 
 func (m *Map) lookupAndDeletePerCPU(key, valueOut any, flags MapLookupFlags) error {
+	slice, err := ensurePerCPUSlice(valueOut, int(m.valueSize))
+	if err != nil {
+		return err
+	}
 	valueBytes := make([]byte, m.fullValueSize)
 	if err := m.lookupAndDelete(key, sys.NewSlicePointer(valueBytes), flags); err != nil {
 		return err
 	}
-	return unmarshalPerCPUValue(valueOut, int(m.valueSize), valueBytes)
+	return unmarshalPerCPUValue(slice, int(m.valueSize), valueBytes)
+}
+
+// ensurePerCPUSlice allocates a slice for a per-CPU value if necessary.
+func ensurePerCPUSlice(sliceOrPtr any, elemLength int) (any, error) {
+	sliceOrPtrType := reflect.TypeOf(sliceOrPtr)
+	if sliceOrPtrType.Kind() == reflect.Slice {
+		// The target is a slice, the caller is responsible for ensuring that
+		// size is correct.
+		return sliceOrPtr, nil
+	}
+
+	slicePtrType := sliceOrPtrType
+	if slicePtrType.Kind() != reflect.Ptr || slicePtrType.Elem().Kind() != reflect.Slice {
+		return nil, fmt.Errorf("per-cpu value requires a slice or a pointer to slice")
+	}
+
+	possibleCPUs, err := PossibleCPU()
+	if err != nil {
+		return nil, err
+	}
+
+	sliceType := slicePtrType.Elem()
+	slice := reflect.MakeSlice(sliceType, possibleCPUs, possibleCPUs)
+
+	sliceElemType := sliceType.Elem()
+	sliceElemIsPointer := sliceElemType.Kind() == reflect.Ptr
+	reflect.ValueOf(sliceOrPtr).Elem().Set(slice)
+	if !sliceElemIsPointer {
+		return slice.Interface(), nil
+	}
+	sliceElemType = sliceElemType.Elem()
+
+	for i := 0; i < possibleCPUs; i++ {
+		newElem := reflect.New(sliceElemType)
+		slice.Index(i).Set(newElem)
+	}
+
+	return slice.Interface(), nil
 }
 
 func (m *Map) lookupAndDelete(key any, valuePtr sys.Pointer, flags MapLookupFlags) error {
diff --git a/marshalers.go b/marshalers.go
index 00911ad..d77a5fb 100644
--- a/marshalers.go
+++ b/marshalers.go
@@ -84,11 +84,11 @@ func marshalPerCPUValue(slice any, elemLength int) (sys.Pointer, error) {
 // unmarshalPerCPUValue decodes a buffer into a slice containing one value per
 // possible CPU.
 //
-// slicePtr must be a pointer to a slice.
-func unmarshalPerCPUValue(slicePtr any, elemLength int, buf []byte) error {
-	slicePtrType := reflect.TypeOf(slicePtr)
-	if slicePtrType.Kind() != reflect.Ptr || slicePtrType.Elem().Kind() != reflect.Slice {
-		return fmt.Errorf("per-cpu value requires pointer to slice")
+// slice must be a literal slice and not a pointer.
+func unmarshalPerCPUValue(slice any, elemLength int, buf []byte) error {
+	sliceType := reflect.TypeOf(slice)
+	if sliceType.Kind() != reflect.Slice {
+		return fmt.Errorf("per-CPU value requires a slice")
 	}
 
 	possibleCPUs, err := PossibleCPU()
@@ -96,26 +96,26 @@ func unmarshalPerCPUValue(slicePtr any, elemLength int, buf []byte) error {
 		return err
 	}
 
-	sliceType := slicePtrType.Elem()
-	slice := reflect.MakeSlice(sliceType, possibleCPUs, possibleCPUs)
+	sliceValue := reflect.ValueOf(slice)
+	if sliceValue.Len() != possibleCPUs {
+		return fmt.Errorf("per-CPU slice has incorrect length, expected %d, got %d",
+			possibleCPUs, sliceValue.Len())
+	}
 
 	sliceElemType := sliceType.Elem()
 	sliceElemIsPointer := sliceElemType.Kind() == reflect.Ptr
-	if sliceElemIsPointer {
-		sliceElemType = sliceElemType.Elem()
-	}
-
 	stride := internal.Align(elemLength, 8)
 	for i := 0; i < possibleCPUs; i++ {
 		var elem any
+		v := sliceValue.Index(i)
 		if sliceElemIsPointer {
-			newElem := reflect.New(sliceElemType)
-			slice.Index(i).Set(newElem)
-			elem = newElem.Interface()
+			if !v.Elem().CanAddr() {
+				return fmt.Errorf("per-CPU slice elements cannot be nil")
+			}
+			elem = v.Elem().Addr().Interface()
 		} else {
-			elem = slice.Index(i).Addr().Interface()
+			elem = v.Addr().Interface()
 		}
-
 		err := sysenc.Unmarshal(elem, buf[:elemLength])
 		if err != nil {
 			return fmt.Errorf("cpu %d: %w", i, err)
@@ -124,6 +124,5 @@ func unmarshalPerCPUValue(slicePtr any, elemLength int, buf []byte) error {
 		buf = buf[stride:]
 	}
 
-	reflect.ValueOf(slicePtr).Elem().Set(slice)
 	return nil
 }


Test patch:
diff --git a/link/program_test.go b/link/program_test.go
index 0e32f33..2d56212 100644
--- a/link/program_test.go
+++ b/link/program_test.go
@@ -1,10 +1,14 @@
 package link
 
 import (
+	"fmt"
+	"net"
 	"testing"
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/internal/testutils"
+
+	qt "github.com/frankban/quicktest"
 )
 
 func TestProgramAlter(t *testing.T) {
@@ -42,3 +46,90 @@ func TestProgramAlter(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+func TestRawAttachProgramAnchor(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "6.6", "attach anchor")
+
+	iface, err := net.InterfaceByName("lo")
+	qt.Assert(t, err, qt.IsNil)
+
+	a := mustLoadProgram(t, ebpf.SchedCLS, 0, "")
+	info, err := a.Info()
+	qt.Assert(t, err, qt.IsNil)
+	aID, _ := info.ID()
+
+	err = RawAttachProgram(RawAttachProgramOptions{
+		Target:  iface.Index,
+		Program: a,
+		Attach:  ebpf.AttachTCXIngress,
+	})
+	qt.Assert(t, err, qt.IsNil)
+	defer RawDetachProgram(RawDetachProgramOptions{
+		Target:  iface.Index,
+		Program: a,
+		Attach:  ebpf.AttachTCXIngress,
+	})
+
+	link, err := AttachTCX(TCXOptions{
+		Interface: iface.Index,
+		Program:   mustLoadProgram(t, ebpf.SchedCLS, 0, ""),
+		Attach:    ebpf.AttachTCXIngress,
+	})
+	qt.Assert(t, err, qt.IsNil)
+	defer link.Close()
+
+	linkInfo, err := link.Info()
+	qt.Assert(t, err, qt.IsNil)
+
+	b := mustLoadProgram(t, ebpf.SchedCLS, 0, "")
+
+	for _, anchor := range []Anchor{
+		First(),
+		Last(),
+		AfterProgram(a),
+		AfterProgramByID(aID),
+		AfterLink(link),
+		AfterLinkByID(linkInfo.ID),
+	} {
+		t.Run(fmt.Sprintf("%T", anchor), func(t *testing.T) {
+			err := RawAttachProgram(RawAttachProgramOptions{
+				Target:  iface.Index,
+				Program: b,
+				Attach:  ebpf.AttachTCXIngress,
+				Anchor:  anchor,
+			})
+			qt.Assert(t, err, qt.IsNil)
+
+			// Detach doesn't allow first or last anchor.
+			if _, ok := anchor.(firstAnchor); ok {
+				anchor = nil
+			} else if _, ok := anchor.(lastAnchor); ok {
+				anchor = nil
+			}
+
+			err = RawDetachProgram(RawDetachProgramOptions{
+				Target:  iface.Index,
+				Program: b,
+				Attach:  ebpf.AttachTCXIngress,
+				Anchor:  anchor,
+			})
+			qt.Assert(t, err, qt.IsNil)
+		})
+	}
+
+	// Check that legacy replacement with a program works.
+	err = RawAttachProgram(RawAttachProgramOptions{
+		Target:  iface.Index,
+		Program: b,
+		Attach:  ebpf.AttachTCXIngress,
+		Anchor:  ReplaceProgram(a),
+	})
+	qt.Assert(t, err, qt.IsNil)
+
+	err = RawDetachProgram(RawDetachProgramOptions{
+		Target:  iface.Index,
+		Program: b,
+		Attach:  ebpf.AttachTCXIngress,
+	})
+	qt.Assert(t, err, qt.IsNil)
+}
diff --git a/link/query_test.go b/link/query_test.go
index c52a4de..c02a21c 100644
--- a/link/query_test.go
+++ b/link/query_test.go
@@ -1,57 +1,91 @@
 package link
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/internal/testutils"
+
+	qt "github.com/frankban/quicktest"
+	"golang.org/x/exp/slices"
 )
 
 func TestQueryPrograms(t *testing.T) {
-	for name, fn := range map[string]func(*testing.T) (*ebpf.Program, QueryOptions){
-		"cgroup": queryCgroupFixtures,
-		"netns":  queryNetNSFixtures,
+	for name, fn := range map[string]func(*testing.T) (*ebpf.Program, Link, QueryOptions){
+		"cgroup":      queryCgroupProgAttachFixtures,
+		"cgroup link": queryCgroupLinkFixtures,
+		"netns":       queryNetNSFixtures,
+		"tcx":         queryTCXFixtures,
 	} {
 		t.Run(name, func(t *testing.T) {
-			prog, opts := fn(t)
-			ids, err := QueryPrograms(opts)
+			prog, link, opts := fn(t)
+			result, err := QueryPrograms(opts)
 			testutils.SkipIfNotSupported(t, err)
-			if err != nil {
-				t.Fatal("Can't query programs:", err)
-			}
+			qt.Assert(t, err, qt.IsNil)
 
 			progInfo, err := prog.Info()
-			if err != nil {
-				t.Fatal("Can't get program info:", err)
+			qt.Assert(t, err, qt.IsNil)
+			progID, _ := progInfo.ID()
+
+			i := slices.IndexFunc(result.Programs, func(ap AttachedProgram) bool {
+				return ap.ID == progID
+			})
+			qt.Assert(t, i, qt.Not(qt.Equals), -1)
+
+			if name == "tcx" {
+				qt.Assert(t, result.Revision, qt.Not(qt.Equals), uint64(0))
 			}
 
-			progId, _ := progInfo.ID()
+			if result.HaveLinkInfo() {
+				ap := result.Programs[i]
+				linkInfo, err := link.Info()
+				qt.Assert(t, err, qt.IsNil)
 
-			for _, id := range ids {
-				if id == progId {
-					return
-				}
+				linkID, ok := ap.LinkID()
+				qt.Assert(t, ok, qt.IsTrue)
+				qt.Assert(t, linkID, qt.Equals, linkInfo.ID)
 			}
-			t.Fatalf("Can't find program ID %d in query result: %v", progId, ids)
 		})
 	}
 }
 
-func queryCgroupFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
+func queryCgroupProgAttachFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
+	cgroup, prog := mustCgroupFixtures(t)
+
+	link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, flagAllowOverride)
+	if err != nil {
+		t.Fatal("Can't create link:", err)
+	}
+	t.Cleanup(func() {
+		qt.Assert(t, link.Close(), qt.IsNil)
+	})
+
+	return prog, nil, QueryOptions{
+		Target: int(cgroup.Fd()),
+		Attach: ebpf.AttachCGroupInetEgress,
+	}
+}
+
+func queryCgroupLinkFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
 	cgroup, prog := mustCgroupFixtures(t)
 
-	link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, 0)
+	link, err := newLinkCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog)
+	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal("Can't create link:", err)
 	}
 	t.Cleanup(func() {
-		link.Close()
+		qt.Assert(t, link.Close(), qt.IsNil)
 	})
 
-	return prog, QueryOptions{Path: cgroup.Name(), Attach: ebpf.AttachCGroupInetEgress}
+	return prog, nil, QueryOptions{
+		Target: int(cgroup.Fd()),
+		Attach: ebpf.AttachCGroupInetEgress,
+	}
 }
 
-func queryNetNSFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
+func queryNetNSFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
 	testutils.SkipOnOldKernel(t, "4.20", "flow_dissector program")
 
 	prog := mustLoadProgram(t, ebpf.FlowDissector, ebpf.AttachFlowDissector, "")
@@ -77,5 +111,25 @@ func queryNetNSFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
 		}
 	})
 
-	return prog, QueryOptions{Path: "/proc/self/ns/net", Attach: ebpf.AttachFlowDissector}
+	netns, err := os.Open("/proc/self/ns/net")
+	qt.Assert(t, err, qt.IsNil)
+	t.Cleanup(func() { netns.Close() })
+
+	return prog, nil, QueryOptions{
+		Target: int(netns.Fd()),
+		Attach: ebpf.AttachFlowDissector,
+	}
+}
+
+func queryTCXFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
+	testutils.SkipOnOldKernel(t, "6.6", "TCX link")
+
+	prog := mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachTCXIngress, "")
+
+	link, iface := mustAttachTCX(t, prog, ebpf.AttachTCXIngress)
+
+	return prog, link, QueryOptions{
+		Target: iface,
+		Attach: ebpf.AttachTCXIngress,
+	}
 }
diff --git a/link/syscalls_test.go b/link/syscalls_test.go
index 361dc47..70fd983 100644
--- a/link/syscalls_test.go
+++ b/link/syscalls_test.go
@@ -17,3 +17,7 @@ func TestHaveProgAttachReplace(t *testing.T) {
 func TestHaveBPFLink(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveBPFLink)
 }
+
+func TestHaveProgQuery(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveProgQuery)
+}
diff --git a/link/tcx_test.go b/link/tcx_test.go
new file mode 100644
index 0000000..77cde74
--- /dev/null
+++ b/link/tcx_test.go
@@ -0,0 +1,90 @@
+package link
+
+import (
+	"fmt"
+	"math"
+	"net"
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/testutils"
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+func TestAttachTCX(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "6.6", "TCX link")
+
+	prog := mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachNone, "")
+	link, _ := mustAttachTCX(t, prog, ebpf.AttachTCXIngress)
+
+	testLink(t, link, prog)
+}
+
+func TestTCXAnchor(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "6.6", "TCX link")
+
+	a := mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachNone, "")
+	b := mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachNone, "")
+
+	linkA, iface := mustAttachTCX(t, a, ebpf.AttachTCXEgress)
+
+	programInfo, err := a.Info()
+	qt.Assert(t, err, qt.IsNil)
+	programID, _ := programInfo.ID()
+
+	linkInfo, err := linkA.Info()
+	qt.Assert(t, err, qt.IsNil)
+	linkID := linkInfo.ID
+
+	for _, anchor := range []Anchor{
+		First(),
+		Last(),
+		BeforeProgram(a),
+		BeforeProgramByID(programID),
+		AfterLink(linkA),
+		AfterLinkByID(linkID),
+	} {
+		t.Run(fmt.Sprintf("%T", anchor), func(t *testing.T) {
+			linkB, err := AttachTCX(TCXOptions{
+				Program:   b,
+				Attach:    ebpf.AttachTCXEgress,
+				Interface: iface,
+				Anchor:    anchor,
+			})
+			qt.Assert(t, err, qt.IsNil)
+			qt.Assert(t, linkB.Close(), qt.IsNil)
+		})
+	}
+}
+
+func TestTCXExpectedRevision(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "6.6", "TCX link")
+
+	iface, err := net.InterfaceByName("lo")
+	qt.Assert(t, err, qt.IsNil)
+
+	_, err = AttachTCX(TCXOptions{
+		Program:          mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachNone, ""),
+		Attach:           ebpf.AttachTCXEgress,
+		Interface:        iface.Index,
+		ExpectedRevision: math.MaxUint64,
+	})
+	qt.Assert(t, err, qt.ErrorIs, unix.ESTALE)
+}
+
+func mustAttachTCX(tb testing.TB, prog *ebpf.Program, attachType ebpf.AttachType) (Link, int) {
+	iface, err := net.InterfaceByName("lo")
+	qt.Assert(tb, err, qt.IsNil)
+
+	link, err := AttachTCX(TCXOptions{
+		Program:   prog,
+		Attach:    attachType,
+		Interface: iface.Index,
+	})
+	qt.Assert(tb, err, qt.IsNil)
+	tb.Cleanup(func() { qt.Assert(tb, link.Close(), qt.IsNil) })
+
+	return link, iface.Index
+}
diff --git a/map_test.go b/map_test.go
index 3d71f4b..e18a1ac 100644
--- a/map_test.go
+++ b/map_test.go
@@ -75,6 +75,10 @@ func TestMap(t *testing.T) {
 		t.Error("Want value 42, got", v)
 	}
 
+	sliceVal := make([]uint32, 1)
+	qt.Assert(t, m.Lookup(uint32(0), sliceVal), qt.IsNil)
+	qt.Assert(t, sliceVal, qt.DeepEquals, []uint32{42})
+
 	var slice []byte
 	qt.Assert(t, m.Lookup(uint32(0), &slice), qt.IsNil)
 	qt.Assert(t, slice, qt.DeepEquals, internal.NativeEndian.AppendUint32(nil, 42))
@@ -1345,6 +1349,16 @@ func TestPerCPUMarshaling(t *testing.T) {
 			}
 
 			// Make sure unmarshaling works on slices containing pointers
+			retrievedVal := make([]*customEncoding, numCPU)
+			if err := arr.Lookup(uint32(0), retrievedVal); err == nil {
+				t.Fatal("Slices with nil values should generate error")
+			}
+			for i := range retrievedVal {
+				retrievedVal[i] = &customEncoding{}
+			}
+			if err := arr.Lookup(uint32(0), retrievedVal); err != nil {
+				t.Fatal("Can't retrieve key 0:", err)
+			}
 			var retrieved []*customEncoding
 			if err := arr.Lookup(uint32(0), &retrieved); err != nil {
 				t.Fatal("Can't retrieve key 0:", err)
@@ -1407,11 +1421,11 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 	defer prog.Close()
 
 	progAttachAttrs := sys.ProgAttachAttr{
-		TargetFd:     uint32(cgroup.Fd()),
-		AttachBpfFd:  uint32(prog.FD()),
-		AttachType:   uint32(AttachCGroupInetEgress),
-		AttachFlags:  0,
-		ReplaceBpfFd: 0,
+		TargetFdOrIfindex: uint32(cgroup.Fd()),
+		AttachBpfFd:       uint32(prog.FD()),
+		AttachType:        uint32(AttachCGroupInetEgress),
+		AttachFlags:       0,
+		ReplaceBpfFd:      0,
 	}
 	err = sys.ProgAttach(&progAttachAttrs)
 	if err != nil {
@@ -1419,9 +1433,9 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 	}
 	defer func() {
 		attr := sys.ProgDetachAttr{
-			TargetFd:    uint32(cgroup.Fd()),
-			AttachBpfFd: uint32(prog.FD()),
-			AttachType:  uint32(AttachCGroupInetEgress),
+			TargetFdOrIfindex: uint32(cgroup.Fd()),
+			AttachBpfFd:       uint32(prog.FD()),
+			AttachType:        uint32(AttachCGroupInetEgress),
 		}
 		if err := sys.ProgDetach(&attr); err != nil {
 			t.Fatal(err)
diff --git a/marshalers_test.go b/marshalers_test.go
new file mode 100644
index 0000000..6695791
--- /dev/null
+++ b/marshalers_test.go
@@ -0,0 +1,37 @@
+package ebpf
+
+import (
+	"testing"
+
+	"github.com/cilium/ebpf/internal"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestUnmarshalPerCPUValue(t *testing.T) {
+	possibleCPUs := MustPossibleCPU()
+	expected := make([]uint32, possibleCPUs)
+	for i := 0; i < possibleCPUs; i++ {
+		expected[i] = uint32(1021 * (i + 1))
+	}
+	elemLength := 4
+
+	buf := make([]byte, possibleCPUs*internal.Align(elemLength, 8))
+	b := buf
+	for _, elem := range expected {
+		internal.NativeEndian.PutUint32(b, elem)
+		b = b[8:]
+	}
+	slice := make([]uint32, possibleCPUs)
+	err := unmarshalPerCPUValue(slice, elemLength, buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	qt.Assert(t, slice, qt.DeepEquals, expected)
+
+	smallSlice := make([]uint32, possibleCPUs-1)
+	qt.Assert(t, unmarshalPerCPUValue(smallSlice, elemLength, buf), qt.IsNotNil)
+
+	nilElemSlice := make([]*uint32, possibleCPUs)
+	qt.Assert(t, unmarshalPerCPUValue(nilElemSlice, elemLength, buf), qt.IsNotNil)
+}