Repository: cilium/ebpf
Pull request #1771 (2025-05-01 14:30:37)

build(deps): bump the docs group in /docs with 3 updates
Bumps the docs group in /docs with 3 updates: [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions), [mkdocs-material](https://github.com/squidfunk/mkdocs-material) and [mkdocs-git-authors-plugin](https://github.com/timvink/mkdocs-git-authors-plugin).

Updates `pymdown-extensions` from 10.14.3 to 10.15
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/facelessuser/pymdown-extensions/releases">pymdown-extensions's releases</a>.</em></p>
<blockquote>
<h2>10.15.0</h2>
<ul>
<li><strong>NEW</strong>: SuperFences: Add <code>relaxed_headers</code> option which can tolerate bad content in the fenced code header. When
enabled, code blocks with bad content in the header will likely still convert into code blocks, often respecting
the specified language.</li>
<li><strong>NEW</strong>: Add type hints to the Blocks interface and a few additional files.</li>
<li><strong>FIX</strong>: Blocks: Fix some corner cases of nested blocks with lists.</li>
<li><strong>FIX</strong>: Tab and Tabbed: Fix a case where tabs could fail if <code>combine_header_slug</code> was enabled and there was no
header.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/d32c6b3fb088d04faf5dfd32de6c1a4714b1ae84"><code>d32c6b3</code></a> Update changelog</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/9f5c3caec4547682af5de03faab5b38a4b3d5f1f"><code>9f5c3ca</code></a> Finish typing the Blocks interface and type additional referenced files (<a href="https://redirect.github.com/facelessuser/pymdown-extensions/issues/2652">#2652</a>)</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/84a087b4024d3d8a290fbcd78dcf86268208e8b1"><code>84a087b</code></a> add type annotations to block.py (<a href="https://redirect.github.com/facelessuser/pymdown-extensions/issues/2634">#2634</a>)</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/70e475cbf95dc1de2bfed018bf51531051d57e93"><code>70e475c</code></a> Docs: update JS deps</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/0eb20475b372083747dba0e38f4910fd45135e81"><code>0eb2047</code></a> Provide option for more relaxed fenced code headers (<a href="https://redirect.github.com/facelessuser/pymdown-extensions/issues/2649">#2649</a>)</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/677a863a807ec0d3ba64562f9921f72a192c425a"><code>677a863</code></a> Fix some corner cases with Blocks and nested lists (<a href="https://redirect.github.com/facelessuser/pymdown-extensions/issues/2651">#2651</a>)</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/f655468301b9372c1619a40c2f4b08e4d936b5d7"><code>f655468</code></a> Fix a corner case with <code>combine_header_slug</code> and Tab and Tabbed</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/3e8a097fb8e8cfc4463ab9161da51e9659e79050"><code>3e8a097</code></a> Only show &quot;steps&quot; as direct list items</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/f8d7f629bf4baa730e6c50dd62c95c45a7dc9eeb"><code>f8d7f62</code></a> Docs: Mermaid Pie chart is now considered viable and update version used</li>
<li><a href="https://github.com/facelessuser/pymdown-extensions/commit/48cda23b6f55bb3583e455287eedef36e758786f"><code>48cda23</code></a> Add a note about snippet syntax and XML files</li>
<li>Additional commits viewable in <a href="https://github.com/facelessuser/pymdown-extensions/compare/10.14.3...10.15">compare view</a></li>
</ul>
</details>
<br />

Updates `mkdocs-material` from 9.6.11 to 9.6.12
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/squidfunk/mkdocs-material/releases">mkdocs-material's releases</a>.</em></p>
<blockquote>
<h2>mkdocs-material-9.6.12</h2>
<ul>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8158">#8158</a>: Flip footnote back reference icon for right-to-left languages</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG">mkdocs-material's changelog</a>.</em></p>
<blockquote>
<p>mkdocs-material-9.6.12 (2025-04-17)</p>
<ul>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8158">#8158</a>: Flip footnote back reference icon for right-to-left languages</li>
</ul>
<p>mkdocs-material-9.6.11 (2025-04-01)</p>
<ul>
<li>Updated Docker image to latest Alpine Linux</li>
<li>Bump required Jinja version to 3.1</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8133">#8133</a>: Jinja filter <code>items</code> not available (9.6.10 regression)</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8128">#8128</a>: Search plugin not entirely disabled via enabled setting</li>
</ul>
<p>mkdocs-material-9.6.10 (2025-03-30)</p>
<p>This version is a pure refactoring release, and does not contain new features
or bug fixes. It strives to improve the compatibility of our templates with
alternative Jinja-like template engines that we're currently exploring,
including minijinja.</p>
<p>Additionally, it replaces several instances of Python function invocations
with idiomatic use of template filters. All instances where variables have
been mutated inside templates have been replaced. Most changes have been made
in partials, and only a few in blocks, and all of them are fully backward
compatible, so no changes to overrides are necessary.</p>
<p>Note that this release does not replace the Jinja template engine with
minijinja. However, our templates are now 99% compatible with minijinja,
which means we can explore alternative Jinja-compatible implementations.
Additionally, immutability and removal of almost all Python function
invocations means much more idiomatic templating.</p>
<p>mkdocs-material-9.6.9 (2025-03-17)</p>
<ul>
<li>Updated Serbo-Croatian translations</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8086">#8086</a>: Custom SVG icons containing hashes break rendering</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8067">#8067</a>: Drawer has gap on right side in Firefox on some OSs</li>
</ul>
<p>mkdocs-material-9.6.8+insiders-4.53.16 (2025-03-13)</p>
<ul>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8019">#8019</a>: Tooltips have precedence over instant previews</li>
</ul>
<p>mkdocs-material-9.6.8 (2025-03-13)</p>
<ul>
<li>Added Welsh translations</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8076">#8076</a>: Privacy plugin crashes if HTTP download fails</li>
</ul>
<p>mkdocs-material-9.6.7 (2025-03-03)</p>
<ul>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8056">#8056</a>: Error in backrefs implementation (9.6.6 regression)</li>
<li>Fixed <a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8054">#8054</a>: Unescaped quotes in ARIA labels of table of contents</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/4c0004e16b1d51511fbd3c8537394069f004ecfe"><code>4c0004e</code></a> Prepare 9.6.12 release</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/45584df9537aeb462ef4101f917aa60ed7e6dce6"><code>45584df</code></a> Updated dependencies</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/475fa2268521ef1de72eb1481f7a0a2a8357ce01"><code>475fa22</code></a> WIP</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/6a1600f9c793e0d8be94db75414b23f0253ec9d8"><code>6a1600f</code></a> Documentation</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/efe44f6c08fe2641140887be92719b7566707148"><code>efe44f6</code></a> Fixed footnote back reference icon orientation for RTL languages</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/5d99a9fcf7761c7bf727c5f96cc289d3c18b7435"><code>5d99a9f</code></a> Documentation (<a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8155">#8155</a>)</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/99fefe8dfbc9352086adebb2ca92a1c706237dd9"><code>99fefe8</code></a> Documentation (<a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8157">#8157</a>)</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/f6480d791cf6c1b340fe75875350cc37df116845"><code>f6480d7</code></a> Update README</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/9baaad9238290f27544de4909b0a01803f1111de"><code>9baaad9</code></a> Updated publishing guide (<a href="https://redirect.github.com/squidfunk/mkdocs-material/issues/8150">#8150</a>)</li>
<li><a href="https://github.com/squidfunk/mkdocs-material/commit/cbe74214d6f2c958a2f194b9e72876af4379f2fe"><code>cbe7421</code></a> Updated premium sponsors</li>
<li>Additional commits viewable in <a href="https://github.com/squidfunk/mkdocs-material/compare/9.6.11...9.6.12">compare view</a></li>
</ul>
</details>
<br />

Updates `mkdocs-git-authors-plugin` from 0.9.4 to 0.9.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/timvink/mkdocs-git-authors-plugin/releases">mkdocs-git-authors-plugin's releases</a>.</em></p>
<blockquote>
<h2>git-authors v0.9.5</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix for <a href="https://redirect.github.com/timvink/mkdocs-git-authors-plugin/issues/90">#90</a> | Sort page authors by page contribution by <a href="https://github.com/TimiMakkonen"><code>@​TimiMakkonen</code></a> in <a href="https://redirect.github.com/timvink/mkdocs-git-authors-plugin/pull/107">timvink/mkdocs-git-authors-plugin#107</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/TimiMakkonen"><code>@​TimiMakkonen</code></a> made their first contribution in <a href="https://redirect.github.com/timvink/mkdocs-git-authors-plugin/pull/107">timvink/mkdocs-git-authors-plugin#107</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.4...v0.9.5">https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.4...v0.9.5</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/timvink/mkdocs-git-authors-plugin/commit/cb1b452e18b97ee1ceb2dd660ff54d13af8e65e0"><code>cb1b452</code></a> bump to 0.9.5</li>
<li><a href="https://github.com/timvink/mkdocs-git-authors-plugin/commit/9277f8c3eba56b0b2c81720c73e7aada873a746d"><code>9277f8c</code></a> Issue <a href="https://redirect.github.com/timvink/mkdocs-git-authors-plugin/issues/90">#90</a> | Sort page authors by page contribution, not site contribution</li>
<li>See full diff in <a href="https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.4...v0.9.5">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>


Patch:
diff --git a/btf/btf.go b/btf/btf.go
index 4bf0f3d..8112b6c 100644
--- a/btf/btf.go
+++ b/btf/btf.go
@@ -12,6 +12,7 @@ import (
 	"math"
 	"os"
 	"reflect"
+	"slices"
 
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/sys"
@@ -243,6 +244,10 @@ func guessRawBTFByteOrder(r io.ReaderAt) binary.ByteOrder {
 // fixupDatasec attempts to patch up missing info in Datasecs and its members by
 // supplementing them with information from the ELF headers and symbol table.
 func (elf *elfData) fixupDatasec(typ Type) error {
+	if elf == nil {
+		return nil
+	}
+
 	if ds, ok := typ.(*Datasec); ok {
 		if elf.fixups[ds] {
 			return nil
@@ -373,10 +378,6 @@ func (s *Spec) TypeByID(id TypeID) (Type, error) {
 		return nil, fmt.Errorf("inflate type: %w", err)
 	}
 
-	if s.elf == nil {
-		return typ, nil
-	}
-
 	if err := s.elf.fixupDatasec(typ); err != nil {
 		return nil, err
 	}
@@ -399,28 +400,25 @@ func (s *Spec) TypeID(typ Type) (TypeID, error) {
 //
 // Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
 func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
-	typeIDs := s.TypeIDsByName(newEssentialName(name))
-	if len(typeIDs) == 0 {
-		return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
-	}
-
-	// Return a copy to prevent changes to namedTypes.
-	result := make([]Type, 0, len(typeIDs))
-	for _, id := range typeIDs {
-		typ, err := s.TypeByID(id)
-		if errors.Is(err, ErrNotFound) {
-			return nil, fmt.Errorf("no type with ID %d", id)
-		} else if err != nil {
-			return nil, err
-		}
+	types, err := s.TypesByName(newEssentialName(name))
+	if err != nil {
+		return nil, err
+	}
 
+	for i := 0; i < len(types); i++ {
 		// 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 {
-			result = append(result, typ)
+		if types[i].TypeName() != name {
+			types = slices.Delete(types, i, i+1)
+			continue
+		}
+
+		if err := s.elf.fixupDatasec(types[i]); err != nil {
+			return nil, err
 		}
 	}
-	return result, nil
+
+	return types, nil
 }
 
 // AnyTypeByName returns a Type with the given name.
diff --git a/btf/core.go b/btf/core.go
index ca4997f..f128011 100644
--- a/btf/core.go
+++ b/btf/core.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"math"
 	"reflect"
-	"slices"
 	"strconv"
 	"strings"
 
@@ -265,16 +264,14 @@ func CORERelocate(relos []*CORERelocation, targets []*Spec, bo binary.ByteOrder,
 
 		var targetTypes []Type
 		for _, target := range targets {
-			namedTypeIDs := target.TypeIDsByName(essentialName)
-			targetTypes = slices.Grow(targetTypes, len(namedTypeIDs))
-			for _, id := range namedTypeIDs {
-				typ, err := target.TypeByID(id)
-				if err != nil {
-					return nil, err
-				}
-
-				targetTypes = append(targetTypes, typ)
+			namedTypes, err := target.TypesByName(essentialName)
+			if errors.Is(err, ErrNotFound) {
+				continue
+			} else if err != nil {
+				return nil, err
 			}
+
+			targetTypes = append(targetTypes, namedTypes...)
 		}
 
 		fixups, err := coreCalculateFixups(group.relos, targetTypes, bo, resolveTargetTypeID)
diff --git a/btf/strings.go b/btf/strings.go
index 7c31461..f4a3957 100644
--- a/btf/strings.go
+++ b/btf/strings.go
@@ -1,21 +1,17 @@
 package btf
 
 import (
-	"bufio"
 	"bytes"
 	"errors"
 	"fmt"
 	"io"
 	"maps"
-	"slices"
 	"strings"
 )
 
 type stringTable struct {
-	base    *stringTable
-	offsets []uint32
-	prevIdx int
-	strings []string
+	base  *stringTable
+	bytes []byte
 }
 
 // sizedReader is implemented by bytes.Reader, io.SectionReader, strings.Reader, etc.
@@ -29,89 +25,67 @@ func readStringTable(r sizedReader, base *stringTable) (*stringTable, error) {
 	// from the last entry offset of the base BTF.
 	firstStringOffset := uint32(0)
 	if base != nil {
-		idx := len(base.offsets) - 1
-		firstStringOffset = base.offsets[idx] + uint32(len(base.strings[idx])) + 1
+		firstStringOffset = uint32(len(base.bytes))
 	}
 
-	// Derived from vmlinux BTF.
-	const averageStringLength = 16
-
-	n := int(r.Size() / averageStringLength)
-	offsets := make([]uint32, 0, n)
-	strings := make([]string, 0, n)
-
-	offset := firstStringOffset
-	scanner := bufio.NewScanner(r)
-	scanner.Split(splitNull)
-	for scanner.Scan() {
-		str := scanner.Text()
-		offsets = append(offsets, offset)
-		strings = append(strings, str)
-		offset += uint32(len(str)) + 1
-	}
-	if err := scanner.Err(); err != nil {
+	bytes := make([]byte, r.Size())
+	if _, err := io.ReadFull(r, bytes); err != nil {
 		return nil, err
 	}
 
-	if len(strings) == 0 {
+	if len(bytes) == 0 {
 		return nil, errors.New("string table is empty")
 	}
 
-	if firstStringOffset == 0 && strings[0] != "" {
-		return nil, errors.New("first item in string table is non-empty")
+	if bytes[len(bytes)-1] != 0 {
+		return nil, errors.New("string table isn't null terminated")
 	}
 
-	return &stringTable{base, offsets, 0, strings}, nil
-}
-
-func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
-	i := bytes.IndexByte(data, 0)
-	if i == -1 {
-		if atEOF && len(data) > 0 {
-			return 0, nil, errors.New("string table isn't null terminated")
-		}
-		return 0, nil, nil
+	if firstStringOffset == 0 && bytes[0] != 0 {
+		return nil, errors.New("first item in string table is non-empty")
 	}
 
-	return i + 1, data[:i], nil
+	return &stringTable{base: base, bytes: bytes}, nil
 }
 
 func (st *stringTable) Lookup(offset uint32) (string, error) {
-	if st.base != nil && offset <= st.base.offsets[len(st.base.offsets)-1] {
-		return st.base.lookup(offset)
+	// Fast path: zero offset is the empty string, looked up frequently.
+	if offset == 0 {
+		return "", nil
 	}
-	return st.lookup(offset)
+
+	b, err := st.lookupSlow(offset)
+	return string(b), err
 }
 
-func (st *stringTable) lookup(offset uint32) (string, error) {
+func (st *stringTable) LookupBytes(offset uint32) ([]byte, error) {
 	// Fast path: zero offset is the empty string, looked up frequently.
-	if offset == 0 && st.base == nil {
-		return "", nil
+	if offset == 0 {
+		return nil, nil
 	}
 
-	// Accesses tend to be globally increasing, so check if the next string is
-	// the one we want. This skips the binary search in about 50% of cases.
-	if st.prevIdx+1 < len(st.offsets) && st.offsets[st.prevIdx+1] == offset {
-		st.prevIdx++
-		return st.strings[st.prevIdx], nil
-	}
+	return st.lookupSlow(offset)
+}
 
-	i, found := slices.BinarySearch(st.offsets, offset)
-	if !found {
-		return "", fmt.Errorf("offset %d isn't start of a string", offset)
+func (st *stringTable) lookupSlow(offset uint32) ([]byte, error) {
+	if st.base != nil {
+		n := uint32(len(st.base.bytes))
+		if offset < n {
+			return st.base.lookupSlow(offset)
+		}
+		offset -= n
 	}
 
-	// Set the new increment index, but only if its greater than the current.
-	if i > st.prevIdx+1 {
-		st.prevIdx = i
+	if offset > uint32(len(st.bytes)) {
+		return nil, fmt.Errorf("offset %d is out of bounds of string table", offset)
 	}
 
-	return st.strings[i], nil
-}
+	if offset > 0 && st.bytes[offset-1] != 0 {
+		return nil, fmt.Errorf("offset %d is not the beginning of a string", offset)
+	}
 
-// Num returns the number of strings in the table.
-func (st *stringTable) Num() int {
-	return len(st.strings)
+	i := bytes.IndexByte(st.bytes[offset:], 0)
+	return st.bytes[offset : offset+uint32(i)], nil
 }
 
 // stringTableBuilder builds BTF string tables.
diff --git a/btf/unmarshal.go b/btf/unmarshal.go
index 98cd017..a55d79d 100644
--- a/btf/unmarshal.go
+++ b/btf/unmarshal.go
@@ -1,12 +1,15 @@
 package btf
 
 import (
+	"bytes"
 	"encoding/binary"
 	"fmt"
+	"hash/maphash"
 	"io"
 	"iter"
 	"maps"
 	"math"
+	"slices"
 	"sync"
 )
 
@@ -21,9 +24,10 @@ type decoder struct {
 	firstTypeID TypeID
 	// Map from TypeID to offset of the marshaled data in raw. Contains an entry
 	// for each TypeID, including 0 aka Void. The offset for Void is invalid.
-	offsets    []int
-	declTags   map[TypeID][]TypeID
-	namedTypes map[essentialName][]TypeID
+	offsets  []int
+	declTags map[TypeID][]TypeID
+	// An index from essentialName to TypeID.
+	namedTypes *fuzzyStringIndex
 
 	// Protection for mutable fields below.
 	mu              sync.Mutex
@@ -74,7 +78,7 @@ func newDecoder(raw []byte, bo binary.ByteOrder, strings *stringTable, base *dec
 
 	offsets := make([]int, 0, numTypes)
 	declTags := make(map[TypeID][]TypeID, numDeclTags)
-	namedTypes := make(map[essentialName][]TypeID, numNamedTypes)
+	namedTypes := newFuzzyStringIndex(numNamedTypes)
 
 	if firstTypeID == 0 {
 		// Add a sentinel for Void.
@@ -94,27 +98,27 @@ func newDecoder(raw []byte, bo binary.ByteOrder, strings *stringTable, base *dec
 		}
 
 		// Build named type index.
-		name, err := strings.Lookup(header.NameOff)
+		name, err := strings.LookupBytes(header.NameOff)
 		if err != nil {
 			return nil, fmt.Errorf("lookup type name for id %v: %w", id, err)
 		}
 
-		if name := newEssentialName(name); name != "" {
-			ids := namedTypes[name]
-			if ids == nil {
-				// Almost all names will only have a single name to them.
-				// Explicitly allocate a slice of capacity 1 instead of relying
-				// on append behaviour.
-				ids = []TypeID{id}
-			} else {
-				ids = append(ids, id)
+		if len(name) > 0 {
+			if i := bytes.Index(name, []byte("___")); i != -1 {
+				// Flavours are rare. It's cheaper to find the first index for some
+				// reason.
+				i = bytes.LastIndex(name, []byte("___"))
+				name = name[:i]
 			}
-			namedTypes[name] = ids
+
+			namedTypes.Add(name, id)
 		}
 
 		id++
 	}
 
+	namedTypes.Build()
+
 	return &decoder{
 		base,
 		bo,
@@ -211,11 +215,28 @@ func (d *decoder) TypeID(typ Type) (TypeID, error) {
 	return id, nil
 }
 
-// TypeIDsByName returns all type IDs which have the given essential name.
+// TypesByName returns all types which have the given essential name.
 //
-// The returned slice must not be modified.
-func (d *decoder) TypeIDsByName(name essentialName) []TypeID {
-	return d.namedTypes[name]
+// Returns ErrNotFound if no matching Type exists.
+func (d *decoder) TypesByName(name essentialName) ([]Type, error) {
+	var types []Type
+	for id := range d.namedTypes.Find(string(name)) {
+		typ, err := d.TypeByID(id)
+		if err != nil {
+			return nil, err
+		}
+
+		if newEssentialName(typ.TypeName()) == name {
+			// Deal with hash collisions by checking against the name.
+			types = append(types, typ)
+		}
+	}
+
+	if len(types) == 0 {
+		return nil, fmt.Errorf("type with name %s: %w", name, ErrNotFound)
+	}
+
+	return types, nil
 }
 
 // TypeByID decodes a type and any of its descendants.
@@ -649,3 +670,73 @@ func (d *decoder) inflateType(id TypeID) (typ Type, err error) {
 
 	return typ, nil
 }
+
+// An index from string to TypeID.
+//
+// Fuzzy because it may return false positive matches.
+type fuzzyStringIndex struct {
+	seed    maphash.Seed
+	entries []fuzzyStringIndexEntry
+}
+
+func newFuzzyStringIndex(capacity int) *fuzzyStringIndex {
+	return &fuzzyStringIndex{
+		maphash.MakeSeed(),
+		make([]fuzzyStringIndexEntry, 0, capacity),
+	}
+}
+
+// Add a string to the index.
+//
+// Calling the method with identical arguments will create duplicate entries.
+func (idx *fuzzyStringIndex) Add(name []byte, id TypeID) {
+	hash := uint32(maphash.Bytes(idx.seed, name))
+	idx.entries = append(idx.entries, newFuzzyStringIndexEntry(hash, id))
+}
+
+// Build the index.
+//
+// Must be called after [Add] and before [Match].
+func (idx *fuzzyStringIndex) Build() {
+	slices.Sort(idx.entries)
+}
+
+// Find TypeIDs which may match the name.
+//
+// May return false positives, but is guaranteed to not have false negatives.
+//
+// You must call [Build] at least once before calling this method.
+func (idx *fuzzyStringIndex) Find(name string) iter.Seq[TypeID] {
+	return func(yield func(TypeID) bool) {
+		hash := uint32(maphash.String(idx.seed, name))
+
+		// We match only on the first 32 bits here, so ignore found.
+		i, _ := slices.BinarySearch(idx.entries, fuzzyStringIndexEntry(hash)<<32)
+		for i := i; i < len(idx.entries); i++ {
+			if idx.entries[i].hash() != hash {
+				break
+			}
+
+			if !yield(idx.entries[i].id()) {
+				return
+			}
+		}
+	}
+}
+
+// Tuple mapping the hash of an essential name to a type.
+//
+// Encoded in an uint64 so that it implements cmp.Ordered.
+type fuzzyStringIndexEntry uint64
+
+func newFuzzyStringIndexEntry(hash uint32, id TypeID) fuzzyStringIndexEntry {
+	return fuzzyStringIndexEntry(hash)<<32 | fuzzyStringIndexEntry(id)
+}
+
+func (e fuzzyStringIndexEntry) hash() uint32 {
+	return uint32(e >> 32)
+}
+
+func (e fuzzyStringIndexEntry) id() TypeID {
+	return TypeID(e)
+}
diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock
index 3395cd8..0f56511 100644
--- a/docs/Pipfile.lock
+++ b/docs/Pipfile.lock
@@ -38,11 +38,11 @@
         },
         "certifi": {
             "hashes": [
-                "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
-                "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
+                "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6",
+                "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==2025.1.31"
+            "version": "==2025.4.26"
         },
         "charset-normalizer": {
             "hashes": [
@@ -214,11 +214,11 @@
         },
         "markdown": {
             "hashes": [
-                "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2",
-                "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"
+                "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc",
+                "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"
             ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.7"
+            "markers": "python_version >= '3.9'",
+            "version": "==3.8"
         },
         "markupsafe": {
             "hashes": [
@@ -314,12 +314,12 @@
         },
         "mkdocs-git-authors-plugin": {
             "hashes": [
-                "sha256:84b9b56c703841189c64d8ff6947034fe0a9c14a0a8f1f6255edfcfe3a56825f",
-                "sha256:f5cfaf93d08981ce25591bbaf642051ed168c3886bb96ecd2dca53f0ef1973b8"
+                "sha256:acdacc8452db90a94d9c395a230b16965a9f2f51e0a6eef182ac7d3e02e394fb",
+                "sha256:e19f0252ead3d626fd73e15bb56d6675704c3b62aa569ebc363f791291b8f60e"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==0.9.4"
+            "version": "==0.9.5"
         },
         "mkdocs-git-revision-date-localized-plugin": {
             "hashes": [
@@ -341,12 +341,12 @@
         },
         "mkdocs-material": {
             "hashes": [
-                "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff",
-                "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263"
+                "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e",
+                "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==9.6.11"
+            "version": "==9.6.12"
         },
         "mkdocs-material-extensions": {
             "hashes": [
@@ -358,11 +358,11 @@
         },
         "packaging": {
             "hashes": [
-                "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
-                "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
+                "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
+                "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==24.2"
+            "version": "==25.0"
         },
         "paginate": {
             "hashes": [
@@ -397,12 +397,12 @@
         },
         "pymdown-extensions": {
             "hashes": [
-                "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9",
-                "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"
+                "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7",
+                "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==10.14.3"
+            "version": "==10.15"
         },
         "python-dateutil": {
             "hashes": [
@@ -628,11 +628,11 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df",
-                "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"
+                "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466",
+                "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"
             ],
             "markers": "python_version >= '3.9'",
-            "version": "==2.3.0"
+            "version": "==2.4.0"
         },
         "watchdog": {
             "hashes": [


Test patch:
diff --git a/btf/btf_test.go b/btf/btf_test.go
index 35fca41..68f57f3 100644
--- a/btf/btf_test.go
+++ b/btf/btf_test.go
@@ -249,22 +249,9 @@ func BenchmarkIterateVmlinux(b *testing.B) {
 func TestParseCurrentKernelBTF(t *testing.T) {
 	spec := vmlinuxSpec(t)
 
-	if len(spec.namedTypes) == 0 {
+	if len(spec.offsets) == 0 {
 		t.Fatal("Empty kernel BTF")
 	}
-
-	totalBytes := 0
-	distinct := 0
-	seen := make(map[string]bool)
-	for _, str := range spec.strings.strings {
-		totalBytes += len(str)
-		if !seen[str] {
-			distinct++
-			seen[str] = true
-		}
-	}
-	t.Logf("%d strings total, %d distinct", len(spec.strings.strings), distinct)
-	t.Logf("Average string size: %.0f", float64(totalBytes)/float64(len(spec.strings.strings)))
 }
 
 func TestFindVMLinux(t *testing.T) {
@@ -280,7 +267,7 @@ func TestFindVMLinux(t *testing.T) {
 		t.Fatal("Can't load BTF:", err)
 	}
 
-	if len(spec.namedTypes) == 0 {
+	if len(spec.offsets) == 0 {
 		t.Fatal("Empty kernel BTF")
 	}
 }
@@ -585,7 +572,7 @@ func BenchmarkSpecTypeByID(b *testing.B) {
 	}
 }
 
-func BenchmarkInspectorGadget(b *testing.B) {
+func BenchmarkInspektorGadget(b *testing.B) {
 	// This benchmark is the baseline for what Inspektor Gadget loads for a
 	// common configuration.
 	types := []string{
diff --git a/btf/unmarshal_test.go b/btf/unmarshal_test.go
new file mode 100644
index 0000000..cf9bd95
--- /dev/null
+++ b/btf/unmarshal_test.go
@@ -0,0 +1,36 @@
+package btf
+
+import (
+	"iter"
+	"math"
+	"testing"
+
+	"github.com/go-quicktest/qt"
+)
+
+func TestFuzzyStringIndex(t *testing.T) {
+	idx := newFuzzyStringIndex(10)
+	count := testing.AllocsPerRun(1, func() {
+		idx.Add([]byte("foo"), 1)
+	})
+	qt.Assert(t, qt.Equals(count, 0))
+
+	idx.entries = idx.entries[:0]
+	idx.Add([]byte("foo"), 1)
+	idx.Add([]byte("bar"), 2)
+	idx.Add([]byte("baz"), 3)
+	idx.Build()
+
+	all := func(it iter.Seq[TypeID]) (ids []TypeID) {
+		for id := range it {
+			ids = append(ids, id)
+		}
+		return
+	}
+
+	qt.Assert(t, qt.SliceContains(all(idx.Find("foo")), 1))
+	qt.Assert(t, qt.SliceContains(all(idx.Find("bar")), 2))
+	qt.Assert(t, qt.SliceContains(all(idx.Find("baz")), 3))
+
+	qt.Assert(t, qt.IsTrue(newFuzzyStringIndexEntry(0, math.MaxUint32) < newFuzzyStringIndexEntry(1, 0)))
+}