1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2024-10-18 00:16:11 -04:00

Compare commits

...

19 commits

Author SHA1 Message Date
Eelco Dolstra c326bfdefa
Merge fc3f70cd5e into f51974d698 2024-10-17 00:09:53 +02:00
Robert Hensing f51974d698
Merge pull request #11665 from roberth/fix-Interrupted-falling-out-of-thread
Fix `Interrupted` falling out of thread crash
2024-10-16 20:09:29 +02:00
Robert Hensing ed184f0b61
Typo
Co-authored-by: Cole Helbling <cole.e.helbling@outlook.com>
2024-10-16 19:40:45 +02:00
Robert Hensing fd8a4a86d9 ThreadPool: don't silently ignore non-std exceptions
Introduced in 8f6b347abd without explanation.

Throwing anything that's not that is a programming mistake that we don't want
to ignore silently. A crash would be ok, because that means we/they can fix
the offending throw.
2024-10-16 17:56:08 +02:00
Robert Hensing 16320f6d24 Handle ThreadPoolShutdown with normal catch 2024-10-16 17:56:08 +02:00
Robert Hensing 3f9ff10786 ThreadPool: catch Interrupted 2024-10-16 17:56:08 +02:00
Robert Hensing de41e46175 Document recursive-nix startDaemon/stopDaemon 2024-10-16 17:56:08 +02:00
Robert Hensing 0e5a5303ad fix: Ignore Interrupted in recursive-nix daemon worker
Otherwise, if checkInterrupt() in any of the supported store operations
would catch onto a user interrupt, the exception would bubble to the thread
start and be handled by std::terminate(): a crash.
2024-10-16 17:56:08 +02:00
Eelco Dolstra fc3f70cd5e Return instead of throw RestrictedPathError 2024-09-25 14:32:39 +02:00
Eelco Dolstra be7dac494f Merge remote-tracking branch 'origin/master' into pathExists-regression 2024-09-25 14:21:43 +02:00
Eelco Dolstra 7a11e60389 Reword 2024-04-18 16:57:02 +02:00
Eelco Dolstra 873be03a42 Fix test 2024-04-16 18:50:06 +02:00
Eelco Dolstra 75a070c52e tests/functional/restricted.sh: Check error message 2024-04-16 18:45:02 +02:00
Eelco Dolstra b3a6b794b0 Fix "access to path is forbidden" 2024-04-16 18:45:02 +02:00
Eelco Dolstra 978b3648df Typo 2024-04-16 18:45:02 +02:00
Eelco Dolstra 9668546178 Remove unnecessary arg highlighting 2024-04-16 18:45:02 +02:00
Eelco Dolstra 702ca51fd2 Restore old behaviour wrt inaccessible parents of accessible paths
E.g. `pathExists` on such parents will return `false`, and `readDir`
will fail.
2024-04-16 18:45:02 +02:00
Eelco Dolstra e2443092a4 tests/functional/restricted.sh: Remove broken test
'--restrict-eval true' was probably not what was intended
(i.e. parsing a file named 'true').
2024-04-16 18:45:02 +02:00
Eelco Dolstra 4065f16888 pathExists: Return false on "/nix/store" in pure mode
AllowListInputAccessor has the invariant that if a path is accessible,
its parent directories are also considered accessible (though reading
them only yields the allowed subdirectories). As a result
`builtins.pathExists "/nix/store"` returns true.

However this wasn't the behaviour of previous path access control,
where `builtins.pathExists "/nix/store"` returns false even if a
subdirectory of the store is accessible.

Fixes #9672.
2024-04-16 13:14:52 +02:00
14 changed files with 114 additions and 71 deletions

View file

@ -247,12 +247,14 @@ EvalState::EvalState(
, emptyBindings(0)
, rootFS(
settings.restrictEval || settings.pureEval
? ref<SourceAccessor>(AllowListSourceAccessor::create(getFSSourceAccessor(), {},
? ref<SourceAccessor>(AllowListSourceAccessor::create(getFSSourceAccessor(), {}, {},
[&settings](const CanonPath & path) -> RestrictedPathError {
auto modeInformation = settings.pureEval
? "in pure evaluation mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation);
return RestrictedPathError(
std::string("access to absolute path '%1%' is forbidden ") +
(settings.pureEval
? "in pure evaluation mode (use '--impure' to override)"
: "in restricted mode"),
path);
}))
: getFSSourceAccessor())
, corepkgsFS(make_ref<MemorySourceAccessor>())

View file

@ -1,5 +1,7 @@
#include "filtering-source-accessor.hh"
#include <unordered_set>
namespace nix {
std::optional<std::filesystem::path> FilteringSourceAccessor::getPhysicalPath(const CanonPath & path)
@ -19,10 +21,15 @@ bool FilteringSourceAccessor::pathExists(const CanonPath & path)
return isAllowed(path) && next->pathExists(prefix / path);
}
std::optional<SourceAccessor::Stat> FilteringSourceAccessor::maybeLstat(const CanonPath & path)
SourceAccessor::Stat FilteringSourceAccessor::lstat(const CanonPath & path)
{
checkAccess(path);
return next->maybeLstat(prefix / path);
return next->lstat(prefix / path);
}
std::optional<SourceAccessor::Stat> FilteringSourceAccessor::maybeLstat(const CanonPath & path)
{
return isAllowed(path) ? next->maybeLstat(prefix / path) : std::nullopt;
}
SourceAccessor::DirEntries FilteringSourceAccessor::readDirectory(const CanonPath & path)
@ -57,33 +64,46 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path)
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
{
std::set<CanonPath> allowedPrefixes;
std::unordered_set<CanonPath> allowedPrefixes;
std::unordered_set<CanonPath> allowedPaths;
AllowListSourceAccessorImpl(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
, allowedPrefixes(std::move(allowedPrefixes))
, allowedPaths(std::move(allowedPaths))
{ }
bool isAllowed(const CanonPath & path) override
{
return path.isAllowed(allowedPrefixes);
return allowedPaths.count(path) || path.isAllowed(allowedPrefixes);
}
void allowPrefix(CanonPath prefix) override
{
allowedPrefixes.insert(std::move(prefix));
}
void allowPath(CanonPath path) override
{
allowedPaths.insert(std::move(path));
}
};
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
{
return make_ref<AllowListSourceAccessorImpl>(next, std::move(allowedPrefixes), std::move(makeNotAllowedError));
return make_ref<AllowListSourceAccessorImpl>(
next,
std::move(allowedPrefixes),
std::move(allowedPaths),
std::move(makeNotAllowedError));
}
bool CachingFilteringSourceAccessor::isAllowed(const CanonPath & path)

View file

@ -36,6 +36,8 @@ struct FilteringSourceAccessor : SourceAccessor
bool pathExists(const CanonPath & path) override;
Stat lstat(const CanonPath & path) override;
std::optional<Stat> maybeLstat(const CanonPath & path) override;
DirEntries readDirectory(const CanonPath & path) override;
@ -63,13 +65,20 @@ struct FilteringSourceAccessor : SourceAccessor
struct AllowListSourceAccessor : public FilteringSourceAccessor
{
/**
* Grant access to the specified prefix.
* Grant access to the specified prefix, i.e. the path *and* its
* children.
*/
virtual void allowPrefix(CanonPath prefix) = 0;
/**
* Grant access to a path but not its children.
*/
virtual void allowPath(CanonPath path) = 0;
static ref<AllowListSourceAccessor> create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPrefixes,
std::unordered_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError);
using FilteringSourceAccessor::FilteringSourceAccessor;

View file

@ -1201,17 +1201,23 @@ ref<SourceAccessor> GitRepoImpl::getAccessor(const Hash & rev, bool exportIgnore
ref<SourceAccessor> GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError)
{
auto self = ref<GitRepoImpl>(shared_from_this());
/* In case of an empty workdir, return an empty in-memory tree. We
cannot use AllowListSourceAccessor because it would return an
error for the root (and we can't add the root to the allow-list
since that would allow access to all its children). */
ref<SourceAccessor> fileAccessor =
wd.files.empty()
? makeEmptySourceAccessor()
: AllowListSourceAccessor::create(
makeFSSourceAccessor(path),
std::set<CanonPath> { wd.files },
std::move(makeNotAllowedError)).cast<SourceAccessor>();
/* Grant access to the parent directories of every accessible
file. The root is always accessible. */
std::unordered_set<CanonPath> files{CanonPath::root};
for (auto path : wd.files) {
while (!path.isRoot()) {
if (!files.insert(path).second) break;
path.pop();
}
}
auto fileAccessor = AllowListSourceAccessor::create(
makeFSSourceAccessor(path),
{},
std::move(files),
std::move(makeNotAllowedError));
if (exportIgnore)
return make_ref<GitExportIgnoreSourceAccessor>(self, fileAccessor, std::nullopt);
else

View file

@ -65,6 +65,7 @@
#include <iostream>
#include "strings.hh"
#include "signals.hh"
namespace nix {
@ -1579,6 +1580,8 @@ void LocalDerivationGoal::startDaemon()
FdSink(remote.get()),
NotTrusted, daemon::Recursive);
debug("terminated daemon connection");
} catch (const Interrupted &) {
debug("interrupted daemon connection");
} catch (SystemError &) {
ignoreExceptionExceptInterrupt();
}

View file

@ -225,8 +225,15 @@ struct LocalDerivationGoal : public DerivationGoal
*/
void writeStructuredAttrs();
/**
* Start an in-process nix daemon thread for recursive-nix.
*/
void startDaemon();
/**
* Stop the in-process nix daemon thread.
* @see startDaemon
*/
void stopDaemon();
/**

View file

@ -90,25 +90,16 @@ CanonPath CanonPath::operator / (std::string_view c) const
return res;
}
bool CanonPath::isAllowed(const std::set<CanonPath> & allowed) const
bool CanonPath::isAllowed(const std::unordered_set<CanonPath> & allowed) const
{
/* Check if `this` is an exact match or the parent of an
allowed path. */
auto lb = allowed.lower_bound(*this);
if (lb != allowed.end()) {
if (lb->isWithin(*this))
return true;
}
/* Check if a parent of `this` is allowed. */
auto path = *this;
while (!path.isRoot()) {
path.pop();
while (true) {
if (allowed.count(path))
return true;
if (path.isRoot())
return false;
path.pop();
}
return false;
}
std::ostream & operator << (std::ostream & stream, const CanonPath & path)

View file

@ -5,7 +5,7 @@
#include <optional>
#include <cassert>
#include <iostream>
#include <set>
#include <unordered_set>
#include <vector>
namespace nix {
@ -209,12 +209,10 @@ public:
CanonPath operator / (std::string_view c) const;
/**
* Check whether access to this path is allowed, which is the case
* if 1) `this` is within any of the `allowed` paths; or 2) any of
* the `allowed` paths are within `this`. (The latter condition
* ensures access to the parents of allowed paths.)
* Check whether access to this path is allowed, i.e. `this` is
* within any of the `allowed` paths.
*/
bool isAllowed(const std::set<CanonPath> & allowed) const;
bool isAllowed(const std::unordered_set<CanonPath> & allowed) const;
/**
* Return a representation `x` of `path` relative to `this`, i.e.

View file

@ -114,7 +114,7 @@ struct SourceAccessor : std::enable_shared_from_this<SourceAccessor>
std::optional<uint64_t> narOffset;
};
Stat lstat(const CanonPath & path);
virtual Stat lstat(const CanonPath & path);
virtual std::optional<Stat> maybeLstat(const CanonPath & path) = 0;

View file

@ -110,10 +110,15 @@ void ThreadPool::doWork(bool mainThread)
propagate it. */
try {
std::rethrow_exception(exc);
} catch (const Interrupted &) {
// The interrupted state may be picked up by multiple
// workers, which is expected, so we should ignore
// it silently and let the first one bubble up,
// rethrown via the original state->exception.
} catch (const ThreadPoolShutDown &) {
// Similarly expected.
} catch (std::exception & e) {
if (!dynamic_cast<ThreadPoolShutDown*>(&e))
ignoreExceptionExceptInterrupt();
} catch (...) {
ignoreExceptionExceptInterrupt();
}
}
}

View file

@ -42,6 +42,7 @@ cat > "$flake2Dir/flake.nix" <<EOF
outputs = { self, flake1 }: rec {
packages.$system.bar = flake1.packages.$system.foo;
foo = builtins.pathExists (self + "/..");
};
}
EOF
@ -274,6 +275,9 @@ nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --commit-lock-file
[[ -e "$flake2Dir/flake.lock" ]]
[[ -z $(git -C "$flake2Dir" diff main || echo failed) ]]
# Test that pathExist on the parent of a flake returns false.
[[ $(nix eval "$flake2Dir#foo") = false ]]
# Rerunning the build should not change the lockfile.
nix build -o "$TEST_ROOT/result" "$flake2Dir#bar"
[[ -z $(git -C "$flake2Dir" diff main || echo failed) ]]

View file

@ -153,7 +153,7 @@ foo + baz
' "3" \
./flake ./flake\#bar --experimental-features 'flakes'
# Test the `:reload` mechansim with flakes:
# Test the `:reload` mechanism with flakes:
# - Eval `./flake#changingThing`
# - Modify the flake
# - Re-eval it

View file

@ -5,15 +5,15 @@ source common.sh
clearStoreIfPossible
nix-instantiate --restrict-eval --eval -E '1 + 2'
(! nix-instantiate --eval --restrict-eval ./restricted.nix)
(! nix-instantiate --eval --restrict-eval <(echo '1 + 2'))
expectStderr 1 nix-instantiate --eval --restrict-eval ./restricted.nix | grepQuiet "forbidden in restricted mode"
expectStderr 1 nix-instantiate --eval --restrict-eval <(echo '1 + 2') | grepQuiet "forbidden in restricted mode"
nix-instantiate --restrict-eval ./simple.nix -I src=.
nix-instantiate --restrict-eval ./simple.nix -I src1=simple.nix -I src2=config.nix -I src3=./simple.builder.sh
# no default NIX_PATH
(unset NIX_PATH; ! nix-instantiate --restrict-eval --find-file .)
(unset NIX_PATH; expectStderr 1 nix-instantiate --restrict-eval --find-file . | grepQuiet "file '.' was not found in the Nix search path")
(! nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix')
expectStderr 1 nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' | grepQuiet "forbidden in restricted mode"
nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=../..
expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile <foo/simple.nix>' | grepQuiet "forbidden in restricted mode"
@ -22,21 +22,21 @@ nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; p
p=$(nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)")
cmp $p restricted.sh
(! nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval)
expectStderr 1 nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval | grepQuiet "forbidden in restricted mode"
(! nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)/restricted.sh/")
expectStderr 1 nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)/restricted.sh/" | grepQuiet "forbidden in restricted mode"
nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)/restricted.sh"
(! nix eval --raw --expr "builtins.fetchurl https://github.com/NixOS/patchelf/archive/master.tar.gz" --impure --restrict-eval)
(! nix eval --raw --expr "builtins.fetchTarball https://github.com/NixOS/patchelf/archive/master.tar.gz" --impure --restrict-eval)
(! nix eval --raw --expr "fetchGit git://github.com/NixOS/patchelf.git" --impure --restrict-eval)
expectStderr 1 nix eval --raw --expr "builtins.fetchurl https://github.com/NixOS/patchelf/archive/master.tar.gz" --impure --restrict-eval | grepQuiet "forbidden in restricted mode"
expectStderr 1 nix eval --raw --expr "builtins.fetchTarball https://github.com/NixOS/patchelf/archive/master.tar.gz" --impure --restrict-eval | grepQuiet "forbidden in restricted mode"
expectStderr 1 nix eval --raw --expr "fetchGit git://github.com/NixOS/patchelf.git" --impure --restrict-eval | grepQuiet "forbidden in restricted mode"
ln -sfn $(pwd)/restricted.nix $TEST_ROOT/restricted.nix
[[ $(nix-instantiate --eval $TEST_ROOT/restricted.nix) == 3 ]]
(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix)
(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT)
(! nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I .)
expectStderr 1 nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix | grepQuiet "forbidden in restricted mode"
expectStderr 1 nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT | grepQuiet "forbidden in restricted mode"
expectStderr 1 nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I . | grepQuiet "forbidden in restricted mode"
nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT -I .
[[ $(nix eval --raw --impure --restrict-eval -I . --expr 'builtins.readFile "${import ./simple.nix}/hello"') == 'Hello World!' ]]
@ -50,8 +50,8 @@ expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { pr
expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir <foo/tunnel/foo2>" -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode"
# Reading the parents of allowed paths should show only the ancestors of the allowed paths.
[[ $(nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir <foo/tunnel>" -I $TEST_ROOT/tunnel.d) == '{ "tunnel.d" = "directory"; }' ]]
# Reading the parents of allowed paths is forbidden.
expectStderr 1 nix-instantiate --restrict-eval --eval -E "let __nixPath = [ { prefix = \"foo\"; path = $TEST_ROOT/tunnel.d; } ]; in builtins.readDir <foo/tunnel>" -I $TEST_ROOT/tunnel.d | grepQuiet "forbidden in restricted mode"
# Check whether we can leak symlink information through directory traversal.
traverseDir="$(pwd)/restricted-traverse-me"
@ -63,5 +63,3 @@ output="$(nix eval --raw --restrict-eval -I "$traverseDir" \
2>&1 || :)"
echo "$output" | grep "is forbidden"
echo "$output" | grepInverse -F restricted-secret
expectStderr 1 nix-instantiate --restrict-eval true ./dependencies.nix | grepQuiet "forbidden in restricted mode"

View file

@ -143,7 +143,7 @@ namespace nix {
}
TEST(CanonPath, allowed) {
std::set<CanonPath> allowed {
std::unordered_set<CanonPath> allowed {
CanonPath("foo/bar"),
CanonPath("foo!"),
CanonPath("xyzzy"),
@ -152,11 +152,11 @@ namespace nix {
ASSERT_TRUE (CanonPath("foo/bar").isAllowed(allowed));
ASSERT_TRUE (CanonPath("foo/bar/bla").isAllowed(allowed));
ASSERT_TRUE (CanonPath("foo").isAllowed(allowed));
ASSERT_FALSE(CanonPath("foo").isAllowed(allowed));
ASSERT_FALSE(CanonPath("bar").isAllowed(allowed));
ASSERT_FALSE(CanonPath("bar/a").isAllowed(allowed));
ASSERT_TRUE (CanonPath("a").isAllowed(allowed));
ASSERT_TRUE (CanonPath("a/b").isAllowed(allowed));
ASSERT_FALSE(CanonPath("a").isAllowed(allowed));
ASSERT_FALSE(CanonPath("a/b").isAllowed(allowed));
ASSERT_TRUE (CanonPath("a/b/c").isAllowed(allowed));
ASSERT_TRUE (CanonPath("a/b/c/d").isAllowed(allowed));
ASSERT_TRUE (CanonPath("a/b/c/d/e").isAllowed(allowed));
@ -164,7 +164,7 @@ namespace nix {
ASSERT_FALSE(CanonPath("a/b/d").isAllowed(allowed));
ASSERT_FALSE(CanonPath("aaa").isAllowed(allowed));
ASSERT_FALSE(CanonPath("zzz").isAllowed(allowed));
ASSERT_TRUE (CanonPath("/").isAllowed(allowed));
ASSERT_FALSE(CanonPath("/").isAllowed(allowed));
}
TEST(CanonPath, makeRelative) {