at master 4.7 kB view raw
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}