diff options
Diffstat (limited to 'src/libutil/archive.cc')
-rw-r--r-- | src/libutil/archive.cc | 156 |
1 files changed, 102 insertions, 54 deletions
diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index d4da18f14..225483804 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -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()) { co_yield std::visit( @@ -347,16 +347,13 @@ static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar) }, [&](nar::File f) { return [](auto f, auto & sink) -> WireFormatGenerator { - sink.createRegularFile(f.path); - sink.preallocateContents(f.size); - if (f.executable) { - sink.isExecutable(); - } + auto handle = sink.createRegularFile(f.path, f.size, f.executable); + 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; } - sink.closeRegularFile(); + handle->close(); }(std::move(f), sink); }, [&](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)); } -void parseDump(ParseSink & sink, Source & source) +void parseDump(NARParseVisitor & sink, Source & source) { auto parser = parseAndCopyDump(sink, source); 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; - 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 { Path p = dstPath + path; @@ -402,49 +487,13 @@ struct RestoreSink : ParseSink 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; - 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); - } - - void closeRegularFile() override - { - /* 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); + return std::unique_ptr<MyFileHandle>(new MyFileHandle(std::move(fd), size, executable)); } void createSymlink(const Path & path, const std::string & target) override @@ -457,7 +506,7 @@ struct RestoreSink : ParseSink void restorePath(const Path & path, Source & source) { - RestoreSink sink; + NARRestoreVisitor sink; sink.dstPath = path; parseDump(sink, source); } @@ -468,10 +517,9 @@ WireFormatGenerator copyNAR(Source & source) // FIXME: if 'source' is the output of dumpPath() followed by EOF, // 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); } - } |