forked from lix-project/lix
Compare commits
7 commits
sb/raito/p
...
main
Author | SHA1 | Date | |
---|---|---|---|
Maximilian Bosch | 80202e3ca3 | ||
jade | 727258241f | ||
jade | 5246cea6c8 | ||
jade | 8f88590d13 | ||
jade | b7fc37b015 | ||
jade | ca1dc3f70b | ||
jade | 81c2e0ac8e |
|
@ -2,7 +2,7 @@
|
||||||
name: Missing or incorrect documentation
|
name: Missing or incorrect documentation
|
||||||
about: Help us improve the reference manual
|
about: Help us improve the reference manual
|
||||||
title: ''
|
title: ''
|
||||||
labels: documentation
|
labels: docs
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -19,10 +19,10 @@ assignees: ''
|
||||||
|
|
||||||
<!-- make sure this issue is not redundant or obsolete -->
|
<!-- make sure this issue is not redundant or obsolete -->
|
||||||
|
|
||||||
- [ ] checked [latest Lix manual] \([source]\)
|
- [ ] checked [latest Lix manual] or its [source code]
|
||||||
- [ ] checked [documentation issues] and [recent documentation changes] for possible duplicates
|
- [ ] checked [documentation issues] and [recent documentation changes] for possible duplicates
|
||||||
|
|
||||||
[latest Nix manual]: https://docs.lix.systems/manual/lix/nightly
|
[latest Lix manual]: https://docs.lix.systems/manual/lix/nightly
|
||||||
[source]: https://git.lix.systems/lix-project/lix/src/main/doc/manual/src
|
[source code]: https://git.lix.systems/lix-project/lix/src/main/doc/manual/src
|
||||||
[documentation issues]: https://git.lix.systems/lix-project/lix/issues?labels=151&state=all
|
[documentation issues]: https://git.lix.systems/lix-project/lix/issues?labels=151&state=all
|
||||||
[recent documentation changes]: https://gerrit.lix.systems/q/p:lix+path:%22%5Edoc/manual/.*%22
|
[recent documentation changes]: https://gerrit.lix.systems/q/p:lix+path:%22%5Edoc/manual/.*%22
|
||||||
|
|
|
@ -9,8 +9,24 @@
|
||||||
#include "store-api.hh"
|
#include "store-api.hh"
|
||||||
#include "command.hh"
|
#include "command.hh"
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
|
static std::regex const identifierRegex("^[A-Za-z_][A-Za-z0-9_'-]*$");
|
||||||
|
static void warnInvalidNixIdentifier(const std::string & name)
|
||||||
|
{
|
||||||
|
std::smatch match;
|
||||||
|
if (!std::regex_match(name, match, identifierRegex)) {
|
||||||
|
warn("This Nix invocation specifies a value for argument '%s' which isn't a valid \
|
||||||
|
Nix identifier. The project is considering to drop support for this \
|
||||||
|
or to require quotes around args that aren't valid Nix identifiers. \
|
||||||
|
If you depend on this behvior, please reach out in \
|
||||||
|
https://git.lix.systems/lix-project/lix/issues/496 so we can discuss \
|
||||||
|
your use-case.", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MixEvalArgs::MixEvalArgs()
|
MixEvalArgs::MixEvalArgs()
|
||||||
{
|
{
|
||||||
addFlag({
|
addFlag({
|
||||||
|
@ -18,7 +34,10 @@ MixEvalArgs::MixEvalArgs()
|
||||||
.description = "Pass the value *expr* as the argument *name* to Nix functions.",
|
.description = "Pass the value *expr* as the argument *name* to Nix functions.",
|
||||||
.category = category,
|
.category = category,
|
||||||
.labels = {"name", "expr"},
|
.labels = {"name", "expr"},
|
||||||
.handler = {[&](std::string name, std::string expr) { autoArgs[name] = 'E' + expr; }}
|
.handler = {[&](std::string name, std::string expr) {
|
||||||
|
warnInvalidNixIdentifier(name);
|
||||||
|
autoArgs[name] = 'E' + expr;
|
||||||
|
}}
|
||||||
});
|
});
|
||||||
|
|
||||||
addFlag({
|
addFlag({
|
||||||
|
@ -26,7 +45,10 @@ MixEvalArgs::MixEvalArgs()
|
||||||
.description = "Pass the string *string* as the argument *name* to Nix functions.",
|
.description = "Pass the string *string* as the argument *name* to Nix functions.",
|
||||||
.category = category,
|
.category = category,
|
||||||
.labels = {"name", "string"},
|
.labels = {"name", "string"},
|
||||||
.handler = {[&](std::string name, std::string s) { autoArgs[name] = 'S' + s; }},
|
.handler = {[&](std::string name, std::string s) {
|
||||||
|
warnInvalidNixIdentifier(name);
|
||||||
|
autoArgs[name] = 'S' + s;
|
||||||
|
}},
|
||||||
});
|
});
|
||||||
|
|
||||||
addFlag({
|
addFlag({
|
||||||
|
|
|
@ -62,8 +62,6 @@ struct LocalStore::State::Stmts {
|
||||||
SQLiteStmt QueryReferences;
|
SQLiteStmt QueryReferences;
|
||||||
SQLiteStmt QueryReferrers;
|
SQLiteStmt QueryReferrers;
|
||||||
SQLiteStmt InvalidatePath;
|
SQLiteStmt InvalidatePath;
|
||||||
SQLiteStmt InvalidatePhantomReferrers;
|
|
||||||
SQLiteStmt QueryPhantomReferrers;
|
|
||||||
SQLiteStmt AddDerivationOutput;
|
SQLiteStmt AddDerivationOutput;
|
||||||
SQLiteStmt RegisterRealisedOutput;
|
SQLiteStmt RegisterRealisedOutput;
|
||||||
SQLiteStmt UpdateRealisedOutput;
|
SQLiteStmt UpdateRealisedOutput;
|
||||||
|
@ -386,10 +384,6 @@ LocalStore::LocalStore(const Params & params)
|
||||||
"select path from Refs join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
|
"select path from Refs join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
|
||||||
state->stmts->InvalidatePath.create(state->db,
|
state->stmts->InvalidatePath.create(state->db,
|
||||||
"delete from ValidPaths where path = ?;");
|
"delete from ValidPaths where path = ?;");
|
||||||
state->stmts->InvalidatePhantomReferrers.create(state->db,
|
|
||||||
"delete from Refs where referrer IN (select referrer from Refs left join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?));");
|
|
||||||
state->stmts->QueryPhantomReferrers.create(state->db,
|
|
||||||
"select referrer from Refs left join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
|
|
||||||
state->stmts->AddDerivationOutput.create(state->db,
|
state->stmts->AddDerivationOutput.create(state->db,
|
||||||
"insert or replace into DerivationOutputs (drv, id, path) values (?, ?, ?);");
|
"insert or replace into DerivationOutputs (drv, id, path) values (?, ?, ?);");
|
||||||
state->stmts->QueryValidDerivers.create(state->db,
|
state->stmts->QueryValidDerivers.create(state->db,
|
||||||
|
@ -1222,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 (...) {
|
||||||
|
@ -1528,18 +1522,6 @@ void LocalStore::invalidatePathChecked(const StorePath & path)
|
||||||
if (!referrers.empty())
|
if (!referrers.empty())
|
||||||
throw PathInUse("cannot delete path '%s' because it is in use by %s",
|
throw PathInUse("cannot delete path '%s' because it is in use by %s",
|
||||||
printStorePath(path), showPaths(referrers));
|
printStorePath(path), showPaths(referrers));
|
||||||
|
|
||||||
// Note: `queryReferrers` will only return *valid* referrers.
|
|
||||||
// i.e. referrer for which there is a *ValidPath* row in the SQLite database.
|
|
||||||
// In the unfortunate situation where a valid path is removed but its corresponding `Refs` are not removed (*), we better just invalidate all these phantom referrers,
|
|
||||||
// otherwise we will create a foreign key violation when we actually try to invalidate paths.
|
|
||||||
//
|
|
||||||
// (*) : yes, there's a "ON DELETE CASCADE" on the referrer foreign key.
|
|
||||||
// Unfortunately, in practice, it doesn't ensure integrity over large SQLite databases.
|
|
||||||
if (hasPhantomReferrers(*state, path)) {
|
|
||||||
warn("'%s' has phantom referrers (disappeared referrers from the valid path table)", printStorePath(path));
|
|
||||||
invalidatePhantomReferrers(*state, path);
|
|
||||||
}
|
|
||||||
invalidatePath(*state, path);
|
invalidatePath(*state, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1547,24 +1529,6 @@ void LocalStore::invalidatePathChecked(const StorePath & path)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LocalStore::hasPhantomReferrers(State & state, const StorePath & path)
|
|
||||||
{
|
|
||||||
return retrySQLite<bool>([&]() -> bool {
|
|
||||||
debug("checking for phantom referrers for '%s'", printStorePath(path));
|
|
||||||
auto useQueryPhantomReferrers(state.stmts->QueryPhantomReferrers.use()(printStorePath(path)));
|
|
||||||
|
|
||||||
return useQueryPhantomReferrers.next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void LocalStore::invalidatePhantomReferrers(State & state, const StorePath & path)
|
|
||||||
{
|
|
||||||
retrySQLite<void>([&]() {
|
|
||||||
debug("invalidating phantom referrers to '%s'", printStorePath(path));
|
|
||||||
state.stmts->InvalidatePhantomReferrers.use()(printStorePath(path)).exec();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
|
bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
|
||||||
{
|
{
|
||||||
|
|
|
@ -322,14 +322,6 @@ private:
|
||||||
* Delete a path from the Nix store.
|
* Delete a path from the Nix store.
|
||||||
*/
|
*/
|
||||||
void invalidatePathChecked(const StorePath & path);
|
void invalidatePathChecked(const StorePath & path);
|
||||||
/**
|
|
||||||
* Check if there's phantom referrers for a certain path in the Nix SQLite database
|
|
||||||
*/
|
|
||||||
bool hasPhantomReferrers(State & state, const StorePath & path);
|
|
||||||
/**
|
|
||||||
* Invalidate all phantom referrers from the Nix SQLite database.
|
|
||||||
*/
|
|
||||||
void invalidatePhantomReferrers(State & state, const StorePath & path);
|
|
||||||
|
|
||||||
void verifyPath(const StorePath & path, const StorePathSet & store,
|
void verifyPath(const StorePath & path, const StorePathSet & store,
|
||||||
StorePathSet & done, StorePathSet & validPaths, RepairFlag repair, bool & errors);
|
StorePathSet & done, StorePathSet & validPaths, RepairFlag repair, bool & errors);
|
||||||
|
|
|
@ -73,8 +73,16 @@ struct SimpleUserLock : UserLock
|
||||||
debug("trying user '%s'", i);
|
debug("trying user '%s'", i);
|
||||||
|
|
||||||
struct passwd * pw = getpwnam(i.c_str());
|
struct passwd * pw = getpwnam(i.c_str());
|
||||||
if (!pw)
|
if (!pw) {
|
||||||
throw Error("the user '%s' in the group '%s' does not exist", i, settings.buildUsersGroup);
|
#ifdef __APPLE__
|
||||||
|
#define APPLE_HINT "\n\nhint: this may be caused by an update to macOS Sequoia breaking existing Lix installations.\n" \
|
||||||
|
"See the macOS Sequoia page on the Lix wiki for detailed repair instructions: https://wiki.lix.systems/link/81"
|
||||||
|
#else
|
||||||
|
#define APPLE_HINT
|
||||||
|
#endif
|
||||||
|
throw Error("the user '%s' in the group '%s' does not exist" APPLE_HINT, i, settings.buildUsersGroup);
|
||||||
|
#undef APPLE_HINT
|
||||||
|
}
|
||||||
|
|
||||||
auto fnUserLock = fmt("%s/userpool/%s", settings.nixStateDir,pw->pw_uid);
|
auto fnUserLock = fmt("%s/userpool/%s", settings.nixStateDir,pw->pw_uid);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ fi
|
||||||
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
|
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
|
||||||
export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
|
export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
|
||||||
export NIX_STATE_DIR=$TEST_ROOT/var/nix
|
export NIX_STATE_DIR=$TEST_ROOT/var/nix
|
||||||
export NIX_SQLITE_DATABASE=$NIX_STATE_DIR/db/db.sqlite
|
|
||||||
export NIX_CONF_DIR=$TEST_ROOT/etc
|
export NIX_CONF_DIR=$TEST_ROOT/etc
|
||||||
export NIX_DAEMON_SOCKET_PATH=$TEST_ROOT/dSocket
|
export NIX_DAEMON_SOCKET_PATH=$TEST_ROOT/dSocket
|
||||||
unset NIX_USER_CONF_FILES
|
unset NIX_USER_CONF_FILES
|
||||||
|
@ -165,10 +164,6 @@ requireDaemonNewerThan () {
|
||||||
isDaemonNewer "$1" || skipTest "Daemon is too old"
|
isDaemonNewer "$1" || skipTest "Daemon is too old"
|
||||||
}
|
}
|
||||||
|
|
||||||
requireSqliteDatabase() {
|
|
||||||
[[ -f "$NIX_SQLITE_DATABASE" ]] || skipTest "SQLite database is not used for this store implementation"
|
|
||||||
}
|
|
||||||
|
|
||||||
canUseSandbox() {
|
canUseSandbox() {
|
||||||
[[ ${_canUseSandbox-} ]]
|
[[ ${_canUseSandbox-} ]]
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,6 @@ functional_tests_scripts = [
|
||||||
'flakes/flake-registry.sh',
|
'flakes/flake-registry.sh',
|
||||||
'flakes/subdir-flake.sh',
|
'flakes/subdir-flake.sh',
|
||||||
'gc.sh',
|
'gc.sh',
|
||||||
'phantom-referrers-gc.sh',
|
|
||||||
'nix-collect-garbage-d.sh',
|
'nix-collect-garbage-d.sh',
|
||||||
'nix-collect-garbage-dry-run.sh',
|
'nix-collect-garbage-dry-run.sh',
|
||||||
'remote-store.sh',
|
'remote-store.sh',
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
source common.sh
|
|
||||||
|
|
||||||
startDaemon
|
|
||||||
|
|
||||||
requireDaemonNewerThan "2.92.0"
|
|
||||||
requireSqliteDatabase
|
|
||||||
|
|
||||||
clearStore
|
|
||||||
|
|
||||||
depOutPath=$(nix-build --no-out-link -E '
|
|
||||||
with import ./config.nix;
|
|
||||||
|
|
||||||
mkDerivation {
|
|
||||||
name = "phantom";
|
|
||||||
outputs = [ "out" ];
|
|
||||||
buildCommand = "
|
|
||||||
echo i will become a phantom soon > $out
|
|
||||||
";
|
|
||||||
}
|
|
||||||
')
|
|
||||||
|
|
||||||
finalOutPath=$(nix-build --no-out-link -E '
|
|
||||||
with import ./config.nix;
|
|
||||||
|
|
||||||
let dep = mkDerivation {
|
|
||||||
name = "phantom";
|
|
||||||
outputs = [ "out" ];
|
|
||||||
buildCommand = "
|
|
||||||
echo i will become a phantom soon > $out
|
|
||||||
";
|
|
||||||
}; in
|
|
||||||
|
|
||||||
mkDerivation {
|
|
||||||
name = "phantom-gc";
|
|
||||||
outputs = [ "out" ];
|
|
||||||
buildCommand = "
|
|
||||||
echo UNUSED: ${dep} > $out
|
|
||||||
";
|
|
||||||
}
|
|
||||||
')
|
|
||||||
|
|
||||||
echo "displaying all valid paths"
|
|
||||||
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
|
|
||||||
select * from validpaths;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
|
|
||||||
echo "displaying the relevant IDs..."
|
|
||||||
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
|
|
||||||
select r.referrer, r.reference from Refs r join ValidPaths vp on r.referrer = vp.id where path = '$finalOutPath';
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "corrupting the SQLite database manually..."
|
|
||||||
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
|
|
||||||
pragma foreign_keys = off;
|
|
||||||
delete from ValidPaths where path = '$finalOutPath';
|
|
||||||
select * from Refs;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
restartDaemon
|
|
||||||
# expect this to work and maybe warn about phantom referrers
|
|
||||||
expectStderr 0 nix-collect-garbage -vvvv | grepQuiet 'phantom referrers'
|
|
Loading…
Reference in a new issue