Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions nixos/modules/system/boot/binfmt.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};
};
}));
};
Expand All @@ -257,16 +280,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));
Expand Down
106 changes: 86 additions & 20 deletions nixos/tests/systemd-binfmt.nix
Original file line number Diff line number Diff line change
@@ -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()
'';
};
}
79 changes: 79 additions & 0 deletions pkgs/applications/virtualization/qemu/binfmt-p-wrapper.c
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions pkgs/applications/virtualization/qemu/binfmt-p-wrapper.nix
Original file line number Diff line number Diff line change
@@ -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
'';
}
2 changes: 2 additions & 0 deletions pkgs/top-level/all-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27324,6 +27324,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;
};
Expand Down