Skip to content

Commit

Permalink
feat: add support for untyped extern __ksym variables
Browse files Browse the repository at this point in the history
It is now possible to use the `__ksym` attribute to load the address of
the ksym for every use of the variable. It works by searching the name
of any external ksym variable in the `/proc/kallsyms` file and patching
any load operation on that external variable to load the address as an
immediate value.

Signed-off-by: Patrick Pichler <[email protected]>
  • Loading branch information
patrickpichler authored and ti-mo committed Oct 15, 2024
1 parent c63a00b commit 9356805
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 18 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ TARGETS := \
testdata/fwd_decl \
testdata/kconfig \
testdata/kconfig_config \
testdata/ksym \
testdata/kfunc \
testdata/invalid-kfunc \
testdata/kfunc-kmod \
Expand Down
16 changes: 11 additions & 5 deletions btf/btf.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,13 +443,19 @@ func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symb
// Some Datasecs are virtual and don't have corresponding ELF sections.
switch name {
case ".ksyms":
// .ksyms describes forward declarations of kfunc signatures.
// .ksyms describes forward declarations of kfunc signatures, as well as
// references to kernel symbols.
// Nothing to fix up, all sizes and offsets are 0.
for _, vsi := range ds.Vars {
_, ok := vsi.Type.(*Func)
if !ok {
// Only Funcs are supported in the .ksyms Datasec.
return fmt.Errorf("data section %s: expected *btf.Func, not %T: %w", name, vsi.Type, ErrNotSupported)
switch t := vsi.Type.(type) {
case *Func:
continue
case *Var:
if _, ok := t.Type.(*Void); !ok {
return fmt.Errorf("data section %s: expected %s to be *Void, not %T: %w", name, vsi.Type.TypeName(), vsi.Type, ErrNotSupported)
}
default:
return fmt.Errorf("data section %s: expected to be either *btf.Func or *btf.Var, not %T: %w", name, vsi.Type, ErrNotSupported)
}
}

Expand Down
37 changes: 35 additions & 2 deletions elf_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/kallsyms"
"github.com/cilium/ebpf/internal/sys"
)

Expand All @@ -32,6 +33,14 @@ type kfuncMeta struct {
Func *btf.Func
}

type ksymMetaKey struct{}

type ksymMeta struct {
Binding elf.SymBind
Addr uint64
Name string
}

// elfCode is a convenience to reduce the amount of arguments that have to
// be passed around explicitly. You should treat its contents as immutable.
type elfCode struct {
Expand All @@ -44,6 +53,7 @@ type elfCode struct {
maps map[string]*MapSpec
vars map[string]*VariableSpec
kfuncs map[string]*btf.Func
ksyms map[string]uint64
kconfig *MapSpec
}

Expand Down Expand Up @@ -136,6 +146,7 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
maps: make(map[string]*MapSpec),
vars: make(map[string]*VariableSpec),
kfuncs: make(map[string]*btf.Func),
ksyms: make(map[string]uint64),
}

symbols, err := f.Symbols()
Expand Down Expand Up @@ -627,6 +638,8 @@ func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) err
}

kf := ec.kfuncs[name]
ksymAddr := ec.ksyms[name]

switch {
// If a Call / DWordLoad instruction is found and the datasec has a btf.Func with a Name
// that matches the symbol name we mark the instruction as a referencing a kfunc.
Expand All @@ -647,6 +660,16 @@ func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) err

ins.Constant = 0

case ksymAddr != 0 && ins.OpCode.IsDWordLoad():
if bind != elf.STB_GLOBAL && bind != elf.STB_WEAK {
return fmt.Errorf("asm relocation: %s: %w: %s", name, errUnsupportedBinding, bind)
}
ins.Metadata.Set(ksymMetaKey{}, &ksymMeta{
Binding: bind,
Name: name,
Addr: ksymAddr,
})

// If no kconfig map is found, this must be a symbol reference from inline
// asm (see testdata/loader.c:asm_relocation()) or a call to a forward
// function declaration (see testdata/fwd_decl.c). Don't interfere, These
Expand Down Expand Up @@ -1280,8 +1303,18 @@ func (ec *elfCode) loadKsymsSection() error {
}

for _, v := range ds.Vars {
// we have already checked the .ksyms Datasec to only contain Func Vars.
ec.kfuncs[v.Type.TypeName()] = v.Type.(*btf.Func)
switch t := v.Type.(type) {
case *btf.Func:
ec.kfuncs[t.TypeName()] = t
case *btf.Var:
ec.ksyms[t.TypeName()] = 0
default:
return fmt.Errorf("unexpected variable type in .ksysm: %T", v)
}
}

if err := kallsyms.LoadSymbolAddresses(ec.ksyms); err != nil {
return fmt.Errorf("error while loading ksym addresses: %w", err)
}

return nil
Expand Down
57 changes: 57 additions & 0 deletions elf_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/kallsyms"
"github.com/cilium/ebpf/internal/linux"
"github.com/cilium/ebpf/internal/sys"
"github.com/cilium/ebpf/internal/testutils"
Expand Down Expand Up @@ -772,6 +773,62 @@ func TestKconfigConfig(t *testing.T) {
qt.Assert(t, qt.Not(qt.Equals(value, 0)))
}

func TestKsym(t *testing.T) {
file := testutils.NativeFile(t, "testdata/ksym-%s.elf")
spec, err := LoadCollectionSpec(file)
qt.Assert(t, qt.IsNil(err))

var obj struct {
Main *Program `ebpf:"ksym_test"`
ArrayMap *Map `ebpf:"array_map"`
}

err = spec.LoadAndAssign(&obj, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
defer obj.Main.Close()
defer obj.ArrayMap.Close()

_, _, err = obj.Main.Test(internal.EmptyBPFContext)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))

ksyms := map[string]uint64{
"socket_file_ops": 0,
"tty_fops": 0,
}

qt.Assert(t, qt.IsNil(kallsyms.LoadSymbolAddresses(ksyms)))

var value uint64
qt.Assert(t, qt.IsNil(obj.ArrayMap.Lookup(uint32(0), &value)))
qt.Assert(t, qt.Equals(value, ksyms["socket_file_ops"]))

err = obj.ArrayMap.Lookup(uint32(1), &value)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Equals(value, ksyms["tty_fops"]))
}

func TestKsymWeakMissing(t *testing.T) {
file := testutils.NativeFile(t, "testdata/ksym-%s.elf")
spec, err := LoadCollectionSpec(file)
qt.Assert(t, qt.IsNil(err))

var obj struct {
Main *Program `ebpf:"ksym_missing_test"`
}

err = spec.LoadAndAssign(&obj, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
defer obj.Main.Close()

res, _, err := obj.Main.Test(internal.EmptyBPFContext)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Equals(res, 1))
}

func TestKfunc(t *testing.T) {
testutils.SkipOnOldKernel(t, "5.18", "kfunc support")
file := testutils.NativeFile(t, "testdata/kfunc-%s.elf")
Expand Down
53 changes: 53 additions & 0 deletions internal/kallsyms/kallsyms.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package kallsyms
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"sync"
Expand Down Expand Up @@ -52,6 +54,8 @@ func FlushKernelModuleCache() {
kernelModules.kmods = nil
}

var errKsymIsAmbiguous = errors.New("ksym is ambiguous")

func loadKernelModuleMapping(f io.Reader) (map[string]string, error) {
mods := make(map[string]string)
scanner := bufio.NewScanner(f)
Expand All @@ -72,3 +76,52 @@ func loadKernelModuleMapping(f io.Reader) (map[string]string, error) {
}
return mods, nil
}

func LoadSymbolAddresses(symbols map[string]uint64) error {
if len(symbols) == 0 {
return nil
}

f, err := os.Open("/proc/kallsyms")
if err != nil {
return err
}

if err := loadSymbolAddresses(f, symbols); err != nil {
return fmt.Errorf("error loading symbol addresses: %w", err)
}

return nil
}

func loadSymbolAddresses(f io.Reader, symbols map[string]uint64) error {
scan := bufio.NewScanner(f)
for scan.Scan() {
var (
addr uint64
t rune
symbol string
)

line := scan.Text()

_, err := fmt.Sscanf(line, "%x %c %s", &addr, &t, &symbol)
if err != nil {
return err
}
// Multiple addresses for a symbol have been found. Lets return an error to not confuse any
// users and handle it the same as libbpf.
if existingAddr, found := symbols[symbol]; existingAddr != 0 {
return fmt.Errorf("symbol %s(0x%x): duplicate found at address 0x%x %w",
symbol, existingAddr, addr, errKsymIsAmbiguous)
} else if found {
symbols[symbol] = addr
}
}

if scan.Err() != nil {
return scan.Err()
}

return nil
}
47 changes: 36 additions & 11 deletions internal/kallsyms/kallsyms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ import (
"github.com/go-quicktest/qt"
)

var kallsyms = []byte(`0000000000000000 t hid_generic_probe [hid_generic]
00000000000000EA t writenote
00000000000000A0 T tcp_connect
00000000000000B0 B empty_zero_page
00000000000000C0 D kimage_vaddr
00000000000000D0 R __start_pci_fixups_early
00000000000000E0 V hv_root_partition
00000000000000F0 W calibrate_delay_is_known
A0000000000000AA a nft_counter_seq [nft_counter]
A0000000000000BA b bootconfig_found
A0000000000000CA d __func__.10
A0000000000000DA r __ksymtab_LZ4_decompress_fast
A0000000000000EA t writenote`)

func TestKernelModule(t *testing.T) {
kallsyms := []byte(`0000000000000000 t hid_generic_probe [hid_generic]
0000000000000000 T tcp_connect
0000000000000000 B empty_zero_page
0000000000000000 D kimage_vaddr
0000000000000000 R __start_pci_fixups_early
0000000000000000 V hv_root_partition
0000000000000000 W calibrate_delay_is_known
0000000000000000 a nft_counter_seq [nft_counter]
0000000000000000 b bootconfig_found
0000000000000000 d __func__.10
0000000000000000 r __ksymtab_LZ4_decompress_fast`)
krdr := bytes.NewBuffer(kallsyms)
kmods, err := loadKernelModuleMapping(krdr)
qt.Assert(t, qt.IsNil(err))
Expand All @@ -37,3 +40,25 @@ func TestKernelModule(t *testing.T) {

qt.Assert(t, qt.Equals(kmods["nft_counter_seq"], ""))
}

func TestLoadSymbolAddresses(t *testing.T) {
b := bytes.NewBuffer(kallsyms)
ksyms := map[string]uint64{
"hid_generic_probe": 0,
"tcp_connect": 0,
"bootconfig_found": 0,
}
qt.Assert(t, qt.IsNil(loadSymbolAddresses(b, ksyms)))

qt.Assert(t, qt.Equals(ksyms["hid_generic_probe"], 0))
qt.Assert(t, qt.Equals(ksyms["tcp_connect"], 0xA0))
qt.Assert(t, qt.Equals(ksyms["bootconfig_found"], 0xA0000000000000BA))

b = bytes.NewBuffer(kallsyms)
ksyms = map[string]uint64{
"hid_generic_probe": 0,
"writenote": 0,
}
err := loadSymbolAddresses(b, ksyms)
qt.Assert(t, qt.ErrorIs(err, errKsymIsAmbiguous))
}
36 changes: 36 additions & 0 deletions linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/fs"
"math"
"slices"
"strings"

"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/btf"
Expand Down Expand Up @@ -457,3 +458,38 @@ func resolveKconfigReferences(insns asm.Instructions) (_ *Map, err error) {

return kconfig, nil
}

func resolveKsymReferences(insns asm.Instructions) error {
var missing []string

iter := insns.Iterate()
for iter.Next() {
ins := iter.Ins
meta, _ := ins.Metadata.Get(ksymMetaKey{}).(*ksymMeta)
if meta == nil {
continue
}

if meta.Addr != 0 {
ins.Constant = int64(meta.Addr)
continue
}

if meta.Binding == elf.STB_WEAK {
// A weak ksym variable in eBPF C means its resolution is optional.
// Set a zero constant explicitly for clarity.
ins.Constant = 0
continue
}

if !slices.Contains(missing, meta.Name) {
missing = append(missing, meta.Name)
}
}

if len(missing) > 0 {
return fmt.Errorf("missing ksyms: %s", strings.Join(missing, ","))
}

return nil
}
4 changes: 4 additions & 0 deletions prog.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
}
defer kconfig.Close()

if err := resolveKsymReferences(insns); err != nil {
return nil, fmt.Errorf("resolve .ksyms: %w", err)
}

if err := fixupAndValidate(insns); err != nil {
return nil, err
}
Expand Down
Binary file added testdata/ksym-eb.elf
Binary file not shown.
Binary file added testdata/ksym-el.elf
Binary file not shown.
Loading

0 comments on commit 9356805

Please sign in to comment.