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