Extended Berkeley Packet Filter (eBPF) assembler and virtual machine
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

284 lines
6.8 KiB

package ebpf
import (
"encoding/binary"
"errors"
"fmt"
"log"
"math"
"unsafe"
)
var (
ErrNoProgram = errors.New("ebpf: no program is loaded")
errDivisionByZero = errors.New("division by zero")
)
const (
// stackSize (in bytes)
stackSize = 512
//stackAddr = math.MaxUint16 - stackSize - 1
//memoryAddr = math.MaxUint16 + 1
// maxInstructions is the maximum number of instructions the VM accepts.
maxInstructions = 4096
)
type Func func(r1, r2, r3, r4, r5 uint64) uint64
type VM struct {
// Register memory.
Register [11]uint64
// Stack memory.
Stack [stackSize]byte
// PC is the program counter.
PC uint32
// Tracer provides callbacks for the VM.
Tracer
memory []byte
memoryAddr uint64
memorySize uint64
stackAddr uint64
program Program
funcs map[Call]Func
}
func New() *VM {
return &VM{
funcs: make(map[Call]Func),
}
}
func (vm *VM) Load(program Program) error {
if l := len(program); l > maxInstructions {
return fmt.Errorf("ebpf: %d instructions exceeds maximum of %d", l, maxInstructions)
}
if err := program.Verify(); err != nil {
return err
}
vm.program = make(Program, len(program))
copy(vm.program, program)
return nil
}
// Attach a function.
func (vm *VM) Attach(call Call, fn Func) {
vm.funcs[call] = fn
}
// Run the eBPF program that was loaded.
func (vm *VM) Run(memory []byte) (uint64, error) {
if len(vm.program) == 0 {
return math.MaxUint64, ErrNoProgram
}
// Reset register and stack.
vm.reset(memory)
// Trace start.
if vm.Tracer != nil {
vm.TraceStart()
}
execution:
for vm.PC < uint32(len(vm.program)) {
var (
pc = vm.PC
in = vm.program[pc]
err error
)
if vm.Tracer != nil {
vm.TracePre(in)
}
switch in := in.(type) {
case ALUOpConstant:
err = vm.aluOp(false, in.Dst, in.Op, uint64(in.Value))
case ALUOpRegister:
err = vm.aluOp(false, in.Dst, in.Op, uint64(uint32(vm.Register[in.Src])))
case ALU64OpConstant:
err = vm.aluOp(true, in.Dst, in.Op, uint64(in.Value))
case ALU64OpRegister:
err = vm.aluOp(true, in.Dst, in.Op, vm.Register[in.Src])
case Negate:
vm.Register[in.Dst] = uint64(^uint32(vm.Register[in.Dst]))
case Negate64:
vm.Register[in.Dst] = ^vm.Register[in.Dst]
case Call:
err = vm.call(in)
case Exit:
break execution
case Jump:
vm.PC += uint32(in.Offset)
err = vm.checkPC()
case JumpIf:
if in.Cond.Match(vm.Register[in.Dst], uint64(in.Value)) {
vm.PC += uint32(in.Offset)
err = vm.checkPC()
}
case JumpIfX:
if in.Cond.Match(vm.Register[in.Dst], vm.Register[in.Src]) {
vm.PC += uint32(in.Offset)
err = vm.checkPC()
}
case LoadConstant:
vm.Register[in.Dst] = in.Value
case LoadIndirect: // TODO(maze): intended to be used in socket filters, and are therefore not general-purpose
case LoadAbsolute: // TODO(maze): intended to be used in socket filters, and are therefore not general-purpose
case LoadRegister:
vm.Register[in.Dst], err = vm.load(uint64(int64(vm.Register[in.Src])+int64(in.Offset)), in.Size)
case StoreImmediate:
err = vm.store(uint64(int64(vm.Register[in.Dst])+int64(in.Offset)), uint64(in.Value), in.Size)
case StoreRegister:
err = vm.store(uint64(int64(vm.Register[in.Dst])+int64(in.Offset)), vm.Register[in.Src], in.Size)
case ByteSwap:
vm.Register[in.Dst] = in.Swap(vm.Register[in.Dst])
default:
err = fmt.Errorf("unhandled instruction %q", in)
}
if err != nil {
return math.MaxUint64, fmt.Errorf("ebpf: at PC=%#04x: %w", pc, err)
}
if vm.Tracer != nil {
vm.TracePost(in)
}
vm.PC++
}
if vm.Tracer != nil {
vm.TraceEnded()
}
return vm.Register[R0], nil
}
func (vm *VM) aluOp(wide bool, dst Register, op ALUOp, value uint64) error {
switch op {
case ALUOpAdd:
vm.Register[dst] += value
case ALUOpSub:
vm.Register[dst] -= value
case ALUOpMul:
vm.Register[dst] *= value
case ALUOpDiv:
if value == 0 {
return errDivisionByZero
}
vm.Register[dst] /= value
case ALUOpOr:
vm.Register[dst] |= value
case ALUOpAnd:
vm.Register[dst] &= value
case ALUOpShiftLeft:
vm.Register[dst] <<= value
case ALUOpShiftRight:
vm.Register[dst] >>= value
case ALUOpMod:
if value == 0 {
return errDivisionByZero
}
vm.Register[dst] %= value
case ALUOpXor:
vm.Register[dst] ^= value
case ALUOpMove:
vm.Register[dst] = value
case ALUOpArithmicShiftRight:
vm.Register[dst] = uint64(int64(vm.Register[dst]) >> value)
}
if !wide {
vm.Register[dst] &= math.MaxUint32
}
return nil
}
func (vm *VM) call(call Call) error {
if fn, ok := vm.funcs[call]; ok {
vm.Register[R0] = fn(vm.Register[R1], vm.Register[R2], vm.Register[R3], vm.Register[R4], vm.Register[R5])
return nil
}
return fmt.Errorf("no function %#08x registered", uint32(call))
}
func (vm *VM) checkPC() error {
if vm.PC >= uint32(len(vm.program)) {
// TODO is this needed?
}
return nil
}
func (vm *VM) load(addr uint64, size uint8) (out uint64, err error) {
var memory []byte
if memory, addr, err = vm.checkMemory(addr, size, "load"); err != nil {
return
}
switch size {
case 1:
return uint64(memory[addr]), nil
case 2:
return uint64(binary.LittleEndian.Uint16(memory[addr:])), nil
case 4:
return uint64(binary.LittleEndian.Uint32(memory[addr:])), nil
case 8:
return binary.LittleEndian.Uint64(memory[addr:]), nil
default:
panic("unreachable")
}
}
func (vm *VM) store(addr, value uint64, size uint8) (err error) {
log.Printf("store at %#x: %#x (size %d)", addr, value, size)
var memory []byte
if memory, addr, err = vm.checkMemory(addr, size, "store"); err != nil {
return err
}
switch size {
case 1:
memory[addr] = uint8(value)
case 2:
binary.LittleEndian.PutUint16(memory[addr:], uint16(value))
case 4:
binary.LittleEndian.PutUint32(memory[addr:], uint32(value))
case 8:
binary.LittleEndian.PutUint64(memory[addr:], value)
}
return nil
}
func (vm *VM) checkMemory(addr uint64, size uint8, operation string) ([]byte, uint64, error) {
if vm.memoryAddr <= addr && addr+uint64(size) <= vm.memoryAddr+vm.memorySize {
// Memory access
return vm.memory, addr - vm.memoryAddr, nil
}
if vm.stackAddr <= addr && addr+uint64(size) <= vm.stackAddr+stackSize {
// Stack access
return vm.Stack[:], addr - vm.stackAddr, nil
}
return nil, 0, fmt.Errorf("out of bounds memory %s at %#x+%d", operation, addr, size)
}
var (
emptyRegisters [11]uint64
emptyStack [stackSize]byte
)
func (vm *VM) reset(memory []byte) {
copy(vm.Register[:], emptyRegisters[:])
copy(vm.Stack[:], emptyStack[:])
vm.memory = memory
vm.memoryAddr = uint64(uintptr(unsafe.Pointer(&memory[0])))
vm.memorySize = uint64(len(memory))
vm.stackAddr = uint64(uintptr(unsafe.Pointer(&vm.Stack[0])))
vm.Register[R1] = vm.memoryAddr
vm.Register[R10] = vm.stackAddr + stackSize - 1 // end of stack
}
type Tracer interface {
TraceStart()
TracePre(instruction Instruction)
TracePost(instruction Instruction)
TraceEnded()
}