Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39735546f1 | ||
|
|
b74f140866 | ||
|
|
639c2ffc9d | ||
|
|
a5c2e1ef44 | ||
|
|
b91c7cf077 | ||
|
|
e38f45b19f | ||
|
|
241c539f6f | ||
|
|
50a71b69b0 | ||
|
|
49ae3b4166 | ||
|
|
6d6ddbf36c | ||
|
|
4e781b4eaa | ||
|
|
5d32212b27 | ||
|
|
f80e0832bc | ||
|
|
11491a2f1f | ||
|
|
3f4e344572 | ||
|
|
6432c21b01 | ||
|
|
53a5266220 | ||
|
|
20ac781190 | ||
|
|
d7f018041e | ||
|
|
fd14479103 | ||
|
|
07b9fae361 | ||
|
|
71af23ff18 | ||
|
|
0882b75ceb | ||
|
|
a156c597ff | ||
|
|
930bb21893 | ||
|
|
022f2db6ef | ||
|
|
560ca6f54f | ||
|
|
bbccb2fc43 | ||
|
|
97253a92c2 | ||
|
|
ba36959311 | ||
|
|
19b179cb08 | ||
|
|
c148aaa998 | ||
|
|
61ab873a22 | ||
|
|
4d788bda18 | ||
|
|
bd8ec66189 |
@@ -98,7 +98,7 @@ in
|
||||
installerScriptForGHA = installScriptFor [
|
||||
# Native
|
||||
self.hydraJobs.binaryTarball."x86_64-linux"
|
||||
self.hydraJobs.binaryTarball."x86_64-darwin"
|
||||
self.hydraJobs.binaryTarball."aarch64-darwin"
|
||||
# Cross
|
||||
self.hydraJobs.binaryTarballCross."x86_64-linux"."armv6l-unknown-linux-gnueabihf"
|
||||
self.hydraJobs.binaryTarballCross."x86_64-linux"."armv7l-unknown-linux-gnueabihf"
|
||||
|
||||
6
doc/manual/rl-next/harden-user-sandboxing.md
Normal file
6
doc/manual/rl-next/harden-user-sandboxing.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
synopsis: Harden the user sandboxing
|
||||
significance: significant
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -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;
|
||||
|
||||
@@ -949,10 +949,20 @@ std::optional<Fingerprint> LockedFlake::getFingerprint(ref<Store> store) const
|
||||
auto fingerprint = flake.lockedRef.input.getFingerprint(store);
|
||||
if (!fingerprint) return std::nullopt;
|
||||
|
||||
*fingerprint += fmt(";%s;%s", flake.lockedRef.subdir, lockFile);
|
||||
|
||||
/* Include revCount and lastModified because they're not
|
||||
necessarily implied by the content fingerprint (e.g. for
|
||||
tarball flakes) but can influence the evaluation result. */
|
||||
if (auto revCount = flake.lockedRef.input.getRevCount())
|
||||
*fingerprint += fmt(";revCount=%d", *revCount);
|
||||
if (auto lastModified = flake.lockedRef.input.getLastModified())
|
||||
*fingerprint += fmt(";lastModified=%d", *lastModified);
|
||||
|
||||
// FIXME: as an optimization, if the flake contains a lock file
|
||||
// and we haven't changed it, then it's sufficient to use
|
||||
// flake.sourceInfo.storePath for the fingerprint.
|
||||
return hashString(HashAlgorithm::SHA256, fmt("%s;%s;%s", *fingerprint, flake.lockedRef.subdir, lockFile));
|
||||
return hashString(HashAlgorithm::SHA256, *fingerprint);
|
||||
}
|
||||
|
||||
Flake::~Flake() { }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS
|
||||
if (drvs.empty()) return {};
|
||||
|
||||
if (isIFD && !evalSettings.enableImportFromDerivation)
|
||||
error<EvalError>(
|
||||
error<EvalBaseError>(
|
||||
"cannot build '%1%' during evaluation because the option 'allow-import-from-derivation' is disabled",
|
||||
drvs.begin()->to_string(*store)
|
||||
).debugThrow();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -260,6 +260,7 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(ref<Store> sto
|
||||
|
||||
auto [accessor, final] = scheme->getAccessor(store, *this);
|
||||
|
||||
assert(!accessor->fingerprint);
|
||||
accessor->fingerprint = scheme->getFingerprint(store, final);
|
||||
|
||||
return {accessor, std::move(final)};
|
||||
@@ -418,7 +419,7 @@ namespace nlohmann {
|
||||
using namespace nix;
|
||||
|
||||
fetchers::PublicKey adl_serializer<fetchers::PublicKey>::from_json(const json & json) {
|
||||
fetchers::PublicKey res = { };
|
||||
fetchers::PublicKey res = { };
|
||||
if (auto type = optionalValueAt(json, "type"))
|
||||
res.type = getString(*type);
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -365,6 +365,16 @@ struct TarballInputScheme : CurlInputScheme
|
||||
|
||||
return {result.accessor, input};
|
||||
}
|
||||
|
||||
std::optional<std::string> getFingerprint(ref<Store> store, const Input & input) const override
|
||||
{
|
||||
if (auto narHash = input.getNarHash())
|
||||
return narHash->to_string(HashFormat::SRI, true);
|
||||
else if (auto rev = input.getRev())
|
||||
return rev->gitRev();
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
static auto rTarballInputScheme = OnStartup([] { registerInputScheme(std::make_unique<TarballInputScheme>()); });
|
||||
|
||||
@@ -503,8 +503,24 @@ 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);
|
||||
topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700);
|
||||
#if __APPLE__
|
||||
if (false) {
|
||||
#else
|
||||
if (useChroot) {
|
||||
#endif
|
||||
/* If sandboxing is enabled, put the actual TMPDIR underneath
|
||||
an inaccessible root-owned directory, to prevent outside
|
||||
access.
|
||||
|
||||
On macOS, we don't use an actual chroot, so this isn't
|
||||
possible. Any mitigation along these lines would have to be
|
||||
done directly in the sandbox profile. */
|
||||
tmpDir = topTmpDir + "/build";
|
||||
createDir(tmpDir, 0700);
|
||||
} else {
|
||||
tmpDir = topTmpDir;
|
||||
}
|
||||
chownToBuilder(tmpDir);
|
||||
|
||||
for (auto & [outputName, status] : initialOutputs) {
|
||||
@@ -672,15 +688,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);
|
||||
|
||||
@@ -2952,7 +2972,7 @@ void LocalDerivationGoal::checkOutputs(const std::map<std::string, ValidPathInfo
|
||||
|
||||
void LocalDerivationGoal::deleteTmpDir(bool force)
|
||||
{
|
||||
if (tmpDir != "") {
|
||||
if (topTmpDir != "") {
|
||||
/* Don't keep temporary directories for builtins because they
|
||||
might have privileged stuff (like a copy of netrc). */
|
||||
if (settings.keepFailed && !force && !drv->isBuiltin()) {
|
||||
@@ -2960,7 +2980,8 @@ void LocalDerivationGoal::deleteTmpDir(bool force)
|
||||
chmod(tmpDir.c_str(), 0755);
|
||||
}
|
||||
else
|
||||
deletePath(tmpDir);
|
||||
deletePath(topTmpDir);
|
||||
topTmpDir = "";
|
||||
tmpDir = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,16 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||
std::optional<Path> cgroup;
|
||||
|
||||
/**
|
||||
* The temporary directory.
|
||||
* The temporary directory used for the build.
|
||||
*/
|
||||
Path tmpDir;
|
||||
|
||||
/**
|
||||
* The top-level temporary directory. `tmpDir` is either equal to
|
||||
* or a child of this directory.
|
||||
*/
|
||||
Path topTmpDir;
|
||||
|
||||
/**
|
||||
* The path of the temporary directory in the sandbox.
|
||||
*/
|
||||
@@ -65,6 +71,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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -232,6 +232,8 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
||||
j["lastModified"] = *lastModified;
|
||||
j["path"] = storePath;
|
||||
j["locks"] = lockedFlake.lockFile.toJSON().first;
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store))
|
||||
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
|
||||
logger->cout("%s", j.dump());
|
||||
} else {
|
||||
logger->cout(
|
||||
@@ -264,6 +266,10 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
||||
logger->cout(
|
||||
ANSI_BOLD "Last modified:" ANSI_NORMAL " %s",
|
||||
std::put_time(std::localtime(&*lastModified), "%F %T"));
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store))
|
||||
logger->cout(
|
||||
ANSI_BOLD "Fingerprint:" ANSI_NORMAL " %s",
|
||||
fingerprint->to_string(HashFormat::Base16, false));
|
||||
|
||||
if (!lockedFlake.lockFile.root->inputs.empty())
|
||||
logger->cout(ANSI_BOLD "Inputs:" ANSI_NORMAL);
|
||||
|
||||
@@ -202,7 +202,11 @@ static PeerInfo getPeerInfo(int remote)
|
||||
|
||||
#if defined(SO_PEERCRED)
|
||||
|
||||
ucred cred;
|
||||
# if defined(__OpenBSD__)
|
||||
struct sockpeercred cred;
|
||||
# else
|
||||
ucred cred;
|
||||
# endif
|
||||
socklen_t credLen = sizeof(cred);
|
||||
if (getsockopt(remote, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == -1)
|
||||
throw SysError("getting peer credentials");
|
||||
@@ -210,9 +214,9 @@ static PeerInfo getPeerInfo(int remote)
|
||||
|
||||
#elif defined(LOCAL_PEERCRED)
|
||||
|
||||
#if !defined(SOL_LOCAL)
|
||||
#define SOL_LOCAL 0
|
||||
#endif
|
||||
# if !defined(SOL_LOCAL)
|
||||
# define SOL_LOCAL 0
|
||||
# endif
|
||||
|
||||
xucred cred;
|
||||
socklen_t credLen = sizeof(cred);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,12 +7,22 @@ requireGit
|
||||
flake1Dir="$TEST_ROOT/eval-cache-flake"
|
||||
|
||||
createGitRepo "$flake1Dir" ""
|
||||
cp ../simple.nix ../simple.builder.sh ../config.nix "$flake1Dir/"
|
||||
git -C "$flake1Dir" add simple.nix simple.builder.sh config.nix
|
||||
git -C "$flake1Dir" commit -m "config.nix"
|
||||
|
||||
cat >"$flake1Dir/flake.nix" <<EOF
|
||||
{
|
||||
description = "Fnord";
|
||||
outputs = { self }: {
|
||||
outputs = { self }: let inherit (import ./config.nix) mkDerivation; in {
|
||||
foo.bar = throw "breaks";
|
||||
drv = mkDerivation {
|
||||
name = "build";
|
||||
buildCommand = ''
|
||||
echo true > \$out
|
||||
'';
|
||||
};
|
||||
ifd = assert (import self.drv); self.drv;
|
||||
};
|
||||
}
|
||||
EOF
|
||||
@@ -22,3 +32,8 @@ git -C "$flake1Dir" commit -m "Init"
|
||||
|
||||
expect 1 nix build "$flake1Dir#foo.bar" 2>&1 | grepQuiet 'error: breaks'
|
||||
expect 1 nix build "$flake1Dir#foo.bar" 2>&1 | grepQuiet 'error: breaks'
|
||||
|
||||
# Conditional error should not be cached
|
||||
expect 1 nix build "$flake1Dir#ifd" --option allow-import-from-derivation false 2>&1 \
|
||||
| grepQuiet 'error: cannot build .* during evaluation because the option '\''allow-import-from-derivation'\'' is disabled'
|
||||
nix build "$flake1Dir#ifd"
|
||||
|
||||
@@ -189,6 +189,7 @@ json=$(nix flake metadata flake1 --json | jq .)
|
||||
[[ -d $(echo "$json" | jq -r .path) ]]
|
||||
[[ $(echo "$json" | jq -r .lastModified) = $(git -C "$flake1Dir" log -n1 --format=%ct) ]]
|
||||
hash1=$(echo "$json" | jq -r .revision)
|
||||
[[ -n $(echo "$json" | jq -r .fingerprint) ]]
|
||||
|
||||
echo foo > "$flake1Dir/foo"
|
||||
git -C "$flake1Dir" add $flake1Dir/foo
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ in
|
||||
# Check that we got redirected to the immutable URL.
|
||||
assert info["locked"]["url"] == "http://localhost/stable/${nixpkgs.rev}.tar.gz"
|
||||
|
||||
# Check that we got a fingerprint for caching.
|
||||
assert info["fingerprint"]
|
||||
|
||||
# Check that we got the rev and revCount attributes.
|
||||
assert info["revision"] == "${nixpkgs.rev}"
|
||||
assert info["revCount"] == 1234
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
129
tests/nixos/user-sandboxing/default.nix
Normal file
129
tests/nixos/user-sandboxing/default.nix
Normal 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())
|
||||
'';
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user