diff --git a/Makefile b/Makefile index 5040d2884..d4f5e53d9 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ makefiles = \ src/libexpr/local.mk \ src/libcmd/local.mk \ src/nix/local.mk \ + src/nix-find-roots/local.mk \ src/resolve-system-dependencies/local.mk \ scripts/local.mk \ misc/bash/local.mk \ diff --git a/src/nix-find-roots/.gitignore b/src/nix-find-roots/.gitignore new file mode 100644 index 000000000..fbbee5484 --- /dev/null +++ b/src/nix-find-roots/.gitignore @@ -0,0 +1 @@ +nix-find-roots diff --git a/src/nix-find-roots/local.mk b/src/nix-find-roots/local.mk new file mode 100644 index 000000000..a1ca2c9bf --- /dev/null +++ b/src/nix-find-roots/local.mk @@ -0,0 +1,7 @@ +programs += nix-find-roots + +nix-find-roots_DIR := $(d) + +nix-find-roots_SOURCES := $(wildcard $(d)/*.cc) + +nix-find-roots_INSTALL_DIR := $(libexecdir)/nix diff --git a/src/nix-find-roots/nix-find-roots.cc b/src/nix-find-roots/nix-find-roots.cc new file mode 100644 index 000000000..904504566 --- /dev/null +++ b/src/nix-find-roots/nix-find-roots.cc @@ -0,0 +1,168 @@ +/* + * 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 +#include +#include +#include +#include + +namespace fs = std::filesystem; +using std::set, std::string; + +struct GlobalOpts { + fs::path storeDir; + enum VerbosityLvl { + Quiet, + Verbose + }; + VerbosityLvl verbosity = Quiet; +}; + +void log(GlobalOpts::VerbosityLvl verbosity, std::string_view msg) +{ + if (verbosity == GlobalOpts::Quiet) + return; + std::cerr << msg << std::endl; +} + +GlobalOpts parseCmdLine(int argc, char** argv) +{ + GlobalOpts res; + auto usage = [&]() { + std::cerr << "Usage: " << string(argv[0]) << " [-v] [storeDir]" << std::endl; + exit(1); + }; + auto args = std::vector(argv+1, argv+argc); + bool storeDirSet = false; + for (auto & arg : args) { + if (string(arg) == "-v") + res.verbosity = GlobalOpts::Verbose; + else if (!storeDirSet) { + res.storeDir = arg; + storeDirSet = true; + } + else usage(); + }; + if (!storeDirSet) + usage(); + return res; +} + +struct TraceResult { + set storeRoots; + set deadLinks; +}; + +void followPathToStore( + const GlobalOpts & opts, + int recursionsLeft, + TraceResult & res, + const fs::path & root, + const fs::file_status & status) +{ + log(opts.verbosity, "Considering file " + root.string()); + + if (recursionsLeft < 0) + return; + + if (std::search(root.begin(), root.end(), opts.storeDir.begin(), opts.storeDir.end()) == root.begin()) { + res.storeRoots.insert(root); + return; + } + + switch (status.type()) { + case fs::file_type::directory: + { + auto directory_iterator = fs::recursive_directory_iterator(root); + for (auto & child : directory_iterator) + followPathToStore(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) { + log(opts.verbosity, "Error accessing the file " + target.string() + ": " + msg); + log(opts.verbosity, "(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"); + followPathToStore(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.insert(possibleStorePath); + } + default: + break; + } +} + +void followPathToStore( + const GlobalOpts & opts, + int recursionsLeft, + TraceResult & res, + const fs::path & root) +{ + try { + auto status = fs::symlink_status(root); + followPathToStore(opts, recursionsLeft, res, root, status); + } catch (fs::filesystem_error & e) { + log(opts.verbosity, "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 followPathsToStore(GlobalOpts opts, set roots) +{ + int maxRecursionLevel = 2; + TraceResult res; + for (auto & root : roots) + followPathToStore(opts, maxRecursionLevel, res, root); + return res; +} + +int main(int argc, char * * argv) +{ + GlobalOpts opts = parseCmdLine(argc, argv); + set originalRoots; + std::string currentLine; + while (std::getline(std::cin, currentLine)) { + originalRoots.insert(fs::path(currentLine)); + } + auto traceResult = followPathsToStore(opts, originalRoots); + for (auto & rootInStore : traceResult.storeRoots) { + std::cout << rootInStore.string() << std::endl; + } + std::cout << std::endl; + for (auto & deadLink : traceResult.deadLinks) { + std::cout << deadLink.string() << std::endl; + } +} +