diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 02:23:15 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 02:23:15 +0000 |
commit | a735309d17eb99e4df3d952c5e5d3e60f8fe5a40 (patch) | |
tree | 1e33fbce443dfaf2f246fe133925c059eb707854 | |
parent | Adding upstream version 252.25. (diff) | |
download | systemd-upstream.tar.xz systemd-upstream.zip |
Adding upstream version 252.26.upstream/252.26upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
32 files changed, 410 insertions, 135 deletions
diff --git a/.semaphore/semaphore-runner.sh b/.semaphore/semaphore-runner.sh index eb10ee6..4a37ee2 100755 --- a/.semaphore/semaphore-runner.sh +++ b/.semaphore/semaphore-runner.sh @@ -7,6 +7,7 @@ set -o pipefail # default to Debian testing DISTRO="${DISTRO:-debian}" RELEASE="${RELEASE:-bookworm}" +SALSA_URL="${SALSA_URL:-https://salsa.debian.org/systemd-team/systemd.git}" BRANCH="${BRANCH:-upstream-ci}" ARCH="${ARCH:-amd64}" CONTAINER="${RELEASE}-${ARCH}" @@ -69,7 +70,7 @@ for phase in "${PHASES[@]}"; do ;; RUN) # add current debian/ packaging - git fetch --depth=1 https://salsa.debian.org/systemd-team/systemd.git "$BRANCH" + git fetch --depth=1 "$SALSA_URL" "$BRANCH" git checkout FETCH_HEAD debian # craft changelog diff --git a/man/systemd-run.xml b/man/systemd-run.xml index 0c91d61..d57eda1 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -64,8 +64,8 @@ other service, and thus shows up in the output of <command>systemctl list-units</command> like any other unit. It will run in a clean and detached execution environment, with the service manager as its parent process. In this mode, <command>systemd-run</command> will start the service asynchronously in the background and return after the - command has begun execution (unless <option>--no-block</option> or <option>--wait</option> are specified, see - below).</para> + command has begun execution (unless <option>--no-block</option>, <option>--wait</option>, <option>--pipe</option>, + or <option>--pty</option> are specified, see below).</para> <para>If a command is run as transient scope unit, it will be executed by <command>systemd-run</command> itself as parent process and will thus inherit the execution environment of the caller. However, the @@ -262,6 +262,11 @@ to the terminal <command>systemd-run</command> is invoked on, via a pseudo TTY device. This allows running programs that expect interactive user input/output as services, such as interactive command shells.</para> + <para>This option will result in <command>systemd-run</command> synchronously waiting for + the transient service to terminate, similar to specifying <option>--wait</option>. If specified + along with <option>--wait</option>, <command>systemd-run</command> won't exit when manually disconnecting + from the pseudo TTY device.</para> + <para>Note that <citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s <command>shell</command> command is usually a better alternative for requesting a new, interactive login @@ -276,15 +281,19 @@ <listitem><para>If specified, standard input, output, and error of the transient service are inherited from the <command>systemd-run</command> command itself. This allows <command>systemd-run</command> - to be used within shell pipelines. - Note that this mode is not suitable for interactive command shells and similar, as the - service process will not become a TTY controller when invoked on a terminal. Use <option>--pty</option> instead - in that case.</para> + to be used within shell pipelines.</para> + + <para>Note that this mode is not suitable for interactive command shells and similar, as the + service process will not become a TTY controller when invoked on a terminal. Use <option>--pty</option> + instead in that case.</para> <para>When both <option>--pipe</option> and <option>--pty</option> are used in combination the more appropriate option is automatically determined and used. Specifically, when invoked with standard input, output and error connected to a TTY <option>--pty</option> is used, and otherwise <option>--pipe</option>.</para> + <para>This option will result in <command>systemd-run</command> synchronously waiting for + the transient service to terminate, similar to specifying <option>--wait</option>.</para> + <para>When this option is used the original file descriptors <command>systemd-run</command> receives are passed to the service processes as-is. If the service runs with different privileges than <command>systemd-run</command>, this means the service might not be able to re-open the passed file diff --git a/src/basic/linux/netfilter.h b/src/basic/linux/netfilter.h new file mode 100644 index 0000000..30c045b --- /dev/null +++ b/src/basic/linux/netfilter.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +#ifndef __LINUX_NETFILTER_H +#define __LINUX_NETFILTER_H + +#include <linux/types.h> + +#include <linux/in.h> +#include <linux/in6.h> + +/* Responses from hook functions. */ +#define NF_DROP 0 +#define NF_ACCEPT 1 +#define NF_STOLEN 2 +#define NF_QUEUE 3 +#define NF_REPEAT 4 +#define NF_STOP 5 /* Deprecated, for userspace nf_queue compatibility. */ +#define NF_MAX_VERDICT NF_STOP + +/* we overload the higher bits for encoding auxiliary data such as the queue + * number or errno values. Not nice, but better than additional function + * arguments. */ +#define NF_VERDICT_MASK 0x000000ff + +/* extra verdict flags have mask 0x0000ff00 */ +#define NF_VERDICT_FLAG_QUEUE_BYPASS 0x00008000 + +/* queue number (NF_QUEUE) or errno (NF_DROP) */ +#define NF_VERDICT_QMASK 0xffff0000 +#define NF_VERDICT_QBITS 16 + +#define NF_QUEUE_NR(x) ((((x) << 16) & NF_VERDICT_QMASK) | NF_QUEUE) + +#define NF_DROP_ERR(x) (((-x) << 16) | NF_DROP) + +/* only for userspace compatibility */ + +/* NF_VERDICT_BITS should be 8 now, but userspace might break if this changes */ +#define NF_VERDICT_BITS 16 + +enum nf_inet_hooks { + NF_INET_PRE_ROUTING, + NF_INET_LOCAL_IN, + NF_INET_FORWARD, + NF_INET_LOCAL_OUT, + NF_INET_POST_ROUTING, + NF_INET_NUMHOOKS, + NF_INET_INGRESS = NF_INET_NUMHOOKS, +}; + +enum nf_dev_hooks { + NF_NETDEV_INGRESS, + NF_NETDEV_EGRESS, + NF_NETDEV_NUMHOOKS +}; + +enum { + NFPROTO_UNSPEC = 0, + NFPROTO_INET = 1, + NFPROTO_IPV4 = 2, + NFPROTO_ARP = 3, + NFPROTO_NETDEV = 5, + NFPROTO_BRIDGE = 7, + NFPROTO_IPV6 = 10, + NFPROTO_DECNET = 12, + NFPROTO_NUMPROTO, +}; + +union nf_inet_addr { + __u32 all[4]; + __be32 ip; + __be32 ip6[4]; + struct in_addr in; + struct in6_addr in6; +}; + +#endif /* __LINUX_NETFILTER_H */ diff --git a/src/core/execute.c b/src/core/execute.c index 2c1dda1..fc3d2ce 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -4710,12 +4710,14 @@ static int exec_child( if (ns_type_supported(NAMESPACE_NET)) { r = setup_shareable_ns(runtime->netns_storage_socket, CLONE_NEWNET); - if (r == -EPERM) - log_unit_warning_errno(unit, r, - "PrivateNetwork=yes is configured, but network namespace setup failed, ignoring: %m"); - else if (r < 0) { - *exit_status = EXIT_NETWORK; - return log_unit_error_errno(unit, r, "Failed to set up network namespacing: %m"); + if (r < 0) { + if (ERRNO_IS_PRIVILEGE(r)) + log_unit_warning_errno(unit, r, + "PrivateNetwork=yes is configured, but network namespace setup failed, ignoring: %m"); + else { + *exit_status = EXIT_NETWORK; + return log_unit_error_errno(unit, r, "Failed to set up network namespacing: %m"); + } } } else if (context->network_namespace_path) { *exit_status = EXIT_NETWORK; @@ -4729,12 +4731,14 @@ static int exec_child( if (ns_type_supported(NAMESPACE_IPC)) { r = setup_shareable_ns(runtime->ipcns_storage_socket, CLONE_NEWIPC); - if (r == -EPERM) - log_unit_warning_errno(unit, r, - "PrivateIPC=yes is configured, but IPC namespace setup failed, ignoring: %m"); - else if (r < 0) { - *exit_status = EXIT_NAMESPACE; - return log_unit_error_errno(unit, r, "Failed to set up IPC namespacing: %m"); + if (r < 0) { + if (ERRNO_IS_PRIVILEGE(r)) + log_unit_warning_errno(unit, r, + "PrivateIPC=yes is configured, but IPC namespace setup failed, ignoring: %m"); + else { + *exit_status = EXIT_NAMESPACE; + return log_unit_error_errno(unit, r, "Failed to set up IPC namespacing: %m"); + } } } else if (context->ipc_namespace_path) { *exit_status = EXIT_NAMESPACE; diff --git a/src/core/import-creds.c b/src/core/import-creds.c index dab7d36..91922ab 100644 --- a/src/core/import-creds.c +++ b/src/core/import-creds.c @@ -512,7 +512,7 @@ static int parse_smbios_strings(ImportCredentialContext *c, const char *data, si return log_oom(); if (!credential_name_valid(cn)) { - log_warning("SMBIOS credential name '%s' is not valid, ignoring: %m", cn); + log_warning("SMBIOS credential name '%s' is not valid, ignoring.", cn); continue; } diff --git a/src/import/curl-util.c b/src/import/curl-util.c index 94f718d..b631f4b 100644 --- a/src/import/curl-util.c +++ b/src/import/curl-util.c @@ -126,6 +126,13 @@ static int curl_glue_timer_callback(CURLM *curl, long timeout_ms, void *userdata assert(curl); + /* Don't configure timer anymore when the event loop is dead already. */ + if (g->timer) { + sd_event *event_loop = sd_event_source_get_event(g->timer); + if (event_loop && sd_event_get_state(event_loop) == SD_EVENT_FINISHED) + return 0; + } + if (timeout_ms < 0) { if (g->timer) { if (sd_event_source_set_enabled(g->timer, SD_EVENT_OFF) < 0) diff --git a/src/libsystemd-network/icmp6-util.c b/src/libsystemd-network/icmp6-util.c index d3ee7c4..8ea09fd 100644 --- a/src/libsystemd-network/icmp6-util.c +++ b/src/libsystemd-network/icmp6-util.c @@ -154,7 +154,7 @@ int icmp6_receive( /* This needs to be initialized with zero. See #20741. */ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(int)) + /* ttl */ CMSG_SPACE_TIMEVAL) control = {}; - struct iovec iov = {}; + struct iovec iov = { buffer, size }; union sockaddr_union sa = {}; struct msghdr msg = { .msg_name = &sa.sa, @@ -169,8 +169,6 @@ int icmp6_receive( triple_timestamp t = {}; ssize_t len; - iov = IOVEC_MAKE(buffer, size); - len = recvmsg_safe(fd, &msg, MSG_DONTWAIT); if (len < 0) return (int) len; diff --git a/src/libsystemd-network/test-dhcp-client.c b/src/libsystemd-network/test-dhcp-client.c index 787dcf1..bf17e51 100644 --- a/src/libsystemd-network/test-dhcp-client.c +++ b/src/libsystemd-network/test-dhcp-client.c @@ -513,7 +513,7 @@ static void test_addr_acq(sd_event *e) { callback_recv = test_addr_acq_recv_discover; assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, - 2 * USEC_PER_SEC, 0, + 30 * USEC_PER_SEC, 0, NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); res = sd_dhcp_client_start(client); diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index f8a4c2c..763ecb9 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -62,7 +62,9 @@ static int test_basic(bool bind_to_interface) { test_pool(&address_lo, 1, 0); r = sd_dhcp_server_start(server); - if (r == -EPERM) + /* skip test if running in an environment with no full networking support, CONFIG_PACKET not + * compiled in kernel, nor af_packet module available. */ + if (r == -EPERM || r == -EAFNOSUPPORT) return r; assert_se(r >= 0); diff --git a/src/libsystemd-network/test-dhcp6-client.c b/src/libsystemd-network/test-dhcp6-client.c index 10233f5..a3a22e9 100644 --- a/src/libsystemd-network/test-dhcp6-client.c +++ b/src/libsystemd-network/test-dhcp6-client.c @@ -1003,7 +1003,7 @@ TEST(dhcp6_client) { assert_se(sd_event_new(&e) >= 0); assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, - 2 * USEC_PER_SEC, 0, + 30 * USEC_PER_SEC, 0, NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); assert_se(sd_dhcp6_client_new(&client) >= 0); diff --git a/src/libsystemd-network/test-ndisc-ra.c b/src/libsystemd-network/test-ndisc-ra.c index d3d96e7..16a934c 100644 --- a/src/libsystemd-network/test-ndisc-ra.c +++ b/src/libsystemd-network/test-ndisc-ra.c @@ -332,7 +332,7 @@ TEST(ra) { assert_se(sd_event_source_set_io_fd_own(recv_router_advertisement, true) >= 0); assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, - 2 * USEC_PER_SEC, 0, + 30 * USEC_PER_SEC, 0, NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); assert_se(sd_radv_start(ra) >= 0); diff --git a/src/machine/image-dbus.c b/src/machine/image-dbus.c index 84dc95e..6acbe8d 100644 --- a/src/machine/image-dbus.c +++ b/src/machine/image-dbus.c @@ -133,9 +133,17 @@ int bus_image_method_rename( if (r == 0) return 1; /* Will call us back */ + /* The image is cached with its name, hence it is necessary to remove from the cache before renaming. */ + assert_se(hashmap_remove_value(m->image_cache, image->name, image)); + r = image_rename(image, new_name); - if (r < 0) + if (r < 0) { + image_unref(image); return r; + } + + /* Then save the object again in the cache. */ + assert_se(hashmap_put(m->image_cache, image->name, image) > 0); return sd_bus_reply_method_return(message, NULL); } @@ -393,30 +401,17 @@ static int image_flush_cache(sd_event_source *s, void *userdata) { return 0; } -static int image_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { - _cleanup_free_ char *e = NULL; - Manager *m = userdata; - Image *image = NULL; - const char *p; +int manager_acquire_image(Manager *m, const char *name, Image **ret) { int r; - assert(bus); - assert(path); - assert(interface); - assert(found); + assert(m); + assert(name); - p = startswith(path, "/org/freedesktop/machine1/image/"); - if (!p) + Image *existing = hashmap_get(m->image_cache, name); + if (existing) { + if (ret) + *ret = existing; return 0; - - e = bus_label_unescape(p); - if (!e) - return -ENOMEM; - - image = hashmap_get(m->image_cache, e); - if (image) { - *found = image; - return 1; } if (!m->image_cache_defer_event) { @@ -433,19 +428,49 @@ static int image_object_find(sd_bus *bus, const char *path, const char *interfac if (r < 0) return r; - r = image_find(IMAGE_MACHINE, e, NULL, &image); - if (r == -ENOENT) - return 0; + _cleanup_(image_unrefp) Image *image = NULL; + r = image_find(IMAGE_MACHINE, name, NULL, &image); if (r < 0) return r; image->userdata = m; r = hashmap_ensure_put(&m->image_cache, &image_hash_ops, image->name, image); - if (r < 0) { - image_unref(image); + if (r < 0) + return r; + + if (ret) + *ret = image; + + TAKE_PTR(image); + return 0; +} + +static int image_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + _cleanup_free_ char *e = NULL; + Manager *m = userdata; + Image *image; + const char *p; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + p = startswith(path, "/org/freedesktop/machine1/image/"); + if (!p) + return 0; + + e = bus_label_unescape(p); + if (!e) + return -ENOMEM; + + r = manager_acquire_image(m, e, &image); + if (r == -ENOENT) + return 0; + if (r < 0) return r; - } *found = image; return 1; diff --git a/src/machine/image-dbus.h b/src/machine/image-dbus.h index 4b00203..0c4fab1 100644 --- a/src/machine/image-dbus.h +++ b/src/machine/image-dbus.h @@ -2,10 +2,12 @@ #pragma once #include "bus-object.h" +#include "discover-image.h" #include "machined.h" extern const BusObjectImplementation image_object; +int manager_acquire_image(Manager *m, const char *name, Image **ret); char *image_bus_path(const char *name); int bus_image_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/machine/machined-dbus.c b/src/machine/machined-dbus.c index 56dd22d..1e16046 100644 --- a/src/machine/machined-dbus.c +++ b/src/machine/machined-dbus.c @@ -541,8 +541,8 @@ static int method_get_machine_uid_shift(sd_bus_message *message, void *userdata, } static int redirect_method_to_image(sd_bus_message *message, Manager *m, sd_bus_error *error, sd_bus_message_handler_t method) { - _cleanup_(image_unrefp) Image* i = NULL; const char *name; + Image *i; int r; assert(message); @@ -556,13 +556,12 @@ static int redirect_method_to_image(sd_bus_message *message, Manager *m, sd_bus_ if (!image_name_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name); - r = image_find(IMAGE_MACHINE, name, NULL, &i); + r = manager_acquire_image(m, name, &i); if (r == -ENOENT) return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name); if (r < 0) return r; - i->userdata = m; return method(message, i, error); } diff --git a/src/run/run.c b/src/run/run.c index 8e4b0ec..c792807 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -772,11 +772,17 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p } if (pty_path) { + _cleanup_close_ int pty_slave = -EBADF; + + pty_slave = open_terminal(pty_path, O_RDWR|O_NOCTTY|O_CLOEXEC); + if (pty_slave < 0) + return pty_slave; + r = sd_bus_message_append(m, "(sv)(sv)(sv)(sv)", - "StandardInput", "s", "tty", - "StandardOutput", "s", "tty", - "StandardError", "s", "tty", + "StandardInputFileDescriptor", "h", pty_slave, + "StandardOutputFileDescriptor", "h", pty_slave, + "StandardErrorFileDescriptor", "h", pty_slave, "TTYPath", "s", pty_path); if (r < 0) return bus_log_create_error(r); @@ -1027,7 +1033,7 @@ static void run_context_check_done(RunContext *c) { else done = true; - if (c->forward && done) /* If the service is gone, it's time to drain the output */ + if (c->forward && !pty_forward_is_done(c->forward) && done) /* If the service is gone, it's time to drain the output */ done = pty_forward_drain(c->forward); if (done) @@ -1095,11 +1101,18 @@ static int on_properties_changed(sd_bus_message *m, void *userdata, sd_bus_error } static int pty_forward_handler(PTYForward *f, int rcode, void *userdata) { - RunContext *c = userdata; + RunContext *c = ASSERT_PTR(userdata); assert(f); - if (rcode < 0) { + if (rcode == -ECANCELED) { + log_debug_errno(rcode, "PTY forwarder disconnected."); + if (!arg_wait) + return sd_event_exit(c->event, EXIT_SUCCESS); + + /* If --wait is specified, we'll only exit the pty forwarding, but will continue to wait + * for the service to end. If the user hits ^C we'll exit too. */ + } else if (rcode < 0) { sd_event_exit(c->event, EXIT_FAILURE); return log_error_errno(rcode, "Error on PTY forwarding logic: %m"); } diff --git a/src/shared/blockdev-util.c b/src/shared/blockdev-util.c index 99e37fd..3be617b 100644 --- a/src/shared/blockdev-util.c +++ b/src/shared/blockdev-util.c @@ -410,15 +410,43 @@ int blockdev_partscan_enabled(int fd) { * is 1, which can be check with 'ext_range' sysfs attribute. Explicit flag ('GENHD_FL_NO_PART_SCAN') * can be obtained from 'capability' sysattr. * - * With https://github.com/torvalds/linux/commit/1ebe2e5f9d68e94c524aba876f27b945669a7879 (v5.17), we - * can check the flag from 'ext_range' sysfs attribute directly. + * With https://github.com/torvalds/linux/commit/46e7eac647b34ed4106a8262f8bedbb90801fadd (v5.17), + * the flag is renamed to GENHD_FL_NO_PART. + * + * With https://github.com/torvalds/linux/commit/1ebe2e5f9d68e94c524aba876f27b945669a7879 (v5.17), + * we can check the flag from 'ext_range' sysfs attribute directly. + * + * With https://github.com/torvalds/linux/commit/430cc5d3ab4d0ba0bd011cfbb0035e46ba92920c (v5.17), + * the value of GENHD_FL_NO_PART is changed from 0x0200 to 0x0004. 💣💣💣 + * Note, the new value was used by the GENHD_FL_MEDIA_CHANGE_NOTIFY flag, which was introduced by + * 86ce18d7b7925bfd6b64c061828ca2a857ee83b8 (v2.6.22), and removed by + * 9243c6f3e012a92dd900d97ef45efaf8a8edc448 (v5.7). If we believe the commit message of + * e81cd5a983bb35dabd38ee472cf3fea1c63e0f23, the flag was never used. So, fortunately, we can use + * both the new and old values safely. + * + * With https://github.com/torvalds/linux/commit/b9684a71fca793213378dd410cd11675d973eaa1 (v5.19), + * another flag GD_SUPPRESS_PART_SCAN is introduced for loopback block device, and partition scanning + * is done only when both GENHD_FL_NO_PART and GD_SUPPRESS_PART_SCAN are not set. Before the commit, + * LO_FLAGS_PARTSCAN flag was directly tied with GENHD_FL_NO_PART. But with this change now it is + * tied with GD_SUPPRESS_PART_SCAN. So, LO_FLAGS_PARTSCAN cannot be obtained from 'ext_range' + * sysattr, which corresponds to GENHD_FL_NO_PART, and we need to read 'loop/partscan'. 💣💣💣 + * + * With https://github.com/torvalds/linux/commit/73a166d9749230d598320fdae3b687cdc0e2e205 (v6.3), + * the GD_SUPPRESS_PART_SCAN flag is also introduced for userspace block device (ublk). Though, not + * sure if we should support the device... * * With https://github.com/torvalds/linux/commit/e81cd5a983bb35dabd38ee472cf3fea1c63e0f23 (v6.3), - * the 'capability' sysfs attribute is deprecated, hence we cannot check the flag from it. + * the 'capability' sysfs attribute is deprecated, hence we cannot check flags from it. 💣💣💣 + * + * With https://github.com/torvalds/linux/commit/a4217c6740dc64a3eb6815868a9260825e8c68c6 (v6.10, + * backported to v6.9), the partscan status is directly exposed as 'partscan' sysattr. * - * To support both old and new kernels, we need to do the following: first check 'ext_range' sysfs - * attribute, and if '1' we can conclude partition scanning is disabled, otherwise check 'capability' - * sysattr for older version. */ + * To support both old and new kernels, we need to do the following: + * 1) check 'partscan' sysfs attribute where the information is made directly available, + * 2) check 'loop/partscan' sysfs attribute for loopback block devices, and if '0' we can conclude + * partition scanning is disabled, + * 3) check 'ext_range' sysfs attribute, and if '1' we can conclude partition scanning is disabled, + * 4) otherwise check 'capability' sysfs attribute for ancient version. */ assert(fd >= 0); @@ -426,6 +454,16 @@ int blockdev_partscan_enabled(int fd) { if (r < 0) return r; + /* For v6.10 or newer. */ + r = device_get_sysattr_bool(dev, "partscan"); + if (r != -ENOENT) + return r; + + /* For loopback block device, especially for v5.19 or newer. Even if this is enabled, we also need to + * check GENHD_FL_NO_PART flag through 'ext_range' and 'capability' sysfs attributes below. */ + if (device_get_sysattr_bool(dev, "loop/partscan") == 0) + return false; + r = device_get_sysattr_int(dev, "ext_range", &ext_range); if (r == -ENOENT) /* If the ext_range file doesn't exist then we are most likely looking at a * partition block device, not the whole block device. And that means we have no @@ -445,12 +483,10 @@ int blockdev_partscan_enabled(int fd) { if (r < 0) return r; -#ifndef GENHD_FL_NO_PART_SCAN -#define GENHD_FL_NO_PART_SCAN (0x0200) -#endif - - /* If 0x200 is set, part scanning is definitely off. */ - if (FLAGS_SET(capability, GENHD_FL_NO_PART_SCAN)) +#define GENHD_FL_NO_PART_OLD 0x0200 +#define GENHD_FL_NO_PART_NEW 0x0004 + /* If one of the NO_PART flags is set, part scanning is definitely off. */ + if ((capability & (GENHD_FL_NO_PART_OLD | GENHD_FL_NO_PART_NEW)) != 0) return false; /* Otherwise, assume part scanning is on, we have no further checks available. Assume the best. */ diff --git a/src/shared/conf-parser.c b/src/shared/conf-parser.c index 327dc38..55301af 100644 --- a/src/shared/conf-parser.c +++ b/src/shared/conf-parser.c @@ -155,7 +155,11 @@ static int next_assignment( /* Warn about unknown non-extension fields. */ if (!(flags & CONFIG_PARSE_RELAXED) && !startswith(lvalue, "X-")) log_syntax(unit, LOG_WARNING, filename, line, 0, - "Unknown key name '%s' in section '%s', ignoring.", lvalue, section); + "Unknown key '%s'%s%s%s, ignoring.", + lvalue, + section ? " in section [" : "", + strempty(section), + section ? "]" : ""); return 0; } diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 0350dd1..11729d0 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -202,6 +202,44 @@ static int extract_pretty(const char *path, const char *suffix, char **ret) { return 0; } +static int image_update_quota(Image *i, int fd) { + _cleanup_close_ int fd_close = -EBADF; + int r; + + assert(i); + + if (IMAGE_IS_VENDOR(i) || IMAGE_IS_HOST(i)) + return -EROFS; + + if (i->type != IMAGE_SUBVOLUME) + return -EOPNOTSUPP; + + if (fd < 0) { + fd_close = open(i->path, O_CLOEXEC|O_NOCTTY|O_DIRECTORY); + if (fd_close < 0) + return -errno; + fd = fd_close; + } + + r = btrfs_quota_scan_ongoing(fd); + if (r < 0) + return r; + if (r > 0) + return 0; + + BtrfsQuotaInfo quota; + r = btrfs_subvol_get_subtree_quota_fd(fd, 0, "a); + if (r < 0) + return r; + + i->usage = quota.referenced; + i->usage_exclusive = quota.exclusive; + i->limit = quota.referenced_max; + i->limit_exclusive = quota.exclusive_max; + + return 1; +} + static int image_make( const char *pretty, int dfd, @@ -288,19 +326,7 @@ static int image_make( if (r < 0) return r; - if (btrfs_quota_scan_ongoing(fd) == 0) { - BtrfsQuotaInfo quota; - - r = btrfs_subvol_get_subtree_quota_fd(fd, 0, "a); - if (r >= 0) { - (*ret)->usage = quota.referenced; - (*ret)->usage_exclusive = quota.exclusive; - - (*ret)->limit = quota.referenced_max; - (*ret)->limit_exclusive = quota.exclusive_max; - } - } - + (void) image_update_quota(*ret, fd); return 0; } } @@ -1002,6 +1028,7 @@ int image_read_only(Image *i, bool b) { return -EOPNOTSUPP; } + i->read_only = b; return 0; } @@ -1100,6 +1127,8 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile } int image_set_limit(Image *i, uint64_t referenced_max) { + int r; + assert(i); if (IMAGE_IS_VENDOR(i) || IMAGE_IS_HOST(i)) @@ -1115,7 +1144,12 @@ int image_set_limit(Image *i, uint64_t referenced_max) { (void) btrfs_qgroup_set_limit(i->path, 0, referenced_max); (void) btrfs_subvol_auto_qgroup(i->path, 0, true); - return btrfs_subvol_set_subtree_quota_limit(i->path, 0, referenced_max); + r = btrfs_subvol_set_subtree_quota_limit(i->path, 0, referenced_max); + if (r < 0) + return r; + + (void) image_update_quota(i, -EBADF); + return 0; } int image_read_metadata(Image *i) { diff --git a/src/shared/journal-importer.c b/src/shared/journal-importer.c index d9eabec..1914e74 100644 --- a/src/shared/journal-importer.c +++ b/src/shared/journal-importer.c @@ -92,7 +92,12 @@ static int get_line(JournalImporter *imp, char **line, size_t *size) { imp->buf + imp->filled, MALLOC_SIZEOF_SAFE(imp->buf) - imp->filled); if (n < 0) { - if (errno != EAGAIN) + if (ERRNO_IS_DISCONNECT(errno)) { + log_debug_errno(errno, "Got disconnect for importer %s.", strna(imp->name)); + return 0; + } + + if (!ERRNO_IS_TRANSIENT(errno)) log_error_errno(errno, "read(%d, ..., %zu): %m", imp->fd, MALLOC_SIZEOF_SAFE(imp->buf) - imp->filled); @@ -133,7 +138,12 @@ static int fill_fixed_size(JournalImporter *imp, void **data, size_t size) { n = read(imp->fd, imp->buf + imp->filled, MALLOC_SIZEOF_SAFE(imp->buf) - imp->filled); if (n < 0) { - if (errno != EAGAIN) + if (ERRNO_IS_DISCONNECT(errno)) { + log_debug_errno(errno, "Got disconnect for importer %s.", strna(imp->name)); + return 0; + } + + if (!ERRNO_IS_TRANSIENT(errno)) log_error_errno(errno, "read(%d, ..., %zu): %m", imp->fd, MALLOC_SIZEOF_SAFE(imp->buf) - imp->filled); return -errno; diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index 81e6f17..5ccf75a 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -114,7 +114,7 @@ static char* systemd_crypt_ra(const char *phrase, const char *setting, void **da if (!*data) { *data = new0(struct crypt_data, 1); if (!*data) { - errno = -ENOMEM; + errno = ENOMEM; return NULL; } diff --git a/src/shared/ptyfwd.c b/src/shared/ptyfwd.c index 6ffe86e..21cfe1d 100644 --- a/src/shared/ptyfwd.c +++ b/src/shared/ptyfwd.c @@ -406,6 +406,9 @@ int pty_forward_new( struct winsize ws; int r; + assert(master >= 0); + assert(ret); + f = new(PTYForward, 1); if (!f) return -ENOMEM; diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index 7084e70..6974ad8 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include <fcntl.h> +#include <sysexits.h> #include <sys/socket.h> #include <sys/stat.h> @@ -85,6 +86,7 @@ TEST(tmpdir) { static void test_shareable_ns(unsigned long nsflag) { _cleanup_close_pair_ int s[2] = { -1, -1 }; + bool permission_denied = false; pid_t pid1, pid2, pid3; int r, n = 0; siginfo_t si; @@ -101,8 +103,8 @@ static void test_shareable_ns(unsigned long nsflag) { if (pid1 == 0) { r = setup_shareable_ns(s, nsflag); - assert_se(r >= 0); - _exit(r); + assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r)); + _exit(r >= 0 ? r : EX_NOPERM); } pid2 = fork(); @@ -110,8 +112,8 @@ static void test_shareable_ns(unsigned long nsflag) { if (pid2 == 0) { r = setup_shareable_ns(s, nsflag); - assert_se(r >= 0); - exit(r); + assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r)); + _exit(r >= 0 ? r : EX_NOPERM); } pid3 = fork(); @@ -119,24 +121,38 @@ static void test_shareable_ns(unsigned long nsflag) { if (pid3 == 0) { r = setup_shareable_ns(s, nsflag); - assert_se(r >= 0); - exit(r); + assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r)); + _exit(r >= 0 ? r : EX_NOPERM); } r = wait_for_terminate(pid1, &si); assert_se(r >= 0); assert_se(si.si_code == CLD_EXITED); - n += si.si_status; + if (si.si_status == EX_NOPERM) + permission_denied = true; + else + n += si.si_status; r = wait_for_terminate(pid2, &si); assert_se(r >= 0); assert_se(si.si_code == CLD_EXITED); - n += si.si_status; + if (si.si_status == EX_NOPERM) + permission_denied = true; + else + n += si.si_status; r = wait_for_terminate(pid3, &si); assert_se(r >= 0); assert_se(si.si_code == CLD_EXITED); - n += si.si_status; + if (si.si_status == EX_NOPERM) + permission_denied = true; + else + n += si.si_status; + + /* LSMs can cause setup_shareable_ns() to fail with permission denied, do not fail the test in that + * case (e.g.: LXC with AppArmor on kernel < v6.2). */ + if (permission_denied) + return (void) log_tests_skipped("insufficient privileges"); assert_se(n == 1); } diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index a186246..b44e572 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -2169,7 +2169,7 @@ static int item_do( de_fd = openat(fd, de->d_name, O_NOFOLLOW|O_CLOEXEC|O_PATH); if (de_fd < 0) { - if (errno != -ENOENT) + if (errno != ENOENT) q = log_error_errno(errno, "Failed to open file '%s': %m", de->d_name); } else { _cleanup_free_ char *de_path = NULL; diff --git a/test/networkd-test.py b/test/networkd-test.py index 84013e7..ebb5553 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -1037,13 +1037,16 @@ DNS=127.0.0.1 self.create_iface(dhcpserver_opts='EmitTimezone=yes\nTimezone=Pacific/Honolulu') self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=false\n[DHCP]\nUseTimezone=true', dhcp_mode='ipv4') - # should have applied the received timezone - try: - self.assertEqual(get_tz(), 'Pacific/Honolulu') - except AssertionError: + # Should have applied the received timezone. This is asynchronous, so we need to wait for a while: + for _ in range(20): + tz = get_tz() + if tz == 'Pacific/Honolulu': + break + time.sleep(0.5) + else: self.show_journal('systemd-networkd.service') - self.show_journal('systemd-hostnamed.service') - raise + self.show_journal('systemd-timedated.service') + self.fail(f'Timezone: {tz}, expected: Pacific/Honolulu') class MatchClientTest(unittest.TestCase, NetworkdTestingUtilities): diff --git a/test/test-functions b/test/test-functions index 097babf..976b172 100644 --- a/test/test-functions +++ b/test/test-functions @@ -183,6 +183,7 @@ BASICTOOLS=( lz4cat mkfifo mktemp + modinfo modprobe mount mountpoint diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index a9286e9..d84350b 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -59,6 +59,7 @@ asan_options = None lsan_options = None ubsan_options = None with_coverage = False +show_journal = True # When true, show journal on stopping networkd. active_units = [] protected_links = { @@ -166,8 +167,10 @@ def expectedFailureIfRoutingPolicyPortRangeIsNotAvailable(): def expectedFailureIfRoutingPolicyIPProtoIsNotAvailable(): def f(func): - rc = call_quiet('ip rule add not from 192.168.100.19 ipproto tcp table 7') - call_quiet('ip rule del not from 192.168.100.19 ipproto tcp table 7') + # IP protocol name is parsed by getprotobyname(), and it requires /etc/protocols. + # Hence. here we use explicit number: 6 == tcp. + rc = call_quiet('ip rule add not from 192.168.100.19 ipproto 6 table 7') + call_quiet('ip rule del not from 192.168.100.19 ipproto 6 table 7') return func if rc == 0 else unittest.expectedFailure(func) return f @@ -247,6 +250,22 @@ def expectedFailureIfNetdevsimWithSRIOVIsNotAvailable(): return f +def expectedFailureIfKernelReturnsInvalidFlags(): + ''' + This checks the kernel bug caused by 3ddc2231c8108302a8229d3c5849ee792a63230d. + It will be fixed by the following patch: + https://patchwork.kernel.org/project/netdevbpf/patch/20240510072932.2678952-1-edumazet@google.com/ + ''' + def f(func): + call_quiet('ip link add dummy98 type dummy') + call_quiet('ip link set up dev dummy98') + call_quiet('ip address add 192.0.2.1/24 dev dummy98 noprefixroute') + output = check_output('ip address show dev dummy98') + remove_link('dummy98') + return func if 'noprefixroute' in output else unittest.expectedFailure(func) + + return f + # pylint: disable=C0415 def compare_kernel_version(min_kernel_version): try: @@ -636,6 +655,8 @@ def read_networkd_log(invocation_id=None): return check_output('journalctl _SYSTEMD_INVOCATION_ID=' + invocation_id) def stop_networkd(show_logs=True): + global show_journal + show_logs = show_logs and show_journal if show_logs: invocation_id = networkd_invocation_id() check_output('systemctl stop systemd-networkd.socket') @@ -647,6 +668,8 @@ def start_networkd(): check_output('systemctl start systemd-networkd') def restart_networkd(show_logs=True): + global show_journal + show_logs = show_logs and show_journal if show_logs: invocation_id = networkd_invocation_id() check_output('systemctl restart systemd-networkd.service') @@ -1319,6 +1342,7 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities): print(output) self.assertRegex(output, 'macvtap mode ' + mode + ' ') + @expectedFailureIfModuleIsNotAvailable('macvlan') def test_macvlan(self): first = True for mode in ['private', 'vepa', 'bridge', 'passthru']: @@ -2610,12 +2634,12 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): output = check_output('ip rule') print(output) - self.assertRegex(output, '111') - self.assertRegex(output, 'from 192.168.100.18') - self.assertRegex(output, '1123-1150') - self.assertRegex(output, '3224-3290') - self.assertRegex(output, 'tcp') - self.assertRegex(output, 'lookup 7') + self.assertIn('111:', output) + self.assertIn('from 192.168.100.18 ', output) + self.assertIn('sport 1123-1150 ', output) + self.assertIn('dport 3224-3290 ', output) + self.assertRegex(output, 'ipproto (tcp|ipproto-6) ') + self.assertIn('lookup 7 ', output) @expectedFailureIfRoutingPolicyIPProtoIsNotAvailable() def test_routing_policy_rule_invert(self): @@ -2625,10 +2649,11 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): output = check_output('ip rule') print(output) - self.assertRegex(output, '111') - self.assertRegex(output, 'not.*?from.*?192.168.100.18') - self.assertRegex(output, 'tcp') - self.assertRegex(output, 'lookup 7') + self.assertIn('111:', output) + self.assertIn('not ', output) + self.assertIn('from 192.168.100.18 ', output) + self.assertRegex(output, 'ipproto (tcp|ipproto-6) ') + self.assertIn('lookup 7 ', output) @expectedFailureIfRoutingPolicyUIDRangeIsNotAvailable() def test_routing_policy_rule_uidrange(self): @@ -2638,10 +2663,10 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): output = check_output('ip rule') print(output) - self.assertRegex(output, '111') - self.assertRegex(output, 'from 192.168.100.18') - self.assertRegex(output, 'lookup 7') - self.assertRegex(output, 'uidrange 100-200') + self.assertIn('111:', output) + self.assertIn('from 192.168.100.18 ', output) + self.assertIn('lookup 7 ', output) + self.assertIn('uidrange 100-200 ', output) def _test_route_static(self, manage_foreign_routes): if not manage_foreign_routes: @@ -4572,6 +4597,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): self.assertIn('DHCPREPLY(veth-peer)', output) self.assertNotIn('rapid-commit', output) + @expectedFailureIfKernelReturnsInvalidFlags() def test_dhcp_client_ipv4_only(self): copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client-ipv4-only.network') @@ -5658,6 +5684,7 @@ if __name__ == '__main__': parser.add_argument('--lsan-options', help='LSAN options', dest='lsan_options') parser.add_argument('--ubsan-options', help='UBSAN options', dest='ubsan_options') parser.add_argument('--with-coverage', help='Loosen certain sandbox restrictions to make gcov happy', dest='with_coverage', type=bool, nargs='?', const=True, default=with_coverage) + parser.add_argument('--no-journal', help='Do not show journal of systemd-networkd on stop', dest='show_journal', action='store_false') ns, unknown_args = parser.parse_known_args(namespace=unittest) if ns.build_dir: @@ -5707,6 +5734,7 @@ if __name__ == '__main__': lsan_options = ns.lsan_options ubsan_options = ns.ubsan_options with_coverage = ns.with_coverage + show_journal = ns.show_journal if use_valgrind: # Do not forget the trailing space. diff --git a/test/test-rpm-macros.sh b/test/test-rpm-macros.sh index c7107de..c9a45dc 100755 --- a/test/test-rpm-macros.sh +++ b/test/test-rpm-macros.sh @@ -137,7 +137,7 @@ for i in sysusers tmpfiles; do PKG_DATA_FILE="$(mktemp "$WORK_DIR/pkg-data-XXX")" EXP_OUT="$(mktemp "$WORK_DIR/exp-out-XXX.log")" - CONF_DIR="$(pkg-config --variable="${i}dir" systemd)" + CONF_DIR="$(PKG_CONFIG_PATH="${BUILD_DIR}/src/core" pkg-config --variable="${i}dir" systemd)" EXTRA_ARGS=() if [[ "$i" == tmpfiles ]]; then diff --git a/test/units/testsuite-38.sh b/test/units/testsuite-38.sh index c5f9bcc..35c4f1c 100755 --- a/test/units/testsuite-38.sh +++ b/test/units/testsuite-38.sh @@ -91,7 +91,7 @@ check_freezer_state() { # Ignore the intermediate freezing & thawing states in case we check # the unit state too quickly - [[ "$state" =~ ^(freezing|thawing)$ ]] || break + [[ "$state" =~ ^(freezing|thawing) ]] || break sleep .5 done diff --git a/test/units/testsuite-46.sh b/test/units/testsuite-46.sh index ec80b71..e41ca4c 100755 --- a/test/units/testsuite-46.sh +++ b/test/units/testsuite-46.sh @@ -20,7 +20,7 @@ inspect() { userdbctl user "$USERNAME" | tee /tmp/b # diff uses the grep BREs for pattern matching - diff -I '^\s*Disk \(Size\|Free\|Floor\|Ceiling\):' /tmp/{a,b} + diff -I '^\s*Disk \(Size\|Free\|Floor\|Ceiling\|Usage\):' /tmp/{a,b} rm /tmp/{a,b} homectl inspect --json=pretty "$USERNAME" diff --git a/test/units/testsuite-58.sh b/test/units/testsuite-58.sh index e369e58..5b72a49 100755 --- a/test/units/testsuite-58.sh +++ b/test/units/testsuite-58.sh @@ -283,7 +283,7 @@ $imgs/zzz6 : start= 4194264, size= 2097152, type=0FC63DAF-8483-4772-8E79 $imgs/zzz7 : start= 6291416, size= 98304, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=7B93D1F2-595D-4CE3-B0B9-837FBD9E63B0, name=\"luks-format-copy\"" loop="$(losetup -P --show --find "$imgs/zzz")" - udevadm wait --timeout 60 --settle "${loop:?}" + udevadm wait --timeout 60 --settle "${loop:?}p7" volume="test-repart-$RANDOM" diff --git a/test/units/testsuite-64.sh b/test/units/testsuite-64.sh index e9f352c..f0cfce3 100755 --- a/test/units/testsuite-64.sh +++ b/test/units/testsuite-64.sh @@ -577,9 +577,10 @@ EOF for ((i = 0; i < ${#devices[@]}; i++)); do # Intentionally use weaker cipher-related settings, since we don't care # about security here as it's a throwaway LUKS partition - cryptsetup luksFormat -q \ - --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ - --uuid "deadbeef-dead-dead-beef-11111111111$i" --label "encdisk$i" "${devices[$i]}" /etc/btrfs_keyfile + udevadm lock --device="${devices[$i]}" \ + cryptsetup luksFormat -q \ + --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + --uuid "deadbeef-dead-dead-beef-11111111111$i" --label "encdisk$i" "${devices[$i]}" /etc/btrfs_keyfile udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/deadbeef-dead-dead-beef-11111111111$i" "/dev/disk/by-label/encdisk$i" # Add the device into /etc/crypttab, reload systemd, and then activate # the device so we can create a filesystem on it later diff --git a/tmpfiles.d/systemd.conf.in b/tmpfiles.d/systemd.conf.in index 74f35cc..01cd7d8 100644 --- a/tmpfiles.d/systemd.conf.in +++ b/tmpfiles.d/systemd.conf.in @@ -26,13 +26,16 @@ Z /run/log/journal/%m ~2750 root systemd-journal - - {% if HAVE_ACL %} {% if ENABLE_ADM_GROUP and ENABLE_WHEEL_GROUP %} a+ /run/log/journal - - - - d:group::r-x,d:group:adm:r-x,d:group:wheel:r-x,group::r-x,group:adm:r-x,group:wheel:r-x -A+ /run/log/journal/%m - - - - d:group:adm:r-x,d:group:wheel:r-x,group:adm:r-X,group:wheel:r-X +a+ /run/log/journal/%m - - - - d:group:adm:r-x,d:group:wheel:r-x,group:adm:r-x,group:wheel:r-x +a+ /run/log/journal/%m/*.journal* - - - - group:adm:r--,group:wheel:r-- {% elif ENABLE_ADM_GROUP %} a+ /run/log/journal - - - - d:group::r-x,d:group:adm:r-x,group::r-x,group:adm:r-x -A+ /run/log/journal/%m - - - - d:group:adm:r-x,group:adm:r-X +a+ /run/log/journal/%m - - - - d:group:adm:r-x,group:adm:r-x +a+ /run/log/journal/%m/*.journal* - - - - group:adm:r-- {% elif ENABLE_WHEEL_GROUP %} a+ /run/log/journal - - - - d:group::r-x,d:group:wheel:r-x,group::r-x,group:wheel:r-x -A+ /run/log/journal/%m - - - - d:group:wheel:r-x,group:wheel:r-X +a+ /run/log/journal/%m - - - - d:group:wheel:r-x,group:wheel:r-x +a+ /run/log/journal/%m/*.journal* - - - - group:wheel:r-- {% endif %} {% endif %} |