{"article":{"slug":"infecting-the-steam-link-with-nixos","title":"Infecting the Steam Link with NixOS","subtitle":null,"summary":"While rummaging through my closet the other day I discovered a [Steam Link](https://en.wikipedia.org/wiki/Steam_Link Hardware_device) I had bought on flash sale way back in 2018 still dutifully humming away all these years later. It occured to me that having an always on low power Arm device with Ethernet, WiFi, Bluetooth, and several USB ports would be handy, so thus began my journey to get NixOS running on the Steam Link.","content_type":"blog_post","language":"en","canonical_url":"https://feyor.sh/blog/infecting-the-steam-link-with-nixos/","author":{"name":"George Huebner","url":"https://feyor.sh/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":null,"url":null,"listing_slug":null,"listing":null},"topics":[{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2996,"reading_minutes":13,"published_at":"2026-09-26T12:00:00.000Z","added_at":"2026-09-26T15:09:24.470Z","updated_at":"2026-09-26T15:09:24.470Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":false},"profile_url":"https://listedarticles.com/articles/infecting-the-steam-link-with-nixos","markdown_url":"https://listedarticles.com/articles/infecting-the-steam-link-with-nixos.md","example":false,"citation":"George Huebner. \"Infecting the Steam Link with NixOS.\" 26 Sept 2026. https://feyor.sh/blog/infecting-the-steam-link-with-nixos/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://feyor.sh/blog/infecting-the-steam-link-with-nixos/"},"body_markdown":"While rummaging through my closet the other day I discovered a [Steam Link](https://en.wikipedia.org/wiki/Steam_Link#Hardware_device) I had bought on flash sale way back in 2018 still dutifully humming away all these years later.\nIt occured to me that having an always-on low power Arm device with Ethernet, WiFi, Bluetooth, and several USB ports would be handy, so thus began my journey to get NixOS running on the Steam Link.\n\nAs it turns out, a fellow named [fijam](https://heap.ovh/getting-linux-on-valve-steam-link.html) already figured out the hard parts involved in running a custom Linux distro on the Steam Link.\nThe most notable obstacle is that the bootloader will only boot kernels signed by Valve; to get around this, we can boot into the Valve-blessed kernel and then `kexec` our new kernel.\nHowever, the kernel shipped with the Steam Link was not built with `CONFIG_KEXEC` enabled.\nThis is where things get *really* clever: we can cobble the pertinent kexec source files into a minimal kernel module that adds the `kexec` syscall to the running system!\n\nSeveral people have successfully used this technique to get other distros<sup>[1](https://feyor.sh#fn:1)</sup> booting, but they all seem to have just copied the `kexec` binary and kernel module from fijam’s website.\nfijam seems like a lovely person and all, but I’m wary of downloading kernel modules from the interwebs so I decided to compile it myself.\n\n## Booting the thing\n\nCompiling a NixOS userspace and kernel/initrd is pretty simple; you just pass the correct `system` (and because I’m using `__splicedPackages`/`crossSystem`, also `pkgs`) to `lib.nixosSystem` and add your modules.\nChoosing the target architecture was slightly less straightforward: Valve’s steamlink toolchain uses `armv7a`, but importing nixpkgs with `crossSystem.config = “armv7a-unknown-linux-gnueabihf”` interacts [poorly with the Go build plumbing](https://github.com/NixOS/nixpkgs/blob/fbe840e7184ed15fd5b1ca8f1a8746462a38d59c/lib/systems/default.nix#L596-L599), so I used the (seemingly) equivalent `armv7l` instead.\n\n## Nix\n\n```\n{\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n  outputs = { self, nixpkgs }:\n    let\n      inherit (nixpkgs) lib;\n      system = \"armv7l-linux\";\n      # hostSystem should be linux but does not have to be arm (x86 should work)\n      hostSystem = \"aarch64-linux\";\n      pkgs = (import nixpkgs {\n        system = hostSystem;\n        crossSystem = {\n          config = \"armv7l-unknown-linux-gnueabihf\";\n        };\n      }).__splicedPackages;\n    in {\n      nixosConfigurations.steamlink = lib.nixosSystem {\n        inherit system pkgs;\n        modules = [\n          # ...\n        ];\n      };\n    };\n}\n```\nThe real challenge is compiling a kernel module for a vendored fork of a 13 year old kernel; the [NixOS wiki](https://wiki.nixos.org/wiki/Linux_kernel#Packaging_out-of-tree_kernel_modules) is actually pretty helpful here and points out some footguns related to the default hardening flags in stdenv.\n\n## Nix\n\n```\nkexecMod = let\n  inherit (pkgs) stdenv;\n  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel oldKernel;\nin stdenv.mkDerivation {\n  pname = \"kexec_mod\";\n  version = \"0.0.1\";\n  src = ./kexec_mod;\n  postPatch = ''\n    for f in machine_kexec.c kexec.c relocate_kernel.S; do\n      substituteInPlace \"$f\" --subst-var-by KERNEL ${oldKernel}\n    done\n  '';\n  nativeBuildInputs = kernel.moduleBuildDependencies;\n  makeFlags = [\n    \"ARCH=${stdenv.hostPlatform.linuxArch}\"\n    \"CROSS_COMPILE=${stdenv.cc.targetPrefix}\"\n    \"KDIR=${oldKernel}\"\n    \"INSTALL_MOD_PATH=$(out)\"\n  ];\n  env.NIX_CFLAGS_COMPILE = toString [\n    \"-std=gnu89\"\n    \"-fno-pie\"\n  ];\n  inherit (kernel) hardeningDisable;\n  meta = {\n    description = \"kexec functionality as a kernel module for old kernels\";\n    homepage = \"https://github.com/lukas2511/steamlink-sdk\";\n    license = lib.licenses.gpl2;\n    platforms = [ system ];\n  };\n};\n```\n(See [Files](https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#files) for the kexec_mod source code.)\n\nIn order to get this to build we need to point Kbuild to a Linux kernel checkout that has been built with `make modules`<sup>[2](https://feyor.sh#fn:2)</sup>.\n(Note that I’m using the `moduleBuildDependencies` attribute of the comparatively modern `kernel` from my NixOS configuration.)\n\n## Nix\n\n```\noldKernel = let\n  inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;\n  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel;\nin stdenv.mkDerivation {\n  pname = \"linux-steamlink\";\n  version = \"3.8.13\";\n  src = fetchFromGitHub {\n    owner = \"ValveSoftware\";\n    repo = \"steamlink-sdk\";\n    rootDir = \"kernel\";\n    rev = \"62b4d098d1472c3534dd098ca2a0e0e10712f1c6\";\n    hash = \"sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=\";\n  };\n  sourceRoot = \"source\";\n  patches = [\n    (fetchpatch {\n      url = \"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch\";\n      hash = \"sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=\";\n    })\n    (fetchpatch {\n      url = \"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch\";\n      hash = \"sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=\";\n    })\n    (writeText \"0001-gcc-bug85745.diff\"\n      ''\n        diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h\n        index 74b17d0..dc64fa2 100644\n        --- a/arch/arm/include/asm/uaccess.h\n        +++ b/arch/arm/include/asm/uaccess.h\n        @@ -164,7 +164,7 @@\n         #define __put_user_check(x,p)''\\t''\\t''\\t''\\t''\\t''\\t''\\t${\"\\\\\"}\n         ''\\t({''\\t''\\t''\\t''\\t''\\t''\\t''\\t''\\t${\"\\\\\"}\n         ''\\t''\\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${\"\\\\\"}\n        -''\\t''\\tregister const typeof(*(p)) __r2 asm(\"r2\") = (x);''\\t${\"\\\\\"}\n        +''\\t''\\tregister typeof(*(p)) __r2 asm(\"r2\") = (x);''\\t${\"\\\\\"}\n         ''\\t''\\tregister const typeof(*(p)) __user *__p asm(\"r0\") = (p);${\"\\\\\"}\n         ''\\t''\\tregister unsigned long __l asm(\"r1\") = __limit;''\\t''\\t${\"\\\\\"}\n         ''\\t''\\tregister int __e asm(\"r0\");''\\t''\\t''\\t''\\t${\"\\\\\"}\n      '')\n  ];\n  postPatch = ''\n    substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' \"a\"'\n    substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'\n    substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail '${\"\\t\"}@$(check_for_multiple_zreladdr)' '${\"\\t\"}echo LDFLAGS_vmlinux = ''${LDFLAGS_vmlinux}${\"\\n\\t\"}@$(check_for_multiple_zreladdr)'\n    cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h\n  '';\n  inherit (kernel) nativeBuildInputs;\n  depsBuildBuild = [\n    buildPackages.stdenv.cc\n  ];\n  makeFlags = [\n    \"ARCH=${stdenv.hostPlatform.linuxArch}\"\n    \"LOCALVERSION=-mrvl\"\n    \"CROSS_COMPILE=${stdenv.cc.targetPrefix}\"\n  ];\n  env.NIX_CFLAGS_COMPILE = toString [\n    \"-std=gnu89\"\n    \"-Wno-error=address\"\n    \"-Wno-error=dangling-pointer\"\n    \"-Wno-error=missing-attributes\"\n  ];\n  inherit (kernel) hardeningDisable;\n  configurePhase = ''\n    make bg2cd_penguin_mlc_defconfig $makeFlags\n    echo \"CONFIG_KEXEC=y\" >> .config\n    echo \"CONFIG_KERNEL_XZ=y\" >> .config\n    make olddefconfig $makeFlags\n  '';\n  postBuild = ''\n    make modules $makeFlags -j$NIX_BUILD_CORES\n  '';\n  installPhase = ''\n    mkdir $out\n    cp -r * $out/\n  '';\n  dontFixup = true;\n};\n```\nIt took several hacks to get things building on a modern version of GCC, but eventually I was able to get the 3.8.13-mrvl kernel and the kexec_mod kernel module building.\n\nNow that we have `kexec_mod.ko`, we need our new initrd and kernel (which all come from our NixOS config), the device tree blob for the Steam Link (which has been [upstreamed](https://github.com/torvalds/linux/blob/6812ce4e4379ffc99c52401ec28f0d7ffbc36206/arch/arm/boot/dts/synaptics/berlin2cd-valve-steamlink.dts) to Linux so we can get it from `hardware.deviceTree.package`), a copy of the `kexec` userland binary (compiled with `pkgsStatic` so we can run it on non-NixOS), and a small script to tie everything together:\n\n## Bash\n\n```\nfts-set steamlink.crashcounter 0 # required to prevent factory reset after a few reboots\nmkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev\nmount -t proc proc /mnt/disk/proc\nmount -o rbind /sys /mnt/disk/sys\nmount -o rbind /dev /mnt/disk/dev\ninsmod /mnt/disk/kexec_load.ko\nchroot /mnt/disk/ /kexec --load /zImage \\\n                         --initrd /initrd \\\n                         --dtb /berlin2cd-valve-steamlink.dtb \\\n                         --command-line \"init=/init root=/dev/sda2 rootwait rw usbcore.autosuspend=-1\"\nchroot /mnt/disk/ /kexec -e\n```\nYou could juggle these files manually and upload them to a USB drive yourself, but it’s much easier to use the [sd-image](https://github.com/NixOS/nixpkgs/blob/5f5458dc42bf4391dc5f85e7682decb8c78e8756/nixos/modules/installer/sd-card/sd-image.nix) NixOS module to create a disk image instead:\n\n## Nix\n\n```\nusb-image = { modulesPath, config, ... }: {\n  imports = [\n    (modulesPath + \"/installer/sd-card/sd-image.nix\")\n  ];\n  image.extension = lib.mkForce \"img\";\n  sdImage = let\n    dtb = \"berlin2cd-valve-steamlink.dtb\";\n    kexecScript = ./kexec-nixos;\n    inherit (config.system.build) kernel initialRamdisk;\n  in {\n    compressImage = false;\n    firmwarePartitionName = \"STEAMLINK\";\n    rootVolumeLabel = \"NIXOS\";\n    populateFirmwareCommands = ''\n      pushd firmware\n      files=(\n        ${kernel}/${config.system.boot.loader.kernelFile}\n        ${initialRamdisk}/${config.system.boot.loader.initrdFile}\n        ${config.hardware.deviceTree.package}/${dtb}\n        ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko\n        ${pkgs.pkgsStatic.kexec-tools}/bin/kexec\n      )\n      for f in ''${files[@]}; do\n        cp $f ./\n      done\n      # factory_test/run.sh will run before this has a chance to\n      # enable ssh; uncomment if not booting straight into NixOS\n      # mkdir -p steamlink/config/system\n      # touch steamlink/config/system/enable_ssh.txt\n      mkdir -p steamlink/factory_test\n      cp ${kexecScript} steamlink/factory_test/run.sh\n      popd\n    '';\n    populateRootCommands = \"\";\n  };\n};\n```\nTesting that the `kexec` handoff works was really tricky because the HDMI output doesn’t work with the kernel I’m using, and since I decided not to open up the device to get at the UART I was flying completely blind.\nI decided to test with a minimal (slop) Busybox-based initramfs that rebooted after a variable amount of time to indicate success.\n\n## Nix\n\n```\ninitramfs = pkgs.buildPackages.runCommand \"build-initramfs\" {}\n  ''\n    mkdir initramfs; cd initramfs\n    mkdir -pv {etc,proc,sys,usr/{bin,sbin}}\n    cp -a ${pkgs.pkgsStatic.busybox}/{bin,sbin} .\n    chmod 755 ./{bin,sbin}\n    cat <<EOF > init\n    #!/bin/sh\n    mount -t proc none /proc\n    mount -t sysfs none /sys\n    mount -t devtmpfs devtmpfs /dev\n    mkdir -p /mnt\n    try_mount() {\n      dev=\"$1\"\n      fs=\"$2\"\n      if [ \"$fs\" = auto ]; then\n        mount -o rw \"$dev\" /mnt 2>/dev/null || return 1\n      else\n        mount -t \"$fs\" -o rw \"$dev\" /mnt 2>/dev/null || return 1\n      fi\n      marker=kexec-mounted-ok\n      if [ -f /mnt/zImage ]; then\n        marker=kexec-steamlink-ok\n      fi\n      {\n        echo \"device=$dev\"\n        echo \"fs=$fs\"\n        cat /proc/partitions\n      } > \"/mnt/$marker\" 2>/dev/null && sync\n      umount /mnt\n      sleep 10\n      reboot -f\n    }\n    for dev in /dev/mmcblk*p* /dev/sd[a-z][0-9]* /dev/vd[a-z][0-9]*; do\n      [ -b \"$dev\" ] || continue\n      try_mount \"$dev\" vfat\n      try_mount \"$dev\" ext4\n      try_mount \"$dev\" auto\n    done\n    sleep 45\n    reboot -f\n    EOF\n    chmod +x init\n    find . -print0 | ${lib.getExe pkgs.buildPackages.cpio} --null -ov --format=newc > $out\n  '';\n```\nOnce I knew that worked, I switched to the NixOS initrd with `boot.initrd.network.enable = true` and used a Netcat based reverse shell to my laptop’s IP for further debugging.\n\n## Nix\n\n```\ndebugModule = { lib, ... }: {\n  boot.initrd.systemd.enable = lib.mkForce false;\n  boot.initrd.kernelModules = [ \"pxa168_eth\" ];\n  boot.initrd.availableKernelModules = [ \"reset_berlin\" ];\n  boot.initrd.network.enable = true;\n  boot.initrd.network.udhcpc.enable = false;\n  boot.kernelParams = [\n    \"ip=192.168.2.2::192.168.2.1:255.255.255.0:stm-link:eth0:off\"\n  ];\n  boot.initrd.network.postCommands = ''\n    mac_peer=192.168.2.1\n    stm_link_ip=192.168.2.2\n    echo \"initrd net debug: interfaces: $(ls /sys/class/net)\" > /dev/kmsg\n    for iface_path in /sys/class/net/*; do\n      iface=\"''${iface_path##*/}\"\n      [ \"$iface\" != lo ] || continue\n      echo \"initrd net debug: configuring $iface\" > /dev/kmsg\n      ip link set dev \"$iface\" up || true\n      ip address flush dev \"$iface\" || true\n      ip address add \"$stm_link_ip/24\" dev \"$iface\" || true\n    done\n    (\n      while true; do\n        ping -c 1 -W 1 \"$mac_peer\"\n        sleep 2\n      done\n    ) &\n    (\n      while true; do\n        rm -f /tmp/revsh\n        mkfifo /tmp/revsh\n        /bin/ash -i < /tmp/revsh 2>&1 | nc \"$mac_peer\" 4444 > /tmp/revsh\n        rm -f /tmp/revsh\n        sleep 2\n      done\n    ) &\n  '';\n};\n```\nThe main things I needed to figure out at this stage were adding `reset_berlin` to `boot.initrd.availableKernelModules` to allow reading from the USB drive and using the old NixOS initrd system in lieu of the new systemd-based version (`boot.initrd.systemd.enable = lib.mkForce false`).\n\nFinally I was able to boot into userspace and connect over SSH! 🥳\n\nThat having been said, the USB image I was booting from was weighing in at a hefty 2.3GB… surely we can do better.\n\n## Trimming the fat\n\nI was surprised that there wasn’t a definitive guide for reducing NixOS closure sizes; I found some NixOS Discourse questions and a few blog posts, but the most useful writeups were [NixOS is a good server OS, except when it isn’t](https://sidhion.com/blog/nixos_server_issues) and [I can haz smoller NixOS ISOs?](https://natkr.com/2026-06-19-nixos-but-smol/).\nThose are good resources, but because we’re targeting actual hardware instead of a VM we must necessarily be more conservative in what we cut.\n\n## Nix\n\n```\nminimal = { modulesPath, pkgs, ... }: {\n  imports = [\n    (modulesPath + \"/profiles/minimal.nix\")\n    (modulesPath + \"/profiles/headless.nix\")\n    # (modulesPath + \"/profiles/perlless.nix\")\n  ];\n  disabledModules = [\n    (modulesPath + \"/profiles/base.nix\")\n  ];\n  boot.loader = {\n    grub.enable = false;\n    systemd-boot.enable = false;\n    supportsInitrdSecrets = false;\n  };\n  boot.initrd.systemd.enable = lib.mkForce false;\n  boot.initrd.availableKernelModules = lib.mkForce [\n    \"reset_berlin\"\n    \"uas\"\n  ];\n  boot.kernelModules = [\n    \"pxa168_eth\"\n    \"mwifiex_sdio\"\n    \"btmrvl_sdio\"\n  ];\n  hardware.firmware = lib.mkForce (with pkgs; [\n    (runCommand \"marvell-firmware\" {} ''\n      mkdir -p $out/lib/firmware/mrvl\n      cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/\n    '')\n    wireless-regdb\n  ]);\n  documentation.enable = false;\n  programs.command-not-found.enable = lib.mkDefault false;\n  networking.networkmanager.enable = false;\n  networking.firewall.enable = false;\n  xdg.icons.enable  = false;\n  xdg.mime.enable   = false;\n  xdg.sounds.enable = false;\n  fonts.fontconfig.enable = false;\n  programs.nano.enable = false;\n  system.disableInstallerTools = true;\n  system.switch.enable = false;\n  system.nixos-init.enable = false;\n  nix.enable = false;\n  systemd.services.register-nix-paths = lib.mkForce {};\n};\n```\nHere are the main things that came up during the slim-ening:\n\n- We only need one firmware blob from the massive `linux-firmware` package (1.8GB compressed!), so we can save a huge amount of space by only adding that blob to`hardware.firmware`  - On a similar note it should be possible to use a kconfig tailored for the Steam Link hardware to build a smaller kernel, but this seemed like more trouble than it was worth\n- OpenSSH does not seem to like it when `i18n.glibcLocales` or the`security.wrappers` module are removed\n- The `kexec` boot flow renders most of the NixOS system administration utilities irrelevant; as a matter of fact, Nix itself is not very useful on such a system, so we can save space by getting rid of that too\n- I feel like it should be possible to disable `boot.initrd` and`boot.kernel` because we provide the kernel/initrd/DTB from the FAT32 partition when we`kexec` , but I was never able to disable these without breaking the boot process\n- If I could switch to the systemd-based initrd I could enable the “perlless” NixOS module to remove Perl from the system closure for additional savings\n\nAfter reaching a point of diminshing returns and most new changes breaking my system, I declared the 1.2GB disk image I had to be “good enough”.\n\n## Files\n\nThe Nix flake I used and the source for the `kexec_mod` kernel module can be downloaded [here](https://feyor.sh/infecting-the-steam-link-with-nixos/steamlink-nixos.tar.gz).\nThe same `flake.nix` is reproduced below for your convenience.\n\n## \n      Nix\n      flake.nix\n    \n  \n  \n\n  ```\n{\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n  outputs = { self, nixpkgs }:\n    let\n      inherit (nixpkgs) lib;\n      system = \"armv7l-linux\";\n      # hostSystem should be linux but does not have to be arm (x86 should work)\n      hostSystem = \"aarch64-linux\";\n      pkgs = (import nixpkgs {\n        system = hostSystem;\n        crossSystem = {\n          config = \"armv7l-unknown-linux-gnueabihf\";\n        };\n        overlays = [\n          # https://github.com/NixOS/nixpkgs/issues/388309\n          (self: super: {\n            efivar = self.emptyDirectory;\n            efibootmgr = self.emptyDirectory;\n          })\n        ];\n      }).__splicedPackages;\n    in {\n      nixosModules = {\n        # https://sidhion.com/blog/nixos_server_issues\n        # https://discourse.nixos.org/t/how-to-have-a-minimal-nixos/22652/4\n        minimal = { modulesPath, pkgs, ... }: {\n          imports = [\n            (modulesPath + \"/profiles/minimal.nix\")\n            (modulesPath + \"/profiles/headless.nix\")\n          ];\n          disabledModules = [\n            (modulesPath + \"/profiles/base.nix\")\n          ];\n          boot.loader = {\n            grub.enable = false;\n            systemd-boot.enable = false;\n            supportsInitrdSecrets = false;\n          };\n          # systemd initrd did not work for me; you could add the perlless profile if you got it working\n          boot.initrd.systemd.enable = lib.mkForce false;\n          boot.initrd.availableKernelModules = lib.mkForce [\n            \"reset_berlin\"\n            \"uas\"\n          ];\n          boot.kernelModules = [\n            \"pxa168_eth\"\n            \"mwifiex_sdio\"\n            \"btmrvl_sdio\"\n          ];\n          hardware.firmware = lib.mkForce (with pkgs; [\n            (runCommand \"marvell-firmware\" {} ''\n              mkdir -p $out/lib/firmware/mrvl\n              cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/\n            '')\n            wireless-regdb\n          ]);\n          documentation.enable = false;\n          programs.command-not-found.enable = lib.mkDefault false;\n          networking.networkmanager.enable = false;\n          networking.firewall.enable = false;\n          xdg.icons.enable  = false;\n          xdg.mime.enable   = false;\n          xdg.sounds.enable = false;\n          fonts.fontconfig.enable = false;\n          programs.nano.enable = false;\n          system.disableInstallerTools = true;\n          system.switch.enable = false;\n          system.nixos-init.enable = false;\n          nix.enable = false;\n          systemd.services.register-nix-paths = lib.mkForce {};\n        };\n        usb-image = { modulesPath, config, ... }: {\n          imports = [\n            (modulesPath + \"/installer/sd-card/sd-image.nix\")\n          ];\n          # in theory it should be possible to disable the kernel\n          # and initrd for the nixos rootfs for some significant\n          # space savings (because we're passing a copy of the\n          # kernel and initrd from the STEAMLINK partition to kexec\n          # directly, but in practice I never got that working)\n          boot.kernelParams = [\n            \"root=/dev/disk/by-label/${config.sdImage.rootVolumeLabel}\" \"rootwait\" \"rw\"\n            \"usbcore.autosuspend=-1\"\n          ];\n          image.extension = lib.mkForce \"img\";\n          sdImage = let\n            dtb = \"berlin2cd-valve-steamlink.dtb\";\n            kexecScript = pkgs.buildPackages.writeScript \"kexec-nixos\" ''\n              #!/bin/sh\n              fts-set steamlink.crashcounter 0\n              mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev\n              mount -t proc proc /mnt/disk/proc\n              mount -o rbind /sys /mnt/disk/sys\n              mount -o rbind /dev /mnt/disk/dev\n              insmod /mnt/disk/kexec_load.ko\n              chroot /mnt/disk/ /kexec --load /zImage \\\n                                       --initrd /initrd \\\n                                       --dtb /berlin2cd-valve-steamlink.dtb \\\n                                       --command-line \"init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}\"\n              chroot /mnt/disk/ /kexec -e\n            '';\n            inherit (self.packages.${system}) kernel initialRamdisk;\n          in {\n            compressImage = false;\n            firmwarePartitionName = \"STEAMLINK\";\n            rootVolumeLabel = \"NIXOS\";\n            populateFirmwareCommands = ''\n              pushd firmware\n              files=(\n                ${kernel}/${config.system.boot.loader.kernelFile}\n                ${initialRamdisk}/${config.system.boot.loader.initrdFile}\n                ${config.hardware.deviceTree.package}/${dtb}\n                ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko\n                ${pkgs.pkgsStatic.kexec-tools}/bin/kexec\n              )\n              for f in ''${files[@]}; do\n                cp $f ./\n              done\n              # factory_test/run.sh will run before this has a chance to\n              # enable ssh; uncomment if not booting straight into NixOS\n              # mkdir -p steamlink/config/system\n              # touch steamlink/config/system/enable_ssh.txt\n              mkdir -p steamlink/factory_test\n              cp ${kexecScript} steamlink/factory_test/run.sh\n              popd\n            '';\n            populateRootCommands = \"\";\n          };\n        };\n      };\n      nixosConfigurations.steamlink = lib.nixosSystem {\n        inherit system pkgs;\n        modules = [\n          self.nixosModules.minimal\n          self.nixosModules.usb-image\n          ({ ... }: {\n            # your NixOS config here!\n            services.tailscale.enable = true;\n            services.openssh = {\n              # might be able to remove security wrappers if using static openssh\n              # see https://sidhion.com/blog/nixos_server_issues#:~:text=While%20looking%20through%20the%20lvm%20stuff\n              # package = pkgs.pkgsStatic.openssh;\n              enable = true;\n              settings = {\n                PermitRootLogin = \"yes\";\n              };\n            };\n            users.users.root.openssh.authorizedKeys.keys = [ \"...\" ];\n            hardware.bluetooth.enable = true;\n            networking = {\n              hostName = \"steamlink\";\n              useDHCP = true;\n              interfaces.eth0 = {\n                useDHCP = true;\n                # prefer DHCP but use a static IP for debugging over a direct ethernet serial line to your host machine\n                ipv4.addresses = [{\n                  address = \"169.254.31.216\";\n                  prefixLength = 16;\n                }];\n              };\n              wireless = {\n                enable = true;\n                networks  = {\n                  \"WiFi\" = {\n                    psk = \"hunter2\";\n                  };\n                };\n              };\n            };\n          })\n        ];\n      };\n      packages.${system} = {\n        inherit (self.nixosConfigurations.steamlink.config.system.build) kernel initialRamdisk sdImage;\n        default = self.packages.${system}.sdImage;\n        oldKernel = let\n          inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;\n          inherit (self.packages.${system}) kernel;\n        in stdenv.mkDerivation {\n          pname = \"linux-steamlink\";\n          version = \"3.8.13\";\n          src = fetchFromGitHub {\n            owner = \"ValveSoftware\";\n            repo = \"steamlink-sdk\";\n            rootDir = \"kernel\";\n            rev = \"62b4d098d1472c3534dd098ca2a0e0e10712f1c6\";\n            hash = \"sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=\";\n          };\n          sourceRoot = \"source\";\n          patches = [\n            (fetchpatch {\n              url = \"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch\";\n              hash = \"sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=\";\n            })\n            (fetchpatch {\n              url = \"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch\";\n              hash = \"sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=\";\n            })\n            (writeText \"0001-gcc-bug85745.diff\"\n              ''\n                diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h\n                index 74b17d0..dc64fa2 100644\n                --- a/arch/arm/include/asm/uaccess.h\n                +++ b/arch/arm/include/asm/uaccess.h\n                @@ -164,7 +164,7 @@\n                 #define __put_user_check(x,p)''\\t''\\t''\\t''\\t''\\t''\\t''\\t${\"\\\\\"}\n                 ''\\t({''\\t''\\t''\\t''\\t''\\t''\\t''\\t''\\t${\"\\\\\"}\n                 ''\\t''\\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${\"\\\\\"}\n                -''\\t''\\tregister const typeof(*(p)) __r2 asm(\"r2\") = (x);''\\t${\"\\\\\"}\n                +''\\t''\\tregister typeof(*(p)) __r2 asm(\"r2\") = (x);''\\t${\"\\\\\"}\n                 ''\\t''\\tregister const typeof(*(p)) __user *__p asm(\"r0\") = (p);${\"\\\\\"}\n                 ''\\t''\\tregister unsigned long __l asm(\"r1\") = __limit;''\\t''\\t${\"\\\\\"}\n                 ''\\t''\\tregister int __e asm(\"r0\");''\\t''\\t''\\t''\\t${\"\\\\\"}\n              '')\n          ];\n          postPatch = ''\n            substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' \"a\"'\n            substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'\n            substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail '${\"\\t\"}@$(check_for_multiple_zreladdr)' '${\"\\t\"}echo LDFLAGS_vmlinux = ''${LDFLAGS_vmlinux}${\"\\n\\t\"}@$(check_for_multiple_zreladdr)'\n            cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h\n          '';\n          inherit (kernel) nativeBuildInputs;\n          depsBuildBuild = [\n            buildPackages.stdenv.cc\n          ];\n          makeFlags = [\n            \"ARCH=${stdenv.hostPlatform.linuxArch}\"\n            \"LOCALVERSION=-mrvl\"\n            \"CROSS_COMPILE=${stdenv.cc.targetPrefix}\"\n          ];\n          env.NIX_CFLAGS_COMPILE = toString [\n            \"-std=gnu89\"\n            \"-Wno-error=address\"\n            \"-Wno-error=dangling-pointer\"\n            \"-Wno-error=missing-attributes\"\n          ];\n          inherit (kernel) hardeningDisable;\n          configurePhase = ''\n            make bg2cd_penguin_mlc_defconfig $makeFlags\n            echo \"CONFIG_KEXEC=y\" >> .config\n            echo \"CONFIG_KERNEL_XZ=y\" >> .config\n            make olddefconfig $makeFlags\n          '';\n          postBuild = ''\n            make modules $makeFlags -j$NIX_BUILD_CORES\n          '';\n          installPhase = ''\n            mkdir $out\n            cp -r * $out/\n          '';\n          dontFixup = true;\n        };\n        kexecMod = let\n          inherit (pkgs) stdenv;\n          inherit (self.packages.${system}) kernel oldKernel;\n        in stdenv.mkDerivation {\n          pname = \"kexec_mod\";\n          version = \"0.0.1\";\n          src = ./kexec_mod;\n          postPatch = ''\n            for f in machine_kexec.c kexec.c relocate_kernel.S; do\n              substituteInPlace \"$f\" --subst-var-by KERNEL ${oldKernel}\n            done\n          '';\n          nativeBuildInputs = kernel.moduleBuildDependencies;\n          makeFlags = [\n            \"ARCH=${stdenv.hostPlatform.linuxArch}\"\n            \"CROSS_COMPILE=${stdenv.cc.targetPrefix}\"\n            \"KDIR=${oldKernel}\"\n            \"INSTALL_MOD_PATH=$(out)\"\n          ];\n          env.NIX_CFLAGS_COMPILE = toString [\n            \"-std=gnu89\"\n            \"-fno-pie\"\n          ];\n          inherit (kernel) hardeningDisable;\n          meta = {\n            description = \"kexec functionality as a kernel module for old kernels\";\n            homepage = \"https://github.com/lukas2511/steamlink-sdk\";\n            license = lib.licenses.gpl2;\n            platforms = [ system ];\n          };\n        };\n      };\n    };\n}\n```\n1. \nRight before I published this I discovered [someone else](https://github.com/Ondra-Zik/steamlink-nixos) had vibed their way to a bootable NixOS install, although their config is more _slop_py, doesn’t handle reboots correctly, and uses a bunch of unnecessary binary blobs instead of building from source.[↩︎](https://feyor.sh#fnref:1)\n2. \nAlthough using `make modules_prepare` lets us build successfully, the resulting kernel module will not have the right vermagic and symbol addresses and will not be accepted by`insmod` :NOTE: “modules_prepare” will not build Module.symvers even if `CONFIG_MODVERSIONS` is set; therefore, a full kernel build needs to be executed to make module versioning work.( [source](https://www.kernel.org/doc/html/latest/kbuild/modules.html%20) )[↩︎](https://feyor.sh#fnref:2)","body_html":"<p>While rummaging through my closet the other day I discovered a <a href=\"https://en.wikipedia.org/wiki/Steam_Link#Hardware_device\" rel=\"nofollow ugc noopener\">Steam Link</a> I had bought on flash sale way back in 2018 still dutifully humming away all these years later.\nIt occured to me that having an always-on low power Arm device with Ethernet, WiFi, Bluetooth, and several USB ports would be handy, so thus began my journey to get NixOS running on the Steam Link.</p>\n<p>As it turns out, a fellow named <a href=\"https://heap.ovh/getting-linux-on-valve-steam-link.html\" rel=\"nofollow ugc noopener\">fijam</a> already figured out the hard parts involved in running a custom Linux distro on the Steam Link.\nThe most notable obstacle is that the bootloader will only boot kernels signed by Valve; to get around this, we can boot into the Valve-blessed kernel and then <code>kexec</code> our new kernel.\nHowever, the kernel shipped with the Steam Link was not built with <code>CONFIG_KEXEC</code> enabled.\nThis is where things get <em>really</em> clever: we can cobble the pertinent kexec source files into a minimal kernel module that adds the <code>kexec</code> syscall to the running system!</p>\n<p>Several people have successfully used this technique to get other distros&lt;sup&gt;<a href=\"https://feyor.sh#fn:1\" rel=\"nofollow ugc noopener\">1</a>&lt;/sup&gt; booting, but they all seem to have just copied the <code>kexec</code> binary and kernel module from fijam’s website.\nfijam seems like a lovely person and all, but I’m wary of downloading kernel modules from the interwebs so I decided to compile it myself.</p>\n<h2 id=\"booting-the-thing\">Booting the thing</h2>\n<p>Compiling a NixOS userspace and kernel/initrd is pretty simple; you just pass the correct <code>system</code> (and because I’m using <code>__splicedPackages</code>/<code>crossSystem</code>, also <code>pkgs</code>) to <code>lib.nixosSystem</code> and add your modules.\nChoosing the target architecture was slightly less straightforward: Valve’s steamlink toolchain uses <code>armv7a</code>, but importing nixpkgs with <code>crossSystem.config = “armv7a-unknown-linux-gnueabihf”</code> interacts <a href=\"https://github.com/NixOS/nixpkgs/blob/fbe840e7184ed15fd5b1ca8f1a8746462a38d59c/lib/systems/default.nix#L596-L599\" rel=\"nofollow ugc noopener\">poorly with the Go build plumbing</a>, so I used the (seemingly) equivalent <code>armv7l</code> instead.</p>\n<h2 id=\"nix\">Nix</h2>\n<pre><code>{\n  inputs.nixpkgs.url = &quot;github:NixOS/nixpkgs/nixos-unstable&quot;;\n  outputs = { self, nixpkgs }:\n    let\n      inherit (nixpkgs) lib;\n      system = &quot;armv7l-linux&quot;;\n      # hostSystem should be linux but does not have to be arm (x86 should work)\n      hostSystem = &quot;aarch64-linux&quot;;\n      pkgs = (import nixpkgs {\n        system = hostSystem;\n        crossSystem = {\n          config = &quot;armv7l-unknown-linux-gnueabihf&quot;;\n        };\n      }).__splicedPackages;\n    in {\n      nixosConfigurations.steamlink = lib.nixosSystem {\n        inherit system pkgs;\n        modules = [\n          # ...\n        ];\n      };\n    };\n}</code></pre>\n<p>The real challenge is compiling a kernel module for a vendored fork of a 13 year old kernel; the <a href=\"https://wiki.nixos.org/wiki/Linux_kernel#Packaging_out-of-tree_kernel_modules\" rel=\"nofollow ugc noopener\">NixOS wiki</a> is actually pretty helpful here and points out some footguns related to the default hardening flags in stdenv.</p>\n<h2 id=\"nix-2\">Nix</h2>\n<pre><code>kexecMod = let\n  inherit (pkgs) stdenv;\n  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel oldKernel;\nin stdenv.mkDerivation {\n  pname = &quot;kexec_mod&quot;;\n  version = &quot;0.0.1&quot;;\n  src = ./kexec_mod;\n  postPatch = &#39;&#39;\n    for f in machine_kexec.c kexec.c relocate_kernel.S; do\n      substituteInPlace &quot;$f&quot; --subst-var-by KERNEL ${oldKernel}\n    done\n  &#39;&#39;;\n  nativeBuildInputs = kernel.moduleBuildDependencies;\n  makeFlags = [\n    &quot;ARCH=${stdenv.hostPlatform.linuxArch}&quot;\n    &quot;CROSS_COMPILE=${stdenv.cc.targetPrefix}&quot;\n    &quot;KDIR=${oldKernel}&quot;\n    &quot;INSTALL_MOD_PATH=$(out)&quot;\n  ];\n  env.NIX_CFLAGS_COMPILE = toString [\n    &quot;-std=gnu89&quot;\n    &quot;-fno-pie&quot;\n  ];\n  inherit (kernel) hardeningDisable;\n  meta = {\n    description = &quot;kexec functionality as a kernel module for old kernels&quot;;\n    homepage = &quot;https://github.com/lukas2511/steamlink-sdk&quot;;\n    license = lib.licenses.gpl2;\n    platforms = [ system ];\n  };\n};</code></pre>\n<p>(See <a href=\"https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#files\" rel=\"nofollow ugc noopener\">Files</a> for the kexec_mod source code.)</p>\n<p>In order to get this to build we need to point Kbuild to a Linux kernel checkout that has been built with <code>make modules</code>&lt;sup&gt;<a href=\"https://feyor.sh#fn:2\" rel=\"nofollow ugc noopener\">2</a>&lt;/sup&gt;.\n(Note that I’m using the <code>moduleBuildDependencies</code> attribute of the comparatively modern <code>kernel</code> from my NixOS configuration.)</p>\n<h2 id=\"nix-3\">Nix</h2>\n<pre><code>oldKernel = let\n  inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;\n  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel;\nin stdenv.mkDerivation {\n  pname = &quot;linux-steamlink&quot;;\n  version = &quot;3.8.13&quot;;\n  src = fetchFromGitHub {\n    owner = &quot;ValveSoftware&quot;;\n    repo = &quot;steamlink-sdk&quot;;\n    rootDir = &quot;kernel&quot;;\n    rev = &quot;62b4d098d1472c3534dd098ca2a0e0e10712f1c6&quot;;\n    hash = &quot;sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=&quot;;\n  };\n  sourceRoot = &quot;source&quot;;\n  patches = [\n    (fetchpatch {\n      url = &quot;https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch&quot;;\n      hash = &quot;sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=&quot;;\n    })\n    (fetchpatch {\n      url = &quot;https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch&quot;;\n      hash = &quot;sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=&quot;;\n    })\n    (writeText &quot;0001-gcc-bug85745.diff&quot;\n      &#39;&#39;\n        diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h\n        index 74b17d0..dc64fa2 100644\n        --- a/arch/arm/include/asm/uaccess.h\n        +++ b/arch/arm/include/asm/uaccess.h\n        @@ -164,7 +164,7 @@\n         #define __put_user_check(x,p)&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\\\&quot;}\n         &#39;&#39;\\t({&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\\\&quot;}\n         &#39;&#39;\\t&#39;&#39;\\tunsigned long __limit = current_thread_info()-&gt;addr_limit - 1; ${&quot;\\\\&quot;}\n        -&#39;&#39;\\t&#39;&#39;\\tregister const typeof(*(p)) __r2 asm(&quot;r2&quot;) = (x);&#39;&#39;\\t${&quot;\\\\&quot;}\n        +&#39;&#39;\\t&#39;&#39;\\tregister typeof(*(p)) __r2 asm(&quot;r2&quot;) = (x);&#39;&#39;\\t${&quot;\\\\&quot;}\n         &#39;&#39;\\t&#39;&#39;\\tregister const typeof(*(p)) __user *__p asm(&quot;r0&quot;) = (p);${&quot;\\\\&quot;}\n         &#39;&#39;\\t&#39;&#39;\\tregister unsigned long __l asm(&quot;r1&quot;) = __limit;&#39;&#39;\\t&#39;&#39;\\t${&quot;\\\\&quot;}\n         &#39;&#39;\\t&#39;&#39;\\tregister int __e asm(&quot;r0&quot;);&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\\\&quot;}\n      &#39;&#39;)\n  ];\n  postPatch = &#39;&#39;\n    substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail &#39;#alloc&#39; &#39; &quot;a&quot;&#39;\n    substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail &#39;/bin/bash&#39; &#39;${stdenv.shell}&#39;\n    substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail &#39;${&quot;\\t&quot;}@$(check_for_multiple_zreladdr)&#39; &#39;${&quot;\\t&quot;}echo LDFLAGS_vmlinux = &#39;&#39;${LDFLAGS_vmlinux}${&quot;\\n\\t&quot;}@$(check_for_multiple_zreladdr)&#39;\n    cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h\n  &#39;&#39;;\n  inherit (kernel) nativeBuildInputs;\n  depsBuildBuild = [\n    buildPackages.stdenv.cc\n  ];\n  makeFlags = [\n    &quot;ARCH=${stdenv.hostPlatform.linuxArch}&quot;\n    &quot;LOCALVERSION=-mrvl&quot;\n    &quot;CROSS_COMPILE=${stdenv.cc.targetPrefix}&quot;\n  ];\n  env.NIX_CFLAGS_COMPILE = toString [\n    &quot;-std=gnu89&quot;\n    &quot;-Wno-error=address&quot;\n    &quot;-Wno-error=dangling-pointer&quot;\n    &quot;-Wno-error=missing-attributes&quot;\n  ];\n  inherit (kernel) hardeningDisable;\n  configurePhase = &#39;&#39;\n    make bg2cd_penguin_mlc_defconfig $makeFlags\n    echo &quot;CONFIG_KEXEC=y&quot; &gt;&gt; .config\n    echo &quot;CONFIG_KERNEL_XZ=y&quot; &gt;&gt; .config\n    make olddefconfig $makeFlags\n  &#39;&#39;;\n  postBuild = &#39;&#39;\n    make modules $makeFlags -j$NIX_BUILD_CORES\n  &#39;&#39;;\n  installPhase = &#39;&#39;\n    mkdir $out\n    cp -r * $out/\n  &#39;&#39;;\n  dontFixup = true;\n};</code></pre>\n<p>It took several hacks to get things building on a modern version of GCC, but eventually I was able to get the 3.8.13-mrvl kernel and the kexec_mod kernel module building.</p>\n<p>Now that we have <code>kexec_mod.ko</code>, we need our new initrd and kernel (which all come from our NixOS config), the device tree blob for the Steam Link (which has been <a href=\"https://github.com/torvalds/linux/blob/6812ce4e4379ffc99c52401ec28f0d7ffbc36206/arch/arm/boot/dts/synaptics/berlin2cd-valve-steamlink.dts\" rel=\"nofollow ugc noopener\">upstreamed</a> to Linux so we can get it from <code>hardware.deviceTree.package</code>), a copy of the <code>kexec</code> userland binary (compiled with <code>pkgsStatic</code> so we can run it on non-NixOS), and a small script to tie everything together:</p>\n<h2 id=\"bash\">Bash</h2>\n<pre><code>fts-set steamlink.crashcounter 0 # required to prevent factory reset after a few reboots\nmkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev\nmount -t proc proc /mnt/disk/proc\nmount -o rbind /sys /mnt/disk/sys\nmount -o rbind /dev /mnt/disk/dev\ninsmod /mnt/disk/kexec_load.ko\nchroot /mnt/disk/ /kexec --load /zImage \\\n                         --initrd /initrd \\\n                         --dtb /berlin2cd-valve-steamlink.dtb \\\n                         --command-line &quot;init=/init root=/dev/sda2 rootwait rw usbcore.autosuspend=-1&quot;\nchroot /mnt/disk/ /kexec -e</code></pre>\n<p>You could juggle these files manually and upload them to a USB drive yourself, but it’s much easier to use the <a href=\"https://github.com/NixOS/nixpkgs/blob/5f5458dc42bf4391dc5f85e7682decb8c78e8756/nixos/modules/installer/sd-card/sd-image.nix\" rel=\"nofollow ugc noopener\">sd-image</a> NixOS module to create a disk image instead:</p>\n<h2 id=\"nix-4\">Nix</h2>\n<pre><code>usb-image = { modulesPath, config, ... }: {\n  imports = [\n    (modulesPath + &quot;/installer/sd-card/sd-image.nix&quot;)\n  ];\n  image.extension = lib.mkForce &quot;img&quot;;\n  sdImage = let\n    dtb = &quot;berlin2cd-valve-steamlink.dtb&quot;;\n    kexecScript = ./kexec-nixos;\n    inherit (config.system.build) kernel initialRamdisk;\n  in {\n    compressImage = false;\n    firmwarePartitionName = &quot;STEAMLINK&quot;;\n    rootVolumeLabel = &quot;NIXOS&quot;;\n    populateFirmwareCommands = &#39;&#39;\n      pushd firmware\n      files=(\n        ${kernel}/${config.system.boot.loader.kernelFile}\n        ${initialRamdisk}/${config.system.boot.loader.initrdFile}\n        ${config.hardware.deviceTree.package}/${dtb}\n        ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko\n        ${pkgs.pkgsStatic.kexec-tools}/bin/kexec\n      )\n      for f in &#39;&#39;${files[@]}; do\n        cp $f ./\n      done\n      # factory_test/run.sh will run before this has a chance to\n      # enable ssh; uncomment if not booting straight into NixOS\n      # mkdir -p steamlink/config/system\n      # touch steamlink/config/system/enable_ssh.txt\n      mkdir -p steamlink/factory_test\n      cp ${kexecScript} steamlink/factory_test/run.sh\n      popd\n    &#39;&#39;;\n    populateRootCommands = &quot;&quot;;\n  };\n};</code></pre>\n<p>Testing that the <code>kexec</code> handoff works was really tricky because the HDMI output doesn’t work with the kernel I’m using, and since I decided not to open up the device to get at the UART I was flying completely blind.\nI decided to test with a minimal (slop) Busybox-based initramfs that rebooted after a variable amount of time to indicate success.</p>\n<h2 id=\"nix-5\">Nix</h2>\n<pre><code>initramfs = pkgs.buildPackages.runCommand &quot;build-initramfs&quot; {}\n  &#39;&#39;\n    mkdir initramfs; cd initramfs\n    mkdir -pv {etc,proc,sys,usr/{bin,sbin}}\n    cp -a ${pkgs.pkgsStatic.busybox}/{bin,sbin} .\n    chmod 755 ./{bin,sbin}\n    cat &lt;&lt;EOF &gt; init\n    #!/bin/sh\n    mount -t proc none /proc\n    mount -t sysfs none /sys\n    mount -t devtmpfs devtmpfs /dev\n    mkdir -p /mnt\n    try_mount() {\n      dev=&quot;$1&quot;\n      fs=&quot;$2&quot;\n      if [ &quot;$fs&quot; = auto ]; then\n        mount -o rw &quot;$dev&quot; /mnt 2&gt;/dev/null || return 1\n      else\n        mount -t &quot;$fs&quot; -o rw &quot;$dev&quot; /mnt 2&gt;/dev/null || return 1\n      fi\n      marker=kexec-mounted-ok\n      if [ -f /mnt/zImage ]; then\n        marker=kexec-steamlink-ok\n      fi\n      {\n        echo &quot;device=$dev&quot;\n        echo &quot;fs=$fs&quot;\n        cat /proc/partitions\n      } &gt; &quot;/mnt/$marker&quot; 2&gt;/dev/null &amp;&amp; sync\n      umount /mnt\n      sleep 10\n      reboot -f\n    }\n    for dev in /dev/mmcblk*p* /dev/sd[a-z][0-9]* /dev/vd[a-z][0-9]*; do\n      [ -b &quot;$dev&quot; ] || continue\n      try_mount &quot;$dev&quot; vfat\n      try_mount &quot;$dev&quot; ext4\n      try_mount &quot;$dev&quot; auto\n    done\n    sleep 45\n    reboot -f\n    EOF\n    chmod +x init\n    find . -print0 | ${lib.getExe pkgs.buildPackages.cpio} --null -ov --format=newc &gt; $out\n  &#39;&#39;;</code></pre>\n<p>Once I knew that worked, I switched to the NixOS initrd with <code>boot.initrd.network.enable = true</code> and used a Netcat based reverse shell to my laptop’s IP for further debugging.</p>\n<h2 id=\"nix-6\">Nix</h2>\n<pre><code>debugModule = { lib, ... }: {\n  boot.initrd.systemd.enable = lib.mkForce false;\n  boot.initrd.kernelModules = [ &quot;pxa168_eth&quot; ];\n  boot.initrd.availableKernelModules = [ &quot;reset_berlin&quot; ];\n  boot.initrd.network.enable = true;\n  boot.initrd.network.udhcpc.enable = false;\n  boot.kernelParams = [\n    &quot;ip=192.168.2.2::192.168.2.1:255.255.255.0:stm-link:eth0:off&quot;\n  ];\n  boot.initrd.network.postCommands = &#39;&#39;\n    mac_peer=192.168.2.1\n    stm_link_ip=192.168.2.2\n    echo &quot;initrd net debug: interfaces: $(ls /sys/class/net)&quot; &gt; /dev/kmsg\n    for iface_path in /sys/class/net/*; do\n      iface=&quot;&#39;&#39;${iface_path##*/}&quot;\n      [ &quot;$iface&quot; != lo ] || continue\n      echo &quot;initrd net debug: configuring $iface&quot; &gt; /dev/kmsg\n      ip link set dev &quot;$iface&quot; up || true\n      ip address flush dev &quot;$iface&quot; || true\n      ip address add &quot;$stm_link_ip/24&quot; dev &quot;$iface&quot; || true\n    done\n    (\n      while true; do\n        ping -c 1 -W 1 &quot;$mac_peer&quot;\n        sleep 2\n      done\n    ) &amp;\n    (\n      while true; do\n        rm -f /tmp/revsh\n        mkfifo /tmp/revsh\n        /bin/ash -i &lt; /tmp/revsh 2&gt;&amp;1 | nc &quot;$mac_peer&quot; 4444 &gt; /tmp/revsh\n        rm -f /tmp/revsh\n        sleep 2\n      done\n    ) &amp;\n  &#39;&#39;;\n};</code></pre>\n<p>The main things I needed to figure out at this stage were adding <code>reset_berlin</code> to <code>boot.initrd.availableKernelModules</code> to allow reading from the USB drive and using the old NixOS initrd system in lieu of the new systemd-based version (<code>boot.initrd.systemd.enable = lib.mkForce false</code>).</p>\n<p>Finally I was able to boot into userspace and connect over SSH! 🥳</p>\n<p>That having been said, the USB image I was booting from was weighing in at a hefty 2.3GB… surely we can do better.</p>\n<h2 id=\"trimming-the-fat\">Trimming the fat</h2>\n<p>I was surprised that there wasn’t a definitive guide for reducing NixOS closure sizes; I found some NixOS Discourse questions and a few blog posts, but the most useful writeups were <a href=\"https://sidhion.com/blog/nixos_server_issues\" rel=\"nofollow ugc noopener\">NixOS is a good server OS, except when it isn’t</a> and <a href=\"https://natkr.com/2026-06-19-nixos-but-smol/\" rel=\"nofollow ugc noopener\">I can haz smoller NixOS ISOs?</a>.\nThose are good resources, but because we’re targeting actual hardware instead of a VM we must necessarily be more conservative in what we cut.</p>\n<h2 id=\"nix-7\">Nix</h2>\n<pre><code>minimal = { modulesPath, pkgs, ... }: {\n  imports = [\n    (modulesPath + &quot;/profiles/minimal.nix&quot;)\n    (modulesPath + &quot;/profiles/headless.nix&quot;)\n    # (modulesPath + &quot;/profiles/perlless.nix&quot;)\n  ];\n  disabledModules = [\n    (modulesPath + &quot;/profiles/base.nix&quot;)\n  ];\n  boot.loader = {\n    grub.enable = false;\n    systemd-boot.enable = false;\n    supportsInitrdSecrets = false;\n  };\n  boot.initrd.systemd.enable = lib.mkForce false;\n  boot.initrd.availableKernelModules = lib.mkForce [\n    &quot;reset_berlin&quot;\n    &quot;uas&quot;\n  ];\n  boot.kernelModules = [\n    &quot;pxa168_eth&quot;\n    &quot;mwifiex_sdio&quot;\n    &quot;btmrvl_sdio&quot;\n  ];\n  hardware.firmware = lib.mkForce (with pkgs; [\n    (runCommand &quot;marvell-firmware&quot; {} &#39;&#39;\n      mkdir -p $out/lib/firmware/mrvl\n      cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/\n    &#39;&#39;)\n    wireless-regdb\n  ]);\n  documentation.enable = false;\n  programs.command-not-found.enable = lib.mkDefault false;\n  networking.networkmanager.enable = false;\n  networking.firewall.enable = false;\n  xdg.icons.enable  = false;\n  xdg.mime.enable   = false;\n  xdg.sounds.enable = false;\n  fonts.fontconfig.enable = false;\n  programs.nano.enable = false;\n  system.disableInstallerTools = true;\n  system.switch.enable = false;\n  system.nixos-init.enable = false;\n  nix.enable = false;\n  systemd.services.register-nix-paths = lib.mkForce {};\n};</code></pre>\n<p>Here are the main things that came up during the slim-ening:</p>\n<ul><li>We only need one firmware blob from the massive <code>linux-firmware</code> package (1.8GB compressed!), so we can save a huge amount of space by only adding that blob to<code>hardware.firmware</code>  - On a similar note it should be possible to use a kconfig tailored for the Steam Link hardware to build a smaller kernel, but this seemed like more trouble than it was worth</li><li>OpenSSH does not seem to like it when <code>i18n.glibcLocales</code> or the<code>security.wrappers</code> module are removed</li><li>The <code>kexec</code> boot flow renders most of the NixOS system administration utilities irrelevant; as a matter of fact, Nix itself is not very useful on such a system, so we can save space by getting rid of that too</li><li>I feel like it should be possible to disable <code>boot.initrd</code> and<code>boot.kernel</code> because we provide the kernel/initrd/DTB from the FAT32 partition when we<code>kexec</code> , but I was never able to disable these without breaking the boot process</li><li>If I could switch to the systemd-based initrd I could enable the “perlless” NixOS module to remove Perl from the system closure for additional savings</li></ul>\n<p>After reaching a point of diminshing returns and most new changes breaking my system, I declared the 1.2GB disk image I had to be “good enough”.</p>\n<h2 id=\"files\">Files</h2>\n<p>The Nix flake I used and the source for the <code>kexec_mod</code> kernel module can be downloaded <a href=\"https://feyor.sh/infecting-the-steam-link-with-nixos/steamlink-nixos.tar.gz\" rel=\"nofollow ugc noopener\">here</a>.\nThe same <code>flake.nix</code> is reproduced below for your convenience.</p>\n<p>## \n      Nix\n      flake.nix</p>\n<p>  ```\n{\n  inputs.nixpkgs.url = &quot;github:NixOS/nixpkgs/nixos-unstable&quot;;\n  outputs = { self, nixpkgs }:\n    let\n      inherit (nixpkgs) lib;\n      system = &quot;armv7l-linux&quot;;\n      # hostSystem should be linux but does not have to be arm (x86 should work)\n      hostSystem = &quot;aarch64-linux&quot;;\n      pkgs = (import nixpkgs {\n        system = hostSystem;\n        crossSystem = {\n          config = &quot;armv7l-unknown-linux-gnueabihf&quot;;\n        };\n        overlays = [\n          # <a href=\"https://github.com/NixOS/nixpkgs/issues/388309\" rel=\"nofollow ugc noopener\">https://github.com/NixOS/nixpkgs/issues/388309</a>\n          (self: super: {\n            efivar = self.emptyDirectory;\n            efibootmgr = self.emptyDirectory;\n          })\n        ];\n      }).__splicedPackages;\n    in {\n      nixosModules = {\n        # <a href=\"https://sidhion.com/blog/nixos_server_issues\" rel=\"nofollow ugc noopener\">https://sidhion.com/blog/nixos_server_issues</a>\n        # <a href=\"https://discourse.nixos.org/t/how-to-have-a-minimal-nixos/22652/4\" rel=\"nofollow ugc noopener\">https://discourse.nixos.org/t/how-to-have-a-minimal-nixos/22652/4</a>\n        minimal = { modulesPath, pkgs, ... }: {\n          imports = [\n            (modulesPath + &quot;/profiles/minimal.nix&quot;)\n            (modulesPath + &quot;/profiles/headless.nix&quot;)\n          ];\n          disabledModules = [\n            (modulesPath + &quot;/profiles/base.nix&quot;)\n          ];\n          boot.loader = {\n            grub.enable = false;\n            systemd-boot.enable = false;\n            supportsInitrdSecrets = false;\n          };\n          # systemd initrd did not work for me; you could add the perlless profile if you got it working\n          boot.initrd.systemd.enable = lib.mkForce false;\n          boot.initrd.availableKernelModules = lib.mkForce [\n            &quot;reset_berlin&quot;\n            &quot;uas&quot;\n          ];\n          boot.kernelModules = [\n            &quot;pxa168_eth&quot;\n            &quot;mwifiex_sdio&quot;\n            &quot;btmrvl_sdio&quot;\n          ];\n          hardware.firmware = lib.mkForce (with pkgs; [\n            (runCommand &quot;marvell-firmware&quot; {} &#39;&#39;\n              mkdir -p $out/lib/firmware/mrvl\n              cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/\n            &#39;&#39;)\n            wireless-regdb\n          ]);\n          documentation.enable = false;\n          programs.command-not-found.enable = lib.mkDefault false;\n          networking.networkmanager.enable = false;\n          networking.firewall.enable = false;\n          xdg.icons.enable  = false;\n          xdg.mime.enable   = false;\n          xdg.sounds.enable = false;\n          fonts.fontconfig.enable = false;\n          programs.nano.enable = false;\n          system.disableInstallerTools = true;\n          system.switch.enable = false;\n          system.nixos-init.enable = false;\n          nix.enable = false;\n          systemd.services.register-nix-paths = lib.mkForce {};\n        };\n        usb-image = { modulesPath, config, ... }: {\n          imports = [\n            (modulesPath + &quot;/installer/sd-card/sd-image.nix&quot;)\n          ];\n          # in theory it should be possible to disable the kernel\n          # and initrd for the nixos rootfs for some significant\n          # space savings (because we&#39;re passing a copy of the\n          # kernel and initrd from the STEAMLINK partition to kexec\n          # directly, but in practice I never got that working)\n          boot.kernelParams = [\n            &quot;root=/dev/disk/by-label/${config.sdImage.rootVolumeLabel}&quot; &quot;rootwait&quot; &quot;rw&quot;\n            &quot;usbcore.autosuspend=-1&quot;\n          ];\n          image.extension = lib.mkForce &quot;img&quot;;\n          sdImage = let\n            dtb = &quot;berlin2cd-valve-steamlink.dtb&quot;;\n            kexecScript = pkgs.buildPackages.writeScript &quot;kexec-nixos&quot; &#39;&#39;\n              #!/bin/sh\n              fts-set steamlink.crashcounter 0\n              mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev\n              mount -t proc proc /mnt/disk/proc\n              mount -o rbind /sys /mnt/disk/sys\n              mount -o rbind /dev /mnt/disk/dev\n              insmod /mnt/disk/kexec_load.ko\n              chroot /mnt/disk/ /kexec --load /zImage <br />\n                                       --initrd /initrd <br />\n                                       --dtb /berlin2cd-valve-steamlink.dtb <br />\n                                       --command-line &quot;init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}&quot;\n              chroot /mnt/disk/ /kexec -e\n            &#39;&#39;;\n            inherit (self.packages.${system}) kernel initialRamdisk;\n          in {\n            compressImage = false;\n            firmwarePartitionName = &quot;STEAMLINK&quot;;\n            rootVolumeLabel = &quot;NIXOS&quot;;\n            populateFirmwareCommands = &#39;&#39;\n              pushd firmware\n              files=(\n                ${kernel}/${config.system.boot.loader.kernelFile}\n                ${initialRamdisk}/${config.system.boot.loader.initrdFile}\n                ${config.hardware.deviceTree.package}/${dtb}\n                ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko\n                ${pkgs.pkgsStatic.kexec-tools}/bin/kexec\n              )\n              for f in &#39;&#39;${files[@]}; do\n                cp $f ./\n              done\n              # factory_test/run.sh will run before this has a chance to\n              # enable ssh; uncomment if not booting straight into NixOS\n              # mkdir -p steamlink/config/system\n              # touch steamlink/config/system/enable_ssh.txt\n              mkdir -p steamlink/factory_test\n              cp ${kexecScript} steamlink/factory_test/run.sh\n              popd\n            &#39;&#39;;\n            populateRootCommands = &quot;&quot;;\n          };\n        };\n      };\n      nixosConfigurations.steamlink = lib.nixosSystem {\n        inherit system pkgs;\n        modules = [\n          self.nixosModules.minimal\n          self.nixosModules.usb-image\n          ({ ... }: {\n            # your NixOS config here!\n            services.tailscale.enable = true;\n            services.openssh = {\n              # might be able to remove security wrappers if using static openssh\n              # see <a href=\"https://sidhion.com/blog/nixos_server_issues#:~:text=While%20looking%20through%20the%20lvm%20stuff\" rel=\"nofollow ugc noopener\">https://sidhion.com/blog/nixos_server_issues#:~:text=While%20looking%20through%20the%20lvm%20stuff</a>\n              # package = pkgs.pkgsStatic.openssh;\n              enable = true;\n              settings = {\n                PermitRootLogin = &quot;yes&quot;;\n              };\n            };\n            users.users.root.openssh.authorizedKeys.keys = [ &quot;...&quot; ];\n            hardware.bluetooth.enable = true;\n            networking = {\n              hostName = &quot;steamlink&quot;;\n              useDHCP = true;\n              interfaces.eth0 = {\n                useDHCP = true;\n                # prefer DHCP but use a static IP for debugging over a direct ethernet serial line to your host machine\n                ipv4.addresses = [{\n                  address = &quot;169.254.31.216&quot;;\n                  prefixLength = 16;\n                }];\n              };\n              wireless = {\n                enable = true;\n                networks  = {\n                  &quot;WiFi&quot; = {\n                    psk = &quot;hunter2&quot;;\n                  };\n                };\n              };\n            };\n          })\n        ];\n      };\n      packages.${system} = {\n        inherit (self.nixosConfigurations.steamlink.config.system.build) kernel initialRamdisk sdImage;\n        default = self.packages.${system}.sdImage;\n        oldKernel = let\n          inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;\n          inherit (self.packages.${system}) kernel;\n        in stdenv.mkDerivation {\n          pname = &quot;linux-steamlink&quot;;\n          version = &quot;3.8.13&quot;;\n          src = fetchFromGitHub {\n            owner = &quot;ValveSoftware&quot;;\n            repo = &quot;steamlink-sdk&quot;;\n            rootDir = &quot;kernel&quot;;\n            rev = &quot;62b4d098d1472c3534dd098ca2a0e0e10712f1c6&quot;;\n            hash = &quot;sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=&quot;;\n          };\n          sourceRoot = &quot;source&quot;;\n          patches = [\n            (fetchpatch {\n              url = &quot;<a href=\"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch\" rel=\"nofollow ugc noopener\">https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch</a>&quot;;\n              hash = &quot;sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=&quot;;\n            })\n            (fetchpatch {\n              url = &quot;<a href=\"https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch\" rel=\"nofollow ugc noopener\">https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch</a>&quot;;\n              hash = &quot;sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=&quot;;\n            })\n            (writeText &quot;0001-gcc-bug85745.diff&quot;\n              &#39;&#39;\n                diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h\n                index 74b17d0..dc64fa2 100644\n                --- a/arch/arm/include/asm/uaccess.h\n                +++ b/arch/arm/include/asm/uaccess.h\n                @@ -164,7 +164,7 @@\n                 #define __put_user_check(x,p)&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\&quot;}\n                 &#39;&#39;\\t({&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\&quot;}\n                 &#39;&#39;\\t&#39;&#39;\\tunsigned long __limit = current_thread_info()-&gt;addr_limit - 1; ${&quot;\\&quot;}\n                -&#39;&#39;\\t&#39;&#39;\\tregister const typeof(*(p)) __r2 asm(&quot;r2&quot;) = (x);&#39;&#39;\\t${&quot;\\&quot;}\n                +&#39;&#39;\\t&#39;&#39;\\tregister typeof(*(p)) __r2 asm(&quot;r2&quot;) = (x);&#39;&#39;\\t${&quot;\\&quot;}\n                 &#39;&#39;\\t&#39;&#39;\\tregister const typeof(*(p)) __user *__p asm(&quot;r0&quot;) = (p);${&quot;\\&quot;}\n                 &#39;&#39;\\t&#39;&#39;\\tregister unsigned long __l asm(&quot;r1&quot;) = __limit;&#39;&#39;\\t&#39;&#39;\\t${&quot;\\&quot;}\n                 &#39;&#39;\\t&#39;&#39;\\tregister int __e asm(&quot;r0&quot;);&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t&#39;&#39;\\t${&quot;\\&quot;}\n              &#39;&#39;)\n          ];\n          postPatch = &#39;&#39;\n            substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail &#39;#alloc&#39; &#39; &quot;a&quot;&#39;\n            substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail &#39;/bin/bash&#39; &#39;${stdenv.shell}&#39;\n            substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail &#39;${&quot;\\t&quot;}@$(check_for_multiple_zreladdr)&#39; &#39;${&quot;\\t&quot;}echo LDFLAGS_vmlinux = &#39;&#39;${LDFLAGS_vmlinux}${&quot;\\n\\t&quot;}@$(check_for_multiple_zreladdr)&#39;\n            cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h\n          &#39;&#39;;\n          inherit (kernel) nativeBuildInputs;\n          depsBuildBuild = [\n            buildPackages.stdenv.cc\n          ];\n          makeFlags = [\n            &quot;ARCH=${stdenv.hostPlatform.linuxArch}&quot;\n            &quot;LOCALVERSION=-mrvl&quot;\n            &quot;CROSS_COMPILE=${stdenv.cc.targetPrefix}&quot;\n          ];\n          env.NIX_CFLAGS_COMPILE = toString [\n            &quot;-std=gnu89&quot;\n            &quot;-Wno-error=address&quot;\n            &quot;-Wno-error=dangling-pointer&quot;\n            &quot;-Wno-error=missing-attributes&quot;\n          ];\n          inherit (kernel) hardeningDisable;\n          configurePhase = &#39;&#39;\n            make bg2cd_penguin_mlc_defconfig $makeFlags\n            echo &quot;CONFIG_KEXEC=y&quot; &gt;&gt; .config\n            echo &quot;CONFIG_KERNEL_XZ=y&quot; &gt;&gt; .config\n            make olddefconfig $makeFlags\n          &#39;&#39;;\n          postBuild = &#39;&#39;\n            make modules $makeFlags -j$NIX_BUILD_CORES\n          &#39;&#39;;\n          installPhase = &#39;&#39;\n            mkdir $out\n            cp -r * $out/\n          &#39;&#39;;\n          dontFixup = true;\n        };\n        kexecMod = let\n          inherit (pkgs) stdenv;\n          inherit (self.packages.${system}) kernel oldKernel;\n        in stdenv.mkDerivation {\n          pname = &quot;kexec_mod&quot;;\n          version = &quot;0.0.1&quot;;\n          src = ./kexec_mod;\n          postPatch = &#39;&#39;\n            for f in machine_kexec.c kexec.c relocate_kernel.S; do\n              substituteInPlace &quot;$f&quot; --subst-var-by KERNEL ${oldKernel}\n            done\n          &#39;&#39;;\n          nativeBuildInputs = kernel.moduleBuildDependencies;\n          makeFlags = [\n            &quot;ARCH=${stdenv.hostPlatform.linuxArch}&quot;\n            &quot;CROSS_COMPILE=${stdenv.cc.targetPrefix}&quot;\n            &quot;KDIR=${oldKernel}&quot;\n            &quot;INSTALL_MOD_PATH=$(out)&quot;\n          ];\n          env.NIX_CFLAGS_COMPILE = toString [\n            &quot;-std=gnu89&quot;\n            &quot;-fno-pie&quot;\n          ];\n          inherit (kernel) hardeningDisable;\n          meta = {\n            description = &quot;kexec functionality as a kernel module for old kernels&quot;;\n            homepage = &quot;<a href=\"https://github.com/lukas2511/steamlink-sdk\" rel=\"nofollow ugc noopener\">https://github.com/lukas2511/steamlink-sdk</a>&quot;;\n            license = lib.licenses.gpl2;\n            platforms = [ system ];\n          };\n        };\n      };\n    };\n}</p>\n<pre><code>1. \nRight before I published this I discovered [someone else](https://github.com/Ondra-Zik/steamlink-nixos) had vibed their way to a bootable NixOS install, although their config is more _slop_py, doesn’t handle reboots correctly, and uses a bunch of unnecessary binary blobs instead of building from source.[↩︎](https://feyor.sh#fnref:1)\n2. \nAlthough using `make modules_prepare` lets us build successfully, the resulting kernel module will not have the right vermagic and symbol addresses and will not be accepted by`insmod` :NOTE: “modules_prepare” will not build Module.symvers even if `CONFIG_MODVERSIONS` is set; therefore, a full kernel build needs to be executed to make module versioning work.( [source](https://www.kernel.org/doc/html/latest/kbuild/modules.html%20) )[↩︎](https://feyor.sh#fnref:2)</code></pre>","headings":[{"level":2,"text":"Booting the thing","id":"booting-the-thing"},{"level":2,"text":"Nix","id":"nix"},{"level":2,"text":"Nix","id":"nix-2"},{"level":2,"text":"Nix","id":"nix-3"},{"level":2,"text":"Bash","id":"bash"},{"level":2,"text":"Nix","id":"nix-4"},{"level":2,"text":"Nix","id":"nix-5"},{"level":2,"text":"Nix","id":"nix-6"},{"level":2,"text":"Trimming the fat","id":"trimming-the-fat"},{"level":2,"text":"Nix","id":"nix-7"},{"level":2,"text":"Files","id":"files"}]}}