1{ pkgs, ... }:
2{
3 name = "boot-stage2";
4
5 nodes.machine =
6 {
7 config,
8 pkgs,
9 lib,
10 ...
11 }:
12 let
13 # Prints the user's UID. Can't just do a shell script
14 # because setuid is ignored for interpreted programs.
15 uid = pkgs.writeCBin "uid" ''
16 #include <unistd.h>
17 #include <stdio.h>
18 int main(void) {
19 printf("%d\n", geteuid());
20 return 0;
21 }
22 '';
23 in
24 {
25 users.users.alice = {
26 isNormalUser = true;
27 uid = 1000;
28 };
29
30 virtualisation = {
31 emptyDiskImages = [ 256 ];
32
33 # Mount an ext4 as the upper layer of the Nix store.
34 fileSystems = {
35 "/nix/store" = lib.mkForce {
36 device = "/dev/vdb"; # the above disk image
37 fsType = "ext4";
38
39 # data=journal always displays after errors=remount-ro; this is only needed because of the overlay
40 # and #375257 will trigger with `errors=remount-ro` on a non-overlaid store:
41 # see ordering in https://github.com/torvalds/linux/blob/v6.12/fs/ext4/super.c#L2974
42 options = [
43 "defaults"
44 "errors=remount-ro"
45 "data=journal"
46 ];
47 };
48 };
49 };
50
51 environment.systemPackages = [ pkgs.xxd ];
52
53 system.extraDependencies = [ uid ];
54
55 boot = {
56 initrd = {
57 # Format the upper Nix store.
58 postDeviceCommands = ''
59 ${pkgs.e2fsprogs}/bin/mkfs.ext4 /dev/vdb
60 '';
61
62 # Overlay the RO store onto it.
63 # Note that bug #375257 can be triggered without an overlay,
64 # using the errors=remount-ro option (or similar) or with an overlay where any of the
65 # paths ends in 'ro'. The offending mountpoint also has to be the last (top) one
66 # if an option ending in 'ro' is the last in the list, so test both cases here.
67 postMountCommands = ''
68 mkdir -p /mnt-root/nix/store/ro /mnt-root/nix/store/rw /mnt-root/nix/store/work
69 mount --bind /mnt-root/nix/.ro-store /mnt-root/nix/store/ro
70 mount -t overlay overlay \
71 -o lowerdir=/mnt-root/nix/store/ro,upperdir=/mnt-root/nix/store/rw,workdir=/mnt-root/nix/store/work \
72 /mnt-root/nix/store
73
74 # Be very rude and try to put suid files and/or devices into the store.
75 evil=/mnt-root/nix/store/evil
76 mkdir -p $evil/bin $evil/dev
77
78 echo "making evil suid..." >&2
79 cp /mnt-root/${builtins.unsafeDiscardStringContext "${uid}"}/bin/uid $evil/bin/suid
80 chmod 4755 $evil/bin/suid
81 [ -u $evil/bin/suid ] || exit 1
82
83 echo "making evil devzero..." >&2
84 mknod -m 666 $evil/dev/zero c 1 5
85 [ -c $evil/dev/zero ] || exit 1
86 '';
87
88 kernelModules = [ "overlay" ];
89 };
90
91 postBootCommands = ''
92 touch /etc/post-boot-ran
93 mount
94 '';
95 };
96 };
97
98 testScript = ''
99 machine.wait_for_unit("multi-user.target")
100 machine.succeed("test /etc/post-boot-ran")
101 machine.fail("touch /nix/store/should-not-work");
102
103 for opt in ["ro", "nosuid", "nodev"]:
104 with subtest(f"testing store mount option: {opt}"):
105 machine.succeed(f'[[ "$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" =~ (^|,){opt}(,|$) ]]')
106
107 # should still be suid
108 machine.succeed('[ -u /nix/store/evil/bin/suid ]')
109 # runs as alice and is not root
110 machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
111 # can be remounted and runs as root
112 machine.succeed('mount -o remount,suid,bind /nix/store && mount >&2')
113 machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 0 ]')
114 # double checking we can undo it
115 machine.succeed('mount -o remount,nosuid,bind /nix/store && mount >&2')
116 machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
117
118 # should still be a character device
119 machine.succeed('[ -c /nix/store/evil/dev/zero ]')
120 # should not work
121 machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
122 # can be remounted and works
123 machine.succeed('mount -o remount,dev,bind /nix/store && mount >&2')
124 machine.succeed('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
125 # double checking we can undo it
126 machine.succeed('mount -o remount,nodev,bind /nix/store && mount >&2')
127 machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
128 '';
129
130 meta.maintainers = with pkgs.lib.maintainers; [ numinit ];
131}