From 4f796c28d57887cc9812190bc99fb45b2acd6d1c Mon Sep 17 00:00:00 2001 From: aszlig Date: Fri, 6 May 2016 16:06:22 +0200 Subject: [PATCH] nixos/tests: Add a test for boot stage 1 We already have a small regression test for #15226 within the swraid installer test. Unfortunately, we only check there whether the md kthread got signalled but not whether other rampaging processes are still alive that *should* have been killed. So in order to do this we provide multiple canary processes which are checked after the system has booted up: * canary1: It's a simple forking daemon which just sleeps until it's going to be killed. Of course we expect this process to not be alive anymore after boot up. * canary2: Similar to canary1, but tries to mimick a kthread to make sure that it's going to be properly killed at the end of stage 1. * canary3: Like canary2, but this time using a @ in front of its command name to actually prevent it from being killed. * kcanary: This one is a real kthread and it runs until killed, which shouldn't be the case. Tested with and without 67223ee and everything works as expected, at least on my machine. Signed-off-by: aszlig --- nixos/release.nix | 1 + nixos/tests/boot-stage1.nix | 153 ++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 nixos/tests/boot-stage1.nix diff --git a/nixos/release.nix b/nixos/release.nix index 97f6df16dc99..c8547784bbcf 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -209,6 +209,7 @@ in rec { tests.bittorrent = callTest tests/bittorrent.nix {}; tests.blivet = callTest tests/blivet.nix {}; tests.boot = callSubTests tests/boot.nix {}; + tests.boot-stage1 = callTest tests/boot-stage1.nix {}; tests.cadvisor = hydraJob (import tests/cadvisor.nix { system = "x86_64-linux"; }); tests.chromium = (callSubTests tests/chromium.nix { system = "x86_64-linux"; }).stable; tests.cjdns = callTest tests/cjdns.nix {}; diff --git a/nixos/tests/boot-stage1.nix b/nixos/tests/boot-stage1.nix new file mode 100644 index 000000000000..311acd7bb1c4 --- /dev/null +++ b/nixos/tests/boot-stage1.nix @@ -0,0 +1,153 @@ +import ./make-test.nix { + name = "boot-stage1"; + + machine = { config, pkgs, lib, ... }: { + boot.extraModulePackages = let + compileKernelModule = name: source: pkgs.runCommand name rec { + inherit source; + kdev = config.boot.kernelPackages.kernel.dev; + kver = config.boot.kernelPackages.kernel.modDirVersion; + ksrc = "${kdev}/lib/modules/${kver}/build"; + } '' + echo "obj-m += $name.o" > Makefile + echo "$source" > "$name.c" + make -C "$ksrc" M=$(pwd) modules + install -vD "$name.ko" "$out/lib/modules/$kver/$name.ko" + ''; + + # This spawns a kthread which just waits until it gets a signal and + # terminates if that is the case. We want to make sure that nothing during + # the boot process kills any kthread by accident, like what happened in + # issue #15226. + kcanary = compileKernelModule "kcanary" '' + #include + #include + #include + #include + #include + + struct task_struct *canaryTask; + + static int kcanary(void *nothing) + { + allow_signal(SIGINT); + allow_signal(SIGTERM); + allow_signal(SIGKILL); + while (!kthread_should_stop()) { + set_current_state(TASK_INTERRUPTIBLE); + schedule_timeout_interruptible(msecs_to_jiffies(100)); + if (signal_pending(current)) break; + } + return 0; + } + + static int kcanaryInit(void) + { + kthread_run(&kcanary, NULL, "kcanary"); + return 0; + } + + static void kcanaryExit(void) + { + kthread_stop(canaryTask); + } + + module_init(kcanaryInit); + module_exit(kcanaryExit); + ''; + + in lib.singleton kcanary; + + boot.initrd.kernelModules = [ "kcanary" ]; + + boot.initrd.extraUtilsCommands = let + compile = name: source: pkgs.runCommand name { inherit source; } '' + mkdir -p "$out/bin" + echo "$source" | gcc -Wall -o "$out/bin/$name" -xc - + ''; + + daemonize = name: source: compile name '' + #include + #include + + void runSource(void) { + ${source} + } + + int main(void) { + if (fork() > 0) return 0; + setsid(); + runSource(); + return 1; + } + ''; + + mkCmdlineCanary = { name, cmdline ? "", source ? "" }: (daemonize name '' + char *argv[] = {"${cmdline}", NULL}; + execvp("${name}-child", argv); + '') // { + child = compile "${name}-child" '' + #include + #include + + int main(void) { + ${source} + while (1) sleep(1); + return 1; + } + ''; + }; + + copyCanaries = with lib; concatMapStrings (canary: '' + ${optionalString (canary ? child) '' + copy_bin_and_libs "${canary.child}/bin/${canary.child.name}" + ''} + copy_bin_and_libs "${canary}/bin/${canary.name}" + ''); + + in copyCanaries [ + # Simple canary process which just sleeps forever and should be killed by + # stage 2. + (daemonize "canary1" "while (1) sleep(1);") + + # We want this canary process to try mimicking a kthread using a cmdline + # with a zero length so we can make sure that the process is properly + # killed in stage 1. + (mkCmdlineCanary { + name = "canary2"; + source = '' + FILE *f; + f = fopen("/run/canary2.pid", "w"); + fprintf(f, "%d\n", getpid()); + fclose(f); + ''; + }) + + # This canary process mimicks a storage daemon, which we do NOT want to be + # killed before going into stage 2. For more on root storage daemons, see: + # https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/ + (mkCmdlineCanary { + name = "canary3"; + cmdline = "@canary3"; + }) + ]; + + boot.initrd.postMountCommands = '' + canary1 + canary2 + canary3 + # Make sure the pidfile of canary 2 is created so that we still can get + # its former pid after the killing spree starts next within stage 1. + while [ ! -s /run/canary2.pid ]; do sleep 0.1; done + ''; + }; + + testScript = '' + $machine->waitForUnit("multi-user.target"); + $machine->succeed('test -s /run/canary2.pid'); + $machine->fail('pgrep -a canary1'); + $machine->fail('kill -0 $(< /run/canary2.pid)'); + $machine->succeed('pgrep -a -f \'^@canary3$\'''); + $machine->succeed('pgrep -a -f \'^kcanary$\'''); + ''; +}