Repository: cilium/ebpf
Pull request #97 (2020-05-21 11:48:48)

Correctly handle BPF and BTF endianness
There are still endianness bugs in the library. Even though we can now parse and load ELFs in host endianness correctly, trying to parse a big-endian ELF on a little-endian system will give bogus results. This PR does two things:

* Make it possible to parse BPF and BTF that is not in native endianness
* Disallow loading BPF and BTF that is not in native endianness into the kernel

This means users can inspect ELFs of arbitrary endianness, while protecting them from hard to diagnose endianness problems should they try to load their programs.

lmb: Thanks for the review. Good point re adding "can't". I'm not sure wrt error wrapping: is there an error we need to expose to the user here?
tklauser: > Thanks for the review. Good point re adding "can't". I'm not sure wrt error wrapping: is there an error we need to expose to the user here?

Personally, I usually consider it good practice to wrap underlying errors, especially in libraries. That way interested users can easily get them, while it doesn't really hurt users not using them. Also, we seem to wrap underlying errors in other cases in this package, so we would stay consistent. That being said, currently there seems to be only one possible underlying error, so it might be overkill in this case.
lmb: Right, that's what I used to do as well. I changed my approach after reading https://blog.golang.org/go1.13-errors#TOC_5. I'm not entirely happy with the result of that, it requires lots of attention when making changes like this. For now I'll stick to not wrapping though.

Patch:
diff --git a/asm/instruction.go b/asm/instruction.go
index 8058c15..02ed6c6 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -3,7 +3,6 @@ package asm
 import (
 	"encoding/binary"
 	"fmt"
-	"github.com/cilium/ebpf/internal"
 	"io"
 	"math"
 	"strings"
@@ -40,10 +39,12 @@ func (ins *Instruction) Unmarshal(r io.Reader, bo binary.ByteOrder) (uint64, err
 	}
 
 	ins.OpCode = bi.OpCode
-	ins.Dst = bi.Registers.Dst()
-	ins.Src = bi.Registers.Src()
 	ins.Offset = bi.Offset
 	ins.Constant = int64(bi.Constant)
+	ins.Dst, ins.Src, err = bi.Registers.Unmarshal(bo)
+	if err != nil {
+		return 0, xerrors.Errorf("can't unmarshal registers: %s", err)
+	}
 
 	if !bi.OpCode.isDWordLoad() {
 		return InstructionSize, nil
@@ -76,9 +77,14 @@ func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error)
 		cons = int32(uint32(ins.Constant))
 	}
 
+	regs, err := newBPFRegisters(ins.Dst, ins.Src, bo)
+	if err != nil {
+		return 0, xerrors.Errorf("can't marshal registers: %s", err)
+	}
+
 	bpfi := bpfInstruction{
 		ins.OpCode,
-		newBPFRegisters(ins.Dst, ins.Src),
+		regs,
 		ins.Offset,
 		cons,
 	}
@@ -433,27 +439,25 @@ type bpfInstruction struct {
 
 type bpfRegisters uint8
 
-func newBPFRegisters(dst, src Register) bpfRegisters {
-	if internal.NativeEndian == binary.LittleEndian {
-		return bpfRegisters((src << 4) | (dst & 0xF))
-	} else {
-		return bpfRegisters((dst << 4) | (src & 0xF))
-	}
-}
-
-func (r bpfRegisters) Dst() Register {
-	if internal.NativeEndian == binary.LittleEndian {
-		return Register(r & 0xF)
-	}else {
-		return Register(r >> 4)
+func newBPFRegisters(dst, src Register, bo binary.ByteOrder) (bpfRegisters, error) {
+	switch bo {
+	case binary.LittleEndian:
+		return bpfRegisters((src << 4) | (dst & 0xF)), nil
+	case binary.BigEndian:
+		return bpfRegisters((dst << 4) | (src & 0xF)), nil
+	default:
+		return 0, xerrors.Errorf("unrecognized ByteOrder %T", bo)
 	}
 }
 
-func (r bpfRegisters) Src() Register {
-	if internal.NativeEndian == binary.LittleEndian {
-		return Register(r >> 4)
-	} else {
-		return Register(r & 0xf)
+func (r bpfRegisters) Unmarshal(bo binary.ByteOrder) (dst, src Register, err error) {
+	switch bo {
+	case binary.LittleEndian:
+		return Register(r & 0xF), Register(r >> 4), nil
+	case binary.BigEndian:
+		return Register(r >> 4), Register(r & 0xf), nil
+	default:
+		return 0, 0, xerrors.Errorf("unrecognized ByteOrder %T", bo)
 	}
 }
 
diff --git a/elf_reader.go b/elf_reader.go
index 344615e..cdbdcb5 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -192,6 +192,7 @@ func (ec *elfCode) loadPrograms(progSections map[elf.SectionIndex]*elf.Section,
 			License:       ec.license,
 			KernelVersion: ec.version,
 			Instructions:  insns,
+			ByteOrder:     ec.ByteOrder,
 		}
 
 		if btf != nil {
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 0103f4c..85968cf 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -30,6 +30,7 @@ type Spec struct {
 	types     map[string][]Type
 	funcInfos map[string]extInfo
 	lineInfos map[string]extInfo
+	byteOrder binary.ByteOrder
 }
 
 type btfHeader struct {
@@ -139,6 +140,7 @@ func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 		strings:   rawStrings,
 		funcInfos: funcInfos,
 		lineInfos: lineInfos,
+		byteOrder: file.ByteOrder,
 	}, nil
 }
 
@@ -411,6 +413,10 @@ func NewHandle(spec *Spec) (*Handle, error) {
 		return nil, err
 	}
 
+	if spec.byteOrder != internal.NativeEndian {
+		return nil, xerrors.Errorf("can't load %s BTF on %s", spec.byteOrder, internal.NativeEndian)
+	}
+
 	btf, err := spec.marshal(internal.NativeEndian)
 	if err != nil {
 		return nil, xerrors.Errorf("can't marshal BTF: %w", err)
diff --git a/internal/btf/btf_types.go b/internal/btf/btf_types.go
index 37c940b..d11240d 100644
--- a/internal/btf/btf_types.go
+++ b/internal/btf/btf_types.go
@@ -179,6 +179,16 @@ type btfVariable struct {
 	Linkage uint32
 }
 
+type btfEnum struct {
+	NameOff uint32
+	Val     int32
+}
+
+type btfParam struct {
+	NameOff uint32
+	Type    TypeID
+}
+
 func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) {
 	var (
 		header btfType
@@ -195,8 +205,7 @@ func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) {
 		var data interface{}
 		switch header.Kind() {
 		case kindInt:
-			// sizeof(uint32)
-			data = make([]byte, 4)
+			data = new(uint32)
 		case kindPointer:
 		case kindArray:
 			data = new(btfArray)
@@ -205,8 +214,7 @@ func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) {
 		case kindUnion:
 			data = make([]btfMember, header.Vlen())
 		case kindEnum:
-			// sizeof(struct btf_enum)
-			data = make([]byte, header.Vlen()*4*2)
+			data = make([]btfEnum, header.Vlen())
 		case kindForward:
 		case kindTypedef:
 		case kindVolatile:
@@ -214,8 +222,7 @@ func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) {
 		case kindRestrict:
 		case kindFunc:
 		case kindFuncProto:
-			// sizeof(struct btf_param)
-			data = make([]byte, header.Vlen()*4*2)
+			data = make([]btfParam, header.Vlen())
 		case kindVar:
 			data = new(btfVariable)
 		case kindDatasec:
diff --git a/prog.go b/prog.go
index 60860a2..ed31ac5 100644
--- a/prog.go
+++ b/prog.go
@@ -2,6 +2,7 @@ package ebpf
 
 import (
 	"bytes"
+	"encoding/binary"
 	"fmt"
 	"math"
 	"strings"
@@ -58,6 +59,9 @@ type ProgramSpec struct {
 	// will most likely invalidate the contained data, and may
 	// result in errors when attempting to load it into the kernel.
 	BTF *btf.Program
+
+	// The byte order this program was compiled for, may be nil.
+	ByteOrder binary.ByteOrder
 }
 
 // Copy returns a copy of the spec.
@@ -188,6 +192,10 @@ func convertProgramSpec(spec *ProgramSpec, handle *btf.Handle) (*bpfProgLoadAttr
 		return nil, xerrors.New("License cannot be empty")
 	}
 
+	if spec.ByteOrder != nil && spec.ByteOrder != internal.NativeEndian {
+		return nil, xerrors.Errorf("can't load %s program on %s", spec.ByteOrder, internal.NativeEndian)
+	}
+
 	buf := bytes.NewBuffer(make([]byte, 0, len(spec.Instructions)*asm.InstructionSize))
 	err := spec.Instructions.Marshal(buf, internal.NativeEndian)
 	if err != nil {


Test patch:
diff --git a/asm/instruction_test.go b/asm/instruction_test.go
index d6ae345..f4467d7 100644
--- a/asm/instruction_test.go
+++ b/asm/instruction_test.go
@@ -8,8 +8,6 @@ import (
 	"io/ioutil"
 	"math"
 	"testing"
-
-	"github.com/cilium/ebpf/internal"
 )
 
 var test64bitImmProg = []byte{
@@ -182,31 +180,34 @@ func ExampleInstructions_Format() {
 	// 	3: Exit
 }
 
-var testSrcDstProg = []byte{
-	// on little-endian: r0 = r1
-	// on big-endian: be: r1 = r0
-	0xbf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-}
-
 func TestReadSrcDst(t *testing.T) {
-	var ins Instruction
-	n, err := ins.Unmarshal(bytes.NewReader(testSrcDstProg), binary.BigEndian)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if want := uint64(InstructionSize); n != want {
-		t.Errorf("Expected %d bytes to be read, got %d", want, n)
-	}
-	var expDst, expSrc Register
-	if internal.NativeEndian == binary.LittleEndian {
-		expDst, expSrc = 0, 1
-	} else {
-		expDst, expSrc = 1, 0
-	}
-	if ins.Dst != expDst {
-		t.Errorf("Expected destination to be %v, got %v", expDst, ins.Dst)
-	}
-	if ins.Src != expSrc {
-		t.Errorf("Expected source to be %v, got %v", expSrc, ins.Src)
+	testSrcDstProg := []byte{
+		// on little-endian: r0 = r1
+		// on big-endian: be: r1 = r0
+		0xbf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	}
+
+	testcases := []struct {
+		bo       binary.ByteOrder
+		dst, src Register
+	}{
+		{binary.BigEndian, R1, R0},
+		{binary.LittleEndian, R0, R1},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.bo.String(), func(t *testing.T) {
+			var ins Instruction
+			_, err := ins.Unmarshal(bytes.NewReader(testSrcDstProg), tc.bo)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if ins.Dst != tc.dst {
+				t.Errorf("Expected destination to be %v, got %v", tc.dst, ins.Dst)
+			}
+			if ins.Src != tc.src {
+				t.Errorf("Expected source to be %v, got %v", tc.src, ins.Src)
+			}
+		})
 	}
 }
diff --git a/elf_reader_test.go b/elf_reader_test.go
index f62025f..16dd4be 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -2,112 +2,107 @@ package ebpf
 
 import (
 	"flag"
-	"fmt"
 	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
 
+	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 )
 
 func TestLoadCollectionSpec(t *testing.T) {
-	pattern := fmt.Sprintf("testdata/loader-*-%s.elf", testutils.GetHostEndianness())
-	files, err := filepath.Glob(pattern)
-	if err != nil {
-		t.Fatal(err)
-	}
+	testutils.TestFiles(t, "testdata/loader-*.elf", func(t *testing.T, file string) {
+		spec, err := LoadCollectionSpec(file)
+		if err != nil {
+			t.Fatal("Can't parse ELF:", err)
+		}
+
+		hashMapSpec := &MapSpec{
+			Name:       "hash_map",
+			Type:       Hash,
+			KeySize:    4,
+			ValueSize:  2,
+			MaxEntries: 1,
+		}
+		checkMapSpec(t, spec.Maps, "hash_map", hashMapSpec)
+		checkMapSpec(t, spec.Maps, "array_of_hash_map", &MapSpec{
+			Name:       "hash_map",
+			Type:       ArrayOfMaps,
+			KeySize:    4,
+			MaxEntries: 2,
+		})
+		spec.Maps["array_of_hash_map"].InnerMap = spec.Maps["hash_map"]
+
+		hashMap2Spec := &MapSpec{
+			Name:       "",
+			Type:       Hash,
+			KeySize:    4,
+			ValueSize:  1,
+			MaxEntries: 2,
+			Flags:      1,
+		}
+		checkMapSpec(t, spec.Maps, "hash_map2", hashMap2Spec)
+		checkMapSpec(t, spec.Maps, "hash_of_hash_map", &MapSpec{
+			Type:       HashOfMaps,
+			KeySize:    4,
+			MaxEntries: 2,
+		})
+		spec.Maps["hash_of_hash_map"].InnerMap = spec.Maps["hash_map2"]
 
-	for _, file := range files {
-		name := filepath.Base(file)
-		t.Run(name, func(t *testing.T) {
-			spec, err := LoadCollectionSpec(file)
-			if err != nil {
-				t.Fatal("Can't parse ELF:", err)
-			}
+		checkProgramSpec(t, spec.Programs, "xdp_prog", &ProgramSpec{
+			Type:          XDP,
+			License:       "MIT",
+			KernelVersion: 0,
+		})
+		checkProgramSpec(t, spec.Programs, "no_relocation", &ProgramSpec{
+			Type:          SocketFilter,
+			License:       "MIT",
+			KernelVersion: 0,
+		})
 
-			hashMapSpec := &MapSpec{
-				Name:       "hash_map",
-				Type:       Hash,
-				KeySize:    4,
-				ValueSize:  2,
-				MaxEntries: 1,
-			}
-			checkMapSpec(t, spec.Maps, "hash_map", hashMapSpec)
-			checkMapSpec(t, spec.Maps, "array_of_hash_map", &MapSpec{
-				Name:       "hash_map",
-				Type:       ArrayOfMaps,
-				KeySize:    4,
-				MaxEntries: 2,
+		if rodata := spec.Maps[".rodata"]; rodata != nil {
+			err := spec.RewriteConstants(map[string]interface{}{
+				"arg": uint32(1),
 			})
-			spec.Maps["array_of_hash_map"].InnerMap = spec.Maps["hash_map"]
-
-			hashMap2Spec := &MapSpec{
-				Name:       "",
-				Type:       Hash,
-				KeySize:    4,
-				ValueSize:  1,
-				MaxEntries: 2,
-				Flags:      1,
+			if err != nil {
+				t.Fatal("Can't rewrite constant:", err)
 			}
-			checkMapSpec(t, spec.Maps, "hash_map2", hashMap2Spec)
-			checkMapSpec(t, spec.Maps, "hash_of_hash_map", &MapSpec{
-				Type:       HashOfMaps,
-				KeySize:    4,
-				MaxEntries: 2,
-			})
-			spec.Maps["hash_of_hash_map"].InnerMap = spec.Maps["hash_map2"]
 
-			checkProgramSpec(t, spec.Programs, "xdp_prog", &ProgramSpec{
-				Type:          XDP,
-				License:       "MIT",
-				KernelVersion: 0,
+			err = spec.RewriteConstants(map[string]interface{}{
+				"totallyBogus": uint32(1),
 			})
-			checkProgramSpec(t, spec.Programs, "no_relocation", &ProgramSpec{
-				Type:          SocketFilter,
-				License:       "MIT",
-				KernelVersion: 0,
-			})
-
-			if rodata := spec.Maps[".rodata"]; rodata != nil {
-				err := spec.RewriteConstants(map[string]interface{}{
-					"arg": uint32(1),
-				})
-				if err != nil {
-					t.Fatal("Can't rewrite constant:", err)
-				}
-
-				err = spec.RewriteConstants(map[string]interface{}{
-					"totallyBogus": uint32(1),
-				})
-				if err == nil {
-					t.Error("Rewriting a bogus constant doesn't fail")
-				}
+			if err == nil {
+				t.Error("Rewriting a bogus constant doesn't fail")
 			}
+		}
 
-			t.Log(spec.Programs["xdp_prog"].Instructions)
-
-			coll, err := NewCollectionWithOptions(spec, CollectionOptions{
-				Programs: ProgramOptions{
-					LogLevel: 1,
-				},
-			})
-			testutils.SkipIfNotSupported(t, err)
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer coll.Close()
+		t.Log(spec.Programs["xdp_prog"].Instructions)
 
-			ret, _, err := coll.Programs["xdp_prog"].Test(make([]byte, 14))
-			if err != nil {
-				t.Fatal("Can't run program:", err)
-			}
+		if spec.Programs["xdp_prog"].ByteOrder != internal.NativeEndian {
+			return
+		}
 
-			if ret != 5 {
-				t.Error("Expected return value to be 5, got", ret)
-			}
+		coll, err := NewCollectionWithOptions(spec, CollectionOptions{
+			Programs: ProgramOptions{
+				LogLevel: 1,
+			},
 		})
-	}
+		testutils.SkipIfNotSupported(t, err)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer coll.Close()
+
+		ret, _, err := coll.Programs["xdp_prog"].Test(make([]byte, 14))
+		if err != nil {
+			t.Fatal("Can't run program:", err)
+		}
+
+		if ret != 5 {
+			t.Error("Expected return value to be 5, got", ret)
+		}
+	})
 }
 
 func checkMapSpec(t *testing.T, maps map[string]*MapSpec, name string, want *MapSpec) {
@@ -164,6 +159,10 @@ func checkProgramSpec(t *testing.T, progs map[string]*ProgramSpec, name string,
 		return
 	}
 
+	if have.ByteOrder == nil {
+		t.Errorf("%s: nil ByteOrder", name)
+	}
+
 	if have.License != want.License {
 		t.Errorf("%s: expected %v license, got %v", name, want.License, have.License)
 	}
@@ -211,12 +210,13 @@ func TestCollectionSpecDetach(t *testing.T) {
 }
 
 func TestLoadInvalidMap(t *testing.T) {
-	path := fmt.Sprintf("testdata/invalid_map-%s.elf", testutils.GetHostEndianness())
-	_, err := LoadCollectionSpec(path)
-	t.Log(err)
-	if err == nil {
-		t.Fatal("should be fail")
-	}
+	testutils.TestFiles(t, "testdata/invalid_map-*.elf", func(t *testing.T, file string) {
+		_, err := LoadCollectionSpec(file)
+		t.Log(err)
+		if err == nil {
+			t.Fatal("Loading an invalid map should fail")
+		}
+	})
 }
 
 var (
@@ -232,32 +232,23 @@ func TestLibBPFCompat(t *testing.T) {
 		t.Skip("No path specified")
 	}
 
-	files, err := filepath.Glob(filepath.Join(*elfPath, *elfPattern))
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	for _, f := range files {
-		file := f // force copy
-		name := filepath.Base(file)
-		t.Run(name, func(t *testing.T) {
-			if strings.Contains(name, "_core_") {
-				t.Skip("CO-RE is not implemented")
-			}
+	testutils.TestFiles(t, filepath.Join(*elfPath, *elfPattern), func(t *testing.T, file string) {
+		if strings.Contains(filepath.Base(file), "_core_") {
+			t.Skip("CO-RE is not implemented")
+		}
 
-			t.Parallel()
+		t.Parallel()
 
-			spec, err := LoadCollectionSpec(file)
-			if err != nil {
-				t.Fatalf("Can't read %s: %s", name, err)
-			}
+		spec, err := LoadCollectionSpec(file)
+		if err != nil {
+			t.Fatalf("Can't read %s: %s", file, err)
+		}
 
-			coll, err := NewCollection(spec)
-			testutils.SkipIfNotSupported(t, err)
-			if err != nil {
-				t.Fatal(err)
-			}
-			coll.Close()
-		})
-	}
+		coll, err := NewCollection(spec)
+		testutils.SkipIfNotSupported(t, err)
+		if err != nil {
+			t.Fatal(err)
+		}
+		coll.Close()
+	})
 }
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index cb1efaf..4b3e6a0 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 )
 
@@ -53,49 +54,51 @@ func TestParseCurrentKernelBTF(t *testing.T) {
 }
 
 func TestLoadSpecFromElf(t *testing.T) {
-	fh, err := os.Open(fmt.Sprintf("../../testdata/loader-clang-9-%s.elf", testutils.GetHostEndianness()));
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer fh.Close()
-
-	spec, err := LoadSpecFromReader(fh)
-	if err != nil {
-		t.Fatal("Can't load BTF:", err)
-	}
+	testutils.TestFiles(t, "../../testdata/loader-clang-9-*.elf", func(t *testing.T, file string) {
+		fh, err := os.Open(file)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer fh.Close()
 
-	if spec == nil {
-		t.Error("No BTF found in ELF")
-	}
+		spec, err := LoadSpecFromReader(fh)
+		if err != nil {
+			t.Fatal("Can't load BTF:", err)
+		}
 
-	if sec, err := spec.Program("xdp", 1); err != nil {
-		t.Error("Can't get BTF for the xdp section:", err)
-	} else if sec == nil {
-		t.Error("Missing BTF for the xdp section")
-	}
+		if spec == nil {
+			t.Error("No BTF found in ELF")
+		}
 
-	if sec, err := spec.Program("socket", 1); err != nil {
-		t.Error("Can't get BTF for the socket section:", err)
-	} else if sec == nil {
-		t.Error("Missing BTF for the socket section")
-	}
+		if sec, err := spec.Program("xdp", 1); err != nil {
+			t.Error("Can't get BTF for the xdp section:", err)
+		} else if sec == nil {
+			t.Error("Missing BTF for the xdp section")
+		}
 
-	var bpfMapDef Struct
-	if err := spec.FindType("bpf_map_def", &bpfMapDef); err != nil {
-		t.Fatal("Can't find bpf_map_def:", err)
-	}
+		if sec, err := spec.Program("socket", 1); err != nil {
+			t.Error("Can't get BTF for the socket section:", err)
+		} else if sec == nil {
+			t.Error("Missing BTF for the socket section")
+		}
 
-	if name := bpfMapDef.Name; name != "bpf_map_def" {
-		t.Error("struct bpf_map_def has incorrect name:", name)
-	}
+		var bpfMapDef Struct
+		if err := spec.FindType("bpf_map_def", &bpfMapDef); err != nil {
+			t.Fatal("Can't find bpf_map_def:", err)
+		}
 
-	t.Run("Handle", func(t *testing.T) {
-		btf, err := NewHandle(spec)
-		testutils.SkipIfNotSupported(t, err)
-		if err != nil {
-			t.Fatal("Can't load BTF:", err)
+		if spec.byteOrder != internal.NativeEndian {
+			return
 		}
-		defer btf.Close()
+
+		t.Run("Handle", func(t *testing.T) {
+			btf, err := NewHandle(spec)
+			testutils.SkipIfNotSupported(t, err)
+			if err != nil {
+				t.Fatal("Can't load BTF:", err)
+			}
+			defer btf.Close()
+		})
 	})
 }
 
diff --git a/internal/testutils/endianness.go b/internal/testutils/endianness.go
deleted file mode 100644
index 211ef38..0000000
--- a/internal/testutils/endianness.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package testutils
-
-import (
-        "encoding/binary"
-
-	"github.com/cilium/ebpf/internal"
-)
-
-func GetHostEndianness() string {
-	if internal.NativeEndian == binary.LittleEndian {
-		return "el"
-	} else {
-		return "eb"
-	}
-}
diff --git a/internal/testutils/glob.go b/internal/testutils/glob.go
new file mode 100644
index 0000000..64de976
--- /dev/null
+++ b/internal/testutils/glob.go
@@ -0,0 +1,30 @@
+package testutils
+
+import (
+	"path/filepath"
+	"testing"
+)
+
+// TestFiles calls fn for each file matching pattern.
+//
+// The function errors out if the pattern matches no files.
+func TestFiles(t *testing.T, pattern string, fn func(*testing.T, string)) {
+	t.Helper()
+
+	files, err := filepath.Glob(pattern)
+	if err != nil {
+		t.Fatal("Can't glob files:", err)
+	}
+
+	if len(files) == 0 {
+		t.Fatalf("Pattern %s matched no files", pattern)
+	}
+
+	for _, f := range files {
+		file := f // force copy
+		name := filepath.Base(file)
+		t.Run(name, func(t *testing.T) {
+			fn(t, file)
+		})
+	}
+}
diff --git a/prog_test.go b/prog_test.go
index 463d0f5..04168fc 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -2,6 +2,7 @@ package ebpf
 
 import (
 	"bytes"
+	"encoding/binary"
 	"fmt"
 	"io/ioutil"
 	"math"
@@ -502,6 +503,20 @@ func TestNewProgramFromID(t *testing.T) {
 	}
 }
 
+func TestProgramRejectIncorrectByteOrder(t *testing.T) {
+	spec := socketFilterSpec.Copy()
+
+	spec.ByteOrder = binary.BigEndian
+	if internal.NativeEndian == binary.BigEndian {
+		spec.ByteOrder = binary.LittleEndian
+	}
+
+	_, err := NewProgram(spec)
+	if err == nil {
+		t.Error("Incorrect ByteOrder should be rejected at load time")
+	}
+}
+
 func createProgramArray(t *testing.T) *Map {
 	t.Helper()