nixpkgs/nixos/tests/wrappers.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

101 lines
4.3 KiB
Nix
Raw Normal View History

import ./make-test-python.nix ({ pkgs, ... }:
let
userUid = 1000;
usersGid = 100;
busybox = pkgs : pkgs.busybox.override {
# Without this, the busybox binary drops euid to ruid for most applets, including id.
# See https://bugs.busybox.net/show_bug.cgi?id=15101
extraConfig = "CONFIG_FEATURE_SUID n";
};
in
{
name = "wrappers";
nodes.machine = { config, pkgs, ... }: {
ids.gids.users = usersGid;
users.users = {
regular = {
uid = userUid;
isNormalUser = true;
};
};
security.wrappers = {
suidRoot = {
owner = "root";
group = "root";
setuid = true;
source = "${busybox pkgs}/bin/busybox";
program = "suid_root_busybox";
};
sgidRoot = {
owner = "root";
group = "root";
setgid = true;
source = "${busybox pkgs}/bin/busybox";
program = "sgid_root_busybox";
};
withChown = {
owner = "root";
group = "root";
source = "${pkgs.libcap}/bin/capsh";
program = "capsh_with_chown";
capabilities = "cap_chown+ep";
};
};
};
testScript =
''
def cmd_as_regular(cmd):
return "su -l regular -c '{0}'".format(cmd)
def test_as_regular(cmd, expected):
out = machine.succeed(cmd_as_regular(cmd)).strip()
assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
nixos/wrappers: allow setuid and setgid wrappers to run in user namespaces In user namespaces where an unprivileged user is mapped as root and root is unmapped, setuid bits have no effect. However setuid root executables like mount are still usable *in the namespace* as the user already has the required privileges. This commit detects the situation where the wrapper gained no privileges that the parent process did not already have and in this case does less sanity checking. In short there is no need to be picky since the parent already can execute the foo.real executable themselves. Details: man 7 user_namespaces: Set-user-ID and set-group-ID programs When a process inside a user namespace executes a set-user-ID (set-group-ID) program, the process's effective user (group) ID inside the namespace is changed to whatever value is mapped for the user (group) ID of the file. However, if either the user or the group ID of the file has no mapping inside the namespace, the set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the process's effective user (group) ID is left unchanged. (This mirrors the semantics of executing a set-user-ID or set-group-ID program that resides on a filesystem that was mounted with the MS_NOSUID flag, as described in mount(2).) The effect of the setuid bit is that the real user id is preserved and the effective and set user ids are changed to the owner of the wrapper. We detect that no privilege was gained by checking that euid == suid == ruid. In this case we stop checking that euid == owner of the wrapper file. As a reminder here are the values of euid, ruid, suid, stat.st_uid and stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000: Normal case: ruid=1000 euid=42 suid=42 setuid=2048, st_uid=42 nosuid mount: ruid=1000 euid=1000 suid=1000 setuid=2048, st_uid=42 inside unshare -rm: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534 inside unshare -rm, on a suid mount: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534
2023-05-13 12:00:00 +00:00
def test_as_regular_in_userns_mapped_as_root(cmd, expected):
out = machine.succeed(f"su -l regular -c '${pkgs.util-linux}/bin/unshare -rm {cmd}'").strip()
assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}')
test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}')
test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}')
test_as_regular('${busybox pkgs}/bin/busybox id -rg', '${toString usersGid}')
test_as_regular('/run/wrappers/bin/suid_root_busybox id -u', '0')
test_as_regular('/run/wrappers/bin/suid_root_busybox id -ru', '${toString userUid}')
test_as_regular('/run/wrappers/bin/suid_root_busybox id -g', '${toString usersGid}')
test_as_regular('/run/wrappers/bin/suid_root_busybox id -rg', '${toString usersGid}')
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -u', '${toString userUid}')
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -ru', '${toString userUid}')
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0')
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}')
nixos/wrappers: allow setuid and setgid wrappers to run in user namespaces In user namespaces where an unprivileged user is mapped as root and root is unmapped, setuid bits have no effect. However setuid root executables like mount are still usable *in the namespace* as the user already has the required privileges. This commit detects the situation where the wrapper gained no privileges that the parent process did not already have and in this case does less sanity checking. In short there is no need to be picky since the parent already can execute the foo.real executable themselves. Details: man 7 user_namespaces: Set-user-ID and set-group-ID programs When a process inside a user namespace executes a set-user-ID (set-group-ID) program, the process's effective user (group) ID inside the namespace is changed to whatever value is mapped for the user (group) ID of the file. However, if either the user or the group ID of the file has no mapping inside the namespace, the set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the process's effective user (group) ID is left unchanged. (This mirrors the semantics of executing a set-user-ID or set-group-ID program that resides on a filesystem that was mounted with the MS_NOSUID flag, as described in mount(2).) The effect of the setuid bit is that the real user id is preserved and the effective and set user ids are changed to the owner of the wrapper. We detect that no privilege was gained by checking that euid == suid == ruid. In this case we stop checking that euid == owner of the wrapper file. As a reminder here are the values of euid, ruid, suid, stat.st_uid and stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000: Normal case: ruid=1000 euid=42 suid=42 setuid=2048, st_uid=42 nosuid mount: ruid=1000 euid=1000 suid=1000 setuid=2048, st_uid=42 inside unshare -rm: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534 inside unshare -rm, on a suid mount: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534
2023-05-13 12:00:00 +00:00
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -u', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -ru', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -g', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -rg', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -u', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -ru', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0')
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0')
# We are only testing the permitted set, because it's easiest to look at with capsh.
machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN'))
machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN'))
machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN'))
machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN'))
nixos/wrappers: allow setuid and setgid wrappers to run in user namespaces In user namespaces where an unprivileged user is mapped as root and root is unmapped, setuid bits have no effect. However setuid root executables like mount are still usable *in the namespace* as the user already has the required privileges. This commit detects the situation where the wrapper gained no privileges that the parent process did not already have and in this case does less sanity checking. In short there is no need to be picky since the parent already can execute the foo.real executable themselves. Details: man 7 user_namespaces: Set-user-ID and set-group-ID programs When a process inside a user namespace executes a set-user-ID (set-group-ID) program, the process's effective user (group) ID inside the namespace is changed to whatever value is mapped for the user (group) ID of the file. However, if either the user or the group ID of the file has no mapping inside the namespace, the set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the process's effective user (group) ID is left unchanged. (This mirrors the semantics of executing a set-user-ID or set-group-ID program that resides on a filesystem that was mounted with the MS_NOSUID flag, as described in mount(2).) The effect of the setuid bit is that the real user id is preserved and the effective and set user ids are changed to the owner of the wrapper. We detect that no privilege was gained by checking that euid == suid == ruid. In this case we stop checking that euid == owner of the wrapper file. As a reminder here are the values of euid, ruid, suid, stat.st_uid and stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000: Normal case: ruid=1000 euid=42 suid=42 setuid=2048, st_uid=42 nosuid mount: ruid=1000 euid=1000 suid=1000 setuid=2048, st_uid=42 inside unshare -rm: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534 inside unshare -rm, on a suid mount: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534
2023-05-13 12:00:00 +00:00
# test a few "attacks" against which the wrapper protects itself
machine.succeed("cp /run/wrappers/bin/suid_root_busybox{,.real} /tmp/")
machine.fail(cmd_as_regular("/tmp/suid_root_busybox id -u"))
machine.succeed("chmod u+s,a+w /run/wrappers/bin/suid_root_busybox")
machine.fail(cmd_as_regular("/run/wrappers/bin/suid_root_busybox id -u"))
'';
})