Merge pull request #27684 from mrc0mmand/more-nspawn-tests

test: further extend systemd-nspawn coverage
This commit is contained in:
Yu Watanabe 2023-05-19 03:00:54 +09:00 committed by GitHub
commit ec0bd9611a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 462 additions and 117 deletions

View file

@ -638,7 +638,7 @@ int mount_all(const char *dest,
r = chase(mount_table[k].where, dest, CHASE_NONEXISTENT|CHASE_PREFIX_ROOT, &where, NULL);
if (r < 0)
return log_error_errno(r, "Failed to resolve %s/%s: %m", dest, mount_table[k].where);
return log_error_errno(r, "Failed to resolve %s%s: %m", strempty(dest), mount_table[k].where);
/* Skip this entry if it is not a remount. */
if (mount_table[k].what) {
@ -697,7 +697,7 @@ int mount_all(const char *dest,
* for those. */
r = chase(mount_table[k].what, dest, CHASE_PREFIX_ROOT, &prefixed, NULL);
if (r < 0)
return log_error_errno(r, "Failed to resolve %s/%s: %m", dest, mount_table[k].what);
return log_error_errno(r, "Failed to resolve %s%s: %m", strempty(dest), mount_table[k].what);
}
r = mount_verbose_full(

View file

@ -752,38 +752,48 @@ int remove_veth_links(const char *primary, char **pairs) {
}
static int network_iface_pair_parse(const char* iftype, char ***l, const char *p, const char* ifprefix) {
_cleanup_free_ char *a = NULL, *b = NULL;
int r;
r = extract_first_word(&p, &a, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return log_error_errno(r, "Failed to extract first word in %s parameter: %m", iftype);
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Short read while reading %s parameter: %m", iftype);
if (!ifname_valid(a))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s, interface name not valid: %s", iftype, a);
for (;;) {
_cleanup_free_ char *word = NULL, *a = NULL, *b = NULL;
const char *interface;
if (isempty(p)) {
if (ifprefix)
b = strjoin(ifprefix, a);
else
b = strdup(a);
} else
b = strdup(p);
if (!b)
return log_oom();
r = extract_first_word(&p, &word, NULL, 0);
if (r < 0)
return log_error_errno(r, "Failed to parse interface name: %m");
if (r == 0)
break;
if (!ifname_valid(b))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s, interface name not valid: %s", iftype, b);
interface = word;
r = extract_first_word(&interface, &a, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return log_error_errno(r, "Failed to extract first word in %s parameter: %m", iftype);
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Short read while reading %s parameter: %m", iftype);
if (!ifname_valid(a))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s, interface name not valid: %s", iftype, a);
r = strv_push_pair(l, a, b);
if (r < 0)
return log_oom();
if (isempty(interface)) {
if (ifprefix)
b = strjoin(ifprefix, a);
else
b = strdup(a);
} else
b = strdup(interface);
if (!b)
return log_oom();
if (!ifname_valid(b))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s, interface name not valid: %s", iftype, b);
r = strv_consume_pair(l, TAKE_PTR(a), TAKE_PTR(b));
if (r < 0)
return log_oom();
}
a = b = NULL;
return 0;
}

View file

@ -90,7 +90,7 @@ static int oci_unsupported(const char *name, JsonVariant *v, JsonDispatchFlags f
}
static int oci_terminal(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = userdata;
Settings *s = ASSERT_PTR(userdata);
/* If not specified, or set to true, we'll default to either an interactive or a read-only
* console. If specified as false, we'll forcibly move to "pipe" mode though. */
@ -115,6 +115,7 @@ static int oci_console_dimension(const char *name, JsonVariant *variant, JsonDis
}
static int oci_console_size(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = ASSERT_PTR(userdata);
static const JsonDispatch table[] = {
{ "height", JSON_VARIANT_UNSIGNED, oci_console_dimension, offsetof(Settings, console_height), JSON_MANDATORY },
@ -122,7 +123,7 @@ static int oci_console_size(const char *name, JsonVariant *v, JsonDispatchFlags
{}
};
return json_dispatch(v, table, oci_unexpected, flags, userdata);
return json_dispatch(v, table, oci_unexpected, flags, s);
}
static int oci_absolute_path(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
@ -186,9 +187,8 @@ static int oci_args(const char *name, JsonVariant *v, JsonDispatchFlags flags, v
static int oci_rlimit_type(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
const char *z;
int t, *type = userdata;
assert_se(type);
int *type = ASSERT_PTR(userdata);
int t;
z = startswith(json_variant_string(v), "RLIMIT_");
if (!z)
@ -206,7 +206,8 @@ static int oci_rlimit_type(const char *name, JsonVariant *v, JsonDispatchFlags f
}
static int oci_rlimit_value(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
rlim_t z, *value = ASSERT_PTR(userdata);
rlim_t *value = ASSERT_PTR(userdata);
rlim_t z;
if (json_variant_is_negative(v))
z = RLIM_INFINITY;
@ -227,7 +228,6 @@ static int oci_rlimit_value(const char *name, JsonVariant *v, JsonDispatchFlags
}
static int oci_rlimits(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = ASSERT_PTR(userdata);
JsonVariant *e;
int r;
@ -276,7 +276,8 @@ static int oci_rlimits(const char *name, JsonVariant *v, JsonDispatchFlags flags
}
static int oci_capability_array(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
uint64_t *mask = userdata, m = 0;
uint64_t *mask = ASSERT_PTR(userdata);
uint64_t m = 0;
JsonVariant *e;
JSON_VARIANT_ARRAY_FOREACH(e, v) {
@ -347,10 +348,10 @@ static int oci_oom_score_adj(const char *name, JsonVariant *v, JsonDispatchFlags
}
static int oci_uid_gid(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
uid_t *uid = userdata, u;
uid_t *uid = ASSERT_PTR(userdata);
uid_t u;
uint64_t k;
assert(uid);
assert_cc(sizeof(uid_t) == sizeof(gid_t));
k = json_variant_unsigned(v);
@ -395,6 +396,7 @@ static int oci_supplementary_gids(const char *name, JsonVariant *v, JsonDispatch
}
static int oci_user(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch table[] = {
{ "uid", JSON_VARIANT_UNSIGNED, oci_uid_gid, offsetof(Settings, uid), JSON_MANDATORY },
{ "gid", JSON_VARIANT_UNSIGNED, oci_uid_gid, offsetof(Settings, gid), JSON_MANDATORY },
@ -427,7 +429,7 @@ static int oci_process(const char *name, JsonVariant *v, JsonDispatchFlags flags
}
static int oci_root(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = userdata;
Settings *s = ASSERT_PTR(userdata);
int r;
static const JsonDispatch table[] = {
@ -511,13 +513,15 @@ typedef struct oci_mount_data {
char **options;
} oci_mount_data;
static void cleanup_oci_mount_data(oci_mount_data *data) {
static void oci_mount_data_done(oci_mount_data *data) {
free(data->destination);
free(data->source);
strv_free(data->options);
free(data->type);
strv_free(data->options);
}
DEFINE_TRIVIAL_DESTRUCTOR(oci_mount_data_donep, oci_mount_data, oci_mount_data_done);
static int oci_mounts(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = ASSERT_PTR(userdata);
JsonVariant *e;
@ -533,8 +537,8 @@ static int oci_mounts(const char *name, JsonVariant *v, JsonDispatchFlags flags,
};
_cleanup_free_ char *joined_options = NULL;
_cleanup_(oci_mount_data_donep) oci_mount_data data = {};
CustomMount *m;
_cleanup_(cleanup_oci_mount_data) oci_mount_data data = {};
r = json_dispatch(e, table, oci_unexpected, flags, &data);
if (r < 0)
@ -610,20 +614,27 @@ static int oci_namespace_type(const char *name, JsonVariant *v, JsonDispatchFlag
return 0;
}
struct namespace_data {
unsigned long type;
char *path;
};
static void namespace_data_done(struct namespace_data *p) {
assert(p);
free(p->path);
}
DEFINE_TRIVIAL_DESTRUCTOR(namespace_data_donep, struct namespace_data, namespace_data_done);
static int oci_namespaces(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
Settings *s = userdata;
Settings *s = ASSERT_PTR(userdata);
unsigned long n = 0;
JsonVariant *e;
int r;
assert_se(s);
JSON_VARIANT_ARRAY_FOREACH(e, v) {
struct namespace_data {
unsigned long type;
char *path;
} data = {};
_cleanup_(namespace_data_donep) struct namespace_data data = {};
static const JsonDispatch table[] = {
{ "type", JSON_VARIANT_STRING, oci_namespace_type, offsetof(struct namespace_data, type), JSON_MANDATORY },
@ -632,26 +643,19 @@ static int oci_namespaces(const char *name, JsonVariant *v, JsonDispatchFlags fl
};
r = json_dispatch(e, table, oci_unexpected, flags, &data);
if (r < 0) {
free(data.path);
if (r < 0)
return r;
}
if (data.path) {
if (data.type != CLONE_NEWNET) {
free(data.path);
if (data.type != CLONE_NEWNET)
return json_log(e, flags, SYNTHETIC_ERRNO(EOPNOTSUPP),
"Specifying namespace path for non-network namespace is not supported.");
}
if (s->network_namespace_path) {
free(data.path);
if (s->network_namespace_path)
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL),
"Network namespace path specified more than once, refusing.");
}
free(s->network_namespace_path);
s->network_namespace_path = data.path;
free_and_replace(s->network_namespace_path, data.path);
}
if (FLAGS_SET(n, data.type))
@ -675,10 +679,10 @@ static int oci_namespaces(const char *name, JsonVariant *v, JsonDispatchFlags fl
}
static int oci_uid_gid_range(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
uid_t *uid = userdata, u;
uid_t *uid = ASSERT_PTR(userdata);
uid_t u;
uint64_t k;
assert(uid);
assert_cc(sizeof(uid_t) == sizeof(gid_t));
/* This is very much like oci_uid_gid(), except the checks are a bit different, as this is a UID range rather
@ -777,11 +781,9 @@ static int oci_device_type(const char *name, JsonVariant *v, JsonDispatchFlags f
}
static int oci_device_major(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
unsigned *u = userdata;
unsigned *u = ASSERT_PTR(userdata);
uint64_t k;
assert_se(u);
k = json_variant_unsigned(v);
if (!DEVICE_MAJOR_VALID(k))
return json_log(v, flags, SYNTHETIC_ERRNO(ERANGE),
@ -792,11 +794,9 @@ static int oci_device_major(const char *name, JsonVariant *v, JsonDispatchFlags
}
static int oci_device_minor(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
unsigned *u = userdata;
unsigned *u = ASSERT_PTR(userdata);
uint64_t k;
assert_se(u);
k = json_variant_unsigned(v);
if (!DEVICE_MINOR_VALID(k))
return json_log(v, flags, SYNTHETIC_ERRNO(ERANGE),
@ -807,11 +807,10 @@ static int oci_device_minor(const char *name, JsonVariant *v, JsonDispatchFlags
}
static int oci_device_file_mode(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
mode_t *mode = userdata, m;
mode_t *mode = ASSERT_PTR(userdata);
mode_t m;
uint64_t k;
assert(mode);
k = json_variant_unsigned(v);
m = (mode_t) k;
@ -931,7 +930,7 @@ static int oci_cgroups_path(const char *name, JsonVariant *v, JsonDispatchFlags
}
static int oci_cgroup_device_type(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
mode_t *mode = userdata;
mode_t *mode = ASSERT_PTR(userdata);
const char *n;
assert_se(n = json_variant_string(v));
@ -958,7 +957,7 @@ struct device_data {
};
static int oci_cgroup_device_access(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
struct device_data *d = userdata;
struct device_data *d = ASSERT_PTR(userdata);
bool r = false, w = false, m = false;
const char *s;
size_t i;
@ -984,7 +983,6 @@ static int oci_cgroup_device_access(const char *name, JsonVariant *v, JsonDispat
}
static int oci_cgroup_devices(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
_cleanup_free_ struct device_data *list = NULL;
Settings *s = ASSERT_PTR(userdata);
size_t n_list = 0, i;
@ -1187,7 +1185,7 @@ static int oci_cgroup_memory(const char *name, JsonVariant *v, JsonDispatchFlags
{}
};
Settings *s = userdata;
Settings *s = ASSERT_PTR(userdata);
int r;
r = json_dispatch(v, table, oci_unexpected, flags, &data);
@ -1303,7 +1301,7 @@ static int oci_cgroup_cpu(const char *name, JsonVariant *v, JsonDispatchFlags fl
.period = UINT64_MAX,
};
Settings *s = userdata;
Settings *s = ASSERT_PTR(userdata);
int r;
r = json_dispatch(v, table, oci_unexpected, flags, &data);
@ -1741,13 +1739,15 @@ struct syscall_rule {
size_t n_arguments;
};
static void syscall_rule_free(struct syscall_rule *rule) {
static void syscall_rule_done(struct syscall_rule *rule) {
assert(rule);
strv_free(rule->names);
free(rule->arguments);
};
DEFINE_TRIVIAL_DESTRUCTOR(syscall_rule_donep, struct syscall_rule, syscall_rule_done);
static int oci_seccomp_action(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
uint32_t *action = ASSERT_PTR(userdata);
int r;
@ -1831,18 +1831,17 @@ static int oci_seccomp_syscalls(const char *name, JsonVariant *v, JsonDispatchFl
{ "args", JSON_VARIANT_ARRAY, oci_seccomp_args, 0, 0 },
{}
};
struct syscall_rule rule = {
_cleanup_(syscall_rule_donep) struct syscall_rule rule = {
.action = UINT32_MAX,
};
r = json_dispatch(e, table, oci_unexpected, flags, &rule);
if (r < 0)
goto fail_rule;
return r;
if (strv_isempty(rule.names)) {
json_log(e, flags, 0, "System call name list is empty.");
r = -EINVAL;
goto fail_rule;
return -EINVAL;
}
STRV_FOREACH(i, rule.names) {
@ -1856,15 +1855,8 @@ static int oci_seccomp_syscalls(const char *name, JsonVariant *v, JsonDispatchFl
r = seccomp_rule_add_array(sc, rule.action, nr, rule.n_arguments, rule.arguments);
if (r < 0)
goto fail_rule;
return r;
}
syscall_rule_free(&rule);
continue;
fail_rule:
syscall_rule_free(&rule);
return r;
}
return 0;
@ -2031,7 +2023,7 @@ static int oci_linux(const char *name, JsonVariant *v, JsonDispatchFlags flags,
}
static int oci_hook_timeout(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
usec_t *u = userdata;
usec_t *u = ASSERT_PTR(userdata);
uint64_t k;
k = json_variant_unsigned(v);

View file

@ -30,7 +30,8 @@ test_append_files() {
seq \
sleep \
stat \
touch
touch \
true
cp /etc/os-release "$container/usr/lib/os-release"
cat >"$container/sbin/init" <<EOF

View file

@ -214,6 +214,7 @@ BASICTOOLS=(
script
sed
seq
setfacl
setfattr
setfont
setpriv
@ -886,8 +887,8 @@ INNER_EOF
# Let's make one to prevent unexpected "<bin> not found" issues in the future
export PATH="/sbin:/bin:/usr/sbin:/usr/bin"
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mountpoint -q /proc || mount -t proc proc /proc
mountpoint -q /sys || mount -t sysfs sysfs /sys
mount -o remount,rw /
DEFAULT_ENVIRONMENT="\$DEFAULT_ENVIRONMENT ASAN_RT_PATH=$ASAN_RT_PATH"
@ -961,7 +962,7 @@ install_fs_tools() {
install_modules() {
dinfo "Install modules"
instmods dummy vfat veth
instmods bridge dummy ipvlan macvlan vfat veth
instmods loop =block
instmods nls_ascii =nls
instmods overlay =overlayfs
@ -2655,13 +2656,14 @@ inst_binary() {
# Same as above, but we need to wrap certain libraries unconditionally
#
# chown, getent, login, su, useradd, userdel - dlopen() (not only) systemd's PAM modules
# chown, getent, login, setfacl, su, useradd, userdel
# - dlopen() (not only) systemd's PAM modules
# ls, mkfs.*, mksquashfs, mkswap, setpriv, stat
# - pull in nss_systemd with certain options (like ls -l) when
# nsswitch.conf uses [SUCCESS=merge] (like on Arch Linux)
# - pull in nss_systemd with certain options (like ls -l) when
# nsswitch.conf uses [SUCCESS=merge] (like on Arch Linux)
# delv, dig - pull in nss_resolve if `resolve` is in nsswitch.conf
# tar - called by machinectl in TEST-25
bin_rx='/(chown|delv|dig|getent|login|ls|mkfs\.[a-z0-9]+|mksquashfs|mkswap|setpriv|stat|su|tar|useradd|userdel)$'
bin_rx='/(chown|delv|dig|getent|login|ls|mkfs\.[a-z0-9]+|mksquashfs|mkswap|setfacl|setpriv|stat|su|tar|useradd|userdel)$'
if get_bool "$IS_BUILT_WITH_ASAN" && [[ "$bin" =~ $bin_rx ]]; then
wrap_binary=1
fi

View file

@ -15,6 +15,7 @@ at_exit() {
machinectl status long-running >/dev/null && machinectl kill --signal=KILL long-running
mountpoint -q /var/lib/machines && timeout 10 sh -c "while ! umount /var/lib/machines; do sleep .5; done"
[[ -n "${NSPAWN_FRAGMENT:-}" ]] && rm -f "/etc/systemd/nspawn/$NSPAWN_FRAGMENT" "/var/lib/machines/$NSPAWN_FRAGMENT"
rm -f /run/systemd/nspawn/*.nspawn
}
trap at_exit EXIT

View file

@ -18,6 +18,7 @@ at_exit() {
[[ -n "${DEV:-}" ]] && rm -f "$DEV"
[[ -n "${NETNS:-}" ]] && umount "$NETNS" && rm -f "$NETNS"
[[ -n "${TMPDIR:-}" ]] && rm -fr "$TMPDIR"
rm -f /run/systemd/nspawn/*.nspawn
}
trap at_exit EXIT
@ -382,4 +383,84 @@ touch /opt/readonly/foo && exit 1
exit 0
EOF
systemd-nspawn --oci-bundle="$OCI"
timeout 30 systemd-nspawn --oci-bundle="$OCI"
# Test a couple of invalid configs
INVALID_SNIPPETS=(
# Invalid object
'"foo" : { }'
'"process" : { "foo" : [ ] }'
# Non-absolute mount
'"mounts" : [ { "destination" : "foo", "type" : "tmpfs", "source" : "tmpfs" } ]'
# Invalid rlimit
'"process" : { "rlimits" : [ { "type" : "RLIMIT_FOO", "soft" : 0, "hard" : 0 } ] }'
# rlimit without RLIMIT_ prefix
'"process" : { "rlimits" : [ { "type" : "CORE", "soft" : 0, "hard" : 0 } ] }'
# Invalid env assignment
'"process" : { "env" : [ "foo" ] }'
'"process" : { "env" : [ "foo=bar", 1 ] }'
# Invalid process args
'"process" : { "args" : [ ] }'
'"process" : { "args" : [ "" ] }'
'"process" : { "args" : [ "foo", 1 ] }'
# Invalid capabilities
'"process" : { "capabilities" : { "bounding" : [ 1 ] } }'
'"process" : { "capabilities" : { "bounding" : [ "FOO_BAR" ] } }'
# Unsupported option (without JSON_PERMISSIVE)
'"linux" : { "resources" : { "cpu" : { "realtimeRuntime" : 1 } } }'
# Invalid namespace
'"linux" : { "namespaces" : [ { "type" : "foo" } ] }'
# Namespace path for a non-network namespace
'"linux" : { "namespaces" : [ { "type" : "user", "path" : "/foo/bar" } ] }'
# Duplicate namespace
'"linux" : { "namespaces" : [ { "type" : "ipc" }, { "type" : "ipc" } ] }'
# Invalid device type
'"linux" : { "devices" : [ { "type" : "foo", "path" : "/dev/foo" } ] }'
# Invalid cgroups path
'"linux" : { "cgroupsPath" : "/foo/bar/baz" }'
'"linux" : { "cgroupsPath" : "foo/bar/baz" }'
# Invalid sysctl assignments
'"linux" : { "sysctl" : { "vm.swappiness" : 60 } }'
'"linux" : { "sysctl" : { "foo..bar" : "baz" } }'
# Invalid seccomp assignments
'"linux" : { "seccomp" : { } }'
'"linux" : { "seccomp" : { "defaultAction" : 1 } }'
'"linux" : { "seccomp" : { "defaultAction" : "foo" } }'
'"linux" : { "seccomp" : { "defaultAction" : "SCMP_ACT_ALLOW", "syscalls" : [ { "action" : "SCMP_ACT_ERRNO", "names" : [ ] } ] } }'
# Invalid masked paths
'"linux" : { "maskedPaths" : [ "/foo", 1 ] }'
'"linux" : { "maskedPaths" : [ "/foo", "bar" ] }'
# Invalid read-only paths
'"linux" : { "readonlyPaths" : [ "/foo", 1 ] }'
'"linux" : { "readonlyPaths" : [ "/foo", "bar" ] }'
# Invalid hooks
'"hooks" : { "prestart" : [ { "path" : "/bin/sh", "timeout" : 0 } ] }'
# Invalid annotations
'"annotations" : { "" : "bar" }'
'"annotations" : { "foo" : 1 }'
)
for snippet in "${INVALID_SNIPPETS[@]}"; do
: "Snippet: $snippet"
cat >"$OCI/config.json" <<EOF
{
"ociVersion" : "1.0.0",
"root" : {
"path" : "rootfs"
},
$snippet
}
EOF
(! systemd-nspawn --oci-bundle="$OCI" sh -c 'echo hello')
done
# Invalid OCI bundle version
cat >"$OCI/config.json" <<EOF
{
"ociVersion" : "6.6.6",
"root" : {
"path" : "rootfs"
}
}
EOF
(! systemd-nspawn --oci-bundle="$OCI" sh -c 'echo hello')

View file

@ -1,6 +1,25 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# shellcheck disable=SC2016
#
# Notes on coverage: when collecting coverage we need the $BUILD_DIR present
# and writable in the container as well. To do this in the least intrusive way,
# two things are going on in the background (only when built with -Db_coverage=true):
# 1) the systemd-nspawn@.service is copied to /etc/systemd/system/ with
# --bind=$BUILD_DIR appended to the ExecStart= line
# 2) each create_dummy_container() call also creates an .nspawn file in /run/systemd/nspawn/
# with the last fragment from the path used as a name
#
# The first change is quite self-contained and applies only to containers run
# with machinectl. The second one might cause some unexpected side-effects, namely:
# - nspawn config (setting) files don't support dropins, so tests that test
# the config files might need some tweaking (as seen below with
# the $COVERAGE_BUILD_DIR shenanigans) since they overwrite the .nspawn file
# - also a note - if /etc/systemd/nspawn/cont-name.nspawn exists, it takes
# precedence and /run/systemd/nspawn/cont-name.nspawn won't be read even
# if it exists
# - in some cases we don't create a test container using create_dummy_container(),
# so in that case an explicit call to coverage_create_nspawn_dropin() is needed
set -eux
set -o pipefail
@ -14,6 +33,7 @@ at_exit() {
set +e
mountpoint -q /var/lib/machines && umount /var/lib/machines
rm -f /run/systemd/nspawn/*.nspawn
}
trap at_exit EXIT
@ -47,7 +67,7 @@ fi
mkdir -p /var/lib/machines
mount -t tmpfs tmpfs /var/lib/machines
testcase_sanity_check() {
testcase_sanity() {
local template root image uuid tmpdir
tmpdir="$(mktemp -d)"
@ -67,6 +87,7 @@ testcase_sanity_check() {
# --template=
root="$(mktemp -u -d /var/lib/machines/testsuite-13.sanity.XXX)"
coverage_create_nspawn_dropin "$root"
(! systemd-nspawn --directory="$root" bash -xec 'echo hello')
# Initialize $root from $template (the $root directory must not exist, hence
# the `mktemp -u` above)
@ -213,6 +234,20 @@ EOF
test ! -e "$tmpdir/3/nope"
rm -fr "$tmpdir"
# --port (sanity only)
systemd-nspawn --network-veth --directory="$root" --port=80 --port=90 true
systemd-nspawn --network-veth --directory="$root" --port=80:8080 true
systemd-nspawn --network-veth --directory="$root" --port=tcp:80 true
systemd-nspawn --network-veth --directory="$root" --port=tcp:80:8080 true
systemd-nspawn --network-veth --directory="$root" --port=udp:80 true
systemd-nspawn --network-veth --directory="$root" --port=udp:80:8080 --port=tcp:80:8080 true
(! systemd-nspawn --network-veth --directory="$root" --port= true)
(! systemd-nspawn --network-veth --directory="$root" --port=-1 true)
(! systemd-nspawn --network-veth --directory="$root" --port=: true)
(! systemd-nspawn --network-veth --directory="$root" --port=icmp:80:8080 true)
(! systemd-nspawn --network-veth --directory="$root" --port=tcp::8080 true)
(! systemd-nspawn --network-veth --directory="$root" --port=8080: true)
# Assorted tests
systemd-nspawn --directory="$root" --suppress-sync=yes bash -xec 'echo hello'
systemd-nspawn --capability=help
@ -263,7 +298,208 @@ EOF
(! systemd-nspawn --rlimit==)
}
testcase_check_bind_tmp_path() {
nspawn_settings_cleanup() {
for dev in sd-host-only sd-shared{1,2} sd-macvlan{1,2} sd-ipvlan{1,2}; do
ip link del "$dev" || :
done
return 0
}
testcase_nspawn_settings() {
local root container dev private_users
mkdir -p /run/systemd/nspawn
root="$(mktemp -d /var/lib/machines/testsuite-13.nspawn-settings.XXX)"
container="$(basename "$root")"
create_dummy_container "$root"
rm -f "/etc/systemd/nspawn/$container.nspawn"
mkdir -p "$root/tmp" "$root"/opt/{tmp,inaccessible,also-inaccessible}
for dev in sd-host-only sd-shared{1,2} sd-macvlan{1,2} sd-ipvlan{1,2}; do
ip link add "$dev" type dummy
done
udevadm settle
ip link
trap nspawn_settings_cleanup RETURN
# Let's start with one huge config to test as much as we can at once
cat >"/run/systemd/nspawn/$container.nspawn" <<EOF
[Exec]
Boot=no
Ephemeral=no
ProcessTwo=no
Parameters=bash /entrypoint.sh "foo bar" 'bar baz'
Environment=FOO=bar
Environment=BAZ="hello world"
User=root
WorkingDirectory=/tmp
Capability=CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN
DropCapability=CAP_AUDIT_CONTROL CAP_AUDIT_WRITE
AmbientCapability=CAP_BPF CAP_CHOWN
NoNewPrivileges=no
MachineID=f28f129b51874b1280a89421ec4b4ad4
PrivateUsers=no
NotifyReady=no
SystemCallFilter=@basic-io @chown
SystemCallFilter=~ @clock
LimitNOFILE=1024:2048
LimitRTPRIO=8:16
OOMScoreAdjust=32
CPUAffinity=0,0-5,1-5
Hostname=nspawn-settings
ResolvConf=copy-host
Timezone=delete
LinkJournal=no
SuppressSync=no
[Files]
ReadOnly=no
Volatile=no
TemporaryFileSystem=/tmp
TemporaryFileSystem=/opt/tmp
Inaccessible=/opt/inaccessible
Inaccessible=/opt/also-inaccessible
PrivateUsersOwnership=auto
Overlay=+/var::/var
${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"}
[Network]
Private=yes
VirtualEthernet=yes
VirtualEthernetExtra=my-fancy-veth1
VirtualEthernetExtra=fancy-veth2:my-fancy-veth2
Interface=sd-shared1 sd-shared2:sd-shared2
MACVLAN=sd-macvlan1 sd-macvlan2:my-macvlan2
IPVLAN=sd-ipvlan1 sd-ipvlan2:my-ipvlan2
Zone=sd-zone0
Port=80
Port=81:8181
Port=tcp:60
Port=udp:60:61
EOF
cat >"$root/entrypoint.sh" <<\EOF
#!/bin/bash -ex
[[ "$1" == "foo bar" ]]
[[ "$2" == "bar baz" ]]
[[ "$USER" == root ]]
[[ "$FOO" == bar ]]
[[ "$BAZ" == "hello world" ]]
[[ "$PWD" == /tmp ]]
[[ "$(</etc/machine-id)" == f28f129b51874b1280a89421ec4b4ad4 ]]
[[ "$(ulimit -S -n)" -eq 1024 ]]
[[ "$(ulimit -H -n)" -eq 2048 ]]
[[ "$(ulimit -S -r)" -eq 8 ]]
[[ "$(ulimit -H -r)" -eq 16 ]]
[[ "$(</proc/self/oom_score_adj)" -eq 32 ]]
[[ "$(hostname)" == nspawn-settings ]]
[[ -e /etc/resolv.conf ]]
[[ ! -e /etc/localtime ]]
mountpoint /tmp
touch /tmp/foo
mountpoint /opt/tmp
touch /opt/tmp/foo
touch /opt/inaccessible/foo && exit 1
touch /opt/also-inaccessible/foo && exit 1
mountpoint /var
ip link
ip link | grep host-only && exit 1
ip link | grep host0@
ip link | grep my-fancy-veth1@
ip link | grep my-fancy-veth2@
ip link | grep sd-shared1
ip link | grep sd-shared2
ip link | grep mv-sd-macvlan1@
ip link | grep my-macvlan2@
ip link | grep iv-sd-ipvlan1@
ip link | grep my-ipvlan2@
EOF
timeout 30 systemd-nspawn --directory="$root"
# And now for stuff that needs to run separately
#
# Note on the condition below: since our container tree is owned by root,
# both "yes" and "identity" private users settings will behave the same
# as PrivateUsers=0:65535, which makes BindUser= fail as the UID already
# exists there, so skip setting it in such case
for private_users in "131072:65536" yes identity pick; do
cat >"/run/systemd/nspawn/$container.nspawn" <<EOF
[Exec]
Hostname=private-users
PrivateUsers=$private_users
[Files]
PrivateUsersOwnership=auto
BindUser=
$([[ "$private_users" =~ (yes|identity) ]] || echo "BindUser=testuser")
${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"}
EOF
cat "/run/systemd/nspawn/$container.nspawn"
chown -R root:root "$root"
systemd-nspawn --directory="$root" bash -xec '[[ "$(hostname)" == private-users ]]'
done
rm -fr "$root" "/run/systemd/nspawn/$container.nspawn"
}
bind_user_cleanup() {
userdel --force --remove nspawn-bind-user-1
userdel --force --remove nspawn-bind-user-2
}
testcase_bind_user() {
local root
root="$(mktemp -d /var/lib/machines/testsuite-13.bind-user.XXX)"
create_dummy_container "$root"
useradd --create-home --user-group nspawn-bind-user-1
useradd --create-home --user-group nspawn-bind-user-2
trap bind_user_cleanup RETURN
touch /home/nspawn-bind-user-1/foo
touch /home/nspawn-bind-user-2/bar
# Add a couple of POSIX ACLs to test the patch-uid stuff
mkdir -p "$root/opt"
setfacl -R -m 'd:u:nspawn-bind-user-1:rwX' -m 'u:nspawn-bind-user-1:rwX' "$root/opt"
setfacl -R -m 'd:g:nspawn-bind-user-1:rwX' -m 'g:nspawn-bind-user-1:rwX' "$root/opt"
systemd-nspawn --directory="$root" \
--private-users=pick \
--bind-user=nspawn-bind-user-1 \
bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo'
systemd-nspawn --directory="$root" \
--private-users=pick \
--private-users-ownership=chown \
--bind-user=nspawn-bind-user-1 \
--bind-user=nspawn-bind-user-2 \
bash -xec 'test -e /run/host/home/nspawn-bind-user-1/foo; test -e /run/host/home/nspawn-bind-user-2/bar'
chown -R root:root "$root"
# User/group name collision
echo "nspawn-bind-user-2:x:1000:1000:nspawn-bind-user-2:/home/nspawn-bind-user-2:/bin/bash" >"$root/etc/passwd"
(! systemd-nspawn --directory="$root" \
--private-users=pick \
--bind-user=nspawn-bind-user-1 \
--bind-user=nspawn-bind-user-2 \
true)
rm -f "$root/etc/passwd"
echo "nspawn-bind-user-2:x:1000:" >"$root/etc/group"
(! systemd-nspawn --directory="$root" \
--private-users=pick \
--bind-user=nspawn-bind-user-1 \
--bind-user=nspawn-bind-user-2 \
true)
rm -f "$root/etc/group"
rm -fr "$root"
}
testcase_bind_tmp_path() {
# https://github.com/systemd/systemd/issues/4789
local root
@ -278,7 +514,7 @@ testcase_check_bind_tmp_path() {
rm -fr "$root" /tmp/bind
}
testcase_check_norbind() {
testcase_norbind() {
# https://github.com/systemd/systemd/issues/13170
local root
@ -298,14 +534,14 @@ testcase_check_norbind() {
rm -fr "$root" /tmp/binddir/
}
check_rootidmap_cleanup() {
rootidmap_cleanup() {
local dir="${1:?}"
mountpoint -q "$dir/bind" && umount "$dir/bind"
rm -fr "$dir"
}
testcase_check_rootidmap() {
testcase_rootidmap() {
local root cmd permissions
local owner=1000
@ -315,7 +551,7 @@ testcase_check_rootidmap() {
dd if=/dev/zero of=/tmp/rootidmap/ext4.img bs=4k count=2048
mkfs.ext4 /tmp/rootidmap/ext4.img
mount /tmp/rootidmap/ext4.img /tmp/rootidmap/bind
trap "check_rootidmap_cleanup /tmp/rootidmap/" RETURN
trap "rootidmap_cleanup /tmp/rootidmap/" RETURN
touch /tmp/rootidmap/bind/file
chown -R "$owner:$owner" /tmp/rootidmap/bind
@ -342,7 +578,7 @@ testcase_check_rootidmap() {
fi
}
testcase_check_notification_socket() {
testcase_notification_socket() {
# https://github.com/systemd/systemd/issues/4944
local root
local cmd='echo a | nc -U -u -w 1 /run/host/notify'
@ -352,12 +588,14 @@ testcase_check_notification_socket() {
systemd-nspawn --register=no --directory="$root" bash -x -c "$cmd"
systemd-nspawn --register=no --directory="$root" -U bash -x -c "$cmd"
rm -fr "$root"
}
testcase_check_os_release() {
testcase_os_release() {
local root entrypoint os_release_source
root="$(mktemp -d /var/lib/machines/testsuite-13.check-os-release.XXX)"
root="$(mktemp -d /var/lib/machines/testsuite-13.os-release.XXX)"
create_dummy_container "$root"
entrypoint="$root/entrypoint.sh"
cat >"$entrypoint" <<\EOF
@ -395,11 +633,11 @@ EOF
rm -fr "$root"
}
testcase_check_machinectl_bind() {
testcase_machinectl_bind() {
local service_path service_name root container_name ec
local cmd='for i in $(seq 1 20); do if test -f /tmp/marker; then exit 0; fi; sleep .5; done; exit 1;'
root="$(mktemp -d /var/lib/machines/testsuite-13.check-machinectl-bind.XXX)"
root="$(mktemp -d /var/lib/machines/testsuite-13.machinectl-bind.XXX)"
create_dummy_container "$root"
container_name="$(basename "$root")"
@ -425,7 +663,7 @@ EOF
return "$ec"
}
testcase_check_selinux() {
testcase_selinux() {
# Basic test coverage to avoid issues like https://github.com/systemd/systemd/issues/19976
if ! command -v selinuxenabled >/dev/null || ! selinuxenabled; then
echo >&2 "SELinux is not enabled, skipping SELinux-related tests"
@ -434,7 +672,7 @@ testcase_check_selinux() {
local root
root="$(mktemp -d /var/lib/machines/testsuite-13.check-selinux.XXX)"
root="$(mktemp -d /var/lib/machines/testsuite-13.selinux.XXX)"
create_dummy_container "$root"
chcon -R -t container_t "$root"
@ -447,17 +685,19 @@ testcase_check_selinux() {
rm -fr "$root"
}
testcase_check_ephemeral_config() {
testcase_ephemeral_config() {
# https://github.com/systemd/systemd/issues/13297
local root container_name
root="$(mktemp -d /var/lib/machines/testsuite-13.check-ephemeral-config.XXX)"
root="$(mktemp -d /var/lib/machines/testsuite-13.ephemeral-config.XXX)"
create_dummy_container "$root"
container_name="${root##*/}"
container_name="$(basename "$root")"
mkdir -p /run/systemd/nspawn/
rm -f "/etc/systemd/nspawn/$container_name.nspawn"
cat >"/run/systemd/nspawn/$container_name.nspawn" <<EOF
[Files]
${COVERAGE_BUILD_DIR:+"Bind=$COVERAGE_BUILD_DIR"}
BindReadOnly=/tmp/ephemeral-config
EOF
touch /tmp/ephemeral-config
@ -473,7 +713,7 @@ EOF
--machine=foobar \
bash -x -c "! test -f /tmp/ephemeral-config"
rm -fr "$root" "/run/systemd/nspawn/$container_name"
rm -fr "$root" "/run/systemd/nspawn/$container_name.nspawn"
}
matrix_run_one() {

View file

@ -81,6 +81,23 @@ runas() {
XDG_RUNTIME_DIR=/run/user/"$(id -u "$userid")" setpriv --reuid="$userid" --init-groups "$@"
}
coverage_create_nspawn_dropin() {
# If we're collecting coverage, bind mount the $BUILD_DIR into the nspawn
# container so gcov can update the counters. This is mostly for standalone
# containers, as machinectl stuff is handled by overriding the systemd-nspawn@.service
# (see test/test-functions:install_systemd())
local root="${1:?}"
local container
if [[ -z "${COVERAGE_BUILD_DIR:-}" ]]; then
return 0
fi
container="$(basename "$root")"
mkdir -p "/run/systemd/nspawn"
echo -ne "[Files]\nBind=$COVERAGE_BUILD_DIR\n" >"/run/systemd/nspawn/${container:?}.nspawn"
}
create_dummy_container() {
local root="${1:?}"
@ -91,4 +108,5 @@ create_dummy_container() {
mkdir -p "$root"
cp -a /testsuite-13-container-template/* "$root"
coverage_create_nspawn_dropin "$root"
}