Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60f41e991d | ||
|
|
741a5ca774 | ||
|
|
2f75369094 | ||
|
|
60d50ea31b | ||
|
|
4f0c59b307 | ||
|
|
46e2bb4ca6 | ||
|
|
201be7976b | ||
|
|
8e4b4c62f5 | ||
|
|
fe82fcdddc | ||
|
|
3af68dcef0 | ||
|
|
0001c1aeaf | ||
|
|
77daadb029 | ||
|
|
3f02ca5c35 | ||
|
|
1a4cb13411 | ||
|
|
e9f0354f7a | ||
|
|
fbd6a014ec | ||
|
|
2387104452 | ||
|
|
d84f13b73f | ||
|
|
37a570bd40 | ||
|
|
e62b7236e8 | ||
|
|
33eaaf02fd | ||
|
|
dcb0a97000 |
@@ -1,4 +1,83 @@
|
||||
# Lix 2.93 "Bici Bici" (2025-05-09)
|
||||
# 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)
|
||||
|
||||
+7
-8
@@ -193,13 +193,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,7 +359,8 @@ 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.";
|
||||
"org.opencontainers.image.description" =
|
||||
"Minimal Lix container image, with some batteries included.";
|
||||
} // lib.optionalAttrs (lixRevision != null) { "org.opencontainers.image.revision" = lixRevision; };
|
||||
};
|
||||
|
||||
|
||||
Generated
+66
-7
@@ -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": 1749522908,
|
||||
"narHash": "sha256-eWANkhWXFL1MmaxzsZ9bhLCNT8OVs7CC+OXaSDGlA8A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e5cb99555c45a13dcc5f1317462238530b0066b7",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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,11 @@
|
||||
{
|
||||
nixStable = prev.nix;
|
||||
|
||||
# Nix 2.18 has been removed from Nixpkgs ≥ 25.05, so we need to reintroduce it ourselves for our tests.
|
||||
nixVersions = prev.nixVersions // {
|
||||
nix_2_18 = nix_2_18.outputs.packages.${currentStdenv.hostPlatform.system}.default;
|
||||
};
|
||||
|
||||
# Forward from the previous stage as we don’t want it to pick the lowdown override
|
||||
nixUnstable = prev.nixUnstable;
|
||||
|
||||
@@ -383,10 +400,12 @@
|
||||
name = "nixpkgs-lib-tests";
|
||||
paths =
|
||||
[ testWithNix ]
|
||||
# NOTE: nixpkgs 24.11 is being ... *creative*, and requires this dance to override
|
||||
# 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" { nixVersions.nix_2_24 = nix; }).attrpathsSuperset
|
||||
((pkgs.callPackage "${nixpkgs}/ci/eval" { nixVersions.latest = nix; }).attrpathsSuperset {
|
||||
evalSystem = system;
|
||||
})
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1011,11 +1011,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",
|
||||
|
||||
@@ -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,6 +27,7 @@
|
||||
#include "lix/libutil/mount.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libutil/thread-name.hh"
|
||||
#include "platform/linux.hh"
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
@@ -391,9 +394,13 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeCheck()
|
||||
|
||||
void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck()
|
||||
{
|
||||
/* Delete unused redirected outputs (when doing hash rewriting). */
|
||||
for (auto & i : redirectedOutputs)
|
||||
deletePath(worker.store.Store::toRealPath(i.second));
|
||||
/* In the past, redirected outputs were manually tracked for deletion.
|
||||
* Now that we have the scratch outputs cleaner which are a superset of
|
||||
* redirected outputs, we just fire all uncancelled automatic deleters now.
|
||||
*
|
||||
* This should clean up any paths that IS NOT registered in the database.
|
||||
*/
|
||||
scratchOutputsCleaner.clear();
|
||||
|
||||
/* Delete the chroot (if we were using one). */
|
||||
autoDelChroot.reset(); /* this runs the destructor */
|
||||
@@ -480,17 +487,24 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
createDirs(settings.buildDir.get());
|
||||
|
||||
/* Create a temporary directory where the build will take
|
||||
place. */
|
||||
tmpDir = createTempDir(
|
||||
settings.buildDir.get().value_or(""),
|
||||
settings.buildDir.get(),
|
||||
"nix-build-" + std::string(drvPath.name()),
|
||||
false,
|
||||
false,
|
||||
0700
|
||||
);
|
||||
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
|
||||
* POSIX semantics.*/
|
||||
tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
|
||||
if (!tmpDirFd)
|
||||
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir);
|
||||
|
||||
chownToBuilder(tmpDir);
|
||||
chownToBuilder(tmpDirFd);
|
||||
|
||||
for (auto & [outputName, status] : initialOutputs) {
|
||||
/* Set scratch path we'll actually use during the build.
|
||||
@@ -521,6 +535,10 @@ try {
|
||||
to use a temporary path */
|
||||
makeFallbackPath(status.known->path);
|
||||
scratchOutputs.insert_or_assign(outputName, scratchPath);
|
||||
/* Schedule this scratch output path for automatic deletion
|
||||
* if we do not cancel it, e.g. when registering the outputs.
|
||||
*/
|
||||
scratchOutputsCleaner.insert_or_assign(outputName, worker.store.printStorePath(scratchPath));
|
||||
|
||||
/* Substitute output placeholders with the scratch output paths.
|
||||
We'll use during the build. */
|
||||
@@ -543,8 +561,6 @@ try {
|
||||
std::string h2 { scratchPath.hashPart() };
|
||||
inputRewrites[h1] = h2;
|
||||
}
|
||||
|
||||
redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath));
|
||||
}
|
||||
|
||||
/* Construct the environment passed to the builder. */
|
||||
@@ -858,8 +874,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 +996,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 +1133,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 +1154,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 +1286,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
|
||||
@@ -1972,7 +2035,9 @@ try {
|
||||
}
|
||||
|
||||
/* Don't register anything, since we already have the
|
||||
previous versions which we're comparing. */
|
||||
previous versions which we're comparing.
|
||||
NOTE: this means that the `.check` path will be automatically deleted.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1996,8 +2061,13 @@ try {
|
||||
/* If it's a CA path, register it right away. This is necessary if it
|
||||
isn't statically known so that we can safely unlock the path before
|
||||
the next iteration */
|
||||
if (newInfo.ca)
|
||||
if (newInfo.ca) {
|
||||
TRY_AWAIT(localStore.registerValidPaths({{newInfo.path, newInfo}}));
|
||||
/* Cancel automatic deletion of that output if it was a scratch output. */
|
||||
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
|
||||
cleaner.mapped().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
infos.emplace(outputName, std::move(newInfo));
|
||||
}
|
||||
@@ -2037,6 +2107,13 @@ try {
|
||||
infos2.insert_or_assign(newInfo.path, newInfo);
|
||||
}
|
||||
TRY_AWAIT(localStore.registerValidPaths(infos2));
|
||||
|
||||
/* Cancel automatic deletion of that output if it was a scratch output that we just registered. */
|
||||
for (auto & [outputName, _ ] : infos) {
|
||||
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
|
||||
cleaner.mapped().cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* In case of a fixed-output derivation hash mismatch, throw an
|
||||
@@ -2070,6 +2147,13 @@ try {
|
||||
builtOutputs.emplace(outputName, thisRealisation);
|
||||
}
|
||||
|
||||
/* NOTE: At this point, all outputs MAY NOT have been registered.
|
||||
* Therefore, there may remains auto-deleters pending in the cleaner list (`scratchOutputsCleaner`).
|
||||
*
|
||||
* They will be finally deleted but we have no way to assert they all have been, e.g.
|
||||
* `assert(scratchOutputsCleaner.size() == 0)` cannot be written.
|
||||
*/
|
||||
|
||||
co_return builtOutputs;
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
|
||||
@@ -31,6 +31,11 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||
*/
|
||||
Path tmpDir;
|
||||
|
||||
/**
|
||||
* The temporary directory file descriptor
|
||||
*/
|
||||
AutoCloseFD tmpDirFd;
|
||||
|
||||
/**
|
||||
* The path of the temporary directory in the sandbox.
|
||||
*/
|
||||
@@ -95,8 +100,6 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||
* Hash rewriting.
|
||||
*/
|
||||
StringMap inputRewrites, outputRewrites;
|
||||
typedef map<StorePath, StorePath> RedirectedOutputs;
|
||||
RedirectedOutputs redirectedOutputs;
|
||||
|
||||
/**
|
||||
* The outputs paths used during the build.
|
||||
@@ -113,6 +116,19 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||
* self-references.
|
||||
*/
|
||||
OutputPathMap scratchOutputs;
|
||||
/**
|
||||
* Output paths used during the build are scheduled for
|
||||
* automatic cleanup unless they have been successfully built.
|
||||
*
|
||||
* `registerOutputs` take care of cancelling the cleanups
|
||||
* and clearing this vector.
|
||||
*
|
||||
* `startBuilder` take care of filling this vector
|
||||
* as `scratchOutputs` gets filled.
|
||||
*
|
||||
* This is a map from output names to automatic delete handles.
|
||||
*/
|
||||
std::map<std::string, AutoDelete> scratchOutputsCleaner;
|
||||
|
||||
/**
|
||||
* Path registration info from the previous round, if we're
|
||||
@@ -191,10 +207,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;
|
||||
|
||||
/**
|
||||
@@ -269,6 +293,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 +308,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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
---
|
||||
name: build-dir
|
||||
internalName: buildDir
|
||||
settingType: PathsSetting<std::optional<Path>>
|
||||
default: null
|
||||
settingType: PathsSetting<Path>
|
||||
defaultText: "`«nixStateDir»/builds`"
|
||||
defaultExpr: nixStateDir + "/builds"
|
||||
---
|
||||
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 `builds` 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
+4
-34
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -632,7 +632,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()) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
+55
-24
@@ -359,25 +359,44 @@ Generator<Bytes> readFileSource(const Path & path)
|
||||
}
|
||||
|
||||
|
||||
void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
|
||||
void writeFile(const Path & path, std::string_view s, mode_t mode)
|
||||
{
|
||||
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
|
||||
if (!fd)
|
||||
throw SysError("opening file '%1%'", path);
|
||||
|
||||
writeFile(fd, s, mode);
|
||||
|
||||
/* Close explicitly to propagate the exceptions. */
|
||||
fd.close();
|
||||
}
|
||||
|
||||
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode)
|
||||
{
|
||||
assert(fd);
|
||||
try {
|
||||
writeFull(fd.get(), s);
|
||||
} catch (Error & e) {
|
||||
e.addTrace({}, "writing file '%1%'", path);
|
||||
e.addTrace({}, "writing file '%1%'", fd.guessOrInventPath());
|
||||
throw;
|
||||
}
|
||||
if (sync)
|
||||
fd.fsync();
|
||||
// Explicitly close to make sure exceptions are propagated.
|
||||
fd.close();
|
||||
if (sync)
|
||||
syncParent(path);
|
||||
}
|
||||
|
||||
void writeFileAndSync(const Path & path, std::string_view s, mode_t mode)
|
||||
{
|
||||
{
|
||||
AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
|
||||
if (!fd)
|
||||
throw SysError("opening file '%1%'", path);
|
||||
|
||||
writeFile(fd, s, mode);
|
||||
fd.fsync();
|
||||
/* Close explicitly to ensure that exceptions are propagated. */
|
||||
fd.close();
|
||||
}
|
||||
|
||||
syncParent(path);
|
||||
}
|
||||
|
||||
static AutoCloseFD openForWrite(const Path & path, mode_t mode)
|
||||
{
|
||||
@@ -397,7 +416,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 +433,11 @@ 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)
|
||||
writeFile(const Path & path, AsyncInputStream & source, mode_t mode)
|
||||
try {
|
||||
AutoCloseFD fd = openForWrite(path, mode);
|
||||
|
||||
@@ -436,7 +455,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 +469,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 +522,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 +556,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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -189,11 +189,18 @@ 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);
|
||||
|
||||
void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false);
|
||||
void writeFile(const Path & path, Source & source, mode_t mode = 0666);
|
||||
|
||||
void writeFile(AutoCloseFD & fd, std::string_view s, mode_t mode = 0666);
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(); };
|
||||
};
|
||||
|
||||
@@ -53,7 +53,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);
|
||||
|
||||
+10
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -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 = [
|
||||
@@ -446,6 +451,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...
|
||||
|
||||
@@ -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)',
|
||||
)
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
})
|
||||
@@ -1 +1,2 @@
|
||||
d @localstatedir@/nix/daemon-socket 0755 root root - -
|
||||
d @localstatedir@/nix/daemon-socket 0755 root root - -
|
||||
d @localstatedir@/nix/builds 0755 root root 7d -
|
||||
|
||||
@@ -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 { };
|
||||
},
|
||||
}:
|
||||
|
||||
@@ -247,6 +251,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,6 +268,8 @@ 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.
|
||||
@@ -330,6 +337,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
++ lib.optionals hostPlatform.isLinux [
|
||||
libseccomp
|
||||
busybox-sandbox-shell
|
||||
passt-lix
|
||||
]
|
||||
++ lib.optionals (
|
||||
stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "11.0"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"; }
|
||||
|
||||
|
||||
@@ -49,27 +49,6 @@ test_custom_build_dir() {
|
||||
}
|
||||
test_custom_build_dir
|
||||
|
||||
test_custom_temp_dir() {
|
||||
# like test_custom_build_dir(), but uses the temp-dir setting instead
|
||||
# build-dir inherits from temp-dir when build-dir is unset
|
||||
local customTempDir="$TEST_ROOT/custom-temp-dir"
|
||||
|
||||
mkdir "$customTempDir"
|
||||
nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \
|
||||
--no-out-link --keep-failed --option temp-dir "$customTempDir" 2> $TEST_ROOT/log || status=$?
|
||||
[ "$status" = "100" ]
|
||||
[[ 1 == "$(count "$customTempDir/nix-build-"*)" ]]
|
||||
local buildDir="$customTempDir/nix-build-"*
|
||||
grep $checkBuildId $buildDir/checkBuildId
|
||||
|
||||
# also check a separate code path that doesn't involve build-dir
|
||||
# nix-shell uses temp-dir for its rcfile path
|
||||
rcpath=$(NIX_BUILD_SHELL=$SHELL nix-shell check.nix -A deterministic --option temp-dir "$customTempDir" --run 'echo $0' 2> $TEST_ROOT/log)
|
||||
# rcpath is <temp-dir>/nix-shell-*/rc
|
||||
[[ $rcpath = "$customTempDir"/* ]]
|
||||
}
|
||||
test_custom_temp_dir
|
||||
|
||||
test_shell_preserves_tmpdir() {
|
||||
# ensure commands that spawn interactive shells don't overwrite TMPDIR with temp-dir
|
||||
local envTempDir=$TEST_ROOT/shell-temp-dir-env
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
'';
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
+8
-21
@@ -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,12 +165,13 @@ 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;
|
||||
|
||||
# Use this test to test things that cannot easily be tested under chroot Nix stores in functional test suite.
|
||||
non-chroot-misc = runNixOSTestFor "x86_64-linux" ./non-chroot-misc;
|
||||
|
||||
noNewPrivilegesInSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/sandbox.nix;
|
||||
|
||||
noNewPrivilegesOutsideSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/no-sandbox.nix;
|
||||
|
||||
@@ -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=\"; }'")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{ ... }:
|
||||
# Misc things we want to test inside of a non redirected, non chroot Nix store.
|
||||
let
|
||||
nonAutoCleaningFailingDerivationCode = ''
|
||||
derivation {
|
||||
name = "scratch-failing";
|
||||
system = builtins.currentSystem;
|
||||
builder = "/bin/sh";
|
||||
args = [ (builtins.toFile "builder.sh" "echo bonjour > $out; echo out: $out; false") ];
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
name = "non-chroot-sandbox-misc";
|
||||
|
||||
nodes.machine = {
|
||||
};
|
||||
|
||||
testScript = { nodes }: ''
|
||||
import re
|
||||
start_all()
|
||||
|
||||
# You might ask yourself why write such a convoluted thing?
|
||||
# The condition for fooling Nix into NOT cleaning up the output path are non trivial and unclear.
|
||||
# This is one of those: create a derivation, mkdir or touch the $out path, communicate it back.
|
||||
# Even with a sandboxed Lix, you will observe leftovers before 2.93.0. After this version, this test passes.
|
||||
result = machine.fail("""nix-build --substituters "" -E '${nonAutoCleaningFailingDerivationCode}' 2>&1""")
|
||||
match = re.search(r'out: (\S+)', result)
|
||||
assert match is not None, "Did not find Nix store path in the result of the failing build"
|
||||
outpath = match.group(1).strip()
|
||||
print(f"Found Nix store path: {outpath}")
|
||||
machine.fail(f'stat {outpath}')
|
||||
'';
|
||||
}
|
||||
@@ -98,7 +98,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")
|
||||
|
||||
|
||||
@@ -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
|
||||
* --------------------------------------------------------------------------*/
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.93.0",
|
||||
"version": "2.93.1",
|
||||
"official_release": true,
|
||||
"release_name": "Bici Bici"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user