staging-nixos merge for 2025-10-12 (#451326)

This commit is contained in:
K900
2025-10-12 12:36:06 +00:00
committed by GitHub
15 changed files with 404 additions and 65 deletions
+24 -5
View File
@@ -36,6 +36,24 @@ in
options = {
environment.shell.enable = lib.mkEnableOption "" // {
default = true;
internal = true;
description = ''
Whether to enable the shell environment.
This does NOT necessarily disable all shells on your system. Instead it
just disables the shell environment that configures user shells (i.e.
those in `environment.shells`).
System services might still depend on and use shells even if this
option is set to false.
Only set this option if you're sure that you can recover from potential
issues.
'';
};
environment.variables = lib.mkOption {
default = { };
example = {
@@ -175,10 +193,10 @@ in
};
environment.binsh = lib.mkOption {
default = "${config.system.build.binsh}/bin/sh";
defaultText = lib.literalExpression ''"''${config.system.build.binsh}/bin/sh"'';
default = null;
defaultText = lib.literalExpression ''"''${pkgs.bashInteractive}/bin/sh"'';
example = lib.literalExpression ''"''${pkgs.dash}/bin/dash"'';
type = lib.types.path;
type = lib.types.nullOr lib.types.path;
visible = false;
description = ''
The shell executable that is linked system-wide to
@@ -201,9 +219,10 @@ in
};
config = {
config = lib.mkIf cfg.shell.enable {
system.build.binsh = pkgs.bashInteractive;
# Set this here so its enabled only when shell support is enabled
environment.binsh = lib.mkDefault "${pkgs.bashInteractive}/bin/sh";
# Set session variables in the shell as well. This is usually
# unnecessary, but it allows changes to session variables to take
+46
View File
@@ -0,0 +1,46 @@
{ lib, ... }:
{
# Bashless builds on perlless
imports = [ ./perlless.nix ];
# Remove bash from activation
system.nixos-init.enable = lib.mkDefault true;
system.activatable = lib.mkDefault false;
environment.shell.enable = lib.mkDefault false;
programs.bash.enable = lib.mkDefault false;
# Random bash remnants
environment.corePackages = lib.mkForce [ ];
# Contains bash completions
nix.enable = lib.mkDefault false;
# The fuse{,3} package contains a runtime dependency on bash.
programs.fuse.enable = lib.mkDefault false;
documentation.man.man-db.enable = lib.mkDefault false;
# autovt depends on bash
console.enable = lib.mkDefault false;
# dhcpcd and openresolv depend on bash
networking.useNetworkd = lib.mkDefault true;
# bcache tools depend on bash.
boot.bcache.enable = lib.mkDefault false;
# iptables depends on bash and nixos-firewall-tool is a bash script
networking.firewall.enable = lib.mkDefault false;
# the wrapper script is in bash
security.enableWrappers = lib.mkDefault false;
# kexec script is written in bash
boot.kexec.enable = lib.mkDefault false;
# Relies on bash scripts
powerManagement.enable = lib.mkDefault false;
# Has some bash inside
systemd.shutdownRamfs.enable = lib.mkDefault false;
# Relies on the gzip command which depends on bash
services.logrotate.enable = lib.mkDefault false;
# Service relies on bash scripts
services.timesyncd.enable = lib.mkDefault false;
# Check that the system does not contain a Nix store path that contains the
# string "bash".
system.forbiddenDependenciesRegexes = [ "bash" ];
}
+2
View File
@@ -158,6 +158,8 @@ in
This must not be a store path, since the path is
used outside the store (in particular in /etc/passwd).
'';
# /bin/sh is also the compiled in default of the shadow package.
default = "/bin/sh";
example = lib.literalExpression "pkgs.zsh";
type = lib.types.either lib.types.path lib.types.shellPackage;
};
@@ -376,7 +376,9 @@ in
);
# End if legacy environment variables
preSwitchCheck = config.system.preSwitchChecksScript;
preSwitchCheck = lib.mkIf (
config.system.preSwitchChecks != { }
) config.system.preSwitchChecksScript;
# Not actually used in the builder. `passedChecks` is just here to create
# the build dependencies. Checks are similar to build dependencies in the
+27 -8
View File
@@ -325,6 +325,22 @@ in
'';
};
shell.enable = lib.mkEnableOption "" // {
default = config.environment.shell.enable;
internal = true;
description = ''
Whether to enable a shell in the initrd.
In contrast to `environment.shell.enable`, this option actually
strictly disables all shells in the initrd because they're not copied
into it anymore. Paths that use a shell (e.g. via the `script` option),
will break if this option is set.
Only set this option if you're sure that you can recover from potential
issues.
'';
};
units = mkOption {
description = "Definition of systemd units.";
default = { };
@@ -460,13 +476,15 @@ in
++ lib.optional (config.boot.initrd.systemd.root == "gpt-auto") "rw";
boot.initrd.systemd = {
# bashInteractive is easier to use and also required by debug-shell.service
initrdBin = [
pkgs.bashInteractive
pkgs.coreutils
cfg.package
]
++ lib.optional (config.system.build.kernel.config.isYes "MODULES") cfg.package.kmod;
++ lib.optional (config.system.build.kernel.config.isYes "MODULES") cfg.package.kmod
++ lib.optionals cfg.shell.enable [
# bashInteractive is easier to use and also required by debug-shell.service
pkgs.bashInteractive
];
extraBin = {
less = "${pkgs.less}/bin/less";
mount = "${cfg.package.util-linux}/bin/mount";
@@ -550,11 +568,6 @@ in
"${cfg.package.util-linux}/bin/umount"
"${cfg.package.util-linux}/bin/sulogin"
# required for services generated with writeShellScript and friends
pkgs.runtimeShell
# some tools like xfs still want the sh symlink
"${pkgs.bashNonInteractive}/bin"
# so NSS can look up usernames
"${pkgs.glibc}/lib/libnss_files.so.2"
@@ -566,6 +579,12 @@ in
++ lib.optionals config.system.nixos-init.enable [
"${config.system.nixos-init.package}/bin/initrd-init"
]
++ lib.optionals cfg.shell.enable [
# required for services generated with writeShellScript and friends
pkgs.runtimeShell
# some tools like xfs still want the sh symlink
"${pkgs.bashNonInteractive}/bin"
]
++ jobScripts
++ map (c: builtins.removeAttrs c [ "text" ]) (builtins.attrValues cfg.contents);
+11 -6
View File
@@ -85,6 +85,9 @@ in
{
options.testing = {
backdoor = lib.mkEnableOption "backdoor service in stage 2" // {
default = true;
};
initrdBackdoor = lib.mkEnableOption ''
backdoor.service in initrd. Requires
@@ -107,12 +110,14 @@ in
}
];
systemd.services.backdoor = lib.mkMerge [
backdoorService
{
wantedBy = [ "multi-user.target" ];
}
];
systemd.services.backdoor = lib.mkIf cfg.backdoor (
lib.mkMerge [
backdoorService
{
wantedBy = [ "multi-user.target" ];
}
]
);
boot.initrd.systemd = lib.mkMerge [
{
@@ -0,0 +1,78 @@
{
nixos,
stdenvNoCC,
jq,
zstd,
cpio,
}:
let
machine = nixos (
{ lib, modulesPath, ... }:
{
imports = [ "${modulesPath}/profiles/bashless.nix" ];
fileSystems."/" = {
device = "/dev/disk/by-partlabel/root";
fsType = "ext4";
};
system.stateVersion = lib.trivial.release;
}
);
in
{
# Keep this around for easier debugging, e.g. with nix why-depends.
inherit (machine) toplevel;
machine = stdenvNoCC.mkDerivation {
name = "bashless-closure-machine";
__structuredAttrs = true;
exportReferencesGraph.closure = [ machine.toplevel ];
preferLocalBuild = true;
nativeBuildInputs = [
jq
];
buildCommand = ''
set +e
jq -r '.closure[].path' < "$NIX_ATTRS_JSON_FILE" | grep bash
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Error: toplevel contains bash"
exit 1
fi
touch $out
'';
};
initrd = stdenvNoCC.mkDerivation {
name = "bashless-closure-initrd";
preferLocalBuild = true;
nativeBuildInputs = [
zstd
cpio
];
buildCommand = ''
set +e
zstd -dfc ${machine.toplevel}/initrd | cpio --quiet -t | grep bash
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Error: initrd contains bash"
exit 1
fi
touch $out
'';
};
}
+109
View File
@@ -0,0 +1,109 @@
# Tests a bashless image. The forbiddenDependenciesRegexes from the bashless
# profile ensures that the closure doesn't contain any bash.
{ lib, ... }:
let
storePartitionLabel = "root";
in
{
name = "activation-bashless-image";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{
config,
pkgs,
modulesPath,
...
}:
{
imports = [
"${modulesPath}/image/repart.nix"
"${modulesPath}/profiles/bashless.nix"
];
# Backdoor uses bash
testing.backdoor = false;
virtualisation = {
directBoot.enable = false;
mountHostNixStore = false;
useEFIBoot = true;
};
boot.loader.grub.enable = false;
virtualisation.fileSystems = lib.mkForce {
"/" = {
fsType = "tmpfs";
options = [ "mode=0755" ];
};
"/nix/store" = {
device = "/dev/disk/by-partlabel/${storePartitionLabel}";
fsType = "erofs"; # Saves ~250MiB over ext4
};
};
image.repart = {
name = "bashless-image";
mkfsOptions = {
erofs = [ "-z lz4" ]; # Saves ~150MiB over no compression
};
partitions = {
"esp" = {
contents = {
"/EFI/BOOT/BOOT${lib.toUpper config.nixpkgs.hostPlatform.efiArch}.EFI".source =
"${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
};
repartConfig = {
Type = "esp";
Format = "vfat";
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
};
};
"root" = {
storePaths = [ config.system.build.toplevel ];
nixStorePrefix = "/";
repartConfig = {
Type = "linux-generic";
Format = config.fileSystems."/nix/store".fsType;
Label = storePartitionLabel;
Minimize = "best";
};
};
};
};
};
testScript =
{ nodes, ... }:
''
import os
import subprocess
import tempfile
tmp_disk_image = tempfile.NamedTemporaryFile()
subprocess.run([
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
"create",
"-f",
"qcow2",
"-b",
"${nodes.machine.system.build.image}/${nodes.machine.image.fileName}",
"-F",
"raw",
tmp_disk_image.name,
])
# Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
machine.start()
machine.wait_for_console_text("Startup finished.")
'';
}
+50
View File
@@ -0,0 +1,50 @@
# This is a smoke test that tests that basic functionality is still available
# with the bashless profile. For this, however, we have to re-enable bash.
{ lib, ... }:
{
name = "activation-bashless";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{ pkgs, modulesPath, ... }:
{
imports = [ "${modulesPath}/profiles/bashless.nix" ];
# Forcibly re-set options that would normally be set by the bashless
# profile but that we have to re-enable to make the test instrumentation
# work.
environment.binsh = lib.mkForce null;
boot.initrd.systemd.shell.enable = false;
# This ensures that we only have the store paths of our closure in the
# in the guest. This is necessary so we can grep in the store.
virtualisation.mountHostNixStore = false;
virtualisation.useNixStoreImage = true;
# Re-enable just enough of a normal NixOS system to be able to run tests
programs.bash.enable = true;
environment.shell.enable = true;
environment.systemPackages = [
pkgs.coreutils
pkgs.gnugrep
];
# Unset the regex because the tests instrumentation needs bash.
system.forbiddenDependenciesRegexes = lib.mkForce [ ];
};
testScript = ''
machine.wait_for_unit("multi-user.target")
with subtest("/bin/sh doesn't exist"):
machine.fail("stat /bin/sh")
bash_store_paths = machine.succeed("ls /nix/store | grep bash || true")
print(bash_store_paths)
'';
}
+3
View File
@@ -186,6 +186,9 @@ in
acme = import ./acme/default.nix { inherit runTest; };
acme-dns = runTest ./acme-dns.nix;
activation = pkgs.callPackage ../modules/system/activation/test.nix { };
activation-bashless = runTest ./activation/bashless.nix;
activation-bashless-closure = pkgs.callPackage ./activation/bashless-closure.nix { };
activation-bashless-image = runTest ./activation/bashless-image.nix;
activation-etc-overlay-immutable = runTest ./activation/etc-overlay-immutable.nix;
activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix;
activation-lib = pkgs.callPackage ../modules/system/activation/lib/test.nix { };
@@ -1,6 +1,5 @@
import argparse
import logging
import os
import sys
from subprocess import CalledProcessError, run
from typing import Final, assert_never
@@ -290,12 +289,7 @@ def execute(argv: list[str]) -> None:
# Re-exec to a newer version of the script before building to ensure we get
# the latest fixes
if (
WITH_REEXEC
and can_run
and not args.no_reexec
and not os.environ.get("_NIXOS_REBUILD_REEXEC")
):
if WITH_REEXEC and can_run and not args.no_reexec:
services.reexec(argv, args, build_flags, flake_build_flags)
profile = Profile.from_arg(args.profile_name)
@@ -4,7 +4,6 @@ import logging
import os
import sys
from pathlib import Path
from subprocess import CalledProcessError
from typing import Final
from . import nix, tmpdir
@@ -14,6 +13,7 @@ from .process import Remote, cleanup_ssh
from .utils import Args, tabulate
NIXOS_REBUILD_ATTR: Final = "config.system.build.nixos-rebuild"
NIXOS_REBUILD_REEXEC_ENV: Final = "_NIXOS_REBUILD_REEXEC"
logger: Final = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
@@ -25,27 +25,26 @@ def reexec(
build_flags: Args,
flake_build_flags: Args,
) -> None:
if os.environ.get(NIXOS_REBUILD_REEXEC_ENV):
return
drv = None
try:
# Parsing the args here but ignore ask_sudo_password since it is not
# needed and we would end up asking sudo password twice
if flake := Flake.from_arg(args.flake, Remote.from_arg(args.target_host, None)):
drv = nix.build_flake(
NIXOS_REBUILD_ATTR,
flake,
flake_build_flags | {"no_link": True},
)
else:
build_attr = BuildAttr.from_arg(args.attr, args.file)
drv = nix.build(
NIXOS_REBUILD_ATTR,
build_attr,
build_flags | {"no_out_link": True},
)
except CalledProcessError:
logger.warning(
"could not build a newer version of nixos-rebuild, using current version",
exc_info=logger.isEnabledFor(logging.DEBUG),
# Parsing the args here but ignore ask_sudo_password since it is not
# needed and we would end up asking sudo password twice
if flake := Flake.from_arg(
args.flake, Remote.from_arg(args.target_host, ask_sudo_password=None)
):
drv = nix.build_flake(
NIXOS_REBUILD_ATTR,
flake,
flake_build_flags | {"no_link": True},
)
else:
build_attr = BuildAttr.from_arg(args.attr, args.file)
drv = nix.build(
NIXOS_REBUILD_ATTR,
build_attr,
build_flags | {"no_out_link": True},
)
if drv:
@@ -62,7 +61,7 @@ def reexec(
cleanup_ssh()
tmpdir.TMPDIR.cleanup()
try:
os.execve(new, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
os.execve(new, argv, os.environ | {NIXOS_REBUILD_REEXEC_ENV: "1"})
except Exception:
# Possible errors that we can have here:
# - Missing the binary
@@ -74,7 +73,7 @@ def reexec(
)
# We already run clean-up, let's re-exec in the current version
# to avoid issues
os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
os.execve(current, argv, os.environ | {NIXOS_REBUILD_REEXEC_ENV: "1"})
def _validate_image_variant(image_variant: str, variants: ImageVariants) -> None:
@@ -39,7 +39,7 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) -
mock_execve.assert_called_once_with(
Path("/path/new/bin/nixos-rebuild-ng"),
["/path/bin/nixos-rebuild-ng", "switch", "--no-flake"],
{"_NIXOS_REBUILD_REEXEC": "1"},
{s.NIXOS_REBUILD_REEXEC_ENV: "1"},
)
mock_execve.reset_mock()
@@ -50,7 +50,7 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) -
mock_execve.assert_any_call(
Path("/path/bin/nixos-rebuild-ng"),
["/path/bin/nixos-rebuild-ng", "switch", "--no-flake"],
{"_NIXOS_REBUILD_REEXEC": "1"},
{s.NIXOS_REBUILD_REEXEC_ENV: "1"},
)
@@ -81,7 +81,7 @@ def test_reexec_flake(
mock_execve.assert_called_once_with(
Path("/path/new/bin/nixos-rebuild-ng"),
["/path/bin/nixos-rebuild-ng", "switch", "--flake"],
{"_NIXOS_REBUILD_REEXEC": "1"},
{s.NIXOS_REBUILD_REEXEC_ENV: "1"},
)
mock_execve.reset_mock()
@@ -92,5 +92,18 @@ def test_reexec_flake(
mock_execve.assert_any_call(
Path("/path/bin/nixos-rebuild-ng"),
["/path/bin/nixos-rebuild-ng", "switch", "--flake"],
{"_NIXOS_REBUILD_REEXEC": "1"},
{s.NIXOS_REBUILD_REEXEC_ENV: "1"},
)
@patch.dict(os.environ, {s.NIXOS_REBUILD_REEXEC_ENV: "1"}, clear=True)
@patch("os.execve", autospec=True)
@patch(get_qualified_name(s.nix.build_flake), autospec=True)
def test_reexec_skip_if_already_reexec(mock_build: Mock, mock_execve: Mock) -> None:
argv = ["/path/bin/nixos-rebuild-ng", "switch", "--flake"]
args, _ = n.parse_args(argv)
mock_build.return_value = Path("/path")
s.reexec(argv, args, {"build": True}, {"flake": True})
mock_build.assert_not_called()
mock_execve.assert_not_called()
+3 -3
View File
@@ -16,18 +16,18 @@
rustPlatform.buildRustPackage (finalAttrs: {
pname = "ruff";
version = "0.13.1";
version = "0.14.0";
src = fetchFromGitHub {
owner = "astral-sh";
repo = "ruff";
tag = finalAttrs.version;
hash = "sha256-dCxCpJLG2qjfrMxDJOL4rCwdVYfrz3P+4kDQ9d9Mbus=";
hash = "sha256-Vueccz5lkUTdgqqZl/+PB0kVYezvPKIVxee8EJYcz4g=";
};
cargoBuildFlags = [ "--package=ruff" ];
cargoHash = "sha256-WtRryq8bmKfL3EL2kRFFokmG2f0lnS6zRMbUzGeYLDM=";
cargoHash = "sha256-bnSTiQzlZrS2tqQiRHr6gNDq40Naqxrxcyc8zcVlt7A=";
nativeBuildInputs = [ installShellFiles ];
@@ -25,23 +25,23 @@
"lts": true
},
"6.6": {
"version": "6.6.110",
"hash": "sha256:07gv37ralrhf709plqj1hzk1adwilh6znmay6agpbf23anphvwhv",
"version": "6.6.111",
"hash": "sha256:1is6nrm5x54bw8zn8l5akp8ign185i19biks442yynfn63hp1i04",
"lts": true
},
"6.12": {
"version": "6.12.51",
"hash": "sha256:08bj3b6a6jwvrpjl5sxvmzwwnc00clq98rjwb61fznd7khaasm9d",
"version": "6.12.52",
"hash": "sha256:1ccyd9h9i3xia1gqq0mggis5yv04c9ys44xp707wfcm0f3v0r1dl",
"lts": true
},
"6.16": {
"version": "6.16.11",
"hash": "sha256:0yxsinhly689327jbvwm2nfr6cx7ynj9sd87a9var1rx8l64yc2z",
"version": "6.16.12",
"hash": "sha256:0vm257d76hmimnac8hzg66gd1mdg330sai39lywfn4m9bjydx93w",
"lts": false
},
"6.17": {
"version": "6.17.1",
"hash": "sha256:182d9xf7j0n4cn61rsqs87al9xc83rawri8p3yk246a984zvwgd5",
"version": "6.17.2",
"hash": "sha256:0zzmcjkmxmcn45y20g8hz7dxs355rmczp9k8vjwc3xb5a03cpszx",
"lts": false
}
}