Skip to content

Commit 383ab87

Browse files
authored
Merge pull request #12046 from roberth/cli-symlink-fixes
CLI symlink fixes
2 parents 6827768 + 4c74d67 commit 383ab87

File tree

13 files changed

+200
-11
lines changed

13 files changed

+200
-11
lines changed

doc/manual/source/command-ref/nix-store/add-fixed.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ This operation has the following options:
2121
Use recursive instead of flat hashing mode, used when adding
2222
directories to the store.
2323

24+
*paths* that refer to symlinks are not dereferenced, but added to the store
25+
as symlinks with the same target.
26+
2427
{{#include ./opt-common.md}}
2528

2629
{{#include ../opt-common.md}}

doc/manual/source/command-ref/nix-store/add.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
The operation `--add` adds the specified paths to the Nix store. It
1212
prints the resulting paths in the Nix store on standard output.
1313

14+
*paths* that refer to symlinks are not dereferenced, but added to the store
15+
as symlinks with the same target.
16+
1417
{{#include ./opt-common.md}}
1518

1619
{{#include ../opt-common.md}}

src/libutil-tests/file-system.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,18 @@ TEST(pathExists, bogusPathDoesNotExist)
261261
{
262262
ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes"));
263263
}
264+
265+
/* ----------------------------------------------------------------------------
266+
* makeParentCanonical
267+
* --------------------------------------------------------------------------*/
268+
269+
TEST(makeParentCanonical, noParent)
270+
{
271+
ASSERT_EQ(makeParentCanonical("file"), absPath(std::filesystem::path("file")));
272+
}
273+
274+
TEST(makeParentCanonical, root)
275+
{
276+
ASSERT_EQ(makeParentCanonical("/"), "/");
277+
}
264278
}

src/libutil/file-system.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,4 +766,19 @@ bool isExecutableFileAmbient(const fs::path & exe) {
766766
) == 0;
767767
}
768768

769+
std::filesystem::path makeParentCanonical(const std::filesystem::path & rawPath)
770+
{
771+
std::filesystem::path path(absPath(rawPath));;
772+
try {
773+
auto parent = path.parent_path();
774+
if (parent == path) {
775+
// `path` is a root directory => trivially canonical
776+
return parent;
777+
}
778+
return std::filesystem::canonical(parent) / path.filename();
779+
} catch (fs::filesystem_error & e) {
780+
throw SysError("canonicalising parent path of '%1%'", path);
781+
}
769782
}
783+
784+
} // namespace nix

src/libutil/file-system.hh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,23 @@ inline bool symlink_exists(const std::filesystem::path & path) {
143143

144144
} // namespace fs
145145

146+
/**
147+
* Canonicalize a path except for the last component.
148+
*
149+
* This is useful for getting the canonical location of a symlink.
150+
*
151+
* Consider the case where `foo/l` is a symlink. `canonical("foo/l")` will
152+
* resolve the symlink `l` to its target.
153+
* `makeParentCanonical("foo/l")` will not resolve the symlink `l` to its target,
154+
* but does ensure that the returned parent part of the path, `foo` is resolved
155+
* to `canonical("foo")`, and can therefore be retrieved without traversing any
156+
* symlinks.
157+
*
158+
* If a relative path is passed, it will be made absolute, so that the parent
159+
* can always be canonicalized.
160+
*/
161+
std::filesystem::path makeParentCanonical(const std::filesystem::path & path);
162+
146163
/**
147164
* A version of pathExists that returns false on a permission error.
148165
* Useful for inferring default paths across directories that might not

src/libutil/posix-source-accessor.hh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,25 @@ struct PosixSourceAccessor : virtual SourceAccessor
4343
std::optional<std::filesystem::path> getPhysicalPath(const CanonPath & path) override;
4444

4545
/**
46-
* Create a `PosixSourceAccessor` and `CanonPath` corresponding to
46+
* Create a `PosixSourceAccessor` and `SourcePath` corresponding to
4747
* some native path.
4848
*
4949
* The `PosixSourceAccessor` is rooted as far up the tree as
5050
* possible, (e.g. on Windows it could scoped to a drive like
5151
* `C:\`). This allows more `..` parent accessing to work.
5252
*
53+
* @note When `path` is trusted user input, canonicalize it using
54+
* `std::filesystem::canonical`, `makeParentCanonical`, `std::filesystem::weakly_canonical`, etc,
55+
* as appropriate for the use case. At least weak canonicalization is
56+
* required for the `SourcePath` to do anything useful at the location it
57+
* points to.
58+
*
59+
* @note A canonicalizing behavior is not built in `createAtRoot` so that
60+
* callers do not accidentally introduce symlink-related security vulnerabilities.
61+
* Furthermore, `createAtRoot` does not know whether the file pointed to by
62+
* `path` should be resolved if it is itself a symlink. In other words,
63+
* `createAtRoot` can not decide between aforementioned `canonical`, `makeParentCanonical`, etc. for its callers.
64+
*
5365
* See
5466
* [`std::filesystem::path::root_path`](https://en.cppreference.com/w/cpp/filesystem/path/root_path)
5567
* and

src/nix-store/nix-store.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ static void opAdd(Strings opFlags, Strings opArgs)
183183
if (!opFlags.empty()) throw UsageError("unknown flag");
184184

185185
for (auto & i : opArgs) {
186-
auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i);
186+
auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i));
187187
cout << fmt("%s\n", store->printStorePath(store->addToStore(
188-
std::string(baseNameOf(i)), {accessor, canonPath})));
188+
std::string(baseNameOf(i)), sourcePath)));
189189
}
190190
}
191191

@@ -207,10 +207,10 @@ static void opAddFixed(Strings opFlags, Strings opArgs)
207207
opArgs.pop_front();
208208

209209
for (auto & i : opArgs) {
210-
auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i);
210+
auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i));
211211
std::cout << fmt("%s\n", store->printStorePath(store->addToStoreSlow(
212212
baseNameOf(i),
213-
{accessor, canonPath},
213+
sourcePath,
214214
method,
215215
hashAlgo).path));
216216
}

src/nix/add-to-store.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ struct CmdAddToStore : MixDryRun, StoreCommand
3737
{
3838
if (!namePart) namePart = baseNameOf(path);
3939

40-
auto [accessor, path2] = PosixSourceAccessor::createAtRoot(path);
40+
auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(path));
4141

4242
auto storePath = dryRun
4343
? store->computeStorePath(
44-
*namePart, {accessor, path2}, caMethod, hashAlgo, {}).first
44+
*namePart, sourcePath, caMethod, hashAlgo, {}).first
4545
: store->addToStoreSlow(
46-
*namePart, {accessor, path2}, caMethod, hashAlgo, {}).path;
46+
*namePart, sourcePath, caMethod, hashAlgo, {}).path;
4747

4848
logger->cout("%s", store->printStorePath(storePath));
4949
}

src/nix/hash.cc

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,35 @@ struct CmdHashBase : Command
8787
return std::make_unique<HashSink>(hashAlgo);
8888
};
8989

90-
auto path2 = PosixSourceAccessor::createAtRoot(path);
90+
auto makeSourcePath = [&]() -> SourcePath {
91+
return PosixSourceAccessor::createAtRoot(makeParentCanonical(path));
92+
};
93+
9194
Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++
9295
switch (mode) {
9396
case FileIngestionMethod::Flat:
97+
{
98+
// While usually we could use the some code as for NixArchive,
99+
// the Flat method needs to support FIFOs, such as those
100+
// produced by bash process substitution, e.g.:
101+
// nix hash --mode flat <(echo hi)
102+
// Also symlinks semantics are unambiguous in the flat case,
103+
// so we don't need to go low-level, or reject symlink `path`s.
104+
auto hashSink = makeSink();
105+
readFile(path, *hashSink);
106+
h = hashSink->finish().first;
107+
break;
108+
}
94109
case FileIngestionMethod::NixArchive:
95110
{
111+
auto sourcePath = makeSourcePath();
96112
auto hashSink = makeSink();
97-
dumpPath(path2, *hashSink, (FileSerialisationMethod) mode);
113+
dumpPath(sourcePath, *hashSink, (FileSerialisationMethod) mode);
98114
h = hashSink->finish().first;
99115
break;
100116
}
101117
case FileIngestionMethod::Git: {
118+
auto sourcePath = makeSourcePath();
102119
std::function<git::DumpHook> hook;
103120
hook = [&](const SourcePath & path) -> git::TreeEntry {
104121
auto hashSink = makeSink();
@@ -109,7 +126,7 @@ struct CmdHashBase : Command
109126
.hash = hash,
110127
};
111128
};
112-
h = hook(path2).hash;
129+
h = hook(sourcePath).hash;
113130
break;
114131
}
115132
}

tests/functional/add.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,47 @@ echo "$hash2"
2929

3030
test "$hash1" = "sha256:$hash2"
3131

32+
# The contents can be accessed through a symlink, and this symlink has no effect on the hash
33+
# https://github.com/NixOS/nix/issues/11941
34+
test_issue_11941() {
35+
local expected actual
36+
mkdir -p "$TEST_ROOT/foo/bar" && ln -s "$TEST_ROOT/foo" "$TEST_ROOT/foo-link"
37+
38+
# legacy
39+
expected=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo/bar")
40+
actual=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo-link/bar")
41+
[[ "$expected" == "$actual" ]]
42+
actual=$(nix-store --add "$TEST_ROOT/foo-link/bar")
43+
[[ "$expected" == "$actual" ]]
44+
45+
# nix store add
46+
actual=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/foo/bar")
47+
[[ "$expected" == "$actual" ]]
48+
49+
# cleanup
50+
rm -r "$TEST_ROOT/foo" "$TEST_ROOT/foo-link"
51+
}
52+
test_issue_11941
53+
54+
# A symlink is added to the store as a symlink, not as a copy of the target
55+
test_add_symlink() {
56+
ln -s /bin "$TEST_ROOT/my-bin"
57+
58+
# legacy
59+
path=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/my-bin")
60+
[[ "$(readlink "$path")" == /bin ]]
61+
path=$(nix-store --add "$TEST_ROOT/my-bin")
62+
[[ "$(readlink "$path")" == /bin ]]
63+
64+
# nix store add
65+
path=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/my-bin")
66+
[[ "$(readlink "$path")" == /bin ]]
67+
68+
# cleanup
69+
rm "$TEST_ROOT/my-bin"
70+
}
71+
test_add_symlink
72+
3273
#### New style commands
3374

3475
clearStoreIfPossible

0 commit comments

Comments
 (0)