Repository: cilium/ebpf
Pull request #194 (2021-01-14 11:35:20)

program: allow calculating the tag for a ProgramSpec
The kernel exposes a tag for each loaded program. This is just the truncated
sha1 hash of the BPF bytecode. We can use this to check whether a loaded
program corresponds to a ProgramSpec, for example to make sure that the
latest version of a program is loaded.

Add a function to asm.Instructions to calculate the tag and a helper to
ProgramSpec to call the function using the native endianness.


Patch:
diff --git a/asm/instruction.go b/asm/instruction.go
index 890dc00..593d2e0 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -1,12 +1,16 @@
 package asm
 
 import (
+	"crypto/sha1"
 	"encoding/binary"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
 	"math"
 	"strings"
+
+	"github.com/cilium/ebpf/internal/unix"
 )
 
 // InstructionSize is the size of a BPF instruction in bytes
@@ -159,6 +163,9 @@ func (ins *Instruction) mapOffset() uint32 {
 	return uint32(uint64(ins.Constant) >> 32)
 }
 
+// isLoadFromMap returns true if the instruction loads from a map.
+//
+// This covers both loading the map pointer and direct map value loads.
 func (ins *Instruction) isLoadFromMap() bool {
 	return ins.OpCode == LoadImmOp(DWord) && (ins.Src == PseudoMapFD || ins.Src == PseudoMapValue)
 }
@@ -390,6 +397,25 @@ func (insns Instructions) Marshal(w io.Writer, bo binary.ByteOrder) error {
 	return nil
 }
 
+// Tag calculates the kernel tag for a series of instructions.
+//
+// It mirrors bpf_prog_calc_tag in the kernel and so can be compared
+// to ProgramInfo.Tag to figure out whether a loaded program matches
+// certain instructions.
+func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) {
+	h := sha1.New()
+	for i, ins := range insns {
+		if ins.isLoadFromMap() {
+			ins.Constant = 0
+		}
+		_, err := ins.Marshal(h, bo)
+		if err != nil {
+			return "", fmt.Errorf("instruction %d: %w", i, err)
+		}
+	}
+	return hex.EncodeToString(h.Sum(nil)[:unix.BPF_TAG_SIZE]), nil
+}
+
 // Iterate allows iterating a BPF program while keeping track of
 // various offsets.
 //
diff --git a/prog.go b/prog.go
index a85458d..6fa25b0 100644
--- a/prog.go
+++ b/prog.go
@@ -88,6 +88,13 @@ func (ps *ProgramSpec) Copy() *ProgramSpec {
 	return &cpy
 }
 
+// Tag calculates the kernel tag for a series of instructions.
+//
+// Use asm.Instructions.Tag if you need to calculate for non-native endianness.
+func (ps *ProgramSpec) Tag() (string, error) {
+	return ps.Instructions.Tag(internal.NativeEndian)
+}
+
 // Program represents BPF program loaded into the kernel.
 //
 // It is not safe to close a Program which is used by other goroutines.


Test patch:
diff --git a/prog_test.go b/prog_test.go
index 7d893db..5b2e177 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -478,6 +478,43 @@ func TestProgramRejectIncorrectByteOrder(t *testing.T) {
 	}
 }
 
+func TestProgramSpecTag(t *testing.T) {
+	arr := createArray(t)
+	defer arr.Close()
+
+	spec := &ProgramSpec{
+		Type: SocketFilter,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, -1, asm.DWord),
+			asm.LoadMapPtr(asm.R1, arr.FD()),
+			asm.Mov.Imm32(asm.R0, 0),
+			asm.Return(),
+		},
+		License: "MIT",
+	}
+
+	prog, err := NewProgram(spec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tag, err := spec.Tag()
+	if err != nil {
+		t.Fatal("Can't calculate tag:", err)
+	}
+
+	if tag != info.Tag {
+		t.Errorf("Calculated tag %s doesn't match kernel tag %s", tag, info.Tag)
+	}
+}
+
 func TestProgramTypeLSM(t *testing.T) {
 	prog, err := NewProgram(&ProgramSpec{
 		AttachTo:   "task_getpgid",
@@ -589,3 +626,24 @@ func ExampleProgram_unmarshalFromMap() {
 		panic(err)
 	}
 }
+
+func ExampleProgramSpec_Tag() {
+	spec := &ProgramSpec{
+		Type: SocketFilter,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	}
+
+	prog, _ := NewProgram(spec)
+	info, _ := prog.Info()
+	tag, _ := spec.Tag()
+
+	if info.Tag != tag {
+		fmt.Printf("The tags don't match: %s != %s\n", info.Tag, tag)
+	} else {
+		fmt.Println("The programs are identical, tag is", tag)
+	}
+}