Repository: cilium/ebpf
Pull request #503 (2021-11-24 14:03:27)

btf: rename FindType to TypeByName, add TypesByName and TypeByID methods
This patch adds two querying primitives to btf.Spec:

- `TypeByID()` looks up a `btf.Type` by its ID in the btf.Spec
- `TypesByName()` returns a list of `btf.Type`s that satisfy the given essential name. This can be used to obtain all CO-RE flavors of a struct, to perform a quick lookup without retaining the result, or if the kind or signature of the type you're looking for is not known up front.

In order to simplify the internal graph bookkeeping, harmonize on a single `btf.Type` interface all BTF types must implement instead of the `Type` and `NamedType` interfaces. This facilitates implementing an intuitive API for querying the graph.

Further document the purpose of `essentialName()`.

ti-mo: > I don't understand the idea behind getting rid of NamedType. Is the problem that []Type and []NamedType are not assignable to each other?

Yes, the fact that they're both discrete types and that they are/were stored in separate arrays makes it slightly awkward to access them. Also, I don't think it made sense from a data modelling perspective:

- whether a type can have a name or not is a property of a type, not a defining characteristic
- named and unnamed types are not semantically different; even named types can be anonymous, so any code that operates on named types must account for `TypeName() == ""`, which kind of defeats the purpose of having a separate namedType interface to begin with
- the querying interface (here e.g. `TypeByID`) would need a variant for both kinds of types, which will just cause confusion and might be more painful to implement/maintain than necessary

> The code I've written for bpf2go is nicer with NamedType vs. having to check the function return: https://github.com/lmb/ebpf/blob/7f73e6889d42ae315d66e8027140850d71c49d0d/cmd/bpf2go/output.go#L258-L292

Sorry, lacking context, not clear what this code is doing. (comments might help)

---

Now I've had time to mull it over, I think I've also changed my mind about `TypeName()` returning `string, bool`:
```
λ  ebpf tb/multiprog ✓ grep -R "\.TypeName()"
internal/cmd/gentypes/main.go:		if n, _ := cs.TypeName(); !ok || n != "" {
internal/btf/core.go:		localTypeName, _ := localType.TypeName()
internal/btf/ext_info.go:	name, _ := typ.TypeName()
internal/btf/btf.go:		if name, _ := typ.TypeName(); name != "" {
internal/btf/btf.go:		if n, _ := typ.TypeName(); n != name {
internal/btf/types.go:		typeName, _ := typ.TypeName()
internal/btf/format_test.go:				n, _ := t.TypeName()
```

Currently, there's not a call site that actually uses the bool, and I think this is just more expressive:
```
if typ.TypeName == "" && typ.IsNamed() {
  return errors.New("some_error")
}
```

WDYT?
lmb: `IsNamed() bool` is confusing I think because it could mean both `name != ""` or `name can be != ""`. Let's go with `TypeName() string` for now and skip the dedicated function. We can figure out what to do once we have the need.

Patch:
diff --git a/elf_reader.go b/elf_reader.go
index b0fccd9..f5f62ac 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -580,7 +580,7 @@ func (ec *elfCode) loadBTFMaps(maps map[string]*MapSpec) error {
 
 		// Each section must appear as a DataSec in the ELF's BTF blob.
 		var ds *btf.Datasec
-		if err := ec.btf.FindType(sec.Name, &ds); err != nil {
+		if err := ec.btf.TypeByName(sec.Name, &ds); err != nil {
 			return fmt.Errorf("cannot find section '%s' in BTF: %w", sec.Name, err)
 		}
 
@@ -952,7 +952,7 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error {
 		}
 
 		var datasec *btf.Datasec
-		if err := ec.btf.FindType(sec.Name, &datasec); err != nil {
+		if err := ec.btf.TypeByName(sec.Name, &datasec); err != nil {
 			return fmt.Errorf("data section %s: can't get BTF: %w", sec.Name, err)
 		}
 
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 38c5791..8c36f8b 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -29,16 +29,29 @@ var (
 // ID represents the unique ID of a BTF object.
 type ID uint32
 
+// essentialName represents the name of a BTF type stripped of any flavor
+// suffixes after a ___ delimiter.
+type essentialname string
+
 // Spec represents decoded BTF.
 type Spec struct {
-	rawTypes   []rawType
-	strings    stringTable
-	types      []Type
-	namedTypes map[string][]NamedType
-	funcInfos  map[string]extInfo
-	lineInfos  map[string]extInfo
-	coreRelos  map[string]coreRelos
-	byteOrder  binary.ByteOrder
+	// Data from .BTF.
+	rawTypes []rawType
+	strings  stringTable
+
+	// Inflated Types.
+	types []Type
+
+	// Types indexed by essential name.
+	// Includes all struct flavors and types with the same name.
+	namedTypes map[essentialname][]Type
+
+	// Data from .BTF.ext.
+	funcInfos map[string]extInfo
+	lineInfos map[string]extInfo
+	coreRelos map[string]coreRelos
+
+	byteOrder binary.ByteOrder
 }
 
 type btfHeader struct {
@@ -373,11 +386,12 @@ func fixupDatasec(rawTypes []rawType, rawStrings stringTable, sectionSizes map[s
 // Copy creates a copy of Spec.
 func (s *Spec) Copy() *Spec {
 	types, _ := copyTypes(s.types, nil)
-	namedTypes := make(map[string][]NamedType)
+
+	namedTypes := make(map[essentialname][]Type)
 	for _, typ := range types {
-		if named, ok := typ.(NamedType); ok {
-			name := essentialName(named.TypeName())
-			namedTypes[name] = append(namedTypes[name], named)
+		if name := typ.TypeName(); name != "" {
+			en := essentialName(name)
+			namedTypes[en] = append(namedTypes[en], typ)
 		}
 	}
 
@@ -484,14 +498,73 @@ func (s *Spec) Program(name string, length uint64) (*Program, error) {
 	return &Program{s, length, funcInfos, lineInfos, relos}, nil
 }
 
-// FindType searches for a type with a specific name.
+// TypeByID returns the BTF Type with the given type ID.
+//
+// Returns an error wrapping ErrNotFound if a Type with the given ID
+// does not exist in the Spec.
+func (s *Spec) TypeByID(id TypeID) (Type, error) {
+	if int(id) > len(s.types) {
+		return nil, fmt.Errorf("type ID %d: %w", id, ErrNotFound)
+	}
+	return s.types[id], nil
+}
+
+// AnyTypesByName returns a list of BTF Types with the given name. If name
+// contains a struct flavor suffix (e.g. 'thread_struct___v46'), an exact match
+// will be performed. Otherwise, returns all types that carry the given name,
+// including all struct flavors.
+//
+// Multiple Types of the same name can exist in case multiple 'struct flavors'
+// are described in BTF info. These are used to deal with changes in kernel
+// data structures.
+//
+// If the BTF blob describes multiple compilation units like vmlinux, multiple
+// Types with the same name and kind can exist, but might not describe the same
+// data structure.
+//
+// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
+func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
+	en := essentialName(name)
+
+	// Named types are indexed by essential name.
+	types, ok := s.namedTypes[en]
+	if !ok {
+		return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
+	}
+
+	if string(en) == name {
+		// Return a copy to prevent changes to namedTypes.
+		cpy := make([]Type, len(types))
+		copy(cpy, types)
+		return cpy, nil
+	}
+
+	// If name contains a flavor suffix, require an exact match.
+	var filtered []Type
+	for _, t := range types {
+		if t.TypeName() == name {
+			filtered = append(filtered, t)
+		}
+	}
+
+	if len(filtered) == 0 {
+		return nil, fmt.Errorf("type flavor %s: %w", name, ErrNotFound)
+	}
+
+	return filtered, nil
+}
+
+// TypeByName searches for a Type with a specific name. Since multiple
+// Types with the same name can exist, the parameter typ is taken to
+// narrow down the search in case of a clash.
 //
-// Called T a type that satisfies Type, typ must be a non-nil **T.
-// On success, the address of the found type will be copied in typ.
+// typ must be a non-nil pointer to an implementation of a Type.
+// On success, the address of the found Type will be copied to typ.
 //
 // Returns an error wrapping ErrNotFound if no matching
-// type exists in spec.
-func (s *Spec) FindType(name string, typ interface{}) error {
+// Type exists in the Spec. If multiple candidates are found,
+// an error is returned.
+func (s *Spec) TypeByName(name string, typ interface{}) error {
 	typValue := reflect.ValueOf(typ)
 	if typValue.Kind() != reflect.Ptr {
 		return fmt.Errorf("%T is not a pointer", typ)
@@ -507,13 +580,19 @@ func (s *Spec) FindType(name string, typ interface{}) error {
 		return fmt.Errorf("%T does not satisfy Type interface", typ)
 	}
 
+	types, err := s.AnyTypesByName(name)
+	if err != nil {
+		return err
+	}
+
 	var candidate Type
-	for _, typ := range s.namedTypes[essentialName(name)] {
+	for _, typ := range types {
 		if reflect.TypeOf(typ) != wanted {
 			continue
 		}
 
-		// Match against the full name, not just the essential one.
+		// Match against the full name, not just the essential one
+		// in case the type being looked up is a struct flavor.
 		if typ.TypeName() != name {
 			continue
 		}
diff --git a/internal/btf/core.go b/internal/btf/core.go
index 3c5c3fa..98d7456 100644
--- a/internal/btf/core.go
+++ b/internal/btf/core.go
@@ -234,13 +234,13 @@ func coreRelocate(local, target *Spec, relos coreRelos) (COREFixups, error) {
 		}
 
 		localType := local.types[id]
-		named, ok := localType.(NamedType)
-		if !ok || named.TypeName() == "" {
+		localTypeName := localType.TypeName()
+		if localTypeName == "" {
 			return nil, fmt.Errorf("relocate unnamed or anonymous type %s: %w", localType, ErrNotSupported)
 		}
 
 		relos := relosByID[id]
-		targets := target.namedTypes[essentialName(named.TypeName())]
+		targets := target.namedTypes[essentialName(localTypeName)]
 		fixups, err := coreCalculateFixups(localType, targets, relos)
 		if err != nil {
 			return nil, fmt.Errorf("relocate %s: %w", localType, err)
@@ -262,7 +262,7 @@ var errImpossibleRelocation = errors.New("impossible relocation")
 //
 // The best target is determined by scoring: the less poisoning we have to do
 // the better the target is.
-func coreCalculateFixups(local Type, targets []NamedType, relos coreRelos) ([]COREFixup, error) {
+func coreCalculateFixups(local Type, targets []Type, relos coreRelos) ([]COREFixup, error) {
 	localID := local.ID()
 	local, err := copyType(local, skipQualifiersAndTypedefs)
 	if err != nil {
diff --git a/internal/btf/types.go b/internal/btf/types.go
index 46c0814..c294170 100644
--- a/internal/btf/types.go
+++ b/internal/btf/types.go
@@ -18,9 +18,12 @@ func (tid TypeID) ID() TypeID {
 
 // Type represents a type described by BTF.
 type Type interface {
+	// The type ID of the Type within this BTF spec.
 	ID() TypeID
 
-	String() string
+	// Name of the type, empty for anonymous types and types that cannot
+	// carry a name, like Void and Pointer.
+	TypeName() string
 
 	// Make a copy of the type, without copying Type members.
 	copy() Type
@@ -28,37 +31,32 @@ type Type interface {
 	// Enumerate all nested Types. Repeated calls must visit nested
 	// types in the same order.
 	walk(*typeDeque)
-}
-
-// NamedType is a type with a name.
-type NamedType interface {
-	Type
 
-	// Name of the type, empty for anonymous types.
-	TypeName() string
+	String() string
 }
 
 var (
-	_ NamedType = (*Int)(nil)
-	_ NamedType = (*Struct)(nil)
-	_ NamedType = (*Union)(nil)
-	_ NamedType = (*Enum)(nil)
-	_ NamedType = (*Fwd)(nil)
-	_ NamedType = (*Func)(nil)
-	_ NamedType = (*Typedef)(nil)
-	_ NamedType = (*Var)(nil)
-	_ NamedType = (*Datasec)(nil)
-	_ NamedType = (*Float)(nil)
+	_ Type = (*Int)(nil)
+	_ Type = (*Struct)(nil)
+	_ Type = (*Union)(nil)
+	_ Type = (*Enum)(nil)
+	_ Type = (*Fwd)(nil)
+	_ Type = (*Func)(nil)
+	_ Type = (*Typedef)(nil)
+	_ Type = (*Var)(nil)
+	_ Type = (*Datasec)(nil)
+	_ Type = (*Float)(nil)
 )
 
 // Void is the unit type of BTF.
 type Void struct{}
 
-func (v *Void) ID() TypeID      { return 0 }
-func (v *Void) String() string  { return "void#0" }
-func (v *Void) size() uint32    { return 0 }
-func (v *Void) copy() Type      { return (*Void)(nil) }
-func (v *Void) walk(*typeDeque) {}
+func (v *Void) ID() TypeID       { return 0 }
+func (v *Void) String() string   { return "void#0" }
+func (v *Void) TypeName() string { return "" }
+func (v *Void) size() uint32     { return 0 }
+func (v *Void) copy() Type       { return (*Void)(nil) }
+func (v *Void) walk(*typeDeque)  {}
 
 type IntEncoding byte
 
@@ -83,6 +81,7 @@ func (ie IntEncoding) IsBool() bool {
 // Int is an integer of a given length.
 type Int struct {
 	TypeID
+
 	Name string
 
 	// The size of the integer in bytes.
@@ -141,6 +140,7 @@ func (p *Pointer) String() string {
 	return fmt.Sprintf("pointer#%d[target=#%d]", p.TypeID, p.Target.ID())
 }
 
+func (p *Pointer) TypeName() string    { return "" }
 func (p *Pointer) size() uint32        { return 8 }
 func (p *Pointer) walk(tdq *typeDeque) { tdq.push(&p.Target) }
 func (p *Pointer) copy() Type {
@@ -159,6 +159,8 @@ func (arr *Array) String() string {
 	return fmt.Sprintf("array#%d[type=#%d n=%d]", arr.TypeID, arr.Type.ID(), arr.Nelems)
 }
 
+func (arr *Array) TypeName() string { return "" }
+
 func (arr *Array) walk(tdq *typeDeque) { tdq.push(&arr.Type) }
 func (arr *Array) copy() Type {
 	cpy := *arr
@@ -355,6 +357,8 @@ func (v *Volatile) String() string {
 	return fmt.Sprintf("volatile#%d[#%d]", v.TypeID, v.Type.ID())
 }
 
+func (v *Volatile) TypeName() string { return "" }
+
 func (v *Volatile) qualify() Type       { return v.Type }
 func (v *Volatile) walk(tdq *typeDeque) { tdq.push(&v.Type) }
 func (v *Volatile) copy() Type {
@@ -372,6 +376,8 @@ func (c *Const) String() string {
 	return fmt.Sprintf("const#%d[#%d]", c.TypeID, c.Type.ID())
 }
 
+func (c *Const) TypeName() string { return "" }
+
 func (c *Const) qualify() Type       { return c.Type }
 func (c *Const) walk(tdq *typeDeque) { tdq.push(&c.Type) }
 func (c *Const) copy() Type {
@@ -389,6 +395,8 @@ func (r *Restrict) String() string {
 	return fmt.Sprintf("restrict#%d[#%d]", r.TypeID, r.Type.ID())
 }
 
+func (r *Restrict) TypeName() string { return "" }
+
 func (r *Restrict) qualify() Type       { return r.Type }
 func (r *Restrict) walk(tdq *typeDeque) { tdq.push(&r.Type) }
 func (r *Restrict) copy() Type {
@@ -433,6 +441,8 @@ func (fp *FuncProto) String() string {
 	return s.String()
 }
 
+func (fp *FuncProto) TypeName() string { return "" }
+
 func (fp *FuncProto) walk(tdq *typeDeque) {
 	tdq.push(&fp.Return)
 	for i := range fp.Params {
@@ -753,7 +763,7 @@ func (dq *typeDeque) all() []*Type {
 // Returns a map of named types (so, where NameOff is non-zero) and a slice of types
 // indexed by TypeID. Since BTF ignores compilation units, multiple types may share
 // the same name. A Type may form a cyclic graph by pointing at itself.
-func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type, namedTypes map[string][]NamedType, err error) {
+func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type, namedTypes map[essentialname][]Type, err error) {
 	type fixupDef struct {
 		id           TypeID
 		expectedKind btfKind
@@ -792,7 +802,7 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 
 	types = make([]Type, 0, len(rawTypes))
 	types = append(types, (*Void)(nil))
-	namedTypes = make(map[string][]NamedType)
+	namedTypes = make(map[essentialname][]Type)
 
 	for i, raw := range rawTypes {
 		var (
@@ -936,10 +946,8 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 
 		types = append(types, typ)
 
-		if named, ok := typ.(NamedType); ok {
-			if name := essentialName(named.TypeName()); name != "" {
-				namedTypes[name] = append(namedTypes[name], named)
-			}
+		if name := essentialName(typ.TypeName()); name != "" {
+			namedTypes[name] = append(namedTypes[name], typ)
 		}
 	}
 
@@ -966,10 +974,15 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 }
 
 // essentialName returns name without a ___ suffix.
-func essentialName(name string) string {
+//
+// CO-RE has the concept of 'struct flavors', which are used to deal with
+// changes in kernel data structures. Anything after three underscores
+// in a type name is ignored for the purpose of finding a candidate type
+// in the kernel's BTF.
+func essentialName(name string) essentialname {
 	lastIdx := strings.LastIndex(name, "___")
 	if lastIdx > 0 {
-		return name[:lastIdx]
+		return essentialname(name[:lastIdx])
 	}
-	return name
+	return essentialname(name)
 }
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index ec3b102..ee71060 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -132,7 +132,7 @@ import (
 		fmt.Println("enum", o.goType)
 
 		var t *btf.Enum
-		if err := spec.FindType(o.cType, &t); err != nil {
+		if err := spec.TypeByName(o.cType, &t); err != nil {
 			return nil, err
 		}
 
@@ -192,7 +192,7 @@ import (
 		fmt.Println("struct", s.goType)
 
 		var t *btf.Struct
-		if err := spec.FindType(s.cType, &t); err != nil {
+		if err := spec.TypeByName(s.cType, &t); err != nil {
 			return nil, err
 		}
 
@@ -414,7 +414,7 @@ func outputPatchedStruct(gf *btf.GoFormatter, w *bytes.Buffer, id string, s *btf
 
 func splitBpfAttr(spec *btf.Spec) (map[string]*btf.Struct, error) {
 	var bpfAttr *btf.Union
-	if err := spec.FindType("bpf_attr", &bpfAttr); err != nil {
+	if err := spec.TypeByName("bpf_attr", &bpfAttr); err != nil {
 		return nil, err
 	}
 
@@ -564,7 +564,7 @@ func flattenAnon(s *btf.Struct) error {
 		m := &s.Members[i]
 
 		cs, ok := m.Type.(*btf.Struct)
-		if !ok || cs.Name != "" {
+		if !ok || cs.TypeName() != "" {
 			continue
 		}
 
diff --git a/link/tracing.go b/link/tracing.go
index 581a455..5913592 100644
--- a/link/tracing.go
+++ b/link/tracing.go
@@ -56,7 +56,7 @@ func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (
 		defer btfHandle.Close()
 
 		var function *btf.Func
-		if err := btfHandle.Spec().FindType(name, &function); err != nil {
+		if err := btfHandle.Spec().TypeByName(name, &function); err != nil {
 			return nil, err
 		}
 
diff --git a/prog.go b/prog.go
index 5abf578..b0dbcb1 100644
--- a/prog.go
+++ b/prog.go
@@ -761,11 +761,11 @@ func resolveBTFType(spec *btf.Spec, name string, progType ProgramType, attachTyp
 
 	if isBTFTypeFunc {
 		var targetFunc *btf.Func
-		err = spec.FindType(typeName, &targetFunc)
+		err = spec.TypeByName(typeName, &targetFunc)
 		target = targetFunc
 	} else {
 		var targetTypedef *btf.Typedef
-		err = spec.FindType(typeName, &targetTypedef)
+		err = spec.TypeByName(typeName, &targetTypedef)
 		target = targetTypedef
 	}
 


Test patch:
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index 3bbdb2b..026229a 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -14,7 +14,7 @@ import (
 	"github.com/cilium/ebpf/internal/testutils"
 )
 
-func parseVmLinux(tb testing.TB) (*Spec, error) {
+func parseVMLinuxBTF(tb testing.TB) (*Spec, error) {
 	fh, err := os.Open("testdata/vmlinux-btf.gz")
 	if err != nil {
 		tb.Fatal(err)
@@ -34,13 +34,52 @@ func parseVmLinux(tb testing.TB) (*Spec, error) {
 	return spec, nil
 }
 
-func TestFindType(t *testing.T) {
-	spec, err := parseVmLinux(t)
+func parseELFBTF(tb testing.TB, file string) *Spec {
+	fh, err := os.Open(file)
+	if err != nil {
+		tb.Fatal(err)
+	}
+	defer fh.Close()
+
+	spec, err := LoadSpecFromReader(fh)
+	if err != nil {
+		tb.Fatal("Can't load BTF:", err)
+	}
+
+	return spec
+}
+
+func TestAnyTypesByName(t *testing.T) {
+	testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
+		spec := parseELFBTF(t, file)
+
+		types, err := spec.AnyTypesByName("ambiguous")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(types) != 2 {
+			t.Fatalf("expected to receive exactly 2 types from querying ambiguous type, got: %v", types)
+		}
+
+		types, err = spec.AnyTypesByName("ambiguous___flavour")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(types) != 1 {
+			t.Fatalf("expected to receive exactly 1 type from querying ambiguous flavour, got: %v", types)
+		}
+	})
+}
+
+func TestTypeByName(t *testing.T) {
+	spec, err := parseVMLinuxBTF(t)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	// spec.FindType MUST fail if typ is not a non-nil **T, where T satisfies btf.Type.
+	// spec.TypeByName MUST fail if typ is a nil btf.Type.
 	i := 0
 	p := &i
 	for _, typ := range []interface{}{
@@ -54,33 +93,56 @@ func TestFindType(t *testing.T) {
 		p,
 		&p,
 	} {
-		if err := spec.FindType("iphdr", typ); err == nil {
-			t.Fatalf("FindType does not fail with type %T", typ)
+		if err := spec.TypeByName("iphdr", typ); err == nil {
+			t.Fatalf("TypeByName does not fail with type %T", typ)
 		}
 	}
 
-	// spec.FindType MUST return the same address for multiple calls with the same type name.
+	// spec.TypeByName MUST return the same address for multiple calls with the same type name.
 	var iphdr1, iphdr2 *Struct
-	if err := spec.FindType("iphdr", &iphdr1); err != nil {
+	if err := spec.TypeByName("iphdr", &iphdr1); err != nil {
 		t.Fatal(err)
 	}
-	if err := spec.FindType("iphdr", &iphdr2); err != nil {
+	if err := spec.TypeByName("iphdr", &iphdr2); err != nil {
 		t.Fatal(err)
 	}
 
 	if iphdr1 != iphdr2 {
-		t.Fatal("multiple FindType calls for `iphdr` name do not return the same addresses")
+		t.Fatal("multiple TypeByName calls for `iphdr` name do not return the same addresses")
 	}
 }
 
+func TestTypeByNameAmbiguous(t *testing.T) {
+	testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
+		spec := parseELFBTF(t, file)
+
+		var typ *Struct
+		if err := spec.TypeByName("ambiguous", &typ); err != nil {
+			t.Fatal(err)
+		}
+
+		if name := typ.TypeName(); name != "ambiguous" {
+			t.Fatal("expected type name 'ambiguous', got:", name)
+		}
+
+		if err := spec.TypeByName("ambiguous___flavour", &typ); err != nil {
+			t.Fatal(err)
+		}
+
+		if name := typ.TypeName(); name != "ambiguous___flavour" {
+			t.Fatal("expected type name 'ambiguous___flavour', got:", name)
+		}
+	})
+}
+
 func TestParseVmlinux(t *testing.T) {
-	spec, err := parseVmLinux(t)
+	spec, err := parseVMLinuxBTF(t)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	var iphdr *Struct
-	err = spec.FindType("iphdr", &iphdr)
+	err = spec.TypeByName("iphdr", &iphdr)
 	if err != nil {
 		t.Fatalf("unable to find `iphdr` struct: %s", err)
 	}
@@ -154,20 +216,7 @@ func TestParseCurrentKernelBTF(t *testing.T) {
 
 func TestLoadSpecFromElf(t *testing.T) {
 	testutils.Files(t, testutils.Glob(t, "../../testdata/loader-e*.elf"), func(t *testing.T, file string) {
-		fh, err := os.Open(file)
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer fh.Close()
-
-		spec, err := LoadSpecFromReader(fh)
-		if err != nil {
-			t.Fatal("Can't load BTF:", err)
-		}
-
-		if spec == nil {
-			t.Error("No BTF found in ELF")
-		}
+		spec := parseELFBTF(t, file)
 
 		if sec, err := spec.Program("xdp", 1); err != nil {
 			t.Error("Can't get BTF for the xdp section:", err)
@@ -181,18 +230,26 @@ func TestLoadSpecFromElf(t *testing.T) {
 			t.Error("Missing BTF for the socket section")
 		}
 
+		vt, err := spec.TypeByID(0)
+		if err != nil {
+			t.Error("Can't retrieve void type by ID:", err)
+		}
+		if _, ok := vt.(*Void); !ok {
+			t.Errorf("Expected Void for type id 0, but got: %T", vt)
+		}
+
 		var bpfMapDef *Struct
-		if err := spec.FindType("bpf_map_def", &bpfMapDef); err != nil {
+		if err := spec.TypeByName("bpf_map_def", &bpfMapDef); err != nil {
 			t.Error("Can't find bpf_map_def:", err)
 		}
 
 		var tmp *Void
-		if err := spec.FindType("totally_bogus_type", &tmp); !errors.Is(err, ErrNotFound) {
-			t.Error("FindType doesn't return ErrNotFound:", err)
+		if err := spec.TypeByName("totally_bogus_type", &tmp); !errors.Is(err, ErrNotFound) {
+			t.Error("TypeByName doesn't return ErrNotFound:", err)
 		}
 
 		var fn *Func
-		if err := spec.FindType("global_fn", &fn); err != nil {
+		if err := spec.TypeByName("global_fn", &fn); err != nil {
 			t.Error("Can't find global_fn():", err)
 		} else {
 			if fn.Linkage != GlobalFunc {
@@ -201,7 +258,7 @@ func TestLoadSpecFromElf(t *testing.T) {
 		}
 
 		var v *Var
-		if err := spec.FindType("key3", &v); err != nil {
+		if err := spec.TypeByName("key3", &v); err != nil {
 			t.Error("Cant find key3:", err)
 		} else {
 			if v.Linkage != GlobalVar {
@@ -236,16 +293,7 @@ func TestLoadKernelSpec(t *testing.T) {
 }
 
 func TestSpecCopy(t *testing.T) {
-	fh, err := os.Open("../../testdata/loader-el.elf")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer fh.Close()
-
-	spec, err := LoadSpecFromReader(fh)
-	if err != nil {
-		t.Fatal("Can't load BTF:", err)
-	}
+	spec := parseELFBTF(t, "../../testdata/loader-el.elf")
 
 	if len(spec.types) < 1 {
 		t.Fatal("Not enough types")
@@ -274,14 +322,14 @@ func TestHaveFuncLinkage(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveFuncLinkage)
 }
 
-func ExampleSpec_FindType() {
+func ExampleSpec_TypeByName() {
 	// Acquire a Spec via one of its constructors.
 	spec := new(Spec)
 
 	// Declare a variable of the desired type
 	var foo *Struct
 
-	if err := spec.FindType("foo", &foo); err != nil {
+	if err := spec.TypeByName("foo", &foo); err != nil {
 		// There is no struct with name foo, or there
 		// are multiple possibilities.
 	}
diff --git a/internal/btf/format_test.go b/internal/btf/format_test.go
index 6360182..08e0bd8 100644
--- a/internal/btf/format_test.go
+++ b/internal/btf/format_test.go
@@ -151,16 +151,16 @@ func TestGoTypeDeclarationNamed(t *testing.T) {
 
 	tests := []struct {
 		typ    Type
-		named  []NamedType
+		named  []Type
 		output string
 	}{
-		{e1, []NamedType{e1}, "type t int32"},
-		{s1, []NamedType{e1, s1}, "type t struct { frob E1; }"},
-		{s2, []NamedType{e1}, "type t struct { frood struct { frob E1; }; }"},
-		{s2, []NamedType{e1, s1}, "type t struct { frood S1; }"},
+		{e1, []Type{e1}, "type t int32"},
+		{s1, []Type{e1, s1}, "type t struct { frob E1; }"},
+		{s2, []Type{e1}, "type t struct { frood struct { frob E1; }; }"},
+		{s2, []Type{e1, s1}, "type t struct { frood S1; }"},
 		{td, nil, "type t int32"},
-		{td, []NamedType{td}, "type t int32"},
-		{arr, []NamedType{td}, "type t [1]TD"},
+		{td, []Type{td}, "type t int32"},
+		{arr, []Type{td}, "type t [1]TD"},
 	}
 
 	for _, test := range tests {