# Nix is a sandboxed build system. But Not everything can be handled inside its
# sandbox: Network access is normally blocked off, but to download sources, a
# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
# The detail here is not important, but in our case it means that the hash of
# the output has to be known beforehand. And if you know that, you get a few
# rights: you no longer run inside a special network namespace!
#
# Now, Linux has a special feature, that not many other unices do: Abstract
# unix domain sockets! Not only that, but those are namespaced using the
# network namespace! That means that we have a way to create sockets that are
# available in every single fixed-output derivation, and also all processes
# running on the host machine! Now, this wouldn't be that much of an issue, as,
# well, the whole idea is that the output is pure, and all processes in the
# sandbox are killed before finalizing the output. What if we didn't need those
# processes at all? Unix domain sockets have a semi-known trick: you can pass
# file descriptors around!
# This makes it possible to exfiltrate a file-descriptor with write access to
# $out outside of the sandbox. And that file-descriptor can be used to modify
# the contents of the store path after it has been registered.

{ config, ... }:

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

  # Simple C program that sends a a file descriptor to `$out` to a Unix
  # domain socket.
  # Compiled statically so that we can easily send it to the VM and use it
  # inside the build sandbox.
  sender =
    pkgs.runCommandWith
      {
        name = "sender";
        stdenv = pkgs.pkgsStatic.stdenv;
      }
      ''
        $CC -static -o $out ${./sender.c}
      '';

  # Okay, so we have a file descriptor shipped out of the FOD now. But the
  # Nix store is read-only, right? .. Well, yeah. But this file descriptor
  # lives in a mount namespace where it is not! So even when this file exists
  # in the actual Nix store, we're capable of just modifying its contents...
  smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);

  # The abstract socket path used to exfiltrate the file descriptor
  socketName = "FODSandboxExfiltrationSocket";
in
{
  name = "ca-fd-leak";

  nodes.machine =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      virtualisation.writableStore = true;
      nix.settings.substituters = lib.mkForce [ ];
      virtualisation.additionalPaths = [
        pkgs.busybox-sandbox-shell
        sender
        smuggler
        pkgs.socat
      ];
    };

  testScript =
    { nodes }:
    ''
      start_all()

      machine.succeed("echo hello")
      # Start the smuggler server
      machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")

      # Build the smuggled derivation.
      # This will connect to the smuggler server and send it the file descriptor
      machine.succeed(r"""
        nix-build -E '
          builtins.derivation {
            name = "smuggled";
            system = builtins.currentSystem;
            # look ma, no tricks!
            outputHashMode = "flat";
            outputHashAlgo = "sha256";
            outputHash = builtins.hashString "sha256" "hello, world\n";
            builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
            args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
        }'
      """.strip())


      # Tell the smuggler server that we're done
      machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")

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

}
