aboutsummaryrefslogtreecommitdiff
path: root/tests/nixos/ca-fd-leak/default.nix
blob: a6ae72adc93ec66fbef97f8c883e215b98810622 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 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())
  '';

}