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
|
|
|
#define _GNU_SOURCE
|
2016-06-30 23:59:32 +00:00
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <unistd.h>
|
2022-01-27 07:12:00 +00:00
|
|
|
#include <stdnoreturn.h>
|
2016-06-30 23:59:32 +00:00
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/stat.h>
|
2021-01-14 07:24:27 +00:00
|
|
|
#include <sys/xattr.h>
|
2016-06-30 23:59:32 +00:00
|
|
|
#include <fcntl.h>
|
|
|
|
#include <dirent.h>
|
|
|
|
#include <errno.h>
|
2017-01-26 08:41:00 +00:00
|
|
|
#include <linux/capability.h>
|
|
|
|
#include <sys/prctl.h>
|
2018-03-24 17:02:24 +00:00
|
|
|
#include <limits.h>
|
2021-01-14 07:24:27 +00:00
|
|
|
#include <stdint.h>
|
|
|
|
#include <syscall.h>
|
|
|
|
#include <byteswap.h>
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2022-11-04 23:09:32 +00:00
|
|
|
#ifndef SOURCE_PROG
|
|
|
|
#error SOURCE_PROG should be defined via preprocessor commandline
|
|
|
|
#endif
|
|
|
|
|
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
|
|
|
// aborts when false, printing the failed expression
|
2022-01-27 07:12:00 +00:00
|
|
|
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
|
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
|
|
|
// aborts when returns non-zero, printing the failed expression and errno
|
|
|
|
#define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0)
|
2016-06-30 23:59:32 +00:00
|
|
|
|
|
|
|
extern char **environ;
|
|
|
|
|
2017-02-14 00:03:06 +00:00
|
|
|
// The WRAPPER_DIR macro is supplied at compile time so that it cannot
|
|
|
|
// be changed at runtime
|
2021-01-14 07:24:27 +00:00
|
|
|
static char *wrapper_dir = WRAPPER_DIR;
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2017-01-30 18:59:29 +00:00
|
|
|
// Wrapper debug variable name
|
2021-01-14 07:24:27 +00:00
|
|
|
static char *wrapper_debug = "WRAPPER_DEBUG";
|
|
|
|
|
|
|
|
#define CAP_SETPCAP 8
|
|
|
|
|
|
|
|
#if __BYTE_ORDER == __BIG_ENDIAN
|
|
|
|
#define LE32_TO_H(x) bswap_32(x)
|
|
|
|
#else
|
|
|
|
#define LE32_TO_H(x) (x)
|
|
|
|
#endif
|
|
|
|
|
2022-01-27 07:12:00 +00:00
|
|
|
static noreturn void assert_failure(const char *assertion) {
|
|
|
|
fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
|
|
|
|
fflush(stderr);
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
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
|
|
|
static noreturn void print_errno_and_die(const char *assertion) {
|
|
|
|
fprintf(stderr, "Call `%s` in NixOS's wrapper.c failed: %s\n", assertion, strerror(errno));
|
|
|
|
fflush(stderr);
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
int get_last_cap(unsigned *last_cap) {
|
|
|
|
FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
|
|
|
|
if (file == NULL) {
|
|
|
|
int saved_errno = errno;
|
|
|
|
fprintf(stderr, "failed to open /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
|
|
|
|
return -saved_errno;
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|
2021-01-14 07:24:27 +00:00
|
|
|
int res = fscanf(file, "%u", last_cap);
|
|
|
|
if (res == EOF) {
|
|
|
|
int saved_errno = errno;
|
|
|
|
fprintf(stderr, "could not read number from /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
|
|
|
|
return -saved_errno;
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|
2021-01-14 07:24:27 +00:00
|
|
|
fclose(file);
|
|
|
|
return 0;
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Given the path to this program, fetch its configured capability set
|
|
|
|
// (as set by `setcap ... /path/to/file`) and raise those capabilities
|
|
|
|
// into the Ambient set.
|
2021-01-14 07:24:27 +00:00
|
|
|
static int make_caps_ambient(const char *self_path) {
|
|
|
|
struct vfs_ns_cap_data data = {};
|
|
|
|
int r = getxattr(self_path, "security.capability", &data, sizeof(data));
|
|
|
|
|
|
|
|
if (r < 0) {
|
|
|
|
if (errno == ENODATA) {
|
|
|
|
// no capabilities set
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
fprintf(stderr, "cannot get capabilities for %s: %s", self_path, strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
size_t size;
|
|
|
|
uint32_t version = LE32_TO_H(data.magic_etc) & VFS_CAP_REVISION_MASK;
|
|
|
|
switch (version) {
|
|
|
|
case VFS_CAP_REVISION_1:
|
|
|
|
size = VFS_CAP_U32_1;
|
|
|
|
break;
|
|
|
|
case VFS_CAP_REVISION_2:
|
|
|
|
case VFS_CAP_REVISION_3:
|
|
|
|
size = VFS_CAP_U32_3;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
fprintf(stderr, "BUG! Unsupported capability version 0x%x on %s. Report to NixOS bugtracker\n", version, self_path);
|
|
|
|
return 1;
|
|
|
|
}
|
2017-01-30 18:59:29 +00:00
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
const struct __user_cap_header_struct header = {
|
|
|
|
.version = _LINUX_CAPABILITY_VERSION_3,
|
|
|
|
.pid = getpid(),
|
|
|
|
};
|
|
|
|
struct __user_cap_data_struct user_data[2] = {};
|
|
|
|
|
|
|
|
for (size_t i = 0; i < size; i++) {
|
|
|
|
// merge inheritable & permitted into one
|
|
|
|
user_data[i].permitted = user_data[i].inheritable =
|
|
|
|
LE32_TO_H(data.data[i].inheritable) | LE32_TO_H(data.data[i].permitted);
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
if (syscall(SYS_capset, &header, &user_data) < 0) {
|
|
|
|
fprintf(stderr, "failed to inherit capabilities: %s", strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
unsigned last_cap;
|
|
|
|
r = get_last_cap(&last_cap);
|
|
|
|
if (r < 0) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
uint64_t set = user_data[0].permitted | (uint64_t)user_data[1].permitted << 32;
|
|
|
|
for (unsigned cap = 0; cap < last_cap; cap++) {
|
|
|
|
if (!(set & (1ULL << cap))) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for the cap_setpcap capability, we set this on the
|
|
|
|
// wrapper so it can elevate the capabilities to the Ambient
|
|
|
|
// set but we do not want to propagate it down into the
|
|
|
|
// wrapped program.
|
|
|
|
//
|
|
|
|
// TODO: what happens if that's the behavior you want
|
|
|
|
// though???? I'm preferring a strict vs. loose policy here.
|
|
|
|
if (cap == CAP_SETPCAP) {
|
|
|
|
if(getenv(wrapper_debug)) {
|
|
|
|
fprintf(stderr, "cap_setpcap in set, skipping it\n");
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0)) {
|
|
|
|
fprintf(stderr, "cannot raise the capability %d into the ambient set: %s\n", cap, strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (getenv(wrapper_debug)) {
|
|
|
|
fprintf(stderr, "raised %d into the ambient capability set\n", cap);
|
|
|
|
}
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
int readlink_malloc(const char *p, char **ret) {
|
|
|
|
size_t l = FILENAME_MAX+1;
|
|
|
|
int r;
|
|
|
|
|
|
|
|
for (;;) {
|
|
|
|
char *c = calloc(l, sizeof(char));
|
|
|
|
if (!c) {
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
|
|
|
|
ssize_t n = readlink(p, c, l-1);
|
|
|
|
if (n < 0) {
|
|
|
|
r = -errno;
|
|
|
|
free(c);
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((size_t) n < l-1) {
|
|
|
|
c[n] = 0;
|
|
|
|
*ret = c;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
free(c);
|
|
|
|
l *= 2;
|
|
|
|
}
|
|
|
|
}
|
2017-01-29 07:07:12 +00:00
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
int main(int argc, char **argv) {
|
2022-01-27 07:14:53 +00:00
|
|
|
ASSERT(argc >= 1);
|
2021-01-14 07:24:27 +00:00
|
|
|
char *self_path = NULL;
|
|
|
|
int self_path_size = readlink_malloc("/proc/self/exe", &self_path);
|
|
|
|
if (self_path_size < 0) {
|
|
|
|
fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
|
|
|
|
}
|
2016-06-30 23:59:32 +00:00
|
|
|
|
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
|
|
|
unsigned int ruid, euid, suid, rgid, egid, sgid;
|
|
|
|
MUSTSUCCEED(getresuid(&ruid, &euid, &suid));
|
|
|
|
MUSTSUCCEED(getresgid(&rgid, &egid, &sgid));
|
|
|
|
|
|
|
|
// If true, then we did not benefit from setuid privilege escalation,
|
|
|
|
// where the original uid is still in ruid and different from euid == suid.
|
|
|
|
int didnt_suid = (ruid == euid) && (euid == suid);
|
|
|
|
// If true, then we did not benefit from setgid privilege escalation
|
|
|
|
int didnt_sgid = (rgid == egid) && (egid == sgid);
|
|
|
|
|
|
|
|
|
2022-11-04 23:09:32 +00:00
|
|
|
// TODO: Determine if this is still useful, in particular if
|
|
|
|
// make_caps_ambient somehow relies on these properties.
|
2016-06-30 23:59:32 +00:00
|
|
|
// Make sure that we are being executed from the right location,
|
2022-11-04 23:09:32 +00:00
|
|
|
// i.e., `safe_wrapper_dir'.
|
2021-01-14 07:24:27 +00:00
|
|
|
int len = strlen(wrapper_dir);
|
|
|
|
if (len > 0 && '/' == wrapper_dir[len - 1])
|
2016-06-30 23:59:32 +00:00
|
|
|
--len;
|
2022-01-27 07:12:00 +00:00
|
|
|
ASSERT(!strncmp(self_path, wrapper_dir, len));
|
|
|
|
ASSERT('/' == wrapper_dir[0]);
|
|
|
|
ASSERT('/' == self_path[len]);
|
2016-06-30 23:59:32 +00:00
|
|
|
|
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
|
|
|
// If we got privileges with the fs set[ug]id bit, check that the privilege we
|
|
|
|
// got matches the one one we expected, ie that our effective uid/gid
|
|
|
|
// matches the uid/gid of `self_path`. This ensures that we were executed as
|
|
|
|
// `self_path', and not, say, as some other setuid program.
|
|
|
|
// We don't check that if we did not benefit from the set[ug]id bit, as
|
|
|
|
// can be the case in nosuid mounts or user namespaces.
|
2016-06-30 23:59:32 +00:00
|
|
|
struct stat st;
|
2022-01-27 07:12:00 +00:00
|
|
|
ASSERT(lstat(self_path, &st) != -1);
|
2016-06-30 23:59:32 +00:00
|
|
|
|
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
|
|
|
// if the wrapper gained privilege with suid, check that we got the uid of the file owner
|
|
|
|
ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid));
|
|
|
|
// if the wrapper gained privilege with sgid, check that we got the gid of the file group
|
|
|
|
ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid));
|
|
|
|
// same, but with suid instead of euid
|
|
|
|
ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid));
|
|
|
|
ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid));
|
2016-06-30 23:59:32 +00:00
|
|
|
|
|
|
|
// And, of course, we shouldn't be writable.
|
2022-01-27 07:12:00 +00:00
|
|
|
ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2017-02-14 00:03:06 +00:00
|
|
|
// Read the capabilities set on the wrapper and raise them in to
|
2021-01-14 07:24:27 +00:00
|
|
|
// the ambient set so the program we're wrapping receives the
|
2016-06-30 23:59:32 +00:00
|
|
|
// capabilities too!
|
2022-11-14 13:45:36 +00:00
|
|
|
if (make_caps_ambient("/proc/self/exe") != 0) {
|
2021-01-14 07:24:27 +00:00
|
|
|
free(self_path);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
free(self_path);
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2022-11-04 23:09:32 +00:00
|
|
|
execve(SOURCE_PROG, argv, environ);
|
2016-06-30 23:59:32 +00:00
|
|
|
|
|
|
|
fprintf(stderr, "%s: cannot run `%s': %s\n",
|
2022-11-04 23:09:32 +00:00
|
|
|
argv[0], SOURCE_PROG, strerror(errno));
|
2016-06-30 23:59:32 +00:00
|
|
|
|
2021-01-14 07:24:27 +00:00
|
|
|
return 1;
|
2016-06-30 23:59:32 +00:00
|
|
|
}
|