From 3543456f84ec2e83e07b6c9bf2b3a1c5d30241d8 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Mon, 30 Sep 2024 09:58:34 -0700 Subject: [PATCH] Add ExtraFileDescriptor property to StartTransientUnit dbus API This adds the ExtraFileDescriptor property to StartTransient dbus API with format "a(hs)" - array of (file descriptor, name) pairs. The FD will be passed to the unit via sd_notify like Socket and OpenFile. systemctl show also shows ExtraFileDescriptorName for these transient units. We only show the name passed to dbus as the FD numbers will change once passed over the unix socket and are duplicated, so its confusing to display the numbers. We do not add this functionality for systemd-run or general systemd service units as it is not useful for general systemd services. Arguably, it could be useful for systemd-run in bash scripts but we prefer to be cautious and not expose the API yet. Fixes: #34396 --- man/org.freedesktop.systemd1.xml | 11 ++ src/core/dbus-service.c | 81 +++++++++++ src/core/exec-invoke.c | 9 +- src/core/execute-serialize.c | 30 +++- src/core/execute.c | 4 +- src/core/execute.h | 1 + src/core/fuzz-execute-serialize.c | 4 +- src/core/service.c | 137 ++++++++++++++---- src/core/service.h | 14 ++ src/systemctl/systemctl-show.c | 15 ++ .../TEST-23-UNIT-FILE.ExtraFileDescriptors.sh | 56 +++++++ 11 files changed, 316 insertions(+), 46 deletions(-) create mode 100755 test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index dd144a6cba0..3b7cad5b47f 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2790,6 +2790,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) OpenFile = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly as ExtraFileDescriptorNames = ['...', ...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i ReloadSignal = ...; readonly t ExecMainStartTimestamp = ...; readonly t ExecMainStartTimestampMonotonic = ...; @@ -4098,6 +4100,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4843,6 +4847,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { StateDirectory, CacheDirectory and LogsDirectory, which will create a symlink of the given name to the respective directory. The messages take an unused flags parameter, reserved for future backward-compatible changes. + + ExtraFileDescriptorNames contains file descriptor names passed to the service via + the ExtraFileDescriptors property in the StartTransientUnit() + method. See sd_listen_fds3 + for more details on how to retrieve these file descriptors. Unlike the ExtraFileDescriptors + input property, ExtraFileDescriptorNames only contains names and not the file descriptors. @@ -12209,6 +12219,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ LiveMountResult, PrivateTmpEx, ImportCredentialEx, + ExtraFileDescriptorNames, BindLogSockets, and PrivateUsersEx were added in version 257. diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c index 43a8fb06175..a0244c18607 100644 --- a/src/core/dbus-service.c +++ b/src/core/dbus-service.c @@ -69,6 +69,34 @@ static int property_get_open_files( return sd_bus_message_close_container(reply); } +static int property_get_extra_file_descriptors( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ServiceExtraFD **extra_fds = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "s"); + if (r < 0) + return r; + + LIST_FOREACH(extra_fd, efd, *extra_fds) { + r = sd_bus_message_append_basic(reply, 's', efd->fdname); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + static int property_get_exit_status_set( sd_bus *bus, const char *path, @@ -339,6 +367,7 @@ const sd_bus_vtable bus_service_vtable[] = { SD_BUS_PROPERTY("NRestarts", "u", bus_property_get_unsigned, offsetof(Service, n_restarts), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("OOMPolicy", "s", bus_property_get_oom_policy, offsetof(Service, oom_policy), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("OpenFile", "a(sst)", property_get_open_files, offsetof(Service, open_files), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("ExtraFileDescriptorNames", "as", property_get_extra_file_descriptors, offsetof(Service, extra_fds), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ReloadSignal", "i", bus_property_get_int, offsetof(Service, reload_signal), SD_BUS_VTABLE_PROPERTY_CONST), BUS_EXEC_STATUS_VTABLE("ExecMain", offsetof(Service, main_exec_status), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), @@ -718,6 +747,58 @@ static int bus_service_set_transient_property( if (streq(name, "ReloadSignal")) return bus_set_transient_reload_signal(u, name, &s->reload_signal, message, flags, error); + if (streq(name, "ExtraFileDescriptors")) { + int fd; + const char *fdname; + + r = sd_bus_message_enter_container(message, 'a', "(hs)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_(service_extra_fd_freep) ServiceExtraFD *efd = NULL; + + r = sd_bus_message_read(message, "(hs)", &fd, &fdname); + if (r < 0) + return r; + if (r == 0) + break; + + /* Disallow empty string for ExtraFileDescriptors. + * Unlike OpenFile, StandardInput and friends, there isn't a good sane + * default for an arbitrary FD. */ + if (fd < 0 || isempty(fdname) || !fdname_is_valid(fdname)) + return -EINVAL; + + if (UNIT_WRITE_FLAGS_NOOP(flags)) + continue; + + efd = new(ServiceExtraFD, 1); + if (!efd) + return -ENOMEM; + + *efd = (ServiceExtraFD) { + .fd = -EBADF, + .fdname = strdup(fdname), + }; + + if (!efd->fdname) + return -ENOMEM; + + efd->fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (efd->fd < 0) + return -errno; + + LIST_APPEND(extra_fd, s->extra_fds, TAKE_PTR(efd)); + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + return 1; + } + return 0; } diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index becc2b92ef8..2bd43a95ddb 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -3912,7 +3912,7 @@ static int exec_context_named_iofds( for (size_t i = 0; i < 3; i++) stdio_fdname[i] = exec_context_fdname(c, i); - n_fds = p->n_storage_fds + p->n_socket_fds; + n_fds = p->n_storage_fds + p->n_socket_fds + p->n_extra_fds; for (size_t i = 0; i < n_fds && targets > 0; i++) if (named_iofds[STDIN_FILENO] < 0 && @@ -4096,7 +4096,7 @@ int exec_invoke( int ngids_after_pam = 0; int socket_fd = -EBADF, named_iofds[3] = EBADF_TRIPLET; - size_t n_storage_fds, n_socket_fds; + size_t n_storage_fds, n_socket_fds, n_extra_fds; assert(command); assert(context); @@ -4133,12 +4133,13 @@ int exec_invoke( return log_exec_error_errno(context, params, SYNTHETIC_ERRNO(EINVAL), "Got no socket."); socket_fd = params->fds[0]; - n_storage_fds = n_socket_fds = 0; + n_storage_fds = n_socket_fds = n_extra_fds = 0; } else { n_socket_fds = params->n_socket_fds; n_storage_fds = params->n_storage_fds; + n_extra_fds = params->n_extra_fds; } - n_fds = n_socket_fds + n_storage_fds; + n_fds = n_socket_fds + n_storage_fds + n_extra_fds; r = exec_context_named_iofds(context, params, named_iofds); if (r < 0) diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index b3035c00267..13e7078b1a9 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -1282,7 +1282,13 @@ static int exec_parameters_serialize(const ExecParameters *p, const ExecContext return r; } - r = serialize_fd_many(f, fds, "exec-parameters-fds", p->fds, p->n_socket_fds + p->n_storage_fds); + if (p->n_extra_fds > 0) { + r = serialize_item_format(f, "exec-parameters-n-extra-fds", "%zu", p->n_extra_fds); + if (r < 0) + return r; + } + + r = serialize_fd_many(f, fds, "exec-parameters-fds", p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); if (r < 0) return r; } @@ -1478,27 +1484,37 @@ static int exec_parameters_deserialize(ExecParameters *p, FILE *f, FDSet *fds) { if (p->n_storage_fds > (size_t) nr_open) return -EINVAL; /* too many, someone is playing games with us */ + } else if ((val = startswith(l, "exec-parameters-n-extra-fds="))) { + if (p->fds) + return -EINVAL; /* Already received */ + + r = safe_atozu(val, &p->n_extra_fds); + if (r < 0) + return r; + + if (p->n_extra_fds > (size_t) nr_open) + return -EINVAL; /* too many, someone is playing games with us */ } else if ((val = startswith(l, "exec-parameters-fds="))) { - if (p->n_socket_fds + p->n_storage_fds == 0) + if (p->n_socket_fds + p->n_storage_fds + p->n_extra_fds == 0) return log_warning_errno( SYNTHETIC_ERRNO(EINVAL), "Got exec-parameters-fds= without " - "prior exec-parameters-n-socket-fds= or exec-parameters-n-storage-fds="); - if (p->n_socket_fds + p->n_storage_fds > (size_t) nr_open) + "prior exec-parameters-n-socket-fds= or exec-parameters-n-storage-fds= or exec-parameters-n-extra-fds="); + if (p->n_socket_fds + p->n_storage_fds + p->n_extra_fds > (size_t) nr_open) return -EINVAL; /* too many, someone is playing games with us */ if (p->fds) return -EINVAL; /* duplicated */ - p->fds = new(int, p->n_socket_fds + p->n_storage_fds); + p->fds = new(int, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); if (!p->fds) return log_oom_debug(); /* Ensure we don't leave any FD uninitialized on error, it makes the fuzzer sad */ - FOREACH_ARRAY(i, p->fds, p->n_socket_fds + p->n_storage_fds) + FOREACH_ARRAY(i, p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds) *i = -EBADF; - r = deserialize_fd_many(fds, val, p->n_socket_fds + p->n_storage_fds, p->fds); + r = deserialize_fd_many(fds, val, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds, p->fds); if (r < 0) continue; diff --git a/src/core/execute.c b/src/core/execute.c index 30fcffcc5bd..0c2c278d69c 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -391,7 +391,7 @@ int exec_spawn( assert(context); assert(params); assert(!params->fds || FLAGS_SET(params->flags, EXEC_PASS_FDS)); - assert(params->fds || (params->n_socket_fds + params->n_storage_fds == 0)); + assert(params->fds || (params->n_socket_fds + params->n_storage_fds + params->n_extra_fds == 0)); assert(!params->files_env); /* We fill this field, ensure it comes NULL-initialized to us */ assert(ret); @@ -2632,7 +2632,7 @@ void exec_params_deep_clear(ExecParameters *p) { * to be fully cleaned up to make sanitizers and analyzers happy, as opposed as the shallow clean * function above. */ - close_many_unset(p->fds, p->n_socket_fds + p->n_storage_fds); + close_many_unset(p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); p->cgroup_path = mfree(p->cgroup_path); diff --git a/src/core/execute.h b/src/core/execute.h index 01a196748b5..29c08d48b9d 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -421,6 +421,7 @@ struct ExecParameters { char **fd_names; size_t n_socket_fds; size_t n_storage_fds; + size_t n_extra_fds; ExecFlags flags; bool selinux_context_net:1; diff --git a/src/core/fuzz-execute-serialize.c b/src/core/fuzz-execute-serialize.c index 5b2dc952add..05abee5a4e6 100644 --- a/src/core/fuzz-execute-serialize.c +++ b/src/core/fuzz-execute-serialize.c @@ -58,8 +58,8 @@ static void exec_fuzz_one(FILE *f, FDSet *fdset) { params.user_lookup_fd = -EBADF; params.bpf_restrict_fs_map_fd = -EBADF; if (!params.fds) - params.n_socket_fds = params.n_storage_fds = 0; - for (size_t i = 0; params.fds && i < params.n_socket_fds + params.n_storage_fds; i++) + params.n_socket_fds = params.n_storage_fds = params.n_extra_fds = 0; + for (size_t i = 0; params.fds && i < params.n_socket_fds + params.n_storage_fds + params.n_extra_fds; i++) params.fds[i] = -EBADF; exec_command_done_array(&command, /* n= */ 1); diff --git a/src/core/service.c b/src/core/service.c index e5db7a085b5..7162016313c 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -454,6 +454,21 @@ static void service_release_fd_store(Service *s) { assert(s->n_fd_store == 0); } +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *efd) { + if (!efd) + return NULL; + + efd->fd = asynchronous_close(efd->fd); + free(efd->fdname); + return mfree(efd); +} + +static void service_release_extra_fds(Service *s) { + assert(s); + + LIST_CLEAR(extra_fd, s->extra_fds, service_extra_fd_free); +} + static void service_release_stdio_fd(Service *s) { assert(s); @@ -510,6 +525,7 @@ static void service_done(Unit *u) { service_release_socket_fd(s); service_release_stdio_fd(s); service_release_fd_store(s); + service_release_extra_fds(s); s->mount_request = sd_bus_message_unref(s->mount_request); } @@ -903,40 +919,59 @@ static int service_load(Unit *u) { return service_verify(s); } +static int service_dump_fd(int fd, const char *fdname, const char *header, FILE *f, const char *prefix) { + _cleanup_free_ char *path = NULL; + struct stat st; + int flags; + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat fdstore entry: %m"); + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + return log_debug_errno(errno, "Failed to get fdstore entry flags: %m"); + + (void) fd_get_path(fd, &path); + + fprintf(f, + "%s%s '%s' (type=%s; dev=" DEVNUM_FORMAT_STR "; inode=%" PRIu64 "; rdev=" DEVNUM_FORMAT_STR "; path=%s; access=%s)\n", + prefix, + header, + fdname, + strna(inode_type_to_string(st.st_mode)), + DEVNUM_FORMAT_VAL(st.st_dev), + (uint64_t) st.st_ino, + DEVNUM_FORMAT_VAL(st.st_rdev), + strna(path), + strna(accmode_to_string(flags))); + + return 0; +} + static void service_dump_fdstore(Service *s, FILE *f, const char *prefix) { assert(s); assert(f); assert(prefix); - LIST_FOREACH(fd_store, i, s->fd_store) { - _cleanup_free_ char *path = NULL; - struct stat st; - int flags; - - if (fstat(i->fd, &st) < 0) { - log_debug_errno(errno, "Failed to stat fdstore entry: %m"); - continue; - } - - flags = fcntl(i->fd, F_GETFL); - if (flags < 0) { - log_debug_errno(errno, "Failed to get fdstore entry flags: %m"); - continue; - } + LIST_FOREACH(fd_store, i, s->fd_store) + (void) service_dump_fd(i->fd, + i->fdname, + i == s->fd_store ? "File Descriptor Store Entry:" : " ", + f, + prefix); +} - (void) fd_get_path(i->fd, &path); +static void service_dump_extra_fds(Service *s, FILE *f, const char *prefix) { + assert(s); + assert(f); + assert(prefix); - fprintf(f, - "%s%s '%s' (type=%s; dev=" DEVNUM_FORMAT_STR "; inode=%" PRIu64 "; rdev=" DEVNUM_FORMAT_STR "; path=%s; access=%s)\n", - prefix, i == s->fd_store ? "File Descriptor Store Entry:" : " ", - i->fdname, - strna(inode_type_to_string(st.st_mode)), - DEVNUM_FORMAT_VAL(st.st_dev), - (uint64_t) st.st_ino, - DEVNUM_FORMAT_VAL(st.st_rdev), - strna(path), - strna(accmode_to_string(flags))); - } + LIST_FOREACH(extra_fd, i, s->extra_fds) + (void) service_dump_fd(i->fd, + i->fdname, + i == s->extra_fds ? "Extra File Descriptor Entry:" : " ", + f, + prefix); } static void service_dump(Unit *u, FILE *f, const char *prefix) { @@ -1093,6 +1128,8 @@ static void service_dump(Unit *u, FILE *f, const char *prefix) { fprintf(f, "%sOpen File: %s\n", prefix, ofs); } + service_dump_extra_fds(s, f, prefix); + cgroup_context_dump(UNIT(s), f, prefix); } @@ -1423,11 +1460,12 @@ static int service_collect_fds( int **fds, char ***fd_names, size_t *n_socket_fds, - size_t *n_storage_fds) { + size_t *n_storage_fds, + size_t *n_extra_fds) { _cleanup_strv_free_ char **rfd_names = NULL; _cleanup_free_ int *rfds = NULL; - size_t rn_socket_fds = 0, rn_storage_fds = 0; + size_t rn_socket_fds = 0, rn_storage_fds = 0, rn_extra_fds = 0; int r; assert(s); @@ -1435,6 +1473,7 @@ static int service_collect_fds( assert(fd_names); assert(n_socket_fds); assert(n_storage_fds); + assert(n_extra_fds); if (s->socket_fd >= 0) { Socket *sock = ASSERT_PTR(SOCKET(UNIT_DEREF(s->accept_socket))); @@ -1512,10 +1551,44 @@ static int service_collect_fds( rfd_names[n_fds] = NULL; } + LIST_FOREACH(extra_fd, extra_fd, s->extra_fds) + rn_extra_fds++; + + if (rn_extra_fds > 0) { + size_t n_fds; + char **nl; + int *t; + + t = reallocarray(rfds, rn_socket_fds + rn_storage_fds + rn_extra_fds, sizeof(int)); + if (!t) + return -ENOMEM; + + rfds = t; + + nl = reallocarray(rfd_names, rn_socket_fds + rn_storage_fds + rn_extra_fds + 1, sizeof(char *)); + if (!nl) + return -ENOMEM; + + rfd_names = nl; + n_fds = rn_socket_fds + rn_storage_fds; + + LIST_FOREACH(extra_fd, extra_fd, s->extra_fds) { + rfds[n_fds] = extra_fd->fd; + rfd_names[n_fds] = strdup(extra_fd->fdname); + if (!rfd_names[n_fds]) + return -ENOMEM; + + n_fds++; + } + + rfd_names[n_fds] = NULL; + } + *fds = TAKE_PTR(rfds); *fd_names = TAKE_PTR(rfd_names); *n_socket_fds = rn_socket_fds; *n_storage_fds = rn_storage_fds; + *n_extra_fds = rn_extra_fds; return 0; } @@ -1714,7 +1787,8 @@ static int service_spawn_internal( &exec_params.fds, &exec_params.fd_names, &exec_params.n_socket_fds, - &exec_params.n_storage_fds); + &exec_params.n_storage_fds, + &exec_params.n_extra_fds); if (r < 0) return r; @@ -1722,7 +1796,7 @@ static int service_spawn_internal( exec_params.flags |= EXEC_PASS_FDS; - log_unit_debug(UNIT(s), "Passing %zu fds to service", exec_params.n_socket_fds + exec_params.n_storage_fds); + log_unit_debug(UNIT(s), "Passing %zu fds to service", exec_params.n_socket_fds + exec_params.n_storage_fds + exec_params.n_extra_fds); } if (!FLAGS_SET(exec_params.flags, EXEC_IS_CONTROL) && s->type == SERVICE_EXEC) { @@ -5288,6 +5362,7 @@ static void service_release_resources(Unit *u) { service_release_socket_fd(s); service_release_stdio_fd(s); + service_release_extra_fds(s); if (s->fd_store_preserve_mode != EXEC_PRESERVE_YES) service_release_fd_store(s); diff --git a/src/core/service.h b/src/core/service.h index 6a0c4929922..97c217209e2 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -3,6 +3,7 @@ typedef struct Service Service; typedef struct ServiceFDStore ServiceFDStore; +typedef struct ServiceExtraFD ServiceExtraFD; #include "exit-status.h" #include "kill.h" @@ -111,6 +112,13 @@ struct ServiceFDStore { LIST_FIELDS(ServiceFDStore, fd_store); }; +struct ServiceExtraFD { + int fd; + char *fdname; + + LIST_FIELDS(ServiceExtraFD, extra_fd); +}; + struct Service { Unit meta; @@ -231,6 +239,9 @@ struct Service { LIST_HEAD(OpenFile, open_files); + /* If service spawned from transient unit, extra file descriptors can be passed via dbus API */ + LIST_HEAD(ServiceExtraFD, extra_fds); + int reload_signal; usec_t reload_begin_usec; @@ -295,3 +306,6 @@ DEFINE_CAST(SERVICE, Service); /* Only exported for unit tests */ int service_deserialize_exec_command(Unit *u, const char *key, const char *value); + +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *efd); +DEFINE_TRIVIAL_CLEANUP_FUNC(ServiceExtraFD*, service_extra_fd_free); diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index 0f954b89b3f..6f3fd766227 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -2017,6 +2017,21 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m if (r < 0) return bus_log_parse_error(r); + return 1; + } else if (streq(name, "ExtraFileDescriptorNames")) { + _cleanup_strv_free_ char **extra_fd_names = NULL; + _cleanup_free_ char *joined = NULL; + + r = sd_bus_message_read_strv(m, &extra_fd_names); + if (r < 0) + return bus_log_parse_error(r); + + joined = strv_join(extra_fd_names, " "); + if (!joined) + return log_oom(); + + bus_print_property_value(name, expected_value, flags, joined); + return 1; } diff --git a/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh new file mode 100755 index 00000000000..cf5c2409086 --- /dev/null +++ b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + set +e + + rm -rf /tmp/test-extra-fd/ +} + +trap at_exit EXIT + +mkdir /tmp/test-extra-fd +echo "Hello" > /tmp/test-extra-fd/1.txt +echo "Extra" > /tmp/test-extra-fd/2.txt + +systemd-analyze log-level debug + +# Open files and assign FD to variables +exec {TEST_FD1}