1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-11 13:06:01 +01:00

Split the root finding in a separate library

This commit is contained in:
Théophane Hufschmitt 2022-04-11 10:20:36 +02:00
parent bbde40af3e
commit fbec849281
5 changed files with 209 additions and 181 deletions

View file

@ -412,7 +412,7 @@
CXXFLAGS = prev.lib.optionalString prev.stdenv.hostPlatform.isStatic "-static"; CXXFLAGS = prev.lib.optionalString prev.stdenv.hostPlatform.isStatic "-static";
buildPhase = '' buildPhase = ''
$CXX $CXXFLAGS -std=c++17 nix-find-roots.cc -o nix-find-roots $CXX $CXXFLAGS -std=c++17 *.cc **/*.cc -I lib -o nix-find-roots
''; '';
installPhase = '' installPhase = ''

View file

@ -7,114 +7,19 @@
* This program intentionnally doesnt depend on any Nix library to reduce the attack surface. * This program intentionnally doesnt depend on any Nix library to reduce the attack surface.
*/ */
#include <filesystem>
#include <iostream>
#include <regex> #include <regex>
#include <set>
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
#include <getopt.h>
#include <fstream> #include <fstream>
#include <sys/socket.h>
#include <sys/un.h>
#include "find-roots.hh"
namespace nix::roots_tracer {
namespace fs = std::filesystem; namespace fs = std::filesystem;
using std::set, std::string; using std::set, std::string;
class Error : public std::exception {
private:
const string message;
public:
Error(std::string message)
: message(message)
{}
const char* what() const noexcept override
{
return message.c_str();
}
};
struct GlobalOpts {
fs::path storeDir = "/nix/store";
fs::path stateDir = "/nix/var/nix";
fs::path socketPath = "/nix/var/nix/gc-socket/socket";
enum VerbosityLvl {
Quiet = 0,
Normal = 1,
Verbose = 2
};
VerbosityLvl verbosity = Quiet;
};
void log(GlobalOpts::VerbosityLvl currentVerbosity, GlobalOpts::VerbosityLvl minVerbosity, std::string_view msg)
{
if (currentVerbosity < minVerbosity)
return;
std::cerr << msg << std::endl;
}
void debug(GlobalOpts::VerbosityLvl currentVerbosity, std::string_view msg)
{
log(currentVerbosity, GlobalOpts::Verbose, msg);
}
GlobalOpts parseCmdLine(int argc, char** argv)
{
GlobalOpts res;
auto usage = [&]() {
std::cerr << "Usage: " << string(argv[0]) << " [--verbose|-v] [-s storeDir] [-d stateDir] [-l socketPath]" << std::endl;
exit(1);
};
static struct option long_options[] = {
{ "verbose", no_argument, (int*)&res.verbosity, GlobalOpts::Verbose },
{ "socket_path", required_argument, 0, 'l' },
{ "store_dir", required_argument, 0, 's' },
{ "state_dir", required_argument, 0, 'd' },
};
int option_index = 0;
int opt_char;
while((opt_char = getopt_long(argc, argv, "vd:s:l:",
long_options, &option_index)) != -1) {
switch (opt_char) {
case 0:
break;
break;
case '?':
usage();
break;
case 'v':
res.verbosity = GlobalOpts::Verbose;
break;
case 's':
res.storeDir = fs::path(optarg);
break;
case 'd':
res.stateDir = fs::path(optarg);
break;
case 'l':
res.socketPath = fs::path(optarg);
break;
default:
std::cerr << "Got invalid char: " << (char)opt_char << std::endl;
abort();
}
};
return res;
}
/*
* A value of type `Roots` is a mapping from a store path to the set of roots that keep it alive
*/
typedef std::map<fs::path, std::set<fs::path>> Roots;
struct TraceResult {
Roots storeRoots;
set<fs::path> deadLinks;
};
string quoteRegexChars(const string & raw) string quoteRegexChars(const string & raw)
{ {
static auto specialRegex = std::regex(R"([.^$\\*+?()\[\]{}|])"); static auto specialRegex = std::regex(R"([.^$\\*+?()\[\]{}|])");
@ -130,8 +35,8 @@ bool isInStore(fs::path storeDir, fs::path dir)
return (std::search(dir.begin(), dir.end(), storeDir.begin(), storeDir.end()) == dir.begin()); return (std::search(dir.begin(), dir.end(), storeDir.begin(), storeDir.end()) == dir.begin());
} }
void followPathToStore( void traceStaticRoot(
const GlobalOpts & opts, const TracerConfig & opts,
int recursionsLeft, int recursionsLeft,
TraceResult & res, TraceResult & res,
const fs::path & root, const fs::path & root,
@ -139,7 +44,7 @@ void followPathToStore(
const std::optional<const fs::path> & parent = std::nullopt const std::optional<const fs::path> & parent = std::nullopt
) )
{ {
debug(opts.verbosity, "Considering file " + root.string()); opts.debug("Considering file " + root.string());
if (recursionsLeft < 0) if (recursionsLeft < 0)
return; return;
@ -149,15 +54,15 @@ void followPathToStore(
{ {
auto directory_iterator = fs::recursive_directory_iterator(root); auto directory_iterator = fs::recursive_directory_iterator(root);
for (auto & child : directory_iterator) for (auto & child : directory_iterator)
followPathToStore(opts, recursionsLeft, res, child.path(), child.symlink_status()); traceStaticRoot(opts, recursionsLeft, res, child.path(), child.symlink_status());
break; break;
} }
case fs::file_type::symlink: case fs::file_type::symlink:
{ {
auto target = root.parent_path() / fs::read_symlink(root); auto target = root.parent_path() / fs::read_symlink(root);
auto not_found = [&](std::string msg) { auto not_found = [&](std::string msg) {
debug(opts.verbosity, "Error accessing the file " + target.string() + ": " + msg); opts.debug("Error accessing the file " + target.string() + ": " + msg);
debug(opts.verbosity, "(When resolving the symlink " + root.string() + ")"); opts.debug("(When resolving the symlink " + root.string() + ")");
res.deadLinks.insert(root); res.deadLinks.insert(root);
}; };
try { try {
@ -169,7 +74,7 @@ void followPathToStore(
res.storeRoots[target].insert(root); res.storeRoots[target].insert(root);
return; return;
} else { } else {
followPathToStore(opts, recursionsLeft - 1, res, target, target_status); traceStaticRoot(opts, recursionsLeft - 1, res, target, target_status);
} }
} catch (fs::filesystem_error & e) { } catch (fs::filesystem_error & e) {
@ -187,17 +92,17 @@ void followPathToStore(
} }
} }
void followPathToStore( void traceStaticRoot(
const GlobalOpts & opts, const TracerConfig & opts,
int recursionsLeft, int recursionsLeft,
TraceResult & res, TraceResult & res,
const fs::path & root) const fs::path & root)
{ {
try { try {
auto status = fs::symlink_status(root); auto status = fs::symlink_status(root);
followPathToStore(opts, recursionsLeft, res, root, status); traceStaticRoot(opts, recursionsLeft, res, root, status);
} catch (fs::filesystem_error & e) { } catch (fs::filesystem_error & e) {
debug(opts.verbosity, "Error accessing the file " + root.string() + ": " + e.what()); opts.debug("Error accessing the file " + root.string() + ": " + e.what());
} }
} }
@ -212,12 +117,12 @@ void followPathToStore(
* Also returns the set of all dead links encountered during the process (so * Also returns the set of all dead links encountered during the process (so
* that they can be removed if it makes sense). * that they can be removed if it makes sense).
*/ */
TraceResult followPathsToStore(GlobalOpts opts, set<fs::path> roots) TraceResult traceStaticRoots(TracerConfig opts, set<fs::path> roots)
{ {
int maxRecursionLevel = 2; int maxRecursionLevel = 2;
TraceResult res; TraceResult res;
for (auto & root : roots) for (auto & root : roots)
followPathToStore(opts, maxRecursionLevel, res, root); traceStaticRoot(opts, maxRecursionLevel, res, root);
return res; return res;
} }
@ -226,7 +131,7 @@ TraceResult followPathsToStore(GlobalOpts opts, set<fs::path> roots)
* like a store path (i.e. that matches `storePathRegex(opts.storeDir)`) and add them * like a store path (i.e. that matches `storePathRegex(opts.storeDir)`) and add them
* to `res` * to `res`
*/ */
void scanFileContent(const GlobalOpts & opts, const fs::path & fileToScan, Roots & res) void scanFileContent(const TracerConfig & opts, const fs::path & fileToScan, Roots & res)
{ {
if (!fs::exists(fileToScan)) if (!fs::exists(fileToScan))
return; return;
@ -250,7 +155,7 @@ void scanFileContent(const GlobalOpts & opts, const fs::path & fileToScan, Roots
* Scan the content of a `/proc/[pid]/maps` file for regions that are mmaped to * Scan the content of a `/proc/[pid]/maps` file for regions that are mmaped to
* a store path * a store path
*/ */
void scanMapsFile(const GlobalOpts & opts, const fs::path & mapsFile, Roots & res) void scanMapsFile(const TracerConfig & opts, const fs::path & mapsFile, Roots & res)
{ {
if (!fs::exists(mapsFile)) if (!fs::exists(mapsFile))
return; return;
@ -274,7 +179,7 @@ void scanMapsFile(const GlobalOpts & opts, const fs::path & mapsFile, Roots & re
} }
Roots getRuntimeRoots(GlobalOpts opts) Roots getRuntimeRoots(TracerConfig opts)
{ {
auto procDir = fs::path("/proc"); auto procDir = fs::path("/proc");
if (!fs::exists(procDir)) if (!fs::exists(procDir))
@ -288,7 +193,7 @@ Roots getRuntimeRoots(GlobalOpts opts)
|| !procEntry.is_directory()) || !procEntry.is_directory())
continue; continue;
debug(opts.verbosity, "Considering path " + procEntry.path().string()); opts.debug("Considering path " + procEntry.path().string());
// A set of paths used by the executable and possibly symlinks to a // A set of paths used by the executable and possibly symlinks to a
// path in the store // path in the store
@ -308,7 +213,7 @@ Roots getRuntimeRoots(GlobalOpts opts)
if (isInStore(opts.storeDir, realPath)) if (isInStore(opts.storeDir, realPath))
res[realPath].insert(path); res[realPath].insert(path);
} catch (fs::filesystem_error &e) { } catch (fs::filesystem_error &e) {
debug(opts.verbosity, e.what()); opts.debug(e.what());
} }
// Scan the environment of the executable // Scan the environment of the executable
@ -325,66 +230,4 @@ Roots getRuntimeRoots(GlobalOpts opts)
return res; return res;
} }
int main(int argc, char * * argv)
{
const GlobalOpts opts = parseCmdLine(argc, argv);
const set<fs::path> standardRoots = {
opts.stateDir / fs::path("profiles"),
opts.stateDir / fs::path("gcroots"),
};
int mySock = socket(PF_UNIX, SOCK_STREAM, 0);
if (mySock == 0) {
throw Error("Cannot create Unix domain socket");
}
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
unlink(opts.socketPath.c_str());
strcpy(addr.sun_path, opts.socketPath.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());
addr.sun_family = AF_UNIX;
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");
}
log(opts.verbosity, GlobalOpts::Quiet, "accepted connection");
auto traceResult = followPathsToStore(opts, standardRoots);
auto runtimeRoots = getRuntimeRoots(opts);
traceResult.storeRoots.insert(runtimeRoots.begin(), runtimeRoots.end());
for (auto & [rootInStore, externalRoots] : traceResult.storeRoots) {
for (auto & externalRoot : externalRoots) {
send(remoteSocket, rootInStore.string().c_str(), rootInStore.string().size(), 0);
send(remoteSocket, "\t", strlen("\t"), 0);
send(remoteSocket, externalRoot.string().c_str(), externalRoot.string().size(), 0);
send(remoteSocket, "\n", strlen("\n"), 0);
}
}
send(remoteSocket, "\n", strlen("\n"), 0);
for (auto & deadLink : traceResult.deadLinks) {
send(remoteSocket, deadLink.string().c_str(), deadLink.string().size(), 0);
send(remoteSocket, "\n", strlen("\n"), 0);
}
close(remoteSocket);
}
} }

View file

@ -0,0 +1,48 @@
#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::exception {
private:
const string message;
public:
Error(std::string message)
: message(message)
{}
const char* what() const noexcept override
{
return message.c_str();
}
};
inline void logNone(std::string_view)
{ }
struct TracerConfig {
fs::path storeDir = "/nix/store";
fs::path stateDir = "/nix/var/nix";
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);
}

View file

@ -1,9 +1,22 @@
ifndef HOST_DARWIN ifndef HOST_DARWIN
libraries += find-roots
find-roots_NAME = libfindroots
find-roots_DIR := $(d)/lib
find-roots_SOURCES := $(wildcard $(d)/lib/*.cc)
programs += nix-find-roots programs += nix-find-roots
nix-find-roots_DIR := $(d) nix-find-roots_DIR := $(d)
nix-find-roots_SOURCES := $(wildcard $(d)/*.cc) nix-find-roots_SOURCES := $(d)/main.cc
nix-find-roots_LIBS := find-roots
nix-find-roots_CXXFLAGS += \
-I src/nix-find-roots/lib
nix-find-roots_INSTALL_DIR := $(libexecdir)/nix nix-find-roots_INSTALL_DIR := $(libexecdir)/nix
endif endif

124
src/nix-find-roots/main.cc Normal file
View file

@ -0,0 +1,124 @@
#include "find-roots.hh"
#include <getopt.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <iostream>
#include <unistd.h>
#include <cstring>
using namespace nix::roots_tracer;
void logStderr(std::string_view msg)
{
std::cerr << msg << std::endl;
}
TracerConfig parseCmdLine(int argc, char** argv)
{
TracerConfig res;
res.log = logStderr;
auto usage = [&]() {
std::cerr << "Usage: " << string(argv[0]) << " [--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' },
};
int option_index = 0;
int opt_char;
while((opt_char = getopt_long(argc, argv, "vd:s:l:",
long_options, &option_index)) != -1) {
switch (opt_char) {
case 0:
break;
break;
case '?':
usage();
break;
case 'v':
res.debug = logStderr;
break;
case 's':
res.storeDir = fs::path(optarg);
break;
case 'd':
res.stateDir = fs::path(optarg);
break;
case 'l':
res.socketPath = fs::path(optarg);
break;
default:
std::cerr << "Got invalid char: " << (char)opt_char << std::endl;
abort();
}
};
return res;
}
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 = socket(PF_UNIX, SOCK_STREAM, 0);
if (mySock == 0) {
throw Error("Cannot create Unix domain socket");
}
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
unlink(opts.socketPath.c_str());
strcpy(addr.sun_path, opts.socketPath.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());
addr.sun_family = AF_UNIX;
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 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) {
send(remoteSocket, rootInStore.string().c_str(), rootInStore.string().size(), 0);
send(remoteSocket, "\t", strlen("\t"), 0);
send(remoteSocket, externalRoot.string().c_str(), externalRoot.string().size(), 0);
send(remoteSocket, "\n", strlen("\n"), 0);
}
}
send(remoteSocket, "\n", strlen("\n"), 0);
for (auto & deadLink : traceResult.deadLinks) {
send(remoteSocket, deadLink.string().c_str(), deadLink.string().size(), 0);
send(remoteSocket, "\n", strlen("\n"), 0);
}
close(remoteSocket);
}
}