
Why Apt Says Bpftool Is a Virtual Package (and the Fix)
Install linux-tools-common first, then linux-tools-$(uname -r), and your bpftool install is finished. On Ubuntu, apt install bpftool stops and tells you the name is virtual because nothing in the archive ships under that name. The common package gives you a wrapper script on your PATH; the kernel-versioned tools package gives you the binary that wrapper calls. Run bpftool version to confirm both halves agree before you start blaming eBPF or your kernel config.
Last updated: 2026-08-02
Why does apt call bpftool a virtual package?
Because no real package is named bpftool. A virtual package is a capability name that one or more real packages declare they provide, and apt refuses to guess which provider you meant. The rules for this live in the Debian Policy Manual, which Ubuntu inherits wholesale.
Run this first and read the output instead of retyping the install command:
apt-cache policy bpftool
apt show bpftool
apt-cache showpkg bpftool
apt-cache policy prints no installed version and no candidate. That tells you the name resolves to nothing installable. apt-cache showpkg is the useful one: its Reverse Provides section names linux-tools-common as the provider. So the message is not a broken system, it's apt handing you the real package name and waiting.
The trap is that the provider alone is a launcher script. It reads uname -r at runtime and execs a binary under a kernel-versioned path. Install the wrapper by itself and you get a command on your PATH that cannot find anything to run.
Which package gives you a working bpftool install on Ubuntu?
Both halves, in one command, pinned to the kernel you booted:
sudo apt update
sudo apt install linux-tools-common linux-tools-$(uname -r)
bpftool version
A version string means the wrapper found its backend. No version string means one half is missing, and the diagnosis section below separates which.
| Package | What it gives you | My call |
|---|---|---|
linux-tools-common | The wrapper script and manpages, no per-kernel binary | Required, never sufficient on its own |
linux-tools-$(uname -r) | The compiled bpftool for the exact running kernel | Required, and the piece people skip |
linux-tools-generic | Meta-package that follows the generic kernel line | Worth adding so upgrades keep working |
| A standalone binary from someone's release page | An unpackaged file with no owner | Skip it, and see the source-build section if you truly need newer |
Do not hardcode a kernel string copied from a forum answer. The $(uname -r) substitution exists so apt pulls the tools package matching your booted kernel rather than whatever is newest in the archive. Hardcoding is how you install tooling for a kernel you are not running and then wonder why nothing changed.
Noble, noble-updates, and the pocket nobody enabled
Kernel-matched tools packages get published to the updates pocket as new kernels ship, so the package you need frequently is not in the original noble release pocket. If apt-cache policy linux-tools-$(uname -r) reports a candidate of (none), suspect your enabled suites before you suspect the archive.
apt policy
apt-cache policy linux-tools-$(uname -r)
apt policy lists every pocket apt is actually reading. Look for noble-updates in that list. Recent Ubuntu releases moved sources into the deb822 format at /etc/apt/sources.list.d/ubuntu.sources, so grepping /etc/apt/sources.list alone will show you an almost empty file and convince you of the wrong thing. Check both locations, or trust apt policy, which reads whatever is really enabled.
The other cause is a kernel flavour mismatch. If you booted a cloud, kvm, or hardware-enablement kernel, the matching tools package carries that flavour in its name too, and the generic meta-package will not track it for you.
Prove the binary you are running came from the package
Verify ownership before you trust output. An old manual build sitting in /usr/local/sbin will happily shadow the packaged wrapper, and it will print a plausible version string while misreading kernel structures.
command -v bpftool
dpkg -S "$(command -v bpftool)"
dpkg -L linux-tools-common | grep bpftool
strace -f -e trace=execve bpftool version 2>&1 | grep bpftool
dpkg -S names the owning package. If it answers "no path found matching pattern", you are running something you or a previous admin dropped there by hand, and no amount of apt work will change that. Delete it or fix your PATH order, then run hash -r so your shell stops caching the stale location.
The strace line is the one that ends arguments. It shows the wrapper exec'ing the versioned binary under /usr/lib/linux-tools/, which proves both packages agree on the kernel string. If the exec fails with ENOENT, the wrapper is fine and the kernel-matched tools package is missing.
When apt refuses, read the failure instead of guessing
Each failure has a different signature, and reinstalling everything hides all of them. Work down this list and stop at the first match:
- Candidate is
(none). The updates pocket is disabled, or your kernel flavour has no matching tools package. Checkapt policyoutput. - Package is held. Run
apt-mark showhold. Somebody pinned the kernel line to stop upgrades and forgot to tell you. - Unmet dependencies. Run
sudo apt --fix-broken installand read the exact package names it prints. Do not delete files out of the dpkg database because a blog said so. - Wrong architecture. Compare
dpkg --print-architectureagainst what the archive publishes for your platform.
When the cause is still unclear, the apt logs beat memory. /var/log/apt/history.log shows what was installed, upgraded, or removed and when; /var/log/apt/term.log shows the raw output that scrolled past you. After a kernel upgrade that broke tooling, those files tell you exactly which packages moved and which ones did not follow.
When is building bpftool from kernel source worth it?
Rarely, and only when the packaged build genuinely lacks a feature you need. Chasing a newer bpftool for its own sake gives you an unowned binary to maintain, and the wrapper will keep calling the packaged one anyway. If you are running a mainline or custom kernel that has no tools package in the archive, that is a real reason.
Build from the tree that matches the kernel you boot, under tools/bpf/bpftool. You will need a compiler toolchain, libelf, zlib, libcap, and pkg-config; LLVM gets you disassembly of jited code. The build also generates headers from the kernel's own definitions, which is why the matching tree matters and a random tarball does not.
sudo apt install build-essential pkg-config libelf-dev zlib1g-dev libcap-dev clang llvm
cd /usr/src/linux/tools/bpf/bpftool
make
Install it somewhere deliberate and know it will take precedence on your PATH. What bpftool can report also depends on how the kernel was configured, so if features are missing, check the kernel flag eBPF programs need before rebuilding userspace again. The same reasoning applies when you compile libbpf from source: match the kernel, then validate.
bpftool's command structure is object first, verb second
Learn the shape and you stop memorizing snippets. Every invocation is bpftool OBJECT COMMAND, where the object is prog, map, link, cgroup, net, btf, feature, perf, or iter. Ask any object for its own help:
bpftool prog help
bpftool map help
bpftool feature probe kernel
feature probe is the command I reach for on an unfamiliar box. It reports which program types, map types, and helpers this kernel actually supports, which answers "why does my program fail to load here but not there" without guesswork.
Add -j for JSON or -p for indented JSON on almost any subcommand. That matters more than it sounds: parsing bpftool's human output with awk breaks every time the format shifts, while the JSON keys stay stable enough to script against.
Look before you load: programs, maps, and translated code
List what is already on the machine before you attach anything. Something else may already own the hook you want.
bpftool prog show
bpftool map show
bpftool link show
bpftool prog show id <ID> --pretty
bpftool prog dump xlated id <ID>
bpftool prog dump jited id <ID>
bpftool map dump id <ID>
prog show gives you the ID, type, name, load time, owning process, and any pinned path under the BPF filesystem. The pinned path tells you whether the program survives its loader exiting. Programs with no pin and no live process are gone already.
The xlated dump shows instructions after the verifier rewrote them; the jited dump shows native code and needs LLVM present to disassemble. Both are readable with BTF loaded, which is where variable names and types come from. For a deeper walk through inspection workflows, my guide to bpftool program inspection and debugging covers the dump output line by line.
Loading and attaching without hiding verifier failures
Mount the BPF filesystem before you pin anything, then load with an explicit pin path:
mount | grep bpf
sudo mount -t bpf bpf /sys/fs/bpf
sudo bpftool prog load prog.o /sys/fs/bpf/myprog
sudo bpftool -d prog load prog.o /sys/fs/bpf/myprog
When a load fails, the useful text is the verifier log, and -d is what prints it. Read the last lines before the rejection, not the top of the dump. Rejections usually come down to unchecked pointer access or a loop the verifier cannot bound, and both point at a specific instruction you can map back to your C.
Attach through the object that owns the hook: bpftool cgroup attach, bpftool net attach, or bpftool link create. Then confirm with bpftool link show rather than assuming. Cleanup means removing the pin with rm, which drops a reference; the program stays alive while anything still holds it. If loading fails before the verifier even runs, you are short on privileges, so check CAP_BPF and CAP_SYS_ADMIN and read dmesg -T | tail for the kernel side of the story. Background on the subsystem itself is on the eBPF project site.
FAQ
Can I run bpftool inside a container?
Usually yes, with work. The container needs the BPF filesystem visible in its mount namespace, CAP_BPF and CAP_SYS_ADMIN, and often the host PID namespace so prog show can name owning processes. A container built on a different distribution release still talks to the host kernel, so the version that matters is the host's, not the image's.
Why does bpftool prog show print nothing when something is definitely loaded?
You are unprivileged. Without the right capabilities the kernel returns an empty list rather than an error, which reads exactly like an idle machine. Try again under sudo before you conclude your loader failed.
Does a hardware-enablement kernel change which package I need?
The linux-tools-$(uname -r) command stays correct, since it resolves to whatever you booted. What changes is the meta-package: the plain generic one will not follow an HWE, cloud, or kvm kernel line, so pick the meta-package carrying the same flavour or accept installing the versioned package by hand after each upgrade.
Is bpftool the same thing as bpftrace?
Different jobs. bpftrace compiles and runs its own tracing scripts; bpftool inspects and manages what the kernel already holds. If your question is "what is this loaded program doing", bpftool answers it. If your question is "which files does this process open", reach for bpftrace or the BCC tracing toolkit.
My kernel is a custom build with no tools package. Now what?
Build bpftool from that exact tree and install it under a path you control. Keep the packaged wrapper installed anyway, because the day you boot back into a distribution kernel it will find its own binary again without any intervention from you.
