Repository: cilium/ebpf
Pull request #60 (2020-02-10 16:31:20)

Add *GetNextID
Signed-off-by: Lehner Florian <dev@der-flo.net>

This PR implements the *GetNextID functions for ebpf maps and programs.

lmb: Sorry for the long wait!
florianl: thanks for the feedback! I will address it and update the PR.
florianl: > How about adding two new types, and make the functions take and return them?

At the moment, I don't see the advantage, of introducing extra types likes `MapID` and `ProgramID`.

> What happens when there is no next ID? I think we should return a wrapped ErrNotExist or something.

In the case, where there is no next map or program id, the syscall returns `unix.ENOENT`, so `ErrNotExist` sounds good to me.

> Should Map and Program get ID()?

Sounds also, like a good idea. I will check, how to do it.

>  If I remember correctly there currently isn't a way to go from an ID to a program. Should that be added?

Yes, currently there is no way to get from an ID to a map or program. So in a next step, `BPF_PROG_GET_FD_BY_ID` and `BPF_MAP_GET_FD_BY_ID` need to be implemented so one can go further. Sounds this reasonable to you?
florianl: In the last commit, I've added the suggested error `ErrNotExist`.

Regarding the next steps and use of the ID:
The command `bpftool prog list` calls `show_prog` [1], which then triggers the syscall for `BPF_OBJ_GET_INFO_BY_FD`. This call path is the same for maps, programs and btf objects. From this returned attribute, one can then return the ID, if needed. This syscall is already implemented by `bpfGetObjectInfoByFD` (and is using `internal.FD` as argument) but not public available.

[1] https://github.com/torvalds/linux/blob/master/tools/bpf/bpftool/prog.c#L410-L428
[2] https://github.com/torvalds/linux/blob/master/tools/lib/bpf/bpf.c#L719-L734

Patch:
diff --git a/map.go b/map.go
index 279d8af..2b6badc 100644
--- a/map.go
+++ b/map.go
@@ -17,6 +17,9 @@ var (
 	ErrIterationAborted = xerrors.New("iteration aborted")
 )
 
+// MapID represents the unique ID of an eBPF map
+type MapID uint32
+
 // MapSpec defines a Map.
 type MapSpec struct {
 	// Name is passed to the kernel as a debug aid. Must only contain
@@ -765,3 +768,11 @@ func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool {
 func (mi *MapIterator) Err() error {
 	return mi.err
 }
+
+// MapGetNextID returns the ID of the next eBPF map.
+//
+// Returns ErrNotExist, if there is no next eBPF map.
+func MapGetNextID(startID MapID) (MapID, error) {
+	id, err := objGetNextID(_MapGetNextID, uint32(startID))
+	return MapID(id), err
+}
diff --git a/prog.go b/prog.go
index 4e0c134..9be13d9 100644
--- a/prog.go
+++ b/prog.go
@@ -19,6 +19,9 @@ import (
 // ErrNotSupported is returned whenever the kernel doesn't support a feature.
 var ErrNotSupported = internal.ErrNotSupported
 
+// ProgramID represents the unique ID of an eBPF program
+type ProgramID uint32
+
 const (
 	// Number of bytes to pad the output buffer for BPF_PROG_TEST_RUN.
 	// This is currently the maximum of spare space allocated for SKB
@@ -517,3 +520,11 @@ func SanitizeName(name string, replacement rune) string {
 		return char
 	}, name)
 }
+
+// ProgramGetNextID returns the ID of the next eBPF program.
+//
+// // Returns ErrNotExist, if there is no next eBPF program.
+func ProgramGetNextID(startID ProgramID) (ProgramID, error) {
+	id, err := objGetNextID(_ProgGetNextID, uint32(startID))
+	return ProgramID(id), err
+}
diff --git a/syscalls.go b/syscalls.go
index 12ac863..fb9c858 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -11,6 +11,11 @@ import (
 	"golang.org/x/xerrors"
 )
 
+// Generic errors returned by BPF syscalls.
+var (
+	ErrNotExist = xerrors.New("requested object does not exit")
+)
+
 // bpfObjName is a null-terminated string made up of
 // 'A-Za-z0-9_' characters.
 type bpfObjName [unix.BPF_OBJ_NAME_LEN]byte
@@ -150,6 +155,12 @@ type bpfMapFreezeAttr struct {
 	mapFd uint32
 }
 
+type bpfObjGetNextIDAttr struct {
+	startID   uint32
+	nextID    uint32
+	openFlags uint32
+}
+
 func bpfProgLoad(attr *bpfProgLoadAttr) (*internal.FD, error) {
 	for {
 		fd, err := internal.BPF(_ProgLoad, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
@@ -301,6 +312,25 @@ func bpfMapGetNextKey(m *internal.FD, key, nextKeyOut internal.Pointer) error {
 	return wrapMapError(err)
 }
 
+func objGetNextID(cmd int, start uint32) (uint32, error) {
+	attr := bpfObjGetNextIDAttr{
+		startID: start,
+	}
+	_, err := internal.BPF(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	return attr.nextID, wrapObjError(err)
+}
+
+func wrapObjError(err error) error {
+	if err == nil {
+		return nil
+	}
+	if xerrors.Is(err, unix.ENOENT) {
+		return xerrors.Errorf("%w", ErrNotExist)
+	}
+
+	return xerrors.New(err.Error())
+}
+
 func wrapMapError(err error) error {
 	if err == nil {
 		return nil
diff --git a/types.go b/types.go
index 6a0228d..673809e 100644
--- a/types.go
+++ b/types.go
@@ -115,6 +115,7 @@ const (
 	_TaskFDQuery
 	_MapLookupAndDeleteElem
 	_MapFreeze
+	_BTFGetNextID
 )
 
 const (


Test patch:
diff --git a/map_test.go b/map_test.go
index 1e402ba..edaa800 100644
--- a/map_test.go
+++ b/map_test.go
@@ -690,6 +690,37 @@ func TestMapFreeze(t *testing.T) {
 	}
 }
 
+func TestMapGetNextID(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "4.13", "bpf_map_get_next_id")
+	var next MapID
+	var err error
+
+	hash := createHash()
+	defer hash.Close()
+
+	if next, err = MapGetNextID(MapID(0)); err != nil {
+		t.Fatal("Can't get next ID:", err)
+	}
+	if next == MapID(0) {
+		t.Fatal("Expected next ID other than 0")
+	}
+
+	// As there can be multiple eBPF maps, we loop over all of them and
+	// make sure, the IDs increase and the last call will return ErrNotExist
+	for {
+		last := next
+		if next, err = MapGetNextID(last); err != nil {
+			if !xerrors.Is(err, ErrNotExist) {
+				t.Fatal("Expected ErrNotExist, got:", err)
+			}
+			break
+		}
+		if next <= last {
+			t.Fatalf("Expected next ID (%d) to be higher than the last ID (%d)", next, last)
+		}
+	}
+}
+
 type benchValue struct {
 	ID      uint32
 	Val16   uint16
diff --git a/prog_test.go b/prog_test.go
index 26ba438..4921a01 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
+	"golang.org/x/xerrors"
 )
 
 func TestProgramRun(t *testing.T) {
@@ -319,6 +320,46 @@ func TestHaveProgTestRun(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveProgTestRun)
 }
 
+func TestProgramGetNextID(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "4.13", "bpf_prog_get_next_id")
+	var next ProgramID
+
+	prog, err := NewProgram(&ProgramSpec{
+		Type: SkSKB,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	if next, err = ProgramGetNextID(ProgramID(0)); err != nil {
+		t.Fatal("Can't get next ID:", err)
+	}
+	if next == ProgramID(0) {
+		t.Fatal("Expected next ID other than 0")
+	}
+
+	// As there can be multiple eBPF programs, we loop over all of them and
+	// make sure, the IDs increase and the last call will return ErrNotExist
+	for {
+		last := next
+		if next, err = ProgramGetNextID(last); err != nil {
+			if !xerrors.Is(err, ErrNotExist) {
+				t.Fatal("Expected ErrNotExist, got:", err)
+			}
+			break
+		}
+		if next <= last {
+			t.Fatalf("Expected next ID (%d) to be higher than the last ID (%d)", next, last)
+		}
+	}
+}
+
 func createProgramArray(t *testing.T) *Map {
 	t.Helper()