Compare commits
60 Commits
2.22.2
...
rootless-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
150f3bb5ea | ||
|
|
c4ca0d45cb | ||
|
|
65387ad3ea | ||
|
|
19d7d7ac42 | ||
|
|
060bfe5084 | ||
|
|
7299ad523b | ||
|
|
2d651ad2d0 | ||
|
|
6c0e7450de | ||
|
|
5c6b9bc361 | ||
|
|
40e95a2e30 | ||
|
|
988c51de7f | ||
|
|
ef644ec753 | ||
|
|
aa97c4f9f2 | ||
|
|
beac0b49e4 | ||
|
|
1cd308194f | ||
|
|
e58c47cc1a | ||
|
|
48b8de73f0 | ||
|
|
83365a79bc | ||
|
|
d4bbb1dec7 | ||
|
|
cd7e22e4e0 | ||
|
|
8de54ed4e1 | ||
|
|
240f1614d8 | ||
|
|
f76bfbfbd6 | ||
|
|
d97b9f138c | ||
|
|
f6a30993f0 | ||
|
|
072f420260 | ||
|
|
2c47b08e17 | ||
|
|
fbec849281 | ||
|
|
bbde40af3e | ||
|
|
80bb58b186 | ||
|
|
2575dd4f19 | ||
|
|
dadc4a42c6 | ||
|
|
1d5d30b12f | ||
|
|
e1df6c220a | ||
|
|
8f622ff71b | ||
|
|
f3b9d3fd88 | ||
|
|
9a3c3cb748 | ||
|
|
63159dd672 | ||
|
|
4d9ca6d09e | ||
|
|
26c802d18c | ||
|
|
aadf585ea3 | ||
|
|
35c7d5d2f1 | ||
|
|
3839eb15d6 | ||
|
|
61c35a810b | ||
|
|
93739ce006 | ||
|
|
5d47c37cbc | ||
|
|
2e7f1d24a1 | ||
|
|
c788718de1 | ||
|
|
b4ab02ef13 | ||
|
|
2253b9044c | ||
|
|
a3c17cfc73 | ||
|
|
303abee699 | ||
|
|
a2bcf35e0d | ||
|
|
be28cb9262 | ||
|
|
a3979e67f4 | ||
|
|
5ed3a9db6a | ||
|
|
c9afca59e8 | ||
|
|
0a26f9ae4a | ||
|
|
fc553fb632 | ||
|
|
ffe155abd3 |
1
Makefile
1
Makefile
@@ -2,6 +2,7 @@ makefiles = \
|
||||
mk/precompiled-headers.mk \
|
||||
local.mk \
|
||||
src/libutil/local.mk \
|
||||
src/nix-find-roots/local.mk \
|
||||
src/libutil/tests/local.mk \
|
||||
src/libstore/local.mk \
|
||||
src/libstore/tests/local.mk \
|
||||
|
||||
@@ -282,15 +282,6 @@ AC_CHECK_FUNCS([setresuid setreuid lchown])
|
||||
AC_CHECK_FUNCS([strsignal posix_fallocate sysconf])
|
||||
|
||||
|
||||
# This is needed if bzip2 is a static library, and the Nix libraries
|
||||
# are dynamic.
|
||||
case "${host_os}" in
|
||||
darwin*)
|
||||
LDFLAGS="-all_load $LDFLAGS"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
AC_ARG_WITH(sandbox-shell, AS_HELP_STRING([--with-sandbox-shell=PATH],[path of a statically-linked shell to use as /bin/sh in sandboxes]),
|
||||
sandbox_shell=$withval)
|
||||
AC_SUBST(sandbox_shell)
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
- [Hacking](contributing/hacking.md)
|
||||
- [CLI guideline](contributing/cli-guideline.md)
|
||||
- [Release Notes](release-notes/release-notes.md)
|
||||
- [Release X.Y (202?-??-??)](release-notes/rl-next.md)
|
||||
- [Release 2.7 (2022-03-07)](release-notes/rl-2.7.md)
|
||||
- [Release 2.6 (2022-01-24)](release-notes/rl-2.6.md)
|
||||
- [Release 2.5 (2021-12-13)](release-notes/rl-2.5.md)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Release X.Y (2022-03-07)
|
||||
# Release 2.7 (2022-03-07)
|
||||
|
||||
* Nix will now make some helpful suggestions when you mistype
|
||||
something on the command line. For instance, if you type `nix build
|
||||
|
||||
8
flake.lock
generated
8
flake.lock
generated
@@ -18,16 +18,16 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1632864508,
|
||||
"narHash": "sha256-d127FIvGR41XbVRDPVvozUPQ/uRHbHwvfyKHwEt5xFM=",
|
||||
"lastModified": 1656604668,
|
||||
"narHash": "sha256-5g8ll8kgstMtdAhKdAfkv5YuhmxVE+77IuFBubK3i3k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "82891b5e2c2359d7e58d08849e4c89511ab94234",
|
||||
"rev": "e1029c6170ccec08a5fc0123a4cca1779a0cd43d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-21.05-small",
|
||||
"ref": "nixos-22.05-small",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
|
||||
22
flake.nix
22
flake.nix
@@ -1,7 +1,7 @@
|
||||
{
|
||||
description = "The purely functional package manager";
|
||||
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-21.05-small";
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-22.05-small";
|
||||
inputs.nixpkgs-regression.url = "nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2";
|
||||
inputs.lowdown-src = { url = "github:kristapsdz/lowdown"; flake = false; };
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
then ""
|
||||
else "pre${builtins.substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101")}_${self.shortRev or "dirty"}";
|
||||
|
||||
officialRelease = false;
|
||||
officialRelease = true;
|
||||
|
||||
linux64BitSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
linuxSystems = linux64BitSystems ++ [ "i686-linux" ];
|
||||
@@ -403,6 +403,23 @@
|
||||
BINDIR=${placeholder "bin"}/bin
|
||||
'';
|
||||
};
|
||||
nix-find-roots = prev.stdenv.mkDerivation {
|
||||
name = "nix-find-roots-${version}";
|
||||
inherit version;
|
||||
|
||||
src = "${self}/src/nix-find-roots";
|
||||
|
||||
CXXFLAGS = prev.lib.optionalString prev.stdenv.hostPlatform.isStatic "-static";
|
||||
|
||||
buildPhase = ''
|
||||
$CXX $CXXFLAGS -std=c++17 *.cc **/*.cc -I lib -o nix-find-roots
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp nix-find-roots $out/bin/
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
@@ -560,6 +577,7 @@
|
||||
packages = forAllSystems (system: {
|
||||
inherit (nixpkgsFor.${system}) nix;
|
||||
} // (nixpkgs.lib.optionalAttrs (builtins.elem system linux64BitSystems) {
|
||||
inherit (nixpkgsFor.${system}.pkgsStatic) nix-find-roots;
|
||||
nix-static = let
|
||||
nixpkgs = nixpkgsFor.${system}.pkgsStatic;
|
||||
in with commonDeps nixpkgs; nixpkgs.stdenv.mkDerivation {
|
||||
|
||||
@@ -96,20 +96,20 @@ RootValue allocRootValue(Value * v)
|
||||
}
|
||||
|
||||
|
||||
void printValue(std::ostream & str, std::set<const void *> & seen, const Value & v)
|
||||
void Value::print(std::ostream & str, std::set<const void *> * seen) const
|
||||
{
|
||||
checkInterrupt();
|
||||
|
||||
switch (v.internalType) {
|
||||
switch (internalType) {
|
||||
case tInt:
|
||||
str << v.integer;
|
||||
str << integer;
|
||||
break;
|
||||
case tBool:
|
||||
str << (v.boolean ? "true" : "false");
|
||||
str << (boolean ? "true" : "false");
|
||||
break;
|
||||
case tString:
|
||||
str << "\"";
|
||||
for (const char * i = v.string.s; *i; i++)
|
||||
for (const char * i = string.s; *i; i++)
|
||||
if (*i == '\"' || *i == '\\') str << "\\" << *i;
|
||||
else if (*i == '\n') str << "\\n";
|
||||
else if (*i == '\r') str << "\\r";
|
||||
@@ -119,19 +119,19 @@ void printValue(std::ostream & str, std::set<const void *> & seen, const Value &
|
||||
str << "\"";
|
||||
break;
|
||||
case tPath:
|
||||
str << v.path; // !!! escaping?
|
||||
str << path; // !!! escaping?
|
||||
break;
|
||||
case tNull:
|
||||
str << "null";
|
||||
break;
|
||||
case tAttrs: {
|
||||
if (!v.attrs->empty() && !seen.insert(v.attrs).second)
|
||||
str << "<REPEAT>";
|
||||
if (seen && !attrs->empty() && !seen->insert(attrs).second)
|
||||
str << "«repeated»";
|
||||
else {
|
||||
str << "{ ";
|
||||
for (auto & i : v.attrs->lexicographicOrder()) {
|
||||
for (auto & i : attrs->lexicographicOrder()) {
|
||||
str << i->name << " = ";
|
||||
printValue(str, seen, *i->value);
|
||||
i->value->print(str, seen);
|
||||
str << "; ";
|
||||
}
|
||||
str << "}";
|
||||
@@ -141,12 +141,12 @@ void printValue(std::ostream & str, std::set<const void *> & seen, const Value &
|
||||
case tList1:
|
||||
case tList2:
|
||||
case tListN:
|
||||
if (v.listSize() && !seen.insert(v.listElems()).second)
|
||||
str << "<REPEAT>";
|
||||
if (seen && listSize() && !seen->insert(listElems()).second)
|
||||
str << "«repeated»";
|
||||
else {
|
||||
str << "[ ";
|
||||
for (auto v2 : v.listItems()) {
|
||||
printValue(str, seen, *v2);
|
||||
for (auto v2 : listItems()) {
|
||||
v2->print(str, seen);
|
||||
str << " ";
|
||||
}
|
||||
str << "]";
|
||||
@@ -166,10 +166,10 @@ void printValue(std::ostream & str, std::set<const void *> & seen, const Value &
|
||||
str << "<PRIMOP-APP>";
|
||||
break;
|
||||
case tExternal:
|
||||
str << *v.external;
|
||||
str << *external;
|
||||
break;
|
||||
case tFloat:
|
||||
str << v.fpoint;
|
||||
str << fpoint;
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
@@ -177,10 +177,16 @@ void printValue(std::ostream & str, std::set<const void *> & seen, const Value &
|
||||
}
|
||||
|
||||
|
||||
std::ostream & operator << (std::ostream & str, const Value & v)
|
||||
void Value::print(std::ostream & str, bool showRepeated) const
|
||||
{
|
||||
std::set<const void *> seen;
|
||||
printValue(str, seen, v);
|
||||
print(str, showRepeated ? nullptr : &seen);
|
||||
}
|
||||
|
||||
|
||||
std::ostream & operator << (std::ostream & str, const Value & v)
|
||||
{
|
||||
v.print(str, false);
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,10 +115,13 @@ private:
|
||||
InternalType internalType;
|
||||
|
||||
friend std::string showType(const Value & v);
|
||||
friend void printValue(std::ostream & str, std::set<const void *> & seen, const Value & v);
|
||||
|
||||
void print(std::ostream & str, std::set<const void *> * seen) const;
|
||||
|
||||
public:
|
||||
|
||||
void print(std::ostream & str, bool showRepeated = false) const;
|
||||
|
||||
// Functions needed to distinguish the type
|
||||
// These should be removed eventually, by putting the functionality that's
|
||||
// needed by callers into methods of this type
|
||||
|
||||
@@ -1394,7 +1394,7 @@ void LocalDerivationGoal::startDaemon()
|
||||
try {
|
||||
daemon::processConnection(store, from, to,
|
||||
daemon::NotTrusted, daemon::Recursive,
|
||||
[&](Store & store) { store.createUser("nobody", 65535); });
|
||||
[&](Store & store) {});
|
||||
debug("terminated daemon connection");
|
||||
} catch (SysError &) {
|
||||
ignoreException();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "local-store.hh"
|
||||
#include "local-fs-store.hh"
|
||||
#include "finally.hh"
|
||||
#include "find-roots.hh"
|
||||
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
@@ -25,6 +26,7 @@ namespace nix {
|
||||
|
||||
|
||||
static std::string gcSocketPath = "/gc-socket/socket";
|
||||
static std::string rootsSocketPath = "/gc-roots-socket/socket";
|
||||
static std::string gcRootsDir = "gcroots";
|
||||
|
||||
|
||||
@@ -122,7 +124,7 @@ void LocalStore::addTempRoot(const StorePath & path)
|
||||
collector is running. So we have to connect to the garbage
|
||||
collector and inform it about our root. */
|
||||
if (!state->fdRootsSocket) {
|
||||
auto socketPath = stateDir.get() + gcSocketPath;
|
||||
auto socketPath = stateDir.get() + rootsSocketPath;
|
||||
debug("connecting to '%s'", socketPath);
|
||||
state->fdRootsSocket = createUnixDomainSocket();
|
||||
try {
|
||||
@@ -216,79 +218,32 @@ void LocalStore::findTempRoots(Roots & tempRoots, bool censor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void LocalStore::findRoots(const Path & path, unsigned char type, Roots & roots)
|
||||
void LocalStore::findRootsNoTempNoExternalDaemon(Roots & roots, bool censor)
|
||||
{
|
||||
auto foundRoot = [&](const Path & path, const Path & target) {
|
||||
try {
|
||||
auto storePath = toStorePath(target).first;
|
||||
if (isValidPath(storePath))
|
||||
roots[std::move(storePath)].emplace(path);
|
||||
else
|
||||
printInfo("skipping invalid root from '%1%' to '%2%'", path, target);
|
||||
} catch (BadStorePath &) { }
|
||||
debug("Can’t connect to the tracing daemon socket, fallback to the internal trace");
|
||||
|
||||
using namespace nix::roots_tracer;
|
||||
|
||||
const TracerConfig opts {
|
||||
.storeDir = fs::path(storeDir),
|
||||
.stateDir = fs::path(stateDir.get())
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
if (type == DT_UNKNOWN)
|
||||
type = getFileType(path);
|
||||
|
||||
if (type == DT_DIR) {
|
||||
for (auto & i : readDirectory(path))
|
||||
findRoots(path + "/" + i.name, i.type, roots);
|
||||
}
|
||||
|
||||
else if (type == DT_LNK) {
|
||||
Path target = readLink(path);
|
||||
if (isInStore(target))
|
||||
foundRoot(path, target);
|
||||
|
||||
/* Handle indirect roots. */
|
||||
else {
|
||||
target = absPath(target, dirOf(path));
|
||||
if (!pathExists(target)) {
|
||||
if (isInDir(path, stateDir + "/" + gcRootsDir + "/auto")) {
|
||||
printInfo(format("removing stale link from '%1%' to '%2%'") % path % target);
|
||||
unlink(path.c_str());
|
||||
}
|
||||
} else {
|
||||
struct stat st2 = lstat(target);
|
||||
if (!S_ISLNK(st2.st_mode)) return;
|
||||
Path target2 = readLink(target);
|
||||
if (isInStore(target2)) foundRoot(target, target2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if (type == DT_REG) {
|
||||
auto storePath = maybeParseStorePath(storeDir + "/" + std::string(baseNameOf(path)));
|
||||
if (storePath && isValidPath(*storePath))
|
||||
roots[std::move(*storePath)].emplace(path);
|
||||
}
|
||||
|
||||
const std::set<fs::path> standardRoots = {
|
||||
opts.stateDir / fs::path(gcRootsDir),
|
||||
opts.stateDir / fs::path("gcroots"),
|
||||
};
|
||||
auto traceResult = traceStaticRoots(opts, standardRoots);
|
||||
auto runtimeRoots = getRuntimeRoots(opts);
|
||||
traceResult.storeRoots.insert(runtimeRoots.begin(), runtimeRoots.end());
|
||||
for (auto & [rawRootInStore, externalRoots] : traceResult.storeRoots) {
|
||||
if (!isInStore(rawRootInStore)) continue;
|
||||
auto rootInStore = toStorePath(rawRootInStore).first;
|
||||
if (!isValidPath(rootInStore)) continue;
|
||||
std::unordered_set<std::string> primRoots;
|
||||
for (const auto & externalRoot : externalRoots)
|
||||
primRoots.insert(externalRoot.string());
|
||||
roots.emplace(rootInStore, primRoots);
|
||||
}
|
||||
|
||||
catch (SysError & e) {
|
||||
/* We only ignore permanent failures. */
|
||||
if (e.errNo == EACCES || e.errNo == ENOENT || e.errNo == ENOTDIR)
|
||||
printInfo("cannot read potential root '%1%'", path);
|
||||
else
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void LocalStore::findRootsNoTemp(Roots & roots, bool censor)
|
||||
{
|
||||
/* Process direct roots in {gcroots,profiles}. */
|
||||
findRoots(stateDir + "/" + gcRootsDir, DT_UNKNOWN, roots);
|
||||
findRoots(stateDir + "/profiles", DT_UNKNOWN, roots);
|
||||
|
||||
/* Add additional roots returned by different platforms-specific
|
||||
heuristics. This is typically used to add running programs to
|
||||
the set of roots (to prevent them from being garbage collected). */
|
||||
findRuntimeRoots(roots, censor);
|
||||
}
|
||||
|
||||
|
||||
@@ -302,153 +257,53 @@ Roots LocalStore::findRoots(bool censor)
|
||||
return roots;
|
||||
}
|
||||
|
||||
void LocalStore::findRootsNoTemp(Roots & roots, bool censor)
|
||||
{
|
||||
|
||||
auto fd = createUnixDomainSocket();
|
||||
|
||||
std::string socketPath = settings.gcSocketPath.get() != "auto"
|
||||
? settings.gcSocketPath.get()
|
||||
: stateDir.get() + gcSocketPath;
|
||||
|
||||
try {
|
||||
nix::connect(fd.get(), socketPath);
|
||||
} catch (SysError & e) {
|
||||
return findRootsNoTempNoExternalDaemon(roots, censor);
|
||||
}
|
||||
|
||||
settings.requireExperimentalFeature(Xp::ExternalGCDaemon);
|
||||
|
||||
try {
|
||||
StringMap unescapes = {
|
||||
{ "\\n", "\n"},
|
||||
{ "\\t", "\t"},
|
||||
};
|
||||
while (true) {
|
||||
auto line = readLine(fd.get());
|
||||
if (line.empty()) break; // TODO: Handle the broken symlinks
|
||||
auto parsedLine = tokenizeString<std::vector<std::string>>(line, "\t");
|
||||
if (parsedLine.size() != 2)
|
||||
throw Error("Invalid result from the gc helper");
|
||||
auto rawDestPath = rewriteStrings(parsedLine[0], unescapes);
|
||||
auto retainer = rewriteStrings(parsedLine[1], unescapes);
|
||||
if (!isInStore(rawDestPath)) continue;
|
||||
try {
|
||||
auto destPath = toStorePath(rawDestPath).first;
|
||||
if (!isValidPath(destPath)) continue;
|
||||
roots[destPath].insert(
|
||||
(!censor || isInDir(retainer, stateDir.get())) ? retainer : censored);
|
||||
} catch (Error &) {
|
||||
}
|
||||
}
|
||||
} catch (EndOfFile &) {
|
||||
}
|
||||
}
|
||||
|
||||
typedef std::unordered_map<Path, std::unordered_set<std::string>> UncheckedRoots;
|
||||
|
||||
static void readProcLink(const std::string & file, UncheckedRoots & roots)
|
||||
{
|
||||
/* 64 is the starting buffer size gnu readlink uses... */
|
||||
auto bufsiz = ssize_t{64};
|
||||
try_again:
|
||||
char buf[bufsiz];
|
||||
auto res = readlink(file.c_str(), buf, bufsiz);
|
||||
if (res == -1) {
|
||||
if (errno == ENOENT || errno == EACCES || errno == ESRCH)
|
||||
return;
|
||||
throw SysError("reading symlink");
|
||||
}
|
||||
if (res == bufsiz) {
|
||||
if (SSIZE_MAX / 2 < bufsiz)
|
||||
throw Error("stupidly long symlink");
|
||||
bufsiz *= 2;
|
||||
goto try_again;
|
||||
}
|
||||
if (res > 0 && buf[0] == '/')
|
||||
roots[std::string(static_cast<char *>(buf), res)]
|
||||
.emplace(file);
|
||||
}
|
||||
|
||||
static std::string quoteRegexChars(const std::string & raw)
|
||||
{
|
||||
static auto specialRegex = std::regex(R"([.^$\\*+?()\[\]{}|])");
|
||||
return std::regex_replace(raw, specialRegex, R"(\$&)");
|
||||
}
|
||||
|
||||
#if __linux__
|
||||
static void readFileRoots(const char * path, UncheckedRoots & roots)
|
||||
{
|
||||
try {
|
||||
roots[readFile(path)].emplace(path);
|
||||
} catch (SysError & e) {
|
||||
if (e.errNo != ENOENT && e.errNo != EACCES)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void LocalStore::findRuntimeRoots(Roots & roots, bool censor)
|
||||
{
|
||||
UncheckedRoots unchecked;
|
||||
|
||||
auto procDir = AutoCloseDir{opendir("/proc")};
|
||||
if (procDir) {
|
||||
struct dirent * ent;
|
||||
auto digitsRegex = std::regex(R"(^\d+$)");
|
||||
auto mapRegex = std::regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)");
|
||||
auto storePathRegex = std::regex(quoteRegexChars(storeDir) + R"(/[0-9a-z]+[0-9a-zA-Z\+\-\._\?=]*)");
|
||||
while (errno = 0, ent = readdir(procDir.get())) {
|
||||
checkInterrupt();
|
||||
if (std::regex_match(ent->d_name, digitsRegex)) {
|
||||
readProcLink(fmt("/proc/%s/exe" ,ent->d_name), unchecked);
|
||||
readProcLink(fmt("/proc/%s/cwd", ent->d_name), unchecked);
|
||||
|
||||
auto fdStr = fmt("/proc/%s/fd", ent->d_name);
|
||||
auto fdDir = AutoCloseDir(opendir(fdStr.c_str()));
|
||||
if (!fdDir) {
|
||||
if (errno == ENOENT || errno == EACCES)
|
||||
continue;
|
||||
throw SysError("opening %1%", fdStr);
|
||||
}
|
||||
struct dirent * fd_ent;
|
||||
while (errno = 0, fd_ent = readdir(fdDir.get())) {
|
||||
if (fd_ent->d_name[0] != '.')
|
||||
readProcLink(fmt("%s/%s", fdStr, fd_ent->d_name), unchecked);
|
||||
}
|
||||
if (errno) {
|
||||
if (errno == ESRCH)
|
||||
continue;
|
||||
throw SysError("iterating /proc/%1%/fd", ent->d_name);
|
||||
}
|
||||
fdDir.reset();
|
||||
|
||||
try {
|
||||
auto mapFile = fmt("/proc/%s/maps", ent->d_name);
|
||||
auto mapLines = tokenizeString<std::vector<std::string>>(readFile(mapFile), "\n");
|
||||
for (const auto & line : mapLines) {
|
||||
auto match = std::smatch{};
|
||||
if (std::regex_match(line, match, mapRegex))
|
||||
unchecked[match[1]].emplace(mapFile);
|
||||
}
|
||||
|
||||
auto envFile = fmt("/proc/%s/environ", ent->d_name);
|
||||
auto envString = readFile(envFile);
|
||||
auto env_end = std::sregex_iterator{};
|
||||
for (auto i = std::sregex_iterator{envString.begin(), envString.end(), storePathRegex}; i != env_end; ++i)
|
||||
unchecked[i->str()].emplace(envFile);
|
||||
} catch (SysError & e) {
|
||||
if (errno == ENOENT || errno == EACCES || errno == ESRCH)
|
||||
continue;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errno)
|
||||
throw SysError("iterating /proc");
|
||||
}
|
||||
|
||||
#if !defined(__linux__)
|
||||
// lsof is really slow on OS X. This actually causes the gc-concurrent.sh test to fail.
|
||||
// See: https://github.com/NixOS/nix/issues/3011
|
||||
// Because of this we disable lsof when running the tests.
|
||||
if (getEnv("_NIX_TEST_NO_LSOF") != "1") {
|
||||
try {
|
||||
std::regex lsofRegex(R"(^n(/.*)$)");
|
||||
auto lsofLines =
|
||||
tokenizeString<std::vector<std::string>>(runProgram(LSOF, true, { "-n", "-w", "-F", "n" }), "\n");
|
||||
for (const auto & line : lsofLines) {
|
||||
std::smatch match;
|
||||
if (std::regex_match(line, match, lsofRegex))
|
||||
unchecked[match[1]].emplace("{lsof}");
|
||||
}
|
||||
} catch (ExecError & e) {
|
||||
/* lsof not installed, lsof failed */
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if __linux__
|
||||
readFileRoots("/proc/sys/kernel/modprobe", unchecked);
|
||||
readFileRoots("/proc/sys/kernel/fbsplash", unchecked);
|
||||
readFileRoots("/proc/sys/kernel/poweroff_cmd", unchecked);
|
||||
#endif
|
||||
|
||||
for (auto & [target, links] : unchecked) {
|
||||
if (!isInStore(target)) continue;
|
||||
try {
|
||||
auto path = toStorePath(target).first;
|
||||
if (!isValidPath(path)) continue;
|
||||
debug("got additional root '%1%'", printStorePath(path));
|
||||
if (censor)
|
||||
roots[path].insert(censored);
|
||||
else
|
||||
roots[path].insert(links.begin(), links.end());
|
||||
} catch (BadStorePath &) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct GCLimitReached { };
|
||||
|
||||
|
||||
void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
|
||||
{
|
||||
bool shouldDelete = options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcDeleteSpecific;
|
||||
@@ -491,7 +346,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
|
||||
FdLock gcLock(fdGCLock.get(), ltWrite, true, "waiting for the big garbage collector lock...");
|
||||
|
||||
/* Start the server for receiving new roots. */
|
||||
auto socketPath = stateDir.get() + gcSocketPath;
|
||||
auto socketPath = stateDir.get() + rootsSocketPath;
|
||||
createDirs(dirOf(socketPath));
|
||||
auto fdServer = createUnixDomainSocket(socketPath, 0666);
|
||||
|
||||
@@ -599,7 +454,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
|
||||
printInfo("finding garbage collector roots...");
|
||||
Roots rootMap;
|
||||
if (!options.ignoreLiveness)
|
||||
findRootsNoTemp(rootMap, true);
|
||||
rootMap = findRoots(true);
|
||||
|
||||
for (auto & i : rootMap) roots.insert(i.first);
|
||||
|
||||
|
||||
@@ -94,6 +94,13 @@ public:
|
||||
Setting<std::string> storeUri{this, getEnv("NIX_REMOTE").value_or("auto"), "store",
|
||||
"The default Nix store to use."};
|
||||
|
||||
Setting<std::string> gcSocketPath {
|
||||
this,
|
||||
"auto",
|
||||
"gc-socket-path",
|
||||
"Path to the socket used to communicate with an external GC."
|
||||
};
|
||||
|
||||
Setting<bool> keepFailed{this, false, "keep-failed",
|
||||
"Whether to keep temporary directories of failed builds."};
|
||||
|
||||
|
||||
@@ -185,8 +185,6 @@ LocalStore::LocalStore(const Params & params)
|
||||
throw SysError("could not set permissions on '%s' to 755", perUserDir);
|
||||
}
|
||||
|
||||
createUser(getUserName(), getuid());
|
||||
|
||||
/* Optionally, create directories and set permissions for a
|
||||
multi-user install. */
|
||||
if (getuid() == 0 && settings.buildUsersGroup != "") {
|
||||
@@ -1786,20 +1784,6 @@ void LocalStore::signPathInfo(ValidPathInfo & info)
|
||||
}
|
||||
|
||||
|
||||
void LocalStore::createUser(const std::string & userName, uid_t userId)
|
||||
{
|
||||
for (auto & dir : {
|
||||
fmt("%s/profiles/per-user/%s", stateDir, userName),
|
||||
fmt("%s/gcroots/per-user/%s", stateDir, userName)
|
||||
}) {
|
||||
createDirs(dir);
|
||||
if (chmod(dir.c_str(), 0755) == -1)
|
||||
throw SysError("changing permissions of directory '%s'", dir);
|
||||
if (chown(dir.c_str(), userId, getgid()) == -1)
|
||||
throw SysError("changing owner of directory '%s'", dir);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<int64_t, Realisation>> LocalStore::queryRealisationCore_(
|
||||
LocalStore::State & state,
|
||||
const DrvOutput & id)
|
||||
|
||||
@@ -254,7 +254,7 @@ private:
|
||||
|
||||
void findRootsNoTemp(Roots & roots, bool censor);
|
||||
|
||||
void findRuntimeRoots(Roots & roots, bool censor);
|
||||
void findRootsNoTempNoExternalDaemon(Roots & roots, bool censor);
|
||||
|
||||
Path createTempDirInStore();
|
||||
|
||||
@@ -275,8 +275,6 @@ private:
|
||||
void signPathInfo(ValidPathInfo & info);
|
||||
void signRealisation(Realisation &);
|
||||
|
||||
void createUser(const std::string & userName, uid_t userId) override;
|
||||
|
||||
// XXX: Make a generic `Store` method
|
||||
FixedOutputHash hashCAPath(
|
||||
const FileIngestionMethod & method,
|
||||
|
||||
@@ -6,7 +6,7 @@ libstore_DIR := $(d)
|
||||
|
||||
libstore_SOURCES := $(wildcard $(d)/*.cc $(d)/builtins/*.cc $(d)/build/*.cc)
|
||||
|
||||
libstore_LIBS = libutil
|
||||
libstore_LIBS = libutil libfindroots
|
||||
|
||||
libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) $(SODIUM_LIBS) -pthread
|
||||
ifdef HOST_LINUX
|
||||
@@ -32,7 +32,7 @@ ifeq ($(HAVE_SECCOMP), 1)
|
||||
endif
|
||||
|
||||
libstore_CXXFLAGS += \
|
||||
-I src/libutil -I src/libstore -I src/libstore/build \
|
||||
-I src/libutil -I src/libstore -I src/libstore/build -I src/nix-find-roots/lib \
|
||||
-DNIX_PREFIX=\"$(prefix)\" \
|
||||
-DNIX_STORE_DIR=\"$(storedir)\" \
|
||||
-DNIX_DATA_DIR=\"$(datadir)\" \
|
||||
|
||||
@@ -280,16 +280,42 @@ std::string optimisticLockProfile(const Path & profile)
|
||||
}
|
||||
|
||||
|
||||
Path profilesDir()
|
||||
{
|
||||
auto profileRoot = getDataDir() + "/nix/profiles";
|
||||
createDirs(profileRoot);
|
||||
return profileRoot;
|
||||
}
|
||||
|
||||
|
||||
Path getDefaultProfile()
|
||||
{
|
||||
Path profileLink = getHome() + "/.nix-profile";
|
||||
try {
|
||||
if (!pathExists(profileLink)) {
|
||||
replaceSymlink(
|
||||
getuid() == 0
|
||||
? settings.nixStateDir + "/profiles/default"
|
||||
: fmt("%s/profiles/per-user/%s/profile", settings.nixStateDir, getUserName()),
|
||||
profileLink);
|
||||
// Migrate from the “old-style” profiles stored under `/nix/var`:
|
||||
// If the link exists and points to the old location, then:
|
||||
// - Rewrite it to point to the new location
|
||||
// - For every generation of the old default profile, create a symlink
|
||||
// from the new directory to it (so that all the previous profiles
|
||||
// and generations are still available).
|
||||
auto legacyProfile = getuid() == 0
|
||||
? settings.nixStateDir + "/profiles/default"
|
||||
: fmt("%s/profiles/per-user/%s/profile", settings.nixStateDir, getUserName());
|
||||
auto newProfile = profilesDir() + "/profile";
|
||||
if (!pathExists(profileLink)
|
||||
|| (isLink(profileLink)
|
||||
&& readLink(profileLink) == legacyProfile)) {
|
||||
warn("Migrating the default profile");
|
||||
replaceSymlink(newProfile, profileLink);
|
||||
replaceSymlink(legacyProfile, newProfile);
|
||||
if (pathExists(legacyProfile)) {
|
||||
for (auto & oldGen : findGenerations(legacyProfile).first) {
|
||||
replaceSymlink(
|
||||
oldGen.path,
|
||||
dirOf(newProfile) + "/"
|
||||
+ std::string(baseNameOf(oldGen.path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return absPath(readLink(profileLink), dirOf(profileLink));
|
||||
} catch (Error &) {
|
||||
|
||||
@@ -68,6 +68,10 @@ void lockProfile(PathLocks & lock, const Path & profile);
|
||||
rebuilt. */
|
||||
std::string optimisticLockProfile(const Path & profile);
|
||||
|
||||
/* Creates and returns the path to a directory suitable for storing the user’s
|
||||
profiles. */
|
||||
Path profilesDir();
|
||||
|
||||
/* Resolve ~/.nix-profile. If ~/.nix-profile doesn't exist yet, create
|
||||
it. */
|
||||
Path getDefaultProfile();
|
||||
|
||||
@@ -630,9 +630,6 @@ public:
|
||||
return toRealPath(printStorePath(storePath));
|
||||
}
|
||||
|
||||
virtual void createUser(const std::string & userName, uid_t userId)
|
||||
{ }
|
||||
|
||||
/*
|
||||
* Synchronises the options of the client with those of the daemon
|
||||
* (a no-op when there’s no daemon)
|
||||
|
||||
@@ -11,6 +11,7 @@ std::map<ExperimentalFeature, std::string> stringifiedXpFeatures = {
|
||||
{ Xp::NixCommand, "nix-command" },
|
||||
{ Xp::RecursiveNix, "recursive-nix" },
|
||||
{ Xp::NoUrlLiterals, "no-url-literals" },
|
||||
{ Xp::ExternalGCDaemon, "external-gc-daemon" },
|
||||
};
|
||||
|
||||
const std::optional<ExperimentalFeature> parseExperimentalFeature(const std::string_view & name)
|
||||
|
||||
@@ -19,7 +19,8 @@ enum struct ExperimentalFeature
|
||||
Flakes,
|
||||
NixCommand,
|
||||
RecursiveNix,
|
||||
NoUrlLiterals
|
||||
NoUrlLiterals,
|
||||
ExternalGCDaemon,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -529,6 +529,16 @@ std::string getUserName()
|
||||
return name;
|
||||
}
|
||||
|
||||
Path getHomeOf(uid_t userId)
|
||||
{
|
||||
std::vector<char> buf(16384);
|
||||
struct passwd pwbuf;
|
||||
struct passwd * pw;
|
||||
if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0
|
||||
|| !pw || !pw->pw_dir || !pw->pw_dir[0])
|
||||
throw Error("cannot determine user's home directory");
|
||||
return pw->pw_dir;
|
||||
}
|
||||
|
||||
Path getHome()
|
||||
{
|
||||
@@ -536,13 +546,7 @@ Path getHome()
|
||||
{
|
||||
auto homeDir = getEnv("HOME");
|
||||
if (!homeDir) {
|
||||
std::vector<char> buf(16384);
|
||||
struct passwd pwbuf;
|
||||
struct passwd * pw;
|
||||
if (getpwuid_r(geteuid(), &pwbuf, buf.data(), buf.size(), &pw) != 0
|
||||
|| !pw || !pw->pw_dir || !pw->pw_dir[0])
|
||||
throw Error("cannot determine user's home directory");
|
||||
homeDir = pw->pw_dir;
|
||||
homeDir = getHomeOf(geteuid());
|
||||
}
|
||||
return *homeDir;
|
||||
}();
|
||||
@@ -1813,19 +1817,32 @@ void connect(int fd, const std::string & path)
|
||||
|
||||
if (path.size() + 1 >= sizeof(addr.sun_path)) {
|
||||
Pid pid = startProcess([&]() {
|
||||
Path dir = dirOf(path);
|
||||
if (chdir(dir.c_str()) == -1)
|
||||
throw SysError("chdir to '%s' failed", dir);
|
||||
std::string base(baseNameOf(path));
|
||||
if (base.size() + 1 >= sizeof(addr.sun_path))
|
||||
throw Error("socket path '%s' is too long", base);
|
||||
memcpy(addr.sun_path, base.c_str(), base.size() + 1);
|
||||
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1)
|
||||
throw SysError("cannot connect to socket at '%s'", path);
|
||||
_exit(0);
|
||||
try {
|
||||
Path dir = dirOf(path);
|
||||
if (chdir(dir.c_str()) == -1)
|
||||
throw SysError("chdir to '%s' failed", dir);
|
||||
std::string base(baseNameOf(path));
|
||||
if (base.size() + 1 >= sizeof(addr.sun_path))
|
||||
throw Error("socket path '%s' is too long", base);
|
||||
memcpy(addr.sun_path, base.c_str(), base.size() + 1);
|
||||
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1)
|
||||
throw SysError("cannot connect to socket at '%s'", path);
|
||||
_exit(0);
|
||||
} catch (SysError & e) {
|
||||
// If this raised a `SysError`, we want to rethrow it in the
|
||||
// parent process.
|
||||
// For that, we need to transmit the associated error code,
|
||||
// which we do by setting it as the return code for the process.
|
||||
_exit(e.errNo);
|
||||
} catch (std::exception &) {
|
||||
_exit(-1);
|
||||
}
|
||||
});
|
||||
int status = pid.wait();
|
||||
if (status != 0)
|
||||
if (status > 0) {
|
||||
errno = status;
|
||||
throw SysError("cannot connect to socket at '%s'", path);
|
||||
} else if (status < 0)
|
||||
throw Error("cannot connect to socket at '%s'", path);
|
||||
} else {
|
||||
memcpy(addr.sun_path, path.c_str(), path.size() + 1);
|
||||
|
||||
@@ -130,6 +130,9 @@ void deletePath(const Path & path, uint64_t & bytesFreed);
|
||||
|
||||
std::string getUserName();
|
||||
|
||||
/* Return the given user's home directory from /etc/passwd. */
|
||||
Path getHomeOf(uid_t userId);
|
||||
|
||||
/* Return $HOME or the user's home directory from /etc/passwd. */
|
||||
Path getHome();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "profiles.hh"
|
||||
#include "shared.hh"
|
||||
#include "globals.hh"
|
||||
#include "filetransfer.hh"
|
||||
#include "store-api.hh"
|
||||
#include "legacy.hh"
|
||||
#include "fetchers.hh"
|
||||
#include "util.hh"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <regex>
|
||||
@@ -166,7 +168,7 @@ static int main_nix_channel(int argc, char ** argv)
|
||||
nixDefExpr = home + "/.nix-defexpr";
|
||||
|
||||
// Figure out the name of the channels profile.
|
||||
profile = fmt("%s/profiles/per-user/%s/channels", settings.nixStateDir, getUserName());
|
||||
profile = profilesDir() + "/channels";
|
||||
|
||||
enum {
|
||||
cNone,
|
||||
|
||||
@@ -105,8 +105,10 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
|
||||
/* Also write a copy of the list of user environment elements to
|
||||
the store; we need it for future modifications of the
|
||||
environment. */
|
||||
std::ostringstream str;
|
||||
manifest.print(str, true);
|
||||
auto manifestFile = state.store->addTextToStore("env-manifest.nix",
|
||||
fmt("%s", manifest), references);
|
||||
str.str(), references);
|
||||
|
||||
/* Get the environment builder expression. */
|
||||
Value envBuilder;
|
||||
|
||||
1
src/nix-find-roots/.gitignore
vendored
Normal file
1
src/nix-find-roots/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
nix-find-roots
|
||||
234
src/nix-find-roots/lib/find-roots.cc
Normal file
234
src/nix-find-roots/lib/find-roots.cc
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* A very simple utility to trace all the gc roots through the file-system
|
||||
* The reason for this program is that tracing these roots is the only part of
|
||||
* Nix that requires to run as root (because it requires reading through the
|
||||
* user home directories to resolve the indirect roots)
|
||||
*
|
||||
* This program intentionnally doesnt depend on any Nix library to reduce the attack surface.
|
||||
*/
|
||||
|
||||
#include <regex>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
#include "find-roots.hh"
|
||||
|
||||
|
||||
namespace nix::roots_tracer {
|
||||
namespace fs = std::filesystem;
|
||||
using std::set, std::string;
|
||||
|
||||
string quoteRegexChars(const string & raw)
|
||||
{
|
||||
static auto specialRegex = std::regex(R"([.^$\\*+?()\[\]{}|])");
|
||||
return std::regex_replace(raw, specialRegex, R"(\$&)");
|
||||
}
|
||||
std::regex storePathRegex(const fs::path storeDir)
|
||||
{
|
||||
return std::regex(quoteRegexChars(storeDir) + R"(/[0-9a-z]+[0-9a-zA-Z\+\-\._\?=]*)");
|
||||
}
|
||||
|
||||
bool isInStore(fs::path storeDir, fs::path dir)
|
||||
{
|
||||
return (std::search(dir.begin(), dir.end(), storeDir.begin(), storeDir.end()) == dir.begin());
|
||||
}
|
||||
|
||||
void traceStaticRoot(
|
||||
const TracerConfig & opts,
|
||||
int recursionsLeft,
|
||||
TraceResult & res,
|
||||
const fs::path & root,
|
||||
const fs::file_status & status,
|
||||
const std::optional<const fs::path> & parent = std::nullopt
|
||||
)
|
||||
{
|
||||
opts.debug("Considering file " + root.string());
|
||||
|
||||
if (recursionsLeft < 0)
|
||||
return;
|
||||
|
||||
switch (status.type()) {
|
||||
case fs::file_type::directory:
|
||||
{
|
||||
auto directory_iterator = fs::recursive_directory_iterator(root);
|
||||
for (auto & child : directory_iterator)
|
||||
traceStaticRoot(opts, recursionsLeft, res, child.path(), child.symlink_status());
|
||||
break;
|
||||
}
|
||||
case fs::file_type::symlink:
|
||||
{
|
||||
auto target = root.parent_path() / fs::read_symlink(root);
|
||||
auto not_found = [&](std::string msg) {
|
||||
opts.debug("Error accessing the file " + target.string() + ": " + msg);
|
||||
opts.debug("(When resolving the symlink " + root.string() + ")");
|
||||
res.deadLinks.insert(root);
|
||||
};
|
||||
try {
|
||||
auto target_status = fs::symlink_status(target);
|
||||
if (target_status.type() == fs::file_type::not_found)
|
||||
not_found("Not found");
|
||||
|
||||
if (isInStore(opts.storeDir, target)) {
|
||||
res.storeRoots[target].insert(root);
|
||||
return;
|
||||
} else {
|
||||
traceStaticRoot(opts, recursionsLeft - 1, res, target, target_status);
|
||||
}
|
||||
|
||||
} catch (fs::filesystem_error & e) {
|
||||
not_found(e.what());
|
||||
}
|
||||
}
|
||||
case fs::file_type::regular:
|
||||
{
|
||||
auto possibleStorePath = opts.storeDir / root.filename();
|
||||
if (fs::exists(possibleStorePath))
|
||||
res.storeRoots[possibleStorePath].insert(root);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void traceStaticRoot(
|
||||
const TracerConfig & opts,
|
||||
int recursionsLeft,
|
||||
TraceResult & res,
|
||||
const fs::path & root)
|
||||
{
|
||||
try {
|
||||
auto status = fs::symlink_status(root);
|
||||
traceStaticRoot(opts, recursionsLeft, res, root, status);
|
||||
} catch (fs::filesystem_error & e) {
|
||||
opts.debug("Error accessing the file " + root.string() + ": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the set of all the store paths that are reachable from the given set
|
||||
* of filesystem paths, by:
|
||||
* - descending into the directories
|
||||
* - following the symbolic links (at most twice)
|
||||
* - reading the name of regular files (when encountering a file
|
||||
* `/foo/bar/abcdef`, the algorithm will try to access `/nix/store/abcdef`)
|
||||
*
|
||||
* Also returns the set of all dead links encountered during the process (so
|
||||
* that they can be removed if it makes sense).
|
||||
*/
|
||||
TraceResult traceStaticRoots(TracerConfig opts, set<fs::path> roots)
|
||||
{
|
||||
int maxRecursionLevel = 2;
|
||||
TraceResult res;
|
||||
for (auto & root : roots)
|
||||
traceStaticRoot(opts, maxRecursionLevel, res, root);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the content of the given file for al the occurences of something that looks
|
||||
* like a store path (i.e. that matches `storePathRegex(opts.storeDir)`) and add them
|
||||
* to `res`
|
||||
*/
|
||||
void scanFileContent(const TracerConfig & opts, const fs::path & fileToScan, Roots & res)
|
||||
{
|
||||
if (!fs::exists(fileToScan))
|
||||
return;
|
||||
|
||||
std::ostringstream contentStream;
|
||||
{
|
||||
std::ifstream fs;
|
||||
fs.open(fileToScan);
|
||||
fs >> contentStream.rdbuf();
|
||||
}
|
||||
std::string content = contentStream.str();
|
||||
auto regex = storePathRegex(opts.storeDir);
|
||||
auto firstMatch
|
||||
= std::sregex_iterator { content.begin(), content.end(), regex };
|
||||
auto fileEnd = std::sregex_iterator{};
|
||||
for (auto i = firstMatch; i != fileEnd; ++i)
|
||||
res[i->str()].emplace(fileToScan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the content of a `/proc/[pid]/maps` file for regions that are mmaped to
|
||||
* a store path
|
||||
*/
|
||||
void scanMapsFile(const TracerConfig & opts, const fs::path & mapsFile, Roots & res)
|
||||
{
|
||||
if (!fs::exists(mapsFile))
|
||||
return;
|
||||
|
||||
static auto mapRegex = std::regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)");
|
||||
std::stringstream mappedFile;
|
||||
{
|
||||
std::ifstream fs;
|
||||
fs.open(mapsFile);
|
||||
fs >> mappedFile.rdbuf();
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(mappedFile, line)) {
|
||||
auto match = std::smatch{};
|
||||
if (std::regex_match(line, match, mapRegex)) {
|
||||
auto matchedPath = fs::path(match[1]);
|
||||
if (isInStore(opts.storeDir, matchedPath))
|
||||
res[fs::path(match[1])].emplace(mapsFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Roots getRuntimeRoots(TracerConfig opts)
|
||||
{
|
||||
auto procDir = fs::path("/proc");
|
||||
if (!fs::exists(procDir))
|
||||
return {};
|
||||
Roots res;
|
||||
auto digitsRegex = std::regex(R"(^\d+$)");
|
||||
for (auto & procEntry : fs::directory_iterator(procDir)) {
|
||||
// Only the directories whose name is a sequence of digits represent
|
||||
// pids
|
||||
if (!std::regex_match(procEntry.path().filename().string(), digitsRegex)
|
||||
|| !procEntry.is_directory())
|
||||
continue;
|
||||
|
||||
opts.debug("Considering path " + procEntry.path().string());
|
||||
|
||||
// A set of paths used by the executable and possibly symlinks to a
|
||||
// path in the store
|
||||
set<fs::path> pathsToConsider;
|
||||
pathsToConsider.insert(procEntry.path()/"exe");
|
||||
pathsToConsider.insert(procEntry.path()/"cwd");
|
||||
try {
|
||||
auto fdDir = procEntry.path()/"fd";
|
||||
for (auto & fdFile : fs::directory_iterator(fdDir))
|
||||
pathsToConsider.insert(fdFile.path());
|
||||
} catch (fs::filesystem_error & e) {
|
||||
if (e.code().value() != ENOENT && e.code().value() != EACCES)
|
||||
throw;
|
||||
}
|
||||
for (auto & path : pathsToConsider) try {
|
||||
auto realPath = fs::read_symlink(path);
|
||||
if (isInStore(opts.storeDir, realPath))
|
||||
res[realPath].insert(path);
|
||||
} catch (fs::filesystem_error &e) {
|
||||
opts.debug(e.what());
|
||||
}
|
||||
|
||||
// Scan the environment of the executable
|
||||
scanFileContent(opts, procEntry.path()/"environ", res);
|
||||
scanMapsFile(opts, procEntry.path()/"maps", res);
|
||||
}
|
||||
|
||||
// Mostly useful for NixOS, but doesn’t hurt to check on other systems
|
||||
// anyways
|
||||
scanFileContent(opts, "/proc/sys/kernel/modprobe", res);
|
||||
scanFileContent(opts, "/proc/sys/kernel/fbsplash", res);
|
||||
scanFileContent(opts, "/proc/sys/kernel/poweroff_cmd", res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
38
src/nix-find-roots/lib/find-roots.hh
Normal file
38
src/nix-find-roots/lib/find-roots.hh
Normal file
@@ -0,0 +1,38 @@
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
namespace nix::roots_tracer {
|
||||
namespace fs = std::filesystem;
|
||||
using std::set, std::map, std::string;
|
||||
|
||||
class Error : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
inline void logNone(std::string_view)
|
||||
{ }
|
||||
|
||||
struct TracerConfig {
|
||||
const fs::path storeDir = "/nix/store";
|
||||
const fs::path stateDir = "/nix/var/nix";
|
||||
const fs::path socketPath = "/nix/var/nix/gc-socket/socket";
|
||||
|
||||
std::function<void(std::string_view msg)> log = logNone;
|
||||
std::function<void(std::string_view msg)> debug = logNone;
|
||||
};
|
||||
|
||||
/*
|
||||
* A value of type `Roots` is a mapping from a store path to the set of roots that keep it alive
|
||||
*/
|
||||
typedef map<fs::path, std::set<fs::path>> Roots;
|
||||
struct TraceResult {
|
||||
Roots storeRoots;
|
||||
set<fs::path> deadLinks;
|
||||
};
|
||||
|
||||
TraceResult traceStaticRoots(TracerConfig opts, set<fs::path> initialRoots);
|
||||
Roots getRuntimeRoots(TracerConfig opts);
|
||||
}
|
||||
24
src/nix-find-roots/local.mk
Normal file
24
src/nix-find-roots/local.mk
Normal file
@@ -0,0 +1,24 @@
|
||||
libraries += libfindroots
|
||||
|
||||
libfindroots_NAME = libnixfindroots
|
||||
|
||||
libfindroots_DIR := $(d)/lib
|
||||
|
||||
libfindroots_SOURCES := $(wildcard $(d)/lib/*.cc)
|
||||
|
||||
ifdef HOST_DARWIN
|
||||
libfindroots_LDFLAGS += -lc++fs
|
||||
endif
|
||||
|
||||
programs += nix-find-roots
|
||||
|
||||
nix-find-roots_DIR := $(d)
|
||||
|
||||
nix-find-roots_SOURCES := $(d)/main.cc
|
||||
|
||||
nix-find-roots_LIBS := libfindroots
|
||||
|
||||
nix-find-roots_CXXFLAGS += \
|
||||
-I src/nix-find-roots/lib
|
||||
|
||||
nix-find-roots_INSTALL_DIR := $(libexecdir)/nix
|
||||
190
src/nix-find-roots/main.cc
Normal file
190
src/nix-find-roots/main.cc
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "find-roots.hh"
|
||||
#include <getopt.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <signal.h>
|
||||
|
||||
using namespace nix::roots_tracer;
|
||||
|
||||
void logStderr(std::string_view msg)
|
||||
{
|
||||
std::cerr << msg << std::endl;
|
||||
}
|
||||
|
||||
TracerConfig parseCmdLine(int argc, char** argv)
|
||||
{
|
||||
std::function<void(std::string_view msg)> log = logStderr;
|
||||
std::function<void(std::string_view msg)> debug = logNone;
|
||||
fs::path storeDir = "/nix/store";
|
||||
fs::path stateDir = "/nix/var/nix";
|
||||
fs::path socketPath = "/nix/var/nix/gc-trace-socket/socket";
|
||||
|
||||
auto usage = [&]() {
|
||||
std::string programName = argc > 0 ? argv[0] : "nix-find-roots";
|
||||
std::cerr << "Usage: " << programName << " [--verbose|-v] [-s storeDir] [-d stateDir] [-l socketPath]" << std::endl;
|
||||
exit(1);
|
||||
};
|
||||
static struct option long_options[] = {
|
||||
{ "verbose", no_argument, 0, 'v' },
|
||||
{ "socket_path", required_argument, 0, 'l' },
|
||||
{ "store_dir", required_argument, 0, 's' },
|
||||
{ "state_dir", required_argument, 0, 'd' },
|
||||
{ "help", no_argument, 0, 'h' },
|
||||
{ 0, 0, 0, 0 },
|
||||
};
|
||||
|
||||
int option_index = 0;
|
||||
int opt_char;
|
||||
while((opt_char = getopt_long(argc, argv, "vd:s:l:h",
|
||||
long_options, &option_index)) != -1) {
|
||||
switch (opt_char) {
|
||||
case 0:
|
||||
break;
|
||||
break;
|
||||
case '?':
|
||||
case 'h':
|
||||
usage();
|
||||
break;
|
||||
case 'v':
|
||||
debug = logStderr;
|
||||
break;
|
||||
case 's':
|
||||
storeDir = fs::path(optarg);
|
||||
break;
|
||||
case 'd':
|
||||
stateDir = fs::path(optarg);
|
||||
break;
|
||||
case 'l':
|
||||
socketPath = fs::path(optarg);
|
||||
break;
|
||||
default:
|
||||
std::cerr << "Got invalid char: " << (char)opt_char << std::endl;
|
||||
abort();
|
||||
}
|
||||
};
|
||||
return TracerConfig {
|
||||
.storeDir = storeDir,
|
||||
.stateDir = stateDir,
|
||||
.socketPath = socketPath,
|
||||
.debug = debug,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `original` with every newline or tab character escaped
|
||||
*/
|
||||
std::string escape(std::string original)
|
||||
{
|
||||
map<string, string> replacements = {
|
||||
{"\n", "\\n"},
|
||||
{"\t", "\\t"},
|
||||
};
|
||||
for (auto [oldStr, newStr] : replacements) {
|
||||
size_t currentPos = 0;
|
||||
while ((currentPos = original.find(oldStr, currentPos)) != std::string::npos) {
|
||||
original.replace(currentPos, oldStr.length(), newStr);
|
||||
currentPos += newStr.length();
|
||||
}
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
#define SD_LISTEN_FDS_START 3 // Like in systemd
|
||||
|
||||
int main(int argc, char * * argv)
|
||||
{
|
||||
const TracerConfig opts = parseCmdLine(argc, argv);
|
||||
const set<fs::path> standardRoots = {
|
||||
opts.stateDir / fs::path("profiles"),
|
||||
opts.stateDir / fs::path("gcroots"),
|
||||
};
|
||||
|
||||
int mySock;
|
||||
|
||||
// Handle socket-based activation by systemd.
|
||||
auto rawListenFds = std::getenv("LISTEN_FDS");
|
||||
if (rawListenFds) {
|
||||
auto listenFds = std::string(rawListenFds);
|
||||
auto listenPid = std::getenv("LISTEN_PID");
|
||||
if (listenPid == nullptr || listenPid != std::to_string(getpid()) || listenFds != "1")
|
||||
throw Error("unexpected systemd environment variables");
|
||||
mySock = SD_LISTEN_FDS_START;
|
||||
} else {
|
||||
mySock = socket(PF_UNIX, SOCK_STREAM, 0);
|
||||
if (mySock == -1) {
|
||||
throw Error(std::string("Cannot create Unix domain socket, got") +
|
||||
std::strerror(errno));
|
||||
}
|
||||
struct sockaddr_un addr;
|
||||
addr.sun_family = AF_UNIX;
|
||||
|
||||
auto socketDir = opts.socketPath.parent_path();
|
||||
auto socketFilename = opts.socketPath.filename();
|
||||
if (socketFilename.string().size() > sizeof(addr.sun_path))
|
||||
throw Error(
|
||||
"Socket filename " + socketFilename.string() +
|
||||
" is too long, should be at most " +
|
||||
std::to_string(sizeof(addr.sun_path))
|
||||
);
|
||||
chdir(socketDir.c_str());
|
||||
|
||||
fs::remove(socketFilename);
|
||||
if (socketFilename.string().size() + 1 >= sizeof(addr.sun_path))
|
||||
throw Error("socket path '" + socketFilename.string() + "' is too long");
|
||||
strcpy(addr.sun_path, socketFilename.c_str());
|
||||
if (bind(mySock, (struct sockaddr*) &addr, sizeof(addr)) == -1) {
|
||||
throw Error("Cannot bind to socket");
|
||||
}
|
||||
|
||||
if (listen(mySock, 5) == -1)
|
||||
throw Error("cannot listen on socket " + opts.socketPath.string());
|
||||
}
|
||||
|
||||
// Ignore SIGPIPE so that an interrupted connection doesn’t stop the daemon
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_un remoteAddr;
|
||||
socklen_t remoteAddrLen = sizeof(remoteAddr);
|
||||
int remoteSocket = accept(
|
||||
mySock,
|
||||
(struct sockaddr*) & remoteAddr,
|
||||
&remoteAddrLen
|
||||
);
|
||||
|
||||
if (remoteSocket == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
throw Error("Error accepting the connection");
|
||||
}
|
||||
|
||||
opts.log("accepted connection");
|
||||
|
||||
auto printToSocket = [&](std::string_view s) {
|
||||
send(remoteSocket, s.data(), s.size(), 0);
|
||||
};
|
||||
|
||||
auto traceResult = traceStaticRoots(opts, standardRoots);
|
||||
auto runtimeRoots = getRuntimeRoots(opts);
|
||||
traceResult.storeRoots.insert(runtimeRoots.begin(), runtimeRoots.end());
|
||||
for (auto & [rootInStore, externalRoots] : traceResult.storeRoots) {
|
||||
for (auto & externalRoot : externalRoots) {
|
||||
printToSocket(escape(rootInStore.string()));
|
||||
printToSocket("\t");
|
||||
printToSocket(escape(externalRoot.string()));
|
||||
printToSocket("\n");
|
||||
}
|
||||
|
||||
}
|
||||
printToSocket("\n");
|
||||
for (auto & deadLink : traceResult.deadLinks) {
|
||||
printToSocket(escape(deadLink.string()));
|
||||
printToSocket("\n");
|
||||
}
|
||||
|
||||
close(remoteSocket);
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,6 @@ static void daemonLoop()
|
||||
querySetting("build-users-group", "") == "")
|
||||
throw Error("if you run 'nix-daemon' as root, then you MUST set 'build-users-group'!");
|
||||
#endif
|
||||
store.createUser(user, peer.uid);
|
||||
});
|
||||
|
||||
exit(0);
|
||||
|
||||
@@ -60,13 +60,13 @@ readLink() {
|
||||
}
|
||||
|
||||
clearProfiles() {
|
||||
profiles="$NIX_STATE_DIR"/profiles
|
||||
profiles="$HOME"/.local/share/nix/profiles
|
||||
rm -rf $profiles
|
||||
}
|
||||
|
||||
clearStore() {
|
||||
echo "clearing store..."
|
||||
chmod -R +w "$NIX_STORE_DIR"
|
||||
chmod -R +w "$NIX_STORE_DIR" || true
|
||||
rm -rf "$NIX_STORE_DIR"
|
||||
mkdir "$NIX_STORE_DIR"
|
||||
rm -rf "$NIX_STATE_DIR"
|
||||
@@ -82,6 +82,15 @@ clearCacheCache() {
|
||||
rm -f $TEST_HOME/.cache/nix/binary-cache*
|
||||
}
|
||||
|
||||
declare -A trapFunctions
|
||||
|
||||
callTraps() {
|
||||
for f in "${trapFunctions[@]}"; do
|
||||
$f
|
||||
done
|
||||
}
|
||||
trap callTraps EXIT
|
||||
|
||||
startDaemon() {
|
||||
# Don’t start the daemon twice, as this would just make it loop indefinitely
|
||||
if [[ "$NIX_REMOTE" == daemon ]]; then
|
||||
@@ -101,7 +110,7 @@ startDaemon() {
|
||||
if [[ -z ${DAEMON_STARTED+x} ]]; then
|
||||
fail "Didn’t manage to start the daemon"
|
||||
fi
|
||||
trap "killDaemon" EXIT
|
||||
trapFunctions[killDaemon]=killDaemon
|
||||
export NIX_REMOTE=daemon
|
||||
}
|
||||
|
||||
@@ -113,7 +122,7 @@ killDaemon() {
|
||||
done
|
||||
kill -9 $pidDaemon 2> /dev/null || true
|
||||
wait $pidDaemon || true
|
||||
trap "" EXIT
|
||||
trapFunctions[killDaemon]=:
|
||||
}
|
||||
|
||||
restartDaemon() {
|
||||
|
||||
41
tests/gc-external-daemon.sh
Normal file
41
tests/gc-external-daemon.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
source common.sh
|
||||
|
||||
sed -i 's/experimental-features .*/& external-gc-daemon/' "$NIX_CONF_DIR"/nix.conf
|
||||
|
||||
export NIX_GC_SOCKET_PATH=$TEST_ROOT/gc.socket
|
||||
echo "gc-socket-path = $NIX_GC_SOCKET_PATH"
|
||||
|
||||
startGcDaemon() {
|
||||
# Start the daemon, wait for the socket to appear. !!!
|
||||
# ‘nix-daemon’ should have an option to fork into the background.
|
||||
rm -f $NIX_GC_SOCKET_PATH
|
||||
$(dirname $(command -v nix))/../libexec/nix/nix-find-roots \
|
||||
-l "$NIX_GC_SOCKET_PATH" \
|
||||
-d "$NIX_STATE_DIR" \
|
||||
-s "$NIX_STORE_DIR" \
|
||||
&
|
||||
for ((i = 0; i < 30; i++)); do
|
||||
if [[ -S $NIX_GC_SOCKET_PATH ]]; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
pidGcDaemon=$!
|
||||
trapFunctions[killGcDaemon]=killGcDaemon
|
||||
}
|
||||
|
||||
killGcDaemon() {
|
||||
kill $pidGcDaemon
|
||||
for i in {0..10}; do
|
||||
kill -0 $pidGcDaemon || break
|
||||
sleep 1
|
||||
done
|
||||
kill -9 $pidGcDaemon || true
|
||||
wait $pidGcDaemon || true
|
||||
trapFunctions[killGcDaemon]=:
|
||||
}
|
||||
|
||||
startGcDaemon
|
||||
|
||||
source ./gc.sh
|
||||
source ./gc-concurrent.sh
|
||||
source ./gc-runtime.sh
|
||||
source ./gc-auto.sh
|
||||
@@ -2,11 +2,13 @@ nix_tests = \
|
||||
flakes.sh \
|
||||
ca/gc.sh \
|
||||
gc.sh \
|
||||
gc-external-daemon.sh \
|
||||
remote-store.sh \
|
||||
lang.sh \
|
||||
fetchMercurial.sh \
|
||||
gc-auto.sh \
|
||||
user-envs.sh \
|
||||
user-envs-migration.sh \
|
||||
binary-cache.sh \
|
||||
multiple-outputs.sh \
|
||||
ca/build.sh \
|
||||
|
||||
@@ -30,7 +30,3 @@ NIX_REMOTE= nix-store --dump-db > $TEST_ROOT/d2
|
||||
cmp $TEST_ROOT/d1 $TEST_ROOT/d2
|
||||
|
||||
killDaemon
|
||||
|
||||
user=$(whoami)
|
||||
[ -e $NIX_STATE_DIR/gcroots/per-user/$user ]
|
||||
[ -e $NIX_STATE_DIR/profiles/per-user/$user ]
|
||||
|
||||
35
tests/user-envs-migration.sh
Normal file
35
tests/user-envs-migration.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
# Test that the migration of user environments
|
||||
# (https://github.com/NixOS/nix/pull/5226) does preserve everything
|
||||
|
||||
source common.sh
|
||||
|
||||
if isDaemonNewer "2.4pre20211005"; then
|
||||
exit 99
|
||||
fi
|
||||
|
||||
|
||||
killDaemon
|
||||
unset NIX_REMOTE
|
||||
|
||||
clearStore
|
||||
clearProfiles
|
||||
rm -rf ~/.nix-profile
|
||||
|
||||
# Fill the environment using the older Nix
|
||||
PATH_WITH_NEW_NIX="$PATH"
|
||||
export PATH="$NIX_DAEMON_PACKAGE/bin:$PATH"
|
||||
|
||||
nix-env -f user-envs.nix -i foo-1.0
|
||||
nix-env -f user-envs.nix -i bar-0.1
|
||||
|
||||
# Migrate to the new profile dir, and ensure that everything’s there
|
||||
export PATH="$PATH_WITH_NEW_NIX"
|
||||
nix-env -q # Trigger the migration
|
||||
( [[ -L ~/.nix-profile ]] && \
|
||||
[[ $(readlink ~/.nix-profile) == ~/.local/share/nix/profiles/profile ]] ) || \
|
||||
fail "The nix profile should point to the new location"
|
||||
|
||||
(nix-env -q | grep foo && nix-env -q | grep bar && \
|
||||
[[ -e ~/.nix-profile/bin/foo ]] && \
|
||||
[[ $(nix-env --list-generations | wc -l) == 2 ]]) ||
|
||||
fail "The nix profile should have the same content as before the migration"
|
||||
@@ -8,6 +8,8 @@ assert foo == "foo";
|
||||
|
||||
let
|
||||
|
||||
platforms = let x = "foobar"; in [ x x ];
|
||||
|
||||
makeDrv = name: progName: (mkDerivation {
|
||||
name = assert progName != "fail"; name;
|
||||
inherit progName system;
|
||||
@@ -15,6 +17,7 @@ let
|
||||
} // {
|
||||
meta = {
|
||||
description = "A silly test package with some \${escaped anti-quotation} in it";
|
||||
inherit platforms;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user