#include "sd-event.h"
+#include "build-path.h"
#include "capability-util.h"
#include "cpu-set-util.h"
#include "copy.h"
#include "path-util.h"
#include "process-util.h"
#include "rm-rf.h"
-#if HAVE_SECCOMP
#include "seccomp-util.h"
-#endif
#include "service.h"
#include "signal-util.h"
#include "static-destruct.h"
#include "stat-util.h"
+#include "sysctl-util.h"
#include "tests.h"
#include "tmpfile-util.h"
#include "unit.h"
static char *user_runtime_unit_dir = NULL;
static bool can_unshare;
+static bool have_net_dummy;
+static bool have_netns;
static unsigned n_ran_tests = 0;
STATIC_DESTRUCTOR_REGISTER(user_runtime_unit_dir, freep);
assert_se(m);
assert_se(unit);
+ /* Bump the timeout when running in plain QEMU, as some more involved tests might start hitting the
+ * default 2m timeout (like exec-dynamicuser-statedir.service) */
+ if (detect_virtualization() == VIRTUALIZATION_QEMU)
+ timeout *= 2;
+
service = SERVICE(unit);
printf("%s\n", unit->id);
exec_context_dump(&service->exec_context, stdout, "\t");
}
}
+static bool apparmor_restrict_unprivileged_userns(void) {
+ _cleanup_free_ char *v = NULL;
+ int r;
+
+ /* If kernel.apparmor_restrict_unprivileged_userns=1, then we cannot
+ * use unprivileged user namespaces. */
+ r = sysctl_read("kernel/apparmor_restrict_unprivileged_userns", &v);
+ if (r < 0) {
+ if (r != -ENOENT)
+ log_debug_errno(r, "Failed to read kernel.apparmor_restrict_unprivileged_userns sysctl, ignoring: %m");
+
+ return false;
+ }
+
+ return streq(v, "1");
+}
+
+static bool have_userns_privileges(void) {
+ pid_t pid;
+ int r;
+
+ if (apparmor_restrict_unprivileged_userns())
+ return false;
+
+ r = safe_fork("(sd-test-check-userns)",
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGKILL,
+ &pid);
+ assert(r >= 0);
+ if (r == 0) {
+ /* Keep CAP_SYS_ADMIN if we have it to ensure we give an
+ * accurate result to the caller. Some kernels have a
+ * kernel.unprivileged_userns_clone sysctl which can be
+ * configured to make CLONE_NEWUSER require CAP_SYS_ADMIN.
+ * Additionally, AppArmor may restrict unprivileged user
+ * namespace creation. */
+ r = capability_bounding_set_drop(UINT64_C(1) << CAP_SYS_ADMIN, /* right_now = */ true);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to drop capabilities: %m");
+ _exit(2);
+ }
+
+ r = RET_NERRNO(unshare(CLONE_NEWUSER));
+ if (r < 0 && !ERRNO_IS_NEG_PRIVILEGE(r))
+ log_debug_errno(r, "Failed to create user namespace: %m");
+
+ _exit(r >= 0 ? EXIT_SUCCESS : ERRNO_IS_NEG_PRIVILEGE(r) ? EXIT_FAILURE : 2);
+ }
+
+ /* The exit code records the result of the check:
+ * EXIT_SUCCESS => we can use user namespaces
+ * EXIT_FAILURE => we can NOT use user namespaces
+ * 2 => some other error occurred */
+ r = wait_for_terminate_and_check("(sd-test-check-userns)", pid, 0);
+ if (!IN_SET(r, EXIT_SUCCESS, EXIT_FAILURE))
+ log_debug("Failed to check if user namespaces can be used, assuming not.");
+
+ return r == EXIT_SUCCESS;
+}
+
static void _test(const char *file, unsigned line, const char *func,
Manager *m, const char *unit_name, int status_expected, int code_expected) {
Unit *unit;
#elif defined(__i386__)
test(m, "exec-personality-x86.service", 0, CLD_EXITED);
-#elif defined(__loongarch64)
+#elif defined(__loongarch_lp64)
test(m, "exec-personality-loongarch64.service", 0, CLD_EXITED);
#else
log_notice("Unknown personality, skipping %s", __func__);
static void test_exec_privatetmp(Manager *m) {
assert_se(touch("/tmp/test-exec_privatetmp") >= 0);
- test(m, "exec-privatetmp-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatetmp-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatetmp-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
test(m, "exec-privatetmp-no.service", 0, CLD_EXITED);
- test(m, "exec-privatetmp-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
(void) unlink("/tmp/test-exec_privatetmp");
}
return;
}
- test(m, "exec-privatedevices-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatedevices-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (access("/dev/kmsg", F_OK) >= 0)
+ test(m, "exec-privatedevices-bind.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-yes-with-group.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
test(m, "exec-privatedevices-no.service", 0, CLD_EXITED);
- test(m, "exec-privatedevices-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
- test(m, "exec-privatedevices-yes-with-group.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
/* We use capsh to test if the capabilities are
* properly set, so be sure that it exists */
return;
}
- test(m, "exec-privatedevices-yes-capability-mknod.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatedevices-yes-capability-mknod.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-yes-capability-sys-rawio.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
test(m, "exec-privatedevices-no-capability-mknod.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
- test(m, "exec-privatedevices-yes-capability-sys-rawio.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
test(m, "exec-privatedevices-no-capability-sys-rawio.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
}
}
test(m, "exec-protectkernelmodules-no-capabilities.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
- test(m, "exec-protectkernelmodules-yes-capabilities.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
- test(m, "exec-protectkernelmodules-yes-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-protectkernelmodules-yes-capabilities.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-protectkernelmodules-yes-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
}
static void test_exec_readonlypaths(Manager *m) {
- test(m, "exec-readonlypaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-readonlypaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
if (path_is_read_only_fs("/var") > 0) {
log_notice("Directory /var is readonly, skipping remaining tests in %s", __func__);
return;
}
- test(m, "exec-inaccessiblepaths-sys.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-inaccessiblepaths-sys.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
if (path_is_read_only_fs("/") > 0) {
log_notice("Root directory is readonly, skipping remaining tests in %s", __func__);
_cleanup_(sd_event_source_unrefp) sd_event_source *sigchld_source = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *stdout_source = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *stderr_source = NULL;
- _cleanup_close_pair_ int outpipe[2] = PIPE_EBADF, errpipe[2] = PIPE_EBADF;
+ _cleanup_close_pair_ int outpipe[2] = EBADF_PAIR, errpipe[2] = EBADF_PAIR;
_cleanup_strv_free_ char **libraries = NULL;
_cleanup_free_ char *result = NULL;
pid_t pid;
assert_se(exec);
assert_se(ret);
- assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
assert_se(pipe2(outpipe, O_NONBLOCK|O_CLOEXEC) == 0);
assert_se(pipe2(errpipe, O_NONBLOCK|O_CLOEXEC) == 0);
r = safe_fork_full("(spawn-ldd)",
(int[]) { -EBADF, outpipe[1], errpipe[1] },
NULL, 0,
- FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_REARRANGE_STDIO|FORK_LOG, &pid);
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG, &pid);
assert_se(r >= 0);
if (r == 0) {
execlp("ldd", "ldd", exec, NULL);
return;
}
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
assert_se(find_libraries(fullpath_touch, &libraries) >= 0);
assert_se(find_libraries(fullpath_test, &libraries_test) >= 0);
assert_se(strv_extend_strv(&libraries, libraries_test, true) >= 0);
static void test_exec_noexecpaths(Manager *m) {
- test(m, "exec-noexecpaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-noexecpaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
}
static void test_exec_temporaryfilesystem(Manager *m) {
test(m, "exec-systemcallfilter-with-errno-in-allow-list.service", errno_from_name("EILSEQ"), CLD_EXITED);
test(m, "exec-systemcallfilter-override-error-action.service", SIGSYS, CLD_KILLED);
test(m, "exec-systemcallfilter-override-error-action2.service", errno_from_name("EILSEQ"), CLD_EXITED);
+
+ test(m, "exec-systemcallfilter-nonewprivileges.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+ test(m, "exec-systemcallfilter-nonewprivileges-protectclock.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ r = find_executable("capsh", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find capsh binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-nonewprivileges-bounding1.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+ test(m, "exec-systemcallfilter-nonewprivileges-bounding2.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
#endif
}
}
static void test_exec_umask(Manager *m) {
- test(m, "exec-umask-default.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
- test(m, "exec-umask-0177.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-umask-default.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-umask-0177.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ } else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
}
static void test_exec_runtimedirectory(Manager *m) {
}
static void test_exec_basic(Manager *m) {
- test(m, "exec-basic.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-basic.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
}
static void test_exec_ambientcapabilities(Manager *m) {
test(m, "exec-ambientcapabilities.service", 0, CLD_EXITED);
test(m, "exec-ambientcapabilities-merge.service", 0, CLD_EXITED);
+ if (have_effective_cap(CAP_SETUID) > 0)
+ test(m, "exec-ambientcapabilities-dynuser.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+
if (!check_nobody_user_and_group()) {
log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
return;
static void test_exec_privatenetwork(Manager *m) {
int r;
+ if (!have_net_dummy)
+ return (void)log_notice("Skipping %s, dummy network interface not available", __func__);
+
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
r = find_executable("ip", NULL);
if (r < 0) {
log_notice_errno(r, "Skipping %s, could not find ip binary: %m", __func__);
static void test_exec_networknamespacepath(Manager *m) {
int r;
+ if (!have_net_dummy)
+ return (void)log_notice("Skipping %s, dummy network interface not available", __func__);
+
+ if (!have_netns)
+ return (void)log_notice("Skipping %s, network namespace not available", __func__);
+
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
r = find_executable("ip", NULL);
if (r < 0) {
log_notice_errno(r, "Skipping %s, could not find ip binary: %m", __func__);
static void test_exec_standardinput(Manager *m) {
test(m, "exec-standardinput-data.service", 0, CLD_EXITED);
test(m, "exec-standardinput-file.service", 0, CLD_EXITED);
+
+ ExecOutput saved = m->defaults.std_output;
+ m->defaults.std_output = EXEC_OUTPUT_NULL;
test(m, "exec-standardinput-file-cat.service", 0, CLD_EXITED);
+ m->defaults.std_output = saved;
}
static void test_exec_standardoutput(Manager *m) {
return (void) log_tests_skipped_errno(r, "manager_new");
assert_se(r >= 0);
- m->default_std_output = EXEC_OUTPUT_NULL; /* don't rely on host journald */
+ m->defaults.std_output = EXEC_OUTPUT_INHERIT; /* don't rely on host journald */
assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
/* Uncomment below if you want to make debugging logs stored to journal. */
r = safe_fork(process_name,
FORK_RESET_SIGNALS |
FORK_CLOSE_ALL_FDS |
- FORK_DEATHSIG |
+ FORK_DEATHSIG_SIGTERM |
FORK_WAIT |
FORK_REOPEN_LOG |
FORK_LOG |
NULL);
assert_se(r >= 0);
if (r == 0) {
- _cleanup_free_ char *unit_dir = NULL;
+ _cleanup_free_ char *unit_dir = NULL, *build_dir = NULL, *build_dir_mount = NULL;
+ int ret;
/* Make "/" read-only. */
assert_se(mount_nofollow_verbose(LOG_DEBUG, NULL, "/", NULL, MS_BIND|MS_REMOUNT|MS_RDONLY, NULL) >= 0);
assert_se(mkdir_p(PRIVATE_UNIT_DIR, 0755) >= 0);
assert_se(mount_nofollow_verbose(LOG_DEBUG, "tmpfs", PRIVATE_UNIT_DIR, "tmpfs", MS_NOSUID|MS_NODEV, NULL) >= 0);
+ /* Mark our test "playground" as MS_SLAVE, so we can MS_MOVE mounts underneath it. */
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, NULL, PRIVATE_UNIT_DIR, NULL, MS_SLAVE, NULL) >= 0);
/* Copy unit files to make them accessible even when unprivileged. */
assert_se(get_testdata_dir("test-execute/", &unit_dir) >= 0);
assert_se(copy_directory_at(AT_FDCWD, unit_dir, AT_FDCWD, PRIVATE_UNIT_DIR, COPY_MERGE_EMPTY) >= 0);
/* Mount tmpfs on the following directories to make not StateDirectory= or friends disturb the host. */
+ ret = get_build_exec_dir(&build_dir);
+ assert_se(ret >= 0 || ret == -ENOEXEC);
+
+ if (build_dir) {
+ /* Account for a build directory being in one of the soon-to-be-tmpfs directories. If we
+ * overmount it with an empty tmpfs, manager_new() will pin the wrong systemd-executor binary,
+ * which can then lead to unexpected (and painful to debug) test fails. */
+ assert_se(access(build_dir, F_OK) >= 0);
+ assert_se(build_dir_mount = path_join(PRIVATE_UNIT_DIR, "build_dir"));
+ assert_se(mkdir_p(build_dir_mount, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, build_dir, build_dir_mount, NULL, MS_BIND, NULL) >= 0);
+ }
+
FOREACH_STRING(p, "/dev/shm", "/root", "/tmp", "/var/tmp", "/var/lib")
assert_se(mount_nofollow_verbose(LOG_DEBUG, "tmpfs", p, "tmpfs", MS_NOSUID|MS_NODEV, NULL) >= 0);
+ if (build_dir_mount) {
+ ret = RET_NERRNO(access(build_dir, F_OK));
+ assert_se(ret >= 0 || ret == -ENOENT);
+
+ if (ret == -ENOENT) {
+ /* The build directory got overmounted by tmpfs, so let's use the "backup" bind mount to
+ * bring it back. */
+ assert_se(mkdir_p(build_dir, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, build_dir_mount, build_dir, NULL, MS_MOVE, NULL) >= 0);
+ }
+ }
+
/* Prepare credstore like tmpfiles.d/credstore.conf for LoadCredential= tests. */
FOREACH_STRING(p, "/run/credstore", "/run/credstore.encrypted") {
assert_se(mkdir_p(p, 0) >= 0);
return log_tests_skipped("/sys is mounted read-only");
/* Create dummy network interface for testing PrivateNetwork=yes */
- (void) system("ip link add dummy-test-exec type dummy");
+ have_net_dummy = system("ip link add dummy-test-exec type dummy") == 0;
- /* Create a network namespace and a dummy interface in it for NetworkNamespacePath= */
- (void) system("ip netns add test-execute-netns");
- (void) system("ip netns exec test-execute-netns ip link add dummy-test-ns type dummy");
+ if (have_net_dummy) {
+ /* Create a network namespace and a dummy interface in it for NetworkNamespacePath= */
+ have_netns = system("ip netns add test-execute-netns") == 0;
+ have_netns = have_netns && system("ip netns exec test-execute-netns ip link add dummy-test-ns type dummy") == 0;
+ }
return EXIT_SUCCESS;
}
static int outro(void) {
- (void) system("ip link del dummy-test-exec");
- (void) system("ip netns del test-execute-netns");
+ if (have_net_dummy) {
+ (void) system("ip link del dummy-test-exec");
+ (void) system("ip netns del test-execute-netns");
+ }
+
(void) rmdir(PRIVATE_UNIT_DIR);
return EXIT_SUCCESS;