Created
July 12, 2026 03:48
-
-
Save fzakaria/0155e11a0882bd3d6e63f4070e7fac0c to your computer and use it in GitHub Desktop.
flake.nix to test Linux kernel with selftests and additional eBPF testing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| 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