Repository: cilium/ebpf
Pull request #1300 (2024-01-08 20:41:05)

Add CO-RE support for kernel modules
re #705 

- Adds `KernelModule string` to `ProgramSpec`. This is automatically populated by `tracefs.KernelModule()` for kprobe/fentry programs, by parsing `available_filter_functions`. 
- Adds `KernelModuleTypes *btf.Spec` to `ProgramOptions`, so user can provide their own kmod BTF.

The usage of a `*Spec` in parts of CO-RE had to be abstracted to avoid copying/merging of `[]Type` slices.

This does work (tested with `nf_conntrack` kmod and `__nf_conntrack_hash_insert` kprobe), but I imagine you have thoughts about how to adapt this approach.

brycekahle: It looks like some examples are failing because tracefs is not mounted. I could make `KernelModule` always return `""` in face of an error, but I'm not sure that is a good idea.
brycekahle: > For the implementation, I was thinking that instead of introducing `mergedSpec` we'd operate on `[]*Spec`. `ProgramOptions.KernelTypes` would become `[]*Spec` as well. `CORERelocate` would iterate the `[]*Spec` and try to find a valid set of relocations for each target.

Since the type IDs of kernel modules conflict with each other, you are only ever going to be dealing with vmlinux plus one optional kmod BTF. libbpf makes this same assumption.
brycekahle: > * You propose that CO-RE for a probe attached to a kprobe in a kernel module will look at `set of vmlinux types` + `set of containing kmod types`. What about a kprobe on a vmlinux function which needs access to kmod types?

With the changes you requested, you could accomplish this by setting `KernelModule` in the program spec (and supplying `KernelModuleTypes` in program options, if you want). That field doesn't affect the attachment in any way, just the BTF load and CO-RE relocation.
brycekahle: > * do we have to deal with module reload / unload?

I don't see a way to know if a module has been unloaded and then loaded with different contents, other than maybe comparing sizes from `/proc/modules`. This would be unlikely for a kernel module in the source tree right?


brycekahle: > * Merge `FlushKernelModuleCache` into `btf.FlushKernelSpec()`. There should be a single function to clean up cached info, and in a way this is all related to btf stuff anyways

`FlushKernelModuleCache` is flushing the cache of `kernel function` -> `module name`, not BTF related.
brycekahle: > * If `KernelTypes` is nil, the library tries to find the appropriate BTF to use, via means of parsing kallsyms / available_filter_functions and doing a lookup of ProgramSpec.AttachTo. Most of the time this will be just vmlinux. Some of the time this will be vmlinux + kmod via LoadKernelModuleSpec.

This moves the loading of kernel/kmod BTF up from `CORERelocate` (which is a public func) to `newProgramWithOptions` because it needs access to `ProgramSpec.AttachTo`. That seems like a breaking change if folks expect `CORERelocate` to load kernel BTF for them.
brycekahle: I made some changes that are close to what you want, but had to fill in some gaps. I would like to see `KernelModule` function exported.
brycekahle: @lmb can you take another look?
lmb: > Ugh. Since we need different `KernelTypes []*btf.Spec` per-program, it kind of excludes us from using `NewCollection/NewCollectionWithOptions` because that only takes one set of options for all the programs.

Ah, that is annoying :( Why do you want to avoid multiple NewCollection calls?

> One idea would be `KernelTypes map[string]*btf.Spec` with either a sentinel or empty string value for the base kernel BTF.

That could work. I'd probably use `vmlinux` as a sentinel. OTOH we could also keep the current `KernelTypes` and add `KernelModuleTypes map[string]*Spec`, which means we don't break that part of the API.

> * both the user and library would have to do the `func->kmod` lookup for each program.

This is because you don't want to load all kmod by default? What if you only parsed kmod for all loaded modules instead?

> * the map only makes sense for a collection, not for a single program, so it would be in `CollectionOptions`

Hmm, I don't follow, sorry. We could still stick the map in ProgramOptions I think? Not sure how we would make use of it when loading `ebpf.Map` for example.

> Another idea is a callback function `func(kmodName string) (*btf.Spec, error)`

I'm a bit wary of callbacks. If we add this it begs the question whether we'd use this for vmlinux as well? Does it deprecate `KernelTypes`? Gut feeling only, but I'd prefer to not use a callback.
brycekahle: > Why do you want to avoid multiple NewCollection calls?

We have a bunch of eBPF programs in the same object file. That is currently a single collection. Anything that needs kernel modules would need to be extracted out and dealt with separately, probably as individual program loads. That makes shared map handling way more complicated. It isn't impossible, but is a smell about the API maybe being wrong.
brycekahle: > This is because you don't want to load all kmod by default? What if you only parsed kmod for all loaded modules instead?

If you mean modules already loaded in the kernel, that can still be 100 or more. That is a lot of CPU and memory used for probably no reason.

brycekahle: > Hmm, I don't follow, sorry. We could still stick the map in ProgramOptions I think?

We could put it in `ProgramOptions` but it doesn't make a lot of sense, because a single eBPF program can have max 2 BTFs, the kernel and a kernel module.
brycekahle: > your current implementation means that a type defined in a kernel module can "shadow" a vmlinux type if it's better according to our scoring.

How would that happen? kmod BTF is generated usingn the vmlinux BTF so that it doesn't include anything already in vmlinux BTF.
brycekahle: > A single eBPF program could use several structs defined in different kernel modules, so that could be more than 2 BTFs.

Can you give a real example? It is worth noting that I don't think libbpf can accomplish this at the moment.
brycekahle: > so it can't be known at program-loading time which module owns the functions to attach to

We have code to look up the module (if any) using `/proc/kallsyms` based on the function they are trying to attach to. Is that sufficient for this use case?
brycekahle: > I'd prefer if that worked, but I understand we have to balance complexity and usefulness from real use cases.

I'd want to make sure it is actually possible before trying to solve it. I don't think we could automagically know which additional modules to load BTF for, so the user would have to provide/specify that somehow. We can only automagically know the module for the function you are trying to attach to.
brycekahle: > No, I don't have a real example with structs. I'd prefer if that worked, but I understand we have to balance complexity and usefulness from real use cases.

I thought about this some more. With kernel module dependencies if A depends on B, it appears that A's BTF contains all the necessary types it is using from B.
brycekahle: @lmb updated using a map for the module types. 
lmb: Sorry for the long radio silence. I've been busy hashing out cilium/cilium work.

@brycekahle 

> We could put it in `ProgramOptions` but it doesn't make a lot of sense, because a single eBPF program can have max 2 BTFs, the kernel and a kernel module.

I don't see the problem to be honest. `ProgramOptions` is already not scoped to a specific program.

> How would that happen? kmod BTF is generated using the vmlinux BTF so that it doesn't include anything already in vmlinux BTF.

By using distinct types with the same name in multiple compilation units. Try grepping for `ring_buffer` in the kernel sources.

Which ring_buffer do we prefer? The kmod one? Or the kernel? Which one of the kernel ones?

> It is worth noting that I don't think libbpf can accomplish [relocating types from multiple kmod] at the moment.

Does that mean we can't do better? ;P

> I don't think we could automagically know which additional modules to load BTF for, so the user would have to provide/specify that somehow.

We can't figure out which modules we need up front, but we could do something like the following:

- Define an ordering in which we visit kmod (one reason I want []*Spec as argument to CORERelocate: it gives us an order)
- Keep iterating specs until we've found a target for all relocations (logic for this will be tricky)

These are the correct semantics to me (rather than just vmlinux + kmod) but I understand that the immediate need is simpler. I still want to keep the door open for this later on though.

> With kernel module dependencies if A depends on B, it appears that A's BTF contains all the necessary types it is using from B.

That's interesting! Can you show me an example or tell me how to reproduce that?

@alban 

> I'd prefer if [merged kmod btf] worked, but I understand we have to balance complexity and usefulness from real use cases.

Thanks for giving your view point and giving me confirmation that I'm not completely off xD
brycekahle: > Finally, the bit I'm still missing is how you're going to use this with external kmod BTF?
> 
> * You don't want to parse all loaded modules, since that might be in the 100s and could still be too slow.
> * There is no feedback mechanism for you to figure out which modules the lib wants.

You can see how I've implemented it at the moment: https://github.com/DataDog/ebpf-manager/commit/8b77c9a990a93b77762fd8a3c7deef2f5209eee1
brycekahle: > If targets is nil it can just return an error, which is what it was doing before.

This is not what it was doing. It tries to auto-load the kernel spec: 

https://github.com/cilium/ebpf/blob/b3432fb1eca90dae072e53d428f248aecec65d18/btf/core.go#L173-L179
lmb: > This is not what it was doing. It tries to auto-load the kernel spec:

I meant this: https://github.com/cilium/ebpf/blob/4267cbc577606bee83f21f5232c18154848292e7/btf/core.go#L169-L170

I'm looking at merging this but realised that you merged main into your branch. Can you please undo that and rebase + squash on top of main?
brycekahle: > Can you please undo that and rebase + squash on top of main?

Done. Let me know if there are any other changes you'd like me to make.

Patch:
diff --git a/btf/btf.go b/btf/btf.go
index 1268a41..3d92467 100644
--- a/btf/btf.go
+++ b/btf/btf.go
@@ -118,6 +118,10 @@ func (mt *mutableTypes) copy() mutableTypes {
 		make(map[Type]TypeID, len(mt.copiedTypeIDs)),
 	}
 
+	// Prevent concurrent modification of mt.copiedTypeIDs.
+	mt.mu.RLock()
+	defer mt.mu.RUnlock()
+
 	copies := make(map[Type]Type, len(mt.copies))
 	for orig, copy := range mt.copies {
 		// NB: We make a copy of copy, not orig, so that changes to mutable types
@@ -396,107 +400,6 @@ func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentia
 	return typeIDs, typesByName
 }
 
-// LoadKernelSpec returns the current kernel's BTF information.
-//
-// Defaults to /sys/kernel/btf/vmlinux and falls back to scanning the file system
-// for vmlinux ELFs. Returns an error wrapping ErrNotSupported if BTF is not enabled.
-func LoadKernelSpec() (*Spec, error) {
-	spec, _, err := kernelSpec()
-	if err != nil {
-		return nil, err
-	}
-	return spec.Copy(), nil
-}
-
-var kernelBTF struct {
-	sync.RWMutex
-	spec *Spec
-	// True if the spec was read from an ELF instead of raw BTF in /sys.
-	fallback bool
-}
-
-// FlushKernelSpec removes any cached kernel type information.
-func FlushKernelSpec() {
-	kernelBTF.Lock()
-	defer kernelBTF.Unlock()
-
-	kernelBTF.spec, kernelBTF.fallback = nil, false
-}
-
-func kernelSpec() (*Spec, bool, error) {
-	kernelBTF.RLock()
-	spec, fallback := kernelBTF.spec, kernelBTF.fallback
-	kernelBTF.RUnlock()
-
-	if spec == nil {
-		kernelBTF.Lock()
-		defer kernelBTF.Unlock()
-
-		spec, fallback = kernelBTF.spec, kernelBTF.fallback
-	}
-
-	if spec != nil {
-		return spec, fallback, nil
-	}
-
-	spec, fallback, err := loadKernelSpec()
-	if err != nil {
-		return nil, false, err
-	}
-
-	kernelBTF.spec, kernelBTF.fallback = spec, fallback
-	return spec, fallback, nil
-}
-
-func loadKernelSpec() (_ *Spec, fallback bool, _ error) {
-	fh, err := os.Open("/sys/kernel/btf/vmlinux")
-	if err == nil {
-		defer fh.Close()
-
-		spec, err := loadRawSpec(fh, internal.NativeEndian, nil)
-		return spec, false, err
-	}
-
-	file, err := findVMLinux()
-	if err != nil {
-		return nil, false, err
-	}
-	defer file.Close()
-
-	spec, err := LoadSpecFromReader(file)
-	return spec, true, err
-}
-
-// findVMLinux scans multiple well-known paths for vmlinux kernel images.
-func findVMLinux() (*os.File, error) {
-	release, err := internal.KernelRelease()
-	if err != nil {
-		return nil, err
-	}
-
-	// use same list of locations as libbpf
-	// https://github.com/libbpf/libbpf/blob/9a3a42608dbe3731256a5682a125ac1e23bced8f/src/btf.c#L3114-L3122
-	locations := []string{
-		"/boot/vmlinux-%s",
-		"/lib/modules/%s/vmlinux-%[1]s",
-		"/lib/modules/%s/build/vmlinux",
-		"/usr/lib/modules/%s/kernel/vmlinux",
-		"/usr/lib/debug/boot/vmlinux-%s",
-		"/usr/lib/debug/boot/vmlinux-%s.debug",
-		"/usr/lib/debug/lib/modules/%s/vmlinux",
-	}
-
-	for _, loc := range locations {
-		file, err := os.Open(fmt.Sprintf(loc, release))
-		if errors.Is(err, os.ErrNotExist) {
-			continue
-		}
-		return file, err
-	}
-
-	return nil, fmt.Errorf("no BTF found for kernel version %s: %w", release, internal.ErrNotSupported)
-}
-
 func guessRawBTFByteOrder(r io.ReaderAt) binary.ByteOrder {
 	buf := new(bufio.Reader)
 	for _, bo := range []binary.ByteOrder{
diff --git a/btf/core.go b/btf/core.go
index e26383b..b743ffe 100644
--- a/btf/core.go
+++ b/btf/core.go
@@ -164,6 +164,33 @@ func (k coreKind) String() string {
 	}
 }
 
+type mergedSpec []*Spec
+
+func (s mergedSpec) TypeByID(id TypeID) (Type, error) {
+	for _, sp := range s {
+		t, err := sp.TypeByID(id)
+		if err != nil {
+			if errors.Is(err, ErrNotFound) {
+				continue
+			}
+			return nil, err
+		}
+		return t, nil
+	}
+	return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s[0].imm.firstTypeID, ErrNotFound)
+}
+
+func (s mergedSpec) NamedTypes(name essentialName) []TypeID {
+	var typeIDs []TypeID
+	for _, sp := range s {
+		namedTypes := sp.imm.namedTypes[name]
+		if len(namedTypes) > 0 {
+			typeIDs = append(typeIDs, namedTypes...)
+		}
+	}
+	return typeIDs
+}
+
 // CORERelocate calculates changes needed to adjust eBPF instructions for differences
 // in types.
 //
@@ -177,17 +204,16 @@ func (k coreKind) String() string {
 //
 // Fixups are returned in the order of relos, e.g. fixup[i] is the solution
 // for relos[i].
-func CORERelocate(relos []*CORERelocation, target *Spec, bo binary.ByteOrder, resolveLocalTypeID func(Type) (TypeID, error)) ([]COREFixup, error) {
-	if target == nil {
-		var err error
-		target, _, err = kernelSpec()
-		if err != nil {
-			return nil, fmt.Errorf("load kernel spec: %w", err)
-		}
+func CORERelocate(relos []*CORERelocation, targets []*Spec, bo binary.ByteOrder, resolveLocalTypeID func(Type) (TypeID, error)) ([]COREFixup, error) {
+	if len(targets) == 0 {
+		// Explicitly check for nil here since the argument used to be optional.
+		return nil, fmt.Errorf("targets must be provided")
 	}
 
-	if bo != target.imm.byteOrder {
-		return nil, fmt.Errorf("can't relocate %s against %s", bo, target.imm.byteOrder)
+	for _, target := range targets {
+		if bo != target.imm.byteOrder {
+			return nil, fmt.Errorf("can't relocate %s against %s", bo, target.imm.byteOrder)
+		}
 	}
 
 	type reloGroup struct {
@@ -229,14 +255,15 @@ func CORERelocate(relos []*CORERelocation, target *Spec, bo binary.ByteOrder, re
 		group.indices = append(group.indices, i)
 	}
 
+	mergeTarget := mergedSpec(targets)
 	for localType, group := range relosByType {
 		localTypeName := localType.TypeName()
 		if localTypeName == "" {
 			return nil, fmt.Errorf("relocate unnamed or anonymous type %s: %w", localType, ErrNotSupported)
 		}
 
-		targets := target.imm.namedTypes[newEssentialName(localTypeName)]
-		fixups, err := coreCalculateFixups(group.relos, target, targets, bo)
+		targets := mergeTarget.NamedTypes(newEssentialName(localTypeName))
+		fixups, err := coreCalculateFixups(group.relos, &mergeTarget, targets, bo)
 		if err != nil {
 			return nil, fmt.Errorf("relocate %s: %w", localType, err)
 		}
@@ -259,7 +286,7 @@ var errIncompatibleTypes = errors.New("incompatible types")
 //
 // The best target is determined by scoring: the less poisoning we have to do
 // the better the target is.
-func coreCalculateFixups(relos []*CORERelocation, targetSpec *Spec, targets []TypeID, bo binary.ByteOrder) ([]COREFixup, error) {
+func coreCalculateFixups(relos []*CORERelocation, targetSpec *mergedSpec, targets []TypeID, bo binary.ByteOrder) ([]COREFixup, error) {
 	bestScore := len(relos)
 	var bestFixups []COREFixup
 	for _, targetID := range targets {
diff --git a/btf/kernel.go b/btf/kernel.go
new file mode 100644
index 0000000..7421a72
--- /dev/null
+++ b/btf/kernel.go
@@ -0,0 +1,158 @@
+package btf
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/kallsyms"
+)
+
+var kernelBTF = struct {
+	sync.RWMutex
+	kernel  *Spec
+	modules map[string]*Spec
+}{
+	modules: make(map[string]*Spec),
+}
+
+// FlushKernelSpec removes any cached kernel type information.
+func FlushKernelSpec() {
+	kallsyms.FlushKernelModuleCache()
+
+	kernelBTF.Lock()
+	defer kernelBTF.Unlock()
+
+	kernelBTF.kernel = nil
+	kernelBTF.modules = make(map[string]*Spec)
+}
+
+// LoadKernelSpec returns the current kernel's BTF information.
+//
+// Defaults to /sys/kernel/btf/vmlinux and falls back to scanning the file system
+// for vmlinux ELFs. Returns an error wrapping ErrNotSupported if BTF is not enabled.
+func LoadKernelSpec() (*Spec, error) {
+	kernelBTF.RLock()
+	spec := kernelBTF.kernel
+	kernelBTF.RUnlock()
+
+	if spec == nil {
+		kernelBTF.Lock()
+		defer kernelBTF.Unlock()
+
+		spec = kernelBTF.kernel
+	}
+
+	if spec != nil {
+		return spec.Copy(), nil
+	}
+
+	spec, _, err := loadKernelSpec()
+	if err != nil {
+		return nil, err
+	}
+
+	kernelBTF.kernel = spec
+	return spec.Copy(), nil
+}
+
+// LoadKernelModuleSpec returns the BTF information for the named kernel module.
+//
+// Defaults to /sys/kernel/btf/<module>.
+// Returns an error wrapping ErrNotSupported if BTF is not enabled.
+func LoadKernelModuleSpec(module string) (*Spec, error) {
+	kernelBTF.RLock()
+	spec := kernelBTF.modules[module]
+	kernelBTF.RUnlock()
+
+	if spec != nil {
+		return spec.Copy(), nil
+	}
+
+	base, err := LoadKernelSpec()
+	if err != nil {
+		return nil, fmt.Errorf("load kernel spec: %w", err)
+	}
+
+	kernelBTF.Lock()
+	defer kernelBTF.Unlock()
+
+	if spec = kernelBTF.modules[module]; spec != nil {
+		return spec.Copy(), nil
+	}
+
+	spec, err = loadKernelModuleSpec(module, base)
+	if err != nil {
+		return nil, err
+	}
+
+	kernelBTF.modules[module] = spec
+	return spec.Copy(), nil
+}
+
+func loadKernelSpec() (_ *Spec, fallback bool, _ error) {
+	fh, err := os.Open("/sys/kernel/btf/vmlinux")
+	if err == nil {
+		defer fh.Close()
+
+		spec, err := loadRawSpec(fh, internal.NativeEndian, nil)
+		return spec, false, err
+	}
+
+	file, err := findVMLinux()
+	if err != nil {
+		return nil, false, err
+	}
+	defer file.Close()
+
+	spec, err := LoadSpecFromReader(file)
+	return spec, true, err
+}
+
+func loadKernelModuleSpec(module string, base *Spec) (*Spec, error) {
+	dir, file := filepath.Split(module)
+	if dir != "" || filepath.Ext(file) != "" {
+		return nil, fmt.Errorf("invalid module name %q", module)
+	}
+
+	fh, err := os.Open(filepath.Join("/sys/kernel/btf", module))
+	if err != nil {
+		return nil, err
+	}
+	defer fh.Close()
+
+	return loadRawSpec(fh, internal.NativeEndian, base)
+}
+
+// findVMLinux scans multiple well-known paths for vmlinux kernel images.
+func findVMLinux() (*os.File, error) {
+	release, err := internal.KernelRelease()
+	if err != nil {
+		return nil, err
+	}
+
+	// use same list of locations as libbpf
+	// https://github.com/libbpf/libbpf/blob/9a3a42608dbe3731256a5682a125ac1e23bced8f/src/btf.c#L3114-L3122
+	locations := []string{
+		"/boot/vmlinux-%s",
+		"/lib/modules/%s/vmlinux-%[1]s",
+		"/lib/modules/%s/build/vmlinux",
+		"/usr/lib/modules/%s/kernel/vmlinux",
+		"/usr/lib/debug/boot/vmlinux-%s",
+		"/usr/lib/debug/boot/vmlinux-%s.debug",
+		"/usr/lib/debug/lib/modules/%s/vmlinux",
+	}
+
+	for _, loc := range locations {
+		file, err := os.Open(fmt.Sprintf(loc, release))
+		if errors.Is(err, os.ErrNotExist) {
+			continue
+		}
+		return file, err
+	}
+
+	return nil, fmt.Errorf("no BTF found for kernel version %s: %w", release, internal.ErrNotSupported)
+}
diff --git a/internal/kallsyms/kallsyms.go b/internal/kallsyms/kallsyms.go
new file mode 100644
index 0000000..776c7a1
--- /dev/null
+++ b/internal/kallsyms/kallsyms.go
@@ -0,0 +1,74 @@
+package kallsyms
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"os"
+	"sync"
+)
+
+var kernelModules struct {
+	sync.RWMutex
+	// function to kernel module mapping
+	kmods map[string]string
+}
+
+// KernelModule returns the kernel module, if any, a probe-able function is contained in.
+func KernelModule(fn string) (string, error) {
+	kernelModules.RLock()
+	kmods := kernelModules.kmods
+	kernelModules.RUnlock()
+
+	if kmods == nil {
+		kernelModules.Lock()
+		defer kernelModules.Unlock()
+		kmods = kernelModules.kmods
+	}
+
+	if kmods != nil {
+		return kmods[fn], nil
+	}
+
+	f, err := os.Open("/proc/kallsyms")
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+	kmods, err = loadKernelModuleMapping(f)
+	if err != nil {
+		return "", err
+	}
+
+	kernelModules.kmods = kmods
+	return kmods[fn], nil
+}
+
+// FlushKernelModuleCache removes any cached information about function to kernel module mapping.
+func FlushKernelModuleCache() {
+	kernelModules.Lock()
+	defer kernelModules.Unlock()
+
+	kernelModules.kmods = nil
+}
+
+func loadKernelModuleMapping(f io.Reader) (map[string]string, error) {
+	mods := make(map[string]string)
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		fields := bytes.Fields(scanner.Bytes())
+		if len(fields) < 4 {
+			continue
+		}
+		switch string(fields[1]) {
+		case "t", "T":
+			mods[string(fields[2])] = string(bytes.Trim(fields[3], "[]"))
+		default:
+			continue
+		}
+	}
+	if scanner.Err() != nil {
+		return nil, scanner.Err()
+	}
+	return mods, nil
+}
diff --git a/linker.go b/linker.go
index b93ca91..2b0ac2c 100644
--- a/linker.go
+++ b/linker.go
@@ -119,7 +119,7 @@ func hasFunctionReferences(insns asm.Instructions) bool {
 //
 // Passing a nil target will relocate against the running kernel. insns are
 // modified in place.
-func applyRelocations(insns asm.Instructions, target *btf.Spec, bo binary.ByteOrder, b *btf.Builder) error {
+func applyRelocations(insns asm.Instructions, targets []*btf.Spec, kmodName string, bo binary.ByteOrder, b *btf.Builder) error {
 	var relos []*btf.CORERelocation
 	var reloInsns []*asm.Instruction
 	iter := insns.Iterate()
@@ -138,7 +138,23 @@ func applyRelocations(insns asm.Instructions, target *btf.Spec, bo binary.ByteOr
 		bo = internal.NativeEndian
 	}
 
-	fixups, err := btf.CORERelocate(relos, target, bo, b.Add)
+	if len(targets) == 0 {
+		kernelTarget, err := btf.LoadKernelSpec()
+		if err != nil {
+			return fmt.Errorf("load kernel spec: %w", err)
+		}
+		targets = append(targets, kernelTarget)
+
+		if kmodName != "" {
+			kmodTarget, err := btf.LoadKernelModuleSpec(kmodName)
+			if err != nil {
+				return fmt.Errorf("load kernel module spec: %w", err)
+			}
+			targets = append(targets, kmodTarget)
+		}
+	}
+
+	fixups, err := btf.CORERelocate(relos, targets, bo, b.Add)
 	if err != nil {
 		return err
 	}
diff --git a/prog.go b/prog.go
index 611e087..d479a38 100644
--- a/prog.go
+++ b/prog.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"
 	"github.com/cilium/ebpf/internal/sysenc"
 	"github.com/cilium/ebpf/internal/unix"
@@ -80,6 +81,14 @@ type ProgramOptions struct {
 	// (containers) or where it is in a non-standard location. Defaults to
 	// use the kernel BTF from a well-known location if nil.
 	KernelTypes *btf.Spec
+
+	// Type information used for CO-RE relocations of kernel modules,
+	// indexed by module name.
+	//
+	// This is useful in environments where the kernel BTF is not available
+	// (containers) or where it is in a non-standard location. Defaults to
+	// use the kernel module BTF from a well-known location if nil.
+	KernelModuleTypes map[string]*btf.Spec
 }
 
 // ProgramSpec defines a Program.
@@ -148,6 +157,28 @@ func (ps *ProgramSpec) Tag() (string, error) {
 	return ps.Instructions.Tag(internal.NativeEndian)
 }
 
+// KernelModule returns the kernel module, if any, the AttachTo function is contained in.
+func (ps *ProgramSpec) KernelModule() (string, error) {
+	if ps.AttachTo == "" {
+		return "", nil
+	}
+
+	switch ps.Type {
+	default:
+		return "", nil
+	case Tracing:
+		switch ps.AttachType {
+		default:
+			return "", nil
+		case AttachTraceFEntry:
+		case AttachTraceFExit:
+		}
+		fallthrough
+	case Kprobe:
+		return kallsyms.KernelModule(ps.AttachTo)
+	}
+}
+
 // VerifierError is returned by [NewProgram] and [NewProgramWithOptions] if a
 // program is rejected by the verifier.
 //
@@ -242,8 +273,23 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
 	insns := make(asm.Instructions, len(spec.Instructions))
 	copy(insns, spec.Instructions)
 
+	kmodName, err := spec.KernelModule()
+	if err != nil {
+		return nil, fmt.Errorf("kernel module search: %w", err)
+	}
+
+	var targets []*btf.Spec
+	if opts.KernelTypes != nil {
+		targets = append(targets, opts.KernelTypes)
+	}
+	if kmodName != "" && opts.KernelModuleTypes != nil {
+		if modBTF, ok := opts.KernelModuleTypes[kmodName]; ok {
+			targets = append(targets, modBTF)
+		}
+	}
+
 	var b btf.Builder
-	if err := applyRelocations(insns, opts.KernelTypes, spec.ByteOrder, &b); err != nil {
+	if err := applyRelocations(insns, targets, kmodName, spec.ByteOrder, &b); err != nil {
 		return nil, fmt.Errorf("apply CO-RE relocations: %w", err)
 	}
 


Test patch:
diff --git a/btf/btf_test.go b/btf/btf_test.go
index 02e6b5f..ab0464e 100644
--- a/btf/btf_test.go
+++ b/btf/btf_test.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"os"
 	"runtime"
 	"sync"
@@ -23,16 +24,15 @@ func vmlinuxSpec(tb testing.TB) *Spec {
 
 	// /sys/kernel/btf was introduced in 341dfcf8d78e ("btf: expose BTF info
 	// through sysfs"), which shipped in Linux 5.4.
-	testutils.SkipOnOldKernel(tb, "5.4", "vmlinux BTF in sysfs")
+	if _, err := os.Stat("/sys/kernel/btf/vmlinux"); errors.Is(err, fs.ErrNotExist) {
+		tb.Skip("No /sys/kernel/btf/vmlinux")
+	}
 
-	spec, fallback, err := kernelSpec()
+	spec, err := LoadKernelSpec()
 	if err != nil {
 		tb.Fatal(err)
 	}
-	if fallback {
-		tb.Fatal("/sys/kernel/btf/vmlinux is not available")
-	}
-	return spec.Copy()
+	return spec
 }
 
 type specAndRawBTF struct {
@@ -320,17 +320,6 @@ func TestVerifierError(t *testing.T) {
 	}
 }
 
-func TestLoadKernelSpec(t *testing.T) {
-	if _, err := os.Stat("/sys/kernel/btf/vmlinux"); os.IsNotExist(err) {
-		t.Skip("/sys/kernel/btf/vmlinux not present")
-	}
-
-	_, err := LoadKernelSpec()
-	if err != nil {
-		t.Fatal("Can't load kernel spec:", err)
-	}
-}
-
 func TestGuessBTFByteOrder(t *testing.T) {
 	bo := guessRawBTFByteOrder(vmlinuxTestdataReader(t))
 	if bo != binary.LittleEndian {
@@ -542,12 +531,16 @@ func TestSpecConcurrentAccess(t *testing.T) {
 		go func() {
 			defer wg.Done()
 
-			cond.Add(1)
+			n := cond.Add(1)
 			for cond.Load() != int64(maxprocs) {
 				// Spin to increase the chances of a race.
 			}
 
-			_, _ = spec.AnyTypeByName("gov_update_cpu_data")
+			if n%2 == 0 {
+				_, _ = spec.AnyTypeByName("gov_update_cpu_data")
+			} else {
+				_ = spec.Copy()
+			}
 		}()
 
 		// Try to get the Goroutines scheduled and spinning.
diff --git a/btf/core_test.go b/btf/core_test.go
index c409309..5ceda03 100644
--- a/btf/core_test.go
+++ b/btf/core_test.go
@@ -592,7 +592,7 @@ func TestCORERelocation(t *testing.T) {
 					relos = append(relos, reloInfo.relo)
 				}
 
-				fixups, err := CORERelocate(relos, spec, spec.imm.byteOrder, spec.TypeID)
+				fixups, err := CORERelocate(relos, []*Spec{spec}, spec.imm.byteOrder, spec.TypeID)
 				if want := errs[name]; want != nil {
 					if !errors.Is(err, want) {
 						t.Fatal("Expected", want, "got", err)
@@ -744,7 +744,7 @@ func BenchmarkCORESkBuff(b *testing.B) {
 			b.ReportAllocs()
 
 			for i := 0; i < b.N; i++ {
-				_, err = CORERelocate([]*CORERelocation{relo}, spec, spec.imm.byteOrder, spec.TypeID)
+				_, err = CORERelocate([]*CORERelocation{relo}, []*Spec{spec}, spec.imm.byteOrder, spec.TypeID)
 				if err != nil {
 					b.Fatal(err)
 				}
diff --git a/btf/kernel_test.go b/btf/kernel_test.go
new file mode 100644
index 0000000..0558cb7
--- /dev/null
+++ b/btf/kernel_test.go
@@ -0,0 +1,28 @@
+package btf
+
+import (
+	"os"
+	"testing"
+
+	"github.com/go-quicktest/qt"
+)
+
+func TestLoadKernelSpec(t *testing.T) {
+	if _, err := os.Stat("/sys/kernel/btf/vmlinux"); os.IsNotExist(err) {
+		t.Skip("/sys/kernel/btf/vmlinux not present")
+	}
+
+	_, err := LoadKernelSpec()
+	if err != nil {
+		t.Fatal("Can't load kernel spec:", err)
+	}
+}
+
+func TestLoadKernelModuleSpec(t *testing.T) {
+	if _, err := os.Stat("/sys/kernel/btf/btf_testmod"); os.IsNotExist(err) {
+		t.Skip("/sys/kernel/btf/btf_testmod not present")
+	}
+
+	_, err := LoadKernelModuleSpec("btf_testmod")
+	qt.Assert(t, qt.IsNil(err))
+}
diff --git a/internal/kallsyms/kallsyms_test.go b/internal/kallsyms/kallsyms_test.go
new file mode 100644
index 0000000..4e245a0
--- /dev/null
+++ b/internal/kallsyms/kallsyms_test.go
@@ -0,0 +1,39 @@
+package kallsyms
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/go-quicktest/qt"
+)
+
+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))
+
+	// present and in module
+	kmod := kmods["hid_generic_probe"]
+	if kmod != "hid_generic" {
+		t.Errorf("expected %q got %q", "hid_generic", kmod)
+	}
+
+	// present but not kernel module
+	kmod = kmods["tcp_connect"]
+	if kmod != "" {
+		t.Errorf("expected %q got %q", "", kmod)
+	}
+
+	qt.Assert(t, qt.Equals(kmods["nft_counter_seq"], ""))
+}