From 2038c3f58486ea5da1a2fbf2fdb898f9fd6f8cae Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 27 Oct 2017 16:09:57 +0200 Subject: [PATCH] core: add support for StandardInputFile= and friends These new settings permit specifiying arbitrary paths as stdin/stdout/stderr locations. We try to open/create them as necessary. Some special magic is applied: 1) if the same path is specified for both input and output/stderr, we'll open it only once O_RDWR, and duplicate them fd instead. 2) If we an AF_UNIX socket path is specified, we'll connect() to it, rather than open() it. This allows invoking systemd services with stdin/stdout/stderr connected to arbitrary foreign service sockets. Fixes: #3991 --- src/core/dbus-execute.c | 44 ++++++++++++ src/core/execute.c | 88 ++++++++++++++++++++++- src/core/execute.h | 3 + src/core/load-fragment.c | 173 +++++++++++++++++++++++++++++---------------- src/shared/bus-unit-util.c | 17 ++++- 5 files changed, 263 insertions(+), 62 deletions(-) diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 7a9d027..4744630 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -1853,6 +1853,50 @@ int bus_exec_context_set_transient_property( return 1; + } else if (STR_IN_SET(name, "StandardInputFile", "StandardOutputFile", "StandardErrorFile")) { + const char *s; + + r = sd_bus_message_read(message, "s", &s); + if (r < 0) + return r; + + if (!path_is_absolute(s)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Path %s is not absolute", s); + if (!path_is_safe(s)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Path %s is not normalized", s); + + if (mode != UNIT_CHECK) { + + if (streq(name, "StandardInputFile")) { + r = free_and_strdup(&c->stdio_file[STDIN_FILENO], s); + if (r < 0) + return r; + + c->std_input = EXEC_INPUT_FILE; + unit_write_drop_in_private_format(u, mode, name, "StandardInput=file:%s", s); + + } else if (streq(name, "StandardOutputFile")) { + r = free_and_strdup(&c->stdio_file[STDOUT_FILENO], s); + if (r < 0) + return r; + + c->std_output = EXEC_OUTPUT_FILE; + unit_write_drop_in_private_format(u, mode, name, "StandardOutput=file:%s", s); + + } else { + assert(streq(name, "StandardErrorFile")); + + r = free_and_strdup(&c->stdio_file[STDERR_FILENO], s); + if (r < 0) + return r; + + c->std_error = EXEC_OUTPUT_FILE; + unit_write_drop_in_private_format(u, mode, name, "StandardError=file:%s", s); + } + } + + return 1; + } else if (streq(name, "StandardInputData")) { const void *p; size_t sz; diff --git a/src/core/execute.c b/src/core/execute.c index 7018322..7d6919f 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -390,6 +390,53 @@ static int open_terminal_as(const char *path, int flags, int nfd) { return move_fd(fd, nfd, false); } +static int acquire_path(const char *path, int flags, mode_t mode) { + union sockaddr_union sa = { + .sa.sa_family = AF_UNIX, + }; + int fd, r; + + assert(path); + + if (IN_SET(flags & O_ACCMODE, O_WRONLY, O_RDWR)) + flags |= O_CREAT; + + fd = open(path, flags|O_NOCTTY, mode); + if (fd >= 0) + return fd; + + if (errno != ENXIO) /* ENXIO is returned when we try to open() an AF_UNIX file system socket on Linux */ + return -errno; + if (strlen(path) > sizeof(sa.un.sun_path)) /* Too long, can't be a UNIX socket */ + return -ENXIO; + + /* So, it appears the specified path could be an AF_UNIX socket. Let's see if we can connect to it. */ + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return -errno; + + strncpy(sa.un.sun_path, path, sizeof(sa.un.sun_path)); + if (connect(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) { + safe_close(fd); + return errno == EINVAL ? -ENXIO : -errno; /* Propagate initial error if we get EINVAL, i.e. we have + * indication that his wasn't an AF_UNIX socket after all */ + } + + if ((flags & O_ACCMODE) == O_RDONLY) + r = shutdown(fd, SHUT_WR); + else if ((flags & O_ACCMODE) == O_WRONLY) + r = shutdown(fd, SHUT_RD); + else + return fd; + if (r < 0) { + safe_close(fd); + return -errno; + } + + return fd; +} + static int fixup_input( const ExecContext *context, int socket_fd, @@ -489,6 +536,22 @@ static int setup_input( return move_fd(fd, STDIN_FILENO, false); } + case EXEC_INPUT_FILE: { + bool rw; + int fd; + + assert(context->stdio_file[STDIN_FILENO]); + + rw = (context->std_output == EXEC_OUTPUT_FILE && streq_ptr(context->stdio_file[STDIN_FILENO], context->stdio_file[STDOUT_FILENO])) || + (context->std_error == EXEC_OUTPUT_FILE && streq_ptr(context->stdio_file[STDIN_FILENO], context->stdio_file[STDERR_FILENO])); + + fd = acquire_path(context->stdio_file[STDIN_FILENO], rw ? O_RDWR : O_RDONLY, 0666 & ~context->umask); + if (fd < 0) + return fd; + + return move_fd(fd, STDIN_FILENO, false); + } + default: assert_not_reached("Unknown input type"); } @@ -625,6 +688,25 @@ static int setup_output( (void) fd_nonblock(named_iofds[fileno], false); return dup2(named_iofds[fileno], fileno) < 0 ? -errno : fileno; + case EXEC_OUTPUT_FILE: { + bool rw; + int fd; + + assert(context->stdio_file[fileno]); + + rw = context->std_input == EXEC_INPUT_FILE && + streq_ptr(context->stdio_file[fileno], context->stdio_file[STDIN_FILENO]); + + if (rw) + return dup2(STDIN_FILENO, fileno) < 0 ? -errno : fileno; + + fd = acquire_path(context->stdio_file[fileno], O_WRONLY, 0666 & ~context->umask); + if (fd < 0) + return fd; + + return move_fd(fd, fileno, false); + } + default: assert_not_reached("Unknown error type"); } @@ -3507,8 +3589,10 @@ void exec_context_done(ExecContext *c) { for (l = 0; l < ELEMENTSOF(c->rlimit); l++) c->rlimit[l] = mfree(c->rlimit[l]); - for (l = 0; l < 3; l++) + for (l = 0; l < 3; l++) { c->stdio_fdname[l] = mfree(c->stdio_fdname[l]); + c->stdio_file[l] = mfree(c->stdio_file[l]); + } c->working_directory = mfree(c->working_directory); c->root_directory = mfree(c->root_directory); @@ -4648,6 +4732,7 @@ static const char* const exec_input_table[_EXEC_INPUT_MAX] = { [EXEC_INPUT_SOCKET] = "socket", [EXEC_INPUT_NAMED_FD] = "fd", [EXEC_INPUT_DATA] = "data", + [EXEC_INPUT_FILE] = "file", }; DEFINE_STRING_TABLE_LOOKUP(exec_input, ExecInput); @@ -4664,6 +4749,7 @@ static const char* const exec_output_table[_EXEC_OUTPUT_MAX] = { [EXEC_OUTPUT_JOURNAL_AND_CONSOLE] = "journal+console", [EXEC_OUTPUT_SOCKET] = "socket", [EXEC_OUTPUT_NAMED_FD] = "fd", + [EXEC_OUTPUT_FILE] = "file", }; DEFINE_STRING_TABLE_LOOKUP(exec_output, ExecOutput); diff --git a/src/core/execute.h b/src/core/execute.h index d1cf7dc..2ae38aa 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -55,6 +55,7 @@ typedef enum ExecInput { EXEC_INPUT_SOCKET, EXEC_INPUT_NAMED_FD, EXEC_INPUT_DATA, + EXEC_INPUT_FILE, _EXEC_INPUT_MAX, _EXEC_INPUT_INVALID = -1 } ExecInput; @@ -71,6 +72,7 @@ typedef enum ExecOutput { EXEC_OUTPUT_JOURNAL_AND_CONSOLE, EXEC_OUTPUT_SOCKET, EXEC_OUTPUT_NAMED_FD, + EXEC_OUTPUT_FILE, _EXEC_OUTPUT_MAX, _EXEC_OUTPUT_INVALID = -1 } ExecOutput; @@ -165,6 +167,7 @@ struct ExecContext { ExecOutput std_output; ExecOutput std_error; char *stdio_fdname[3]; + char *stdio_file[3]; void *stdin_data; size_t stdin_data_size; diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index cff3779..37f0229 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -847,7 +847,9 @@ int config_parse_exec_input( void *userdata) { ExecContext *c = data; - const char *name; + Unit *u = userdata; + const char *n; + ExecInput ei; int r; assert(data); @@ -855,31 +857,55 @@ int config_parse_exec_input( assert(line); assert(rvalue); - name = startswith(rvalue, "fd:"); - if (name) { - /* Strip prefix and validate fd name */ - if (!fdname_is_valid(name)) { - log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid file descriptor name, ignoring: %s", name); - return 0; + n = startswith(rvalue, "fd:"); + if (n) { + _cleanup_free_ char *resolved = NULL; + + r = unit_full_printf(u, n, &resolved); + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve unit specifiers on %s: %m", n); + + if (isempty(resolved)) + resolved = mfree(resolved); + else if (!fdname_is_valid(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid file descriptor name: %s", resolved); + return -EINVAL; } - r = free_and_strdup(&c->stdio_fdname[STDIN_FILENO], name); + free_and_replace(c->stdio_fdname[STDIN_FILENO], resolved); + + ei = EXEC_INPUT_NAMED_FD; + + } else if ((n = startswith(rvalue, "file:"))) { + _cleanup_free_ char *resolved = NULL; + + r = unit_full_printf(u, n, &resolved); if (r < 0) - return log_oom(); + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve unit specifiers on %s: %m", n); - c->std_input = EXEC_INPUT_NAMED_FD; - } else { - ExecInput ei; + if (!path_is_absolute(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "file: requires an absolute path name: %s", resolved); + return -EINVAL; + } + + if (!path_is_safe(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "file: requires a normalized path name: %s", resolved); + return -EINVAL; + } + + free_and_replace(c->stdio_file[STDIN_FILENO], resolved); + ei = EXEC_INPUT_FILE; + + } else { ei = exec_input_from_string(rvalue); if (ei < 0) { log_syntax(unit, LOG_ERR, filename, line, 0, "Failed to parse input specifier, ignoring: %s", rvalue); return 0; } - - c->std_input = ei; } + c->std_input = ei; return 0; } @@ -915,22 +941,18 @@ int config_parse_exec_input_text( } r = cunescape(rvalue, 0, &unescaped); - if (r < 0) { - log_syntax(unit, LOG_ERR, filename, line, r, "Failed to decode C escaped text, ignoring: %s", rvalue); - return 0; - } + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to decode C escaped text: %s", rvalue); r = unit_full_printf(u, unescaped, &resolved); - if (r < 0) { - log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve specifiers, ignoring: %s", unescaped); - return 0; - } + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve specifiers: %s", unescaped); sz = strlen(resolved); if (c->stdin_data_size + sz + 1 < c->stdin_data_size || /* check for overflow */ c->stdin_data_size + sz + 1 > EXEC_STDIN_DATA_MAX) { - log_syntax(unit, LOG_ERR, filename, line, r, "Standard input data too large (%zu), maximum of %zu permitted, ignoring.", c->stdin_data_size + sz, (size_t) EXEC_STDIN_DATA_MAX); - return 0; + log_syntax(unit, LOG_ERR, filename, line, 0, "Standard input data too large (%zu), maximum of %zu permitted, ignoring.", c->stdin_data_size + sz, (size_t) EXEC_STDIN_DATA_MAX); + return -E2BIG; } p = realloc(c->stdin_data, c->stdin_data_size + sz + 1); @@ -983,17 +1005,15 @@ int config_parse_exec_input_data( delete_chars(cleaned, WHITESPACE); r = unbase64mem(cleaned, (size_t) -1, &p, &sz); - if (r < 0) { - log_syntax(unit, LOG_ERR, filename, line, r, "Failed to decode base64 data, ignoring: %s", cleaned); - return 0; - } + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to decode base64 data, ignoring: %s", cleaned); assert(sz > 0); if (c->stdin_data_size + sz < c->stdin_data_size || /* check for overflow */ c->stdin_data_size + sz > EXEC_STDIN_DATA_MAX) { - log_syntax(unit, LOG_ERR, filename, line, r, "Standard input data too large (%zu), maximum of %zu permitted, ignoring.", c->stdin_data_size + sz, (size_t) EXEC_STDIN_DATA_MAX); - return 0; + log_syntax(unit, LOG_ERR, filename, line, 0, "Standard input data too large (%zu), maximum of %zu permitted, ignoring.", c->stdin_data_size + sz, (size_t) EXEC_STDIN_DATA_MAX); + return -E2BIG; } q = realloc(c->stdin_data, c->stdin_data_size + sz); @@ -1008,19 +1028,23 @@ int config_parse_exec_input_data( return 0; } -int config_parse_exec_output(const char *unit, - const char *filename, - unsigned line, - const char *section, - unsigned section_line, - const char *lvalue, - int ltype, - const char *rvalue, - void *data, - void *userdata) { +int config_parse_exec_output( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *resolved = NULL; + const char *n; ExecContext *c = data; + Unit *u = userdata; ExecOutput eo; - const char *name; int r; assert(data); @@ -1029,38 +1053,67 @@ int config_parse_exec_output(const char *unit, assert(lvalue); assert(rvalue); - name = startswith(rvalue, "fd:"); - if (name) { - /* Strip prefix and validate fd name */ - if (!fdname_is_valid(name)) { - log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid file descriptor name, ignoring: %s", name); - return 0; + n = startswith(rvalue, "fd:"); + if (n) { + r = unit_full_printf(u, n, &resolved); + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve unit specifiers on %s: %m", n); + + if (isempty(resolved)) + resolved = mfree(resolved); + else if (!fdname_is_valid(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid file descriptor name: %s", resolved); + return -EINVAL; } + eo = EXEC_OUTPUT_NAMED_FD; + + } else if ((n = startswith(rvalue, "file:"))) { + + r = unit_full_printf(u, n, &resolved); + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve unit specifiers on %s: %m", n); + + if (!path_is_absolute(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "file: requires an absolute path name: %s", resolved); + return -EINVAL; + } + + if (!path_is_safe(resolved)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "file: requires a normalized path name, ignoring: %s", resolved); + return -EINVAL; + } + + eo = EXEC_OUTPUT_FILE; + } else { eo = exec_output_from_string(rvalue); - if (eo == _EXEC_OUTPUT_INVALID) { + if (eo < 0) { log_syntax(unit, LOG_ERR, filename, line, 0, "Failed to parse output specifier, ignoring: %s", rvalue); return 0; } } if (streq(lvalue, "StandardOutput")) { + if (eo == EXEC_OUTPUT_NAMED_FD) + free_and_replace(c->stdio_fdname[STDOUT_FILENO], resolved); + else + free_and_replace(c->stdio_file[STDOUT_FILENO], resolved); + c->std_output = eo; - r = free_and_strdup(&c->stdio_fdname[STDOUT_FILENO], name); - if (r < 0) - log_oom(); - return r; - } else if (streq(lvalue, "StandardError")) { - c->std_error = eo; - r = free_and_strdup(&c->stdio_fdname[STDERR_FILENO], name); - if (r < 0) - log_oom(); - return r; + } else { - log_syntax(unit, LOG_ERR, filename, line, 0, "Failed to parse output property, ignoring: %s", lvalue); - return 0; + assert(streq(lvalue, "StandardError")); + + if (eo == EXEC_OUTPUT_NAMED_FD) + free_and_replace(c->stdio_fdname[STDERR_FILENO], resolved); + else + free_and_replace(c->stdio_file[STDERR_FILENO], resolved); + + c->std_error = eo; } + + return 0; } int config_parse_exec_io_class(const char *unit, diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 01beac0..beabb37 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -275,6 +275,22 @@ int bus_append_unit_property_assignment(sd_bus_message *m, const char *assignmen r = sd_bus_message_append(m, "sv", "TasksMax", "t", t); goto finish; + } else if (STR_IN_SET(field, "StandardInput", "StandardOutput", "StandardError")) { + const char *n, *appended; + + n = startswith(eq, "fd:"); + if (n) { + appended = strjoina(field, "FileDescriptorName"); + r = sd_bus_message_append(m, "sv", appended, "s", n); + + } else if ((n = startswith(eq, "file:"))) { + appended = strjoina(field, "File"); + r = sd_bus_message_append(m, "sv", appended, "s", n); + } else + r = sd_bus_message_append(m, "sv", field, "s", eq); + + goto finish; + } else if (streq(field, "StandardInputText")) { _cleanup_free_ char *unescaped = NULL; @@ -387,7 +403,6 @@ int bus_append_unit_property_assignment(sd_bus_message *m, const char *assignmen } else if (STR_IN_SET(field, "User", "Group", "DevicePolicy", "KillMode", "UtmpIdentifier", "UtmpMode", "PAMName", "TTYPath", - "StandardInput", "StandardOutput", "StandardError", "Description", "Slice", "Type", "WorkingDirectory", "RootDirectory", "SyslogIdentifier", "ProtectSystem", "ProtectHome", "SELinuxContext", "Restart", "RootImage", -- 2.7.4