Visitar URL original
nix profile: Build the profile in memory instead of a temporary directory by yungweng · Pull Request #16792 · NixOS/nix · GitHub
Skip to content

nix profile: Build the profile in memory instead of a temporary directory - #16792

Open
yungweng wants to merge 2 commits into
NixOS:masterfrom
yungweng:profile-tempdir-leak
Open

yungweng wants to merge 2 commits into
NixOS:masterfrom
yungweng:profile-tempdir-leak

Conversation

@yungweng

@yungweng yungweng commented Nov 9, 2026 •

Copy link
Copy Markdown

Motivation

Every nix profile add, remove and upgrade leaves the temporary profile tree behind in $TMPDIR. With a devbox profile that is about 7,400 files per call, which on a tmpfs /tmp eventually exhausts the inode limit.

Fixes #10404

Implementation

buildProfile() builds the symlink tree in memory and returns it as a ref<MemorySourceAccessor>, so there is no temporary directory any more. nix profile adds it to the store with the SourcePath overload of Store::addToStore(). builtin:buildenv (nix-env) writes the tree to its output with a RestoreSink, as InputScheme::clone does. The mkdir/unlink calls and the Windows #ifdef in createLinks go away. createLinks also works on std::filesystem::path now instead of converting to strings, and the skip list compares file names instead of path suffixes.

The output is unchanged. The old and new builds produce the same store path, NAR hash, references and content address for nix profile (local store and through the daemon) and the same store path and NAR hash for nix-env, including a directory collision (symlink replaced by a real directory), a priority collision, the skipped files and the same-priority conflict error. The only visible difference is the "collision between non-directory ... and directory ..." error: it used to name the path inside the leaked temporary directory and now names the path inside the profile.

The functional test runs nix profile add with a fresh TMPDIR and checks that it is empty afterwards. It fails on master. nix-profile, user-envs, nix-channel, nix-collect-garbage-d, remote-store, binary-cache, tarball, simple and misc pass.

@yungweng
yungweng requested a review from edolstra as a code owner November 9, 2026 17:57
Comment thread src/nix/profile.cc Outdated
Comment on lines 239 to 259

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like this even needs to write anything to disk tbh... A MemorySink + MemorySourceAccessor should be plenty enough.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Claude Opus 5.5 responding on behalf of yungweng

Done in d419c7b. buildProfile() now fills a MemorySourceAccessor. nix profile dumps from it directly, and builtin:buildenv restores it to $out with a RestoreSink. Old and new builds give identical store paths and NAR hashes for both nix profile and nix-env, collisions included.

@yungweng
yungweng force-pushed the profile-tempdir-leak branch from 7f78ce8 to d419c7b Compare November 9, 2026 20:03
@yungweng
yungweng requested a review from Ericson2314 as a code owner November 9, 2026 20:03
@yungweng yungweng changed the title nix profile: Delete the temporary profile directory after building nix profile: Build the profile in memory instead of a temporary directory Nov 9, 2026
@github-actions github-actions Bot added new-cli Relating to the "nix" command with-tests Issues related to testing. PRs with tests have some priority labels Nov 9, 2026
Comment thread src/libstore/builtins/buildenv.cc Outdated
Comment on lines 56 to 59
auto srcFile = (std::filesystem::path{srcDir} / name).string();
auto dstFile = (std::filesystem::path{dstDir} / name).string();
auto dstFile = dstDir / name.string();

auto srcStOpt = maybeStat(srcFile.c_str());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw this whole .string() and .c_str() can also be cleaned up :)

@yungweng yungweng Nov 9, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Claude Opus 5.5 responding on behalf of yungweng

Done in 35a1f7d. srcFile stays a std::filesystem::path, and the skip list compares name instead of path suffixes (info/dir checks srcDir.filename()). .string() remains only where a std::string is needed: the dot check, the CanonPath component and the MemorySink::createSymlink target. Store paths and NAR hashes are still identical to master, including the skipped files.

@yungweng
yungweng force-pushed the profile-tempdir-leak branch from d419c7b to 35a1f7d Compare November 9, 2026 20:28
Comment thread src/nix/profile.cc Resolved
Comment thread src/libstore/builtins/buildenv.cc Resolved
…tory

ProfileManifest::build() created a temporary directory for the profile's
symlink tree and never removed it, so every `nix profile add`, `remove`
and `upgrade` left a `nix-<pid>-<n>` directory in $TMPDIR.

buildProfile() now returns the tree as a MemorySourceAccessor. `nix
profile` adds it to the store with Store::addToStore(), and
builtin:buildenv (nix-env) writes it to its output with a RestoreSink.

Fixes NixOS#10404

Assisted-by: Claude Code (Claude Opus 5.5)
Assisted-by: Claude Code (Claude Fable 5.2)
@yungweng
yungweng force-pushed the profile-tempdir-leak branch from 35a1f7d to 2630567 Compare November 9, 2026 21:45
Comment thread tests/functional/nix-profile.sh Outdated
Comment on lines +53 to +56
# Building the new profile must not leak its temporary directory (#10404).
mkdir "$TEST_ROOT/tmp"
TMPDIR=$TEST_ROOT/tmp nix profile add "$flake1Dir" -L
[[ -z $(ls -A "$TEST_ROOT/tmp") ]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is now pretty irrelevant that the implementation doesn't create temp directories right?

Comment thread src/libstore/builtins/buildenv.cc Outdated
Comment on lines 34 to 36
static void createLinks(State & state, const std::filesystem::path & srcDir, const CanonPath & dstDir, int priority)
{
DirectoryIterator srcFiles;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance that we could honor the NAR recursion depth here?

} else if (auto * link = std::get_if<MemorySourceAccessor::File::Symlink>(&dst->raw)) {
auto target = canonPath(link->target, true);
if (!S_ISDIR(lstat(target).st_mode))
throw Error("collision between %1% and non-directory %2%", PathFmt(srcFile), PathFmt(target));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should no longer be using PathFmt though? Since it's a CanonPath

class BuildProfileTest : public ::testing::Test
{
protected:
AutoDelete tempDir{createTempDir()};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shoule be in SetUp and TearDown, no?

makePackage("third", narMaxDepth - 2)});

auto dir = CanonPath(std::vector<std::string>(narMaxDepth - 2, "d"));
EXPECT_EQ(profile->readDirectory(dir).size(), 3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have testing::HasDirectory matcher for this

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-cli Relating to the "nix" command with-tests Issues related to testing. PRs with tests have some priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nix profile install leaks temporary directory

2 participants