From 533624231fc41da093166c334bf1e40dfadddb1f Mon Sep 17 00:00:00 2001 From: Florian Lehner Date: Tue, 8 Feb 2022 19:34:43 +0100 Subject: [PATCH 1/2] asm: add Instructions.FixupReferences Signed-off-by: Florian Lehner --- asm/instruction.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ linker.go | 56 ++--------------------------------- linker_test.go | 2 +- prog.go | 5 +--- prog_test.go | 4 +-- 5 files changed, 80 insertions(+), 60 deletions(-) diff --git a/asm/instruction.go b/asm/instruction.go index 22975e8f7..8c6d502be 100644 --- a/asm/instruction.go +++ b/asm/instruction.go @@ -19,6 +19,9 @@ const InstructionSize = 8 // RawInstructionOffset is an offset in units of raw BPF instructions. type RawInstructionOffset uint64 +var ErrUnsatisfiedMapReference = errors.New("unsatisfied map reference") +var ErrUnsatisfiedReference = errors.New("unsatisfied reference") + // Bytes returns the offset of an instruction in bytes. func (rio RawInstructionOffset) Bytes() uint64 { return uint64(rio) * InstructionSize @@ -177,6 +180,17 @@ func (ins *Instruction) RewriteMapOffset(offset uint32) error { return nil } +// RewriteJumpOffset sets the offset for a jump operation. +// +// Returns an error if the instruction is not a jump operation. +func (ins *Instruction) RewriteJumpOffset(offset int16) error { + if ins.OpCode.JumpOp() == InvalidJumpOp { + return errors.New("not a jump operation") + } + ins.Offset = offset + return nil +} + func (ins *Instruction) mapOffset() uint32 { return uint32(uint64(ins.Constant) >> 32) } @@ -527,6 +541,65 @@ func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) { return hex.EncodeToString(h.Sum(nil)[:unix.BPF_TAG_SIZE]), nil } +// FixupReferences updates all references to also take the symbol offset into account. +// +// Returns an error if a reference isn't used, see ErrUnsatisfiedMapReference and ErrUnsatisfiedReference. +func (insns Instructions) FixupReferences() error { + symbolOffsets := make(map[string]RawInstructionOffset) + iter := insns.Iterate() + for iter.Next() { + ins := iter.Ins + + if ins.Symbol == "" { + continue + } + + if _, ok := symbolOffsets[ins.Symbol]; ok { + return fmt.Errorf("duplicate symbol %s", ins.Symbol) + } + + symbolOffsets[ins.Symbol] = iter.Offset + } + + iter = insns.Iterate() + for iter.Next() { + i := iter.Index + offset := iter.Offset + ins := iter.Ins + + if ins.Reference == "" { + continue + } + + symOffset, ok := symbolOffsets[ins.Reference] + switch { + case ins.IsFunctionReference() && ins.Constant == -1: + if !ok { + break + } + + ins.Constant = int64(symOffset - offset - 1) + continue + + case ins.OpCode.Class().IsJump() && ins.Offset == -1: + if !ok { + break + } + + ins.Offset = int16(symOffset - offset - 1) + continue + + case ins.IsLoadFromMap() && ins.MapPtr() == -1: + return fmt.Errorf("map %s: %w", ins.Reference, ErrUnsatisfiedMapReference) + default: + // no fixup needed + continue + } + return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, ErrUnsatisfiedReference) + } + return nil +} + // Iterate allows iterating a BPF program while keeping track of // various offsets. // diff --git a/linker.go b/linker.go index b056f99ae..21f7db8ad 100644 --- a/linker.go +++ b/linker.go @@ -115,63 +115,13 @@ func marshalLineInfos(layout []reference) ([]byte, error) { } func fixupJumpsAndCalls(insns asm.Instructions) error { - symbolOffsets := make(map[string]asm.RawInstructionOffset) - iter := insns.Iterate() - for iter.Next() { - ins := iter.Ins - - if ins.Symbol == "" { - continue - } - - if _, ok := symbolOffsets[ins.Symbol]; ok { - return fmt.Errorf("duplicate symbol %s", ins.Symbol) - } - - symbolOffsets[ins.Symbol] = iter.Offset - } - - iter = insns.Iterate() - for iter.Next() { - i := iter.Index - offset := iter.Offset - ins := iter.Ins - - if ins.Reference == "" { - continue - } - - symOffset, ok := symbolOffsets[ins.Reference] - switch { - case ins.IsFunctionReference() && ins.Constant == -1: - if !ok { - break - } - - ins.Constant = int64(symOffset - offset - 1) - continue - - case ins.OpCode.Class().IsJump() && ins.Offset == -1: - if !ok { - break - } - - ins.Offset = int16(symOffset - offset - 1) - continue - - case ins.IsLoadFromMap() && ins.MapPtr() == -1: - return fmt.Errorf("map %s: %w", ins.Reference, errUnsatisfiedMap) - default: - // no fixup needed - continue - } - - return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, errUnsatisfiedProgram) + if err := insns.FixupReferences(); err != nil { + return err } // fixupBPFCalls replaces bpf_probe_read_{kernel,user}[_str] with bpf_probe_read[_str] on older kernels // https://github.com/libbpf/libbpf/blob/master/src/libbpf.c#L6009 - iter = insns.Iterate() + iter := insns.Iterate() for iter.Next() { ins := iter.Ins if !ins.IsBuiltinCall() { diff --git a/linker_test.go b/linker_test.go index cb75c744f..bcb763525 100644 --- a/linker_test.go +++ b/linker_test.go @@ -74,7 +74,7 @@ func TestForwardFunctionDeclaration(t *testing.T) { // This program calls an unimplemented forward function declaration. _, err = NewProgram(spec) - if !errors.Is(err, errUnsatisfiedProgram) { + if !errors.Is(err, asm.ErrUnsatisfiedReference) { t.Fatal("Expected an error wrapping errUnsatisfiedProgram, got:", err) } diff --git a/prog.go b/prog.go index 523e6a54e..df2c710f1 100644 --- a/prog.go +++ b/prog.go @@ -21,9 +21,6 @@ import ( // ErrNotSupported is returned whenever the kernel doesn't support a feature. var ErrNotSupported = internal.ErrNotSupported -var errUnsatisfiedMap = errors.New("unsatisfied map reference") -var errUnsatisfiedProgram = errors.New("unsatisfied program reference") - // ProgramID represents the unique ID of an eBPF program. type ProgramID uint32 @@ -235,7 +232,7 @@ func NewProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er defer handles.close() prog, err := newProgramWithOptions(spec, opts, handles) - if errors.Is(err, errUnsatisfiedMap) { + if errors.Is(err, asm.ErrUnsatisfiedMapReference) { return nil, fmt.Errorf("cannot load program without loading its whole collection: %w", err) } return prog, err diff --git a/prog_test.go b/prog_test.go index 079fa1e6d..448fa0a38 100644 --- a/prog_test.go +++ b/prog_test.go @@ -374,8 +374,8 @@ func TestProgramWithUnsatisfiedMap(t *testing.T) { progSpec.ByteOrder = nil _, err = NewProgram(progSpec) - if !errors.Is(err, errUnsatisfiedMap) { - t.Fatal("Expected an error wrapping errUnsatisfiedMap, got", err) + if !errors.Is(err, asm.ErrUnsatisfiedMapReference) { + t.Fatal("Expected an error wrapping errUnsatisfiedMapReference, got", err) } t.Log(err) } From 4bfa5ee2e1a5482d52f6130736bf61908b3475aa Mon Sep 17 00:00:00 2001 From: Florian Lehner Date: Tue, 8 Feb 2022 19:36:13 +0100 Subject: [PATCH 2/2] features: add misc probes Signed-off-by: Florian Lehner --- features/misc.go | 232 ++++++++++++++++++++++++++++++++++++++++++ features/misc_test.go | 42 ++++++++ 2 files changed, 274 insertions(+) create mode 100644 features/misc.go create mode 100644 features/misc_test.go diff --git a/features/misc.go b/features/misc.go new file mode 100644 index 000000000..093465289 --- /dev/null +++ b/features/misc.go @@ -0,0 +1,232 @@ +package features + +import ( + "bytes" + "errors" + "fmt" + "os" + "sync" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/unix" +) + +func init() { + miscs.miscTypes = make(map[miscType]error, maxMiscType) +} + +var ( + miscs miscCache +) + +type miscCache struct { + sync.Mutex + miscTypes map[miscType]error +} + +type miscType uint32 + +// Max returns the latest supported MiscType. +func (_ miscType) max() miscType { + return maxMiscType - 1 +} + +const ( + // largeInsn support introduced in + // commit c04c0d2b968ac45d6ef020316808ef6c82325a82 + largeInsn miscType = iota + // boundedLoops support introduced in + // commit 2589726d12a1b12eaaa93c7f1ea64287e383c7a5 + boundedLoops + // v2ISA support introduced in + // commit 92b31a9af73b3a3fc801899335d6c47966351830 + v2ISA + // v3ISA support introduced in + // commit 092ed0968bb648cd18e8a0430cd0a8a71727315c + v3ISA + // maxMiscType - Bound enum of FeatureTypes, has to be last in enum. + maxMiscType +) + +const ( + maxInsns = 4096 +) + +// HaveLargeInstructions probes the running kernel if more than 4096 instructions +// per program are supported. +// Return values have the following semantics: +// +// err == nil: The feature is available. +// errors.Is(err, ebpf.ErrNotSupported): The feature is not available. +// err != nil: Any errors encountered during probe execution, wrapped. +// +// Note that the latter case may include false negatives, and that program creation may +// succeed despite an error being returned. Some program types cannot reliably be probed and +// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive. +// +// Probe results are cached and persist throughout any process capability changes. +func HaveLargeInstructions() error { + return probeMisc(largeInsn) +} + +// HaveBoundedLoops probes the running kernel if bounded loops are supported. +// Return values have the following semantics: +// +// err == nil: The feature is available. +// errors.Is(err, ebpf.ErrNotSupported): The feature is not available. +// err != nil: Any errors encountered during probe execution, wrapped. +// +// Note that the latter case may include false negatives, and that program creation may +// succeed despite an error being returned. Some program types cannot reliably be probed and +// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive. +// +// Probe results are cached and persist throughout any process capability changes. +func HaveBoundedLoops() error { + return probeMisc(boundedLoops) +} + +// HaveV2ISA probes the running kernel if instructions of the v2 ISA are supported. +// Return values have the following semantics: +// +// err == nil: The feature is available. +// errors.Is(err, ebpf.ErrNotSupported): The feature is not available. +// err != nil: Any errors encountered during probe execution, wrapped. +// +// Note that the latter case may include false negatives, and that program creation may +// succeed despite an error being returned. Some program types cannot reliably be probed and +// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive. +// +// Probe results are cached and persist throughout any process capability changes. +func HaveV2ISA() error { + return probeMisc(v2ISA) +} + +// HaveV3ISA probes the running kernel if instructions of the v3 ISA are supported. +// Return values have the following semantics: +// +// err == nil: The feature is available. +// errors.Is(err, ebpf.ErrNotSupported): The feature is not available. +// err != nil: Any errors encountered during probe execution, wrapped. +// +// Note that the latter case may include false negatives, and that program creation may +// succeed despite an error being returned. Some program types cannot reliably be probed and +// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive. +// +// Probe results are cached and persist throughout any process capability changes. +func HaveV3ISA() error { + return probeMisc(v3ISA) +} + +// probeMisc checks the kernel for a given supported misc by creating +// a specialized program probe and loading it. +// Results are cached and persist throughout any process capability changes. +func probeMisc(mt miscType) error { + if mt > mt.max() { + return os.ErrInvalid + } + mc.Lock() + defer mc.Unlock() + err, ok := miscs.miscTypes[mt] + if ok { + return err + } + + attr, err := createMiscProbeAttr(mt) + if err != nil { + return fmt.Errorf("couldn't create the attributes for the probe: %w", err) + } + + fd, err := sys.ProgLoad(attr) + + switch { + // EINVAL occurs when attempting to create a program with an unknown type. + // E2BIG occurs when ProgLoadAttr contains non-zero bytes past the end + // of the struct known by the running kernel, meaning the kernel is too old + // to support the given map type. + case errors.Is(err, unix.EINVAL), errors.Is(err, unix.E2BIG): + err = ebpf.ErrNotSupported + + // EPERM is kept as-is and is not converted or wrapped. + case errors.Is(err, unix.EPERM): + break + + // Wrap unexpected errors. + case err != nil: + err = fmt.Errorf("unexpected error during feature probe: %w", err) + + default: + fd.Close() + } + + miscs.miscTypes[mt] = err + + return err +} + +func createMiscProbeAttr(mt miscType) (*sys.ProgLoadAttr, error) { + var ( + insns asm.Instructions + label string + ) + + switch mt { + case largeInsn: + for i := 0; i < maxInsns; i++ { + insns = append(insns, asm.Mov.Imm(asm.R0, 1)) + } + insns = append(insns, asm.Return()) + case boundedLoops: + label = "boundedLoop" + insns = asm.Instructions{ + asm.Mov.Imm(asm.R0, 10), + asm.Sub.Imm(asm.R0, 1).Sym(label), + asm.JNE.Imm(asm.R0, 0, label), + asm.Return(), + } + case v2ISA: + label = "v2isa" + insns = asm.Instructions{ + asm.Mov.Imm(asm.R0, 0).Sym(label), + asm.JLT.Imm(asm.R0, 0, label), + asm.Mov.Imm(asm.R0, 1), + asm.Return(), + } + // To test the v2 ISA we need a dedicated jump offset other + // than the one we would get from Instruction.FixupReferences(). + if err := insns[1].RewriteJumpOffset(1); err != nil { + return nil, err + } + case v3ISA: + label = "v3isa" + insns = asm.Instructions{ + asm.Mov.Imm(asm.R0, 0).Sym(label), + asm.JLT.Imm32(asm.R0, 0, label), + asm.Mov.Imm(asm.R0, 1), + asm.Return(), + } + default: + return nil, fmt.Errorf("feature %d not yet implemented", mt) + } + + if err := insns.FixupReferences(); err != nil { + return nil, err + } + + buf := bytes.NewBuffer(make([]byte, 0, insns.Size())) + if err := insns.Marshal(buf, internal.NativeEndian); err != nil { + return nil, err + } + + bytecode := buf.Bytes() + instructions := sys.NewSlicePointer(bytecode) + + return &sys.ProgLoadAttr{ + ProgType: sys.BPF_PROG_TYPE_SOCKET_FILTER, + Insns: instructions, + InsnCnt: uint32(len(bytecode) / asm.InstructionSize), + License: sys.NewStringPointer("MIT"), + }, nil +} diff --git a/features/misc_test.go b/features/misc_test.go new file mode 100644 index 000000000..207dd64da --- /dev/null +++ b/features/misc_test.go @@ -0,0 +1,42 @@ +package features + +import ( + "errors" + "fmt" + "math" + "os" + "testing" + + "github.com/cilium/ebpf/internal/testutils" +) + +func TestInvalidMisc(t *testing.T) { + if err := probeMisc(miscType(math.MaxUint32)); !errors.Is(err, os.ErrInvalid) { + t.Fatalf("Expected os.ErrInvalid but was: %v", err) + } +} + +func TestHaveMisc(t *testing.T) { + tests := map[miscType]struct { + probe func() error + minKernel string + }{ + largeInsn: {probe: HaveLargeInstructions, minKernel: "5.1"}, + boundedLoops: {probe: HaveBoundedLoops, minKernel: "5.2"}, + v2ISA: {probe: HaveV2ISA, minKernel: "4.13"}, + v3ISA: {probe: HaveV3ISA, minKernel: "5.0"}, + } + + for misc, test := range tests { + test := test + probe := fmt.Sprintf("misc-%d", misc) + t.Run(probe, func(t *testing.T) { + testutils.SkipOnOldKernel(t, test.minKernel, probe) + + if err := test.probe(); err != nil { + t.Fatalf("Feature %s isn't supported even though kernel is at least %s: %v", + probe, test.minKernel, err) + } + }) + } +}