1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-27 00:11:17 +02:00

Merge pull request #12046 from roberth/cli-symlink-fixes

CLI symlink fixes
This commit is contained in:
Jörg Thalheim 2025-01-07 07:01:59 +01:00 committed by GitHub
commit 383ab87da3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 200 additions and 11 deletions

View file

@ -21,6 +21,9 @@ This operation has the following options:
Use recursive instead of flat hashing mode, used when adding Use recursive instead of flat hashing mode, used when adding
directories to the store. directories to the store.
*paths* that refer to symlinks are not dereferenced, but added to the store
as symlinks with the same target.
{{#include ./opt-common.md}} {{#include ./opt-common.md}}
{{#include ../opt-common.md}} {{#include ../opt-common.md}}

View file

@ -11,6 +11,9 @@
The operation `--add` adds the specified paths to the Nix store. It The operation `--add` adds the specified paths to the Nix store. It
prints the resulting paths in the Nix store on standard output. prints the resulting paths in the Nix store on standard output.
*paths* that refer to symlinks are not dereferenced, but added to the store
as symlinks with the same target.
{{#include ./opt-common.md}} {{#include ./opt-common.md}}
{{#include ../opt-common.md}} {{#include ../opt-common.md}}

View file

@ -261,4 +261,18 @@ TEST(pathExists, bogusPathDoesNotExist)
{ {
ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes")); ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes"));
} }
/* ----------------------------------------------------------------------------
* makeParentCanonical
* --------------------------------------------------------------------------*/
TEST(makeParentCanonical, noParent)
{
ASSERT_EQ(makeParentCanonical("file"), absPath(std::filesystem::path("file")));
}
TEST(makeParentCanonical, root)
{
ASSERT_EQ(makeParentCanonical("/"), "/");
}
} }

View file

@ -766,4 +766,19 @@ bool isExecutableFileAmbient(const fs::path & exe) {
) == 0; ) == 0;
} }
std::filesystem::path makeParentCanonical(const std::filesystem::path & rawPath)
{
std::filesystem::path path(absPath(rawPath));;
try {
auto parent = path.parent_path();
if (parent == path) {
// `path` is a root directory => trivially canonical
return parent;
}
return std::filesystem::canonical(parent) / path.filename();
} catch (fs::filesystem_error & e) {
throw SysError("canonicalising parent path of '%1%'", path);
}
} }
} // namespace nix

View file

@ -143,6 +143,23 @@ inline bool symlink_exists(const std::filesystem::path & path) {
} // namespace fs } // namespace fs
/**
* Canonicalize a path except for the last component.
*
* This is useful for getting the canonical location of a symlink.
*
* Consider the case where `foo/l` is a symlink. `canonical("foo/l")` will
* resolve the symlink `l` to its target.
* `makeParentCanonical("foo/l")` will not resolve the symlink `l` to its target,
* but does ensure that the returned parent part of the path, `foo` is resolved
* to `canonical("foo")`, and can therefore be retrieved without traversing any
* symlinks.
*
* If a relative path is passed, it will be made absolute, so that the parent
* can always be canonicalized.
*/
std::filesystem::path makeParentCanonical(const std::filesystem::path & path);
/** /**
* A version of pathExists that returns false on a permission error. * A version of pathExists that returns false on a permission error.
* Useful for inferring default paths across directories that might not * Useful for inferring default paths across directories that might not

View file

@ -43,13 +43,25 @@ struct PosixSourceAccessor : virtual SourceAccessor
std::optional<std::filesystem::path> getPhysicalPath(const CanonPath & path) override; std::optional<std::filesystem::path> getPhysicalPath(const CanonPath & path) override;
/** /**
* Create a `PosixSourceAccessor` and `CanonPath` corresponding to * Create a `PosixSourceAccessor` and `SourcePath` corresponding to
* some native path. * some native path.
* *
* The `PosixSourceAccessor` is rooted as far up the tree as * The `PosixSourceAccessor` is rooted as far up the tree as
* possible, (e.g. on Windows it could scoped to a drive like * possible, (e.g. on Windows it could scoped to a drive like
* `C:\`). This allows more `..` parent accessing to work. * `C:\`). This allows more `..` parent accessing to work.
* *
* @note When `path` is trusted user input, canonicalize it using
* `std::filesystem::canonical`, `makeParentCanonical`, `std::filesystem::weakly_canonical`, etc,
* as appropriate for the use case. At least weak canonicalization is
* required for the `SourcePath` to do anything useful at the location it
* points to.
*
* @note A canonicalizing behavior is not built in `createAtRoot` so that
* callers do not accidentally introduce symlink-related security vulnerabilities.
* Furthermore, `createAtRoot` does not know whether the file pointed to by
* `path` should be resolved if it is itself a symlink. In other words,
* `createAtRoot` can not decide between aforementioned `canonical`, `makeParentCanonical`, etc. for its callers.
*
* See * See
* [`std::filesystem::path::root_path`](https://en.cppreference.com/w/cpp/filesystem/path/root_path) * [`std::filesystem::path::root_path`](https://en.cppreference.com/w/cpp/filesystem/path/root_path)
* and * and

View file

@ -183,9 +183,9 @@ static void opAdd(Strings opFlags, Strings opArgs)
if (!opFlags.empty()) throw UsageError("unknown flag"); if (!opFlags.empty()) throw UsageError("unknown flag");
for (auto & i : opArgs) { for (auto & i : opArgs) {
auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i); auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i));
cout << fmt("%s\n", store->printStorePath(store->addToStore( cout << fmt("%s\n", store->printStorePath(store->addToStore(
std::string(baseNameOf(i)), {accessor, canonPath}))); std::string(baseNameOf(i)), sourcePath)));
} }
} }
@ -207,10 +207,10 @@ static void opAddFixed(Strings opFlags, Strings opArgs)
opArgs.pop_front(); opArgs.pop_front();
for (auto & i : opArgs) { for (auto & i : opArgs) {
auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i); auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i));
std::cout << fmt("%s\n", store->printStorePath(store->addToStoreSlow( std::cout << fmt("%s\n", store->printStorePath(store->addToStoreSlow(
baseNameOf(i), baseNameOf(i),
{accessor, canonPath}, sourcePath,
method, method,
hashAlgo).path)); hashAlgo).path));
} }

View file

@ -37,13 +37,13 @@ struct CmdAddToStore : MixDryRun, StoreCommand
{ {
if (!namePart) namePart = baseNameOf(path); if (!namePart) namePart = baseNameOf(path);
auto [accessor, path2] = PosixSourceAccessor::createAtRoot(path); auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(path));
auto storePath = dryRun auto storePath = dryRun
? store->computeStorePath( ? store->computeStorePath(
*namePart, {accessor, path2}, caMethod, hashAlgo, {}).first *namePart, sourcePath, caMethod, hashAlgo, {}).first
: store->addToStoreSlow( : store->addToStoreSlow(
*namePart, {accessor, path2}, caMethod, hashAlgo, {}).path; *namePart, sourcePath, caMethod, hashAlgo, {}).path;
logger->cout("%s", store->printStorePath(storePath)); logger->cout("%s", store->printStorePath(storePath));
} }

View file

@ -87,18 +87,35 @@ struct CmdHashBase : Command
return std::make_unique<HashSink>(hashAlgo); return std::make_unique<HashSink>(hashAlgo);
}; };
auto path2 = PosixSourceAccessor::createAtRoot(path); auto makeSourcePath = [&]() -> SourcePath {
return PosixSourceAccessor::createAtRoot(makeParentCanonical(path));
};
Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++ Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++
switch (mode) { switch (mode) {
case FileIngestionMethod::Flat: case FileIngestionMethod::Flat:
{
// While usually we could use the some code as for NixArchive,
// the Flat method needs to support FIFOs, such as those
// produced by bash process substitution, e.g.:
// nix hash --mode flat <(echo hi)
// Also symlinks semantics are unambiguous in the flat case,
// so we don't need to go low-level, or reject symlink `path`s.
auto hashSink = makeSink();
readFile(path, *hashSink);
h = hashSink->finish().first;
break;
}
case FileIngestionMethod::NixArchive: case FileIngestionMethod::NixArchive:
{ {
auto sourcePath = makeSourcePath();
auto hashSink = makeSink(); auto hashSink = makeSink();
dumpPath(path2, *hashSink, (FileSerialisationMethod) mode); dumpPath(sourcePath, *hashSink, (FileSerialisationMethod) mode);
h = hashSink->finish().first; h = hashSink->finish().first;
break; break;
} }
case FileIngestionMethod::Git: { case FileIngestionMethod::Git: {
auto sourcePath = makeSourcePath();
std::function<git::DumpHook> hook; std::function<git::DumpHook> hook;
hook = [&](const SourcePath & path) -> git::TreeEntry { hook = [&](const SourcePath & path) -> git::TreeEntry {
auto hashSink = makeSink(); auto hashSink = makeSink();
@ -109,7 +126,7 @@ struct CmdHashBase : Command
.hash = hash, .hash = hash,
}; };
}; };
h = hook(path2).hash; h = hook(sourcePath).hash;
break; break;
} }
} }

View file

@ -29,6 +29,47 @@ echo "$hash2"
test "$hash1" = "sha256:$hash2" test "$hash1" = "sha256:$hash2"
# The contents can be accessed through a symlink, and this symlink has no effect on the hash
# https://github.com/NixOS/nix/issues/11941
test_issue_11941() {
local expected actual
mkdir -p "$TEST_ROOT/foo/bar" && ln -s "$TEST_ROOT/foo" "$TEST_ROOT/foo-link"
# legacy
expected=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo/bar")
actual=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo-link/bar")
[[ "$expected" == "$actual" ]]
actual=$(nix-store --add "$TEST_ROOT/foo-link/bar")
[[ "$expected" == "$actual" ]]
# nix store add
actual=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/foo/bar")
[[ "$expected" == "$actual" ]]
# cleanup
rm -r "$TEST_ROOT/foo" "$TEST_ROOT/foo-link"
}
test_issue_11941
# A symlink is added to the store as a symlink, not as a copy of the target
test_add_symlink() {
ln -s /bin "$TEST_ROOT/my-bin"
# legacy
path=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/my-bin")
[[ "$(readlink "$path")" == /bin ]]
path=$(nix-store --add "$TEST_ROOT/my-bin")
[[ "$(readlink "$path")" == /bin ]]
# nix store add
path=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/my-bin")
[[ "$(readlink "$path")" == /bin ]]
# cleanup
rm "$TEST_ROOT/my-bin"
}
test_add_symlink
#### New style commands #### New style commands
clearStoreIfPossible clearStoreIfPossible

View file

@ -92,3 +92,32 @@ try2 md5 "20f3ffe011d4cfa7d72bfabef7882836"
rm "$TEST_ROOT/hash-path/hello" rm "$TEST_ROOT/hash-path/hello"
ln -s x "$TEST_ROOT/hash-path/hello" ln -s x "$TEST_ROOT/hash-path/hello"
try2 md5 "f78b733a68f5edbdf9413899339eaa4a" try2 md5 "f78b733a68f5edbdf9413899339eaa4a"
# Flat mode supports process substitution
h=$(nix hash path --mode flat --type sha256 --base32 <(printf "SMASH THE STATE"))
[[ 0d9n3r2i4m1zgy0wpqbsyabsfzgs952066bfp8gwvcg4mkr4r5g8 == "$h" ]]
# Flat mode supports process substitution (hash file)
h=$(nix hash file --type sha256 --base32 <(printf "SMASH THE STATE"))
[[ 0d9n3r2i4m1zgy0wpqbsyabsfzgs952066bfp8gwvcg4mkr4r5g8 == "$h" ]]
# Symlinks in the ancestry are ok and don't affect the result
mkdir -p "$TEST_ROOT/simple" "$TEST_ROOT/try/to/mess/with/it"
echo hi > "$TEST_ROOT/simple/hi"
ln -s "$TEST_ROOT/simple" "$TEST_ROOT/try/to/mess/with/it/simple-link"
h=$(nix hash path --type sha256 --base32 "$TEST_ROOT/simple/hi")
[[ 1xmr8jicvzszfzpz46g37mlpvbzjl2wpwvl2b05psipssyp1sm8h == "$h" ]]
h=$(nix hash path --type sha256 --base32 "$TEST_ROOT/try/to/mess/with/it/simple-link/hi")
[[ 1xmr8jicvzszfzpz46g37mlpvbzjl2wpwvl2b05psipssyp1sm8h == "$h" ]]
# nix hash --mode nar does not canonicalize a symlink argument.
# Otherwise it can't generate a NAR whose root is a symlink.
# If you want to follow the symlink, pass $(realpath -s ...) instead.
ln -s /non-existent-48cujwe8ndf4as0bne "$TEST_ROOT/symlink-to-nowhere"
h=$(nix hash path --mode nar --type sha256 --base32 "$TEST_ROOT/symlink-to-nowhere")
[[ 1bl5ry3x1fcbwgr5c2x50bn572iixh4j1p6ax5isxly2ddgn8pbp == "$h" ]] # manually verified hash
if [[ -e /bin ]]; then
ln -s /bin "$TEST_ROOT/symlink-to-bin"
h=$(nix hash path --mode nar --type sha256 --base32 "$TEST_ROOT/symlink-to-bin")
[[ 0z2mdmkd43l0ijdxfbj1y8vzli15yh9b09n3a3rrygmjshbyypsw == "$h" ]] # manually verified hash
fi

View file

@ -159,6 +159,8 @@ in
functional_root = runNixOSTestFor "x86_64-linux" ./functional/as-root.nix; functional_root = runNixOSTestFor "x86_64-linux" ./functional/as-root.nix;
functional_symlinked-home = runNixOSTestFor "x86_64-linux" ./functional/symlinked-home.nix;
user-sandboxing = runNixOSTestFor "x86_64-linux" ./user-sandboxing; user-sandboxing = runNixOSTestFor "x86_64-linux" ./user-sandboxing;
s3-binary-cache-store = runNixOSTestFor "x86_64-linux" ./s3-binary-cache-store.nix; s3-binary-cache-store = runNixOSTestFor "x86_64-linux" ./s3-binary-cache-store.nix;

View file

@ -0,0 +1,36 @@
/**
This test runs the functional tests on a NixOS system where the home directory
is symlinked to another location.
The purpose of this test is to find cases where Nix uses low-level operations
that don't support symlinks on paths that include them.
It is not a substitute for more intricate, use case-specific tests, but helps
catch common issues.
*/
# TODO: add symlinked tmpdir
{ ... }:
{
name = "functional-tests-on-nixos_user_symlinked-home";
imports = [ ./common.nix ];
nodes.machine = {
users.users.alice = { isNormalUser = true; };
};
testScript = ''
machine.wait_for_unit("multi-user.target")
with subtest("prepare symlinked home"):
machine.succeed("""
(
set -x
mv /home/alice /home/alice.real
ln -s alice.real /home/alice
) 1>&2
""")
machine.succeed("""
su --login --command "run-test-suite" alice >&2
""")
'';
}