Skip to content
56 changes: 48 additions & 8 deletions src/nixos-anywhere.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1036,16 +1036,56 @@ main() {
sshConnection="root@${sshHost}"
fi

# Get substituters from the machine and add them to the installer
if [[ ${machineSubstituters} == "y" && -n ${flake} ]]; then
substituters=$(nix --extra-experimental-features 'nix-command flakes' eval --apply toString "${flake}"#"${flakeAttr}".nix.settings.substituters)
trustedPublicKeys=$(nix --extra-experimental-features 'nix-command flakes' eval --apply toString "${flake}"#"${flakeAttr}".nix.settings.trusted-public-keys)

runSsh sh <<SSH || true
if [[ -n ${flake} ]]; then
system_features=$(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}\";
# 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}")
Comment on lines 1044 to 1081
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.


# 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
echo "${nixConfContent}" >> ~/.config/nix/nix.conf
SSH
fi
fi

if [[ ${phases[disko]} == 1 ]]; then
Expand Down
Loading