feyor.sh

Infecting the Steam Link with NixOS

While rummaging through my closet the other day I discovered a Steam Link 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.

As it turns out, a fellow named fijam already figured out the hard parts involved in running a custom Linux distro on the Steam Link. The 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. However, the kernel shipped with the Steam Link was not built with CONFIG_KEXEC enabled. This 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!

Several people have successfully used this technique to get other distros1 booting, but they all seem to have just copied the kexec binary and kernel module from fijam’s website. fijam 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.

Booting the thing

Compiling 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. Choosing 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, so I used the (seemingly) equivalent armv7l instead.

Nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      inherit (nixpkgs) lib;
      system = "armv7l-linux";
      # hostSystem should be linux but does not have to be arm (x86 should work)
      hostSystem = "aarch64-linux";
      pkgs = (import nixpkgs {
        system = hostSystem;
        crossSystem = {
          config = "armv7l-unknown-linux-gnueabihf";
        };
      }).__splicedPackages;
    in {
      nixosConfigurations.steamlink = lib.nixosSystem {
        inherit system pkgs;

        modules = [
          # ...
        ];
      };
    };
}

The real challenge is compiling a kernel module for a vendored fork of a 13 year old kernel; the NixOS wiki is actually pretty helpful here and points out some footguns related to the default hardening flags in stdenv.

Nix
kexecMod = let
  inherit (pkgs) stdenv;
  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel oldKernel;
in stdenv.mkDerivation {
  pname = "kexec_mod";
  version = "0.0.1";

  src = ./kexec_mod;

  postPatch = ''
    for f in machine_kexec.c kexec.c relocate_kernel.S; do
      substituteInPlace "$f" --subst-var-by KERNEL ${oldKernel}
    done
  '';

  nativeBuildInputs = kernel.moduleBuildDependencies;

  makeFlags = [
    "ARCH=${stdenv.hostPlatform.linuxArch}"
    "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
    "KDIR=${oldKernel}"
    "INSTALL_MOD_PATH=$(out)"
  ];

  env.NIX_CFLAGS_COMPILE = toString [
    "-std=gnu89"
    "-fno-pie"
  ];
  inherit (kernel) hardeningDisable;

  meta = {
    description = "kexec functionality as a kernel module for old kernels";
    homepage = "https://github.com/lukas2511/steamlink-sdk";
    license = lib.licenses.gpl2;
    platforms = [ system ];
  };
};

(See Files for the kexec_mod source code.)

In order to get this to build we need to point Kbuild to a Linux kernel checkout that has been built with make modules2. (Note that I’m using the moduleBuildDependencies attribute of the comparatively modern kernel from my NixOS configuration.)

Nix
oldKernel = let
  inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;
  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel;
in stdenv.mkDerivation {
  pname = "linux-steamlink";
  version = "3.8.13";

  src = fetchFromGitHub {
    owner = "ValveSoftware";
    repo = "steamlink-sdk";
    rootDir = "kernel";
    rev = "62b4d098d1472c3534dd098ca2a0e0e10712f1c6";
    hash = "sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=";
  };
  sourceRoot = "source";

  patches = [
    (fetchpatch {
      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";
      hash = "sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=";
    })
    (fetchpatch {
      url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch";
      hash = "sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=";
    })
    (writeText "0001-gcc-bug85745.diff"
      ''
        diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h
        index 74b17d0..dc64fa2 100644
        --- a/arch/arm/include/asm/uaccess.h
        +++ b/arch/arm/include/asm/uaccess.h
        @@ -164,7 +164,7 @@
         #define __put_user_check(x,p)''\t''\t''\t''\t''\t''\t''\t${"\\"}
         ''\t({''\t''\t''\t''\t''\t''\t''\t''\t${"\\"}
         ''\t''\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${"\\"}
        -''\t''\tregister const typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
        +''\t''\tregister typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
         ''\t''\tregister const typeof(*(p)) __user *__p asm("r0") = (p);${"\\"}
         ''\t''\tregister unsigned long __l asm("r1") = __limit;''\t''\t${"\\"}
         ''\t''\tregister int __e asm("r0");''\t''\t''\t''\t${"\\"}
      '')
  ];

  postPatch = ''
    substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' "a"'

    substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'

    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)'

    cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h
  '';

  inherit (kernel) nativeBuildInputs;
  depsBuildBuild = [
    buildPackages.stdenv.cc
  ];

  makeFlags = [
    "ARCH=${stdenv.hostPlatform.linuxArch}"
    "LOCALVERSION=-mrvl"
    "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
  ];
  env.NIX_CFLAGS_COMPILE = toString [
    "-std=gnu89"
    "-Wno-error=address"
    "-Wno-error=dangling-pointer"
    "-Wno-error=missing-attributes"
  ];
  inherit (kernel) hardeningDisable;

  configurePhase = ''
    make bg2cd_penguin_mlc_defconfig $makeFlags
    echo "CONFIG_KEXEC=y" >> .config
    echo "CONFIG_KERNEL_XZ=y" >> .config
    make olddefconfig $makeFlags
  '';

  postBuild = ''
    make modules $makeFlags -j$NIX_BUILD_CORES
  '';

  installPhase = ''
    mkdir $out
    cp -r * $out/
  '';

  dontFixup = true;
};

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.

Now 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 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:

Bash
fts-set steamlink.crashcounter 0 # required to prevent factory reset after a few reboots

mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev
mount -t proc proc /mnt/disk/proc
mount -o rbind /sys /mnt/disk/sys
mount -o rbind /dev /mnt/disk/dev

insmod /mnt/disk/kexec_load.ko
chroot /mnt/disk/ /kexec --load /zImage \
                         --initrd /initrd \
                         --dtb /berlin2cd-valve-steamlink.dtb \
                         --command-line "init=/init root=/dev/sda2 rootwait rw usbcore.autosuspend=-1"
chroot /mnt/disk/ /kexec -e

You could juggle these files manually and upload them to a USB drive yourself, but it’s much easier to use the sd-image NixOS module to create a disk image instead:

Nix
usb-image = { modulesPath, config, ... }: {
  imports = [
    (modulesPath + "/installer/sd-card/sd-image.nix")
  ];

  image.extension = lib.mkForce "img";

  sdImage = let
    dtb = "berlin2cd-valve-steamlink.dtb";
    kexecScript = ./kexec-nixos;
    inherit (config.system.build) kernel initialRamdisk;
  in {
    compressImage = false;
    firmwarePartitionName = "STEAMLINK";
    rootVolumeLabel = "NIXOS";
    populateFirmwareCommands = ''
      pushd firmware

      files=(
        ${kernel}/${config.system.boot.loader.kernelFile}
        ${initialRamdisk}/${config.system.boot.loader.initrdFile}
        ${config.hardware.deviceTree.package}/${dtb}
        ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko
        ${pkgs.pkgsStatic.kexec-tools}/bin/kexec
      )
      for f in ''${files[@]}; do
        cp $f ./
      done

      # factory_test/run.sh will run before this has a chance to
      # enable ssh; uncomment if not booting straight into NixOS
      # mkdir -p steamlink/config/system
      # touch steamlink/config/system/enable_ssh.txt

      mkdir -p steamlink/factory_test
      cp ${kexecScript} steamlink/factory_test/run.sh

      popd
    '';
    populateRootCommands = "";
  };
};

Testing 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. I decided to test with a minimal (slop) Busybox-based initramfs that rebooted after a variable amount of time to indicate success.

Nix
initramfs = pkgs.buildPackages.runCommand "build-initramfs" {}
  ''
    mkdir initramfs; cd initramfs
    mkdir -pv {etc,proc,sys,usr/{bin,sbin}}
    cp -a ${pkgs.pkgsStatic.busybox}/{bin,sbin} .
    chmod 755 ./{bin,sbin}

    cat <<EOF > init
    #!/bin/sh
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev

    mkdir -p /mnt

    try_mount() {
      dev="$1"
      fs="$2"

      if [ "$fs" = auto ]; then
        mount -o rw "$dev" /mnt 2>/dev/null || return 1
      else
        mount -t "$fs" -o rw "$dev" /mnt 2>/dev/null || return 1
      fi

      marker=kexec-mounted-ok
      if [ -f /mnt/zImage ]; then
        marker=kexec-steamlink-ok
      fi

      {
        echo "device=$dev"
        echo "fs=$fs"
        cat /proc/partitions
      } > "/mnt/$marker" 2>/dev/null && sync

      umount /mnt
      sleep 10
      reboot -f
    }

    for dev in /dev/mmcblk*p* /dev/sd[a-z][0-9]* /dev/vd[a-z][0-9]*; do
      [ -b "$dev" ] || continue
      try_mount "$dev" vfat
      try_mount "$dev" ext4
      try_mount "$dev" auto
    done

    sleep 45
    reboot -f
    EOF
    chmod +x init

    find . -print0 | ${lib.getExe pkgs.buildPackages.cpio} --null -ov --format=newc > $out
  '';

Once 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.

Nix
debugModule = { lib, ... }: {
  boot.initrd.systemd.enable = lib.mkForce false;
  boot.initrd.kernelModules = [ "pxa168_eth" ];
  boot.initrd.availableKernelModules = [ "reset_berlin" ];

  boot.initrd.network.enable = true;
  boot.initrd.network.udhcpc.enable = false;

  boot.kernelParams = [
    "ip=192.168.2.2::192.168.2.1:255.255.255.0:stm-link:eth0:off"
  ];

  boot.initrd.network.postCommands = ''
    mac_peer=192.168.2.1
    stm_link_ip=192.168.2.2

    echo "initrd net debug: interfaces: $(ls /sys/class/net)" > /dev/kmsg

    for iface_path in /sys/class/net/*; do
      iface="''${iface_path##*/}"
      [ "$iface" != lo ] || continue

      echo "initrd net debug: configuring $iface" > /dev/kmsg
      ip link set dev "$iface" up || true
      ip address flush dev "$iface" || true
      ip address add "$stm_link_ip/24" dev "$iface" || true
    done

    (
      while true; do
        ping -c 1 -W 1 "$mac_peer"
        sleep 2
      done
    ) &

    (
      while true; do
        rm -f /tmp/revsh
        mkfifo /tmp/revsh
        /bin/ash -i < /tmp/revsh 2>&1 | nc "$mac_peer" 4444 > /tmp/revsh
        rm -f /tmp/revsh
        sleep 2
      done
    ) &
  '';
};

The 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).

Finally I was able to boot into userspace and connect over SSH! 🥳

That having been said, the USB image I was booting from was weighing in at a hefty 2.3GB… surely we can do better.

Trimming the fat

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 NixOS is a good server OS, except when it isn’t and I can haz smoller NixOS ISOs?. Those are good resources, but because we’re targeting actual hardware instead of a VM we must necessarily be more conservative in what we cut.

Nix
minimal = { modulesPath, pkgs, ... }: {
  imports = [
    (modulesPath + "/profiles/minimal.nix")
    (modulesPath + "/profiles/headless.nix")
    # (modulesPath + "/profiles/perlless.nix")
  ];

  disabledModules = [
    (modulesPath + "/profiles/base.nix")
  ];

  boot.loader = {
    grub.enable = false;
    systemd-boot.enable = false;
    supportsInitrdSecrets = false;
  };

  boot.initrd.systemd.enable = lib.mkForce false;
  boot.initrd.availableKernelModules = lib.mkForce [
    "reset_berlin"
    "uas"
  ];
  boot.kernelModules = [
    "pxa168_eth"
    "mwifiex_sdio"
    "btmrvl_sdio"
  ];
  hardware.firmware = lib.mkForce (with pkgs; [
    (runCommand "marvell-firmware" {} ''
      mkdir -p $out/lib/firmware/mrvl
      cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/
    '')
    wireless-regdb
  ]);

  documentation.enable = false;
  programs.command-not-found.enable = lib.mkDefault false;

  networking.networkmanager.enable = false;
  networking.firewall.enable = false;

  xdg.icons.enable  = false;
  xdg.mime.enable   = false;
  xdg.sounds.enable = false;
  fonts.fontconfig.enable = false;

  programs.nano.enable = false;

  system.disableInstallerTools = true;
  system.switch.enable = false;
  system.nixos-init.enable = false;

  nix.enable = false;
  systemd.services.register-nix-paths = lib.mkForce {};
};

Here are the main things that came up during the slim-ening:

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”.

Files

The Nix flake I used and the source for the kexec_mod kernel module can be downloaded here. The same flake.nix is reproduced below for your convenience.

Nix flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      inherit (nixpkgs) lib;
      system = "armv7l-linux";
      # hostSystem should be linux but does not have to be arm (x86 should work)
      hostSystem = "aarch64-linux";
      pkgs = (import nixpkgs {
        system = hostSystem;
        crossSystem = {
          config = "armv7l-unknown-linux-gnueabihf";
        };
        overlays = [
          # https://github.com/NixOS/nixpkgs/issues/388309
          (self: super: {
            efivar = self.emptyDirectory;
            efibootmgr = self.emptyDirectory;
          })
        ];
      }).__splicedPackages;
    in {
      nixosModules = {
        # https://sidhion.com/blog/nixos_server_issues
        # https://discourse.nixos.org/t/how-to-have-a-minimal-nixos/22652/4
        minimal = { modulesPath, pkgs, ... }: {
          imports = [
            (modulesPath + "/profiles/minimal.nix")
            (modulesPath + "/profiles/headless.nix")
          ];

          disabledModules = [
            (modulesPath + "/profiles/base.nix")
          ];

          boot.loader = {
            grub.enable = false;
            systemd-boot.enable = false;
            supportsInitrdSecrets = false;
          };

          # systemd initrd did not work for me; you could add the perlless profile if you got it working
          boot.initrd.systemd.enable = lib.mkForce false;
          boot.initrd.availableKernelModules = lib.mkForce [
            "reset_berlin"
            "uas"
          ];
          boot.kernelModules = [
            "pxa168_eth"
            "mwifiex_sdio"
            "btmrvl_sdio"
          ];
          hardware.firmware = lib.mkForce (with pkgs; [
            (runCommand "marvell-firmware" {} ''
              mkdir -p $out/lib/firmware/mrvl
              cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/
            '')
            wireless-regdb
          ]);

          documentation.enable = false;
          programs.command-not-found.enable = lib.mkDefault false;

          networking.networkmanager.enable = false;
          networking.firewall.enable = false;

          xdg.icons.enable  = false;
          xdg.mime.enable   = false;
          xdg.sounds.enable = false;
          fonts.fontconfig.enable = false;

          programs.nano.enable = false;

          system.disableInstallerTools = true;
          system.switch.enable = false;
          system.nixos-init.enable = false;

          nix.enable = false;
          systemd.services.register-nix-paths = lib.mkForce {};
        };

        usb-image = { modulesPath, config, ... }: {
          imports = [
            (modulesPath + "/installer/sd-card/sd-image.nix")
          ];

          # in theory it should be possible to disable the kernel
          # and initrd for the nixos rootfs for some significant
          # space savings (because we're passing a copy of the
          # kernel and initrd from the STEAMLINK partition to kexec
          # directly, but in practice I never got that working)

          boot.kernelParams = [
            "root=/dev/disk/by-label/${config.sdImage.rootVolumeLabel}" "rootwait" "rw"
            "usbcore.autosuspend=-1"
          ];

          image.extension = lib.mkForce "img";

          sdImage = let
            dtb = "berlin2cd-valve-steamlink.dtb";
            kexecScript = pkgs.buildPackages.writeScript "kexec-nixos" ''
              #!/bin/sh

              fts-set steamlink.crashcounter 0

              mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev
              mount -t proc proc /mnt/disk/proc
              mount -o rbind /sys /mnt/disk/sys
              mount -o rbind /dev /mnt/disk/dev

              insmod /mnt/disk/kexec_load.ko
              chroot /mnt/disk/ /kexec --load /zImage \
                                       --initrd /initrd \
                                       --dtb /berlin2cd-valve-steamlink.dtb \
                                       --command-line "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"
              chroot /mnt/disk/ /kexec -e
            '';
            inherit (self.packages.${system}) kernel initialRamdisk;
          in {
            compressImage = false;
            firmwarePartitionName = "STEAMLINK";
            rootVolumeLabel = "NIXOS";
            populateFirmwareCommands = ''
              pushd firmware

              files=(
                ${kernel}/${config.system.boot.loader.kernelFile}
                ${initialRamdisk}/${config.system.boot.loader.initrdFile}
                ${config.hardware.deviceTree.package}/${dtb}
                ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko
                ${pkgs.pkgsStatic.kexec-tools}/bin/kexec
              )
              for f in ''${files[@]}; do
                cp $f ./
              done

              # factory_test/run.sh will run before this has a chance to
              # enable ssh; uncomment if not booting straight into NixOS
              # mkdir -p steamlink/config/system
              # touch steamlink/config/system/enable_ssh.txt

              mkdir -p steamlink/factory_test
              cp ${kexecScript} steamlink/factory_test/run.sh

              popd
            '';
            populateRootCommands = "";
          };
        };
      };

      nixosConfigurations.steamlink = lib.nixosSystem {
        inherit system pkgs;

        modules = [
          self.nixosModules.minimal
          self.nixosModules.usb-image

          ({ ... }: {
            # your NixOS config here!

            services.tailscale.enable = true;

            services.openssh = {
              # might be able to remove security wrappers if using static openssh
              # see https://sidhion.com/blog/nixos_server_issues#:~:text=While%20looking%20through%20the%20lvm%20stuff
              # package = pkgs.pkgsStatic.openssh;
              enable = true;
              settings = {
                PermitRootLogin = "yes";
              };
            };
            users.users.root.openssh.authorizedKeys.keys = [ "..." ];

            hardware.bluetooth.enable = true;

            networking = {
              hostName = "steamlink";

              useDHCP = true;
              interfaces.eth0 = {
                useDHCP = true;
                # prefer DHCP but use a static IP for debugging over a direct ethernet serial line to your host machine
                ipv4.addresses = [{
                  address = "169.254.31.216";
                  prefixLength = 16;
                }];
              };
              wireless = {
                enable = true;
                networks  = {
                  "WiFi" = {
                    psk = "hunter2";
                  };
                };
              };
            };
          })
        ];
      };

      packages.${system} = {
        inherit (self.nixosConfigurations.steamlink.config.system.build) kernel initialRamdisk sdImage;

        default = self.packages.${system}.sdImage;

        oldKernel = let
          inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;
          inherit (self.packages.${system}) kernel;
        in stdenv.mkDerivation {
          pname = "linux-steamlink";
          version = "3.8.13";

          src = fetchFromGitHub {
            owner = "ValveSoftware";
            repo = "steamlink-sdk";
            rootDir = "kernel";
            rev = "62b4d098d1472c3534dd098ca2a0e0e10712f1c6";
            hash = "sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=";
          };
          sourceRoot = "source";

          patches = [
            (fetchpatch {
              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";
              hash = "sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=";
            })
            (fetchpatch {
              url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch";
              hash = "sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=";
            })
            (writeText "0001-gcc-bug85745.diff"
              ''
                diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h
                index 74b17d0..dc64fa2 100644
                --- a/arch/arm/include/asm/uaccess.h
                +++ b/arch/arm/include/asm/uaccess.h
                @@ -164,7 +164,7 @@
                 #define __put_user_check(x,p)''\t''\t''\t''\t''\t''\t''\t${"\\"}
                 ''\t({''\t''\t''\t''\t''\t''\t''\t''\t${"\\"}
                 ''\t''\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${"\\"}
                -''\t''\tregister const typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
                +''\t''\tregister typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
                 ''\t''\tregister const typeof(*(p)) __user *__p asm("r0") = (p);${"\\"}
                 ''\t''\tregister unsigned long __l asm("r1") = __limit;''\t''\t${"\\"}
                 ''\t''\tregister int __e asm("r0");''\t''\t''\t''\t${"\\"}
              '')
          ];

          postPatch = ''
            substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' "a"'

            substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'

            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)'

            cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h
          '';

          inherit (kernel) nativeBuildInputs;
          depsBuildBuild = [
            buildPackages.stdenv.cc
          ];

          makeFlags = [
            "ARCH=${stdenv.hostPlatform.linuxArch}"
            "LOCALVERSION=-mrvl"
            "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
          ];
          env.NIX_CFLAGS_COMPILE = toString [
            "-std=gnu89"
            "-Wno-error=address"
            "-Wno-error=dangling-pointer"
            "-Wno-error=missing-attributes"
          ];
          inherit (kernel) hardeningDisable;

          configurePhase = ''
            make bg2cd_penguin_mlc_defconfig $makeFlags
            echo "CONFIG_KEXEC=y" >> .config
            echo "CONFIG_KERNEL_XZ=y" >> .config
            make olddefconfig $makeFlags
          '';

          postBuild = ''
            make modules $makeFlags -j$NIX_BUILD_CORES
          '';

          installPhase = ''
            mkdir $out
            cp -r * $out/
          '';

          dontFixup = true;
        };

        kexecMod = let
          inherit (pkgs) stdenv;
          inherit (self.packages.${system}) kernel oldKernel;
        in stdenv.mkDerivation {
          pname = "kexec_mod";
          version = "0.0.1";

          src = ./kexec_mod;

          postPatch = ''
            for f in machine_kexec.c kexec.c relocate_kernel.S; do
              substituteInPlace "$f" --subst-var-by KERNEL ${oldKernel}
            done
          '';

          nativeBuildInputs = kernel.moduleBuildDependencies;

          makeFlags = [
            "ARCH=${stdenv.hostPlatform.linuxArch}"
            "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
            "KDIR=${oldKernel}"
            "INSTALL_MOD_PATH=$(out)"
          ];

          env.NIX_CFLAGS_COMPILE = toString [
            "-std=gnu89"
            "-fno-pie"
          ];
          inherit (kernel) hardeningDisable;

          meta = {
            description = "kexec functionality as a kernel module for old kernels";
            homepage = "https://github.com/lukas2511/steamlink-sdk";
            license = lib.licenses.gpl2;
            platforms = [ system ];
          };
        };
      };
    };
}

  1. Right before I published this I discovered someone else 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. ↩︎

  2. Although 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) ↩︎

#nix