From 8c01a856cedd705f58e20bcccafaa123c28b6603 Mon Sep 17 00:00:00 2001 From: Davin McCall Date: Sun, 26 Jun 2016 19:21:10 +0100 Subject: [PATCH] Add a "pass-cs-fd" service option, which passes a control socket file descriptor directly to service processes, allowing them issue commands even if the regular control socket has not been created (or if they will not have permission to access it). This has limited use cases and has security implications when used, but allows early launching of a service which can pass events / issue commands to dinit. In particular this could be useful for a device watcher daemon which starts services as certain devices come online (eg check and mount disks as they are detected by the kernel). --- README | 11 ++ TODO | 2 - src/load_service.cc | 3 + src/service.cc | 249 +++++++++++++++++++++++++++----------------- src/service.h | 14 ++- 5 files changed, 175 insertions(+), 104 deletions(-) diff --git a/README b/README index 9c14a4a..ee9aa7b 100644 --- a/README +++ b/README @@ -235,3 +235,14 @@ options = ( runs-on-console | nosigterm | starts-rwfs | starts-log ) ... starts-log : this service starts the system log daemon. Dinit will begin logging via the /dev/log socket. + + pass-cs-fd : pass an open Dinit control socket to the process when launching + it (the DINIT_CS_FD environment variable will be set to the file + descriptor of the socket). This allows the service to issue + commands to Dinit even if the regular control socket is not + available yet. + + Using this option has security implications! The service which + receives the control socket must close it before launching any + untrusted processes. You should not use this option unless the + service is designed to receive a Dinit control socket. diff --git a/TODO b/TODO index 41c027c..9364656 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,3 @@ -* Sort out single-user mode startup - For version 1.0: ---------------- * Documentation including sample service definitions diff --git a/src/load_service.cc b/src/load_service.cc index 02991a7..1124785 100644 --- a/src/load_service.cc +++ b/src/load_service.cc @@ -469,6 +469,9 @@ ServiceRecord * ServiceSet::loadServiceRecord(const char * name) else if (option_txt == "runs-on-console") { onstart_flags.runs_on_console = true; } + else if (option_txt == "pass-cs-fd") { + onstart_flags.pass_cs_fd = true; + } else { throw new ServiceDescriptionExc(name, "Unknown option: " + option_txt); } diff --git a/src/service.cc b/src/service.cc index 5425a21..264562b 100644 --- a/src/service.cc +++ b/src/service.cc @@ -656,136 +656,191 @@ bool ServiceRecord::start_ps_process(const std::vector &cmd, bool int pipefd[2]; if (pipe2(pipefd, O_CLOEXEC)) { - // TODO log error + log(LogLevel::ERROR, service_name, ": can't create status check pipe: ", strerror(errno)); return false; } - - // Set up the argument array and other data now (before fork), in case memory allocation fails. - - auto args = cmd.data(); - + const char * logfile = this->logfile.c_str(); if (*logfile == 0) { logfile = "/dev/null"; } + bool child_status_registered = false; + ControlConn *control_conn = nullptr; + + int control_socket[2] = {-1, -1}; + if (onstart_flags.pass_cs_fd) { + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, /* protocol */ 0, control_socket)) { + log(LogLevel::ERROR, service_name, ": can't create control socket: ", strerror(errno)); + goto out_p; + } + + // Make the server side socket close-on-exec: + int fdflags = fcntl(control_socket[0], F_GETFD); + fcntl(control_socket[0], F_SETFD, fdflags | FD_CLOEXEC); + + try { + control_conn = new ControlConn(&eventLoop, service_set, control_socket[0]); + } + catch (std::exception &exc) { + log(LogLevel::ERROR, service_name, ": can't launch process; out of memory"); + goto out_cs; + } + } + + // Set up complete, now fork and exec: + pid_t forkpid; - bool child_status_registered = false; try { child_status_listener.addWatch(eventLoop, pipefd[0], IN_EVENTS); child_status_registered = true; forkpid = child_listener.fork(eventLoop); } - catch (...) { - if (child_status_registered) { - child_status_listener.deregister(eventLoop); + catch (std::exception &e) { + log(LogLevel::ERROR, service_name, ": Could not fork: ", e.what()); + goto out_cs_h; + } + + if (forkpid == 0) { + run_child_proc(cmd.data(), logfile, on_console, pipefd[1], control_socket[1]); + } + else { + // Parent process + close(pipefd[1]); // close the 'other end' fd + if (control_socket[1] != -1) { + close(control_socket[1]); } - close(pipefd[0]); - close(pipefd[1]); - return false; + pid = forkpid; + + waiting_for_execstat = true; + return true; } + // Failure exit: + + out_cs_h: + if (child_status_registered) { + child_status_listener.deregister(eventLoop); + } + + if (onstart_flags.pass_cs_fd) { + delete control_conn; + + out_cs: + close(control_socket[0]); + close(control_socket[1]); + } + + out_p: + close(pipefd[0]); + close(pipefd[1]); + + return false; +} + +void ServiceRecord::run_child_proc(const char * const *args, const char *logfile, bool on_console, + int wpipefd, int csfd) noexcept +{ + // Child process. Must not allocate memory (or otherwise risk throwing any exception) + // from here until exit(). + // If the console already has a session leader, presumably it is us. On the other hand // if it has no session leader, and we don't create one, then control inputs such as // ^C will have no effect. bool do_set_ctty = (tcgetsid(0) == -1); + + // Copy signal mask, but unmask signals that we masked on startup. For the moment, we'll + // also block all signals, since apparently dup() can be interrupted (!!! really, POSIX??). + sigset_t sigwait_set; + sigset_t sigall_set; + sigfillset(&sigall_set); + sigprocmask(SIG_SETMASK, &sigall_set, &sigwait_set); + sigdelset(&sigwait_set, SIGCHLD); + sigdelset(&sigwait_set, SIGINT); + sigdelset(&sigwait_set, SIGTERM); + + constexpr int bufsz = ((CHAR_BIT * sizeof(pid_t)) / 3 + 2) + 11; + // "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate + // on the maxiumum number of bytes required for LISTEN=xxx, including nul terminator, + // where xxx is a pid_t in decimal (i.e. one decimal digit is worth just over 3 bits). + char nbuf[bufsz]; + + // "DINIT_CS_FD=" - 12 bytes. (we -1 from sizeof(int) in account of sign bit). + constexpr int csenvbufsz = ((CHAR_BIT * sizeof(int) - 1) / 3 + 2) + 12; + char csenvbuf[csenvbufsz]; + + int minfd = (socket_fd == -1) ? 3 : 4; - if (forkpid == 0) { - // Child process. Must not allocate memory (or otherwise risk throwing any exception) - // from here until exit(). - // TODO: we may need an equivalent for the following: - // ev_default_destroy(); // won't need that on this side, free up fds. + // Move wpipefd/csfd to another fd if necessary + if (wpipefd < minfd) { + wpipefd = fcntl(wpipefd, F_DUPFD_CLOEXEC, minfd); + if (wpipefd == -1) goto failure_out; + } + + if (csfd != -1 && csfd < minfd) { + csfd = fcntl(csfd, F_DUPFD, minfd); + if (csfd == -1) goto failure_out; + } + + if (socket_fd != -1) { - // Copy signal mask, but unmask signals that we masked on startup. For the moment, we'll - // also block all signals, since apparently dup() can be interrupted (!!! really, POSIX??). - sigset_t sigwait_set; - sigset_t sigall_set; - sigfillset(&sigall_set); - sigprocmask(SIG_SETMASK, &sigall_set, &sigwait_set); - sigdelset(&sigwait_set, SIGCHLD); - sigdelset(&sigwait_set, SIGINT); - sigdelset(&sigwait_set, SIGTERM); + if (dup2(socket_fd, 3) == -1) goto failure_out; + if (socket_fd != 3) { + close(socket_fd); + } - constexpr int bufsz = ((CHAR_BIT * sizeof(pid_t)) / 3 + 2) + 11; - // "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate - // on the maxiumum number of bytes required for LISTEN=xxx, including nul terminator, - // where xxx is a pid_t in decimal (i.e. one decimal digit is worth just over 3 bits). - char nbuf[bufsz]; + if (putenv(const_cast("LISTEN_FDS=1"))) goto failure_out; + snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast(getpid())); + if (putenv(nbuf)) goto failure_out; + } + + if (csfd != -1) { + snprintf(csenvbuf, csenvbufsz, "DINIT_CS_FD=%d", csfd); + if (putenv(csenvbuf)) goto failure_out; + } - if (socket_fd != -1) { - - if (pipefd[1] == 3) { - dup3(pipefd[1], 4, O_CLOEXEC); - } - - dup2(socket_fd, 3); - if (socket_fd != 3) { - close(socket_fd); - } - - if (putenv(const_cast("LISTEN_FDS=1"))) goto failure_out; - - snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast(getpid())); - - if (putenv(nbuf)) goto failure_out; - } + if (! on_console) { + // Re-set stdin, stdout, stderr + close(0); close(1); close(2); - if (! on_console) { - while (pipefd[1] < 3) { - pipefd[1] = dup(pipefd[1]); - if (pipefd[1] == -1) goto failure_out; + if (open("/dev/null", O_RDONLY) == 0) { + // stdin = 0. That's what we should have; proceed with opening + // stdout and stderr. + if (open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) != 1) { + goto failure_out; } - - // Re-set stdin, stdout, stderr - close(0); close(1); close(2); - - if (open("/dev/null", O_RDONLY) == 0) { - // stdin = 0. That's what we should have; proceed with opening - // stdout and stderr. - if (open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) != 1) { - goto failure_out; - } - if (dup2(1, 2) != 2) { - goto failure_out; - } + if (dup2(1, 2) != 2) { + goto failure_out; } - else goto failure_out; } - else { - // "run on console" - run as a foreground job on the terminal/console device - - if (do_set_ctty) { - // Disable suspend (^Z) (and on some systems, delayed suspend / ^Y) - signal(SIGTSTP, SIG_IGN); - - // Become session leader - setsid(); - ioctl(0, TIOCSCTTY, 0); - } - setpgid(0,0); - tcsetpgrp(0, getpgrp()); - } - - sigprocmask(SIG_SETMASK, &sigwait_set, nullptr); - - execvp(exec_arg_parts[0], const_cast(args)); - - // If we got here, the exec failed: - failure_out: - int exec_status = errno; - write(pipefd[1], &exec_status, sizeof(int)); - _exit(0); + else goto failure_out; } else { - // Parent process - close(pipefd[1]); // close the 'other end' fd - pid = forkpid; - - waiting_for_execstat = true; - return true; + // "run on console" - run as a foreground job on the terminal/console device + + if (do_set_ctty) { + // Disable suspend (^Z) (and on some systems, delayed suspend / ^Y) + signal(SIGTSTP, SIG_IGN); + + // Become session leader + setsid(); + ioctl(0, TIOCSCTTY, 0); + } + setpgid(0,0); + tcsetpgrp(0, getpgrp()); } + + sigprocmask(SIG_SETMASK, &sigwait_set, nullptr); + + execvp(args[0], const_cast(args)); + + // If we got here, the exec failed: + failure_out: + int exec_status = errno; + write(wpipefd, &exec_status, sizeof(int)); + _exit(0); } // Mark this and all dependent services as force-stopped. diff --git a/src/service.h b/src/service.h index ae84946..605984f 100644 --- a/src/service.h +++ b/src/service.h @@ -77,9 +77,10 @@ struct OnstartFlags { // Not actually "onstart" commands: bool no_sigterm : 1; // do not send SIGTERM bool runs_on_console : 1; // run "in the foreground" + bool pass_cs_fd : 1; // pass this service a control socket connection via fd OnstartFlags() noexcept : rw_ready(false), log_ready(false), - no_sigterm(false), runs_on_console(false) + no_sigterm(false), runs_on_console(false), pass_cs_fd(false) { } }; @@ -211,11 +212,11 @@ class ServiceRecord ServiceState service_state = ServiceState::STOPPED; /* ServiceState::STOPPED, STARTING, STARTED, STOPPING */ ServiceState desired_state = ServiceState::STOPPED; /* ServiceState::STOPPED / STARTED */ - string program_name; /* storage for program/script and arguments */ - std::vector exec_arg_parts; /* pointer to each argument/part of the program_name */ + string program_name; // storage for program/script and arguments + std::vector exec_arg_parts; // pointer to each argument/part of the program_name, and nullptr - string stop_command; /* storage for stop program/script and arguments */ - std::vector stop_arg_parts; /* pointer to each argument/part of the stop_command */ + string stop_command; // storage for stop program/script and arguments + std::vector stop_arg_parts; // pointer to each argument/part of the stop_command, and nullptr string pid_file; @@ -311,6 +312,9 @@ class ServiceRecord bool start_ps_process() noexcept; bool start_ps_process(const std::vector &args, bool on_console) noexcept; + void run_child_proc(const char * const *args, const char *logfile, bool on_console, int wpipefd, + int csfd) noexcept; + // Callback from libev when a child process dies static void process_child_callback(EventLoop_t *loop, ServiceChildWatcher *w, int revents) noexcept; -- 2.25.1