Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 32 additions & 42 deletions devices/devicefilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
"errors"
"fmt"
"math"
"strconv"

"github.com/cilium/ebpf/asm"
devices "github.com/opencontainers/cgroups/devices/config"
"golang.org/x/sys/unix"
)
Expand All @@ -23,7 +21,7 @@ const (
)

// deviceFilter returns eBPF device filter program and its license string.
func deviceFilter(rules []*devices.Rule) (asm.Instructions, string, error) {
func deviceFilter(rules []*devices.Rule) (bpfInstructions, string, error) {
// Generate the minimum ruleset for the device rules we are given. While we
// don't care about minimum transitions in cgroupv2, using the emulator
// gives us a guarantee that the behaviour of devices filtering is the same
Expand Down Expand Up @@ -68,9 +66,9 @@ func deviceFilter(rules []*devices.Rule) (asm.Instructions, string, error) {
}

type program struct {
insts asm.Instructions
insts bpfInstructions
defaultAllow bool
blockID int
finalized bool
}

func (p *program) init() {
Expand All @@ -82,30 +80,30 @@ func (p *program) init() {
*/
// R2 <- type (lower 16 bit of u32 access_type at R1[0])
p.insts = append(p.insts,
asm.LoadMem(asm.R2, asm.R1, 0, asm.Word),
asm.And.Imm32(asm.R2, 0xFFFF))
bpfInstruction{code: unix.BPF_LDX | unix.BPF_MEM | unix.BPF_W, dst: 2, src: 1},
bpfInstruction{code: unix.BPF_ALU | unix.BPF_AND | unix.BPF_K, dst: 2, imm: 0xFFFF})

// R3 <- access (upper 16 bit of u32 access_type at R1[0])
p.insts = append(p.insts,
asm.LoadMem(asm.R3, asm.R1, 0, asm.Word),
bpfInstruction{code: unix.BPF_LDX | unix.BPF_MEM | unix.BPF_W, dst: 3, src: 1},
// RSh: bitwise shift right
asm.RSh.Imm32(asm.R3, 16))
bpfInstruction{code: unix.BPF_ALU | unix.BPF_RSH | unix.BPF_K, dst: 3, imm: 16})

// R4 <- major (u32 major at R1[4])
p.insts = append(p.insts,
asm.LoadMem(asm.R4, asm.R1, 4, asm.Word))
bpfInstruction{code: unix.BPF_LDX | unix.BPF_MEM | unix.BPF_W, dst: 4, src: 1, offset: 4})

// R5 <- minor (u32 minor at R1[8])
p.insts = append(p.insts,
asm.LoadMem(asm.R5, asm.R1, 8, asm.Word))
bpfInstruction{code: unix.BPF_LDX | unix.BPF_MEM | unix.BPF_W, dst: 5, src: 1, offset: 8})
}

// appendRule rule converts an OCI rule to the relevant eBPF block and adds it
// to the in-progress filter program. In order to operate properly, it must be
// called with a "clean" rule list (generated by devices.Emulator.Rules() --
// with any "a" rules removed).
func (p *program) appendRule(rule *devices.Rule) error {
if p.blockID < 0 {
if p.finalized {
return errors.New("the program is finalized")
}

Expand Down Expand Up @@ -143,65 +141,57 @@ func (p *program) appendRule(rule *devices.Rule) error {
// If the access is rwm, skip the check.
hasAccess := bpfAccess != (unix.BPF_DEVCG_ACC_READ | unix.BPF_DEVCG_ACC_WRITE | unix.BPF_DEVCG_ACC_MKNOD)

var (
blockSym = "block-" + strconv.Itoa(p.blockID)
nextBlockSym = "block-" + strconv.Itoa(p.blockID+1)
prevBlockLastIdx = len(p.insts) - 1
)
first := len(p.insts)
p.insts = append(p.insts,
// if (R2 != bpfType) goto next
asm.JNE.Imm(asm.R2, bpfType, nextBlockSym),
bpfInstruction{code: unix.BPF_JMP | unix.BPF_JNE | unix.BPF_K, dst: 2, imm: bpfType},
)
if hasAccess {
p.insts = append(p.insts,
// if (R3 & bpfAccess != R3 /* use R1 as a temp var */) goto next
asm.Mov.Reg32(asm.R1, asm.R3),
asm.And.Imm32(asm.R1, bpfAccess),
asm.JNE.Reg(asm.R1, asm.R3, nextBlockSym),
bpfInstruction{code: unix.BPF_ALU | unix.BPF_MOV | unix.BPF_X, dst: 1, src: 3},
bpfInstruction{code: unix.BPF_ALU | unix.BPF_AND | unix.BPF_K, dst: 1, imm: bpfAccess},
bpfInstruction{code: unix.BPF_JMP | unix.BPF_JNE | unix.BPF_X, dst: 1, src: 3},
)
}
if hasMajor {
p.insts = append(p.insts,
// if (R4 != major) goto next
asm.JNE.Imm(asm.R4, int32(rule.Major), nextBlockSym),
bpfInstruction{code: unix.BPF_JMP | unix.BPF_JNE | unix.BPF_K, dst: 4, imm: int32(rule.Major)},
)
}
if hasMinor {
p.insts = append(p.insts,
// if (R5 != minor) goto next
asm.JNE.Imm(asm.R5, int32(rule.Minor), nextBlockSym),
bpfInstruction{code: unix.BPF_JMP | unix.BPF_JNE | unix.BPF_K, dst: 5, imm: int32(rule.Minor)},
)
}
p.insts = append(p.insts, acceptBlock(rule.Allow)...)
// set blockSym to the first instruction we added in this iteration
p.insts[prevBlockLastIdx+1] = p.insts[prevBlockLastIdx+1].WithSymbol(blockSym)
p.blockID++
// All conditional jumps skip to the next rule. A rule has at most eight
// instructions, so these offsets always fit in an int16.
for i := first; i < len(p.insts); i++ {
switch p.insts[i].code {
case unix.BPF_JMP | unix.BPF_JNE | unix.BPF_K, unix.BPF_JMP | unix.BPF_JNE | unix.BPF_X:
p.insts[i].offset = int16(len(p.insts) - i - 1)
}
}
return nil
}

func (p *program) finalize() asm.Instructions {
var v int32
if p.defaultAllow {
v = 1
}
blockSym := "block-" + strconv.Itoa(p.blockID)
p.insts = append(p.insts,
// R0 <- v
asm.Mov.Imm32(asm.R0, v).WithSymbol(blockSym),
asm.Return(),
)
p.blockID = -1
func (p *program) finalize() bpfInstructions {
p.insts = append(p.insts, acceptBlock(p.defaultAllow)...)
p.finalized = true
return p.insts
}

func acceptBlock(accept bool) asm.Instructions {
func acceptBlock(accept bool) bpfInstructions {
var v int32
if accept {
v = 1
}
return []asm.Instruction{
return bpfInstructions{
// R0 <- v
asm.Mov.Imm32(asm.R0, v),
asm.Return(),
{code: unix.BPF_ALU | unix.BPF_MOV | unix.BPF_K, dst: 0, imm: v},
{code: unix.BPF_JMP | unix.BPF_EXIT},
}
}
57 changes: 56 additions & 1 deletion devices/devicefilter_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package devices

import (
"bytes"
"encoding/binary"
"slices"
"strconv"
"strings"
"testing"

"github.com/cilium/ebpf/asm"
devices "github.com/opencontainers/cgroups/devices/config"
)

Expand All @@ -19,12 +24,62 @@ func hash(s, comm string) string {
return strings.Join(res, "\n")
}

// disassemble decodes the program with cilium/ebpf/asm and labels it the way
// deviceFilter lays it out: a block starts right after the instructions added
// by init and at every jump target, and each jump refers to the block it
// lands on. A jump with a wrong offset lands elsewhere and shows up as a
// misplaced label in the listing.
func disassemble(t testing.TB, insts bpfInstructions, bo binary.ByteOrder) string {
t.Helper()
var decoded asm.Instructions
r := bytes.NewReader(insts.marshal(bo))
for r.Len() > 0 {
var ins asm.Instruction
if err := ins.Unmarshal(r, bo, "linux"); err != nil {
t.Fatal(err)
}
decoded = append(decoded, ins)
}

var p program
p.init()
starts := []int{len(p.insts)}
for i, ins := range decoded {
if ins.OpCode.JumpOp() == asm.JNE {
starts = append(starts, i+1+int(ins.Offset))
}
}
slices.Sort(starts)
starts = slices.Compact(starts)
label := func(i int) string {
return "block-" + strconv.Itoa(slices.Index(starts, i))
}
for i, ins := range decoded {
if ins.OpCode.JumpOp() == asm.JNE {
target := i + 1 + int(ins.Offset)
// Print the jump the way the assembler shows a jump to a label.
ins.Offset = -1
decoded[i] = ins.WithReference(label(target))
}
}
for _, i := range starts {
if i >= len(decoded) {
t.Fatalf("jump to instruction %d of a %d-instruction program", i, len(decoded))
}
decoded[i] = decoded[i].WithSymbol(label(i))
}
return decoded.String()
}

func testDeviceFilter(t testing.TB, devices []*devices.Rule, expectedStr string) {
insts, _, err := deviceFilter(devices)
if err != nil {
t.Fatalf("%s: %v (devices: %+v)", t.Name(), err, devices)
}
s := insts.String()
s := disassemble(t, insts, binary.LittleEndian)
if be := disassemble(t, insts, binary.BigEndian); be != s {
t.Fatalf("big-endian encoding decodes to:\n%s\nlittle-endian encoding decodes to:\n%s", be, s)
}
if expectedStr != "" {
hashed := hash(s, "//")
expectedHashed := hash(expectedStr, "//")
Expand Down
51 changes: 39 additions & 12 deletions devices/ebpf_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package devices

import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"os"
"runtime"
"sync"
"unsafe"

"github.com/cilium/ebpf/asm"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
Expand All @@ -18,6 +18,37 @@ import (
// after being interrupted by a signal (EAGAIN).
const bpfMaxRetries = 30

const bpfInstructionSize = 8

// bpfInstruction holds a single-width eBPF instruction. Device filters do not
// need wide immediates, relocations, or any of the other assembler features.
type bpfInstruction struct {
code uint8
dst, src uint8
offset int16
imm int32
}

type bpfInstructions []bpfInstruction

func (insns bpfInstructions) marshal(bo binary.ByteOrder) []byte {
data := make([]byte, len(insns)*bpfInstructionSize)
littleEndian := bo.Uint16([]byte{1, 0}) == 1
for i, ins := range insns {
raw := data[i*bpfInstructionSize : (i+1)*bpfInstructionSize]
raw[0] = ins.code
// The register bitfields reverse order on big-endian architectures.
if littleEndian {
raw[1] = ins.src<<4 | ins.dst
} else {
raw[1] = ins.dst<<4 | ins.src
}
bo.PutUint16(raw[2:4], uint16(ins.offset))
bo.PutUint32(raw[4:8], uint32(ins.imm))
}
return data
}

func bpf(cmd uintptr, attr unsafe.Pointer, size uintptr) (uintptr, error) {
// Prevent the Go profiler from repeatedly interrupting the verifier.
if cmd == unix.BPF_PROG_LOAD || cmd == unix.BPF_PROG_RUN {
Expand Down Expand Up @@ -65,12 +96,8 @@ func bpfFD(cmd uintptr, attr unsafe.Pointer, size uintptr) (int, error) {
//
// It is roughly equivalent to [github.com/cilium/ebpf/internal/sys.ProgLoad],
// and the "retry with verifier log" is taken from [github.com/cilium/ebpf.NewProgram].
func bpfProgLoad(insns asm.Instructions, license string) (int, error) {
buf := bytes.NewBuffer(make([]byte, 0, insns.Size()))
if err := insns.Marshal(buf, nativeEndian); err != nil {
return -1, err
}
insnsBytes := buf.Bytes()
func bpfProgLoad(insns bpfInstructions, license string) (int, error) {
insnsBytes := insns.marshal(binary.NativeEndian)

licensePtr, err := unix.BytePtrFromString(license)
if err != nil {
Expand All @@ -90,7 +117,7 @@ func bpfProgLoad(insns asm.Instructions, license string) (int, error) {
logBuf uint64 // pointer
}{
progType: unix.BPF_PROG_TYPE_CGROUP_DEVICE,
insnCnt: uint32(len(insnsBytes) / asm.InstructionSize),
insnCnt: uint32(len(insns)),
insns: uint64(uintptr(unsafe.Pointer(&insnsBytes[0]))),
license: uint64(uintptr(unsafe.Pointer(licensePtr))),
}
Expand Down Expand Up @@ -290,9 +317,9 @@ var (
// https://github.com/cilium/ebpf/blob/v0.6.0/link/syscalls.go.
func haveBpfProgReplace() bool {
haveBpfProgReplaceOnce.Do(func() {
progFd, err := bpfProgLoad(asm.Instructions{
asm.Mov.Imm(asm.R0, 0),
asm.Return(),
progFd, err := bpfProgLoad(bpfInstructions{
{code: unix.BPF_ALU64 | unix.BPF_MOV | unix.BPF_K, dst: 0, imm: 0},
{code: unix.BPF_JMP | unix.BPF_EXIT},
}, "MIT")
if err != nil {
logrus.Warnf("checking for BPF_F_REPLACE support: bpfProgLoad failed: %v", err)
Expand Down Expand Up @@ -337,7 +364,7 @@ func haveBpfProgReplace() bool {
// Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 .
//
// https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92
func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) error {
func loadAttachCgroupDeviceFilter(insts bpfInstructions, license string, dirFd int) error {
// Increase `ulimit -l` limit to avoid BPF_PROG_LOAD error (#2167).
// This limit is not inherited into the container.
memlockLimit := &unix.Rlimit{
Expand Down
20 changes: 0 additions & 20 deletions devices/endian.go

This file was deleted.

Loading