Repository: cilium/ebpf
Pull request #538 (2021-12-27 21:22:27)

program: retrieve xlated Instructions from Program
Allow introspecting instructions after they have been translated by and loaded into the kernel.

Just putting this up here for discussion, I remember there being some prior discussion around this a while back.

---

There's also the following cosmetic bug:
```
tail_main:
	0: LoadMapPtr dst: r2 fd: 226
```
226 is the map's BPF ID, not its fd. Not sure if this needs to be taken into account.


Patch:
diff --git a/asm/instruction.go b/asm/instruction.go
index d376b1d..22975e8 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -309,6 +309,31 @@ func (ins Instruction) Size() uint64 {
 // Instructions is an eBPF program.
 type Instructions []Instruction
 
+// Unmarshal unmarshals an Instructions from a binary instruction stream.
+// All instructions in insns are replaced by instructions decoded from r.
+func (insns *Instructions) Unmarshal(r io.Reader, bo binary.ByteOrder) error {
+	if len(*insns) > 0 {
+		*insns = nil
+	}
+
+	var offset uint64
+	for {
+		var ins Instruction
+		n, err := ins.Unmarshal(r, bo)
+		if errors.Is(err, io.EOF) {
+			break
+		}
+		if err != nil {
+			return fmt.Errorf("offset %d: %w", offset, err)
+		}
+
+		*insns = append(*insns, ins)
+		offset += n
+	}
+
+	return nil
+}
+
 // Name returns the name of the function insns belongs to, if any.
 func (insns Instructions) Name() string {
 	if len(insns) == 0 {
diff --git a/info.go b/info.go
index 59305f6..cf692c7 100644
--- a/info.go
+++ b/info.go
@@ -2,6 +2,7 @@ package ebpf
 
 import (
 	"bufio"
+	"bytes"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -12,6 +13,7 @@ import (
 	"time"
 	"unsafe"
 
+	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/btf"
 	"github.com/cilium/ebpf/internal/sys"
@@ -91,12 +93,12 @@ type ProgramInfo struct {
 	Tag string
 	// Name as supplied by user space at load time. Available from 4.15.
 	Name string
-	// BTF for the program.
-	btf btf.ID
-	// IDS map ids related to program.
-	ids []MapID
 
+	btf   btf.ID
 	stats *programStats
+
+	maps  []MapID
+	insns []byte
 }
 
 func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
@@ -109,33 +111,40 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		return nil, err
 	}
 
-	var mapIDs []MapID
-	if info.NrMapIds > 0 {
-		mapIDs = make([]MapID, info.NrMapIds)
-		info = sys.ProgInfo{
-			// We have to zero other fields here, otherwise we may get
-			// EFAULT.
-			NrMapIds: uint32(len(mapIDs)),
-			MapIds:   sys.NewPointer(unsafe.Pointer(&mapIDs[0])),
-		}
-		err = sys.ObjInfo(fd, &info)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return &ProgramInfo{
+	pi := ProgramInfo{
 		Type: ProgramType(info.Type),
 		id:   ProgramID(info.Id),
 		Tag:  hex.EncodeToString(info.Tag[:]),
 		Name: unix.ByteSliceToString(info.Name[:]),
 		btf:  btf.ID(info.BtfId),
-		ids:  mapIDs,
 		stats: &programStats{
 			runtime:  time.Duration(info.RunTimeNs),
 			runCount: info.RunCnt,
 		},
-	}, nil
+	}
+
+	// Start with a clean struct for the second call, otherwise we may get EFAULT.
+	var info2 sys.ProgInfo
+
+	if info.NrMapIds > 0 {
+		pi.maps = make([]MapID, info.NrMapIds)
+		info2.NrMapIds = info.NrMapIds
+		info2.MapIds = sys.NewPointer(unsafe.Pointer(&pi.maps[0]))
+	}
+
+	if info.XlatedProgLen > 0 {
+		pi.insns = make([]byte, info.XlatedProgLen)
+		info2.XlatedProgLen = info.XlatedProgLen
+		info2.XlatedProgInsns = sys.NewSlicePointer(pi.insns)
+	}
+
+	if info.NrMapIds > 0 || info.XlatedProgLen > 0 {
+		if err := sys.ObjInfo(fd, &info2); err != nil {
+			return nil, err
+		}
+	}
+
+	return &pi, nil
 }
 
 func newProgramInfoFromProc(fd *sys.FD) (*ProgramInfo, error) {
@@ -199,13 +208,43 @@ func (pi *ProgramInfo) Runtime() (time.Duration, bool) {
 	return time.Duration(0), false
 }
 
+// Instructions returns the 'xlated' instruction stream of the program
+// after it has been verified and rewritten by the kernel. These instructions
+// cannot be loaded back into the kernel as-is, this is mainly used for
+// inspecting loaded programs for troubleshooting, dumping, etc.
+//
+// For example, map accesses are made to reference their kernel map IDs,
+// not the FDs they had when the program was inserted.
+//
+// The first instruction is marked as a symbol using the Program's name.
+//
+// Available from 4.13. Requires CAP_BPF or equivalent.
+func (pi *ProgramInfo) Instructions() (asm.Instructions, error) {
+	// If the calling process is not BPF-capable or if the kernel doesn't
+	// support getting xlated instructions, the field will be zero.
+	if len(pi.insns) == 0 {
+		return nil, fmt.Errorf("insufficient permissions or unsupported kernel: %w", ErrNotSupported)
+	}
+
+	r := bytes.NewReader(pi.insns)
+	var insns asm.Instructions
+	if err := insns.Unmarshal(r, internal.NativeEndian); err != nil {
+		return nil, fmt.Errorf("unmarshaling instructions: %w", err)
+	}
+
+	// Tag the first instruction with the name of the program, if available.
+	insns[0] = insns[0].Sym(pi.Name)
+
+	return insns, nil
+}
+
 // MapIDs returns the maps related to the program.
 //
 // Available from 4.15.
 //
 // The bool return value indicates whether this optional field is available.
 func (pi *ProgramInfo) MapIDs() ([]MapID, bool) {
-	return pi.ids, pi.ids != nil
+	return pi.maps, pi.maps != nil
 }
 
 func scanFdInfo(fd *sys.FD, fields map[string]interface{}) error {
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 58272f3..587e421 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -153,7 +153,11 @@ import (
 	}{
 		{
 			"ProgInfo", "bpf_prog_info",
-			[]patch{replace(objName, "name"), replace(pointer, "map_ids")},
+			[]patch{
+				replace(objName, "name"),
+				replace(pointer, "xlated_prog_insns"),
+				replace(pointer, "map_ids"),
+			},
 		},
 		{
 			"MapInfo", "bpf_map_info",
diff --git a/internal/sys/types.go b/internal/sys/types.go
index 9646a5d..ab40cef 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -456,7 +456,7 @@ type ProgInfo struct {
 	JitedProgLen         uint32
 	XlatedProgLen        uint32
 	JitedProgInsns       uint64
-	XlatedProgInsns      uint64
+	XlatedProgInsns      Pointer
 	LoadTime             uint64
 	CreatedByUid         uint32
 	NrMapIds             uint32


Test patch:
diff --git a/asm/instruction_test.go b/asm/instruction_test.go
index 74b4d65..b18c0f3 100644
--- a/asm/instruction_test.go
+++ b/asm/instruction_test.go
@@ -73,6 +73,26 @@ func BenchmarkWrite64BitImmediate(b *testing.B) {
 	}
 }
 
+func TestUnmarshalInstructions(t *testing.T) {
+	r := bytes.NewReader(test64bitImmProg)
+
+	var insns Instructions
+	if err := insns.Unmarshal(r, binary.LittleEndian); err != nil {
+		t.Fatal(err)
+	}
+
+	// Unmarshaling into the same Instructions multiple times replaces
+	// the instruction stream.
+	r.Reset(test64bitImmProg)
+	if err := insns.Unmarshal(r, binary.LittleEndian); err != nil {
+		t.Fatal(err)
+	}
+
+	if len(insns) != 1 {
+		t.Fatalf("Expected one instruction, got %d", len(insns))
+	}
+}
+
 func TestSignedJump(t *testing.T) {
 	insns := Instructions{
 		JSGT.Imm(R0, -1, "foo"),
diff --git a/prog_test.go b/prog_test.go
index fc599a5..c6a14ca 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -686,6 +686,41 @@ func TestProgramBindMap(t *testing.T) {
 	}
 }
 
+func TestProgramInstructions(t *testing.T) {
+	name := "test_prog"
+	spec := &ProgramSpec{
+		Type: SocketFilter,
+		Name: name,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, -1, asm.DWord).Sym(name),
+			asm.Mov.Imm32(asm.R0, 0),
+			asm.Return(),
+		},
+		License: "MIT",
+	}
+
+	prog, err := NewProgram(spec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	pi, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	insns, err := pi.Instructions()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if diff := cmp.Diff(insns, spec.Instructions); diff != "" {
+		t.Fatal(diff)
+	}
+}
+
 type testReaderAt struct {
 	file *os.File
 	read bool