core: allow using seccomp without no_new_privs when unprivileged

Until now, using any form of seccomp while being unprivileged (User=)
resulted in systemd enabling no_new_privs.

There's no need for doing this because:

* We trust the filters we apply
* If User= is set and a process wants to apply a new seccomp filter, it
will need to set no_new_privs itself

An example of application that might want seccomp + !no_new_privs is a
program that wants to run as an unprivileged user but uses file
capabilities to start a web server on a privileged port while
benefitting from a restrictive seccomp profile.

We now keep the privileges needed to do seccomp before calling
enforce_user() and drop them after the seccomp filters are applied.

If the syscall filter doesn't allow the needed syscalls to drop the
privileges, we keep the previous behavior by enabling no_new_privs.
This commit is contained in:
Iago López Galeiras
2023-11-07 11:31:53 +01:00
parent b3e199cec8
commit 24832d10b6
4 changed files with 181 additions and 101 deletions
+135 -17
View File
@@ -1378,15 +1378,7 @@ static bool context_has_syscall_logs(const ExecContext *c) {
!hashmap_isempty(c->syscall_log);
}
static bool context_has_no_new_privileges(const ExecContext *c) {
assert(c);
if (c->no_new_privileges)
return true;
if (have_effective_cap(CAP_SYS_ADMIN) > 0) /* if we are privileged, we don't need NNP */
return false;
static bool context_has_seccomp(const ExecContext *c) {
/* We need NNP if we have any form of seccomp and are unprivileged */
return c->lock_personality ||
c->memory_deny_write_execute ||
@@ -1405,8 +1397,49 @@ static bool context_has_no_new_privileges(const ExecContext *c) {
context_has_syscall_logs(c);
}
static bool context_has_no_new_privileges(const ExecContext *c) {
assert(c);
if (c->no_new_privileges)
return true;
if (have_effective_cap(CAP_SYS_ADMIN) > 0) /* if we are privileged, we don't need NNP */
return false;
return context_has_seccomp(c);
}
#if HAVE_SECCOMP
static bool seccomp_allows_drop_privileges(const ExecContext *c) {
void *id, *val;
bool has_capget = false, has_capset = false, has_prctl = false;
assert(c);
/* No syscall filter, we are allowed to drop privileges */
if (hashmap_isempty(c->syscall_filter))
return true;
HASHMAP_FOREACH_KEY(val, id, c->syscall_filter) {
_cleanup_free_ char *name = NULL;
name = seccomp_syscall_resolve_num_arch(SCMP_ARCH_NATIVE, PTR_TO_INT(id) - 1);
if (streq(name, "capget"))
has_capget = true;
else if (streq(name, "capset"))
has_capset = true;
else if (streq(name, "prctl"))
has_prctl = true;
}
if (c->syscall_allow_list)
return has_capget && has_capset && has_prctl;
else
return !(has_capget || has_capset || has_prctl);
}
static bool skip_seccomp_unavailable(const ExecContext *c, const ExecParameters *p, const char* msg) {
if (is_seccomp_available())
@@ -3911,6 +3944,7 @@ int exec_invoke(
needs_setuid, /* Do we need to do the actual setresuid()/setresgid() calls? */
needs_mount_namespace, /* Do we need to set up a mount namespace for this kernel? */
needs_ambient_hack; /* Do we need to apply the ambient capabilities hack? */
bool keep_seccomp_privileges = false;
#if HAVE_SELINUX
_cleanup_free_ char *mac_selinux_context_net = NULL;
bool use_selinux = false;
@@ -3920,6 +3954,9 @@ int exec_invoke(
#endif
#if HAVE_APPARMOR
bool use_apparmor = false;
#endif
#if HAVE_SECCOMP
uint64_t saved_bset = 0;
#endif
uid_t saved_uid = getuid();
gid_t saved_gid = getgid();
@@ -4817,6 +4854,28 @@ int exec_invoke(
(UINT64_C(1) << CAP_SETUID) |
(UINT64_C(1) << CAP_SETGID);
#if HAVE_SECCOMP
/* If the service has any form of a seccomp filter and it allows dropping privileges, we'll
* keep the needed privileges to apply it even if we're not root. */
if (needs_setuid &&
uid_is_valid(uid) &&
context_has_seccomp(context) &&
seccomp_allows_drop_privileges(context)) {
keep_seccomp_privileges = true;
if (prctl(PR_SET_KEEPCAPS, 1) < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, errno, "Failed to enable keep capabilities flag: %m");
}
/* Save the current bounding set so we can restore it after applying the seccomp
* filter */
saved_bset = bset;
bset |= (UINT64_C(1) << CAP_SYS_ADMIN) |
(UINT64_C(1) << CAP_SETPCAP);
}
#endif
if (!cap_test_all(bset)) {
r = capability_bounding_set_drop(bset, /* right_now= */ false);
if (r < 0) {
@@ -4858,6 +4917,26 @@ int exec_invoke(
return log_exec_error_errno(context, params, r, "Failed to change UID to " UID_FMT ": %m", uid);
}
if (keep_seccomp_privileges) {
r = drop_capability(CAP_SETUID);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to drop CAP_SETUID: %m");
}
r = keep_capability(CAP_SYS_ADMIN);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to keep CAP_SYS_ADMIN: %m");
}
r = keep_capability(CAP_SETPCAP);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to keep CAP_SETPCAP: %m");
}
}
if (!needs_ambient_hack && capability_ambient_set != 0) {
/* Raise the ambient capabilities after user change. */
@@ -5027,14 +5106,6 @@ int exec_invoke(
*exit_status = EXIT_SECCOMP;
return log_exec_error_errno(context, params, r, "Failed to apply system call log filters: %m");
}
/* This really should remain the last step before the execve(), to make sure our own code is unaffected
* by the filter as little as possible. */
r = apply_syscall_filter(context, params, needs_ambient_hack);
if (r < 0) {
*exit_status = EXIT_SECCOMP;
return log_exec_error_errno(context, params, r, "Failed to apply system call filters: %m");
}
#endif
#if HAVE_LIBBPF
@@ -5045,6 +5116,53 @@ int exec_invoke(
}
#endif
#if HAVE_SECCOMP
/* This really should remain as close to the execve() as possible, to make sure our own code is unaffected
* by the filter as little as possible. */
r = apply_syscall_filter(context, params, needs_ambient_hack);
if (r < 0) {
*exit_status = EXIT_SECCOMP;
return log_exec_error_errno(context, params, r, "Failed to apply system call filters: %m");
}
if (keep_seccomp_privileges) {
/* Restore the capability bounding set with what's expected from the service + the
* ambient capabilities hack */
if (!cap_test_all(saved_bset)) {
r = capability_bounding_set_drop(saved_bset, /* right_now= */ false);
if (r < 0) {
*exit_status = EXIT_CAPABILITIES;
return log_exec_error_errno(context, params, r, "Failed to drop bset capabilities: %m");
}
}
/* Only drop CAP_SYS_ADMIN if it's not in the bounding set, otherwise we'll break
* applications that use it. */
if (!FLAGS_SET(saved_bset, (UINT64_C(1) << CAP_SYS_ADMIN))) {
r = drop_capability(CAP_SYS_ADMIN);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to drop CAP_SYS_ADMIN: %m");
}
}
/* Only drop CAP_SETPCAP if it's not in the bounding set, otherwise we'll break
* applications that use it. */
if (!FLAGS_SET(saved_bset, (UINT64_C(1) << CAP_SETPCAP))) {
r = drop_capability(CAP_SETPCAP);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to drop CAP_SETPCAP: %m");
}
}
if (prctl(PR_SET_KEEPCAPS, 0) < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, errno, "Failed to drop keep capabilities flag: %m");
}
}
#endif
}
if (!strv_isempty(context->unset_environment)) {