Skip to content

Instantly share code, notes, and snippets.

@fzakaria
Created July 12, 2026 03:48
Show Gist options
  • Select an option

  • Save fzakaria/0155e11a0882bd3d6e63f4070e7fac0c to your computer and use it in GitHub Desktop.

Select an option

Save fzakaria/0155e11a0882bd3d6e63f4070e7fac0c to your computer and use it in GitHub Desktop.
flake.nix to test Linux kernel with selftests and additional eBPF testing
{
description = "Linux Kernel Development Environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs = {
self,
nixpkgs,
}: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
# --- Goal 2 demo artifacts (hermetic, baked into the VM) ---
# A tiny fully-static aarch64 binary (raw syscalls, no libc) that just
# prints a marker. It only runs under an aarch64 interpreter (qemu-user).
aarch64HelloSrc = pkgs.writeText "aarch64_hello.S" ''
.section .text
.global _start
_start:
mov x0, #1
ldr x1, =msg
mov x2, #21
mov x8, #64
svc #0
mov x0, #0
mov x8, #93
svc #0
.section .data
msg:
.ascii "AARCH64_RAN_VIA_QEMU\n"
'';
aarch64Hello = pkgs.runCommand "aarch64-hello" {
nativeBuildInputs = [ pkgs.pkgsCross.aarch64-multiplatform.buildPackages.binutils ];
} ''
aarch64-unknown-linux-gnu-as ${aarch64HelloSrc} -o hello.o
aarch64-unknown-linux-gnu-ld hello.o -o $out
'';
# The struct_ops BPF handler (qemu_aarch64.bpf.o) is compiled on the HOST
# against the freshly-built kernel's BTF (bpftool btf dump -> vmlinux.h;
# clang -target bpf). It cannot be a hermetic derivation because it needs
# this exact kernel's BTF; the VM reaches it over the 9p mount at
# /mnt/host_project. Rebuild it after each kernel build with:
# bpftool btf dump file vmlinux format c > .../exec/vmlinux.h
# sed -i '/__ksym;$/d' .../exec/vmlinux.h # drop libbpf-conflicting externs
# clang -g -O2 -target bpf -I.../exec -I$libbpf/include \
# -c .../exec/qemu_aarch64.bpf.c -o .../exec/qemu_aarch64.bpf.o
# Demo (goal 2): an aarch64 binary is routed to qemu by a *BPF* struct_ops
# handler whose interpreter is chosen by the program via
# bpf_binprm_set_interp() -- the 'B' registration carries NO interpreter,
# only the handler name.
binfmtBpfDemo = pkgs.writeShellScript "binfmt-bpf-demo" ''
set -u
export PATH=${pkgs.lib.makeBinPath (with pkgs; [ coreutils util-linux ])}:$PATH
echo "BINFMT_DEMO_BEGIN"
# The self-contained upstream selftest does everything internally: it
# mounts bpffs/binfmt_misc, loads and attaches each struct_ops handler,
# activates a 'B' entry by name, and checks both cases -- match-header ->
# program-chosen interpreter, and $ORIGIN -> interpreter resolved relative
# to the binary. So the VM just runs it. Build it on the host first:
# make -C tools/testing/selftests/exec binfmt_misc_bpf \
# VMLINUX_BTF=$PWD/vmlinux LIBBPF_CFLAGS=-I<libbpf>/include ...
SELFTEST=/mnt/host_project/tools/testing/selftests/exec/binfmt_misc_bpf
echo "== running $SELFTEST =="
if [ -x "$SELFTEST" ]; then
"$SELFTEST"; STATUS=$?
else
echo "selftest binary not built at $SELFTEST"; STATUS=1
fi
echo "BINFMT_DEMO_STATUS=$STATUS"
echo "BINFMT_DEMO_END"
exit "$STATUS"
'';
in {
nixosConfigurations.micro-vm = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# Import the QEMU VM module directly so `config.system.build.vm`
# (the `run-micro-vm-vm` launcher) and all `virtualisation.*` options
# -- sharedDirectories, memorySize, etc. -- are first-class here.
# Previously these only existed transiently under `nixos-rebuild build-vm`.
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
({
pkgs,
lib,
...
}: {
networking.hostName = "micro-vm";
# Instantly drop you into a root shell on boot
services.getty.autologinUser = "root";
# Strip out all documentation and manuals to speed up evaluation
documentation.enable = false;
documentation.nixos.enable = false;
documentation.info.enable = false;
documentation.man.enable = false;
# Remove nano, perl, rsync, etc., from the default environment
environment.defaultPackages = lib.mkForce [];
# Include only the absolute essentials for debugging your kernel
environment.systemPackages = with pkgs; [
htop # Check CPU/Memory
pciutils # Run `lspci` to see virtio devices
kmod # Run `lsmod` to check module status
bpftools # Load and pin BPF programs
# --- Essential Kernel Dev Additions ---
# Visibility into what the kernel is doing
strace # The ultimate debugger for syscalls (would have caught your execve failure instantly)
util-linux # Provides dmesg, mount, lsblk, findmnt
# Binary inspection (Crucial for binfmt_misc testing)
file # Check ELF types and interpreters
binutils # readelf, objdump, strings
# Emergency editors and networking
vim # Or nano/vim, just in case you need to edit a script inside the VM
iproute2 # 'ip' command for network debugging
socat # Great for forwarding ports or serial consoles
];
# We let NixOS use its stock kernel here, so we can override
# it dynamically at runtime without rebuilding the VM script!
# Share the kernel source tree into the guest over 9p/virtio.
# This generates BOTH the QEMU `-virtfs` flag and the guest-side
# 9p mount unit automatically -- no manual `virtualisation.qemu.options`
# or `fileSystems.*` plumbing required.
#
# NOTE: binaries here must be built STATICally (see build_initramfs.sh /
# `pkgsStatic.gcc`); dynamically-linked selftest binaries won't find a
# loader at the FHS path on NixOS and fail with ENOENT.
virtualisation.sharedDirectories.host_project = {
source = "/home/fmzakari/code/github.com/torvalds/linux";
target = "/mnt/host_project";
};
# Give the VM room for BPF prog loading + tooling.
virtualisation.memorySize = 2048;
# nix-ld: run *dynamically-linked* non-Nix binaries (a plain
# `make -C tools/testing/selftests/exec`) directly in the guest.
# It installs a shim at the FHS loader path /lib64/ld-linux-x86-64.so.2
# and resolves libc from the libraries below -- so you no longer need
# the pkgsStatic dance for the selftest binaries. (This is the native
# nixpkgs module; the nix-community/nix-ld input is not required.)
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [ glibc ];
# Registering an emulated system makes the NixOS `boot.binfmt` module
# (nixos/modules/system/boot/binfmt.nix) mount binfmt_misc at boot --
# so /proc/sys/fs/binfmt_misc/register exists with no manual `mount`.
# As a bonus it drops a static qemu-aarch64 into the store that your
# BPF rule can use as its interpreter for the aarch64 demo.
#
# It also registers the *traditional* magic-based `qemu-aarch64`
# handler. To demo BPF *replacing* it, remove that one first in the
# guest: `echo -1 > /proc/sys/fs/binfmt_misc/qemu-aarch64`.
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
# Demo artifacts at stable paths for driving the demo by hand:
# /etc/binfmt-demo/filter.bpf.o - the aarch64 matcher + set_interp
# /etc/binfmt-demo/hello-aarch64 - a static aarch64 test binary
environment.etc."binfmt-demo/hello-aarch64" = {
source = aarch64Hello;
mode = "0555";
};
# Automated goal-2 validation. Only runs when `binfmt_autotest` is on
# the kernel command line (pass it via QEMU_KERNEL_PARAMS), so normal
# interactive boots are unaffected. Runs the demo, then powers off.
systemd.services.binfmt-bpf-demo = {
description = "binfmt_misc BPF aarch64 demo (autotest)";
after = [ "systemd-binfmt.service" ];
requires = [ "systemd-binfmt.service" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionKernelCommandLine = "binfmt_autotest";
serviceConfig = {
Type = "oneshot";
StandardOutput = "journal+console";
StandardError = "journal+console";
ExecStart = "${binfmtBpfDemo}";
ExecStopPost = "${pkgs.systemd}/bin/systemctl poweroff";
};
};
})
];
};
devShells.${system}.default = pkgs.mkShell {
name = "linux-kernel-dev";
# CRITICAL: Disable Nixpkgs compiler hardening
# The kernel bare-metal build handles its own hardening.
hardeningDisable = ["all"];
packages = with pkgs; [
# Compilation toolchain and dependencies
gcc
gnumake
flex
bison
elfutils
openssl
ncurses # Required for 'make menuconfig'
bc
perl
rsync
cpio
pkg-config
# Testing dependencies
qemu
clang # Compile the demo BPF object (-target bpf)
bpftools # bpftool: load/pin BPF programs
libbpf # libbpf headers (bpf_helpers.h) for the demo program
];
shellHook = ''
echo "🐧 Linux Kernel Dev Shell Loaded!"
echo "Compilers and QEMU are ready to go."
'';
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment