summaryrefslogtreecommitdiffstats
path: root/nixos/tests/boot-stage2.nix
blob: 08c00d46c6c61d3e7e3582757e453744fdc35220 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
{ pkgs, ... }:
{
  name = "boot-stage2";

  nodes.machine =
    {
      config,
      pkgs,
      lib,
      ...
    }:
    let
      # Prints the user's UID. Can't just do a shell script
      # because setuid is ignored for interpreted programs.
      uid = pkgs.writeCBin "uid" ''
        #include <unistd.h>
        #include <stdio.h>
        int main(void) {
          printf("%d\n", geteuid());
          return 0;
        }
      '';
    in
    {
      users.users.alice = {
        isNormalUser = true;
        uid = 1000;
      };

      virtualisation = {
        emptyDiskImages = [ 256 ];

        # Mount an ext4 as the upper layer of the Nix store.
        fileSystems = {
          "/nix/store" = lib.mkForce {
            device = "/dev/vdb"; # the above disk image
            fsType = "ext4";

            # data=journal always displays after errors=remount-ro; this is only needed because of the overlay
            # and #375257 will trigger with `errors=remount-ro` on a non-overlaid store:
            # see ordering in https://github.com/torvalds/linux/blob/v6.12/fs/ext4/super.c#L2974
            options = [
              "defaults"
              "errors=remount-ro"
              "data=journal"
            ];
          };
        };
      };

      environment.systemPackages = [ pkgs.xxd ];

      system.extraDependencies = [ uid ];

      boot = {
        initrd = {
          # TODO: Switch to systemd initrd
          systemd.enable = false;

          # Format the upper Nix store.
          postDeviceCommands = ''
            ${pkgs.e2fsprogs}/bin/mkfs.ext4 /dev/vdb
          '';

          # Overlay the RO store onto it.
          # Note that bug #375257 can be triggered without an overlay,
          # using the errors=remount-ro option (or similar) or with an overlay where any of the
          # paths ends in 'ro'. The offending mountpoint also has to be the last (top) one
          # if an option ending in 'ro' is the last in the list, so test both cases here.
          postMountCommands = ''
            mkdir -p /mnt-root/nix/store/ro /mnt-root/nix/store/rw /mnt-root/nix/store/work
            mount --bind /mnt-root/nix/.ro-store /mnt-root/nix/store/ro
            mount -t overlay overlay \
              -o lowerdir=/mnt-root/nix/store/ro,upperdir=/mnt-root/nix/store/rw,workdir=/mnt-root/nix/store/work \
              /mnt-root/nix/store

            # Be very rude and try to put suid files and/or devices into the store.
            evil=/mnt-root/nix/store/evil
            mkdir -p $evil/bin $evil/dev

            echo "making evil suid..." >&2
            cp /mnt-root/${builtins.unsafeDiscardStringContext "${uid}"}/bin/uid $evil/bin/suid
            chmod 4755 $evil/bin/suid
            [ -u $evil/bin/suid ] || exit 1

            echo "making evil devzero..." >&2
            mknod -m 666 $evil/dev/zero c 1 5
            [ -c $evil/dev/zero ] || exit 1
          '';

          kernelModules = [ "overlay" ];
        };

        postBootCommands = ''
          touch /etc/post-boot-ran
          mount
        '';
      };
    };

  testScript = ''
    machine.wait_for_unit("multi-user.target")
    machine.succeed("test /etc/post-boot-ran")
    machine.fail("touch /nix/store/should-not-work");

    for opt in ["ro", "nosuid", "nodev"]:
      with subtest(f"testing store mount option: {opt}"):
        machine.succeed(f'[[ "$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" =~ (^|,){opt}(,|$) ]]')

    # should still be suid
    machine.succeed('[ -u /nix/store/evil/bin/suid ]')
    # runs as alice and is not root
    machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
    # can be remounted and runs as root
    machine.succeed('mount -o remount,suid,bind /nix/store && mount >&2')
    machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 0 ]')
    # double checking we can undo it
    machine.succeed('mount -o remount,nosuid,bind /nix/store && mount >&2')
    machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')

    # should still be a character device
    machine.succeed('[ -c /nix/store/evil/dev/zero ]')
    # should not work
    machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
    # can be remounted and works
    machine.succeed('mount -o remount,dev,bind /nix/store && mount >&2')
    machine.succeed('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
    # double checking we can undo it
    machine.succeed('mount -o remount,nodev,bind /nix/store && mount >&2')
    machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
  '';

  meta.maintainers = with pkgs.lib.maintainers; [ numinit ];
}