Browse Source

Ported the bpf vm with pluggable extensions

tags/v1.0.0
maze 9 months ago
parent
commit
9d17131fc0
11 changed files with 2210 additions and 14 deletions
  1. 0
    0
      Makefile
  2. 10
    14
      assembler.go
  3. 145
    0
      vm.go
  4. 495
    0
      vm_alu_test.go
  5. 14
    0
      vm_debug.go
  6. 709
    0
      vm_jump_test.go
  7. 235
    0
      vm_load_test.go
  8. 188
    0
      vm_registers.go
  9. 111
    0
      vm_ret_test.go
  10. 245
    0
      vm_scratch_test.go
  11. 58
    0
      vm_test.go

+ 0
- 0
Makefile View File


+ 10
- 14
assembler.go View File

@@ -5,7 +5,6 @@ package bpf
import (
"errors"
"fmt"
"log"

"github.com/antlr/antlr4/runtime/Go/antlr"
"golang.org/x/net/bpf"
@@ -13,8 +12,15 @@ import (
"maze.io/x/bpf/internal/parser"
)

// Program is an assembled program.
type Program []bpf.Instruction

func (p Program) Assemble() ([]bpf.RawInstruction, error) {
return bpf.Assemble(p)
}

// Assemble BPF instructions from source.
func Assemble(source string) ([]bpf.RawInstruction, error) {
func Assemble(source string) (Program, error) {
var (
input = antlr.NewInputStream(source)
lexer = parser.NewBPFLexer(input)
@@ -26,17 +32,8 @@ func Assemble(source string) ([]bpf.RawInstruction, error) {
asm := new(assembler)
result := asm.Visit(parse.Program()).([]interface{})

/*
tokens := stream.GetAllTokens()
log.Printf("%d tokens", len(tokens))
for _, token := range tokens {
log.Printf("token: %s", token)
}
*/
log.Printf("result: %+v", result)

if result[1] == nil {
return result[0].([]bpf.RawInstruction), nil
return result[0].([]bpf.Instruction), nil
}
return nil, result[1].(error)
}
@@ -166,8 +163,7 @@ func (asm *assembler) VisitProgram(ctx *parser.ProgramContext) interface{} {
}
}

raw, err := bpf.Assemble(instructions)
return []interface{}{raw, err}
return []interface{}{instructions, nil}
}

func (asm *assembler) VisitLabelDefinition(ctx *parser.LabelDefinitionContext) interface{} {

+ 145
- 0
vm.go View File

@@ -0,0 +1,145 @@
package bpf

import (
"errors"
"fmt"

"golang.org/x/net/bpf"
)

// A VM is an emulated BPF virtual machine.
type VM struct {
// Extensions are optional callback to load extensions. Note that the
// "len" extension is always provided by the VM.
Extensions map[bpf.Extension]func(*Registers) uint32

program Program
}

func NewVM(program Program) *VM {
return &VM{
program: program,
}
}

// Verify runs sanity checks on the loaded program.
func (vm *VM) Verify() error {
if len(vm.program) == 0 {
return errors.New("bpf: no instructions in program")
}

for i, ins := range vm.program {
check := len(vm.program) - (i + 1)
switch ins := ins.(type) {
// Check for out-of-bounds jumps in instructions
case bpf.Jump:
if check <= int(ins.Skip) {
return fmt.Errorf("bpf: cannot jump %d instructions; jumping past program bounds", ins.Skip)
}
case bpf.JumpIf:
if check <= int(ins.SkipTrue) {
return fmt.Errorf("bpf: cannot jump %d instructions in true case; jumping past program bounds", ins.SkipTrue)
}
if check <= int(ins.SkipFalse) {
return fmt.Errorf("bpf: cannot jump %d instructions in false case; jumping past program bounds", ins.SkipFalse)
}
case bpf.JumpIfX:
if check <= int(ins.SkipTrue) {
return fmt.Errorf("bpf: cannot jump %d instructions in true case; jumping past program bounds", ins.SkipTrue)
}
if check <= int(ins.SkipFalse) {
return fmt.Errorf("bpf: cannot jump %d instructions in false case; jumping past program bounds", ins.SkipFalse)
}
// Check for division or modulus by zero
case bpf.ALUOpConstant:
if ins.Val != 0 {
break
}

switch ins.Op {
case bpf.ALUOpDiv, bpf.ALUOpMod:
return errors.New("cannot divide by zero using ALUOpConstant")
}
// Check for unknown extensions
case bpf.LoadExtension:
switch ins.Num {
case bpf.ExtLen:
default:
if vm.Extensions != nil {
if _, ok := vm.Extensions[ins.Num]; ok {
continue
}
}
return fmt.Errorf("extension %d not implemented", ins.Num)
}
}
}

// Make sure last instruction is a return instruction
switch vm.program[len(vm.program)-1].(type) {
case bpf.RetA, bpf.RetConstant:
default:
return 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 := bpf.Assemble(vm.program)

return err
}

func (vm *VM) Run(in []byte) (verdict uint32, err error) {
var (
reg = new(Registers)
end = uint32(len(vm.program))
ok = true
)
for ok && reg.PC < end {
debugf("ins=%s (%T) reg=%+v -> ", vm.program[reg.PC], vm.program[reg.PC], reg)
switch ins := vm.program[reg.PC].(type) {
case bpf.ALUOpConstant:
reg.aluOpConstant(ins)
case bpf.ALUOpX:
ok = reg.aluOpX(ins)
case bpf.Jump:
reg.PC += uint32(ins.Skip)
case bpf.JumpIf:
reg.jumpIf(ins)
case bpf.JumpIfX:
reg.jumpIfX(ins)
case bpf.LoadAbsolute:
ok = reg.loadAbsolute(ins, in)
case bpf.LoadConstant:
reg.loadConstant(ins)
case bpf.LoadExtension:
reg.loadExtension(ins, in, vm.Extensions)
case bpf.LoadIndirect:
ok = reg.loadIndirect(ins, in)
case bpf.LoadMemShift:
ok = reg.loadMemShift(ins, in)
case bpf.LoadScratch:
reg.loadScratch(ins)
case bpf.RetA:
return reg.A, nil
case bpf.RetConstant:
return ins.Val, nil
case bpf.StoreScratch:
reg.storeScratch(ins)
case bpf.TAX:
reg.X = reg.A
case bpf.TXA:
reg.A = reg.X
case bpf.NegateA:
reg.A = uint32(-int32(reg.A))
default:
return 0, fmt.Errorf("bpf: unknown instruction at pc=%d: %T", reg.PC-1, ins)
}

debugf("reg=%+v\n", reg)
reg.PC++
}

return
}

+ 495
- 0
vm_alu_test.go View File

@@ -0,0 +1,495 @@
package bpf_test

import (
"testing"

"golang.org/x/net/bpf"
)

func TestVMALUOpAdd(t *testing.T) {
vm, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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("failed to load BPF program: %v", err)
}
}

func TestVMALUOpDivByZeroALUOpX(t *testing.T) {
vm, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(1), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

+ 14
- 0
vm_debug.go View File

@@ -0,0 +1,14 @@
package bpf

import (
"fmt"
"os"
)

var debug = os.Getenv("BPF_VM_DEBUG") != ""

func debugf(format string, v ...interface{}) {
if debug {
fmt.Fprintf(os.Stderr, format, v...)
}
}

+ 709
- 0
vm_jump_test.go View File

@@ -0,0 +1,709 @@
// 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, 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)
}

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 := uint32(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) != "bpf: 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) != "bpf: 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) != "bpf: cannot jump 3 instructions in false case; jumping past program bounds" {
t.Fatalf("unexpected error: %v", err)
}
}

func TestVMJumpIfXTrueOutOfProgram(t *testing.T) {
_, err := testVM(t, []bpf.Instruction{
bpf.JumpIfX{
Cond: bpf.JumpEqual,
SkipTrue: 2,
},
bpf.RetA{},
})
if errStr(err) != "bpf: cannot jump 2 instructions in true case; jumping past program bounds" {
t.Fatalf("unexpected error: %v", err)
}
}

func TestVMJumpIfXFalseOutOfProgram(t *testing.T) {
_, err := testVM(t, []bpf.Instruction{
bpf.JumpIfX{
Cond: bpf.JumpEqual,
SkipFalse: 3,
},
bpf.RetA{},
})
if errStr(err) != "bpf: cannot jump 3 instructions in false case; jumping past program bounds" {
t.Fatalf("unexpected error: %v", err)
}
}

func TestVMJumpIfEqual(t *testing.T) {
vm, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(2), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXEqual(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 1,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 1,
},
bpf.JumpIfX{
Cond: bpf.JumpEqual,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 9,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(1), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXNotEqual(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 1,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 1,
},
bpf.JumpIfX{
Cond: bpf.JumpNotEqual,
SkipFalse: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 9,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(1), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXGreaterThan(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 4,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0x00010202,
},
bpf.JumpIfX{
Cond: bpf.JumpGreaterThan,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 12,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(4), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXLessThan(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 4,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0xff010203,
},
bpf.JumpIfX{
Cond: bpf.JumpLessThan,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 12,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(4), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXGreaterOrEqual(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 4,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0x00010203,
},
bpf.JumpIfX{
Cond: bpf.JumpGreaterOrEqual,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 12,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(4), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXLessOrEqual(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 4,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0xff010203,
},
bpf.JumpIfX{
Cond: bpf.JumpLessOrEqual,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 12,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(4), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXBitsSet(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 2,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0x1122,
},
bpf.JumpIfX{
Cond: bpf.JumpBitsSet,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 10,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(2), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

func TestVMJumpIfXBitsNotSet(t *testing.T) {
vm, err := testVM(t, []bpf.Instruction{
bpf.LoadAbsolute{
Off: 8,
Size: 2,
},
bpf.LoadConstant{
Dst: bpf.RegX,
Val: 0x1221,
},
bpf.JumpIfX{
Cond: bpf.JumpBitsNotSet,
SkipTrue: 1,
},
bpf.RetConstant{
Val: 0,
},
bpf.RetConstant{
Val: 10,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(2), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

+ 235
- 0
vm_load_test.go View File

@@ -0,0 +1,235 @@
package bpf_test

import (
"net"
"testing"

"golang.org/x/net/bpf"
"golang.org/x/net/ipv4"
)

func TestVMLoadAbsoluteOffsetOutOfBounds(t *testing.T) {
vm, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, err := testVM(t, []bpf.Instruction{
bpf.LoadMemShift{
Off: 100,
},
bpf.RetA{},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(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 := testDHCPv4(t)

// 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 := uint32(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 := testDHCPv4(t)

// 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 := uint32(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) (*testVirtualMachine, []byte) {
// DHCPv4 test data courtesy of David Anderson:
// https://github.com/google/netboot/blob/master/dhcp4/conn_linux.go#L59-L70
vm, 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
}

+ 188
- 0
vm_registers.go View File

@@ -0,0 +1,188 @@
package bpf

import (
"encoding/binary"
"fmt"

"golang.org/x/net/bpf"
)

// Registers for a BPF virtual machine.
type Registers struct {
PC uint32 // Program Counter
A uint32 // Accumulator
X uint32 //
R [16]uint32 // Scratch
}

func (reg *Registers) aluOpConstant(ins bpf.ALUOpConstant) {
reg.A = reg.aluOpCommon(ins.Op, ins.Val)
}

func (reg *Registers) aluOpX(ins bpf.ALUOpX) bool {
// Guard against division or modulus by zero by terminating
// the program, as the OS BPF VM does
if reg.X == 0 {
switch ins.Op {
case bpf.ALUOpDiv, bpf.ALUOpMod:
reg.A = 0
return false
}
}
reg.A = reg.aluOpCommon(ins.Op, reg.X)
return true
}

func (reg *Registers) aluOpCommon(op bpf.ALUOp, value uint32) uint32 {
switch op {
case bpf.ALUOpAdd:
return reg.A + value
case bpf.ALUOpSub:
return reg.A - value
case bpf.ALUOpMul:
return reg.A * value
case bpf.ALUOpDiv:
return reg.A / value
case bpf.ALUOpMod:
return reg.A % value
case bpf.ALUOpAnd:
return reg.A & value
case bpf.ALUOpOr:
return reg.A | value
case bpf.ALUOpXor:
return reg.A ^ value
case bpf.ALUOpShiftLeft:
return reg.A << value
case bpf.ALUOpShiftRight:
return reg.A >> value
default:
return reg.A
}
}

func (reg *Registers) jumpIf(ins bpf.JumpIf) {
reg.jumpIfCommon(ins.Cond, ins.SkipTrue, ins.SkipFalse, ins.Val)
}

func (reg *Registers) jumpIfX(ins bpf.JumpIfX) {
reg.jumpIfCommon(ins.Cond, ins.SkipTrue, ins.SkipFalse, reg.X)
}

func (reg *Registers) jumpIfCommon(cond bpf.JumpTest, skipTrue, skipFalse uint8, value uint32) {
var ok bool

switch cond {
case bpf.JumpEqual:
ok = reg.A == value
case bpf.JumpNotEqual:
ok = reg.A != value
case bpf.JumpGreaterThan:
ok = reg.A > value
case bpf.JumpLessThan:
ok = reg.A < value
case bpf.JumpGreaterOrEqual:
ok = reg.A >= value
case bpf.JumpLessOrEqual:
ok = reg.A <= value
case bpf.JumpBitsSet:
ok = (reg.A & value) != 0
case bpf.JumpBitsNotSet:
ok = (reg.A & value) == 0
}

if ok {
reg.PC += uint32(skipTrue)
} else {
reg.PC += uint32(skipFalse)
}
}

func (reg *Registers) loadAbsolute(ins bpf.LoadAbsolute, in []byte) (ok bool) {
offset := int(ins.Off)
size := int(ins.Size)

reg.A, ok = loadCommon(in, offset, size)
return
}

func (reg *Registers) loadConstant(ins bpf.LoadConstant) {
switch ins.Dst {
case bpf.RegA:
reg.A = ins.Val
case bpf.RegX:
reg.X = ins.Val
}
}

func (reg *Registers) loadExtension(ins bpf.LoadExtension, in []byte, ext map[bpf.Extension]func(*Registers) uint32) uint32 {
switch ins.Num {
case bpf.ExtLen:
// Builtin
return uint32(len(in))

default:
if ext != nil {
if fn, ok := ext[ins.Num]; ok {
return fn(reg)
}
}
panic(fmt.Sprintf("unimplemented extension: %d", ins.Num))
}
}

func (reg *Registers) loadIndirect(ins bpf.LoadIndirect, in []byte) (ok bool) {
offset := int(ins.Off) + int(reg.X)
size := int(ins.Size)
reg.A, ok = loadCommon(in, offset, size)
return
}

func (reg *Registers) loadMemShift(ins bpf.LoadMemShift, in []byte) (ok bool) {
offset := int(ins.Off)
if !inBounds(len(in), offset, 0) {
return false
}

// Mask off high 4 bits and multiply low 4 bits by 4
reg.X, ok = uint32(in[offset]&0x0f)*4, true
return
}

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 (reg *Registers) loadScratch(ins bpf.LoadScratch) {
switch ins.Dst {
case bpf.RegA:
reg.A = reg.R[ins.N]
case bpf.RegX:
reg.X = reg.R[ins.N]
}
}

func (reg *Registers) storeScratch(ins bpf.StoreScratch) {
switch ins.Src {
case bpf.RegA:
reg.R[ins.N] = reg.A
case bpf.RegX:
reg.R[ins.N] = reg.X
}
}

+ 111
- 0
vm_ret_test.go View File

@@ -0,0 +1,111 @@
// 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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(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, err := testVM(t, []bpf.Instruction{
bpf.RetConstant{
Val: 9,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(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, err := testVM(t, []bpf.Instruction{
bpf.RetConstant{
Val: 16,
},
})
if err != nil {
t.Fatalf("failed to load BPF program: %v", err)
}

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 := uint32(2), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

+ 245
- 0
vm_scratch_test.go View File

@@ -0,0 +1,245 @@
// 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, 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)
}

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 := uint32(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, 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)
}

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 := uint32(2), out; want != got {
t.Fatalf("unexpected number of output bytes:\n- want: %d\n- got: %d",
want, got)
}
}

+ 58
- 0
vm_test.go View File

@@ -0,0 +1,58 @@
package bpf_test

import (
"testing"

gobpf "golang.org/x/net/bpf"
"maze.io/x/bpf"
)

// udpHeaderLen is the length of a UDP header.
const udpHeaderLen = 8

type testVirtualMachine struct {
*bpf.VM
}

func (vm *testVirtualMachine) Run(in []byte) (uint32, error) {
out, err := vm.VM.Run(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.
if out >= udpHeaderLen {
out -= udpHeaderLen