Browse Source

bpf: add Go implementation of virtual machine

Fixes golang/go#16055.

Change-Id: I80437e2895b0f2bf23e090dec29bd20c2900db9a
Reviewed-on: https://go-review.googlesource.com/24136
Reviewed-by: David Anderson <danderson@google.com>
Run-TryBot: Mikio Hara <mikioh.mikioh@gmail.com>
Reviewed-by: Mikio Hara <mikioh.mikioh@gmail.com>
Matt Layher 9 years ago
parent
commit
1961d9def2
10 changed files with 2137 additions and 1 deletions
  1. 2 1
      bpf/doc.go
  2. 134 0
      bpf/vm.go
  3. 512 0
      bpf/vm_aluop_test.go
  4. 192 0
      bpf/vm_bpf_test.go
  5. 165 0
      bpf/vm_instructions.go
  6. 380 0
      bpf/vm_jump_test.go
  7. 246 0
      bpf/vm_load_test.go
  8. 115 0
      bpf/vm_ret_test.go
  9. 247 0
      bpf/vm_scratch_test.go
  10. 144 0
      bpf/vm_test.go

+ 2 - 1
bpf/doc.go

@@ -5,7 +5,8 @@
 /*
 /*
 
 
 Package bpf implements marshaling and unmarshaling of programs for the
 Package bpf implements marshaling and unmarshaling of programs for the
-Berkeley Packet Filter virtual machine.
+Berkeley Packet Filter virtual machine, and provides a Go implementation
+of the virtual machine.
 
 
 BPF's main use is to specify a packet filter for network taps, so that
 BPF's main use is to specify a packet filter for network taps, so that
 the kernel doesn't have to expensively copy every packet it sees to
 the kernel doesn't have to expensively copy every packet it sees to

+ 134 - 0
bpf/vm.go

@@ -0,0 +1,134 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf
+
+import (
+	"errors"
+	"fmt"
+)
+
+// A VM is an emulated BPF virtual machine.
+type VM struct {
+	filter []Instruction
+}
+
+// NewVM returns a new VM using the input BPF program.
+func NewVM(filter []Instruction) (*VM, error) {
+	if len(filter) == 0 {
+		return nil, errors.New("one or more Instructions must be specified")
+	}
+
+	for i, ins := range filter {
+		check := len(filter) - (i + 1)
+		switch ins := ins.(type) {
+		// Check for out-of-bounds jumps in instructions
+		case Jump:
+			if check <= int(ins.Skip) {
+				return nil, fmt.Errorf("cannot jump %d instructions; jumping past program bounds", ins.Skip)
+			}
+		case JumpIf:
+			if check <= int(ins.SkipTrue) {
+				return nil, fmt.Errorf("cannot jump %d instructions in true case; jumping past program bounds", ins.SkipTrue)
+			}
+			if check <= int(ins.SkipFalse) {
+				return nil, fmt.Errorf("cannot jump %d instructions in false case; jumping past program bounds", ins.SkipFalse)
+			}
+		// Check for division or modulus by zero
+		case ALUOpConstant:
+			if ins.Val != 0 {
+				break
+			}
+
+			switch ins.Op {
+			case ALUOpDiv, ALUOpMod:
+				return nil, errors.New("cannot divide by zero using ALUOpConstant")
+			}
+		}
+	}
+
+	// Make sure last instruction is a return instruction
+	switch filter[len(filter)-1].(type) {
+	case RetA, RetConstant:
+	default:
+		return nil, errors.New("BPF program must end with RetA or RetConstant")
+	}
+
+	// Though our VM works using disassembled instructions, we
+	// attempt to assemble the input filter anyway to ensure it is compatible
+	// with an operating system VM.
+	_, err := Assemble(filter)
+
+	return &VM{
+		filter: filter,
+	}, err
+}
+
+// Run runs the VM's BPF program against the input bytes.
+// Run returns the number of bytes accepted by the BPF program, and any errors
+// which occurred while processing the program.
+func (v *VM) Run(in []byte) (int, error) {
+	var (
+		// Registers of the virtual machine
+		regA       uint32
+		regX       uint32
+		regScratch [16]uint32
+
+		// OK is true if the program should continue processing the next
+		// instruction, or false if not, causing the loop to break
+		ok = true
+	)
+
+	// TODO(mdlayher): implement:
+	// - NegateA:
+	//   - would require a change from uint32 registers to int32
+	//     registers
+	// - Extension:
+	//   - implement extensions that do not depend on kernel-specific
+	//     functionality, such as 'rand'
+
+	// TODO(mdlayher): add interop tests that check signedness of ALU
+	// operations against kernel implementation, and make sure Go
+	// implementation matches behavior
+
+	for i := 0; i < len(v.filter) && ok; i++ {
+		ins := v.filter[i]
+
+		switch ins := ins.(type) {
+		case ALUOpConstant:
+			regA = aluOpConstant(ins, regA)
+		case ALUOpX:
+			regA, ok = aluOpX(ins, regA, regX)
+		case Jump:
+			i += int(ins.Skip)
+		case JumpIf:
+			jump := jumpIf(ins, regA)
+			i += jump
+		case LoadAbsolute:
+			regA, ok = loadAbsolute(ins, in)
+		case LoadConstant:
+			regA, regX = loadConstant(ins, regA, regX)
+		case LoadIndirect:
+			regA, ok = loadIndirect(ins, in, regX)
+		case LoadMemShift:
+			regX, ok = loadMemShift(ins, in)
+		case LoadScratch:
+			regA, regX = loadScratch(ins, regScratch, regA, regX)
+		case RetA:
+			return int(regA), nil
+		case RetConstant:
+			return int(ins.Val), nil
+		case StoreScratch:
+			regScratch = storeScratch(ins, regScratch, regA, regX)
+		case TAX:
+			regX = regA
+		case TXA:
+			regA = regX
+		default:
+			return 0, fmt.Errorf("unknown Instruction at index %d: %T", i, ins)
+		}
+	}
+
+	return 0, nil
+}

+ 512 - 0
bpf/vm_aluop_test.go

@@ -0,0 +1,512 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"testing"
+
+	"golang.org/x/net/bpf"
+)
+
+func TestVMALUOpAdd(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpAdd,
+			Val: 3,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		8, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 3, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpSub(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.TAX{},
+		bpf.ALUOpX{
+			Op: bpf.ALUOpSub,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpMul(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpMul,
+			Val: 2,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		6, 2, 3, 4,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 4, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpDiv(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpDiv,
+			Val: 2,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		20, 2, 3, 4,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpDivByZeroALUOpConstant(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpDiv,
+			Val: 0,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "cannot divide by zero using ALUOpConstant" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMALUOpDivByZeroALUOpX(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		// Load byte 0 into X
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.TAX{},
+		// Load byte 1 into A
+		bpf.LoadAbsolute{
+			Off:  9,
+			Size: 1,
+		},
+		// Attempt to perform 1/0
+		bpf.ALUOpX{
+			Op: bpf.ALUOpDiv,
+		},
+		// Return 4 bytes if program does not terminate
+		bpf.LoadConstant{
+			Val: 12,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 3, 4,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpOr(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpOr,
+			Val: 0x01,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x00, 0x10, 0x03, 0x04,
+		0x05, 0x06, 0x07, 0x08,
+		0x09, 0xff,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 9, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpAnd(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpAnd,
+			Val: 0x0019,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0xaa, 0x09,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpShiftLeft(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpShiftLeft,
+			Val: 0x01,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      0x02,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x01, 0xaa,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpShiftRight(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpShiftRight,
+			Val: 0x01,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      0x04,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x08, 0xff, 0xff,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpMod(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpMod,
+			Val: 20,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		30, 0, 0,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpModByZeroALUOpConstant(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpMod,
+			Val: 0,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "cannot divide by zero using ALUOpConstant" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMALUOpModByZeroALUOpX(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		// Load byte 0 into X
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.TAX{},
+		// Load byte 1 into A
+		bpf.LoadAbsolute{
+			Off:  9,
+			Size: 1,
+		},
+		// Attempt to perform 1%0
+		bpf.ALUOpX{
+			Op: bpf.ALUOpMod,
+		},
+		// Return 4 bytes if program does not terminate
+		bpf.LoadConstant{
+			Val: 12,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 3, 4,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpXor(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpXor,
+			Val: 0x0a,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      0x01,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x0b, 0x00, 0x00, 0x00,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMALUOpUnknown(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.ALUOpConstant{
+			Op:  bpf.ALUOpAdd,
+			Val: 1,
+		},
+		// Verify that an unknown operation is a no-op
+		bpf.ALUOpConstant{
+			Op: 100,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      0x02,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}

+ 192 - 0
bpf/vm_bpf_test.go

@@ -0,0 +1,192 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"net"
+	"runtime"
+	"testing"
+	"time"
+
+	"golang.org/x/net/bpf"
+	"golang.org/x/net/ipv4"
+)
+
+// A virtualMachine is a BPF virtual machine which can process an
+// input packet against a BPF program and render a verdict.
+type virtualMachine interface {
+	Run(in []byte) (int, error)
+}
+
+// canUseOSVM indicates if the OS BPF VM is available on this platform.
+func canUseOSVM() bool {
+	// OS BPF VM can only be used on platforms where x/net/ipv4 supports
+	// attaching a BPF program to a socket.
+	switch runtime.GOOS {
+	case "linux":
+		return true
+	}
+
+	return false
+}
+
+// All BPF tests against both the Go VM and OS VM are assumed to
+// be used with a UDP socket.  As a result, the entire contents
+// of a UDP datagram is sent through the BPF program, but only
+// the body after the UDP header will ever be returned in output.
+
+// testVM sets up a Go BPF VM, and if available, a native OS BPF VM
+// for integration testing.
+func testVM(t *testing.T, filter []bpf.Instruction) (virtualMachine, func(), error) {
+	goVM, err := bpf.NewVM(filter)
+	if err != nil {
+		// Some tests expect an error, so this error must be returned
+		// instead of fatally exiting the test
+		return nil, nil, err
+	}
+
+	mvm := &multiVirtualMachine{
+		goVM: goVM,
+
+		t: t,
+	}
+
+	// If available, add the OS VM for tests which verify that both the Go
+	// VM and OS VM have exactly the same output for the same input program
+	// and packet.
+	done := func() {}
+	if canUseOSVM() {
+		osVM, osVMDone := testOSVM(t, filter)
+		done = func() { osVMDone() }
+		mvm.osVM = osVM
+	}
+
+	return mvm, done, nil
+}
+
+// udpHeaderLen is the length of a UDP header.
+const udpHeaderLen = 8
+
+// A multiVirtualMachine is a virtualMachine which can call out to both the Go VM
+// and the native OS VM, if the OS VM is available.
+type multiVirtualMachine struct {
+	goVM virtualMachine
+	osVM virtualMachine
+
+	t *testing.T
+}
+
+func (mvm *multiVirtualMachine) Run(in []byte) (int, error) {
+	if len(in) < udpHeaderLen {
+		mvm.t.Fatalf("input must be at least length of UDP header (%d), got: %d",
+			udpHeaderLen, len(in))
+	}
+
+	// All tests have a UDP header as part of input, because the OS VM
+	// packets always will.  For the Go VM, this output is trimmed before
+	// being sent back to tests.
+	goOut, goErr := mvm.goVM.Run(in)
+	if goOut >= udpHeaderLen {
+		goOut -= udpHeaderLen
+	}
+
+	// If Go output is larger than the size of the packet, packet filtering
+	// interop tests must trim the output bytes to the length of the packet.
+	// The BPF VM should not do this on its own, as other uses of it do
+	// not trim the output byte count.
+	trim := len(in) - udpHeaderLen
+	if goOut > trim {
+		goOut = trim
+	}
+
+	// When the OS VM is not available, process using the Go VM alone
+	if mvm.osVM == nil {
+		return goOut, goErr
+	}
+
+	// The OS VM will apply its own UDP header, so remove the pseudo header
+	// that the Go VM needs.
+	osOut, err := mvm.osVM.Run(in[udpHeaderLen:])
+	if err != nil {
+		mvm.t.Fatalf("error while running OS VM: %v", err)
+	}
+
+	// Verify both VMs return same number of bytes
+	var mismatch bool
+	if goOut != osOut {
+		mismatch = true
+		mvm.t.Logf("output byte count does not match:\n- go: %v\n- os: %v", goOut, osOut)
+	}
+
+	if mismatch {
+		mvm.t.Fatal("Go BPF and OS BPF packet outputs do not match")
+	}
+
+	return goOut, goErr
+}
+
+// An osVirtualMachine is a virtualMachine which uses the OS's BPF VM for
+// processing BPF programs.
+type osVirtualMachine struct {
+	l net.PacketConn
+	s net.Conn
+}
+
+// testOSVM creates a virtualMachine which uses the OS's BPF VM by injecting
+// packets into a UDP listener with a BPF program attached to it.
+func testOSVM(t *testing.T, filter []bpf.Instruction) (virtualMachine, func()) {
+	l, err := net.ListenPacket("udp4", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("failed to open OS VM UDP listener: %v", err)
+	}
+
+	prog, err := bpf.Assemble(filter)
+	if err != nil {
+		t.Fatalf("failed to compile BPF program: %v", err)
+	}
+
+	p := ipv4.NewPacketConn(l)
+	if err = p.SetBPF(prog); err != nil {
+		t.Fatalf("failed to attach BPF program to listener: %v", err)
+	}
+
+	s, err := net.Dial("udp4", l.LocalAddr().String())
+	if err != nil {
+		t.Fatalf("failed to dial connection to listener: %v", err)
+	}
+
+	done := func() {
+		_ = s.Close()
+		_ = l.Close()
+	}
+
+	return &osVirtualMachine{
+		l: l,
+		s: s,
+	}, done
+}
+
+// Run sends the input bytes into the OS's BPF VM and returns its verdict.
+func (vm *osVirtualMachine) Run(in []byte) (int, error) {
+	go func() {
+		_, _ = vm.s.Write(in)
+	}()
+
+	vm.l.SetDeadline(time.Now().Add(50 * time.Millisecond))
+
+	var b [512]byte
+	n, _, err := vm.l.ReadFrom(b[:])
+	if err != nil {
+		// A timeout indicates that BPF filtered out the packet, and thus,
+		// no input should be returned.
+		if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
+			return n, nil
+		}
+
+		return n, err
+	}
+
+	return n, nil
+}

+ 165 - 0
bpf/vm_instructions.go

@@ -0,0 +1,165 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf
+
+import (
+	"encoding/binary"
+	"fmt"
+)
+
+func aluOpConstant(ins ALUOpConstant, regA uint32) uint32 {
+	return aluOpCommon(ins.Op, regA, ins.Val)
+}
+
+func aluOpX(ins ALUOpX, regA uint32, regX uint32) (uint32, bool) {
+	// Guard against division or modulus by zero by terminating
+	// the program, as the OS BPF VM does
+	if regX == 0 {
+		switch ins.Op {
+		case ALUOpDiv, ALUOpMod:
+			return 0, false
+		}
+	}
+
+	return aluOpCommon(ins.Op, regA, regX), true
+}
+
+func aluOpCommon(op ALUOp, regA uint32, value uint32) uint32 {
+	switch op {
+	case ALUOpAdd:
+		return regA + value
+	case ALUOpSub:
+		return regA - value
+	case ALUOpMul:
+		return regA * value
+	case ALUOpDiv:
+		// Division by zero not permitted by NewVM and aluOpX checks
+		return regA / value
+	case ALUOpOr:
+		return regA | value
+	case ALUOpAnd:
+		return regA & value
+	case ALUOpShiftLeft:
+		return regA << value
+	case ALUOpShiftRight:
+		return regA >> value
+	case ALUOpMod:
+		// Modulus by zero not permitted by NewVM and aluOpX checks
+		return regA % value
+	case ALUOpXor:
+		return regA ^ value
+	default:
+		return regA
+	}
+}
+
+func jumpIf(ins JumpIf, value uint32) int {
+	var ok bool
+	inV := uint32(ins.Val)
+
+	switch ins.Cond {
+	case JumpEqual:
+		ok = value == inV
+	case JumpNotEqual:
+		ok = value != inV
+	case JumpGreaterThan:
+		ok = value > inV
+	case JumpLessThan:
+		ok = value < inV
+	case JumpGreaterOrEqual:
+		ok = value >= inV
+	case JumpLessOrEqual:
+		ok = value <= inV
+	case JumpBitsSet:
+		ok = (value & inV) != 0
+	case JumpBitsNotSet:
+		ok = (value & inV) == 0
+	}
+
+	if ok {
+		return int(ins.SkipTrue)
+	}
+
+	return int(ins.SkipFalse)
+}
+
+func loadAbsolute(ins LoadAbsolute, in []byte) (uint32, bool) {
+	offset := int(ins.Off)
+	size := int(ins.Size)
+
+	return loadCommon(in, offset, size)
+}
+
+func loadConstant(ins LoadConstant, regA uint32, regX uint32) (uint32, uint32) {
+	switch ins.Dst {
+	case RegA:
+		regA = ins.Val
+	case RegX:
+		regX = ins.Val
+	}
+
+	return regA, regX
+}
+
+func loadIndirect(ins LoadIndirect, in []byte, regX uint32) (uint32, bool) {
+	offset := int(ins.Off) + int(regX)
+	size := int(ins.Size)
+
+	return loadCommon(in, offset, size)
+}
+
+func loadMemShift(ins LoadMemShift, in []byte) (uint32, bool) {
+	offset := int(ins.Off)
+
+	if !inBounds(len(in), offset, 0) {
+		return 0, false
+	}
+
+	// Mask off high 4 bits and multiply low 4 bits by 4
+	return uint32(in[offset]&0x0f) * 4, true
+}
+
+func inBounds(inLen int, offset int, size int) bool {
+	return offset+size <= inLen
+}
+
+func loadCommon(in []byte, offset int, size int) (uint32, bool) {
+	if !inBounds(len(in), offset, size) {
+		return 0, false
+	}
+
+	switch size {
+	case 1:
+		return uint32(in[offset]), true
+	case 2:
+		return uint32(binary.BigEndian.Uint16(in[offset : offset+size])), true
+	case 4:
+		return uint32(binary.BigEndian.Uint32(in[offset : offset+size])), true
+	default:
+		panic(fmt.Sprintf("invalid load size: %d", size))
+	}
+}
+
+func loadScratch(ins LoadScratch, regScratch [16]uint32, regA uint32, regX uint32) (uint32, uint32) {
+	switch ins.Dst {
+	case RegA:
+		regA = regScratch[ins.N]
+	case RegX:
+		regX = regScratch[ins.N]
+	}
+
+	return regA, regX
+}
+
+func storeScratch(ins StoreScratch, regScratch [16]uint32, regA uint32, regX uint32) [16]uint32 {
+	switch ins.Src {
+	case RegA:
+		regScratch[ins.N] = regA
+	case RegX:
+		regScratch[ins.N] = regX
+	}
+
+	return regScratch
+}

+ 380 - 0
bpf/vm_jump_test.go

@@ -0,0 +1,380 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"testing"
+
+	"golang.org/x/net/bpf"
+)
+
+func TestVMJumpOne(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.Jump{
+			Skip: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpOutOfProgram(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.Jump{
+			Skip: 1,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "cannot jump 1 instructions; jumping past program bounds" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMJumpIfTrueOutOfProgram(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			SkipTrue: 2,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "cannot jump 2 instructions in true case; jumping past program bounds" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMJumpIfFalseOutOfProgram(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.JumpIf{
+			Cond:      bpf.JumpEqual,
+			SkipFalse: 3,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "cannot jump 3 instructions in false case; jumping past program bounds" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMJumpIfEqual(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      1,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfNotEqual(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.JumpIf{
+			Cond:      bpf.JumpNotEqual,
+			Val:       1,
+			SkipFalse: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfGreaterThan(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 4,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpGreaterThan,
+			Val:      0x00010202,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 12,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 4, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfLessThan(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 4,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpLessThan,
+			Val:      0xff010203,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 12,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 4, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfGreaterOrEqual(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 4,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpGreaterOrEqual,
+			Val:      0x00010203,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 12,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 4, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfLessOrEqual(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 4,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpLessOrEqual,
+			Val:      0xff010203,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 12,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 4, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfBitsSet(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpBitsSet,
+			Val:      0x1122,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 10,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x01, 0x02,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMJumpIfBitsNotSet(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.JumpIf{
+			Cond:     bpf.JumpBitsNotSet,
+			Val:      0x1221,
+			SkipTrue: 1,
+		},
+		bpf.RetConstant{
+			Val: 0,
+		},
+		bpf.RetConstant{
+			Val: 10,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x01, 0x02,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}

+ 246 - 0
bpf/vm_load_test.go

@@ -0,0 +1,246 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"net"
+	"testing"
+
+	"golang.org/x/net/bpf"
+	"golang.org/x/net/ipv4"
+)
+
+func TestVMLoadAbsoluteOffsetOutOfBounds(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  100,
+			Size: 2,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1, 2, 3,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMLoadAbsoluteOffsetPlusSizeOutOfBounds(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMLoadAbsoluteBadInstructionSize(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Size: 5,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid load byte length 0" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMLoadConstantOK(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadConstant{
+			Dst: bpf.RegX,
+			Val: 9,
+		},
+		bpf.TXA{},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMLoadIndirectOutOfBounds(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadIndirect{
+			Off:  100,
+			Size: 1,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMLoadMemShiftOutOfBounds(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadMemShift{
+			Off: 100,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+const (
+	dhcp4Port = 53
+)
+
+func TestVMLoadMemShiftLoadIndirectNoResult(t *testing.T) {
+	vm, in, done := testDHCPv4(t)
+	defer done()
+
+	// Append mostly empty UDP header with incorrect DHCPv4 port
+	in = append(in, []byte{
+		0, 0,
+		0, dhcp4Port + 1,
+		0, 0,
+		0, 0,
+	}...)
+
+	out, err := vm.Run(in)
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 0, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMLoadMemShiftLoadIndirectOK(t *testing.T) {
+	vm, in, done := testDHCPv4(t)
+	defer done()
+
+	// Append mostly empty UDP header with correct DHCPv4 port
+	in = append(in, []byte{
+		0, 0,
+		0, dhcp4Port,
+		0, 0,
+		0, 0,
+	}...)
+
+	out, err := vm.Run(in)
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := len(in)-8, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func testDHCPv4(t *testing.T) (virtualMachine, []byte, func()) {
+	// DHCPv4 test data courtesy of David Anderson:
+	// https://github.com/google/netboot/blob/master/dhcp4/conn_linux.go#L59-L70
+	vm, done, err := testVM(t, []bpf.Instruction{
+		// Load IPv4 packet length
+		bpf.LoadMemShift{Off: 8},
+		// Get UDP dport
+		bpf.LoadIndirect{Off: 8 + 2, Size: 2},
+		// Correct dport?
+		bpf.JumpIf{Cond: bpf.JumpEqual, Val: dhcp4Port, SkipFalse: 1},
+		// Accept
+		bpf.RetConstant{Val: 1500},
+		// Ignore
+		bpf.RetConstant{Val: 0},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+
+	// Minimal requirements to make a valid IPv4 header
+	h := &ipv4.Header{
+		Len: ipv4.HeaderLen,
+		Src: net.IPv4(192, 168, 1, 1),
+		Dst: net.IPv4(192, 168, 1, 2),
+	}
+	hb, err := h.Marshal()
+	if err != nil {
+		t.Fatalf("failed to marshal IPv4 header: %v", err)
+	}
+
+	hb = append([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+	}, hb...)
+
+	return vm, hb, done
+}

+ 115 - 0
bpf/vm_ret_test.go

@@ -0,0 +1,115 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"testing"
+
+	"golang.org/x/net/bpf"
+)
+
+func TestVMRetA(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		9,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMRetALargerThanInput(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 2,
+		},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 255,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMRetConstant(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.RetConstant{
+			Val: 9,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 1, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMRetConstantLargerThanInput(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.RetConstant{
+			Val: 16,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0, 1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}

+ 247 - 0
bpf/vm_scratch_test.go

@@ -0,0 +1,247 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"testing"
+
+	"golang.org/x/net/bpf"
+)
+
+func TestVMStoreScratchInvalidScratchRegisterTooSmall(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   -1,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid scratch slot -1" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMStoreScratchInvalidScratchRegisterTooLarge(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   16,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid scratch slot 16" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMStoreScratchUnknownSourceRegister(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.StoreScratch{
+			Src: 100,
+			N:   0,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid source register 100" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMLoadScratchInvalidScratchRegisterTooSmall(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadScratch{
+			Dst: bpf.RegX,
+			N:   -1,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid scratch slot -1" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMLoadScratchInvalidScratchRegisterTooLarge(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadScratch{
+			Dst: bpf.RegX,
+			N:   16,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid scratch slot 16" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMLoadScratchUnknownDestinationRegister(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadScratch{
+			Dst: 100,
+			N:   0,
+		},
+		bpf.RetA{},
+	})
+	if errStr(err) != "assembling instruction 1: invalid target register 100" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMStoreScratchLoadScratchOneValue(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		// Load byte 255
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		// Copy to X and store in scratch[0]
+		bpf.TAX{},
+		bpf.StoreScratch{
+			Src: bpf.RegX,
+			N:   0,
+		},
+		// Load byte 1
+		bpf.LoadAbsolute{
+			Off:  9,
+			Size: 1,
+		},
+		// Overwrite 1 with 255 from scratch[0]
+		bpf.LoadScratch{
+			Dst: bpf.RegA,
+			N:   0,
+		},
+		// Return 255
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		255, 1, 2,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 3, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}
+
+func TestVMStoreScratchLoadScratchMultipleValues(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		// Load byte 10
+		bpf.LoadAbsolute{
+			Off:  8,
+			Size: 1,
+		},
+		// Store in scratch[0]
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   0,
+		},
+		// Load byte 20
+		bpf.LoadAbsolute{
+			Off:  9,
+			Size: 1,
+		},
+		// Store in scratch[1]
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   1,
+		},
+		// Load byte 30
+		bpf.LoadAbsolute{
+			Off:  10,
+			Size: 1,
+		},
+		// Store in scratch[2]
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   2,
+		},
+		// Load byte 1
+		bpf.LoadAbsolute{
+			Off:  11,
+			Size: 1,
+		},
+		// Store in scratch[3]
+		bpf.StoreScratch{
+			Src: bpf.RegA,
+			N:   3,
+		},
+		// Load in byte 10 to X
+		bpf.LoadScratch{
+			Dst: bpf.RegX,
+			N:   0,
+		},
+		// Copy X -> A
+		bpf.TXA{},
+		// Verify value is 10
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      10,
+			SkipTrue: 1,
+		},
+		// Fail test if incorrect
+		bpf.RetConstant{
+			Val: 0,
+		},
+		// Load in byte 20 to A
+		bpf.LoadScratch{
+			Dst: bpf.RegA,
+			N:   1,
+		},
+		// Verify value is 20
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      20,
+			SkipTrue: 1,
+		},
+		// Fail test if incorrect
+		bpf.RetConstant{
+			Val: 0,
+		},
+		// Load in byte 30 to A
+		bpf.LoadScratch{
+			Dst: bpf.RegA,
+			N:   2,
+		},
+		// Verify value is 30
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      30,
+			SkipTrue: 1,
+		},
+		// Fail test if incorrect
+		bpf.RetConstant{
+			Val: 0,
+		},
+		// Return first two bytes on success
+		bpf.RetConstant{
+			Val: 10,
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to load BPF program: %v", err)
+	}
+	defer done()
+
+	out, err := vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		10, 20, 30, 1,
+	})
+	if err != nil {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+	if want, got := 2, out; want != got {
+		t.Fatalf("unexpected number of output bytes:\n- want: %d\n-  got: %d",
+			want, got)
+	}
+}

+ 144 - 0
bpf/vm_test.go

@@ -0,0 +1,144 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bpf_test
+
+import (
+	"fmt"
+	"testing"
+
+	"golang.org/x/net/bpf"
+)
+
+var _ bpf.Instruction = unknown{}
+
+type unknown struct{}
+
+func (unknown) Assemble() (bpf.RawInstruction, error) {
+	return bpf.RawInstruction{}, nil
+}
+
+func TestVMUnknownInstruction(t *testing.T) {
+	vm, done, err := testVM(t, []bpf.Instruction{
+		bpf.LoadConstant{
+			Dst: bpf.RegA,
+			Val: 100,
+		},
+		// Should terminate the program with an error immediately
+		unknown{},
+		bpf.RetA{},
+	})
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	defer done()
+
+	_, err = vm.Run([]byte{
+		0xff, 0xff, 0xff, 0xff,
+		0xff, 0xff, 0xff, 0xff,
+		0x00, 0x00,
+	})
+	if errStr(err) != "unknown Instruction at index 1: bpf_test.unknown" {
+		t.Fatalf("unexpected error while running program: %v", err)
+	}
+}
+
+func TestVMNoReturnInstruction(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{
+		bpf.LoadConstant{
+			Dst: bpf.RegA,
+			Val: 1,
+		},
+	})
+	if errStr(err) != "BPF program must end with RetA or RetConstant" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+func TestVMNoInputInstructions(t *testing.T) {
+	_, _, err := testVM(t, []bpf.Instruction{})
+	if errStr(err) != "one or more Instructions must be specified" {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}
+
+// ExampleNewVM demonstrates usage of a VM, using an Ethernet frame
+// as input and checking its EtherType to determine if it should be accepted.
+func ExampleNewVM() {
+	// Offset | Length | Comment
+	// -------------------------
+	//   00   |   06   | Ethernet destination MAC address
+	//   06   |   06   | Ethernet source MAC address
+	//   12   |   02   | Ethernet EtherType
+	const (
+		etOff = 12
+		etLen = 2
+
+		etARP = 0x0806
+	)
+
+	// Set up a VM to filter traffic based on if its EtherType
+	// matches the ARP EtherType.
+	vm, err := bpf.NewVM([]bpf.Instruction{
+		// Load EtherType value from Ethernet header
+		bpf.LoadAbsolute{
+			Off:  etOff,
+			Size: etLen,
+		},
+		// If EtherType is equal to the ARP EtherType, jump to allow
+		// packet to be accepted
+		bpf.JumpIf{
+			Cond:     bpf.JumpEqual,
+			Val:      etARP,
+			SkipTrue: 1,
+		},
+		// EtherType does not match the ARP EtherType
+		bpf.RetConstant{
+			Val: 0,
+		},
+		// EtherType matches the ARP EtherType, accept up to 1500
+		// bytes of packet
+		bpf.RetConstant{
+			Val: 1500,
+		},
+	})
+	if err != nil {
+		panic(fmt.Sprintf("failed to load BPF program: %v", err))
+	}
+
+	// Create an Ethernet frame with the ARP EtherType for testing
+	frame := []byte{
+		0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+		0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
+		0x08, 0x06,
+		// Payload ommitted for brevity
+	}
+
+	// Run our VM's BPF program using the Ethernet frame as input
+	out, err := vm.Run(frame)
+	if err != nil {
+		panic(fmt.Sprintf("failed to accept Ethernet frame: %v", err))
+	}
+
+	// BPF VM can return a byte count greater than the number of input
+	// bytes, so trim the output to match the input byte length
+	if out > len(frame) {
+		out = len(frame)
+	}
+
+	fmt.Printf("out: %d bytes", out)
+
+	// Output:
+	// out: 14 bytes
+}
+
+// errStr returns the string representation of an error, or
+// "<nil>" if it is nil.
+func errStr(err error) string {
+	if err == nil {
+		return "<nil>"
+	}
+
+	return err.Error()
+}