Make the NAR parser much stricter wrt field order

We really want to enforce a canonical representation since NAR
hashing/signing/deduplication depends on that.
This commit is contained in:
Eelco Dolstra 2024-09-12 15:57:46 +02:00
parent 27ec0def74
commit 7aa3e7e3a5
4 changed files with 85 additions and 100 deletions

View file

@ -168,49 +168,42 @@ struct CaseInsensitiveCompare
static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath & path) static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath & path)
{ {
std::string s;
s = readString(source);
if (s != "(") throw badArchive("expected open tag");
auto getString = [&]() { auto getString = [&]() {
checkInterrupt(); checkInterrupt();
return readString(source); return readString(source);
}; };
// For first iteration auto expectTag = [&](std::string_view expected) {
s = getString(); auto tag = getString();
if (tag != expected)
throw badArchive("expected tag '%s', got '%s'", expected, tag);
};
while (1) { expectTag("(");
if (s == ")") { expectTag("type");
break;
}
else if (s == "type") { auto type = getString();
std::string t = getString();
if (t == "regular") { if (type == "regular") {
sink.createRegularFile(path, [&](auto & crf) { sink.createRegularFile(path, [&](auto & crf) {
while (1) { auto tag = getString();
s = getString();
if (s == "contents") { if (tag == "executable") {
parseContents(crf, source);
}
else if (s == "executable") {
auto s2 = getString(); auto s2 = getString();
if (s2 != "") throw badArchive("executable marker has non-empty value"); if (s2 != "") throw badArchive("executable marker has non-empty value");
crf.isExecutable(); crf.isExecutable();
tag = getString();
} }
else break; if (tag == "contents")
} parseContents(crf, source);
expectTag(")");
}); });
} }
else if (t == "directory") { else if (type == "directory") {
sink.createDirectory(path); sink.createDirectory(path);
std::map<Path, int, CaseInsensitiveCompare> names; std::map<Path, int, CaseInsensitiveCompare> names;
@ -218,21 +211,18 @@ static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath
std::string prevName; std::string prevName;
while (1) { while (1) {
s = getString(); auto tag = getString();
if (s == "entry") { if (tag == ")") break;
std::string name;
s = getString(); if (tag != "entry")
if (s != "(") throw badArchive("expected open tag '%s'", s); throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
while (1) { expectTag("(");
s = getString();
if (s == ")") { expectTag("name");
break;
} else if (s == "name") { auto name = getString();
name = getString();
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos || name.find((char) 0) != std::string::npos) if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos || name.find((char) 0) != std::string::npos)
throw badArchive("NAR contains invalid file name '%1%'", name); throw badArchive("NAR contains invalid file name '%1%'", name);
if (name <= prevName) if (name <= prevName)
@ -250,38 +240,25 @@ static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath
} else } else
names[name] = 0; names[name] = 0;
} }
} else if (s == "node") {
if (name.empty()) throw badArchive("entry name missing"); expectTag("node");
parse(sink, source, path / name); parse(sink, source, path / name);
} else
throw badArchive("unknown field '%s'", s); expectTag(")");
} }
} }
else break; else if (type == "symlink") {
} expectTag("target");
}
else if (t == "symlink") { auto target = getString();
s = getString();
if (s != "target")
throw badArchive("expected 'target', got '%s'", s);
std::string target = getString();
sink.createSymlink(path, target); sink.createSymlink(path, target);
// for the next iteration expectTag(")");
s = getString();
} }
else throw badArchive("unknown file type '%s'", t); else throw badArchive("unknown file type '%s'", type);
}
else
throw badArchive("unknown field '%s'", s);
}
} }

Binary file not shown.

Binary file not shown.

View file

@ -112,3 +112,11 @@ expectStderr 1 nix-store --restore "$TEST_ROOT/out" < slash.nar | grepQuiet "NAR
# Likewise for an empty filename. # Likewise for an empty filename.
rm -rf "$TEST_ROOT/out" rm -rf "$TEST_ROOT/out"
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < empty.nar | grepQuiet "NAR contains invalid file name ''" expectStderr 1 nix-store --restore "$TEST_ROOT/out" < empty.nar | grepQuiet "NAR contains invalid file name ''"
# Test that the 'executable' field cannot come before the 'contents' field.
rm -rf "$TEST_ROOT/out"
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < executable-after-contents.nar | grepQuiet "expected tag ')', got 'executable'"
# Test that the 'name' field cannot come before the 'node' field in a directory entry.
rm -rf "$TEST_ROOT/out"
expectStderr 1 nix-store --restore "$TEST_ROOT/out" < name-after-node.nar | grepQuiet "expected tag 'name'"