Merge changes Ia1481da4,Ifca1d74d into main

* changes:
  archive: refactor bad mutable-state API in the NAR parse listener
  archive: rename ParseSink to NARParseVisitor
This commit is contained in:
jade 2024-09-14 19:26:08 +00:00 committed by Gerrit Code Review
commit 8f88590d13
6 changed files with 199 additions and 114 deletions

View file

@ -1216,7 +1216,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
bool narRead = false; bool narRead = false;
Finally cleanup = [&]() { Finally cleanup = [&]() {
if (!narRead) { if (!narRead) {
ParseSink sink; NARParseVisitor sink;
try { try {
parseDump(sink, source); parseDump(sink, source);
} catch (...) { } catch (...) {

View file

@ -2,6 +2,7 @@
#include "archive.hh" #include "archive.hh"
#include <map> #include <map>
#include <memory>
#include <stack> #include <stack>
#include <algorithm> #include <algorithm>
@ -33,7 +34,7 @@ struct NarAccessor : public FSAccessor
NarMember root; NarMember root;
struct NarIndexer : ParseSink, Source struct NarIndexer : NARParseVisitor, Source
{ {
NarAccessor & acc; NarAccessor & acc;
Source & source; Source & source;
@ -44,11 +45,12 @@ struct NarAccessor : public FSAccessor
uint64_t pos = 0; uint64_t pos = 0;
public:
NarIndexer(NarAccessor & acc, Source & source) NarIndexer(NarAccessor & acc, Source & source)
: acc(acc), source(source) : acc(acc), source(source)
{ } { }
void createMember(const Path & path, NarMember member) NarMember & createMember(const Path & path, NarMember member)
{ {
size_t level = std::count(path.begin(), path.end(), '/'); size_t level = std::count(path.begin(), path.end(), '/');
while (parents.size() > level) parents.pop(); while (parents.size() > level) parents.pop();
@ -62,6 +64,8 @@ struct NarAccessor : public FSAccessor
auto result = parents.top()->children.emplace(baseNameOf(path), std::move(member)); auto result = parents.top()->children.emplace(baseNameOf(path), std::move(member));
parents.push(&result.first->second); parents.push(&result.first->second);
} }
return *parents.top();
} }
void createDirectory(const Path & path) override void createDirectory(const Path & path) override
@ -69,28 +73,17 @@ struct NarAccessor : public FSAccessor
createMember(path, {FSAccessor::Type::tDirectory, false, 0, 0}); createMember(path, {FSAccessor::Type::tDirectory, false, 0, 0});
} }
void createRegularFile(const Path & path) override std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{ {
createMember(path, {FSAccessor::Type::tRegular, false, 0, 0}); auto & memb = createMember(path, {FSAccessor::Type::tRegular, false, 0, 0});
}
void closeRegularFile() override
{ }
void isExecutable() override
{
parents.top()->isExecutable = true;
}
void preallocateContents(uint64_t size) override
{
assert(size <= std::numeric_limits<uint64_t>::max()); assert(size <= std::numeric_limits<uint64_t>::max());
parents.top()->size = (uint64_t) size; memb.size = (uint64_t) size;
parents.top()->start = pos; memb.start = pos;
} memb.isExecutable = executable;
void receiveContents(std::string_view data) override return std::make_unique<FileHandle>();
{ } }
void createSymlink(const Path & path, const std::string & target) override void createSymlink(const Path & path, const std::string & target) override
{ {

View file

@ -379,6 +379,48 @@ void Store::addMultipleToStore(
} }
} }
namespace {
/**
* If the NAR archive contains a single file at top-level, then save
* the contents of the file to `s`. Otherwise assert.
*/
struct RetrieveRegularNARVisitor : NARParseVisitor
{
struct MyFileHandle : public FileHandle
{
Sink & sink;
void receiveContents(std::string_view data) override
{
sink(data);
}
private:
MyFileHandle(Sink & sink) : sink(sink) {}
friend struct RetrieveRegularNARVisitor;
};
Sink & sink;
RetrieveRegularNARVisitor(Sink & sink) : sink(sink) { }
std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{
return std::unique_ptr<MyFileHandle>(new MyFileHandle{sink});
}
void createDirectory(const Path & path) override
{
assert(false && "RetrieveRegularNARVisitor::createDirectory must not be called");
}
void createSymlink(const Path & path, const std::string & target) override
{
assert(false && "RetrieveRegularNARVisitor::createSymlink must not be called");
}
};
}
/* /*
The aim of this function is to compute in one pass the correct ValidPathInfo for The aim of this function is to compute in one pass the correct ValidPathInfo for
@ -413,7 +455,7 @@ ValidPathInfo Store::addToStoreSlow(std::string_view name, const Path & srcPath,
/* Note that fileSink and unusualHashTee must be mutually exclusive, since /* Note that fileSink and unusualHashTee must be mutually exclusive, since
they both write to caHashSink. Note that that requisite is currently true they both write to caHashSink. Note that that requisite is currently true
because the former is only used in the flat case. */ because the former is only used in the flat case. */
RetrieveRegularNARSink fileSink { caHashSink }; RetrieveRegularNARVisitor fileSink { caHashSink };
TeeSink unusualHashTee { narHashSink, caHashSink }; TeeSink unusualHashTee { narHashSink, caHashSink };
auto & narSink = method == FileIngestionMethod::Recursive && hashAlgo != HashType::SHA256 auto & narSink = method == FileIngestionMethod::Recursive && hashAlgo != HashType::SHA256
@ -429,7 +471,7 @@ ValidPathInfo Store::addToStoreSlow(std::string_view name, const Path & srcPath,
information to narSink. */ information to narSink. */
TeeSource tapped { fileSource, narSink }; TeeSource tapped { fileSource, narSink };
ParseSink blank; NARParseVisitor blank;
auto & parseSink = method == FileIngestionMethod::Flat auto & parseSink = method == FileIngestionMethod::Flat
? fileSink ? fileSink
: blank; : blank;

View file

@ -334,7 +334,7 @@ Generator<Entry> parse(Source & source)
} }
static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar) static WireFormatGenerator restore(NARParseVisitor & sink, Generator<nar::Entry> nar)
{ {
while (auto entry = nar.next()) { while (auto entry = nar.next()) {
co_yield std::visit( co_yield std::visit(
@ -347,16 +347,13 @@ static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar)
}, },
[&](nar::File f) { [&](nar::File f) {
return [](auto f, auto & sink) -> WireFormatGenerator { return [](auto f, auto & sink) -> WireFormatGenerator {
sink.createRegularFile(f.path); auto handle = sink.createRegularFile(f.path, f.size, f.executable);
sink.preallocateContents(f.size);
if (f.executable) {
sink.isExecutable();
}
while (auto block = f.contents.next()) { while (auto block = f.contents.next()) {
sink.receiveContents(std::string_view{block->data(), block->size()}); handle->receiveContents(std::string_view{block->data(), block->size()});
co_yield *block; co_yield *block;
} }
sink.closeRegularFile(); handle->close();
}(std::move(f), sink); }(std::move(f), sink);
}, },
[&](nar::Symlink sl) { [&](nar::Symlink sl) {
@ -377,12 +374,12 @@ static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar)
} }
} }
WireFormatGenerator parseAndCopyDump(ParseSink & sink, Source & source) WireFormatGenerator parseAndCopyDump(NARParseVisitor & sink, Source & source)
{ {
return restore(sink, nar::parse(source)); return restore(sink, nar::parse(source));
} }
void parseDump(ParseSink & sink, Source & source) void parseDump(NARParseVisitor & sink, Source & source)
{ {
auto parser = parseAndCopyDump(sink, source); auto parser = parseAndCopyDump(sink, source);
while (parser.next()) { while (parser.next()) {
@ -390,11 +387,99 @@ void parseDump(ParseSink & sink, Source & source)
} }
} }
struct RestoreSink : ParseSink /*
* Note [NAR restoration security]:
* It's *critical* that NAR restoration will never overwrite anything even if
* duplicate filenames are passed in. It is inevitable that not all NARs are
* fit to actually successfully restore to the target filesystem; errors may
* occur due to collisions, and this *must* cause the NAR to be rejected.
*
* Although the filenames are blocked from being *the same bytes* by a higher
* layer, filesystems have other ideas on every platform:
* - The store may be on a case-insensitive filesystem like APFS, ext4 with
* casefold directories, zfs with casesensitivity=insensitive
* - The store may be on a Unicode normalizing (or normalization-insensitive)
* filesystem like APFS (where files are looked up by
* hash(normalize(fname))), HFS+ (where file names are always normalized to
* approximately NFD), or zfs with normalization=formC, etc.
*
* It is impossible to know the version of Unicode being used by the underlying
* filesystem, thus it is *impossible* to stop these collisions.
*
* Overwriting files as a result of invalid NARs will cause a security bug like
* CppNix's CVE-2024-45593 (GHSA-h4vv-h3jq-v493)
*/
/**
* This code restores NARs from disk.
*
* See Note [NAR restoration security] for security invariants in this procedure.
*
*/
struct NARRestoreVisitor : NARParseVisitor
{ {
Path dstPath; Path dstPath;
AutoCloseFD fd;
private:
class MyFileHandle : public FileHandle
{
AutoCloseFD fd;
MyFileHandle(AutoCloseFD && fd, uint64_t size, bool executable) : FileHandle(), fd(std::move(fd))
{
if (executable) {
makeExecutable();
}
maybePreallocateContents(size);
}
void makeExecutable()
{
struct stat st;
if (fstat(fd.get(), &st) == -1)
throw SysError("fstat");
if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1)
throw SysError("fchmod");
}
void maybePreallocateContents(uint64_t len)
{
if (!archiveSettings.preallocateContents)
return;
#if HAVE_POSIX_FALLOCATE
if (len) {
errno = posix_fallocate(fd.get(), 0, len);
/* Note that EINVAL may indicate that the underlying
filesystem doesn't support preallocation (e.g. on
OpenSolaris). Since preallocation is just an
optimisation, ignore it. */
if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS)
throw SysError("preallocating file of %1% bytes", len);
}
#endif
}
public:
~MyFileHandle() = default;
virtual void close() override
{
/* Call close explicitly to make sure the error is checked */
fd.close();
}
void receiveContents(std::string_view data) override
{
writeFull(fd.get(), data);
}
friend struct NARRestoreVisitor;
};
public:
void createDirectory(const Path & path) override void createDirectory(const Path & path) override
{ {
Path p = dstPath + path; Path p = dstPath + path;
@ -402,49 +487,13 @@ struct RestoreSink : ParseSink
throw SysError("creating directory '%1%'", p); throw SysError("creating directory '%1%'", p);
}; };
void createRegularFile(const Path & path) override std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{ {
Path p = dstPath + path; Path p = dstPath + path;
fd = AutoCloseFD{open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)}; AutoCloseFD fd = AutoCloseFD{open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)};
if (!fd) throw SysError("creating file '%1%'", p); if (!fd) throw SysError("creating file '%1%'", p);
}
void closeRegularFile() override return std::unique_ptr<MyFileHandle>(new MyFileHandle(std::move(fd), size, executable));
{
/* Call close explicitly to make sure the error is checked */
fd.close();
}
void isExecutable() override
{
struct stat st;
if (fstat(fd.get(), &st) == -1)
throw SysError("fstat");
if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1)
throw SysError("fchmod");
}
void preallocateContents(uint64_t len) override
{
if (!archiveSettings.preallocateContents)
return;
#if HAVE_POSIX_FALLOCATE
if (len) {
errno = posix_fallocate(fd.get(), 0, len);
/* Note that EINVAL may indicate that the underlying
filesystem doesn't support preallocation (e.g. on
OpenSolaris). Since preallocation is just an
optimisation, ignore it. */
if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS)
throw SysError("preallocating file of %1% bytes", len);
}
#endif
}
void receiveContents(std::string_view data) override
{
writeFull(fd.get(), data);
} }
void createSymlink(const Path & path, const std::string & target) override void createSymlink(const Path & path, const std::string & target) override
@ -457,7 +506,7 @@ struct RestoreSink : ParseSink
void restorePath(const Path & path, Source & source) void restorePath(const Path & path, Source & source)
{ {
RestoreSink sink; NARRestoreVisitor sink;
sink.dstPath = path; sink.dstPath = path;
parseDump(sink, source); parseDump(sink, source);
} }
@ -468,10 +517,9 @@ WireFormatGenerator copyNAR(Source & source)
// FIXME: if 'source' is the output of dumpPath() followed by EOF, // FIXME: if 'source' is the output of dumpPath() followed by EOF,
// we should just forward all data directly without parsing. // we should just forward all data directly without parsing.
static ParseSink parseSink; /* null sink; just parse the NAR */ static NARParseVisitor parseSink; /* null sink; just parse the NAR */
return parseAndCopyDump(parseSink, source); return parseAndCopyDump(parseSink, source);
} }
} }

View file

@ -76,45 +76,47 @@ WireFormatGenerator dumpString(std::string_view s);
/** /**
* \todo Fix this API, it sucks. * \todo Fix this API, it sucks.
* A visitor for NAR parsing that performs filesystem (or virtual-filesystem)
* actions to restore a NAR.
*
* Methods of this may arbitrarily fail due to filename collisions.
*/ */
struct ParseSink struct NARParseVisitor
{ {
virtual void createDirectory(const Path & path) { }; /**
* A type-erased file handle specific to this particular NARParseVisitor.
virtual void createRegularFile(const Path & path) { }; */
virtual void closeRegularFile() { }; struct FileHandle
virtual void isExecutable() { };
virtual void preallocateContents(uint64_t size) { };
virtual void receiveContents(std::string_view data) { };
virtual void createSymlink(const Path & path, const std::string & target) { };
};
/**
* If the NAR archive contains a single file at top-level, then save
* the contents of the file to `s`. Otherwise barf.
*/
struct RetrieveRegularNARSink : ParseSink
{
bool regular = true;
Sink & sink;
RetrieveRegularNARSink(Sink & sink) : sink(sink) { }
void createDirectory(const Path & path) override
{ {
regular = false; FileHandle() {}
FileHandle(FileHandle const &) = delete;
FileHandle & operator=(FileHandle &) = delete;
/** Puts one block of data into the file */
virtual void receiveContents(std::string_view data) { }
/**
* Explicitly closes the file. Further operations may throw an assert.
* This exists so that closing can fail and throw an exception without doing so in a destructor.
*/
virtual void close() { }
virtual ~FileHandle() = default;
};
virtual void createDirectory(const Path & path) { }
/**
* Creates a regular file in the extraction output with the given size and executable flag.
* The size is guaranteed to be the true size of the file.
*/
[[nodiscard]]
virtual std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable)
{
return std::make_unique<FileHandle>();
} }
void receiveContents(std::string_view data) override virtual void createSymlink(const Path & path, const std::string & target) { }
{
sink(data);
}
void createSymlink(const Path & path, const std::string & target) override
{
regular = false;
}
}; };
namespace nar { namespace nar {
@ -160,8 +162,8 @@ Generator<Entry> parse(Source & source);
} }
WireFormatGenerator parseAndCopyDump(ParseSink & sink, Source & source); WireFormatGenerator parseAndCopyDump(NARParseVisitor & sink, Source & source);
void parseDump(ParseSink & sink, Source & source); void parseDump(NARParseVisitor & sink, Source & source);
void restorePath(const Path & path, Source & source); void restorePath(const Path & path, Source & source);

View file

@ -210,7 +210,7 @@ inline Paths createDirs(PathView path)
} }
/** /**
* Create a symlink. * Create a symlink. Throws if the symlink exists.
*/ */
void createSymlink(const Path & target, const Path & link); void createSymlink(const Path & target, const Path & link);