Skip to content

Commit 448cfb7

Browse files
committed
Merge remote-tracking branch 'cve/fod-cves-master'
2 parents f9afc1e + 37685b1 commit 448cfb7

File tree

15 files changed

+180
-53
lines changed

15 files changed

+180
-53
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
synopsis: "`build-dir` no longer defaults to `$TMPDIR`"
3+
---
4+
5+
The directory in which temporary build directories are created no longer defaults
6+
to `TMPDIR` or `/tmp`, to avoid builders making their directories
7+
world-accessible. This behavior allowed escaping the build sandbox and can
8+
cause build impurities even when not used maliciously. We now default to `builds`
9+
in `NIX_STATE_DIR` (which is `/nix/var/nix/builds` in the default configuration).

misc/systemd/nix-daemon.conf.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
d @localstatedir@/nix/daemon-socket 0755 root root - -
1+
d @localstatedir@/nix/daemon-socket 0755 root root - -
2+
d @localstatedir@/nix/builds 0755 root root 7d -

src/libstore/include/nix/store/globals.hh

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -697,14 +697,7 @@ public:
697697

698698
Setting<std::optional<Path>> buildDir{this, std::nullopt, "build-dir",
699699
R"(
700-
The directory on the host, in which derivations' temporary build directories are created.
701-
702-
If not set, Nix uses the system temporary directory indicated by the `TMPDIR` environment variable.
703-
Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface.
704-
705-
This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files.
706-
707-
If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment contains this directory instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir).
700+
Override the `build-dir` store setting for all stores that have this setting.
708701
)"};
709702

710703
Setting<PathSet> allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps",

src/libstore/include/nix/store/local-store.hh

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,39 @@ struct OptimiseStats
3434
uint64_t bytesFreed = 0;
3535
};
3636

37-
struct LocalStoreConfig : std::enable_shared_from_this<LocalStoreConfig>, virtual LocalFSStoreConfig
37+
struct LocalBuildStoreConfig : virtual LocalFSStoreConfig
38+
{
39+
40+
private:
41+
/**
42+
Input for computing the build directory. See `getBuildDir()`.
43+
*/
44+
Setting<std::optional<Path>> buildDir{this, std::nullopt, "build-dir",
45+
R"(
46+
The directory on the host, in which derivations' temporary build directories are created.
47+
48+
If not set, Nix will use the `builds` subdirectory of its configured state directory.
49+
50+
Note that builds are often performed by the Nix daemon, so its `build-dir` applies.
51+
52+
Nix will create this directory automatically with suitable permissions if it does not exist.
53+
Otherwise its permissions must allow all users to traverse the directory (i.e. it must have `o+x` set, in unix parlance) for non-sandboxed builds to work correctly.
54+
55+
This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files.
56+
57+
If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir).
58+
59+
> **Warning**
60+
>
61+
> `build-dir` must not be set to a world-writable directory.
62+
> Placing temporary build directories in a world-writable place allows other users to access or modify build data that is currently in use.
63+
> This alone is merely an impurity, but combined with another factor this has allowed malicious derivations to escape the build sandbox.
64+
)"};
65+
public:
66+
Path getBuildDir() const;
67+
};
68+
69+
struct LocalStoreConfig : std::enable_shared_from_this<LocalStoreConfig>, virtual LocalFSStoreConfig, virtual LocalBuildStoreConfig
3870
{
3971
using LocalFSStoreConfig::LocalFSStoreConfig;
4072

src/libstore/local-store.cc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ std::string LocalStoreConfig::doc()
7777
;
7878
}
7979

80+
Path LocalBuildStoreConfig::getBuildDir() const
81+
{
82+
return
83+
settings.buildDir.get().has_value()
84+
? *settings.buildDir.get()
85+
: buildDir.get().has_value()
86+
? *buildDir.get()
87+
: stateDir.get() + "/builds";
88+
}
89+
8090
ref<Store> LocalStore::Config::openStore() const
8191
{
8292
return make_ref<LocalStore>(ref{shared_from_this()});
@@ -247,7 +257,7 @@ LocalStore::LocalStore(ref<const Config> config)
247257
else if (curSchema == 0) { /* new store */
248258
curSchema = nixSchemaVersion;
249259
openDB(*state, true);
250-
writeFile(schemaPath, fmt("%1%", curSchema), 0666, true);
260+
writeFile(schemaPath, fmt("%1%", curSchema), 0666, FsSync::Yes);
251261
}
252262

253263
else if (curSchema < nixSchemaVersion) {
@@ -298,7 +308,7 @@ LocalStore::LocalStore(ref<const Config> config)
298308
txn.commit();
299309
}
300310

301-
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true);
311+
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, FsSync::Yes);
302312

303313
lockFile(globalLock.get(), ltRead, true);
304314
}

src/libstore/unix/build/derivation-builder.cc

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilder
9595
*/
9696
Path topTmpDir;
9797

98+
/**
99+
* The file descriptor of the temporary directory.
100+
*/
101+
AutoCloseFD tmpDirFd;
102+
98103
/**
99104
* The sort of derivation we are building.
100105
*
@@ -300,9 +305,24 @@ class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilder
300305

301306
/**
302307
* Make a file owned by the builder.
308+
*
309+
* SAFETY: this function is prone to TOCTOU as it receives a path and not a descriptor.
310+
* It's only safe to call in a child of a directory only visible to the owner.
303311
*/
304312
void chownToBuilder(const Path & path);
305313

314+
/**
315+
* Make a file owned by the builder addressed by its file descriptor.
316+
*/
317+
void chownToBuilder(int fd, const Path & path);
318+
319+
/**
320+
* Create a file in `tmpDir` owned by the builder.
321+
*/
322+
void writeBuilderFile(
323+
const std::string & name,
324+
std::string_view contents);
325+
306326
/**
307327
* Run the builder's process.
308328
*/
@@ -678,6 +698,18 @@ static void handleChildException(bool sendException)
678698
}
679699
}
680700

701+
static bool checkNotWorldWritable(std::filesystem::path path)
702+
{
703+
while (true) {
704+
auto st = lstat(path);
705+
if (st.st_mode & S_IWOTH)
706+
return false;
707+
if (path == path.parent_path()) break;
708+
path = path.parent_path();
709+
}
710+
return true;
711+
}
712+
681713
void DerivationBuilderImpl::startBuilder()
682714
{
683715
/* Make sure that no other processes are executing under the
@@ -705,12 +737,26 @@ void DerivationBuilderImpl::startBuilder()
705737
throw BuildError(msg);
706738
}
707739

740+
auto buildDir = getLocalStore(store).config->getBuildDir();
741+
742+
createDirs(buildDir);
743+
744+
if (buildUser && !checkNotWorldWritable(buildDir))
745+
throw Error("Path %s or a parent directory is world-writable or a symlink. That's not allowed for security.", buildDir);
746+
708747
/* Create a temporary directory where the build will take
709748
place. */
710-
topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), 0700);
749+
topTmpDir = createTempDir(buildDir, "nix-build-" + std::string(drvPath.name()), 0700);
711750
setBuildTmpDir();
712751
assert(!tmpDir.empty());
713-
chownToBuilder(tmpDir);
752+
753+
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
754+
POSIX semantics.*/
755+
tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
756+
if (!tmpDirFd)
757+
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir);
758+
759+
chownToBuilder(tmpDirFd.get(), tmpDir);
714760

715761
for (auto & [outputName, status] : initialOutputs) {
716762
/* Set scratch path we'll actually use during the build.
@@ -876,7 +922,7 @@ DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox()
876922
store.computeFSClosure(store.toStorePath(i.second.source).first, closure);
877923
} catch (InvalidPath & e) {
878924
} catch (Error & e) {
879-
e.addTrace({}, "while processing 'sandbox-paths'");
925+
e.addTrace({}, "while processing sandbox path '%s'", i.second.source);
880926
throw;
881927
}
882928
for (auto & i : closure) {
@@ -1049,13 +1095,10 @@ void DerivationBuilderImpl::initEnv()
10491095
} else {
10501096
auto hash = hashString(HashAlgorithm::SHA256, i.first);
10511097
std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false);
1052-
Path p = tmpDir + "/" + fn;
1053-
writeFile(p, rewriteStrings(i.second, inputRewrites));
1054-
chownToBuilder(p);
1098+
writeBuilderFile(fn, rewriteStrings(i.second, inputRewrites));
10551099
env[i.first + "Path"] = tmpDirInSandbox() + "/" + fn;
10561100
}
10571101
}
1058-
10591102
}
10601103

10611104
/* For convenience, set an environment pointing to the top build
@@ -1130,11 +1173,9 @@ void DerivationBuilderImpl::writeStructuredAttrs()
11301173

11311174
auto jsonSh = StructuredAttrs::writeShell(json);
11321175

1133-
writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites));
1134-
chownToBuilder(tmpDir + "/.attrs.sh");
1176+
writeBuilderFile(".attrs.sh", rewriteStrings(jsonSh, inputRewrites));
11351177
env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox() + "/.attrs.sh";
1136-
writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites));
1137-
chownToBuilder(tmpDir + "/.attrs.json");
1178+
writeBuilderFile(".attrs.json", rewriteStrings(json.dump(), inputRewrites));
11381179
env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox() + "/.attrs.json";
11391180
}
11401181
}
@@ -1255,6 +1296,25 @@ void DerivationBuilderImpl::chownToBuilder(const Path & path)
12551296
throw SysError("cannot change ownership of '%1%'", path);
12561297
}
12571298

1299+
void DerivationBuilderImpl::chownToBuilder(int fd, const Path & path)
1300+
{
1301+
if (!buildUser) return;
1302+
if (fchown(fd, buildUser->getUID(), buildUser->getGID()) == -1)
1303+
throw SysError("cannot change ownership of file '%1%'", path);
1304+
}
1305+
1306+
void DerivationBuilderImpl::writeBuilderFile(
1307+
const std::string & name,
1308+
std::string_view contents)
1309+
{
1310+
auto path = std::filesystem::path(tmpDir) / name;
1311+
AutoCloseFD fd{openat(tmpDirFd.get(), name.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)};
1312+
if (!fd)
1313+
throw SysError("creating file %s", path);
1314+
writeFile(fd, path, contents);
1315+
chownToBuilder(fd.get(), path);
1316+
}
1317+
12581318
void DerivationBuilderImpl::runChild()
12591319
{
12601320
/* Warning: in the child we should absolutely not make any SQLite
@@ -2063,6 +2123,15 @@ void DerivationBuilderImpl::checkOutputs(const std::map<std::string, ValidPathIn
20632123
void DerivationBuilderImpl::deleteTmpDir(bool force)
20642124
{
20652125
if (topTmpDir != "") {
2126+
/* As an extra precaution, even in the event of `deletePath` failing to
2127+
* clean up, the `tmpDir` will be chowned as if we were to move
2128+
* it inside the Nix store.
2129+
*
2130+
* This hardens against an attack which smuggles a file descriptor
2131+
* to make use of the temporary directory.
2132+
*/
2133+
chmod(topTmpDir.c_str(), 0000);
2134+
20662135
/* Don't keep temporary directories for builtins because they
20672136
might have privileged stuff (like a copy of netrc). */
20682137
if (settings.keepFailed && !force && !drv.isBuiltin()) {

src/libutil/file-content-address.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void restorePath(
9393
{
9494
switch (method) {
9595
case FileSerialisationMethod::Flat:
96-
writeFile(path, source, 0666, startFsync);
96+
writeFile(path, source, 0666, startFsync ? FsSync::Yes : FsSync::No);
9797
break;
9898
case FileSerialisationMethod::NixArchive:
9999
restorePath(path, source, startFsync);

src/libutil/file-system.cc

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ void readFile(const Path & path, Sink & sink, bool memory_map)
304304
}
305305

306306

307-
void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
307+
void writeFile(const Path & path, std::string_view s, mode_t mode, FsSync sync)
308308
{
309309
AutoCloseFD fd = toDescriptor(open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT
310310
// TODO
@@ -314,22 +314,29 @@ void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
314314
, mode));
315315
if (!fd)
316316
throw SysError("opening file '%1%'", path);
317+
318+
writeFile(fd, path, s, mode, sync);
319+
320+
/* Close explicitly to propagate the exceptions. */
321+
fd.close();
322+
}
323+
324+
void writeFile(AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode, FsSync sync)
325+
{
326+
assert(fd);
317327
try {
318328
writeFull(fd.get(), s);
329+
330+
if (sync == FsSync::Yes)
331+
fd.fsync();
332+
319333
} catch (Error & e) {
320-
e.addTrace({}, "writing file '%1%'", path);
334+
e.addTrace({}, "writing file '%1%'", origPath);
321335
throw;
322336
}
323-
if (sync)
324-
fd.fsync();
325-
// Explicitly close to make sure exceptions are propagated.
326-
fd.close();
327-
if (sync)
328-
syncParent(path);
329337
}
330338

331-
332-
void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
339+
void writeFile(const Path & path, Source & source, mode_t mode, FsSync sync)
333340
{
334341
AutoCloseFD fd = toDescriptor(open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT
335342
// TODO
@@ -353,11 +360,11 @@ void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
353360
e.addTrace({}, "writing file '%1%'", path);
354361
throw;
355362
}
356-
if (sync)
363+
if (sync == FsSync::Yes)
357364
fd.fsync();
358365
// Explicitly close to make sure exceptions are propagated.
359366
fd.close();
360-
if (sync)
367+
if (sync == FsSync::Yes)
361368
syncParent(path);
362369
}
363370

@@ -435,7 +442,8 @@ static void _deletePath(Descriptor parentfd, const std::filesystem::path & path,
435442
}
436443
#endif
437444

438-
std::string name(baseNameOf(path.native()));
445+
std::string name(path.filename());
446+
assert(name != "." && name != ".." && !name.empty());
439447

440448
struct stat st;
441449
if (fstatat(parentfd, name.c_str(), &st,
@@ -476,7 +484,7 @@ static void _deletePath(Descriptor parentfd, const std::filesystem::path & path,
476484
throw SysError("chmod %1%", path);
477485
}
478486

479-
int fd = openat(parentfd, path.c_str(), O_RDONLY);
487+
int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
480488
if (fd == -1)
481489
throw SysError("opening directory %1%", path);
482490
AutoCloseDir dir(fdopendir(fd));
@@ -488,7 +496,7 @@ static void _deletePath(Descriptor parentfd, const std::filesystem::path & path,
488496
checkInterrupt();
489497
std::string childName = dirent->d_name;
490498
if (childName == "." || childName == "..") continue;
491-
_deletePath(dirfd(dir.get()), path + "/" + childName, bytesFreed, ex MOUNTEDPATHS_ARG);
499+
_deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG);
492500
}
493501
if (errno) throw SysError("reading directory %1%", path);
494502
}
@@ -513,14 +521,13 @@ static void _deletePath(Descriptor parentfd, const std::filesystem::path & path,
513521

514522
static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM)
515523
{
516-
Path dir = dirOf(path.string());
517-
if (dir == "")
518-
dir = "/";
524+
assert(path.is_absolute());
525+
assert(path.parent_path() != path);
519526

520-
AutoCloseFD dirfd = toDescriptor(open(dir.c_str(), O_RDONLY));
527+
AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY));
521528
if (!dirfd) {
522529
if (errno == ENOENT) return;
523-
throw SysError("opening directory '%1%'", path);
530+
throw SysError("opening directory %s", path.parent_path());
524531
}
525532

526533
std::exception_ptr ex;

0 commit comments

Comments
 (0)