Skip to content

Fixup single user curing#1781

Open
cole-h wants to merge 13 commits intomainfrom
fixup-single-user-curing
Open

Fixup single user curing#1781
cole-h wants to merge 13 commits intomainfrom
fixup-single-user-curing

Conversation

@cole-h
Copy link
Member

@cole-h cole-h commented Feb 23, 2026

Description

Fixes #1594
Fixes #1773
Fixes #1215
Fixes #1765
Fixes #819
Fixes #680

Checklist
  • Formatted with cargo fmt
  • Built with nix build
  • Ran flake checks with nix flake check
  • Added or updated relevant tests (leave unchecked if not applicable)
  • Added or updated relevant documentation (leave unchecked if not applicable)
  • Linked to related issues (leave unchecked if not applicable)
Validating with install.determinate.systems

If a maintainer has added the upload to s3 label to this PR, it will become available for installation via install.determinate.systems:

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix/pr/$PR_NUMBER | sh -s -- install

Summary by CodeRabbit

  • Refactor

    • More precise Nix ownership handling: per-user paths treated specially, explicit inclusion of the root /nix, and ownership checks now require root ownership plus correct group to enforce consistent permissions across store paths.
  • Style

    • Removed an explicit rustfmt setting.
  • Chores

    • Simplified error handling and logging for clearer messages and streamlined control flow.
@cole-h cole-h added the upload to s3 The labeled PR is allowed to upload its artifacts to S3 for easy testing label Feb 23, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-user path constants and switches to PathBuf-based iteration for Nix tree ownership checks; renames and expands ownership-checking logic to consider UID and GID; adjusts filtering to prefix checks and explicitly includes /nix; minor error-handling and rustfmt config edits.

Changes

Cohort / File(s) Summary
Nix tree ownership enforcement
src/action/common/create_nix_tree.rs
Introduce PathBuf and constants PROFILES_PER_USER, GCROOTS_PER_USER; rename ensure_nix_var_ownershipensure_nix_ownership; iterate over PathBufs, filter using starts_with (excluding exact per-user paths), include /nix, update logging and chown usage.
Provisioning / store ownership
src/action/common/provision_nix.rs
Rename ensure_nix_store_groupensure_nix_store_ownership; change ownership checks to consider both metadata.gid() and metadata.uid() (process entries where gid != desired OR uid != 0); collect entries before applying chown.
Small error-handling concisions
src/action/base/create_or_insert_into_file.rs, src/action/base/create_or_merge_nix_config.rs
Collapse multi-line map_err closures when opening temp files into single-expression map_err chains; behavior unchanged.
Formatting config
rustfmt.toml
Remove use_try_shorthand = true and its comment.
Manifest
Cargo.toml
Touched in diff; no public API signature changes reported.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • grahamc
  • colemickens

Poem

I hop the /nix paths, paws soft and swift,
I mark each owner, give each folder a lift,
per-user trails and the root I include,
tidy hops of chown, precise and shrewd,
a rabbit's nudge to keep the tree well-fixed.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fixup single user curing' is vague and does not clearly convey the changeset's purpose; 'curing' appears to be unclear terminology. Clarify the title to describe the actual fix, such as 'Fix ownership transition errors in single-user Nix installations' or 'Ensure correct /nix directory ownership for single-user installations'.
Out of Scope Changes check ❓ Inconclusive The code changes focus on ownership corrections and function renaming but include a minor rustfmt.toml configuration removal that appears tangential to the core issue. Clarify whether the rustfmt.toml change (removing use_try_shorthand = true) is necessary for this fix or should be in a separate PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR changes directly address the linked issues by ensuring proper /nix ownership, fixing the unsafe path transition errors and systemd-tmpfiles failures across all reported issues [#1594, #1773, #1215, #1765, #819, #680].
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fixup-single-user-curing

Comment @coderabbitai help to get the list of available commands and usage tips.

@cole-h cole-h force-pushed the fixup-single-user-curing branch 4 times, most recently from 7ba308b to 147718e Compare February 24, 2026 16:50
@cole-h cole-h marked this pull request as ready for review February 24, 2026 16:51
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/action/common/provision_nix.rs (1)

174-179: ⚠️ Potential issue | 🟠 Major

Ownership enforcement is skipped when only UID is wrong.
The guard runs the walk only when the store’s GID mismatches. If /nix/store (or entries) have uid != 0 but correct GID, the function now returns early and leaves bad ownership despite the “ownership” semantics.

✅ Fix: gate on UID or GID mismatch
-    let previous_store_group_id = previous_store_metadata.gid();
-    if previous_store_group_id != desired_nix_build_group_id {
+    let previous_store_group_id = previous_store_metadata.gid();
+    let previous_store_user_id = previous_store_metadata.uid();
+    if previous_store_group_id != desired_nix_build_group_id || previous_store_user_id != 0 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action/common/provision_nix.rs` around lines 174 - 179, The code
currently only triggers the WalkDir when previous_store_group_id !=
desired_nix_build_group_id, skipping fixes if UID differs; change the guard to
check either UID or GID mismatch by reading previous_store_metadata.uid() and
comparing it to the desired UID (root/0) as well as comparing gid to
desired_nix_build_group_id, and only skip the walk when both UID and GID already
match; then proceed with walkdir::WalkDir over NIX_STORE_LOCATION to enforce
ownership when either value is wrong.
src/action/common/create_nix_tree.rs (1)

159-169: ⚠️ Potential issue | 🟡 Minor

Keep the per-user directory itself eligible for ownership fixes.
filter_entry returning false skips the directory entry itself, so /nix/var/nix/*/per-user won’t be re-owned even though only its children are exempted by the doc comment. This can leave the parent directory user-owned and contradict the stated rule.

🔧 Suggested fix
-        .filter_entry(|entry| {
-            let path = entry.path();
-
-            if path.starts_with("/nix/var/nix/profiles/per-user")
-                || path.starts_with("/nix/var/nix/gcroots/per-user")
-            {
-                // False means do *not* descend into this directory
-                // ...which we don't want to do, because the per-user subdirectories are usually owned by that user.
-                return false;
-            }
-
-            true
-        })
+        .filter_entry(|entry| {
+            let path = entry.path();
+            let profiles_per_user = std::path::Path::new("/nix/var/nix/profiles/per-user");
+            let gcroots_per_user = std::path::Path::new("/nix/var/nix/gcroots/per-user");
+
+            if path == profiles_per_user || path == gcroots_per_user {
+                return true;
+            }
+            if path.starts_with(profiles_per_user) || path.starts_with(gcroots_per_user) {
+                // do not descend into per-user subdirectories
+                return false;
+            }
+            true
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action/common/create_nix_tree.rs` around lines 159 - 169, The
filter_entry closure currently returns false for paths that start_with the
per-user prefixes, which excludes the per-user directory itself; change the
logic so the exact per-user directory paths ("/nix/var/nix/profiles/per-user"
and "/nix/var/nix/gcroots/per-user") are allowed (return true) but descendants
are still pruned (return false when path.starts_with(...) and path != that exact
directory). Edit the filter_entry closure in create_nix_tree.rs (the closure
using let path = entry.path()) to check for equality with the two per-user
directory paths first and return true for those, otherwise return false for
their descendants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/action/common/create_nix_tree.rs`:
- Around line 159-169: The filter_entry closure currently returns false for
paths that start_with the per-user prefixes, which excludes the per-user
directory itself; change the logic so the exact per-user directory paths
("/nix/var/nix/profiles/per-user" and "/nix/var/nix/gcroots/per-user") are
allowed (return true) but descendants are still pruned (return false when
path.starts_with(...) and path != that exact directory). Edit the filter_entry
closure in create_nix_tree.rs (the closure using let path = entry.path()) to
check for equality with the two per-user directory paths first and return true
for those, otherwise return false for their descendants.

In `@src/action/common/provision_nix.rs`:
- Around line 174-179: The code currently only triggers the WalkDir when
previous_store_group_id != desired_nix_build_group_id, skipping fixes if UID
differs; change the guard to check either UID or GID mismatch by reading
previous_store_metadata.uid() and comparing it to the desired UID (root/0) as
well as comparing gid to desired_nix_build_group_id, and only skip the walk when
both UID and GID already match; then proceed with walkdir::WalkDir over
NIX_STORE_LOCATION to enforce ownership when either value is wrong.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abe9458 and 147718e.

📒 Files selected for processing (2)
  • src/action/common/create_nix_tree.rs
  • src/action/common/provision_nix.rs
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 147718e to 5989b3a Compare February 24, 2026 17:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/action/common/create_nix_tree.rs (1)

162-201: ⚠️ Potential issue | 🔴 Critical

Operator precedence bug + tautological condition in filter_entry.

Two issues in the filter predicate at Lines 165–167:

  1. Tautology: path != PROFILES_PER_USER || path != GCROOTS_PER_USER is always true — a path cannot be equal to both distinct strings simultaneously. This should use &&:

    path != PROFILES_PER_USER && path != GCROOTS_PER_USER
    
  2. Operator precedence: && binds tighter than ||, so the "not-equal" guard only applies to the GCROOTS_PER_USER branch, leaving the PROFILES_PER_USER branch unguarded. The whole expression needs outer parentheses around the ||.

Because of (1), the guard is dead code and the condition effectively becomes path.starts_with(PROFILES_PER_USER) || path.starts_with(GCROOTS_PER_USER), which excludes the per-user directories themselves (not just their children) from the walk — filter_entry returning false both prevents descent and drops the entry. In a single-user → multi-user migration scenario (the exact case this PR targets), those directories may be user-owned and will silently remain un-chowned.

If the intent is "don't descend into per-user children, but do chown the per-user dirs themselves," filter_entry alone can't express that. A straightforward fix: unconditionally exclude them from the walk and chain them like you already do for /nix:

Proposed fix
         .filter_entry(|entry| {
             let path = entry.path();
 
-            if path.starts_with(PROFILES_PER_USER)
-                || path.starts_with(GCROOTS_PER_USER)
-                    && (path != PROFILES_PER_USER || path != GCROOTS_PER_USER)
-            {
+            if path.starts_with(PROFILES_PER_USER) || path.starts_with(GCROOTS_PER_USER) {
                 // False means do *not* descend into this directory
                 // ...which we don't want to do, because the per-user subdirectories are usually owned by that user.
                 return false;
             }
 
             true
         })

…and at the chain site:

         // Ensure /nix is also owned by 0:0
-        .chain(std::iter::once(PathBuf::from("/nix")));
+        .chain(
+            [PROFILES_PER_USER, GCROOTS_PER_USER, "/nix"]
+                .into_iter()
+                .map(PathBuf::from),
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action/common/create_nix_tree.rs` around lines 162 - 201, The
filter_entry closure incorrectly uses a tautological and precedence-misordered
expression; replace its predicate with a grouped expression so only per-user
children are skipped (not the per-user dirs themselves). Specifically, in the
filter_entry closure in create_nix_tree.rs, change the condition to: if
(path.starts_with(PROFILES_PER_USER) || path.starts_with(GCROOTS_PER_USER)) &&
(path != PROFILES_PER_USER && path != GCROOTS_PER_USER) { return false; } so the
starts_with checks are grouped and the not-equal checks use &&, ensuring you
prevent descent into per-user subdirs but still keep the per-user directories
for later chown handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/action/common/create_nix_tree.rs`:
- Around line 162-201: The filter_entry closure incorrectly uses a tautological
and precedence-misordered expression; replace its predicate with a grouped
expression so only per-user children are skipped (not the per-user dirs
themselves). Specifically, in the filter_entry closure in create_nix_tree.rs,
change the condition to: if (path.starts_with(PROFILES_PER_USER) ||
path.starts_with(GCROOTS_PER_USER)) && (path != PROFILES_PER_USER && path !=
GCROOTS_PER_USER) { return false; } so the starts_with checks are grouped and
the not-equal checks use &&, ensuring you prevent descent into per-user subdirs
but still keep the per-user directories for later chown handling.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 147718e and 5989b3a.

📒 Files selected for processing (5)
  • rustfmt.toml
  • src/action/base/create_or_insert_into_file.rs
  • src/action/base/create_or_merge_nix_config.rs
  • src/action/common/create_nix_tree.rs
  • src/action/common/provision_nix.rs
💤 Files with no reviewable changes (1)
  • rustfmt.toml
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 5989b3a to 7581ecc Compare February 24, 2026 17:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/action/common/create_nix_tree.rs`:
- Around line 165-167: The conditional in filter_entry wrongly relies on
operator precedence so PROFILES_PER_USER itself is excluded; update the guard
around the starts_with checks so the exclusion (path != PROFILES_PER_USER &&
path != GCROOTS_PER_USER) applies to both branches—i.e., add parentheses
grouping (path.starts_with(PROFILES_PER_USER) ||
path.starts_with(GCROOTS_PER_USER)) && (path != PROFILES_PER_USER && path !=
GCROOTS_PER_USER) in src/action/common/create_nix_tree.rs (referencing the
path.starts_with checks, PROFILES_PER_USER, GCROOTS_PER_USER and the
filter_entry conditional) to ensure the per-user directories themselves are not
skipped.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5989b3a and 7581ecc.

📒 Files selected for processing (5)
  • rustfmt.toml
  • src/action/base/create_or_insert_into_file.rs
  • src/action/base/create_or_merge_nix_config.rs
  • src/action/common/create_nix_tree.rs
  • src/action/common/provision_nix.rs
💤 Files with no reviewable changes (1)
  • rustfmt.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/action/base/create_or_insert_into_file.rs
This way we handle the possibility of multiple child directories.
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 7581ecc to 7fefbd6 Compare February 24, 2026 17:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/action/common/create_nix_tree.rs (1)

82-86: ⚠️ Potential issue | 🟡 Minor

Stale user-facing description after /nix was added to the ownership pass.

execute_description still reads "Synchronize /nix/var ownership" / "Will update existing files in /nix/var", but ensure_nix_ownership now also covers /nix itself.

📝 Proposed fix
-            ActionDescription::new(
-                "Synchronize /nix/var ownership".to_string(),
-                vec![format!(
-                    "Will update existing files in /nix/var to be owned by User ID 0, Group ID 0"
-                )],
-            ),
+            ActionDescription::new(
+                "Synchronize /nix ownership".to_string(),
+                vec![format!(
+                    "Will update /nix and existing files in /nix/var to be owned by User ID 0, Group ID 0"
+                )],
+            ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action/common/create_nix_tree.rs` around lines 82 - 86, Update the stale
user-facing description strings to reflect that ownership is synchronized for
both /nix and /nix/var (not just /nix/var): change the title "Synchronize
/nix/var ownership" and the detail "Will update existing files in /nix/var to be
owned by User ID 0, Group ID 0" to mention both /nix and /nix/var (e.g.,
"Synchronize /nix and /nix/var ownership" and "Will update existing files in
/nix and /nix/var to be owned by User ID 0, Group ID 0") where these strings are
defined in create_nix_tree.rs so they match the behavior of
ensure_nix_ownership.
♻️ Duplicate comments (1)
src/action/common/create_nix_tree.rs (1)

165-166: Operator-precedence fix correctly applied — starts_with branches are now properly grouped.

Current form (starts_with(PER_USER) || starts_with(GCROOTS)) && (path != PER_USER && path != GCROOTS) is logically equivalent to the fix proposed in the previous review. Both PROFILES_PER_USER and GCROOTS_PER_USER exact-match entries now correctly pass through filter_entry and receive ownership treatment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action/common/create_nix_tree.rs` around lines 165 - 166, The conditional
using path.starts_with(PROFILES_PER_USER) || path.starts_with(GCROOTS_PER_USER)
grouped with && (path != PROFILES_PER_USER && path != GCROOTS_PER_USER) is
correct and matches the intended operator-precedence fix; no code change
required — keep the condition as written in create_nix_tree (the expression
referencing PROFILES_PER_USER, GCROOTS_PER_USER, and path) so exact-match
entries still pass through filter_entry and receive ownership treatment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/action/common/create_nix_tree.rs`:
- Around line 82-86: Update the stale user-facing description strings to reflect
that ownership is synchronized for both /nix and /nix/var (not just /nix/var):
change the title "Synchronize /nix/var ownership" and the detail "Will update
existing files in /nix/var to be owned by User ID 0, Group ID 0" to mention both
/nix and /nix/var (e.g., "Synchronize /nix and /nix/var ownership" and "Will
update existing files in /nix and /nix/var to be owned by User ID 0, Group ID
0") where these strings are defined in create_nix_tree.rs so they match the
behavior of ensure_nix_ownership.

---

Duplicate comments:
In `@src/action/common/create_nix_tree.rs`:
- Around line 165-166: The conditional using path.starts_with(PROFILES_PER_USER)
|| path.starts_with(GCROOTS_PER_USER) grouped with && (path != PROFILES_PER_USER
&& path != GCROOTS_PER_USER) is correct and matches the intended
operator-precedence fix; no code change required — keep the condition as written
in create_nix_tree (the expression referencing PROFILES_PER_USER,
GCROOTS_PER_USER, and path) so exact-match entries still pass through
filter_entry and receive ownership treatment.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7581ecc and 7fefbd6.

📒 Files selected for processing (5)
  • rustfmt.toml
  • src/action/base/create_or_insert_into_file.rs
  • src/action/base/create_or_merge_nix_config.rs
  • src/action/common/create_nix_tree.rs
  • src/action/common/provision_nix.rs
💤 Files with no reviewable changes (1)
  • rustfmt.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/action/base/create_or_insert_into_file.rs
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 7fefbd6 to 31856b5 Compare February 24, 2026 17:54
This will house the final actions for every planner, allowing for easily
extending it in the future.
This will be used in the next commit.
… with new default profile

When we migrate from a single-user install to a multi-user, daemon-based
install, we need to take special care to make sure that the user's
profile doesn't include packages that conflict with the new default
profile (most specifically, Nix itself). We don't want a user to be
stuck with mismatched client-daemon versions!
The receipt format changed.
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 49c8165 to 7d3d72c Compare February 26, 2026 15:13
@cole-h cole-h force-pushed the fixup-single-user-curing branch from 7d3d72c to fd8343d Compare February 26, 2026 15:37
impl NixCommandExt for tokio::process::Command {
fn set_nix_options(&mut self) -> Result<&mut tokio::process::Command, Error> {
Ok(self
.args(["--option", "substitute", "false"])
Copy link
Member

Choose a reason for hiding this comment

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

Since we're dropping the nss cacert we might want to pass --offline or similar

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately, this is used with both the new CLI (nix build) and the old CLI (nix-env).

nix build --offline would work just fine, but nix-env --install etc have no equivalent flag to disable networking:

vin@scadrial ~/w/d/harmonic (7c105d4d3d11 (empty)) 08:45:52 26 Feb 2026 1m 32.6s
;: nix-env --install --offline
error: unknown flag '--offline'

Try 'nix-env --help' for more information.
vin@scadrial ~/w/d/harmonic (7c105d4d3d11 (empty)) [1] 08:46:00 26 Feb 2026 0.4s
;: nix-env --install --no-net
error: unknown flag '--no-net'

Try 'nix-env --help' for more information.
vin@scadrial ~/w/d/harmonic (7c105d4d3d11 (empty)) [1] 08:46:02 26 Feb 2026 19ms
;: nix-env --install --no-networking
error: unknown flag '--no-networking'

Try 'nix-env --help' for more information.
Copy link
Member

Choose a reason for hiding this comment

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

drat

Copy link
Member

@grahamc grahamc left a comment

Choose a reason for hiding this comment

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

lgtm, nice

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

Labels

upload to s3 The labeled PR is allowed to upload its artifacts to S3 for easy testing

2 participants