Skip to content

Documentation

Silent-failure candidates in the October 2026 nixpkgs pin

Research date: 2026-10-04. This report contains 23 release-documented candidates, one additional established Python-builder trap, and five multi-host silent-regression traps. All admitted old/new forms evaluated with exit 0 and empty stderr. All NixOS forms also produced `warnings=[]` while forcing the full system `drvPath`.

Maintained in docs/research/silent-failure-changes-2026-10.md

Documentation

Research date: 2026-10-04. This report contains 23 release-documented candidates, one additional established Python-builder trap, and five multi-host silent-regression traps. All admitted old/new forms evaluated with exit 0 and empty stderr. All NixOS forms also produced warnings=[] while forcing the full system drvPath.

The 3.1 calibration found that all fifteen attempts missed the runtime-only LUKS root requirement, while most diagnostic-driven tasks saturated for frontier models. The predecessor report, including its candidate tables, too-trivial section, and probe ledger, supplied the starting inventory. This report applies a stricter admission rule: an old form that errors, asserts, warns, or prints a rename message is not a silent candidate.

The first 23 are documented in the pinned 26.05 NixOS or Nixpkgs release notes. That is evidence of a release change, not proof of a post-June-2026 introduction date. The supplied revision is 774debe7a0d1b496e35677ad955a1011c6ff74f3, supplied date 2026-10-02; the unpacked tree identifies itself as 26.05 and contains no Git history for authenticating individual introduction dates. Do not label these a verified post-training holdout without dating their commits.

What was verified

The read-only tree was /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source. Probes used x86_64-linux, explicit empty nixpkgs config/overlays, offline evaluation, and disabled import-from-derivation. The final evidence comprises 40 full NixOS evaluations for S01–S20, eight package evaluations for S21–S23 and B01, and 30 full host evaluations for the five three-state/two-host traps. Each package result forces its derivation path. Scratch probes were created under /tmp; the sole repository output is this report.

No packages were built, no VMs were run, and no live services or persistent application data were changed. Evaluation proves quiet acceptance and the recorded resolved-config/generated-input differences. The runtime consequences below are supported by pinned source or release notes, with explicit fixture conditions. VM scripts are test sketches, not completed runtime validation. No runtime closure was measured: where package paths appear, they are selected outputs, generated wrapper references, or derivation inputs.

The static probes intentionally use unbuilt service packages and synthetic disks/secrets. A runnable VM fixture must provision the named paths, credentials, disks and data; a successful system derivation is not proof those resources exist.

Ranked candidates

Scores are (old-form probability / consequence / evaluator feasibility), each 1–5. These are task-authoring judgments, not measured model results. Rank also penalizes closely related tasks and requirements that only matter under a narrow policy. The user prompt must state the desired behavior, existing data/layout, and host invariants; the evaluator must not invent those requirements afterward.

RankIDAreaScoresQuiet failure / hidden observable
1S02Tor onion socket is outside the chroot5/5/5Tor has no bind mount for its configured Unix socket.
2S01Hardened-profile import becomes a no-op5/5/5Profile imports but resolved hardening is absent.
3S04Wireless TLS key becomes unreadable5/5/5Generated EAP config points at an inaccessible root-home key.
4S12vsftpd local-user login loses PAM policy5/5/5Local FTP authentication has no generated PAM policy.
5S17ESPHome persistence follows the obsolete private directory5/5/5Persistence bind covers the old DynamicUser private path.
6S10Post-resume hook is attached to a removed target5/4/5Resume service is enabled under a target no longer supplied.
7S11A service requires removed network-setup.service5/4/5Consumer Requires points at removed network-setup.service.
8S15OpenSnitch rule files remain in the old directory5/5/5Existing rule file is outside the daemon's new rules directory.
9S19InvoicePlane Caddy customization applies to a stale site key5/4/5Policy lands on a second HTTP vhost, not the generated app site.
10S06Jenkins loses Git and SSH from its service PATH5/4/5Jenkins service PATH lacks Git and SSH.
11S07The root filesystem still references /dev/root5/5/5Root device references a symlink systemd initrd does not create.
12S21Neovim silently disables Python and Ruby providers5/4/5Wrapper explicitly disables required remote-plugin providers.
13S14Vikunja resolves SQLite relative to service.rootpath4/5/4Relative SQLite location moves under the configured rootpath.
14S13Calibre-Web library is hidden by ProtectHome4/4/5Library lies under ProtectHome's inaccessible hierarchy.
15S16Explicit Tandoor media root preserves database exposure4/5/5Explicit legacy MEDIA_ROOT serves the database directory.
16S22Corepack moves out of the nodejs-slim default output5/4/5Selected environment omits the new Corepack output.
17S05Legacy wireless control-socket directory is unwritable4/4/5Later ctrl_interface assignment targets an unwritable path.
18S03Forced wireless disable removes NetworkManager's supplicant4/4/5Forced wireless disable leaves NetworkManager without a supplicant unit.
19S09LVM-on-LUKS root retains a finite device timeout4/5/4Root LV mount retains finite device waiting during delayed unlock.
20S23Pikepdf notebook rendering loses its bundled mutool3/4/5Rendering helper references an absent bare mutool executable.
21S08Scripted-initrd debug parameter becomes inert5/3/5Old debug kernel argument no longer stops stage 1.
22S18Firewall audit logging defaults off4/3/5Generated nftables input chain omits refused-connection logs.
23S20D-Bus default changes the live-switch compatibility contract4/3/5Default daemon changes the live-switch inhibitor value.

S01, S02, S04, S12 and S17 have the strongest combination of familiar old assumptions and security, availability or persistence consequences. S09 is deliberately below the top twelve because it is related to the already-used encrypted-root task. S08 and S18 are better as secondary constraints in a larger task than as isolated edits. S20 is a live-switch requirement and only belongs in a task that explicitly needs continuity with an existing classic-daemon generation.

Reproducing the NixOS probes

For any S01–S20 entry, paste its common body plus either old or new body into BODY and its projection into VALUE. Empty bodies mean "make no additional definition." The baseline is a separate module so root-device overrides and imports do not duplicate Nix attribute definitions.

let
  s = import /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source/nixos {
    system = "x86_64-linux";
    configuration = { imports = [
      ({ lib, ... }: {
        nixpkgs.config = {};
        nixpkgs.overlays = [];
        boot.loader.grub.enable = false;
        fileSystems."/" = { device = lib.mkDefault "/dev/sda1"; fsType = "ext4"; };
        system.stateVersion = lib.mkDefault "26.05";
      })
      ({ lib, pkgs, modulesPath, ... }: {
        # BODY
      })
    ]; };
  };
  c = s.config;
  lib = s.pkgs.lib;
  pkgs = s.pkgs;
in {
  drv = c.system.build.toplevel.drvPath;
  warnings = c.warnings;
  value = VALUE;
}
nix eval --offline --impure --option allow-import-from-derivation false --json --file probe.nix

Actual JSON is reproduced below, pretty-printed without shortening store paths or diagnostic lists. Every admitted NixOS result had exit 0 and zero bytes of stderr. Full system derivation evaluation makes the warning/assertion check stronger than reading a service subattribute alone. It still does not build generated files or validate them with the upstream daemon.

<a id="s01"></a>

S01: Hardened-profile import becomes a no-op

Old form: Import profiles/hardened.nix and rely on it for the host hardening policy.

New form: Declare the required policy explicitly; this probe requires kernel-image protection and disabling unprivileged BPF.

The compatibility file only declares a removed option. Merely importing it does not define that option, so the import silently supplies no hardening. The probe resolves protectKernelImage=false and no BPF sysctl. These are a concrete task policy, not a claim to reconstruct every setting in the deleted profile.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:203; nixos/modules/profiles/hardened.nix:1; nixos/modules/security/misc.nix:44.

Hidden evaluator: Require security.protectKernelImage=true and the declared BPF sysctl. For a stronger task add independently justified policy requirements; do not accept merely deleting the import.

VM grading: Yes. Boot the candidate, then machine.succeed("test $(sysctl -n kernel.kexec_load_disabled) = 1; test $(sysctl -n kernel.unprivileged_bpf_disabled) = 1").

Common body:

# No additional common definitions.

Old body:

imports = [ (modulesPath + "/profiles/hardened.nix") ];

New body:

security.protectKernelImage = true; boot.kernel.sysctl."kernel.unprivileged_bpf_disabled" = 1;

Projection:

{ inherit (c.security) protectKernelImage; bpf = c.boot.kernel.sysctl."kernel.unprivileged_bpf_disabled" or null; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/m4365isvd3ip1hvm738zin1l7gfbdfcy-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "bpf": null,
    "protectKernelImage": false
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/jjxqlb5j49x8zspmzw0aqw60plylqcwa-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "bpf": 1,
    "protectKernelImage": true
  },
  "warnings": []
}

<a id="s02"></a>

S02: Tor onion socket is outside the chroot

Old form: Declare an onion-service target at /run/onion-web/http.sock and assume Tor exposes that socket inside its chroot.

New form: Bind the stable parent directory into Tor and create it before Tor starts.

The configured onion service still exists, but Tor runs with RootDirectory=/run/tor/root and an empty BindPaths. Connections cannot reach the host socket. The release notes explicitly removed automatic socket bind mounts.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:337; nixos/modules/services/security/tor.nix:641.

Hidden evaluator: Assert the socket endpoint is retained, its parent has a tmpfiles rule or equivalent provisioning, and Tor binds that directory. In a full fixture order Tor after directory creation and the backend; bind the parent so socket recreation remains visible.

VM grading: Yes. Start a Unix-socket HTTP backend and Tor; enter the Tor process mount namespace/root and curl the socket. For end-to-end grading use a local test Tor network, not the public network. Sketch: machine.wait_for_unit("tor.service"); machine.succeed("curl --fail --unix-socket /proc/$(systemctl show tor -p MainPID --value)/root/run/onion-web/http.sock http://localhost").

Common body:

services.tor = { enable = true; relay.onionServices.web.map = [{ port = 80; target.unix = "/run/onion-web/http.sock"; }]; }; systemd.tmpfiles.rules = [ "d /run/onion-web 0755 root root - -" ];

Old body:

# No additional definitions.

New body:

systemd.services.tor.serviceConfig.BindPaths = [ "/run/onion-web" ];

Projection:

{ root = c.systemd.services.tor.serviceConfig.RootDirectory; bind = c.systemd.services.tor.serviceConfig.BindPaths or []; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/iafa1c7sd7x0rysiaxsc37k9xniqsayr-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "bind": [],
    "root": "/run/tor/root"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/50lrh8fx50kiriaj44jcz5i7iffy5l41-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "bind": [
      "/run/onion-web"
    ],
    "root": "/run/tor/root"
  },
  "warnings": []
}

<a id="s03"></a>

S03: Forced wireless disable removes NetworkManager's supplicant

Old form: networking.networkmanager.enable=true together with a legacy networking.wireless.enable=lib.mkForce false.

New form: Remove the forced disable and let NetworkManager enable the independently managed supplicant.

The NetworkManager module now relies on the NixOS wpa_supplicant service. The bad configuration retains DBus control settings but creates no supplicant unit, breaking Wi-Fi with that backend. The force is material: an ordinary same-priority false can cause a conflict, which would disqualify it from this silent pool.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:366; nixos/modules/services/networking/networkmanager.nix:675.

Hidden evaluator: Require wireless enablement, the supplicant unit, and the intended NetworkManager backend. Do not silently switch to iwd as an unrelated workaround.

VM grading: Yes, with mac80211_hwsim and a test AP. Wait for NetworkManager, connect to the AP, then machine.succeed("nmcli -g GENERAL.STATE device show wlan0 | grep connected").

Common body:

networking.networkmanager.enable = true;

Old body:

networking.wireless.enable = lib.mkForce false;

New body:

# No additional definitions.

Projection:

{ wireless = c.networking.wireless.enable; unit = c.systemd.services ? wpa_supplicant; dbus = c.networking.wireless.dbusControlled; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/8ivlw4wh6mypbw4420g7g22p92y3cvlw-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "dbus": true,
    "unit": false,
    "wireless": false
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/jlhwxws3lqa24nvzhziknl07hxjanl7g-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "dbus": true,
    "unit": true,
    "wireless": true
  },
  "warnings": []
}

<a id="s04"></a>

S04: Wireless TLS key becomes unreadable

Old form: An EAP-TLS network references /root/client.key, relying on the old root daemon.

New form: Provision the key under /etc/wpa_supplicant, owned by wpa_supplicant, and update the network configuration.

The hardened service runs as wpa_supplicant, sets ProtectHome=true, and uses a private root. A root-home key is inaccessible even though the generated EAP configuration is accepted by Nix. The probe includes unchanged CA/client certificate references and generates key_mgmt=WPA-EAP in both forms.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:353; nixos/modules/services/networking/wpa_supplicant.nix:141; nixos/modules/services/networking/wpa_supplicant.nix:467.

Hidden evaluator: Check generated wpa_supplicant/nixos.conf, service identity, and key provisioning ownership together. The tmpfiles copy in the probe assumes /run/secrets/client.key already exists at tmpfiles time; production fixtures should use the stated secret manager and its ordering/rotation contract.

VM grading: Yes. Provision fixture certificates/key before startup; run a read test in the supplicant sandbox with its user. A full EAP-TLS test also needs a test AP and RADIUS server. Sketch: machine.succeed("runuser -u wpa_supplicant -- test -r /etc/wpa_supplicant/client.key"); machine.wait_for_unit("wpa_supplicant.service").

Common body:

networking.wireless.enable = true; networking.wireless.networks.corp.authProtocols = [ "WPA-EAP" ];

Old body:

networking.wireless.networks.corp.auth = "eap=TLS\nidentity=\"probe\"\nclient_cert=\"/etc/wpa_supplicant/client.crt\"\nca_cert=\"/etc/wpa_supplicant/ca.crt\"\nprivate_key=\"/root/client.key\"";

New body:

networking.wireless.networks.corp.auth = "eap=TLS\nidentity=\"probe\"\nclient_cert=\"/etc/wpa_supplicant/client.crt\"\nca_cert=\"/etc/wpa_supplicant/ca.crt\"\nprivate_key=\"/etc/wpa_supplicant/client.key\""; systemd.tmpfiles.rules = [ "C /etc/wpa_supplicant/client.key 0600 wpa_supplicant wpa_supplicant - /run/secrets/client.key" ];

Projection:

{ user = c.systemd.services.wpa_supplicant.serviceConfig.User; conf = c.environment.etc."wpa_supplicant/nixos.conf".text; rules = lib.filter (x: lib.hasInfix "client.key" x) c.systemd.tmpfiles.rules; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/vrgwmgnxmhw4s148qg45jfywll5c35hy-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "conf": "network={\n  ssid=\"corp\"\n  key_mgmt=WPA-EAP\n  eap=TLS\n  identity=\"probe\"\n  client_cert=\"/etc/wpa_supplicant/client.crt\"\n  ca_cert=\"/etc/wpa_supplicant/ca.crt\"\n  private_key=\"/root/client.key\"\n}\n\npmf=1\nbgscan=\"simple:30:-70:3600\"",
    "rules": [],
    "user": "wpa_supplicant"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/gj0lvs5nd3xfrrk5p7nwrnlvsklnm3qr-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "conf": "network={\n  ssid=\"corp\"\n  key_mgmt=WPA-EAP\n  eap=TLS\n  identity=\"probe\"\n  client_cert=\"/etc/wpa_supplicant/client.crt\"\n  ca_cert=\"/etc/wpa_supplicant/ca.crt\"\n  private_key=\"/etc/wpa_supplicant/client.key\"\n}\n\npmf=1\nbgscan=\"simple:30:-70:3600\"",
    "rules": [
      "C /etc/wpa_supplicant/client.key 0600 wpa_supplicant wpa_supplicant - /run/secrets/client.key"
    ],
    "user": "wpa_supplicant"
  },
  "warnings": []
}

<a id="s05"></a>

S05: Legacy wireless control-socket directory is unwritable

Old form: networking.wireless.extraConfig="ctrl_interface=/run/old-wpa-control".

New form: Remove the override; retain userControlled=true so the module creates its supported control directory.

The custom path overrides the generated ctrl_interface but is absent from the service's writable bind mounts. The unprivileged daemon cannot create that socket and can fail to start. Both generated files include the same network and user-control policy.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:359; nixos/modules/services/networking/wpa_supplicant.nix:157; nixos/modules/services/networking/wpa_supplicant.nix:720.

Hidden evaluator: Require the generated control path /run/wpa_supplicant/control, the dedicated group, and no later conflicting control-interface assignment. Do not grade only the first matching line.

VM grading: Yes, with a virtual Wi-Fi interface. machine.wait_for_unit("wpa_supplicant.service"); machine.succeed("wpa_cli -p /run/wpa_supplicant/control ping | grep PONG").

Common body:

networking.wireless.enable = true; networking.wireless.networks.lab.psk = "test-password"; networking.wireless.userControlled = true;

Old body:

networking.wireless.extraConfig = "ctrl_interface=/run/old-wpa-control";

New body:

# No additional definitions.

Projection:

{ user = c.systemd.services.wpa_supplicant.serviceConfig.User; root = c.systemd.services.wpa_supplicant.serviceConfig.RootDirectory; conf = c.environment.etc."wpa_supplicant/nixos.conf".text; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/v2d1gd8v2nvlsjrl0ssnx7mhfpyqmgg4-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "conf": "network={\n  ssid=\"lab\"\n  key_mgmt=WPA-PSK WPA-EAP SAE FT-PSK FT-EAP FT-SAE\n  psk=\"test-password\"\n  priority=1\n}\n\nnetwork={\n  ssid=\"lab\"\n  key_mgmt=WPA-PSK WPA-EAP FT-PSK FT-EAP\n  psk=\"test-password\"\n}\n\nctrl_interface=/run/wpa_supplicant/control\nctrl_interface_group=wpa_supplicant\nupdate_config=1\npmf=1\nbgscan=\"simple:30:-70:3600\"\nctrl_interface=/run/old-wpa-control",
    "root": "/run/wpa_supplicant",
    "user": "wpa_supplicant"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/vngpibcjv4kjlpla7hz13vgbb998b1z9-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "conf": "network={\n  ssid=\"lab\"\n  key_mgmt=WPA-PSK WPA-EAP SAE FT-PSK FT-EAP FT-SAE\n  psk=\"test-password\"\n  priority=1\n}\n\nnetwork={\n  ssid=\"lab\"\n  key_mgmt=WPA-PSK WPA-EAP FT-PSK FT-EAP\n  psk=\"test-password\"\n}\n\nctrl_interface=/run/wpa_supplicant/control\nctrl_interface_group=wpa_supplicant\nupdate_config=1\npmf=1\nbgscan=\"simple:30:-70:3600\"",
    "root": "/run/wpa_supplicant",
    "user": "wpa_supplicant"
  },
  "warnings": []
}

<a id="s06"></a>

S06: Jenkins loses Git and SSH from its service PATH

Old form: Enable Jenkins and rely on its former default tool list.

New form: Set services.jenkins.packages=[pkgs.git pkgs.openssh] for jobs requiring Git-over-SSH.

The module's packages default is now empty. Jenkins starts, but a job invoking git or ssh cannot find it in the controlled service PATH. This is a per-service environment failure even if an administrator's shell has those tools.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:179; nixos/modules/services/continuous-integration/jenkins/default.nix:88; nixos/modules/services/continuous-integration/jenkins/default.nix:217.

Hidden evaluator: Inspect the resolved Jenkins PATH and declared tools, preserving other job tools. A hidden test should verify executable availability under this PATH, not merely membership in environment.systemPackages.

VM grading: Yes. Launch a fixture Jenkins job that runs git --version and ssh -V; alternatively run both commands as the service user under the unit's resolved PATH. Sketch: machine.succeed("runuser -u jenkins -- env -i PATH=" + service_path + " git --version"); machine.succeed("runuser -u jenkins -- env -i PATH=" + service_path + " ssh -V"). Bind service_path from the evaluated Jenkins unit, not the test shell PATH.

Common body:

services.jenkins.enable = true;

Old body:

# No additional definitions.

New body:

services.jenkins.packages = [ pkgs.git pkgs.openssh ];

Projection:

{ tools = map lib.getName c.services.jenkins.packages; path = c.systemd.services.jenkins.environment.PATH; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/wf3zzczq6g0y65bpj914nwlmx78asllm-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "path": "/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/bin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/bin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/bin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/bin:/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/sbin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/sbin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/sbin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/sbin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/sbin",
    "tools": []
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/z4443c446229n9xvxr1wvq9n99gzb794-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "path": "/nix/store/q9wb526c4vn8mz2sh24hpfxha83vqbsf-git-2.54.0/bin:/nix/store/5lxyvxz845rzklwn556f6cizv2sg0rrg-openssh-10.5p1/bin:/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/bin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/bin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/bin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/bin:/nix/store/q9wb526c4vn8mz2sh24hpfxha83vqbsf-git-2.54.0/sbin:/nix/store/5lxyvxz845rzklwn556f6cizv2sg0rrg-openssh-10.5p1/sbin:/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/sbin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/sbin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/sbin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/sbin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/sbin",
    "tools": [
      "git",
      "openssh"
    ]
  },
  "warnings": []
}

<a id="s07"></a>

S07: The root filesystem still references /dev/root

Old form: fileSystems."/".device="/dev/root" with the now-default systemd initrd.

New form: Use the fixture disk's stable label, UUID, or correct mapper path.

Systemd stage 1 does not create the scripted initrd's /dev/root symlink. The system evaluates but cannot find that root device at boot. This is distinct from the previously used LUKS-label timing task and does not require encryption.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:16; nixos/modules/tasks/filesystems.nix:1.

Hidden evaluator: Require systemd initrd and a root path that matches the fixture disk. Reject disabling systemd or inventing an unrelated label. A projection of the resolved root path is sufficient for static grading.

VM grading: Yes, custom disk image. Label its root filesystem nixos, boot with the candidate, then machine.wait_for_unit("multi-user.target"); the old form should fail to reach stage 2.

Common body:

# No additional common definitions.

Old body:

fileSystems."/".device = "/dev/root";

New body:

fileSystems."/".device = "/dev/disk/by-label/nixos";

Projection:

{ systemd = c.boot.initrd.systemd.enable; root = c.fileSystems."/".device; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/x2m1in1h1rbjw0nshcygjhh98yg4mgcf-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "root": "/dev/root",
    "systemd": true
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/59d5hrfi7kpb91rsqi8fhr5yq6rr79m9-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "root": "/dev/disk/by-label/nixos",
    "systemd": true
  },
  "warnings": []
}

<a id="s08"></a>

S08: Scripted-initrd debug parameter becomes inert

Old form: boot.kernelParams=["boot.debug1devices"] when an operator expects a stop before root mounting.

New form: Use rd.systemd.break=pre-mount for the systemd initrd.

The manual explicitly says the scripted-stage-1 parameters have no effect with systemd stage 1. The old form boots past the requested diagnostic stop and leaves a recovery procedure unusable.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:15; nixos/doc/manual/administration/boot-problems.section.md:18; nixos/doc/manual/administration/boot-problems.section.md:37.

Hidden evaluator: Require the native parameter and the intended initrd implementation; reject retaining contradictory legacy flags. This is a good secondary operational constraint, but an easy single-token task on its own.

VM grading: Yes, specialized serial-console test. Boot and wait for the pre-mount breakpoint shell, assert stage 2 has not started, then continue boot through the console. Sketch: machine.wait_for_console_text("pre-mount"); machine.send_chars("exit\n"). Check the actual console prompt for the pinned systemd version; do not use the stage-2 test agent until boot continues.

Common body:

# No additional common definitions.

Old body:

boot.kernelParams = [ "boot.debug1devices" ];

New body:

boot.kernelParams = [ "rd.systemd.break=pre-mount" ];

Projection:

{ systemd = c.boot.initrd.systemd.enable; params = c.boot.kernelParams; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/129mhcx8wfcg3rsibvs8y8skp1pl1xnb-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "params": [
      "boot.debug1devices",
      "root=fstab",
      "loglevel=4",
      "lsm=landlock,yama,bpf"
    ],
    "systemd": true
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/mijhbyrkag4zyf0qbv8kbf4wpd0157kh-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "params": [
      "rd.systemd.break=pre-mount",
      "root=fstab",
      "loglevel=4",
      "lsm=landlock,yama,bpf"
    ],
    "systemd": true
  },
  "warnings": []
}

<a id="s09"></a>

S09: LVM-on-LUKS root retains a finite device timeout

Old form: An encrypted PV backs /dev/mapper/vg-root, but the root filesystem omits a device-timeout override.

New form: Keep the LV root path and add x-systemd.device-timeout=infinity to root mount options.

A sufficiently slow passphrase response can outlast the root LV's device timeout before the encrypted PV is opened. The release notes explicitly describe LVM-on-LUKS as a case needing this option. This is related to the 3.1 task and should not be counted as independent evidence of difficulty.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:13; nixos/modules/tasks/filesystems.nix:1.

Hidden evaluator: Assert LV and encrypted-PV identities are unchanged and the root options include the timeout. A realistic fixture must create the VG/LV and LUKS hierarchy; the evaluation probe only represents that declared topology.

VM grading: Yes, custom encrypted image and console interaction. Delay unlocking past the normal device timeout, provide the password, then wait for stage 2; keep this longer test outside the fast evaluator. Sketch: time.sleep(100); machine.send_chars("fixture-passphrase\n"); machine.wait_for_unit("multi-user.target").

Common body:

boot.initrd.luks.devices.cryptpv.device = "/dev/disk/by-label/cryptpv"; fileSystems."/".device = "/dev/mapper/vg-root";

Old body:

# No additional definitions.

New body:

fileSystems."/".options = [ "x-systemd.device-timeout=infinity" ];

Projection:

{ systemd = c.boot.initrd.systemd.enable; root = c.fileSystems."/".device; options = c.fileSystems."/".options; luks = builtins.attrNames c.boot.initrd.luks.devices; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/f62y9a8q6nwpnh7kkk7fkqlil9f881db-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "luks": [
      "cryptpv"
    ],
    "options": [
      "x-initrd.mount"
    ],
    "root": "/dev/mapper/vg-root",
    "systemd": true
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/sqzbszyq4q7djmq34b0s46j6dpswdva2-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "luks": [
      "cryptpv"
    ],
    "options": [
      "x-initrd.mount",
      "x-systemd.device-timeout=infinity"
    ],
    "root": "/dev/mapper/vg-root",
    "systemd": true
  },
  "warnings": []
}

<a id="s10"></a>

S10: Post-resume hook is attached to a removed target

Old form: A service is wanted by and ordered after post-resume.target.

New form: Activate before sleep.target, stay active across sleep, and perform the post-resume action in ExecStop with StopWhenUnneeded=true.

The removed target is not supplied. An enablement link to its name does not cause the hook to run during a real suspend/resume cycle. The old unit text is perfectly valid Nix.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:260; nixos/modules/system/boot/systemd.nix:121.

Hidden evaluator: Check the sleep target dependency, before ordering, oneshot lifetime, stop behavior, and marker command. Also check there is no supplied post-resume.target; a unit-name string alone is not proof of a live target.

VM grading: Yes, on a suspend-capable VM. Suspend/resume it and machine.succeed("test -f /run/resumed"); use a synthetic sleep-target start/stop test only as a narrower unit-lifecycle test. Sketch: machine.succeed("rtcwake -m mem -s 5"); machine.succeed("test -f /run/resumed").

Common body:

# No additional common definitions.

Old body:

systemd.services.resume-marker = { wantedBy = [ "post-resume.target" ]; after = [ "post-resume.target" ]; serviceConfig.Type = "oneshot"; script = "touch /run/resumed"; };

New body:

systemd.services.resume-marker = { wantedBy = [ "sleep.target" ]; before = [ "sleep.target" ]; unitConfig.StopWhenUnneeded = true; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${pkgs.coreutils}/bin/true"; ExecStop = "${pkgs.coreutils}/bin/touch /run/resumed"; }; };

Projection:

{ obsoleteTarget = c.systemd.units ? "post-resume.target"; unit = c.systemd.units."resume-marker.service".text; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/w9i16fpi3lygx4cm025xr6b32fcdpxwh-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "obsoleteTarget": false,
    "unit": "[Unit]\nAfter=post-resume.target\n\n[Service]\nEnvironment=\"LOCALE_ARCHIVE=/nix/store/qfxqcsrsl9mmgq319a218c0xijz3d0qx-glibc-locales-2.42-84/lib/locale/locale-archive\"\nEnvironment=\"PATH=/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/bin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/bin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/bin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/bin:/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/sbin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/sbin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/sbin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/sbin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/sbin\"\nEnvironment=\"TZDIR=/nix/store/xl5gj173j6x670xn9xhldj7giiygclrm-tzdata-2026c/share/zoneinfo\"\nExecStart=/nix/store/v3hbx2qry4bg75cwpzaqk7w8dsfyx0wc-unit-script-resume-marker-start/bin/resume-marker-start \nType=oneshot\n\n[Install]\nWantedBy=post-resume.target\n"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/z3nk3d38hkzd1ynqbsmdal51wcxnc7dd-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "obsoleteTarget": false,
    "unit": "[Unit]\nBefore=sleep.target\nStopWhenUnneeded=true\n\n[Service]\nEnvironment=\"LOCALE_ARCHIVE=/nix/store/qfxqcsrsl9mmgq319a218c0xijz3d0qx-glibc-locales-2.42-84/lib/locale/locale-archive\"\nEnvironment=\"PATH=/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/bin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/bin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/bin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/bin:/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/sbin:/nix/store/g6d53gb3cw6x1imfbw5v4088lkdkkc6i-findutils-4.10.0/sbin:/nix/store/2d2jxw071wai1hh6dj0shlgqmlz8zmy1-gnugrep-3.12/sbin:/nix/store/sazlbkll5x2dw1kkx0l2a8b74w6rgdxm-gnused-4.10/sbin:/nix/store/b4b1x5fhxpc78qlclj3kdbl9dd49g917-systemd-260.4/sbin\"\nEnvironment=\"TZDIR=/nix/store/xl5gj173j6x670xn9xhldj7giiygclrm-tzdata-2026c/share/zoneinfo\"\nExecStart=/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin/true\nExecStop=/nix/store/833invqvsk7anqnqrki5hqncrcllj825-coreutils-9.11/bin/touch /run/resumed\nRemainAfterExit=true\nType=oneshot\n\n[Install]\nWantedBy=sleep.target\n"
  },
  "warnings": []
}

<a id="s11"></a>

S11: A service requires removed network-setup.service

Old form: A consumer uses requires and after edges to network-setup.service.

New form: For this static-address fixture, depend on network-addresses-eth1.service.

The old aggregate service is gone. Requires points to a missing unit, so the consumer cannot start. Replacing it with network.target alone would not establish that the fixture's address has been assigned; address setup is asynchronous.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:368; nixos/modules/tasks/network-interfaces-scripted.nix:336; nixos/modules/tasks/network-interfaces-scripted.nix:877.

Hidden evaluator: Assert the intended address survives and both requirement/order edges reach the actual per-interface address job. Keep backend-specific solutions scoped; networkd has different readiness mechanisms.

VM grading: Yes. Provide eth1, make the consumer check ip -4 addr show eth1 for 192.0.2.2/24, then wait for the consumer unit to succeed. Sketch: machine.wait_for_unit("consumer.service"); machine.succeed("ip -4 addr show eth1 | grep 192.0.2.2/24").

Common body:

networking.useDHCP = false; networking.interfaces.eth1.ipv4.addresses = [{ address = "192.0.2.2"; prefixLength = 24; }];

Old body:

systemd.services.consumer = { wantedBy = [ "multi-user.target" ]; requires = [ "network-setup.service" ]; after = [ "network-setup.service" ]; script = "true"; };

New body:

systemd.services.consumer = { wantedBy = [ "multi-user.target" ]; requires = [ "network-addresses-eth1.service" ]; after = [ "network-addresses-eth1.service" ]; script = "true"; };

Projection:

{ obsoleteUnit = c.systemd.services ? network-setup; addressUnit = c.systemd.services ? network-addresses-eth1; requires = c.systemd.services.consumer.requires; after = c.systemd.services.consumer.after; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/h04gslia9crwdhsvjlgr96dalhicwkdw-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "addressUnit": true,
    "after": [
      "network-setup.service"
    ],
    "obsoleteUnit": false,
    "requires": [
      "network-setup.service"
    ]
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/vmkw0753rg84m9klg3vxxxl6wh3s38my-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "addressUnit": true,
    "after": [
      "network-addresses-eth1.service"
    ],
    "obsoleteUnit": false,
    "requires": [
      "network-addresses-eth1.service"
    ]
  },
  "warnings": []
}

<a id="s12"></a>

S12: vsftpd local-user login loses PAM policy

Old form: services.vsftpd={enable=true;localUsers=true;} with no explicit PAM policy.

New form: Add a deliberate PAM policy. The probe restores the standard Unix policy through security.pam.services.vsftpd={};.

The module no longer creates a PAM service for local users automatically. Local authentication fails despite successful Nix evaluation. The release note cautions that restoring the old default policy may not be secure for every deployment; a benchmark must state its allowed users and authentication policy.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:262; nixos/modules/services/networking/vsftpd.nix:337.

Hidden evaluator: Assert the generated vsftpd PAM service and relevant auth/account rules, keeping localUsers and the intended user restrictions. A task can instead require explicit virtual-user PAM, but that changes the authentication contract and should not be an unstated repair.

VM grading: Yes. Create a fixture local account and FTP login via Python ftplib; assert the correct password succeeds and an incorrect password fails. Sketch: machine.succeed("python3 /etc/fixture/ftp-login-check.py"). The fixture script checks both correct-password success and wrong-password rejection.

Common body:

services.vsftpd = { enable = true; localUsers = true; };

Old body:

# No additional definitions.

New body:

security.pam.services.vsftpd = {};

Projection:

{ pam = c.security.pam.services ? vsftpd; text = if c.security.pam.services ? vsftpd then c.security.pam.services.vsftpd.text else null; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/rjryrgs1zxr8h6h8cl8rcxn0izqk38fl-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "pam": false,
    "text": null
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/vh53a59ic12070rwcq3yjnjfzcv1jhn2-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "pam": true,
    "text": "# Account management.\naccount required /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_unix.so # unix (order 11000)\n\n# Authentication management.\nauth sufficient /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_unix.so likeauth try_first_pass # unix (order 11700)\nauth required /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_deny.so # deny (order 12500)\n\n# Password management.\npassword sufficient /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_unix.so nullok yescrypt # unix (order 10200)\n\n# Session management.\nsession required /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_env.so conffile=/etc/pam/environment readenv=0 # env (order 10100)\nsession required /nix/store/q4hdc3r589f4k450sr9fb8rc6rqyihh1-linux-pam-1.7.2/lib/security/pam_unix.so # unix (order 10200)\n"
  },
  "warnings": []
}

<a id="s13"></a>

S13: Calibre-Web library is hidden by ProtectHome

Old form: Use /home/books/library as the Calibre library.

New form: Move or bind the prepared library to /srv/calibre/library, retaining sandbox protections and matching permissions.

The hardened service has ProtectHome=true and ProtectSystem=strict. Its ReadWritePaths entry does not by itself make a library under the inaccessible home hierarchy usable. The module checks for metadata.db before startup, so this can prevent the service from starting.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:489; nixos/modules/services/web-apps/calibre-web.nix:180; nixos/modules/services/web-apps/calibre-web.nix:190.

Hidden evaluator: Inspect the library path, the generated writable-path exception, and retained hardening together. The task must supply the migrated/bound library or explicitly require data migration; a path edit alone does not move books.

VM grading: Yes. Seed a minimal Calibre library at the intended mount, wait for calibre-web, and request its HTTP endpoint. Assert existing fixture books remain visible. Sketch: machine.wait_for_unit("calibre-web.service"); machine.succeed("python3 /etc/fixture/check-calibre-book-via-http.py"). The fixture helper logs in over HTTP if needed and requires the seeded book.

Common body:

services.calibre-web.enable = true;

Old body:

services.calibre-web.options.calibreLibrary = "/home/books/library";

New body:

services.calibre-web.options.calibreLibrary = "/srv/calibre/library";

Projection:

{ inherit (c.systemd.services.calibre-web.serviceConfig) ProtectHome ProtectSystem ReadWritePaths; library = c.services.calibre-web.options.calibreLibrary; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/xkzfi4cdz460lsfwbgcakvc0ng1pm9zw-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "ProtectHome": true,
    "ProtectSystem": "strict",
    "ReadWritePaths": [
      "/home/books/library"
    ],
    "library": "/home/books/library"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/bxx3j9xs97a7gsgpf43licpw2d75xwy4-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "ProtectHome": true,
    "ProtectSystem": "strict",
    "ReadWritePaths": [
      "/srv/calibre/library"
    ],
    "library": "/srv/calibre/library"
  },
  "warnings": []
}

<a id="s14"></a>

S14: Vikunja resolves SQLite relative to service.rootpath

Old form: Use database.path="vikunja.db" while settings.service.rootpath="/var/lib/vikunja/app".

New form: Set the absolute existing database path /var/lib/vikunja/vikunja.db.

The notes state that relative SQLite paths are now relative to service.rootpath. This points the old form at a different database, causing missing tasks/new database creation or startup failure. The probe proves the relative/absolute resolved setting difference; it does not execute Vikunja's path resolver.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:391; nixos/modules/services/web-apps/vikunja.nix:106; nixos/modules/services/web-apps/vikunja.nix:114.

Hidden evaluator: Require the effective database location to remain the fixture's existing database. Accept an equivalent explicit root/path pairing; do not demand one spelling if both resolve identically.

VM grading: Yes. Seed a database with a known task, start Vikunja, and query that task through its API. A successful HTTP startup alone would miss creation of an empty replacement database. Sketch: machine.wait_for_unit("vikunja.service"); machine.succeed("/etc/fixture/check-seeded-task-via-api"). The fixture helper authenticates against Vikunja and requires the seeded task title from its API.

Common body:

services.vikunja = { enable = true; frontendScheme = "https"; frontendHostname = "tasks.example.test"; settings.service.rootpath = "/var/lib/vikunja/app"; };

Old body:

services.vikunja.database.path = "vikunja.db";

New body:

services.vikunja.database.path = "/var/lib/vikunja/vikunja.db";

Projection:

{ root = c.services.vikunja.settings.service.rootpath; db = c.services.vikunja.settings.database.path; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/p7by9kfijrj0aji40srp10y15q068ih2-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "db": "vikunja.db",
    "root": "/var/lib/vikunja/app"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/a8xwhi68y88340wsny6yij23x06377ha-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "db": "/var/lib/vikunja/vikunja.db",
    "root": "/var/lib/vikunja/app"
  },
  "warnings": []
}

<a id="s15"></a>

S15: OpenSnitch rule files remain in the old directory

Old form: Provision JSON files only under /etc/opensnitchd/rules and omit settings.Rules.Path.

New form: Use services.opensnitch.rules so the module installs rules in the current mutable directory, or explicitly point the daemon at a suitable intended directory.

The daemon now defaults to /var/lib/opensnitch/rules. The legacy file is still generated under /etc but is not in the daemon's rule directory. The probe's old preStart is empty; the repaired one creates the rule symlink under the current path.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:211; nixos/modules/services/security/opensnitch.nix:141; nixos/modules/services/security/opensnitch.nix:178.

Hidden evaluator: Check rule content as well as active rule path and preStart installation. An unrelated file under /etc must not count as a loaded firewall policy. Preserve the intended allow/deny semantics.

VM grading: Yes, with kernel support for the selected OpenSnitch monitor. Start a deterministic test process and exercise a declared allow/deny rule; check the daemon loaded it from the current directory. Sketch: machine.wait_for_unit("opensnitchd.service"); machine.succeed("dig @192.168.1.2 example.test"). Configure default-deny and a test DNS server; otherwise a successful lookup might bypass the intended rule test.

Common body:

services.opensnitch.enable = true; environment.etc."opensnitchd/rules/allow-dns.json".text = builtins.toJSON { name = "allow-dns"; enabled = true; action = "allow"; duration = "always"; operator = { type = "simple"; operand = "dest.port"; data = "53"; }; };

Old body:

# No additional definitions.

New body:

services.opensnitch.rules.allow-dns = { name = "allow-dns"; enabled = true; action = "allow"; duration = "always"; operator = { type = "simple"; operand = "dest.port"; data = "53"; }; };

Projection:

{ path = c.services.opensnitch.settings.Rules.Path; preStart = c.systemd.services.opensnitchd.preStart; rules = builtins.attrNames c.services.opensnitch.rules; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/yp8nz7sqxgnns9mj5aqv74pxvcryk9r1-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "path": "/var/lib/opensnitch/rules",
    "preStart": "",
    "rules": []
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/d6s6vy3sqadvwk027s0zsav0p5kgn42i-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "path": "/var/lib/opensnitch/rules",
    "preStart": "# Remove all firewall rules from rules path (configured with\n# cfg.settings.Rules.Path) that are symlinks to a store-path, but aren't\n# declared in `cfg.rules` (i.e. all networks that were \"removed\" from\n# `cfg.rules`).\nfind /var/lib/opensnitch/rules -type l -lname '/nix/store/*' -not \\( -name 'allow-dns.json*' \\) \\\n -delete\nln -sf '/nix/store/54zs47yyn53b0lnva12wy3gn9zp2ngji-rule' \"/var/lib/opensnitch/rules/allow-dns.json\"\n\n",
    "rules": [
      "allow-dns"
    ]
  },
  "warnings": []
}

<a id="s16"></a>

S16: Explicit Tandoor media root preserves database exposure

Old form: On a 25.11 installation explicitly set MEDIA_ROOT=/var/lib/tandoor-recipes, keeping the old layout.

New form: Move media into the media subdirectory, update the setting and any reverse proxy, and preserve the database outside it.

Serving the entire data directory as media can expose db.sqlite3. The module warns only when old-state MEDIA_ROOT is omitted, so an explicit legacy value is a quiet security regression. The pinned migration manual documents the vulnerability and data-move sequence.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:234; nixos/modules/services/misc/tandoor-recipes.nix:124; nixos/modules/services/misc/tandoor-recipes.md:3.

Hidden evaluator: Assert MEDIA_ROOT is the migrated subtree, required StateDirectory entries exist, and any proxy serves that subtree. Also assert system.stateVersion stays at 25.11; raising it is not a data migration.

VM grading: Yes. Seed a media image and SQLite database, then request both through the configured media endpoint: the image must succeed and the database request must return 404/403. Sketch: machine.succeed("curl --fail http://localhost/media/fixture.png"); machine.fail("curl --fail http://localhost/media/db.sqlite3").

Common body:

services.tandoor-recipes.enable = true; system.stateVersion = "25.11";

Old body:

services.tandoor-recipes.extraConfig.MEDIA_ROOT = "/var/lib/tandoor-recipes";

New body:

services.tandoor-recipes.extraConfig.MEDIA_ROOT = "/var/lib/tandoor-recipes/media";

Projection:

{ media = c.systemd.services.tandoor-recipes.environment.MEDIA_ROOT; state = c.systemd.services.tandoor-recipes.serviceConfig.StateDirectory; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/081szlqq0yyc7fh9vm21392d67gmh4cq-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "media": "/var/lib/tandoor-recipes",
    "state": [
      "tandoor-recipes"
    ]
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/7yk6h16w70d4kadzakz0vdb4qpn7aylz-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "media": "/var/lib/tandoor-recipes/media",
    "state": [
      "tandoor-recipes",
      "tandoor-recipes/media"
    ]
  },
  "warnings": []
}

<a id="s17"></a>

S17: ESPHome persistence follows the obsolete private directory

Old form: On an ephemeral root persist only /var/lib/private/esphome.

New form: Persist /var/lib/esphome, where the static service user now writes.

ESPHome now uses a static user and a real StateDirectory path. A persistence mount at the old private path no longer covers the working state, so YAML files and PlatformIO state can vanish on reboot. The probe models persistence with a bind mount and needs no third-party impermanence module.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:403; nixos/modules/services/home-automation/esphome.nix:108.

Hidden evaluator: Require static user identity, StateDirectory, and a mount/persistence entry covering the actual state path. Do not infer durability merely from a directory named esphome somewhere in the filesystem configuration.

VM grading: Yes, ephemeral-root VM. Create an ESPHome configuration in /var/lib/esphome, reboot with root reset while preserving /persist, then assert the file remains. Sketch: machine.succeed("echo fixture > /var/lib/esphome/persist-check"); machine.reboot(); machine.succeed("grep fixture /var/lib/esphome/persist-check").

Common body:

services.esphome.enable = true;

Old body:

fileSystems."/var/lib/private/esphome" = { device = "/persist/esphome"; fsType = "none"; options = [ "bind" ]; };

New body:

fileSystems."/var/lib/esphome" = { device = "/persist/esphome"; fsType = "none"; options = [ "bind" ]; };

Projection:

{ mounts = builtins.attrNames c.fileSystems; user = c.systemd.services.esphome.serviceConfig.User; state = c.systemd.services.esphome.serviceConfig.StateDirectory; dynamic = c.systemd.services.esphome.serviceConfig.DynamicUser or false; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/q55kylwr72bk318hxydhnmm6np3bfi9n-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "dynamic": false,
    "mounts": [
      "/",
      "/var/lib/private/esphome"
    ],
    "state": "esphome",
    "user": "esphome"
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/19d9xv7m264p0vcghbr2i4mp22nvnb6n-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "dynamic": false,
    "mounts": [
      "/",
      "/var/lib/esphome"
    ],
    "state": "esphome",
    "user": "esphome"
  },
  "warnings": []
}

<a id="s18"></a>

S18: Firewall audit logging defaults off

Old form: Enable the firewall and assume refused TCP connection logging remains on.

New form: Set networking.firewall.logRefusedConnections=true for a host whose monitoring policy requires those messages.

The new default removes refused-connection log rules while still dropping traffic. A monitoring or incident-response workflow silently loses its input. This is wrong only under an explicit logging requirement; the new default is intentional.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:483; nixos/modules/services/networking/firewall.nix:119; nixos/modules/services/networking/firewall-nftables.nix:133.

Hidden evaluator: Check the generated nftables rule containing the refused-connection prefix, not only the Boolean. This probe explicitly opts into nftables; it does not claim nftables is the global default.

VM grading: Yes, two VMs. Send a TCP SYN to a blocked port from the peer, then machine.wait_until_succeeds("journalctl -k | grep 'refused connection:'").

Common body:

networking.firewall.enable = true; networking.nftables.enable = true;

Old body:

# No additional definitions.

New body:

networking.firewall.logRefusedConnections = true;

Projection:

{ log = c.networking.firewall.logRefusedConnections; logRules = lib.filter (line: lib.hasInfix "refused connection:" line) (lib.splitString "\n" c.networking.nftables.tables.nixos-fw.content); }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/3x59qs4whgm1wd6c0qqjmbl8m8x0pik8-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "log": false,
    "logRules": []
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/kz9djm124npsspgy67mzgazbaps26lf0-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "log": true,
    "logRules": [
      "  tcp flags syn / fin,syn,rst,ack log level info prefix \"refused connection: \""
    ]
  },
  "warnings": []
}

<a id="s19"></a>

S19: InvoicePlane Caddy customization applies to a stale site key

Old form: Customize services.caddy.virtualHosts."http://billing.example.test" for an InvoicePlane site.

New form: Customize the generated key "billing.example.test"; separately set hostName to an http URL only if the task explicitly requires HTTP.

The module now creates the bare-hostname site with automatic HTTPS. The old customization creates a second HTTP site and leaves the actual InvoicePlane site without the intended header, access policy, or other customization. The probe uses a visible header to avoid claiming a particular authorization mechanism.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:476; nixos/modules/services/web-apps/invoiceplane.nix:451.

Hidden evaluator: Assert the required directive is in the generated application vhost, its PHP handler remains, and no unwanted duplicate site exists. Merely finding the header anywhere in the Caddy configuration is insufficient.

VM grading: Yes. Resolve the test hostname locally and use a test/internal certificate; curl the InvoicePlane HTTPS endpoint and assert the X-Bench header on its response. Sketch: machine.succeed("curl --fail -k -sD - https://billing.example.test/ -o /dev/null | grep -i "X-Bench: policy"").

Common body:

services.invoiceplane.sites."billing.example.test" = {};

Old body:

services.caddy.virtualHosts."http://billing.example.test".extraConfig = "header X-Bench policy";

New body:

services.caddy.virtualHosts."billing.example.test".extraConfig = "header X-Bench policy";

Projection:

lib.mapAttrs (_: v: { inherit (v) hostName extraConfig; }) c.services.caddy.virtualHosts

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/08am40rs6c6g2cgdg5x9vkxggq4k3kpf-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "billing.example.test": {
      "extraConfig": "root * /nix/store/2m1449w5f6k328knhinmnfn7gc4h9ijx-invoiceplane-billing.example.test-1.7.2\nfile_server\nphp_fastcgi unix//run/phpfpm/invoiceplane-billing.example.test.sock\n",
      "hostName": "billing.example.test"
    },
    "http://billing.example.test": {
      "extraConfig": "header X-Bench policy",
      "hostName": "http://billing.example.test"
    }
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/lqn6xf2jnpm4bfx6lhs4w306f2c5hnav-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "billing.example.test": {
      "extraConfig": "root * /nix/store/2m1449w5f6k328knhinmnfn7gc4h9ijx-invoiceplane-billing.example.test-1.7.2\nfile_server\nphp_fastcgi unix//run/phpfpm/invoiceplane-billing.example.test.sock\n\nheader X-Bench policy",
      "hostName": "billing.example.test"
    }
  },
  "warnings": []
}

<a id="s20"></a>

S20: D-Bus default changes the live-switch compatibility contract

Old form: Omit services.dbus.implementation on a classic-daemon host that must remain live-switch compatible.

New form: Explicitly retain services.dbus.implementation="dbus", or schedule the intended broker transition with a reboot.

The new generation selects broker and records a different switch-inhibitor value. Evaluation is quiet, but switching from a classic-daemon generation is refused and needs a reboot. This is an operational trap, not evidence that broker is generally incorrect.

Pinned evidence: nixos/doc/manual/release-notes/rl-2605.section.md:44; nixos/modules/services/system/dbus.nix:66; nixos/modules/system/activation/switchable-system.nix:34.

Hidden evaluator: Compare both generations' system.switch.inhibitors.dbus-implementation, requiring the task's stated continuity policy. Do not call the inhibitor a config warning or remove it to force an unsafe switch.

VM grading: Yes, two-generation VM. Boot classic D-Bus and attempt the candidate switch; assert the preserved implementation succeeds, while the broker transition is rejected without the bypass environment variable. Sketch: machine.succeed("/run/candidate/bin/switch-to-configuration switch").

Common body:

services.dbus.enable = true;

Old body:

# No additional definitions.

New body:

services.dbus.implementation = "dbus";

Projection:

{ implementation = c.services.dbus.implementation; inhibitors = c.system.switch.inhibitors; }

Actual old output, exit 0, stderr empty:

{
  "drv": "/nix/store/m4365isvd3ip1hvm738zin1l7gfbdfcy-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "implementation": "broker",
    "inhibitors": {
      "dbus-implementation": "broker"
    }
  },
  "warnings": []
}

Actual new output, exit 0, stderr empty:

{
  "drv": "/nix/store/g6lqq6w8s6qawg64p40nhl8l6vhagac7-nixos-system-nixos-26.05pre-git.drv",
  "value": {
    "implementation": "dbus",
    "inhibitors": {
      "dbus-implementation": "dbus"
    }
  },
  "warnings": []
}

<a id="s21"></a>

S21: Neovim silently disables Python and Ruby providers

Old form: pkgs.neovim with an existing Python/Ruby remote-plugin workflow.

New form: pkgs.neovim.override { withPython3=true; withRuby=true; }.

The wrapper now explicitly sets both loaded-provider flags to zero by default. An otherwise available Python or Ruby executable does not undo those generated settings, so remote plugins stop working.

Pinned evidence: doc/release-notes/rl-2605.section.md:451; pkgs/applications/editors/neovim/wrapper.nix:40; pkgs/applications/editors/neovim/wrapper.nix:185.

Hidden evaluator: Inspect providerLuaRc and the resolved wrapper dependencies. Require enabled provider host paths, preserving unrelated plugin configuration. Separate NixOS module options can also be a valid repair.

VM grading: Yes. Install the candidate and run machine.succeed("nvim --headless '+python3 print(42)' +qa"); add an analogous Ruby provider command if both are contractual.

Run the package expression in the following section and select neovim for this pair. Both forms evaluate in that invocation. Actual output, exit 0, stderr empty:

{
  "new": {
    "drv": "/nix/store/4lnhdpqrcd1gyn7a57aclnzjivb900ms-neovim-0.12.4.drv",
    "providerLuaRc": "vim.g.loaded_node_provider=0;vim.g.loaded_perl_provider=0;vim.g.ruby_host_prog='/nix/store/9bbdan6b6z2p53a5gzqz8i5kz1a59na9-neovim-ruby-env/bin/neovim-ruby-host';vim.g.python3_host_prog='/nix/store/b6n1n9znp2bl9s5ixzkvyfrw4sk96dl6-nvim-host-python3-3.13.15-env/bin/nvim-python3'",
    "withPython3": true,
    "withRuby": true
  },
  "old": {
    "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
    "providerLuaRc": "vim.g.loaded_node_provider=0;vim.g.loaded_perl_provider=0;vim.g.loaded_ruby_provider=0;vim.g.loaded_python3_provider=0",
    "withPython3": false,
    "withRuby": false
  }
}

<a id="s22"></a>

S22: Corepack moves out of the nodejs-slim default output

Old form: An environment includes only pkgs.nodejs-slim and calls corepack.

New form: Include pkgs.nodejs-slim.corepack alongside the main output.

The package exposes Corepack in a separate output. Evaluation succeeds, but the resulting selected environment lacks the command. The explicit selected paths and the package install logic establish the difference without building.

Pinned evidence: doc/release-notes/rl-2605.section.md:443; pkgs/development/web/nodejs/nodejs.nix:303; pkgs/development/web/nodejs/nodejs.nix:558.

Hidden evaluator: Inspect the actual environment paths or generated buildEnv derivation. Accept a suitable wrapped pkgs.nodejs if allowed by the task's closure requirements; do not require one source spelling.

VM grading: Yes. Install only the specified outputs and machine.succeed("corepack --version"); this does not need a registry connection or package-manager download.

Run the package expression in the following section and select node for this pair. Both forms evaluate in that invocation. Actual output, exit 0, stderr empty:

{
  "new": {
    "drv": "/nix/store/jdhjk1asx6gsqmw7n1cch27vns0n2ybk-node-tools.drv",
    "outputs": [
      "out",
      "libv8",
      "npm",
      "corepack",
      "dev"
    ],
    "paths": [
      "/nix/store/503mh0fj8j1cfvrxcavr76szbn4dk2r7-nodejs-slim-24.21.0",
      "/nix/store/l02f9vbbl64hr98y23plibfxl1cbf3ww-nodejs-slim-24.21.0-corepack"
    ]
  },
  "old": {
    "drv": "/nix/store/z1wsiprg3d1ggmq6dk0hblwrcnx582ga-node-tools.drv",
    "outputs": [
      "out",
      "libv8",
      "npm",
      "corepack",
      "dev"
    ],
    "paths": [
      "/nix/store/503mh0fj8j1cfvrxcavr76szbn4dk2r7-nodejs-slim-24.21.0"
    ]
  }
}

<a id="s23"></a>

S23: Pikepdf notebook rendering loses its bundled mutool

Old form: pkgs.python3Packages.pikepdf in an isolated notebook environment without MuPDF on PATH.

New form: Override pikepdf with withMupdf=true.

The package now substitutes bare mutool into the page-rendering helper by default, while the enabled form substitutes an absolute store executable. Opening/editing PDFs can still work; notebook page rendering fails when it calls the absent tool.

Pinned evidence: doc/release-notes/rl-2605.section.md:251; pkgs/development/python-modules/pikepdf/default.nix:10; pkgs/development/python-modules/pikepdf/default.nix:44; pkgs/development/python-modules/pikepdf/paths.patch:7.

Hidden evaluator: Inspect the generated paths patch and its store reference. The probe evaluates the patch derivation's buildPhase, not an unbuilt output file or an actual closure query.

VM grading: Yes. In a Python environment with no mutool on PATH, create a one-page PDF and invoke pikepdf._methods._run_mudraw; assert its output image exists. Sketch: machine.succeed("python3 /etc/fixture/render-one-page-with-pikepdf.py"). The fixture script creates the PDF, calls _run_mudraw, and checks the PNG under a PATH without mutool.

Run the package expression in the following section and select pikepdf for this pair. Both forms evaluate in that invocation. Actual output, exit 0, stderr empty:

{
  "new": {
    "drv": "/nix/store/5b2v1by0pzfljpg4csa8c780s42zm42z-python3.13-pikepdf-10.5.1.drv",
    "patchBuildPhase": "runHook preBuild\n\ntarget=$out\nif test -n \"$dir\"; then\n    target=$out/$dir/$name\n    mkdir -p $out/$dir\nfi\n\nsubstitute \"$src\" \"$target\" --replace-fail @jbig2dec@ /nix/store/pszh9jlrsmljjx08d2w0lbppzwfid5wi-jbig2dec-0.20/bin/jbig2dec --replace-fail @mutool@ /nix/store/3aq8hi729ah10fb0qqgjc0smrchh2j2j-mupdf-1.27.2-bin/bin/mutool\n\nif test -n \"$isExecutable\"; then\n    chmod +x $target\nfi\n\nrunHook postBuild\n"
  },
  "old": {
    "drv": "/nix/store/94v7q70p1qvzr6i8v955594skjxk0bsn-python3.13-pikepdf-10.5.1.drv",
    "patchBuildPhase": "runHook preBuild\n\ntarget=$out\nif test -n \"$dir\"; then\n    target=$out/$dir/$name\n    mkdir -p $out/$dir\nfi\n\nsubstitute \"$src\" \"$target\" --replace-fail @jbig2dec@ /nix/store/pszh9jlrsmljjx08d2w0lbppzwfid5wi-jbig2dec-0.20/bin/jbig2dec --replace-fail @mutool@ mutool\n\nif test -n \"$isExecutable\"; then\n    chmod +x $target\nfi\n\nrunHook postBuild\n"
  }
}

Package probe expressions

The first expression produces S21, S23 and the established Python-builder trap B01 below. The second produces S22. Both were run with the same nix eval command above; each command exited 0 with empty stderr. Their paired JSON values appear under the relevant candidate.

let
  p = import /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source { system = "x86_64-linux"; config = {}; overlays = []; };
  patchInfo = x: { drv = x.drvPath; patchBuildPhase = (builtins.head x.patches).drvAttrs.buildPhase; };
  nvimInfo = x: { drv = x.drvPath; inherit (x) withPython3 withRuby providerLuaRc; };
  py = p.python3Packages.buildPythonPackage { pname = "silent-probe"; version = "1"; pyproject = true; src = p.path; checkPhase = "echo OLD_TEST"; };
  pyInfo = x: { drv = x.drvPath; checkPhase = x.checkPhase or null; inherit (x) installCheckPhase doCheck doInstallCheck; };
in {
  pikepdf = { old = patchInfo p.python3Packages.pikepdf; new = patchInfo (p.python3Packages.pikepdf.override { withMupdf = true; }); };
  neovim = { old = nvimInfo p.neovim; new = nvimInfo (p.neovim.override { withPython3 = true; withRuby = true; }); };
  pythonOverride = { old = pyInfo (py.overrideAttrs { checkPhase = "echo NEW_TEST"; }); new = pyInfo (py.overridePythonAttrs { checkPhase = "echo NEW_TEST"; }); };
}
let
  p = import /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source { system = "x86_64-linux"; config = {}; overlays = []; };
  info = paths: let x = p.buildEnv { name = "node-tools"; inherit paths; }; in { drv = x.drvPath; paths = map toString paths; inherit (p.nodejs-slim) outputs; };
in { old = info [ p.nodejs-slim ]; new = info [ p.nodejs-slim p.nodejs-slim.corepack ]; }

Additional established builder trap

B01: Python override changes a phase the builder does not run

Old form: pythonPackage.overrideAttrs { checkPhase = "echo NEW_TEST"; }. New form: pythonPackage.overridePythonAttrs { checkPhase = "echo NEW_TEST"; }, or explicitly override the active installCheckPhase.

The Python builder translates checkPhase into installCheckPhase and sets doCheck=false. An override applied after that translation can look correct while leaving the old installed-package test in place. This is a silent failure to run the requested regression test, not a demonstrated new 26.05 behavior. Keep it out of the fresh-material count.

Pinned evidence: pkgs/development/interpreters/python/mk-python-derivation.nix:391; pkgs/development/interpreters/python/mk-python-derivation.nix:437; doc/languages-frameworks/python.section.md:287.

Hidden evaluator: require the effective installCheckPhase to contain the replacement test and doInstallCheck to remain true. Better build grading uses a replacement check that creates a marker required by postInstallCheck. A VM adds little here; this needs a package build. The synthetic source in the evaluation probe is not a buildable Python fixture.

Actual output, exit 0, stderr empty:

{
  "new": {
    "checkPhase": null,
    "doCheck": false,
    "doInstallCheck": true,
    "drv": "/nix/store/wxj6j9za7a94wjr1y541067jfwrcm1hy-python3.13-silent-probe-1.drv",
    "installCheckPhase": "echo NEW_TEST"
  },
  "old": {
    "checkPhase": "echo NEW_TEST",
    "doCheck": false,
    "doInstallCheck": true,
    "drv": "/nix/store/x3vn674glmjyhbb9dgn9gdhfqjvqakjj-python3.13-silent-probe-1.drv",
    "installCheckPhase": "echo OLD_TEST"
  }
}

Silent-regression traps

These five tasks need no newly introduced nixpkgs behavior. Use a repository layout such as profiles/shared.nix, hosts/a.nix, hosts/b.nix, and overlays/default.nix; the self-contained expressions below model those module boundaries with separate module values. Every trap has an original state, a natural but overbroad fix, and a scoped repair. Both hosts evaluate in all three states without warnings or stderr.

The evaluator must check the requested change on A and the preservation contract on B. For these probes, every repaired B full-system derivation exactly equals its original derivation, while every overbroad repair changes B's derivation. That is stronger evidence than checking only the target option, but task grading should assert the stated host contracts instead of hard-coding an entire derivation hash.

Pinned priority semantics: lib/modules.nix:1408 and lib/modules.nix:1582. Normal definitions have priority 100, mkDefault 1000, and mkForce 50. Lower numbers win, and discarded definitions do not necessarily produce a conflict or warning.

T01: Shared profile shadows a sibling's SSH default

A must temporarily permit password authentication; B must remain keys-only. Both originally inherit PasswordAuthentication=mkDefault false. Setting a normal true in the shared profile silently makes both true. Put the true definition only in A's module.

Hidden evaluator and VM sketch: Assert A=true and B=false in the resolved sshd settings. VM: verify an A fixture password login succeeds while B rejects a password login and still accepts its authorized key.

Actual resolved values, extracted unchanged from the full JSON below:

{
  "bad": {
    "a": true,
    "b": true
  },
  "good": {
    "a": true,
    "b": false
  },
  "original": {
    "a": false,
    "b": false
  }
}

T02: Forced firewall list removes sibling service access

A must expose only TCP 443 because its SSH access is out of band; B still needs SSH and exporter port 9100. A shared allowedTCPPorts=mkForce [443] replaces the merged list for both hosts, including ports added by service modules. Scope that force to A.

Hidden evaluator and VM sketch: Assert A=[443], B contains 22 and 9100, and B gains no 443 rule. VM: peer-connect to B's SSH/exporter and confirm A's closed ports remain unreachable.

Actual resolved values, extracted unchanged from the full JSON below:

{
  "bad": {
    "a": [
      443
    ],
    "b": [
      443
    ]
  },
  "good": {
    "a": [
      443
    ],
    "b": [
      22,
      9100
    ]
  },
  "original": {
    "a": [
      22,
      443
    ],
    "b": [
      22,
      9100
    ]
  }
}

T03: Shared overlay enables providers in the sibling package set

A needs Neovim's Python provider; B must retain its provider-free editor closure. Putting the provider override in a shared overlay changes the same package on both hosts. Apply the overlay only when constructing A's package set, or override A's selected editor package directly.

Hidden evaluator and VM sketch: Assert A's selected neovim provider is true and B's selected package/provider derivation is unchanged. VM/build: run the provider on A and inspect B's editor configuration; a built closure test can additionally exclude the provider Python environment.

Actual resolved values, extracted unchanged from the full JSON below:

{
  "bad": {
    "a": {
      "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
      "provider": true
    },
    "b": {
      "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
      "provider": true
    }
  },
  "good": {
    "a": {
      "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
      "provider": true
    },
    "b": {
      "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
      "provider": false
    }
  },
  "original": {
    "a": {
      "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
      "provider": false
    },
    "b": {
      "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
      "provider": false
    }
  }
}

T04: Shared sandbox repair drops B's writable state path

A needs /srv/uploads writable in addition to its cache, while B needs /srv/backups writable. A shared ReadWritePaths=mkForce ["/srv/uploads"] makes both evaluate but discards each existing exception under ProtectSystem=strict. Add the uploads exception only on A and preserve B's list.

Hidden evaluator and VM sketch: Assert A retains /srv/cache plus /srv/uploads and B retains /srv/backups. VM: have each worker write to its required directories from inside the actual service sandbox, then check the marker files.

Actual resolved values, extracted unchanged from the full JSON below:

{
  "bad": {
    "a": [
      "/srv/uploads"
    ],
    "b": [
      "/srv/uploads"
    ]
  },
  "good": {
    "a": [
      "/srv/cache",
      "/srv/uploads"
    ],
    "b": [
      "/srv/backups"
    ]
  },
  "original": {
    "a": [
      "/srv/cache"
    ],
    "b": [
      "/srv/backups"
    ]
  }
}

T05: Shared networkd fix accepts DHCP DNS on an isolated sibling

A must start accepting DHCP DNS; B must keep rejecting DHCP DNS and use its controlled resolver policy. A shared forced dhcpV4Config.UseDNS=true fixes A while changing B. Set true in A's interface module and keep B=false.

Hidden evaluator and VM sketch: Assert both use networkd and the interface-specific DHCPv4 DNS policy is true only on A. VM: a fixture DHCP server advertises a distinctive DNS address; inspect resolvectl status eth1 and make a lookup demonstrating A uses it while B does not.

Actual resolved values, extracted unchanged from the full JSON below:

{
  "bad": {
    "a": true,
    "b": true
  },
  "good": {
    "a": true,
    "b": false
  },
  "original": {
    "a": false,
    "b": false
  }
}

Reproducible multi-host expression

Run this file with the same nix eval command. Its three states and both hosts are all forced by JSON serialization. Exit 0, zero bytes of stderr.

let
 pin = /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source;
 lib = import (pin + /lib);
 base = { lib, ... }: {
   nixpkgs.config = {}; nixpkgs.overlays = [];
   boot.loader.grub.enable = false;
   fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
   system.stateVersion = "26.05";
 };
 host = name: modules: project:
   let s = import (pin + /nixos) { system = "x86_64-linux"; configuration = {
     imports = [ base { networking.hostName = name; } ] ++ modules;
   }; }; in { drv = s.config.system.build.toplevel.drvPath; warnings = s.config.warnings; value = project s; };
 pair = modulesA: modulesB: project: {
   a = host "a" modulesA project;
   b = host "b" modulesB project;
 };
 sshBase = { lib, ... }: { services.openssh.enable = true; services.openssh.settings.PasswordAuthentication = lib.mkDefault false; };
 sshProject = s: s.config.services.openssh.settings.PasswordAuthentication;
 fwBase = { services.openssh.enable = true; };
 fwA = { networking.firewall.allowedTCPPorts = [ 443 ]; };
 fwB = { networking.firewall.allowedTCPPorts = [ 9100 ]; };
 fwProject = s: s.config.networking.firewall.allowedTCPPorts;
 editor = { pkgs, ... }: { environment.systemPackages = [ pkgs.neovim ]; };
 providerOverlay = { nixpkgs.overlays = [ (final: prev: { neovim = prev.neovim.override { withPython3 = true; }; }) ]; };
 editorProject = s: let n = lib.findFirst (p: lib.getName p == "neovim") null s.config.environment.systemPackages; in { provider = n.withPython3; drv = n.drvPath; };
 worker = { systemd.services.worker = { wantedBy = [ "multi-user.target" ]; script = "true"; serviceConfig = { Type = "oneshot"; ProtectSystem = "strict"; }; }; };
 workerA = { systemd.services.worker.serviceConfig.ReadWritePaths = [ "/srv/cache" ]; };
 workerB = { systemd.services.worker.serviceConfig.ReadWritePaths = [ "/srv/backups" ]; };
 workerProject = s: s.config.systemd.services.worker.serviceConfig.ReadWritePaths;
 resolver = { networking.useNetworkd = true; networking.useDHCP = false; services.resolved.enable = true;
   systemd.network.networks."10-uplink" = { matchConfig.Name = "eth1"; DHCP = "ipv4"; };
 };
 dnsA = { systemd.network.networks."10-uplink".dhcpV4Config.UseDNS = true; };
 dnsB = { systemd.network.networks."10-uplink".dhcpV4Config.UseDNS = false; };
 dnsProject = s: s.config.systemd.network.networks."10-uplink".dhcpV4Config.UseDNS;
in {
 T01 = {
  original = pair [ sshBase ] [ sshBase ] sshProject;
  bad = pair [ sshBase { services.openssh.settings.PasswordAuthentication = true; } ] [ sshBase { services.openssh.settings.PasswordAuthentication = true; } ] sshProject;
  good = pair [ sshBase { services.openssh.settings.PasswordAuthentication = true; } ] [ sshBase ] sshProject;
 };
 T02 = let sharedFix = { lib, ... }: { networking.firewall.allowedTCPPorts = lib.mkForce [ 443 ]; }; in {
  original = pair [ fwBase fwA ] [ fwBase fwB ] fwProject;
  bad = pair [ fwBase fwA sharedFix ] [ fwBase fwB sharedFix ] fwProject;
  good = pair [ fwBase fwA sharedFix ] [ fwBase fwB ] fwProject;
 };
 T03 = {
  original = pair [ editor ] [ editor ] editorProject;
  bad = pair [ editor providerOverlay ] [ editor providerOverlay ] editorProject;
  good = pair [ editor providerOverlay ] [ editor ] editorProject;
 };
 T04 = let sharedFix = { lib, ... }: { systemd.services.worker.serviceConfig.ReadWritePaths = lib.mkForce [ "/srv/uploads" ]; }; in {
  original = pair [ worker workerA ] [ worker workerB ] workerProject;
  bad = pair [ worker workerA sharedFix ] [ worker workerB sharedFix ] workerProject;
  good = pair [ worker workerA { systemd.services.worker.serviceConfig.ReadWritePaths = [ "/srv/uploads" ]; } ] [ worker workerB ] workerProject;
 };
 T05 = let sharedFix = { lib, ... }: { systemd.network.networks."10-uplink".dhcpV4Config.UseDNS = lib.mkForce true; }; in {
  original = pair [ resolver dnsB ] [ resolver dnsB ] dnsProject;
  bad = pair [ resolver dnsB sharedFix ] [ resolver dnsB sharedFix ] dnsProject;
  good = pair [ resolver dnsA ] [ resolver dnsB ] dnsProject;
 };
}

Full actual output, including all system derivation paths and warning lists:

{
  "T01": {
    "bad": {
      "a": {
        "drv": "/nix/store/aiinq1mwmkz8rqj1yi9izyz316gr1w48-nixos-system-a-26.05pre-git.drv",
        "value": true,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/6czpqb57n4ghj3vvvnpg9v73g1a2y4j3-nixos-system-b-26.05pre-git.drv",
        "value": true,
        "warnings": []
      }
    },
    "good": {
      "a": {
        "drv": "/nix/store/aiinq1mwmkz8rqj1yi9izyz316gr1w48-nixos-system-a-26.05pre-git.drv",
        "value": true,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/vsbs5bl95rkmkha4w2fi4jjz6wwfzdwx-nixos-system-b-26.05pre-git.drv",
        "value": false,
        "warnings": []
      }
    },
    "original": {
      "a": {
        "drv": "/nix/store/ji5qfapszbq9gx7bp5wdygld3a4lrhcc-nixos-system-a-26.05pre-git.drv",
        "value": false,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/vsbs5bl95rkmkha4w2fi4jjz6wwfzdwx-nixos-system-b-26.05pre-git.drv",
        "value": false,
        "warnings": []
      }
    }
  },
  "T02": {
    "bad": {
      "a": {
        "drv": "/nix/store/q2ys3x4n0l6g3qisrs4mmnnhlvsb1fsk-nixos-system-a-26.05pre-git.drv",
        "value": [
          443
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/zb4wrbpzbvdxsc8p57zv0a7widax2b8s-nixos-system-b-26.05pre-git.drv",
        "value": [
          443
        ],
        "warnings": []
      }
    },
    "good": {
      "a": {
        "drv": "/nix/store/q2ys3x4n0l6g3qisrs4mmnnhlvsb1fsk-nixos-system-a-26.05pre-git.drv",
        "value": [
          443
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/799yi60knaq0g7g4x4d6ky0263fgpbap-nixos-system-b-26.05pre-git.drv",
        "value": [
          22,
          9100
        ],
        "warnings": []
      }
    },
    "original": {
      "a": {
        "drv": "/nix/store/w6gpdnajnvk7mxna035g811vshqx39na-nixos-system-a-26.05pre-git.drv",
        "value": [
          22,
          443
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/799yi60knaq0g7g4x4d6ky0263fgpbap-nixos-system-b-26.05pre-git.drv",
        "value": [
          22,
          9100
        ],
        "warnings": []
      }
    }
  },
  "T03": {
    "bad": {
      "a": {
        "drv": "/nix/store/0x7lri1vmhn4489l0b8r91vchnl9721d-nixos-system-a-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
          "provider": true
        },
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/0cs4zsfjiwnh7hhkdfdsmd8yp6dhzn8p-nixos-system-b-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
          "provider": true
        },
        "warnings": []
      }
    },
    "good": {
      "a": {
        "drv": "/nix/store/0x7lri1vmhn4489l0b8r91vchnl9721d-nixos-system-a-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/593jvqdcll7g6a2y9sz8g3kv9ffcjikd-neovim-0.12.4.drv",
          "provider": true
        },
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/pdq9r83qb17czqsr83w4crwwrxx8ygig-nixos-system-b-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
          "provider": false
        },
        "warnings": []
      }
    },
    "original": {
      "a": {
        "drv": "/nix/store/zxjh44qcka2i7xns27q8jrfqb6fw3ryg-nixos-system-a-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
          "provider": false
        },
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/pdq9r83qb17czqsr83w4crwwrxx8ygig-nixos-system-b-26.05pre-git.drv",
        "value": {
          "drv": "/nix/store/djngjqsmsv0cllfsmc06ab3p0mdnhy3x-neovim-0.12.4.drv",
          "provider": false
        },
        "warnings": []
      }
    }
  },
  "T04": {
    "bad": {
      "a": {
        "drv": "/nix/store/7cxvq3bm2lclqs1brs8a2h0l4izishlr-nixos-system-a-26.05pre-git.drv",
        "value": [
          "/srv/uploads"
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/x5a42kyv1vmb4qdfvy6d689sir4n0g7f-nixos-system-b-26.05pre-git.drv",
        "value": [
          "/srv/uploads"
        ],
        "warnings": []
      }
    },
    "good": {
      "a": {
        "drv": "/nix/store/i8ghp05xyh5341m9hz2kc6kak8xi65di-nixos-system-a-26.05pre-git.drv",
        "value": [
          "/srv/cache",
          "/srv/uploads"
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/5lm4pjh9l7gcjcnnz973dd0lycpqkhg2-nixos-system-b-26.05pre-git.drv",
        "value": [
          "/srv/backups"
        ],
        "warnings": []
      }
    },
    "original": {
      "a": {
        "drv": "/nix/store/lsbxa0391kq0hwlxvzk08b911g5mdrna-nixos-system-a-26.05pre-git.drv",
        "value": [
          "/srv/cache"
        ],
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/5lm4pjh9l7gcjcnnz973dd0lycpqkhg2-nixos-system-b-26.05pre-git.drv",
        "value": [
          "/srv/backups"
        ],
        "warnings": []
      }
    }
  },
  "T05": {
    "bad": {
      "a": {
        "drv": "/nix/store/nlnzmhh43s9z2z4bmwn10q5p5kbwqdv9-nixos-system-a-26.05pre-git.drv",
        "value": true,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/5hlwxsv2b93rz9865whhndyqxfcc3krl-nixos-system-b-26.05pre-git.drv",
        "value": true,
        "warnings": []
      }
    },
    "good": {
      "a": {
        "drv": "/nix/store/nlnzmhh43s9z2z4bmwn10q5p5kbwqdv9-nixos-system-a-26.05pre-git.drv",
        "value": true,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/sh4wal12pxfq12lyjll7m2hg9mc6301i-nixos-system-b-26.05pre-git.drv",
        "value": false,
        "warnings": []
      }
    },
    "original": {
      "a": {
        "drv": "/nix/store/5m15ab6fs4i8nkwcdv69njsjqli98dmq-nixos-system-a-26.05pre-git.drv",
        "value": false,
        "warnings": []
      },
      "b": {
        "drv": "/nix/store/sh4wal12pxfq12lyjll7m2hg9mc6301i-nixos-system-b-26.05pre-git.drv",
        "value": false,
        "warnings": []
      }
    }
  }
}

Search coverage and rejected leads

The source search covered the 26.05 and 26.11 NixOS release notes, the Nixpkgs package release notes, changed defaults and default definitions in the relevant modules, generated service configuration, hardening directives, state directories, tmpfiles, initrd and network backend source, Python/Rust/Go builders, lib.fileset, cleanSource, and option-migration helpers. The 26.11 notes are mostly placeholders; their tarball notice describes a future removal rather than a present silent failure.

The following exclusions matter because weakening evaluation to a subattribute read can incorrectly admit them.

  • mkRemovedOptionModule does not provide a generic silent replacement. Defining the old option adds a failing assertion. Reading its value throws. S01 is a narrower exception: importing the compatibility profile without defining its removed marker is inert.
  • mkChangedOptionModule delegates to mkMergedOptionModule; migration messages are not automatically silent. mkAliasOptionModule deliberately suppresses warnings but normally preserves behavior, so changing only its spelling gives no behavioral task.
  • Dovecot removed fields and required 2.4 format versions, Stalwart's missing module stateVersion, and Home Assistant's old lovelace.mode are excluded using the predecessor's recorded errors/warnings. This report does not relabel those diagnostic-driven probes as quiet. Arbitrary unknown freeform keys are not accepted as candidates without a pinned schema/serializer reason establishing their consequence.
  • An attempted SSH initrd probe with /bin/cryptsetup-askpass produced exit 1 with the actual assertion cryptsetup-askpass is not available in systemd stage 1. Please remove it from: boot.initrd.systemd.users.root.shell. It also hit the baseline bootloader's initrd-secret-path guard; those results are rejected, not counted among the 40 admitted NixOS results. Setting the old boot.initrd.network.ssh.shell also has a migration warning. S09 is the separate verified LV-timeout probe, not a disguised askpass task.
  • The NetworkManager release-note suggestion to remove an ordinary wireless.enable=false can encounter an option conflict. Only the explicitly forced legacy definition in S03 is claimed quiet. Do not remove that detail from a fixture and assume it retains the same admission status.
  • The current firewall backend default remains conditional on firewalld and networking.nftables.enable; it does not unconditionally switch every host to nftables. The nftables module asserts that legacy extraCommands and extraStopCommands are empty. A direct iptables-extraCommands migration therefore does not meet this task's quietness requirement. S18 explicitly enables nftables and verifies the independent logging-default change.
  • Networkd disables dhcpcd by default and uses generated per-interface network files. Some old backend settings can become irrelevant, but the default-gateway interface omission is explicitly asserted. T05 uses a proven cross-host DNS-policy regression instead of claiming an undocumented global DNS migration.
  • Avahi wide-area opt-in emits a security warning in the predecessor's source finding. It is not an admitted both-forms-quiet pair here.
  • The Python format and pytestFlagsArray transitions, Rust useFetchCargoVendor=false, and nested stdenv dependency lists all have errors or warnings already recorded by the predecessor. The inspected Rust and Go builder sources still default doCheck to true in the ordinary cases; no new silent test-disable transition was established. B01 is the verified established phase-translation trap.
  • The fileset and cleanSource sources describe compositional filtering and membership behavior. This scan did not establish a fresh silent behavior change with a documented old/new repair. Nor did it establish a warning-free deprecated lib function that both changes behavior and has an actionable replacement. These are search negatives, not claims that all possible traps in those categories are absent.

Exploratory fixture errors were corrected before admission: a duplicate root-device attribute was split into separate modules; InvoicePlane uses sites and has no enable option; wireless EAP settings must populate auth to generate WPA-EAP rather than key_mgmt=NONE. The final code and JSON above are the corrected, rerun probes. No success is inferred from an earlier failed probe.

Relevant source for those limits: lib/modules.nix:1686; lib/modules.nix:1893; nixos/modules/system/boot/initrd-ssh.nix:236; nixos/modules/config/users-groups.nix:1146; nixos/modules/services/networking/firewall.nix:83; nixos/modules/services/networking/firewall-nftables.nix:63; nixos/modules/tasks/network-interfaces-systemd.nix:243; pkgs/build-support/rust/build-rust-package/default.nix:173; pkgs/build-support/go/module.nix:367; lib/sources.nix:108; lib/fileset/default.nix:852.

Authoring constraints for NixBench 3.2

A probe is a candidate, not an authored task. Build real starter/reference repositories, retain the initial service requirements and sibling-host invariants, and mutation-test each hidden assertion. Prefer a prompt describing the broken runtime behavior and existing environment over one that names the new option. Most single-line default repairs will still be easy if the prompt gives away the cause.

Use combined requirements where they represent one coherent operation: Tor endpoint plus socket ownership and lifecycle; wireless key path plus ownership and preserved EAP settings; ESPHome static state plus reboot persistence; InvoicePlane policy plus the intended application vhost. Do not combine unrelated defaults merely to increase edit count.

Keep data migration requirements explicit. A changed path in configuration does not prove that books, mail, recipes, SQLite contents or firmware state were moved safely. Use the VM sketches when runtime behavior matters, and retain static projections for fast, deterministic grading. None of these probes establishes model difficulty; calibration must do that.