Building an Out-of-Tree Linux Kernel Module in Nix

Published by Philipp Schuster on

In this blog post, I’m going to show you briefly how you can compile an out-of-tree Linux kernel module in Nix. I use regular Nix for that and no Nix flakes. However, with flakes, the approach is similar.

Background Knowledge

An out-of-tree Linux kernel module is a standalone piece of code outside the Linux kernel source repository. They are used when people don’t want to, are not allowed to or can’t upstream drivers. Background information on how to build them can be found on kernel.org. When a module is build, it must match a specific version of kernel source and kernel configuration.

Nix is a functional package manager suitable for reproducible and stable build environments.

Pre-Requisite: Packaged Linux Kernel in Nix

At first, we need a Nix-packaged kernel. For this, we can use one of the existing kernels in nixpkgs, such as pkgs.linux_6_2. We can either use the default configuration (pkgs.linux_6_2 as is) or a custom one, as in the following example:

kernel.nix

{ lib
, pkgs
}:

let
  # Function that builds a kernel from the provided Linux source with the
  # given config.
  buildKernel = selectedLinuxKernelPkg: pkgs.linuxKernel.manualConfig {
    inherit (pkgs) stdenv lib;

    src = selectedLinuxKernelPkg.src;

    # Add your kernel config file here.
    configfile = ./kernel.config;

    version = "${selectedLinuxKernelPkg.version}";
    # Probably that's a weird nixpkgs upstream thingy. Linux 6.2 wants
    # "6.2.0" instead of "6.2". Hence, I add ".0" here.
    modDirVersion = "${selectedLinuxKernelPkg.version}.0";

    allowImportFromDerivation = true;
  };
in
buildKernel pkgs.linux_6_2

The example above expects that you know how to get a Linux kernel config. The kernel.config file corresponds to the .config file in a Linux source tree build.

Both attributes, pkgs.linux_6_2 and the result of pkgs.callPackage ./kernel.nix {} do have the same structure. They only differ in their kernel configuration. This is relevant for the Nix derivation of the module following shortly.

Linux Driver

The driver used in this minimal example is written in C and might look like this:
foo.c

// basic definitions for kernel module development
#include <linux/module.h>

// Module/Driver description.
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <foo@bar.com>");
MODULE_DESCRIPTION("Description Foo Bar");

static int __init foobar_init(void) {
    int rc;
    pr_info("Foobar Driver inserted.\n");
    return 0;
}

static void __exit foobar_exit(void) {
    pr_info("Foobar Driver unloaded.\n");
}

module_init(foobar_init);
module_exit(foobar_exit);

A corresponding Makefile may look like this:

default: driver

obj-m = foo.o

.PHONY: clean default driver

driver:
	make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Packaging the Out-of-Tree Driver in Nix

However, we can’t use this Makefile as is easily in a Nix derivation (or on NixOS in general), as /lib/modules/$(shell uname -r)/ highly depends on system state which is not directly available in a Nix environment. (It would work on Debian-based systems for example, tho.) Instead, we must construct make flags in a Nix derivation that match the expectations of the kernel build system. The kernel_module.nix for the Kernel module then might look like this:

{ lib
# The Linux kernel Nix package for which this module is compiled.
# Either 'pkgs.linux_6_2` or the custom kernel created above.
, kernel
, stdenv
}:

stdenv.mkDerivation rec {
  pname = "foobar-driver";
  version = "0.1";

  src = ./.;

  setSourceRoot = ''
    export sourceRoot=$(pwd)/source
  '';

  nativeBuildInputs = kernel.moduleBuildDependencies;

  makeFlags = kernel.makeFlags ++ [
    "-C"
    "${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"
    "M=$(sourceRoot)"
  ];

  buildFlags = [ "modules" ];
  installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ];
  installTargets = [ "modules_install" ];

  meta = with lib; {
    description = "A foobar Driver for Linux";
    platforms = platforms.linux;
  };
}

I used an existing Nix-packaged kernel module as template, so thanks to the original author. Now, we only need to invoke the kernel_module.nix with its desired parameters and in the end, we get a kernel module for the given Linux kernel in result/.

Summary

The typical approach to build an out-of-tree Linux module doesn’t work on NixOS and in Nix derivations. Instead, we need some magic wrapping around the build. However, as you can see, the necessary clutter in the Nix file is manageable. And of course, Nix is awesome for many reasons, so the whole effort is worth it.

A fully working example can be found on my GitHub.


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *