Skip to content

Conversation

sabify
Copy link

@sabify sabify commented Sep 18, 2025

This is mostly useful when we are building for a specific cpu architecture by specifying nixpkgs.hostPlatform.gcc.arch that needs its companion gccarch-<arch> as system-features.

Summary by CodeRabbit

  • Refactor
    • Streamlined installer behavior when using --flake: single evaluation, fewer remote reads, and automatic detection/deduplication of system features (includes host gcc arch when present).
    • Consolidates feature and substituter/trusted-key resolution into one evaluated flow.
  • Bug Fixes
    • Avoids overwriting user nix.conf by writing only when generated content is non-empty.
  • Chores
    • Simplified internal logic with no changes to user-facing commands or public interfaces.

@sabify sabify force-pushed the system-features branch 2 times, most recently from b4daf60 to 4ce51b3 Compare September 18, 2025 09:22
@sabify sabify marked this pull request as draft September 18, 2025 15:57
@sabify sabify marked this pull request as ready for review September 18, 2025 16:59
Copy link

coderabbitai bot commented Sep 30, 2025

Walkthrough

Updates nix.conf generation in src/nixos-anywhere.sh for --flake: fetches remote system-features once via SSH, runs a single nix eval to build consolidated nix.conf content (system-features, substituters, trusted-public-keys) with deduplication and conditional write to ~/.config/nix/nix.conf only if non-empty.

Changes

Cohort / File(s) Summary
Installer nix.conf generation flow
src/nixos-anywhere.sh
Replace multiple remote evaluations and unconditional key/substituter writes with: one runSshNoTty read for remote features, one nix eval assembling nix.conf (system-features with optional gccarch-<arch>, extra substituters, extra trusted-public-keys, deduplication, conditional inclusion based on settings), and conditional write to ~/.config/nix/nix.conf only when content is non-empty. Preserves non-flake behavior and overall flow.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant I as Installer Script
  participant R as Remote Host (SSH)
  participant N as nix eval
  participant FS as Local FS (~/.config/nix/nix.conf)

  U->>I: Run with --flake
  I->>R: runSshNoTty -> read remote system-features (and gcc arch)
  I->>N: nix eval -> compute nixConfContent (features, substituters, trusted-public-keys)
  N-->>I: nixConfContent (string or empty)
  alt nixConfContent not empty
    I->>FS: Write nixConfContent (preserve formatting)
  else nixConfContent empty
    I-->>U: Skip write
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly signals that the pull request adds support for Nix system-features and directly ties to the primary change of taking system-features into account in the installer script’s flow.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

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

🧹 Nitpick comments (2)
src/nixos-anywhere.sh (2)

954-957: Use consistent variable naming convention.

The variable platform_arch uses underscores instead of camelCase, which is inconsistent with the rest of the script. While the logic to augment system-features with gccarch-<arch> is correct, maintaining consistent naming improves readability.

Apply this diff to improve naming consistency:

-    platform_arch=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nixpkgs.hostPlatform.gcc.arch or "")' "${flake}#${flakeAttr}")
-    if [[ -n ${platform_arch} ]]; then
-      system_features="${system_features} gccarch-${platform_arch}"
+    platformArch=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nixpkgs.hostPlatform.gcc.arch or "")' "${flake}#${flakeAttr}")
+    if [[ -n ${platformArch} ]]; then
+      systemFeatures="${systemFeatures} gccarch-${platformArch}"

962-965: Consider logging when system-features configuration fails.

The || true on line 962 silently ignores all errors from the SSH command, which is consistent with the substituters block above (lines 940-944). However, if writing the system-features configuration fails, the user won't be notified, potentially leading to unexpected build behavior.

Consider adding a warning message when the configuration fails:

-    runSsh sh <<SSH || true
+    if ! runSsh sh <<SSH; then
+      echo "Warning: Failed to configure system-features on the installer" >&2
+    fi
 mkdir -p ~/.config/nix
-echo "system-features = ${system_features}" >> ~/.config/nix/nix.conf
+echo "system-features = ${systemFeatures}" >> ~/.config/nix/nix.conf
 SSH
-  fi

Note: This also assumes the variable naming changes from the previous comments are applied.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af70cbe and 9a6bb22.

📒 Files selected for processing (1)
  • src/nixos-anywhere.sh (1 hunks)
🔇 Additional comments (1)
src/nixos-anywhere.sh (1)

959-960: Deduplication logic validated. Validation shows the pipeline yields an empty string for empty input and correctly deduplicates features.

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

♻️ Duplicate comments (1)
src/nixos-anywhere.sh (1)

1051-1070: Variable naming inconsistency remains unaddressed.

The variables system_features and platform_arch use snake_case, which is inconsistent with the rest of the script's camelCase convention (e.g., sshConnection, flakeAttr, diskoScript).

Based on learnings from the previous review, this naming inconsistency was already flagged but not yet resolved.

Apply this diff to use consistent camelCase naming:

-  # Get system-features with a specific cpu architecture from the machine and add them to the installer
   if [[ -n ${flake} ]]; then
-    system_features=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nix.settings.system-features or "")' "${flake}#${flakeAttr}")
-    if [[ -z ${system_features} ]]; then
-      system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
+    systemFeatures=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nix.settings.system-features or "")' "${flake}#${flakeAttr}")
+    if [[ -z ${systemFeatures} ]]; then
+      systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
     fi
 
-    platform_arch=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nixpkgs.hostPlatform.gcc.arch or "")' "${flake}#${flakeAttr}")
-    if [[ -n ${platform_arch} ]]; then
-      system_features="${system_features} gccarch-${platform_arch}"
+    platformArch=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nixpkgs.hostPlatform.gcc.arch or "")' "${flake}#${flakeAttr}")
+    if [[ -n ${platformArch} ]]; then
+      systemFeatures="${systemFeatures} gccarch-${platformArch}"
     fi
 
     # deduplicate the features
-    system_features=$(echo "${system_features}" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
+    systemFeatures=$(echo "${systemFeatures}" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
 
     runSsh sh <<SSH || true
 mkdir -p ~/.config/nix
-echo "system-features = ${system_features}" >> ~/.config/nix/nix.conf
+echo "system-features = ${systemFeatures}" >> ~/.config/nix/nix.conf
 SSH
   fi
🧹 Nitpick comments (3)
src/nixos-anywhere.sh (3)

1053-1056: Use consistent experimental features flags.

Line 1053 uses 'nix-command flakes' but line 1055 uses only 'nix-command'. While config show doesn't strictly require the flakes feature, using consistent flags throughout the script improves maintainability and prevents confusion.

Apply this diff:

     systemFeatures=$(nix --extra-experimental-features 'nix-command flakes' eval --apply 'x: toString (x.nix.settings.system-features or "")' "${flake}#${flakeAttr}")
     if [[ -z ${systemFeatures} ]]; then
-      systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
+      systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command flakes' config show system-features)
     fi

1063-1064: Edge case: deduplication fails with empty input.

If systemFeatures is empty, the deduplication pipeline will still work correctly but the sed 's/ $//' at the end is unnecessary. However, if systemFeatures contains only whitespace, this could produce unexpected results.

Consider guarding the deduplication and write operation:

+    if [[ -n ${systemFeatures} ]]; then
       # deduplicate the features
       systemFeatures=$(echo "${systemFeatures}" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
 
       runSsh sh <<SSH || true
 mkdir -p ~/.config/nix
 echo "system-features = ${systemFeatures}" >> ~/.config/nix/nix.conf
 SSH
+    fi

1040-1049: Apply similar error handling improvements to substituters block.

Similar to the system-features block, the substituters configuration at lines 1044-1048 uses || true to silence all errors. This could hide legitimate SSH or permission failures.

Apply consistent error handling:

-    runSsh sh <<SSH || true
+    if ! runSsh sh <<SSH; then
+      echo "Warning: Failed to configure substituters in installer environment" >&2
+    fi
 mkdir -p ~/.config/nix
 echo "extra-substituters = ${substituters}" >> ~/.config/nix/nix.conf
 echo "extra-trusted-public-keys = ${trustedPublicKeys}" >> ~/.config/nix/nix.conf
 SSH
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 554757f and 24c7cba.

📒 Files selected for processing (1)
  • src/nixos-anywhere.sh (1 hunks)

@Mic92
Copy link
Member

Mic92 commented Sep 30, 2025

I wanted to avoid evaluating nixos multiple times, so i came up with: 8a49b35

But haven't test it properly yet.

@sabify
Copy link
Author

sabify commented Sep 30, 2025

I wanted to avoid evaluating nixos multiple times, so i came up with: 8a49b35

But haven't test it properly yet.

The applied logic is incomplete. Please look at the following options:

  1. defining both nix.settings.system-features and nixpkgs.hostPlatform.gcc.arch in the flake configuration.
  2. defining only nix.settings.system-features in the flake configuration.
  3. defining only nixpkgs.hostPlatform.gcc.arch in the flake configuration.
  4. defining none of them in the flake configuration.

Your proposal addresses the first and second options in which you intentionally define system-features. For addressing the other two, we have to fetch the default target machine's system-features before adding the gccarch feature to it, otherwise it will override the default features and won't work anymore.

@Mic92
Copy link
Member

Mic92 commented Sep 30, 2025

Okay, could switch the code over to use a single nixos eval to get all this information out of the nixos configuration, similar to my code. It might be also possible to load the nix code from an external file to make it a bit prettier (we have to get useSubstitute injected though - maybe with a @placeholder@ like we do for @TAR_COMMAND@.

@Mic92
Copy link
Member

Mic92 commented Sep 30, 2025

Otherwise nixos-facter might even provide more information about the target cpu.

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: 3

♻️ Duplicate comments (1)
src/nixos-anywhere.sh (1)

1040-1040: Use camelCase and avoid suppressing real errors.

The variable system_features uses snake_case, which is inconsistent with the rest of the script (e.g., sshConnection, flakeAttr, nixConfContent on line 1043). Additionally, || true suppresses all errors, including legitimate SSH connection failures.

Apply this diff:

-    system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || true)
+    systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || echo "")

Then update line 1054 to reference systemFeatures:

-            remoteFeaturesStr = \"${system_features}\";
+            remoteFeaturesStr = \"${systemFeatures}\";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24c7cba and b5793e8.

📒 Files selected for processing (1)
  • src/nixos-anywhere.sh (1 hunks)

Comment on lines 1043 to 1080
nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
config:
let
settings = config.nix.settings or {};
gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
# Check if system-features are defined in configuration
configFeatures = settings.system-features or null;
hasConfigFeatures = configFeatures != null && configFeatures != [];
remoteFeatures = let
remoteFeaturesStr = \"${system_features}\";
# Parse remote features string (space-separated) into list
remoteFeaturesList = if remoteFeaturesStr != \"\" then
builtins.filter (x: x != \"\") (builtins.split \" +\" remoteFeaturesStr)
else [];
in remoteFeaturesList;
# Combine base features (config or remote) with platform-specific features
baseFeatures = if hasConfigFeatures then configFeatures else remoteFeatures;
# At least one of nix.settings.system-features or nixpkgs.hostPlatform.gcc.arch has been explicitly defined
allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []) else [];
# Deduplicate using listToAttrs trick
uniqueFeatures = builtins.attrNames (builtins.listToAttrs (map (f: { name = f; value = true; }) allFeatures));
substituters = builtins.toString (settings.substituters or []);
trustedPublicKeys = builtins.toString (settings.trusted-public-keys or []);
systemFeatures = builtins.toString uniqueFeatures;
# Helper function for optional config lines
optionalLine = cond: line: if cond then line + \"\n\" else \"\";
useSubstituters = \"${machineSubstituters}\" == \"y\";
in
optionalLine (useSubstituters && substituters != \"\") \"extra-substituters = \${substituters}\"
+ optionalLine (useSubstituters && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
+ optionalLine (systemFeatures != \"\") \"system-features = \${systemFeatures}\"
" "${flake}#${flakeAttr}")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for nix eval.

The nix eval command (line 1043) has no error handling. If the flake evaluation fails (e.g., syntax error, missing attribute, network issue), the script will continue with an empty or partial nixConfContent, potentially causing silent failures.

Consider adding error handling:

     # First, try to evaluate all nix settings from the flake in one go
+    if ! nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
-    nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
       config:
       ...
-    " "${flake}#${flakeAttr}")
+    " "${flake}#${flakeAttr}"); then
+      echo "Warning: Failed to evaluate nix settings from flake" >&2
+      nixConfContent=""
+    fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
config:
let
settings = config.nix.settings or {};
gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
# Check if system-features are defined in configuration
configFeatures = settings.system-features or null;
hasConfigFeatures = configFeatures != null && configFeatures != [];
remoteFeatures = let
remoteFeaturesStr = \"${system_features}\";
# Parse remote features string (space-separated) into list
remoteFeaturesList = if remoteFeaturesStr != \"\" then
builtins.filter (x: x != \"\") (builtins.split \" +\" remoteFeaturesStr)
else [];
in remoteFeaturesList;
# Combine base features (config or remote) with platform-specific features
baseFeatures = if hasConfigFeatures then configFeatures else remoteFeatures;
# At least one of nix.settings.system-features or nixpkgs.hostPlatform.gcc.arch has been explicitly defined
allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []) else [];
# Deduplicate using listToAttrs trick
uniqueFeatures = builtins.attrNames (builtins.listToAttrs (map (f: { name = f; value = true; }) allFeatures));
substituters = builtins.toString (settings.substituters or []);
trustedPublicKeys = builtins.toString (settings.trusted-public-keys or []);
systemFeatures = builtins.toString uniqueFeatures;
# Helper function for optional config lines
optionalLine = cond: line: if cond then line + \"\n\" else \"\";
useSubstituters = \"${machineSubstituters}\" == \"y\";
in
optionalLine (useSubstituters && substituters != \"\") \"extra-substituters = \${substituters}\"
+ optionalLine (useSubstituters && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
+ optionalLine (systemFeatures != \"\") \"system-features = \${systemFeatures}\"
" "${flake}#${flakeAttr}")
# First, try to evaluate all nix settings from the flake in one go
if ! nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
config:
let
settings = config.nix.settings or {};
gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
# Check if system-features are defined in configuration
configFeatures = settings.system-features or null;
hasConfigFeatures = configFeatures != null && configFeatures != [];
remoteFeatures = let
remoteFeaturesStr = \"${system_features}\";
# Parse remote features string (space-separated) into list
remoteFeaturesList = if remoteFeaturesStr != \"\" then
builtins.filter (x: x != \"\") (builtins.split \" +\" remoteFeaturesStr)
else [];
in remoteFeaturesList;
# Combine base features (config or remote) with platform-specific features
baseFeatures = if hasConfigFeatures then configFeatures else remoteFeatures;
# At least one of nix.settings.system-features or nixpkgs.hostPlatform.gcc.arch has been explicitly defined
allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []) else [];
# Deduplicate using listToAttrs trick
uniqueFeatures = builtins.attrNames (builtins.listToAttrs (map (f: { name = f; value = true; }) allFeatures));
substituters = builtins.toString (settings.substituters or []);
trustedPublicKeys = builtins.toString (settings.trusted-public-keys or []);
systemFeatures = builtins.toString uniqueFeatures;
# Helper function for optional config lines
optionalLine = cond: line: if cond then line + \"\n\" else \"\";
useSubstituters = \"${machineSubstituters}\" == \"y\";
in
optionalLine (useSubstituters && substituters != \"\") \"extra-substituters = \${substituters}\"
optionalLine (useSubstituters && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
optionalLine (systemFeatures != \"\") \"system-features = \${systemFeatures}\"
" "${flake}#${flakeAttr}"); then
echo "Warning: Failed to evaluate nix settings from flake" >&2
nixConfContent=""
fi

⚠️ Potential issue | 🟠 Major

Quote bash variable interpolation and fix builtins.split usage.

Two issues:

  1. Shell injection risk: Line 1054 interpolates the bash variable ${system_features} directly into a Nix string without escaping. If system_features contains quotes, backslashes, or newlines, it could break the Nix expression or cause injection.

  2. Incorrect parsing: Line 1057 uses builtins.split " +" which returns a list containing both matched strings and null values for the separators. The current code doesn't filter out the nulls, potentially causing downstream issues.

Apply this diff to fix both issues:

     # First, try to evaluate all nix settings from the flake in one go
-    nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
+    # Escape the bash variable for safe interpolation into Nix
+    escapedSystemFeatures=$(printf '%s' "$systemFeatures" | sed 's/\\/\\\\/g; s/"/\\"/g')
+    nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
       config:
       let
         settings = config.nix.settings or {};
         gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
 
         # Check if system-features are defined in configuration
         configFeatures = settings.system-features or null;
         hasConfigFeatures = configFeatures != null && configFeatures != [];
 
         remoteFeatures = let
-            remoteFeaturesStr = \"${system_features}\";
+            remoteFeaturesStr = \"${escapedSystemFeatures}\";
             # Parse remote features string (space-separated) into list
             remoteFeaturesList = if remoteFeaturesStr != \"\" then
-              builtins.filter (x: x != \"\") (builtins.split \" +\" remoteFeaturesStr)
+              builtins.filter (x: builtins.isString x && x != \"\") (builtins.split \" +\" remoteFeaturesStr)
             else [];
           in remoteFeaturesList;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/nixos-anywhere.sh around lines 1043 to 1080, the Nix eval interpolates
the shell variable ${system_features} directly into the --apply string (risking
shell/Nix injection) and uses builtins.split with a regex that produces
empty/null entries; change the invocation to pass the value safely with --argstr
system_features "${system_features}" (remove the inline \"${system_features}\"
interpolation) and inside the Nix expression set remoteFeaturesStr =
system_features; then replace the current split logic with a space-split plus
explicit empty-string filtering, e.g. remoteFeaturesList = if remoteFeaturesStr
!= "" then builtins.filter (x: x != "") (builtins.split " " remoteFeaturesStr)
else []; this removes injection risk and drops empty entries from the feature
list.

@sabify
Copy link
Author

sabify commented Sep 30, 2025

@Mic92 I made changes according to your suggested script. Please check it out.

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: 0

♻️ Duplicate comments (2)
src/nixos-anywhere.sh (2)

1083-1088: Critical: Command injection vulnerability in heredoc.

Line 1086 uses unquoted ${nixConfContent} within the heredoc. If nixConfContent contains backticks, $(), or other special characters, this could cause command injection or shell parsing errors on the remote host.

Apply this diff to safely write the content:

     # Write to nix.conf if we have any content
     if [[ -n ${nixConfContent} ]]; then
-      runSsh sh <<SSH
+      runSsh sh <<'SSH'
 mkdir -p ~/.config/nix
-echo "${nixConfContent}" >> ~/.config/nix/nix.conf
+cat >> ~/.config/nix/nix.conf
 SSH
+      printf '%s\n' "$nixConfContent" | runSsh 'cat >> ~/.config/nix/nix.conf'
     fi

Note: Using <<'SSH' (quoted delimiter) prevents variable expansion. The content is then piped separately using printf which safely handles special characters.


1043-1080: Critical: Shell injection vulnerability and missing error handling.

Three critical issues:

  1. Shell injection risk: Lines 1054 and 1075 interpolate bash variables ${system_features} and ${machineSubstituters} directly into the Nix expression string. If these variables contain special characters like quotes, backticks, $(), or newlines, they could break the Nix expression or enable command injection.

  2. Incorrect list parsing: Line 1057 uses builtins.split " +" which returns a list containing both matched strings and null values for separators. While line 1057 filters empty strings, it doesn't filter nulls, which could cause downstream issues.

  3. No error handling: The nix eval command has no error handling. If evaluation fails (syntax error, missing attribute, network issue), the script continues with empty nixConfContent, causing silent failures.

Apply this diff to fix all three issues:

   if [[ -n ${flake} ]]; then
     systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || true)
 
     # First, try to evaluate all nix settings from the flake in one go
-    nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
+    if ! nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw \
+      --argstr systemFeatures "${systemFeatures}" \
+      --argstr useSubstituters "${machineSubstituters}" \
+      --apply "
+      systemFeatures: useSubstituters:
       config:
       let
         settings = config.nix.settings or {};
         gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
 
         # Check if system-features are defined in configuration
         configFeatures = settings.system-features or null;
         hasConfigFeatures = configFeatures != null && configFeatures != [];
 
         remoteFeatures = let
-            remoteFeaturesStr = \"${system_features}\";
             # Parse remote features string (space-separated) into list
-            remoteFeaturesList = if remoteFeaturesStr != \"\" then
-              builtins.filter (x: x != \"\") (builtins.split \" +\" remoteFeaturesStr)
+            remoteFeaturesList = if systemFeatures != \"\" then
+              builtins.filter (x: builtins.isString x && x != \"\") (builtins.split \" \" systemFeatures)
             else [];
           in remoteFeaturesList;
 
         # Combine base features (config or remote) with platform-specific features
         baseFeatures = if hasConfigFeatures then configFeatures else remoteFeatures;
         # At least one of nix.settings.system-features or nixpkgs.hostPlatform.gcc.arch has been explicitly defined
         allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []) else [];
 
         # Deduplicate using listToAttrs trick
         uniqueFeatures = builtins.attrNames (builtins.listToAttrs (map (f: { name = f; value = true; }) allFeatures));
 
         substituters = builtins.toString (settings.substituters or []);
         trustedPublicKeys = builtins.toString (settings.trusted-public-keys or []);
-        systemFeatures = builtins.toString uniqueFeatures;
+        systemFeaturesStr = builtins.toString uniqueFeatures;
 
         # Helper function for optional config lines
         optionalLine = cond: line: if cond then line + \"\n\" else \"\";
-        useSubstituters = \"${machineSubstituters}\" == \"y\";
+        useSubs = useSubstituters == \"y\";
       in
-        optionalLine (useSubstituters && substituters != \"\") \"extra-substituters = \${substituters}\"
-        + optionalLine (useSubstituters && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
-        + optionalLine (systemFeatures != \"\") \"system-features = \${systemFeatures}\"
-    " "${flake}#${flakeAttr}")
+        optionalLine (useSubs && substituters != \"\") \"extra-substituters = \${substituters}\"
+        + optionalLine (useSubs && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
+        + optionalLine (systemFeaturesStr != \"\") \"system-features = \${systemFeaturesStr}\"
+    " "${flake}#${flakeAttr}"); then
+      echo "Warning: Failed to evaluate nix settings from flake" >&2
+      nixConfContent=""
+    fi
🧹 Nitpick comments (1)
src/nixos-anywhere.sh (1)

1039-1040: Use consistent variable naming convention.

The variable system_features uses snake_case, which is inconsistent with the rest of the script's camelCase convention (e.g., flakeAttr, sshConnection, nixConfContent).

Apply this diff to use camelCase consistently:

   if [[ -n ${flake} ]]; then
-    system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || true)
+    systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || true)
 
     # First, try to evaluate all nix settings from the flake in one go
     nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
       config:
       let
         settings = config.nix.settings or {};
         gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
 
         # Check if system-features are defined in configuration
         configFeatures = settings.system-features or null;
         hasConfigFeatures = configFeatures != null && configFeatures != [];
 
         remoteFeatures = let
-            remoteFeaturesStr = \"${system_features}\";
+            remoteFeaturesStr = \"${systemFeatures}\";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5793e8 and e174e75.

📒 Files selected for processing (1)
  • src/nixos-anywhere.sh (1 hunks)

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: 2

♻️ Duplicate comments (1)
src/nixos-anywhere.sh (1)

1044-1081: Use --argstr to avoid shell injection risk and add error handling.

Two concerns:

  1. Shell injection risk: Lines 1055 and 1076 interpolate bash variables (${system_features}, ${machineSubstituters}) directly into the Nix expression string. While the escaping on line 1042 mitigates some risk, using --argstr or --arg is the safer, idiomatic approach for passing external values into Nix expressions.

  2. Missing error handling: The nix eval command (line 1044) has no error handling. If evaluation fails (syntax error, missing attribute, network issue), the script continues with an empty nixConfContent, potentially causing silent failures.

Apply this diff to use --argstr for safe parameter passing:

     # Escape the bash variable for safe interpolation into Nix
-    escapedSystemFeatures="$(printf '%s' "$systemFeatures" | sed 's/\\/\\\\/g; s/"/\\"/g')"
     # First, try to evaluate all nix settings from the flake in one go
-    nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
+    if ! nixConfContent=$(nix --extra-experimental-features 'nix-command flakes' eval --raw \
+      --argstr systemFeatures "$systemFeatures" \
+      --argstr machineSubstituters "$machineSubstituters" \
+      --apply "
+      systemFeatures: machineSubstituters:
       config:
       let
         settings = config.nix.settings or {};
@@ -1052,7 +1054,7 @@
         hasConfigFeatures = configFeatures != null && configFeatures != [];
 
         remoteFeatures = let
-            remoteFeaturesStr = \"${escapedSystemFeatures}\";
+            remoteFeaturesStr = systemFeatures;
             # Parse remote features string (space-separated) into list
             remoteFeaturesList = if remoteFeaturesStr != \"\" then
               builtins.filter (x: builtins.isString x && x != \"\") (builtins.split \" +\" remoteFeaturesStr)
@@ -1073,12 +1075,15 @@
 
         # Helper function for optional config lines
         optionalLine = cond: line: if cond then line + \"\n\" else \"\";
-        useSubstituters = \"${machineSubstituters}\" == \"y\";
+        useSubstituters = machineSubstituters == \"y\";
       in
         optionalLine (useSubstituters && substituters != \"\") \"extra-substituters = \${substituters}\"
         + optionalLine (useSubstituters && trustedPublicKeys != \"\") \"extra-trusted-public-keys = \${trustedPublicKeys}\"
         + optionalLine (systemFeatures != \"\") \"system-features = \${systemFeatures}\"
-    " "${flake}#${flakeAttr}")
+    " "${flake}#${flakeAttr}"); then
+      echo "Warning: Failed to evaluate nix settings from flake" >&2
+      nixConfContent=""
+    fi
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e174e75 and 36a39a7.

📒 Files selected for processing (1)
  • src/nixos-anywhere.sh (1 hunks)
🔇 Additional comments (1)
src/nixos-anywhere.sh (1)

1062-1065: Case 4 (neither gccArch nor system-features defined) may not be handled as intended.

According to the PR objectives, when neither nix.settings.system-features nor nixpkgs.hostPlatform.gcc.arch is defined (case 4), the target machine's default system-features should be used. However, the current logic on line 1065:

allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ ... else [];

Sets allFeatures to an empty list when both are undefined, resulting in no system-features line being written to nix.conf. This means the installer environment will use its own default features rather than the target machine's features fetched on line 1040.

Verify if this behavior is intentional:

If case 4 should preserve the remote features, apply this diff:

         # Combine base features (config or remote) with platform-specific features
         baseFeatures = if hasConfigFeatures then configFeatures else remoteFeatures;
-        # At least one of nix.settings.system-features or nixpkgs.hostPlatform.gcc.arch has been explicitly defined
-        allFeatures = if (gccArch != null) || hasConfigFeatures then baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []) else [];
+        # Use base features (from config or remote) and optionally add gccarch
+        allFeatures = baseFeatures ++ (if gccArch != null then [\"gccarch-\${gccArch}\"] else []);

This ensures that:

  • Case 1 (both defined): config features + gccarch
  • Case 2 (only config): config features
  • Case 3 (only gccArch): remote features + gccarch
  • Case 4 (neither): remote features

Otherwise, if the current behavior is correct (don't write system-features when neither is defined), please clarify this in a comment.

Comment on lines +1039 to +1043
if [[ -n ${flake} ]]; then
system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
# Escape the bash variable for safe interpolation into Nix
system_features="$(printf '%s' "$system_features" | sed 's/\\/\\\\/g; s/"/\\"/g')"
# First, try to evaluate all nix settings from the flake in one go
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use consistent variable naming and add error handling for remote SSH command.

Two issues:

  1. Naming inconsistency: system_features uses snake_case, while the rest of the script uses camelCase (e.g., sshConnection, flakeAttr, nixConfContent).

  2. Missing error handling: Line 1040 will cause the script to abort under set -euo pipefail if the SSH command fails (e.g., network issue, nix not found on remote).

Apply this diff:

   if [[ -n ${flake} ]]; then
-    system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
+    systemFeatures=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features 2>/dev/null || echo "")
     # Escape the bash variable for safe interpolation into Nix
-    system_features="$(printf '%s' "$system_features" | sed 's/\\/\\\\/g; s/"/\\"/g')"
+    escapedSystemFeatures="$(printf '%s' "$systemFeatures" | sed 's/\\/\\\\/g; s/"/\\"/g')"

Then update line 1055 to use the renamed variable:

         remoteFeatures = let
-            remoteFeaturesStr = \"${system_features}\";
+            remoteFeaturesStr = \"${escapedSystemFeatures}\";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ -n ${flake} ]]; then
system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
# Escape the bash variable for safe interpolation into Nix
system_features="$(printf '%s' "$system_features" | sed 's/\\/\\\\/g; s/"/\\"/g')"
# First, try to evaluate all nix settings from the flake in one go
if [[ -n ${flake} ]]; then
# Fetch remote system-features, but never fail under set -euo pipefail
systemFeatures=$(runSshNoTty -o ConnectTimeout=10 \
nix --extra-experimental-features 'nix-command' config show system-features \
2>/dev/null || echo "")
# Escape the bash variable for safe interpolation into Nix
escapedSystemFeatures="$(printf '%s' "$systemFeatures" \
| sed 's/\\/\\\\/g; s/"/\\"/g')"
# First, try to evaluate all nix settings from the flake in one go
nixConfContent=$(
nix --extra-experimental-features 'nix-command flakes' eval --raw --apply "
config:
let
settings = config.nix.settings or {};
gccArch = config.nixpkgs.hostPlatform.gcc.arch or null;
# Check if system-features are defined in configuration
configFeatures = settings.system-features or null;
hasConfig = configFeatures != null && configFeatures != [];
# Fallback to the escaped remote features if none in the flake
remoteFeatures = let
remoteFeaturesStr = \"${escapedSystemFeatures}\";
remoteFeaturesList = if remoteFeaturesStr != \"\" then
builtins.filter (x: builtins.isString x && x != \"\")
(builtins.split \" +\" remoteFeaturesStr)
else
[];
in remoteFeaturesList;
in
# … rest of the nixConfContent construction …
" "${flake}#${flakeAttr}"
)
🤖 Prompt for AI Agents
In src/nixos-anywhere.sh around lines 1039-1043, rename the snake_case variable
system_features to camelCase systemFeatures for consistency with
sshConnection/flakeAttr/nixConfContent, and preserve the escaping step for
quotes/backslashes; additionally, run the remote SSH command with explicit error
handling: capture both its stdout and exit code, and if the SSH command fails
log a clear error including the exit code/output and exit non-zero to avoid an
unhelpful abrupt failure under set -euo pipefail. After renaming, update the
later usage on line 1055 to reference systemFeatures.

Comment on lines +1083 to +1092
# Write to nix.conf if we have any content
if [[ -n ${nixConfContent} ]]; then
runSsh sh <<SSH
mkdir -p ~/.config/nix
echo "extra-substituters = ${substituters}" >> ~/.config/nix/nix.conf
echo "extra-trusted-public-keys = ${trustedPublicKeys}" >> ~/.config/nix/nix.conf
printf '%s\n' "\$(cat <<'CONTENT'
${nixConfContent}
CONTENT
)" >> ~/.config/nix/nix.conf
SSH
fi
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for nix.conf write operation.

The SSH command that writes to ~/.config/nix/nix.conf has no error handling. Failures (SSH connection issues, permission problems, disk full) will be silently ignored, leaving the system in an inconsistent state.

Apply this diff:

     # Write to nix.conf if we have any content
     if [[ -n ${nixConfContent} ]]; then
-      runSsh sh <<SSH
+      if ! runSsh sh <<SSH; then
+        echo "Warning: Failed to write nix.conf in installer environment" >&2
+      fi
 mkdir -p ~/.config/nix
 printf '%s\n' "\$(cat <<'CONTENT'
 ${nixConfContent}
 CONTENT
 )" >> ~/.config/nix/nix.conf
 SSH
     fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Write to nix.conf if we have any content
if [[ -n ${nixConfContent} ]]; then
runSsh sh <<SSH
mkdir -p ~/.config/nix
echo "extra-substituters = ${substituters}" >> ~/.config/nix/nix.conf
echo "extra-trusted-public-keys = ${trustedPublicKeys}" >> ~/.config/nix/nix.conf
printf '%s\n' "\$(cat <<'CONTENT'
${nixConfContent}
CONTENT
)" >> ~/.config/nix/nix.conf
SSH
fi
# Write to nix.conf if we have any content
if [[ -n ${nixConfContent} ]]; then
if ! runSsh sh <<SSH; then
echo "Warning: Failed to write nix.conf in installer environment" >&2
fi
mkdir -p ~/.config/nix
printf '%s\n' "\$(cat <<'CONTENT'
${nixConfContent}
CONTENT
)" >> ~/.config/nix/nix.conf
SSH
fi

runSsh sh <<SSH || true
if [[ -n ${flake} ]]; then
system_features=$(runSshNoTty -o ConnectTimeout=10 nix --extra-experimental-features 'nix-command' config show system-features)
Copy link
Member

Choose a reason for hiding this comment

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

why is the list of current system-features important? Should this not be overruled by what the user has specified in the machine?

Copy link
Author

Choose a reason for hiding this comment

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

Sometimes you may not want to explicitly provide system-features and only want to configure gcc.arch, in that case the default target platform feature set needs to append gccarch-${cpu-arch} to it. If you don't do this and only set the gccarch feature, it will override (not append) the whole default features and the build will fail.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants