diff --git a/Makefile b/Makefile index 03daa184c..f2444120a 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ TARGETS := \ testdata/fwd_decl \ testdata/kconfig \ testdata/kconfig_config \ + testdata/ksym \ testdata/kfunc \ testdata/invalid-kfunc \ testdata/kfunc-kmod \ diff --git a/btf/btf.go b/btf/btf.go index ff9fe4d95..880c5ade0 100644 --- a/btf/btf.go +++ b/btf/btf.go @@ -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) } } diff --git a/elf_reader.go b/elf_reader.go index eb7f03319..6887f819d 100644 --- a/elf_reader.go +++ b/elf_reader.go @@ -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" ) @@ -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 { @@ -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 } @@ -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() @@ -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. @@ -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 @@ -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 diff --git a/elf_reader_test.go b/elf_reader_test.go index 0d5408ecd..af352d643 100644 --- a/elf_reader_test.go +++ b/elf_reader_test.go @@ -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" @@ -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") diff --git a/internal/kallsyms/kallsyms.go b/internal/kallsyms/kallsyms.go index 776c7a10a..721d69d03 100644 --- a/internal/kallsyms/kallsyms.go +++ b/internal/kallsyms/kallsyms.go @@ -3,6 +3,8 @@ package kallsyms import ( "bufio" "bytes" + "errors" + "fmt" "io" "os" "sync" @@ -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) @@ -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 +} diff --git a/internal/kallsyms/kallsyms_test.go b/internal/kallsyms/kallsyms_test.go index 4e245a0eb..ac3be9e69 100644 --- a/internal/kallsyms/kallsyms_test.go +++ b/internal/kallsyms/kallsyms_test.go @@ -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)) @@ -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)) +} diff --git a/linker.go b/linker.go index 788f21b7b..1466bea6d 100644 --- a/linker.go +++ b/linker.go @@ -9,6 +9,7 @@ import ( "io/fs" "math" "slices" + "strings" "github.com/cilium/ebpf/asm" "github.com/cilium/ebpf/btf" @@ -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 +} diff --git a/prog.go b/prog.go index 983044c0c..98c11cba5 100644 --- a/prog.go +++ b/prog.go @@ -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 } diff --git a/testdata/ksym-eb.elf b/testdata/ksym-eb.elf new file mode 100644 index 000000000..5345cb759 Binary files /dev/null and b/testdata/ksym-eb.elf differ diff --git a/testdata/ksym-el.elf b/testdata/ksym-el.elf new file mode 100644 index 000000000..b9a7523d3 Binary files /dev/null and b/testdata/ksym-el.elf differ diff --git a/testdata/ksym.c b/testdata/ksym.c new file mode 100644 index 000000000..0f70d93bc --- /dev/null +++ b/testdata/ksym.c @@ -0,0 +1,37 @@ +#include "common.h" + +char __license[] __section("license") = "Dual MIT/GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 2); + __type(key, uint32_t); + __type(value, uint64_t); +} array_map __section(".maps"); + +extern void socket_file_ops __ksym; +extern void tty_fops __ksym __weak; + +__section("socket") int ksym_test() { + uint32_t i; + uint64_t val; + + i = 0; + val = (uint64_t)&socket_file_ops; + bpf_map_update_elem(&array_map, &i, &val, 0); + + i = 1; + val = (uint64_t)&tty_fops; + bpf_map_update_elem(&array_map, &i, &val, 0); + + return 0; +} + +extern void non_existing_symbol __ksym __weak; + +__section("socket") int ksym_missing_test() { + if (&non_existing_symbol == 0) { + return 1; + } + return 0; +}