837a2c9ee2181abfcd90531133423064fa337d22
[oweals/dinit.git] / src / run-child-proc.cc
1 #include <cstdlib>
2 #include <cstring>
3
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <sys/ioctl.h>
7 #include <sys/un.h>
8 #include <sys/socket.h>
9 #include <fcntl.h>
10 #include <unistd.h>
11 #include <termios.h>
12
13 #include "service.h"
14 #include "proc-service.h"
15
16 // Move an fd, if necessary, to another fd. The destination fd must be available (not open).
17 // if fd is specified as -1, returns -1 immediately. Returns 0 on success.
18 static int move_fd(int fd, int dest)
19 {
20     if (fd == -1) return -1;
21     if (fd == dest) return 0;
22
23     if (dup2(fd, dest) == -1) {
24         return -1;
25     }
26
27     close(fd);
28     return 0;
29 }
30
31 // Move a file descriptor to another, freeing up the original descriptor so that it can be used
32 // for some reserved purpose.
33 static int move_reserved_fd(int *fd, int min_fd)
34 {
35     int new_fd = fcntl(*fd, F_DUPFD_CLOEXEC, min_fd);
36     if (new_fd != -1) {
37         close(*fd);
38         *fd = new_fd;
39     }
40     return new_fd;
41 }
42
43 void base_process_service::run_child_proc(run_proc_params params) noexcept
44 {
45     // Child process. Must not risk throwing any uncaught exception from here until exit().
46     const char * const *args = params.args;
47     const char *working_dir = params.working_dir;
48     const char *logfile = params.logfile;
49     bool on_console = params.on_console;
50     int wpipefd = params.wpipefd;
51     int csfd = params.csfd;
52     int notify_fd = params.notify_fd;
53     int force_notify_fd = params.force_notify_fd;
54     const char *notify_var = params.notify_var;
55     uid_t uid = params.uid;
56     gid_t gid = params.gid;
57     const std::vector<service_rlimits> &rlimits = params.rlimits;
58
59     // If the console already has a session leader, presumably it is us. On the other hand
60     // if it has no session leader, and we don't create one, then control inputs such as
61     // ^C will have no effect.
62     bool do_set_ctty = (tcgetsid(0) == -1);
63
64     // Copy signal mask, but unmask signals that we masked on startup. For the moment, we'll
65     // also block all signals, since apparently dup() can be interrupted (!!! really, POSIX??).
66     sigset_t sigwait_set;
67     sigset_t sigall_set;
68     sigfillset(&sigall_set);
69     sigprocmask(SIG_SETMASK, &sigall_set, &sigwait_set);
70     sigdelset(&sigwait_set, SIGCHLD);
71     sigdelset(&sigwait_set, SIGINT);
72     sigdelset(&sigwait_set, SIGTERM);
73     sigdelset(&sigwait_set, SIGQUIT);
74
75     constexpr int bufsz = 11 + ((CHAR_BIT * sizeof(pid_t) + 2) / 3) + 1;
76     // "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate
77     // on the maxiumum number of bytes required for LISTEN=nnn, including nul terminator,
78     // where nnn is a pid_t in decimal (i.e. one decimal digit is worth just over 3 bits).
79     char nbuf[bufsz];
80
81     // "DINIT_CS_FD=" - 12 bytes. (we -1 from sizeof(int) in account of sign bit).
82     constexpr int csenvbufsz = 12 + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
83     char csenvbuf[csenvbufsz];
84
85     int minfd = (socket_fd == -1) ? 3 : 4;
86
87     if (force_notify_fd != -1) {
88         // Move wpipefd/csfd/socket_fd to another fd if necessary:
89         if (wpipefd == force_notify_fd) {
90             if (move_reserved_fd(&wpipefd, minfd) == -1) {
91                 goto failure_out;
92             }
93         }
94         if (csfd == force_notify_fd) {
95             if (move_reserved_fd(&csfd, minfd) == -1) {
96                 goto failure_out;
97             }
98         }
99         if (socket_fd == force_notify_fd) {
100             // Note that we might move this again later
101             if (move_reserved_fd(&socket_fd, 0) == -1) {
102                 goto failure_out;
103             }
104         }
105
106         // allocate the forced notification fd:
107         if (notify_fd != force_notify_fd) {
108             if (dup2(notify_fd, force_notify_fd) == -1) {
109                 goto failure_out;
110             }
111             close(notify_fd);
112             notify_fd = force_notify_fd;
113         }
114     }
115
116     // Make sure we have the fds for stdin/out/err (and pre-opened socket) available:
117     if (wpipefd < minfd) {
118         wpipefd = fcntl(wpipefd, F_DUPFD_CLOEXEC, minfd);
119         if (wpipefd == -1) goto failure_out;
120     }
121
122     if (csfd != -1 && csfd < minfd) {
123         csfd = fcntl(csfd, F_DUPFD, minfd);
124         if (csfd == -1) goto failure_out;
125     }
126
127     if (notify_fd < minfd && notify_fd != force_notify_fd) {
128         notify_fd = fcntl(notify_fd, F_DUPFD, minfd);
129         if (notify_fd == -1) goto failure_out;
130     }
131
132     // Set up notify-fd variable:
133     if (notify_var != nullptr && *notify_var != 0) {
134         // We need to do an allocation: the variable name length, '=', and space for the value,
135         // and nul terminator:
136         int notify_var_len = strlen(notify_var);
137         int req_sz = notify_var_len + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
138         char * var_str = (char *) malloc(req_sz);
139         if (var_str == nullptr) goto failure_out;
140         snprintf(var_str, req_sz, "%s=%d", notify_var, notify_fd);
141         if (putenv(var_str)) goto failure_out;
142     }
143
144     // Set up Systemd-style socket activation:
145     if (socket_fd != -1) {
146         // If we passing a pre-opened socket, it has to be fd number 3. (Thanks, Systemd).
147         if (dup2(socket_fd, 3) == -1) goto failure_out;
148         if (socket_fd != 3) close(socket_fd);
149
150         if (putenv(const_cast<char *>("LISTEN_FDS=1"))) goto failure_out;
151         snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast<intmax_t>(getpid()));
152         if (putenv(nbuf)) goto failure_out;
153     }
154
155     if (csfd != -1) {
156         snprintf(csenvbuf, csenvbufsz, "DINIT_CS_FD=%d", csfd);
157         if (putenv(csenvbuf)) goto failure_out;
158     }
159
160     if (working_dir != nullptr && *working_dir != 0) {
161         if (chdir(working_dir) == -1) {
162             goto failure_out;
163         }
164     }
165
166     if (! on_console) {
167         // Re-set stdin, stdout, stderr
168         for (int i = 0; i < 3; i++) {
169             if (i != force_notify_fd) close(i);
170         }
171
172         if (notify_fd == 0 || move_fd(open("/dev/null", O_RDONLY), 0) == 0) {
173             // stdin = 0. That's what we should have; proceed with opening stdout and stderr. We have to
174             // take care not to clobber the notify_fd.
175             if (notify_fd != 1) {
176                 if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 1) != 0) {
177                     goto failure_out;
178                 }
179                 if (notify_fd != 2 && dup2(1, 2) != 2) {
180                     goto failure_out;
181                 }
182             }
183             else if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 2) != 0) {
184                 goto failure_out;
185             }
186         }
187         else goto failure_out;
188
189         // We have the option of creating a session and process group, or just a new process
190         // group. If we just create a new process group, the child process cannot make itself
191         // a session leader if it wants to do that (eg getty/login will generally want this).
192         // If we do neither, and we are running with a controlling terminal, a ^C or similar
193         // will also affect the child process (which probably isn't so bad, though since we
194         // will handle the shutdown ourselves it's not necessary). Creating a new session
195         // (and a new process group as part of that) seems like a safe bet, and has the
196         // advantage of letting us signal the process as part of a process group.
197         setsid();
198     }
199     else {
200         // "run on console" - run as a foreground job on the terminal/console device
201
202         // if do_set_ctty is false, we are the session leader; we are probably running
203         // as a user process. Don't create a new session leader in that case, and run
204         // as part of the parent session. Otherwise, the new session cannot claim the
205         // terminal as a controlling terminal (it is already claimed), meaning that it
206         // will not see control signals from ^C etc.
207
208         if (do_set_ctty) {
209             // Disable suspend (^Z) (and on some systems, delayed suspend / ^Y)
210             signal(SIGTSTP, SIG_IGN);
211
212             // Become session leader
213             setsid();
214             ioctl(0, TIOCSCTTY, 0);
215         }
216         setpgid(0,0);
217         tcsetpgrp(0, getpgrp());
218     }
219
220     // Resource limits
221     for (auto &limit : rlimits) {
222         rlimit setlimits;
223         if (!limit.hard_set || !limit.soft_set) {
224             // if either hard or soft limit is not set, use current:
225             if (getrlimit(limit.resource_id, &setlimits) != 0) goto failure_out;
226         }
227         if (limit.hard_set) setlimits.rlim_max = limit.limits.rlim_max;
228         if (limit.soft_set) setlimits.rlim_cur = limit.limits.rlim_cur;
229         if (setrlimit(limit.resource_id, &setlimits) != 0) goto failure_out;
230     }
231
232     if (uid != uid_t(-1)) {
233         if (setreuid(uid, uid) != 0) goto failure_out;
234         if (setregid(gid, gid) != 0) goto failure_out;
235     }
236
237     sigprocmask(SIG_SETMASK, &sigwait_set, nullptr);
238
239     execvp(args[0], const_cast<char **>(args));
240
241     // If we got here, the exec failed:
242     failure_out:
243     int exec_status = errno;
244     write(wpipefd, &exec_status, sizeof(int));
245     _exit(0);
246 }