diff --git a/demo/automated-demo-strip.sh b/demo/automated-demo-strip.sh new file mode 100755 index 0000000..6f91597 --- /dev/null +++ b/demo/automated-demo-strip.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Automated xcover demo for asciinema +# Run as: sudo bash automated-demo.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEMO_APP="./demo-app" +XCOVER="xcover" +PATH="/home/linuxbrew/.linuxbrew/bin:${PATH}" +export $PATH + +function main() { + # Check if running as root + if [ "$EUID" -ne 0 ]; then + echo "Please run as root: sudo bash $0" + exit 1 + fi + + clear + runCmd "# === xcover: Functional Test Coverage Profiler ===" + runCmd "# Profile coverage without instrumenting your binaries!" + echo + runCmd "# Let's test a demo C application" + runCmd "bat demo-app.c" + sleep 2 + clear + runCmd "gcc -O0 -o demo-app demo-app.c" + runCmd "ls demo-app" + runCmd "readelf --symbols demo-app | wc -l" + runCmd "# Let's strip the binary, because this must be a production binary" + runCmd "strip --strip-all demo-app" + runCmd "readelf --symbols demo-app | wc -l" + runCmd "# Start the profiler before running the functional tests" + runCmd "xcover run --detach --path demo-app --include '^main\.'" + runCmd "# Wait for the profiler to be ready" + runCmd "xcover wait" + runCmd "# Run test scenarios - xcover is tracing all function calls" + clear + runCmd "./demo-app add" + runCmd "./demo-app multiply" + runCmd "./demo-app greet" + runCmd "# Now let's stop the profiler" + clear + runCmd "xcover stop" + runCmd "# Collect the coverage results:" + runCmd "cat xcover-report.json | jq '.cov_by_func'" + runCmd "cat xcover-report.json | jq '.funcs_traced | length'" + runCmd "cat xcover-report.json | jq '.funcs_ack | length'" + runCmd "cat xcover-report.json | jq" + runCmd "# Coverage profiled without source code changes or recompilation on production binaries!" +} + +function runCmd() { + cmd=$1 + echo "$ ${cmd}" + eval "${cmd}" + sleep 2 +} + +main $@ diff --git a/demo/demo-app.c b/demo/demo-app.c new file mode 100644 index 0000000..fff43ee --- /dev/null +++ b/demo/demo-app.c @@ -0,0 +1,72 @@ +/* + * demo-app.c — xcover userspace BPF demo target + * + * A simple C program with several distinct functions so xcover can measure + * which ones were executed. Build with: + * + * gcc -O0 -o demo-app demo-app.c + * + * -O0 ensures every function gets a proper prologue that Frida-GUM can + * intercept (avoids trivially short leaf functions). + */ +#include +#include +#include + +int add(int a, int b) { + return a + b; +} + +int multiply(int a, int b) { + return a * b; +} + +int subtract(int a, int b) { + return a - b; +} + +int divide(int a, int b) { + if (b == 0) { + fprintf(stderr, "division by zero\n"); + return 0; + } + return a / b; +} + +void greet(const char *name) { + printf("Hello, %s!\n", name); +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: demo-app \n"); + fprintf(stderr, "Commands: add, multiply, subtract, divide, greet, all\n"); + return 1; + } + + const char *cmd = argv[1]; + + if (strcmp(cmd, "add") == 0) { + printf("10 + 20 = %d\n", add(10, 20)); + } else if (strcmp(cmd, "multiply") == 0) { + printf("5 * 6 = %d\n", multiply(5, 6)); + } else if (strcmp(cmd, "subtract") == 0) { + printf("30 - 10 = %d\n", subtract(30, 10)); + } else if (strcmp(cmd, "divide") == 0) { + printf("100 / 5 = %d\n", divide(100, 5)); + } else if (strcmp(cmd, "greet") == 0) { + greet("xcover"); + } else if (strcmp(cmd, "all") == 0) { + printf("Running all functions:\n"); + printf("Add: %d\n", add(10, 20)); + printf("Multiply: %d\n", multiply(5, 6)); + printf("Subtract: %d\n", subtract(30, 10)); + printf("Divide: %d\n", divide(100, 5)); + greet("xcover"); + } else { + fprintf(stderr, "Unknown command: %s\n", cmd); + return 1; + } + + return 0; +} diff --git a/docs/talks/opensouthcode-2026/slides.md b/docs/talks/opensouthcode-2026/slides.md new file mode 100644 index 0000000..700eae8 --- /dev/null +++ b/docs/talks/opensouthcode-2026/slides.md @@ -0,0 +1,1082 @@ +--- +marp: true +theme: default +paginate: true +style: | + section { + background: #ffffff; + color: #1a1a1a; + font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 1.35rem; + padding: 56px 80px; + } + + /* Section break slides */ + section.break { + background: #0d1117; + color: #ffffff; + text-align: center; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + } + section.break h1 { + font-size: 3rem; + color: #ffffff; + border: none; + } + section.break p { + color: #8b949e; + font-size: 1.1rem; + margin-top: 0.4em; + } + + /* Title slide */ + section.title { + justify-content: flex-end; + padding-bottom: 80px; + } + section.title h1 { + font-size: 2.6rem; + line-height: 1.2; + border: none; + margin-bottom: 0.3em; + } + section.title p { + color: #555; + font-size: 1rem; + } + + /* Statement slides */ + section.statement { + justify-content: center; + align-items: flex-start; + } + section.statement h1 { + border: none; + font-size: 2.4rem; + line-height: 1.3; + color: #1a1a1a; + } + + /* Regular slides */ + h1 { + font-size: 1.8rem; + color: #1a1a1a; + border-bottom: 2px solid #e8e8e8; + padding-bottom: 0.3em; + margin-bottom: 0.6em; + } + h2 { + font-size: 1.4rem; + color: #444; + } + ul { + margin-top: 0.4em; + padding-left: 1.4em; + } + li { + margin: 0.45em 0; + line-height: 1.5; + } + strong { + color: #1a1a1a; + } + blockquote { + border-left: 3px solid #d0d0d0; + padding-left: 1em; + color: #444; + margin: 0.8em 0; + font-style: italic; + } + table { + font-size: 0.9em; + width: 100%; + border-collapse: collapse; + } + th { + background: #f6f6f6; + border-bottom: 2px solid #ddd; + padding: 0.5em 0.8em; + text-align: left; + } + td { + padding: 0.45em 0.8em; + border-bottom: 1px solid #eee; + } + + /* Code blocks - gruvbox dark */ + pre { + background: #282828; + border-radius: 8px; + padding: 1em 1.2em; + margin: 0.6em 0; + overflow: hidden; + /* Theme ships light-palette syntax colors; remap them to the + gruvbox dark palette so tokens are readable on the dark background. */ + --color-prettylights-syntax-comment: #928374; + --color-prettylights-syntax-constant: #d3869b; + --color-prettylights-syntax-entity: #fabd2f; + --color-prettylights-syntax-entity-tag: #8ec07c; + --color-prettylights-syntax-keyword: #fb4934; + --color-prettylights-syntax-string: #b8bb26; + --color-prettylights-syntax-string-regexp: #8ec07c; + --color-prettylights-syntax-variable: #fe8019; + --color-prettylights-syntax-storage-modifier-import: #ebdbb2; + --color-prettylights-syntax-constant-other-reference-link: #b8bb26; + --color-prettylights-syntax-markup-heading: #83a598; + --color-prettylights-syntax-markup-list: #fabd2f; + --color-prettylights-syntax-markup-bold: #fbf1c7; + --color-prettylights-syntax-markup-italic: #fbf1c7; + --color-prettylights-syntax-markup-inserted-text: #b8bb26; + --color-prettylights-syntax-markup-inserted-bg: #32361a; + --color-prettylights-syntax-markup-deleted-text: #fb4934; + --color-prettylights-syntax-markup-deleted-bg: #3c1f1e; + } + pre code { + color: #ebdbb2; + font-size: 0.78em; + line-height: 1.6; + background: none; + padding: 0; + border-radius: 0; + } + code { + background: #f0f0f0; + padding: 0.1em 0.35em; + border-radius: 4px; + font-size: 0.85em; + } + + /* Page number */ + section::after { + font-size: 0.6rem; + color: #bbb; + content: attr(data-marpit-pagination) ' / ' attr(data-marpit-pagination-total); + } +--- + + + +# The Binary You Ship
Is the Binary You Test + +Massimiliano · OpenSouthCode 2026 + + + +--- + +# `$whoami` + +- Massimiliano Giovagnoli +- Software engineer @ Chainguard +- OSS, building and observing all the things +- Music, racing cars, nature +- `@maxgio92` on GitHub, X and Telegram, `@maxgio92.bsky.social`, `@maxgio92@hachyderm.io` +- linkedin.com/in/maxgio + +--- + +# Agenda + +- The standard coverage story +- Profiling with the kernel +- xcover in practice +- Demo +- Challenges with production binaries +- Demo: xcover on a stripped binary +- The cost of profiling with eBPF +- Benchmarking the overhead +- Eliminate the kernel trap +- Demo: userspace eBPF mode +- Limitations & what's next +- Wrapping up + +--- + +# Quality + +- How do you ensure that your software works? +- How do you ensure that your strategy is telling you a reliable story? +- How do you ensure that positive signals are meaningful? + + + +--- + + + +# The standard coverage story + + + +--- + +# Coverage today: the build-time approach + +```sh +# Go +go build -cover -covermode=count -coverpkg=./... -o myapp ./main.go + +# C/C++ with GCC +gcc -fprofile-arcs -ftest-coverage -o myapp src/*.c + +# LLVM source-based +clang -fprofile-instr-generate -fcoverage-mapping -o myapp src/*.c +``` + +- Instrumentation is **injected at compile time** +- Produces a separate artifact: the instrumented build +- Run tests against that artifact, collect the report + + + +--- + +# Testing Linux distro packages + +```yaml + - name: crane-cov + description: "Crane compiled for collecting coverage profiles" + pipeline: + - uses: go/build + with: + extra-args: -cover + test: + environment: + contents: + packages: + - busybox + - go + environment: + GOCOVERDIR: /home/build + pipeline: + - name: Run a command with the instrumented binary + runs: | + crane manifest chainguard/static + - name: Report function coverage + runs: | + go tool covdata func -i=. +``` + +--- + +# The problem at scale + +``` +package-foo/ + build/ + package-foo-1.0.0.apk ← what you ship + package-foo-1.0.0-instrumented.apk ← what you test +``` + +- Every package needs two build targets +- You maintain **thousands of packages** (Go, C, C++, Rust, Python extensions...) +- Doubled CI time, doubled storage, doubled maintenance +- Reproducibility: **You're not testing what you ship** + + + +--- + + + + + +# What if coverage didn't require
a separate build at all? + + + +--- + + + +# Profiling with the kernel + +--- + +# The kernel sees everything + +- **uprobes**: Linux kernel user-level dynamic tracing +- Available since Linux 3.5 [1] +- Attach to a function by **binary path + offset** +- Works on **any ELF binary** +- Integrated into eBPF since Linux 3.18 [2] - `BPF_PROG_TYPE_UPROBE` + +> 1. https://lwn.net/Articles/499190/ +> 2. https://lwn.net/Articles/637391/ + + + +--- + +# What is a uprobe? + +``` +Loaded image: + offset 0x1a40: PUSH RBP ← function entry + offset 0x1a41: MOV RBP, RSP + +With uprobe attached: + offset 0x1a40: INT3 ← kernel patches this + (original bytes saved) +``` + +At runtime, on each call: +1. Software interrupt causes a trap into kernel mode +3. BPF program runs +4. Registers restored, execution continues in userland + + + +--- + +# eBPF makes uprobes programmable + +- **eBPF**: run sandboxed programs in the kernel, triggered by events +- Loaded programs are **verifier-checked**: no crashes, bounded execution, safe +- **Attach** programs to specific kernel paths - i.e. uprobe +- Access to kernel data and communicate with userland with **maps** + +``` +userspace (xcover) kernel + │ │ + │── load BPF program ─▶ │ (verifier checks it) + │── attach uprobe ────▶ │ function entry at offset 0x1a40 + │ │ + │ [test runs] │ + │ │ + │◀── read BPF map ──── │ which functions fired +``` + + + +--- + +# Introducing xcover + +> The binary you ship is the binary you test. + +- eBPF uprobe-based coverage profiler for compiled binaries +- Runs as a **daemon** alongside your test suite +- Attaches to the binary at its path +- Reports function-level coverage when you stop it +- Ships as a **single static binary**, zero runtime dependencies + +`github.com/maxgio92/xcover` + + + +--- + + + +# xcover in practice + + + +--- + +# Architecture + +``` +┌────────────────────────────────────────────────────────┐ +│ xcover (daemon) │ +│ ┌─────────────┐ ┌──────────────┐ │ +│ │ UserTracee │ │ UserTracer │ │ +│ │ │ │ │ │ +│ │ resolve │───▶│ load BPF │ │ +│ │ functions │ │ attach probes│ │ +│ │ │ │ read events │ │ +│ └─────────────┘ └──────┬───────┘ │ +└─────────────────────────── │ ──────────────────────────┘ + │ BPF map +┌─────────────────────────── │ ──────────────────────────┐ +│ kernel │ │ +│ uprobes ◀────┘ │ +└────────────────────────────────────────────────────────┘ + ↕ calls traced functions +┌────────────────────────────────────────────────────────┐ +│ your binary (tracee) ← runs independently │ +└────────────────────────────────────────────────────────┘ +``` + + + +--- + +# xcover API + +```go +tracee := trace.NewUserTracee( + trace.WithTraceeExePath("./myapp"), + trace.WithTraceeSymPatternInclude(`^github\.com/myorg`), + trace.WithTraceeSymPatternExclude(`_test$`), +) + +tracer := trace.NewUserTracer( + trace.WithTracerLogger(logger), + trace.WithTracerReport(true), + trace.WithTracerTracee(tracee), +) + +if err := tracer.Init(ctx); err != nil { ... } +if err := tracer.Run(ctx); err != nil { ... } +``` + + + +--- + +# The CLI workflow + +```sh +# Start the profiler (detach to background) +xcover run --path ./myapp --detach + +# Wait until all probes are attached (ready to trace) +xcover wait + +# Run your tests — nothing changes here +./myapp foo +./myapp bar +./myapp baz + +# Stop the profiler and collect the report +xcover stop +``` + + + +--- + +# Filtering functions + +```sh +# Only trace functions in your own packages +xcover run --path ./myapp \ + --include '^github\.com/myorg/myapp' \ + --detach + +# Exclude generated code +xcover run --path ./myapp \ + --include '^github\.com/myorg' \ + --exclude '\.pb\.go' \ + --detach +``` + +- `--include` and `--exclude` take Go regexes +- Applied to function names at probe-attachment time + + + +--- + +# The coverage report + +```json +{ + "exe_path": "./myapp", + "funcs_traced": [ + "github.com/myorg/myapp/pkg/server.HandleRequest", + "github.com/myorg/myapp/pkg/server.parseConfig", + "github.com/myorg/myapp/pkg/db.Connect", + ... + ], + "funcs_ack": [ + "github.com/myorg/myapp/pkg/server.HandleRequest", + "github.com/myorg/myapp/pkg/db.Connect" + ], + "cov_by_func": 0.72 +} +``` + +- `funcs_traced`: all functions with probes attached +- `funcs_ack`: functions called at least once during the run +- `cov_by_func`: ratio: 72% of functions were exercised + + + +--- + + + +# Demo + + + +--- + + + +# Challenges with
production binaries + + + +--- + +# What strip actually removes + +```sh +$ ls -lh myapp myapp-stripped +-rwxr-xr-x 1 user user 14M myapp +-rwxr-xr-x 1 user user 6M myapp-stripped + +$ readelf --sections myapp | grep -E '\.symtab|\.debug' + [32] .symtab SYMTAB ... ← function names + offsets + [33] .strtab STRTAB ... ← symbol name strings + [34] .debug_info PROGBITS ... + [35] .debug_line PROGBITS ... + +$ readelf --sections myapp-stripped | grep -E '\.symtab|\.debug' +$ +$ readelf --symbols myapp-stripped +$ +``` + +- `strip` removes `.symtab`, `.strtab`, and all `.debug_*` sections +- **The code is still there.** Only the metadata is gone. + + + +--- + +# What strip does NOT remove + +```sh +$ readelf --sections myapp-stripped | grep '\.eh_frame' + [17] .eh_frame_hdr PROGBITS ... + [18] .eh_frame PROGBITS ... +``` + +- `.eh_frame`: DWARF Call Frame Information, needed for stack unwinding +- Survives `strip --strip-all`: needed for **exception handling** (e.g. C++) +- Encodes function boundaries and stack layout +- The compiler always emits it. The linker always keeps it. + + + +--- + +# Introducing resurgo library + +Static function recovery for stripped ELF binaries. + +Deterministic: +1. **DWARF CFI** (`.eh_frame` ELF section): function ranges, survives strip, high confidence + +Heuristic: +1. **Prologue patterns**: at function entry, architecture-specific +2. **Call-site analysis**: `call` targets are function entry points by definition +3. **Alignment boundaries**: compilers align functions to 16/32-byte boundaries + +Heuristic- and determinism-based validation for confidence. + +`github.com/maxgio92/resurgo` + + + +--- + +# xcover function recovery with resurgo + +``` +ELF binary (stripped) + │ + ├── parse .eh_frame ──────▶ function ranges (high confidence) + │ + ├── scan for prologues ───▶ entry candidates (medium confidence) + │ + ├── walk call graph ──────▶ call targets (medium confidence) + │ + └── check alignment ──────▶ alignment candidates (low confidence) + │ + ▼ + cross-validate + │ + ▼ + function list + offsets ──▶ xcover attaches uprobes +``` + + + +--- + +# Transparent to the user + +```sh +# Unstripped binary +$ xcover run --path ./myapp --detach --log-level debug +DBG resolved 1842 functions via .symtab + +# Stripped binary — same command +$ xcover run --path ./myapp-stripped --detach --log-level debug +DBG .symtab not found, falling back to static recovery +DBG resolved 1791 functions via resurgo (eh_frame + prologue analysis) +``` + +- xcover detects the binary is stripped and calls resurgo automatically +- No flags, no config change, no separate step +- Coverage report is identical in format + + + +--- + + + +# Demo: xcover on a stripped binary + + + +--- + + + +# The cost of profiling with eBPF + +--- + + + +# Benchmarking the overhead + +--- + +# The overhead model + +Every call to a traced function pays: + +``` +function called + │ + ▼ + INT3 trap ──▶ kernel mode + │ + ▼ + BPF program runs + (cookie lookup) + (potential map and ring buffer update) + │ + ▼ + return to userspace + │ + ▼ +function body executes +``` + +- **Fixed cost per call**, independent of function body size +- Overhead scales with **call frequency**, not binary size or number of functions + + + +--- + +# The probe + +```c +SEC("uprobe/handle_user_function") +int handle_user_function(struct pt_regs *ctx) { + __u64 cookie = bpf_get_attach_cookie(ctx); + u8 seen = 1; + + /* Check if the function has been already reported */ + if (bpf_map_lookup_elem(&seen_funcs, &cookie)) { + return 0; + } + + /* Track which functions have been reported */ + bpf_map_update_elem(&seen_funcs, &cookie, &seen, BPF_ANY); + + struct event_t *event = bpf_ringbuf_reserve(&events, sizeof(struct event_t), 0); + if (!event) { + return 0; + } + + event->cookie = cookie; + bpf_ringbuf_submit(event, ringbuffer_flags); + + return 0; +} +``` + +--- + +# Benchmark setup + +| Scenario | Description | How | +|---|---|---| +| **Baseline** | No tracing | Tracee runs without uprobes | +| **Idle** | uprobe attached | Probe attached to functions that never run | +| **Hit** | uprobe firing, already seen | Tracee runs the same probed function N times | +| **Miss** | uprobe firing, new function | Tracee runs N probed unique functions only once | + + + +--- + +# Aggregate overhead + +| Scenario | Description | Time per call | Overhead vs Baseline | +|---|---|---|---| +| **Baseline** | No tracing | ~2 ns | | +| **Idle** | uprobe attached | ~2 ns | | +| **Hit** | uprobe firing, already seen | ~2000 ns | 1000x | +| **Miss** | uprobe firing, new function | ~4000 ns | 2000x | + +> **+X% wall time for full test coverage on the exact production binary.** + +- Filtering to your own packages reduces overhead +- Overhead is deterministic; no jitter, no GC interaction +- Acceptable for CI pipelines; not for latency-sensitive benchmarks + + + +--- + +# Memory: BPF maps + +``` +Map: per-function event counter + Key size: 8 bytes (cookie / function ID) + Value size: 8 bytes (uint64 hit count) + Max entries: N (one per traced function) + +Total: N × 16 bytes +``` + +- 1000 functions → 16 KB +- 10,000 functions → 160 KB +- **Negligible** compared to typical process memory + + + +--- + +# The tradeoff + +| | Instrumented build | xcover | +|---|---|---| +| Build changes required | Yes | **No** | +| Works on stripped binaries | No | **Yes** | +| Cross-language | No | **Yes** | +| Same binary as production | No | **Yes** | +| Per-call overhead | ~0 | ~2000 ns | +| Setup complexity | Per-language | Once | + +> You pay a small, predictable cost per function call.
In exchange: no instrumented builds, one tool, any binary. + + + +--- + + + +# Eliminate the kernel trap + + + +--- + +# The cost is in the trap + +``` +Current: kernel uprobe + function call → INT3 → kernel trap → BPF runs → return to userspace + +Hypothesis: userspace BPF + function call → intercept in-process → BPF runs → continue + (no kernel transition) +``` + +- Userspace eBPF runtimes: run BPF programs in the same process as the tracee +- No kernel involved → no trap → potentially zero context-switch overhead +- The BPF program runs as a JIT-compiled function in userspace + + + +--- + +# eunomia-bpf/bpftime + +- Userspace eBPF runtime: `syscall_server.so` and `agent.so` libraries +- `syscall_server` intercepts `perf_event_open` / `bpf_link_create` syscalls via `LD_PRELOAD` +- `syscall_server` creates data structures in a shared memory, read by the `agent` +- `agent` sets up the trampolines at function entry to the BPF `BPF_PROG_TYPE_UPROBE` program +- No kernel trap +- API-compatible with kernel eBPF + +``` +┌──────────────────────────────────────────────┐ +│ tracee process │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ bpftime agent │ │ your binary │ │ +│ │ (LD_PRELOAD) │ │ (unmodified) │ │ +│ │ │ │ │ │ +│ │ JIT BPF prog ◀─┼──┼── function call │ │ +│ │ (shm) │ │ │ │ +│ └──────────────────┘ └──────────────────┘ │ +└──────────────────────────────────────────────┘ +``` + + + +--- + +# xcover + bpftime: the experimental path + +```sh +# Start the bpftime syscall server (intercepts BPF syscalls from xcover) +xcover run --path ./myapp --userspace-bpf --detach + +# Inject the bpftime agent into the tracee +LD_PRELOAD=$(xcover agent extract) ./myapp +``` + +- `xcover agent extract` unpacks the embedded bpftime shared library +- bpftime `agent` intercepts uprobe hits in-process +- Everything else is identical: same report, same workflow +- Flag: `--userspace-bpf` (experimental) + + + +--- + +# Let's benchmark again! + +| Scenario | Description | Overhead reduction Userspace vs Kernel | +|---|---|---| +| **Baseline** | No tracing | ~ | +| **Idle** | uprobe attached | ~ | +| **Hit** | uprobe firing, already seen | **-69%** | +| **Miss** | uprobe firing, new function | **-72%** | + +--- + + + +# Demo: userspace eBPF mode + +--- + +# Current status of the userspace mode + +## What works +- Single-uprobe attachment (perf-event based) ✅ +- bpf_cookie propagation (function identification) ✅ +- Coverage report generation ✅ +- Unprivileged run (no `CAP_BPF`) + +## Known limitations +- No `uprobe_multi` link support (perf-based) +- Requires `LD_PRELOAD` injection (not transparent for `execve`-started processes) +- Statically linked / musl binaries: no agent injection ⚠️ +- Frida Gum interceptor: some aggressive compiler optimisations (tail-call elision, LTO) are not yet handled + +> If you've worked with userspace eBPF runtimes, find me after the talk. + + + +--- + +# Limitations & what's next + +## Limitations +- Function inlining defeats uprobe-based interception +- There is an overhead cost +- Userspace mode requires dynamically linked tracee (`LD_PRELOAD`ed agent) +- Frida Gum interceptor: some aggressive compiler optimisations (tail-call elision, LTO) are not yet handled + +## Upstream gaps +- **bpftime**: some patches that are going to be proposed upstream +- **libbpfgo**: missing `AttachUprobeWithCookie` libbpf API (single-uprobe attach with per-probe `bpf_cookie`) + - https://github.com/aquasecurity/libbpfgo/pull/523 + +## What I'm working on +- Rolling out xcover on thousands of packages +- Performance optimizations +- Stress test the userspace mode + + + +--- + +# Wrapping up + +**The binary you ship is the binary you test.** + +- Coverage tooling today assumes you control the build. At scale, that assumption breaks +- Kernel instrumentation allows to observe the runtime +- Recent work improves Go project scoping and uses debug files when available +- The overhead is real, measured. It can be acceptable for test workloads +- If you want to cut the overhead, userspace BPF runtimes can reduce it up to 4x + +
+ +`github.com/maxgio92/xcover` · `github.com/maxgio92/resurgo` + + + +--- + + + +# Questions? + +linkedin.com/in/maxgio +@maxgio92 on Telegram + +github.com/maxgio92/xcover +github.com/maxgio92/resurgo diff --git a/docs/talks/opensouthcode-2026/slides.pdf b/docs/talks/opensouthcode-2026/slides.pdf new file mode 100644 index 0000000..e55a21d Binary files /dev/null and b/docs/talks/opensouthcode-2026/slides.pdf differ