summary refs log tree commit diff
diff options
context:
space:
mode:
author0x4A6F <0x4A6F@users.noreply.github.com>2022-01-06 19:21:47 +0100
committerGitHub <noreply@github.com>2022-01-06 19:21:47 +0100
commit29acc14f0db8957855c1e629a105992a4c975100 (patch)
tree7980af52690dc26b93263efcfa53d18253cdf858
parent7a022212c83000b4d56d93df4cd5610caa74e4b9 (diff)
parentc8d137961d29b7ad1e9470718802209b0e636776 (diff)
downloadnixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar.gz
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar.bz2
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar.lz
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar.xz
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.tar.zst
nixpkgs-29acc14f0db8957855c1e629a105992a4c975100.zip
Merge pull request #143060 from zhaofengli/binfmt-argv0
nixos/binfmt: Add QEMU wrapper to preserve argv[0]
-rw-r--r--nixos/modules/system/boot/binfmt.nix52
-rw-r--r--nixos/tests/systemd-binfmt.nix106
-rw-r--r--pkgs/applications/virtualization/qemu/binfmt-p-wrapper.c79
-rw-r--r--pkgs/applications/virtualization/qemu/binfmt-p-wrapper.nix31
-rw-r--r--pkgs/top-level/all-packages.nix2
5 files changed, 246 insertions, 24 deletions
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index fdb4d0e4c7f..5bc603530f7 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -20,16 +20,20 @@ let
                  optionalString fixBinary "F";
   in ":${name}:${type}:${offset'}:${magicOrExtension}:${mask'}:${interpreter}:${flags}";
 
-  activationSnippet = name: { interpreter, ... }: ''
+  activationSnippet = name: { interpreter, wrapInterpreterInShell, ... }: if wrapInterpreterInShell then ''
     rm -f /run/binfmt/${name}
     cat > /run/binfmt/${name} << 'EOF'
     #!${pkgs.bash}/bin/sh
     exec -- ${interpreter} "$@"
     EOF
     chmod +x /run/binfmt/${name}
+  '' else ''
+    rm -f /run/binfmt/${name}
+    ln -s ${interpreter} /run/binfmt/${name}
   '';
 
   getEmulator = system: (lib.systems.elaborate { inherit system; }).emulator pkgs;
+  getQemuArch = system: (lib.systems.elaborate { inherit system; }).qemuArch;
 
   # Mapping of systems to “magicOrExtension” and “mask”. Mostly taken from:
   # - https://github.com/cleverca22/nixos-configs/blob/master/qemu.nix
@@ -238,6 +242,25 @@ in {
               '';
               type = types.bool;
             };
+
+            wrapInterpreterInShell = mkOption {
+              default = true;
+              description = ''
+                Whether to wrap the interpreter in a shell script.
+
+                This allows a shell command to be set as the interpreter.
+              '';
+              type = types.bool;
+            };
+
+            interpreterSandboxPath = mkOption {
+              internal = true;
+              default = null;
+              description = ''
+                Path of the interpreter to expose in the build sandbox.
+              '';
+              type = types.nullOr types.path;
+            };
           };
         }));
       };
@@ -258,16 +281,37 @@ in {
   config = {
     boot.binfmt.registrations = builtins.listToAttrs (map (system: {
       name = system;
-      value = {
+      value = let
         interpreter = getEmulator system;
+        qemuArch = getQemuArch system;
+
+        preserveArgvZero = "qemu-${qemuArch}" == baseNameOf interpreter;
+        interpreterReg = let
+          wrapperName = "qemu-${qemuArch}-binfmt-P";
+          wrapper = pkgs.wrapQemuBinfmtP wrapperName interpreter;
+        in
+          if preserveArgvZero then "${wrapper}/bin/${wrapperName}"
+          else interpreter;
+      in {
+        inherit preserveArgvZero;
+
+        interpreter = interpreterReg;
+        wrapInterpreterInShell = !preserveArgvZero;
+        interpreterSandboxPath = dirOf (dirOf interpreterReg);
       } // (magics.${system} or (throw "Cannot create binfmt registration for system ${system}"));
     }) cfg.emulatedSystems);
     # TODO: add a nix.extraPlatforms option to NixOS!
     nix.extraOptions = lib.mkIf (cfg.emulatedSystems != []) ''
       extra-platforms = ${toString (cfg.emulatedSystems ++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 "i686-linux")}
     '';
-    nix.sandboxPaths = lib.mkIf (cfg.emulatedSystems != [])
-      ([ "/run/binfmt" "${pkgs.bash}" ] ++ (map (system: dirOf (dirOf (getEmulator system))) cfg.emulatedSystems));
+    nix.sandboxPaths = lib.mkIf (cfg.emulatedSystems != []) (
+      let
+        ruleFor = system: cfg.registrations.${system};
+        hasWrappedRule = lib.any (system: (ruleFor system).wrapInterpreterInShell) cfg.emulatedSystems;
+      in [ "/run/binfmt" ]
+        ++ lib.optional hasWrappedRule "${pkgs.bash}"
+        ++ (map (system: (ruleFor system).interpreterSandboxPath) cfg.emulatedSystems)
+      );
 
     environment.etc."binfmt.d/nixos.conf".source = builtins.toFile "binfmt_nixos.conf"
       (lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine config.boot.binfmt.registrations));
diff --git a/nixos/tests/systemd-binfmt.nix b/nixos/tests/systemd-binfmt.nix
index 2a676f3da98..a3a6efac3e4 100644
--- a/nixos/tests/systemd-binfmt.nix
+++ b/nixos/tests/systemd-binfmt.nix
@@ -1,24 +1,90 @@
 # Teach the kernel how to run armv7l and aarch64-linux binaries,
 # and run GNU Hello for these architectures.
-import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "systemd-binfmt";
-  machine = {
-    boot.binfmt.emulatedSystems = [
-      "armv7l-linux"
-      "aarch64-linux"
-    ];
-  };
 
-  testScript = let
-    helloArmv7l = pkgs.pkgsCross.armv7l-hf-multiplatform.hello;
-    helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
-  in ''
-    machine.start()
-    assert "world" in machine.succeed(
-        "${helloArmv7l}/bin/hello"
-    )
-    assert "world" in machine.succeed(
-        "${helloAarch64}/bin/hello"
-    )
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  expectArgv0 = xpkgs: xpkgs.runCommandCC "expect-argv0" {
+    src = pkgs.writeText "expect-argv0.c" ''
+      #include <stdio.h>
+      #include <string.h>
+
+      int main(int argc, char **argv) {
+        fprintf(stderr, "Our argv[0] is %s\n", argv[0]);
+
+        if (strcmp(argv[0], argv[1])) {
+          fprintf(stderr, "ERROR: argv[0] is %s, should be %s\n", argv[0], argv[1]);
+          return 1;
+        }
+
+        return 0;
+      }
+    '';
+  } ''
+    $CC -o $out $src
   '';
-})
+in {
+  basic = makeTest {
+    name = "systemd-binfmt";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "armv7l-linux"
+        "aarch64-linux"
+      ];
+    };
+
+    testScript = let
+      helloArmv7l = pkgs.pkgsCross.armv7l-hf-multiplatform.hello;
+      helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
+    in ''
+      machine.start()
+
+      assert "world" in machine.succeed(
+          "${helloArmv7l}/bin/hello"
+      )
+
+      assert "world" in machine.succeed(
+          "${helloAarch64}/bin/hello"
+      )
+    '';
+  };
+
+  preserveArgvZero = makeTest {
+    name = "systemd-binfmt-preserve-argv0";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "aarch64-linux"
+      ];
+    };
+    testScript = let
+      testAarch64 = expectArgv0 pkgs.pkgsCross.aarch64-multiplatform;
+    in ''
+      machine.start()
+      machine.succeed("exec -a meow ${testAarch64} meow")
+    '';
+  };
+
+  ldPreload = makeTest {
+    name = "systemd-binfmt-ld-preload";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "aarch64-linux"
+      ];
+    };
+    testScript = let
+      helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
+      libredirectAarch64 = pkgs.pkgsCross.aarch64-multiplatform.libredirect;
+    in ''
+      machine.start()
+
+      assert "error" not in machine.succeed(
+          "LD_PRELOAD='${libredirectAarch64}/lib/libredirect.so' ${helloAarch64}/bin/hello 2>&1"
+      ).lower()
+    '';
+  };
+}
diff --git a/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.c b/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.c
new file mode 100644
index 00000000000..f956768862e
--- /dev/null
+++ b/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.c
@@ -0,0 +1,79 @@
+// This is a tiny wrapper that converts the extra arv[0] argument
+// from binfmt-misc with the P flag enabled to QEMU parameters.
+// It also prevents LD_* environment variables from being applied
+// to QEMU itself.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifndef TARGET_QEMU
+#error "Define TARGET_QEMU to be the path to the qemu-user binary (e.g., -DTARGET_QEMU=\"/full/path/to/qemu-riscv64\")"
+#endif
+
+extern char **environ;
+
+int main(int argc, char *argv[]) {
+    if (argc < 3) {
+        fprintf(stderr, "%s: This should be run as the binfmt interpreter with the P flag\n", argv[0]);
+        fprintf(stderr, "%s: My preconfigured qemu-user binary: %s\n", argv[0], TARGET_QEMU);
+        return 1;
+    }
+
+    size_t environ_count = 0;
+    for (char **cur = environ; *cur != NULL; ++cur) {
+        environ_count++;
+    }
+
+    size_t new_argc = 3;
+    size_t new_argv_alloc = argc + 2 * environ_count + 2; // [ "-E", env ] for each LD_* env + [ "-0", argv0 ]
+    char **new_argv = (char**)malloc((new_argv_alloc + 1) * sizeof(char*));
+    if (!new_argv) {
+        fprintf(stderr, "FATAL: Failed to allocate new argv array\n");
+        abort();
+    }
+
+    new_argv[0] = TARGET_QEMU;
+    new_argv[1] = "-0";
+    new_argv[2] = argv[2];
+
+    // Pass all LD_ env variables as -E and strip them in `new_environ`
+    size_t new_environc = 0;
+    char **new_environ = (char**)malloc((environ_count + 1) * sizeof(char*));
+    if (!new_environ) {
+        fprintf(stderr, "FATAL: Failed to allocate new environ array\n");
+        abort();
+    }
+
+    for (char **cur = environ; *cur != NULL; ++cur) {
+        if (strncmp("LD_", *cur, 3) == 0) {
+            new_argv[new_argc++] = "-E";
+            new_argv[new_argc++] = *cur;
+        } else {
+            new_environ[new_environc++] = *cur;
+        }
+    }
+    new_environ[new_environc] = NULL;
+
+    size_t new_arg_start = new_argc;
+    new_argc += argc - 3 + 2; // [ "--", full_binary_path ]
+
+    if (argc > 3) {
+        memcpy(&new_argv[new_arg_start + 2], &argv[3], (argc - 3) * sizeof(char**));
+    }
+
+    new_argv[new_arg_start] = "--";
+    new_argv[new_arg_start + 1] = argv[1];
+    new_argv[new_argc] = NULL;
+
+#ifdef DEBUG
+    for (size_t i = 0; i < new_argc; ++i) {
+        fprintf(stderr, "argv[%zu] = %s\n", i, new_argv[i]);
+    }
+#endif
+
+    return execve(new_argv[0], new_argv, new_environ);
+}
+
+// vim: et:ts=4:sw=4
diff --git a/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.nix b/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.nix
new file mode 100644
index 00000000000..fada1456929
--- /dev/null
+++ b/pkgs/applications/virtualization/qemu/binfmt-p-wrapper.nix
@@ -0,0 +1,31 @@
+# binfmt preserve-argv[0] wrapper
+#
+# More details in binfmt-p-wrapper.c
+#
+# The wrapper has to be static so LD_* environment variables
+# cannot affect the execution of the wrapper itself.
+
+{ lib, stdenv, pkgsStatic, enableDebug ? false }:
+
+name: emulator:
+
+pkgsStatic.stdenv.mkDerivation {
+  inherit name;
+
+  src = ./binfmt-p-wrapper.c;
+
+  dontUnpack = true;
+  dontInstall = true;
+
+  buildPhase = ''
+    runHook preBuild
+
+    mkdir -p $out/bin
+    $CC -o $out/bin/${name} -static -std=c99 -O2 \
+        -DTARGET_QEMU=\"${emulator}\" \
+        ${lib.optionalString enableDebug "-DDEBUG"} \
+        $src
+
+    runHook postBuild
+  '';
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index bc24b12640e..5d9ee548986 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -28115,6 +28115,8 @@ with pkgs;
 
   qemu-utils = callPackage ../applications/virtualization/qemu/utils.nix {};
 
+  wrapQemuBinfmtP = callPackage ../applications/virtualization/qemu/binfmt-p-wrapper.nix { };
+
   qgis-unwrapped = libsForQt5.callPackage ../applications/gis/qgis/unwrapped.nix {
     withGrass = false;
   };