From 2f1ce8900b6ab4eec9633cd0f79d09a0ed1ab2c7 Mon Sep 17 00:00:00 2001 From: Artemis Tosini Date: Thu, 22 Jan 2026 16:37:09 -0500 Subject: [PATCH] Ignore delete failures during garbage collection When running nix as an unprivileged user it may not be able to write to all paths in the nix store. Ignore deletion failures to fix tests that run `nix-collect-garbage` in this configuration. Co-Authored-By: John Ericson --- src/libstore/gc.cc | 8 +++---- .../include/nix/store/local-overlay-store.hh | 2 +- src/libstore/include/nix/store/local-store.hh | 19 ++++++++++++++++- src/libstore/local-overlay-store.cc | 6 +++--- src/libstore/local-store.cc | 21 +++++++++++++++++-- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index ad97d6fae..67199ade6 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -651,7 +651,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) /* Helper function that deletes a path from the store and throws GCLimitReached if we've deleted enough garbage. */ - auto deleteFromStore = [&](std::string_view baseName) { + auto deleteFromStore = [&](std::string_view baseName, bool isKnownPath) { Path path = storeDir + "/" + std::string(baseName); Path realPath = config->realStoreDir + "/" + std::string(baseName); @@ -671,7 +671,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) results.paths.insert(path); uint64_t bytesFreed; - deleteStorePath(realPath, bytesFreed); + deleteStorePath(realPath, bytesFreed, isKnownPath); results.bytesFreed += bytesFreed; @@ -793,7 +793,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) if (shouldDelete) { try { invalidatePathChecked(path); - deleteFromStore(path.to_string()); + deleteFromStore(path.to_string(), true); referrersCache.erase(path); } catch (PathInUse & e) { // If we end up here, it's likely a new occurrence @@ -844,7 +844,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) if (auto storePath = maybeParseStorePath(storeDir + "/" + name)) deleteReferrersClosure(*storePath); else - deleteFromStore(name); + deleteFromStore(name, false); } } catch (GCLimitReached & e) { } diff --git a/src/libstore/include/nix/store/local-overlay-store.hh b/src/libstore/include/nix/store/local-overlay-store.hh index 1d69d3417..c0efdece8 100644 --- a/src/libstore/include/nix/store/local-overlay-store.hh +++ b/src/libstore/include/nix/store/local-overlay-store.hh @@ -184,7 +184,7 @@ private: * Check which layers the store object exists in to try to avoid * needing to remount. */ - void deleteStorePath(const Path & path, uint64_t & bytesFreed) override; + void deleteStorePath(const Path & path, uint64_t & bytesFreed, bool isKnownPath) override; /** * Deduplicate by removing store objects from the upper layer that diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh index 320e9ff90..ef24fe059 100644 --- a/src/libstore/include/nix/store/local-store.hh +++ b/src/libstore/include/nix/store/local-store.hh @@ -117,6 +117,20 @@ public: > While the filesystem the database resides on might appear to be read-only, consider whether another user or system might have write access to it. )"}; + Setting ignoreGcDeleteFailure{ + this, + false, + "ignore-gc-delete-failure", + R"( + Whether to ignore failures when deleting items with the garbage collector. + + Normally the garbage collector will fail with an error if the nix daemon cannot delete a file, with this setting such errors will only be printed as warnings. + )", + {}, + true, + Xp::LocalOverlayStore, + }; + static const std::string name() { return "Local Store"; @@ -312,8 +326,11 @@ public: * Called by `collectGarbage` to recursively delete a path. * The default implementation simply calls `deletePath`, but it can be * overridden by stores that wish to provide their own deletion behaviour. + * + * @param isKnownPath true if this is a known store path, false if it's + * garbage/unknown content found in the store directory */ - virtual void deleteStorePath(const Path & path, uint64_t & bytesFreed); + virtual void deleteStorePath(const Path & path, uint64_t & bytesFreed, bool isKnownPath); /** * Optimise the disk space usage of the Nix store by hard-linking diff --git a/src/libstore/local-overlay-store.cc b/src/libstore/local-overlay-store.cc index c8aa1d1a2..edd549801 100644 --- a/src/libstore/local-overlay-store.cc +++ b/src/libstore/local-overlay-store.cc @@ -204,7 +204,7 @@ void LocalOverlayStore::collectGarbage(const GCOptions & options, GCResults & re remountIfNecessary(); } -void LocalOverlayStore::deleteStorePath(const Path & path, uint64_t & bytesFreed) +void LocalOverlayStore::deleteStorePath(const Path & path, uint64_t & bytesFreed, bool isKnownPath) { auto mergedDir = config->realStoreDir.get() + "/"; if (path.substr(0, mergedDir.length()) != mergedDir) { @@ -226,7 +226,7 @@ void LocalOverlayStore::deleteStorePath(const Path & path, uint64_t & bytesFreed } else { // Path does not exist in lower store. // So we can delete via overlayfs and not need to remount. - LocalStore::deleteStorePath(path, bytesFreed); + LocalStore::deleteStorePath(path, bytesFreed, isKnownPath); } } } @@ -246,7 +246,7 @@ void LocalOverlayStore::optimiseStore() if (lowerStore->isValidPath(path)) { uint64_t bytesFreed = 0; // Deduplicate store path - deleteStorePath(toRealPath(path), bytesFreed); + deleteStorePath(toRealPath(path), bytesFreed, true); } done++; act.progress(done, paths.size()); diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 1e255068f..ede73a89c 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -408,9 +408,26 @@ AutoCloseFD LocalStore::openGCLock() return toDescriptor(fdGCLock); } -void LocalStore::deleteStorePath(const Path & path, uint64_t & bytesFreed) +void LocalStore::deleteStorePath(const Path & path, uint64_t & bytesFreed, bool isKnownPath) { - deletePath(path, bytesFreed); + try { + deletePath(path, bytesFreed); + } catch (SystemError & e) { + if (config->ignoreGcDeleteFailure) { + logWarning( + {.msg = HintFmt( + isKnownPath ? "ignoring failure to remove store path '%1%': %2%" + : "ignoring failure to remove garbage in store directory '%1%': %2%", + path, + e.info().msg)}); + } else { + e.addTrace( + {}, + isKnownPath ? "While deleting store path '%1%'" : "While deleting garbage in store directory '%1%'", + path); + throw; + } + } } LocalStore::~LocalStore()