core: add support for StandardInputFile= and friends
authorLennart Poettering <lennart@poettering.net>
Fri, 27 Oct 2017 14:09:57 +0000 (16:09 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 17 Nov 2017 10:13:44 +0000 (11:13 +0100)
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
src/core/execute.c
src/core/execute.h
src/core/load-fragment.c
src/shared/bus-unit-util.c

index 7a9d027..4744630 100644 (file)
@@ -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;
index 7018322..7d6919f 100644 (file)
@@ -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);
index d1cf7dc..2ae38aa 100644 (file)
@@ -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;
index cff3779..37f0229 100644 (file)
@@ -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,
index 01beac0..beabb37 100644 (file)
@@ -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",