Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symlink review #9384

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ perl/Makefile.config
/src/libexpr/nix.tbl
/src/libexpr/tests/libnixexpr-tests

# /src/libfetchers
/src/libfetchers/tests/libnixfetchers-tests

# /src/libstore/
*.gen.*
/src/libstore/tests/libnixstore-tests
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ UNIT_TEST_ENV = _NIX_TEST_UNIT_DATA=unit-test-data
makefiles += \
src/libutil/tests/local.mk \
src/libstore/tests/local.mk \
src/libfetchers/tests/local.mk \
src/libexpr/tests/local.mk
endif

Expand Down
18 changes: 17 additions & 1 deletion src/libfetchers/input-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,27 @@ SourcePath SourcePath::parent() const
return {accessor, std::move(*p)};
}

SourcePath SourcePath::followSymlinks() const {
SourcePath path = *this;
unsigned int followCount = 0, maxFollow = 1000;

/* If `path' is a symlink, follow it. This is so that relative
path references work. */
while (true) {
// Basic cycle/depth limit to avoid infinite loops.
if (++followCount >= maxFollow)
throw Error("too many levels of symbolic links while traversing the path '%s'; assuming it leads to a cycle after following %d indirections", this->to_string(), maxFollow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw Error("too many levels of symbolic links while traversing the path '%s'; assuming it leads to a cycle after following %d indirections", this->to_string(), maxFollow);
throw Error("cycle detected following symlink '%s'", this->to_string());

After 1000 links we can safely assume there's a cycle, so no need to be very verbose.

if (path.lstat().type != InputAccessor::tSymlink) break;
path = {path.accessor, CanonPath(path.readLink(), path.path.parent().value_or(CanonPath::root))};
}
return path;
}

SourcePath SourcePath::resolveSymlinks() const
{
auto res = accessor->root();

int linksAllowed = 1024;
int linksAllowed = 1000;

std::list<std::string> todo;
for (auto & c : path)
Expand Down
13 changes: 11 additions & 2 deletions src/libfetchers/input-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,19 @@ struct SourcePath

/**
* Resolve any symlinks in this `SourcePath` (including its
* parents). The result is a `SourcePath` in which no element is a
* symlink.
* parents).
*
* @return A `SourcePath` in which no element is a symlink.
*/
SourcePath resolveSymlinks() const;

/**
* If this `SourcePath` is a symlink, resolve it, but do not resolve
* symlinks in its parent paths.
*
* @return A `SourcePath` in which the final element is not a symlink.
*/
SourcePath followSymlinks() const;
};

std::ostream & operator << (std::ostream & str, const SourcePath & path);
Expand Down
8 changes: 8 additions & 0 deletions src/libfetchers/memory-input-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ struct MemoryInputAccessorImpl : MemoryInputAccessor, MemorySourceAccessor
MemorySourceAccessor::addFile(path, std::move(contents))
};
}

SourcePath addSymlink(CanonPath path, std::string && contents) override
{
return {
ref(shared_from_this()),
MemorySourceAccessor::addSymlink(path, std::move(contents))
};
}
};

ref<MemoryInputAccessor> makeMemoryInputAccessor()
Expand Down
1 change: 1 addition & 0 deletions src/libfetchers/memory-input-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace nix {
struct MemoryInputAccessor : InputAccessor
{
virtual SourcePath addFile(CanonPath path, std::string && contents) = 0;
virtual SourcePath addSymlink(CanonPath path, std::string && contents) = 0;
};

ref<MemoryInputAccessor> makeMemoryInputAccessor();
Expand Down
29 changes: 29 additions & 0 deletions src/libfetchers/tests/input-accessor.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include "../input-accessor.hh"
#include "../memory-input-accessor.hh"
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include "terminal.hh"

namespace nix {

TEST(SourcePath, followSymlinks_cycle) {
auto fs = makeMemoryInputAccessor();
fs->addSymlink({"origin", CanonPath::root}, "a");
fs->addSymlink({"a", CanonPath::root}, "b");
fs->addSymlink({"b", CanonPath::root}, "a");

ASSERT_TRUE(fs->pathExists({"a", CanonPath::root}));
SourcePath origin (fs->root() + "origin");
try {
origin.followSymlinks();
ASSERT_TRUE(false);
} catch (const Error &e) {
auto msg = filterANSIEscapes(e.what(), true);
// EXPECT_THAT(msg, ("too many levels of symbolic links"));
EXPECT_THAT(msg, testing::HasSubstr("too many levels of symbolic links"));
EXPECT_THAT(msg, testing::HasSubstr("'/origin'"));
EXPECT_THAT(msg, testing::HasSubstr("assuming it leads to a cycle after following 1000 indirections"));
}
}

}
39 changes: 39 additions & 0 deletions src/libfetchers/tests/local.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
check: libfetchers-tests-exe_RUN

programs += libfetchers-tests-exe

libfetchers-tests-exe_NAME = libnixfetchers-tests

libfetchers-tests-exe_DIR := $(d)

ifeq ($(INSTALL_UNIT_TESTS), yes)
libfetchers-tests-exe_INSTALL_DIR := $(checkbindir)
else
libfetchers-tests-exe_INSTALL_DIR :=
endif


libfetchers-tests-exe_LIBS = libfetchers-tests libstore


libfetchers-tests-exe_LDFLAGS := $(GTEST_LIBS)

libraries += libfetchers-tests

libfetchers-tests_NAME = libnixfetchers-tests

libfetchers-tests_DIR := $(d)

ifeq ($(INSTALL_UNIT_TESTS), yes)
libfetchers-tests_INSTALL_DIR := $(checklibdir)
else
libfetchers-tests_INSTALL_DIR :=
endif

libfetchers-tests_SOURCES := $(wildcard $(d)/*.cc)

libfetchers-tests_CXXFLAGS += -I src/libfetchers -I src/libutil -I src/libstore

libfetchers-tests_LIBS = libutil-tests libstore-tests libutil libstore libfetchers

libfetchers-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS)
Ericson2314 marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 13 additions & 0 deletions src/libutil/memory-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ CanonPath MemorySourceAccessor::addFile(CanonPath path, std::string && contents)
return path;
}

CanonPath MemorySourceAccessor::addSymlink(CanonPath path, std::string &&contents)
{
auto * f = open(path, File { File::Symlink {} });
if (!f)
throw Error("file '%s' cannot be made because some parent file is not a directory", path);
if (auto * s = std::get_if<File::Symlink>(&f->raw))
s->target = std::move(contents);
else
throw Error("file '%s' is not a symbolic link", path);

return path;
}


using File = MemorySourceAccessor::File;

Expand Down
1 change: 1 addition & 0 deletions src/libutil/memory-source-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ struct MemorySourceAccessor : virtual SourceAccessor
File * open(const CanonPath & path, std::optional<File> create);

CanonPath addFile(CanonPath path, std::string && contents);
CanonPath addSymlink(CanonPath path, std::string && contents);
};

/**
Expand Down
Loading