{ config, ... }:

let
  pkgs = config.nodes.machine.nixpkgs.pkgs;

  attacker = pkgs.runCommandWith {
    name = "attacker";
    stdenv = pkgs.pkgsStatic.stdenv;
  } ''
    $CC -static -o $out ${./attacker.c}
  '';

  try-open-build-dir = pkgs.writeScript "try-open-build-dir" ''
    export PATH=${pkgs.coreutils}/bin:$PATH

    set -x

    chmod 700 .
    # Shouldn't be able to open the root build directory
    (! chmod 700 ..)

    touch foo

    # Synchronisation point: create a world-writable fifo and wait for someone
    # to write into it
    mkfifo syncPoint
    chmod 777 syncPoint
    cat syncPoint

    touch $out

    set +x
  '';

  create-hello-world = pkgs.writeScript "create-hello-world" ''
    export PATH=${pkgs.coreutils}/bin:$PATH

    set -x

    echo "hello, world" > result

    # Synchronisation point: create a world-writable fifo and wait for someone
    # to write into it
    mkfifo syncPoint
    chmod 777 syncPoint
    cat syncPoint

    cp result $out

    set +x
  '';

in
{
  name = "sandbox-setuid-leak";

  nodes.machine =
    { config, lib, pkgs, ... }:
    { virtualisation.writableStore = true;
      nix.settings.substituters = lib.mkForce [ ];
      nix.nrBuildUsers = 1;
      virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell attacker try-open-build-dir create-hello-world pkgs.socat ];
      boot.kernelPackages = pkgs.linuxPackages_latest;
      users.users.alice = {
        isNormalUser = true;
      };
    };

  testScript = { nodes }: ''
    start_all()

    with subtest("A builder can't give access to its build directory"):
        # Make sure that a builder can't change the permissions on its build
        # directory to the point of opening it up to external users

        # A derivation whose builder tries to make its build directory as open
        # as possible and wait for someone to hijack it
        machine.succeed(r"""
          nix-build -v -E '
            builtins.derivation {
              name = "open-build-dir";
              system = builtins.currentSystem;
              builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
              args = [ (builtins.storePath "${try-open-build-dir}") ];
          }' >&2 &
        """.strip())

        # Wait for the build to be ready
        # This is OK because it runs as root, so we can access everything
        machine.wait_for_file("/tmp/nix-build-open-build-dir.drv-0/build/syncPoint")

        # But Alice shouldn't be able to access the build directory
        machine.fail("su alice -c 'ls /tmp/nix-build-open-build-dir.drv-0/build'")
        machine.fail("su alice -c 'touch /tmp/nix-build-open-build-dir.drv-0/build/bar'")
        machine.fail("su alice -c 'cat /tmp/nix-build-open-build-dir.drv-0/build/foo'")

        # Tell the user to finish the build
        machine.succeed("echo foo > /tmp/nix-build-open-build-dir.drv-0/build/syncPoint")

    with subtest("Being able to execute stuff as the build user doesn't give access to the build dir"):
        machine.succeed(r"""
          nix-build -E '
            builtins.derivation {
              name = "innocent";
              system = builtins.currentSystem;
              builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
              args = [ (builtins.storePath "${create-hello-world}") ];
          }' >&2 &
        """.strip())
        machine.wait_for_file("/tmp/nix-build-innocent.drv-0/build/syncPoint")

        # The build ran as `nixbld1` (which is the only build user on the
        # machine), but a process running as `nixbld1` outside the sandbox
        # shouldn't be able to touch the build directory regardless
        machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'ls /tmp/nix-build-innocent.drv-0/build'")
        machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'echo pwned > /tmp/nix-build-innocent.drv-0/build/result'")

        # Finish the build
        machine.succeed("echo foo > /tmp/nix-build-innocent.drv-0/build/syncPoint")

        # Check that the build was not affected
        machine.succeed(r"""
          cat ./result
          test "$(cat ./result)" = "hello, world"
        """.strip())
  '';

}

