1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2024-09-19 23:03:53 -04:00
nix/tests/nixos/ca-fd-leak/default.nix
Théophane Hufschmitt a55c6a0f47 Add a NixOS test for the sandbox escape
Test that we can't leverage abstract unix domain sockets to leak file
descriptors out of the sandbox and modify the path after it has been
registered.
2024-03-01 09:31:28 +01:00

91 lines
3.5 KiB
Nix

# 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 modified
machine.succeed(r"""
cat ./result
test "$(cat ./result)" = "hello, world"
""".strip())
'';
}