From 372fae5a2ed68d1167405f7ed0c2ee6575e885c4 Mon Sep 17 00:00:00 2001 From: Patrick Pichler Date: Thu, 3 Oct 2024 17:39:51 +0200 Subject: [PATCH] feat: add support for untyped extern __ksym variables 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 --- Makefile | 1 + btf/btf.go | 16 +++++--- elf_reader.go | 37 ++++++++++++++++++- elf_reader_test.go | 57 +++++++++++++++++++++++++++++ internal/kallsyms/kallsyms.go | 53 +++++++++++++++++++++++++++ internal/kallsyms/kallsyms_test.go | 47 ++++++++++++++++++------ linker.go | 36 ++++++++++++++++++ prog.go | 4 ++ testdata/ksym-eb.elf | Bin 0 -> 3160 bytes testdata/ksym-el.elf | Bin 0 -> 3160 bytes testdata/ksym.c | 37 +++++++++++++++++++ 11 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 testdata/ksym-eb.elf create mode 100644 testdata/ksym-el.elf create mode 100644 testdata/ksym.c 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 0000000000000000000000000000000000000000..5345cb7591c2573a04bcfe2940a1eaa4c62d99d9 GIT binary patch literal 3160 zcmbtWPiS0482?_{v>V&BskRb9^bsT^5H?9IDimQGn%H85Py^a3OkQ?h(r4bjUG}{# z8>^U$5^o|3Uc^*-Q1Kvw#9Mmo(PNJ}7KD;pJrvgOH}9L5ak9~aAAB?4@B2IRy_wnl zWOjb8ug{}`JbKKsr6?>c3=C^jk#Uf`;UO&-!|glWJ%DY|SMPM60PlQ&k?8mmQMcQD zvh$&abiAqYo||W9G`H`(n$O#neEu|_`Xs++^1Jb79j^XA^XB>KYniw6`M^HU4dp!& zd*Oe%9mM`yR~IMWSeTa-|2qBooo;pBSMnZk>%qSws4tH^!jmT-%A96f11sK6hklc60IJb;Z#@`hk7eg=jptT z*A$Fu+#_$p!Jj+$TP|i&pG6Q8N}tnGE=l=E0u=TENUcUBl)^U#?cx7faln_f15t0>dm0V4UVH~ zn53a>z1Rzxs+2Sm6?URDLnZFSawDcPlBQJV4W?u=5Xl|oZ>s5e9>G5|-`AS_JhoZJ z9R%S>?K0yP1jB~=1p0{iOQHqbNnh}9MDxH?f{p!(;2okx!&$z7uKzCEFQXwn z=muY6?7=3V!9@%`=mjhPuGrDPnlHeaKI8m&mu!-!Myv3Cm>!_-@HH5H#@Y`nL1Lzy9-=_n2Sv$(BRoqzTS(0m7 zygj{dbR6HRtDshE@uO-z3*|IxWx+C)`NHd27Dn?kGt=rkum1nj7o5U9|B|(|D#9;L z8FRj%hHS8VFDh{8LFT-`WX=fWL*l(57%eJU@)XNBgP@8YivfQjG%fIm-#oKV6OX)` z%x3+@&MJ_HIFNn+fQ>)oMGL#}OK@QF^|(Kn`q12Ew)5?Kk++e9Bi~y}h<5+B7$3HO zvwpLG(-2c1`!D|IuEEGts literal 0 HcmV?d00001 diff --git a/testdata/ksym-el.elf b/testdata/ksym-el.elf new file mode 100644 index 0000000000000000000000000000000000000000..b9a7523d338f48862a0e928c14c007c490e67b06 GIT binary patch literal 3160 zcmbtWO=w(I6h8SeO>B}TwUt;zA3;I_VUpA$LJ_8+i7iGLYM{0XFE5jqbjZw`VcwgP zP{nMNxD!!uBL*q{(S-<-?$TwKU3QseK#=ToQHbAn?|CoJo7gTq%z5X0=l9+-@7|eD zX69zw+u9_hHu*~qQEC+#>N(VlMlDJoB8QydJLmQf=-yU)`P>tOcYZKQa-vJ*$-%+F z&X0;ui0lKqb#@l5eZLnOV@znLI}W{2FHw zTo}2$WKlQ9EyGqn634H2LgSu?d{g7PA&XWQ)b8}4&w|rszN2~(@;aF7*Z_Y3X6^%U z00#A+#3Rjv+Q)UTT3c@)q>{0}ia>mNPr(@2Br|o?OiP#<48qSnGk@87`cFfr+E5JD z%xJ^GpF8+lU=(FbIl~8tA!m(d^%rycT{!VE@x5yIAb-i(Ur@W*UnNS!_b*+$cIk%y z-qo8k{``&7jPFYej@QroiL|1)vJ!<$9wTXe<3iqAtw$@8tT#fb1#5m7CCy40%WAkT zw}Wacl%UxR*8N)0kkxp-<|kpCNL(+khKWpsi;d+8T9$$&n4rHtFaLq z?k`uWplz;8WV|DhqhsD3`B=)gf+hxBt(3zk z4t49|J3s`{eRF!JIjXnD-cG zOd{s$w!oy+&l!@4yTMyjK)je(?_Lfw|9%cL{*hwFvfo|~^FAI2ljx`G0s$z+S1io% z`wqV4;57$tI`|t0|K#BN4u0t1#|}PuYOuGje?SUJxRywv8CDC^^Rop4bArwNbmT0X zK)toH6YE?{dQ7vs=lBy{t=_Kr!O~I_mul`>D8xxK2^OV*18<}*Oy;Jir~LD}iT@$K zK(6_5a%!w}Tf?u8UBC9sN=!aH5!O|-;>eTlL!S7aJS~5~X{gJ}O7J}K{VQe`Rrm)2 zRI);PA-T^qe?aYDy4__s+uH z`E4Qgxc!%o**{Hb|8)P({~SbEdX0x1v}Z;3bo;aBas9Wn{{zkB{2Ax==Q;0~JE%i! jfHPXqKrNHm-C6M-NPo|=ASeU+Lhh@PdHL^uNbUasMn6V2 literal 0 HcmV?d00001 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; +}