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 <patrick@cast.ai>
  • Loading branch information
patrickpichler committed Oct 4, 2024
1 parent 4724081 commit 411de9f
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 6 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
14 changes: 10 additions & 4 deletions btf/btf.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,10 +446,16 @@ func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symb
// .ksyms describes forward declarations of kfunc signatures.
// 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:
case *Var:
switch t.Type.(type) {
case *Void:
default:
return fmt.Errorf("data section %s: expected to be void var, not %T: %w", name, 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
33 changes: 31 additions & 2 deletions elf_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ type kfuncMeta struct {
Func *btf.Func
}

type ksymMetaKey struct{}

type ksymMeta struct {
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 @@ -43,6 +49,7 @@ type elfCode struct {
extInfo *btf.ExtInfos
maps map[string]*MapSpec
vars map[string]*VariableSpec
ksymVars map[string]*btf.Var
kfuncs map[string]*btf.Func
kconfig *MapSpec
}
Expand Down Expand Up @@ -136,6 +143,7 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
maps: make(map[string]*MapSpec),
vars: make(map[string]*VariableSpec),
kfuncs: make(map[string]*btf.Func),
ksymVars: make(map[string]*btf.Var),
}

symbols, err := f.Symbols()
Expand Down Expand Up @@ -626,7 +634,11 @@ func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) err
return fmt.Errorf("asm relocation: %s: unsupported type %s", name, typ)
}

// TODO(patrick.pichler): handle me

kf := ec.kfuncs[name]
ksymsVar := ec.ksymVars[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 +659,15 @@ func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) err

ins.Constant = 0

case ksymsVar != nil && ins.OpCode.IsDWordLoad():
if bind != elf.STB_GLOBAL {
return fmt.Errorf("asm relocation: %s: %w: %s", name, errUnsupportedBinding, bind)
}
println("==> set")
ins.Metadata.Set(ksymMetaKey{}, &ksymMeta{
rel.Name,
})

// 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 +1301,16 @@ 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.ksymVars[t.TypeName()] = t
// This case should never happen, as we already checked that only vars and funcs
// are present in the vars.
default:
return fmt.Errorf("unexpected variable types: %T", v)
}
}

return nil
Expand Down
46 changes: 46 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,51 @@ func TestKconfigConfig(t *testing.T) {
qt.Assert(t, qt.Not(qt.Equals(value, 0)))
}

func TestKsym(t *testing.T) {
if !haveTestmod(t) {
t.Skip("bpf_testmod not loaded")
}

file := testutils.NativeFile(t, "testdata/ksym-%s.elf")
spec, err := LoadCollectionSpec(file)
if err != nil {
t.Fatal(err)
}

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

err = spec.LoadAndAssign(&obj, nil)
testutils.SkipIfNotSupported(t, err)
if err != nil {
t.Fatal(err)
}
defer obj.Main.Close()
defer obj.ArrayMap.Close()

_, _, err = obj.Main.Test(internal.EmptyBPFContext)
testutils.SkipIfNotSupported(t, err)
if err != nil {
t.Fatal(err)
}

var value uint64
err = obj.ArrayMap.Lookup(uint32(0), &value)
if err != nil {
t.Fatal(err)
}

ksyms, err := kallsyms.LoadSymbolAddresses()
if err != nil {
t.Fatal(err)
}

// socket_file_ops must be present and the value we have in kallsyms
qt.Assert(t, qt.Not(qt.Equals(value, ksyms["socket_file_ops __ksym"])))
}

func TestKfunc(t *testing.T) {
testutils.SkipOnOldKernel(t, "5.18", "kfunc support")
file := testutils.NativeFile(t, "testdata/kfunc-%s.elf")
Expand Down
58 changes: 58 additions & 0 deletions internal/kallsyms/kallsyms.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kallsyms
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"sync"
Expand All @@ -14,6 +15,11 @@ var kernelModules struct {
kmods map[string]string
}

var kernelSymbols struct {
sync.RWMutex
ksyms map[string]uint64
}

// KernelModule returns the kernel module, if any, a probe-able function is contained in.
func KernelModule(fn string) (string, error) {
kernelModules.RLock()
Expand Down Expand Up @@ -72,3 +78,55 @@ func loadKernelModuleMapping(f io.Reader) (map[string]string, error) {
}
return mods, nil
}

func LoadSymbolAddresses() (map[string]uint64, error) {
kernelSymbols.RLock()
ksyms := kernelSymbols.ksyms
kernelSymbols.RUnlock()

if ksyms == nil {
kernelSymbols.Lock()
defer kernelSymbols.Unlock()
ksyms = kernelSymbols.ksyms
}

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

ksyms, err = loadSymbolAddresses(f)
if err != nil {
return nil, err
}

return ksyms, nil
}

func loadSymbolAddresses(f io.Reader) (map[string]uint64, error) {
scan := bufio.NewScanner(f)

result := make(map[string]uint64)

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 nil, err
}
result[symbol] = addr
}

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

return result, nil
}
21 changes: 21 additions & 0 deletions internal/kallsyms/kallsyms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,24 @@ func TestKernelModule(t *testing.T) {

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

func TestLoadSymbolAddresses(t *testing.T) {
kallsyms := []byte(`0000000000000000 t hid_generic_probe [hid_generic]
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`)
krdr := bytes.NewBuffer(kallsyms)
ksyms, err := loadSymbolAddresses(krdr)
qt.Assert(t, qt.IsNil(err))

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))
}
40 changes: 40 additions & 0 deletions linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"io/fs"
"math"
"slices"
"strings"

"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/kallsyms"
)

// handles stores handle objects to avoid gc cleanup
Expand Down Expand Up @@ -457,3 +459,41 @@ func resolveKconfigReferences(insns asm.Instructions) (_ *Map, err error) {

return kconfig, nil
}

func resolveKsymReferences(insns asm.Instructions) error {
var ksymMap map[string]uint64
var err error

iter := insns.Iterate()
var missingKsyms []string

for iter.Next() {
meta, _ := iter.Ins.Metadata.Get(ksymMetaKey{}).(*ksymMeta)
if meta == nil {
continue
}
// We load the kallsyms map only when really needed
if ksymMap == nil {
ksymMap, err = kallsyms.LoadSymbolAddresses()
if err != nil {
return fmt.Errorf("error whild trying to load kallsyms: %v", err)
}
}

addr, found := ksymMap[meta.name]
if !found {
if !slices.Contains(missingKsyms, meta.name) {
missingKsyms = append(missingKsyms, meta.name)
}
}

iter.Ins.OpCode = asm.LoadImmOp(asm.DWord)
iter.Ins.Constant = int64(addr)
}

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

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

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

if err := fixupAndValidate(insns); err != nil {
return nil, err
}
Expand Down
25 changes: 25 additions & 0 deletions testdata/ksym.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "common.h"

char __license[] __section("license") = "Dual MIT/GPL";

struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, uint32_t);
__type(value, uint64_t);
} array_map __section(".maps");

static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, uint64_t flags) = (void *)2;

extern void socket_file_ops __ksym;

__section("socket") int ksym_test() {
uint32_t i;
uint64_t val;

i = 0;
val = &socket_file_ops;
bpf_map_update_elem(&array_map, &i, &val, 0);

return 0;
}

0 comments on commit 411de9f

Please sign in to comment.