Skip to content

Commit 0dce56f

Browse files
committed
nixos-rebuild-ng: validate NixOS configuration path
When `path://` or `git+file://` protocol is used in Flake mode (that is the most common case since we normalize the paths, see PR #375493) and the current working directory in a symlink pointing base store path to the Nix store (e.g., /run/opengl-driver/lib), there is a nasty bug where Nix resolves the path as the Nix store path of the current derivation instead of the target derivation. Since we blindly activate this path, this can corrupt the installation and break some other activation scripts, like `systemd-boot-builder.py`. While it is possible to recover this situation using `nix-env -p /nix/var/nix/profiles/system --delete-generations old`, this is far from ideal. This commit solves it by validating that the resolved NixOS configuration path includes at least `$out/nixos-version`. I am not sure if this is going to break some cases so there is a escape hatch in the form of the environment variable `NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM`, but in general it looks safe.
1 parent 2576cf9 commit 0dce56f

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
from pathlib import Path
77
from subprocess import CalledProcessError, run
8+
from textwrap import dedent
89
from typing import Final, assert_never
910

1011
from . import nix, tmpdir
@@ -329,6 +330,30 @@ def reexec(
329330
os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
330331

331332

333+
def validate_nixos_config(path_to_config: Path) -> None:
334+
if not (path_to_config / "nixos-version").exists() and not os.environ.get(
335+
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
336+
):
337+
msg = dedent(
338+
# the lowercase for the first letter below is proposital
339+
f"""
340+
your NixOS configuration path seems to be missing essential files.
341+
To avoid corrupting your current NixOS installation, the activation will abort.
342+
343+
This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
344+
This is the evaluated NixOS configuration path: {path_to_config}.
345+
Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
346+
347+
If you think this is a mistake, you can set the environment variable
348+
NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
349+
and re-run the command to continue.
350+
Please open an issue if this is the case.
351+
"""
352+
).strip()
353+
logger.error(msg)
354+
sys.exit(1)
355+
356+
332357
def execute(argv: list[str]) -> None:
333358
args, args_groups = parse_args(argv)
334359

@@ -488,6 +513,7 @@ def validate_image_variant(variants: ImageVariants) -> None:
488513
copy_flags=copy_flags,
489514
)
490515
if action in (Action.SWITCH, Action.BOOT):
516+
validate_nixos_config(path_to_config)
491517
nix.set_profile(
492518
profile,
493519
path_to_config,

pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,11 @@ def test_reexec_flake(
213213
)
214214

215215

216-
@patch.dict(os.environ, {}, clear=True)
216+
@patch.dict(
217+
os.environ,
218+
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
219+
clear=True,
220+
)
217221
@patch("subprocess.run", autospec=True)
218222
def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
219223
nixpkgs_path = tmp_path / "nixpkgs"
@@ -291,7 +295,15 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
291295
"boot",
292296
],
293297
check=True,
294-
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "0"}}),
298+
**(
299+
DEFAULT_RUN_KWARGS
300+
| {
301+
"env": {
302+
"NIXOS_INSTALL_BOOTLOADER": "0",
303+
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
304+
}
305+
}
306+
),
295307
),
296308
]
297309
)
@@ -421,7 +433,11 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
421433
)
422434

423435

424-
@patch.dict(os.environ, {}, clear=True)
436+
@patch.dict(
437+
os.environ,
438+
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
439+
clear=True,
440+
)
425441
@patch("subprocess.run", autospec=True)
426442
def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
427443
config_path = tmp_path / "test"
@@ -498,13 +514,25 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
498514
"switch",
499515
],
500516
check=True,
501-
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "1"}}),
517+
**(
518+
DEFAULT_RUN_KWARGS
519+
| {
520+
"env": {
521+
"NIXOS_INSTALL_BOOTLOADER": "1",
522+
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
523+
}
524+
}
525+
),
502526
),
503527
]
504528
)
505529

506530

507-
@patch.dict(os.environ, {}, clear=True)
531+
@patch.dict(
532+
os.environ,
533+
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
534+
clear=True,
535+
)
508536
@patch("subprocess.run", autospec=True)
509537
@patch("uuid.uuid4", autospec=True)
510538
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
@@ -714,7 +742,11 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
714742
)
715743

716744

717-
@patch.dict(os.environ, {}, clear=True)
745+
@patch.dict(
746+
os.environ,
747+
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
748+
clear=True,
749+
)
718750
@patch("subprocess.run", autospec=True)
719751
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
720752
def test_execute_nix_switch_flake_target_host(
@@ -817,7 +849,11 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
817849
)
818850

819851

820-
@patch.dict(os.environ, {}, clear=True)
852+
@patch.dict(
853+
os.environ,
854+
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
855+
clear=True,
856+
)
821857
@patch("subprocess.run", autospec=True)
822858
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
823859
def test_execute_nix_switch_flake_build_host(

0 commit comments

Comments
 (0)