Try to fix case where we can't getpgid() treating pid == pgid.
[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
15 // Move an fd, if necessary, to another fd. The destination fd must be available (not open).
16 // if fd is specified as -1, returns -1 immediately. Returns 0 on success.
17 static int move_fd(int fd, int dest)
18 {
19     if (fd == -1) return -1;
20     if (fd == dest) return 0;
21
22     if (dup2(fd, dest) == -1) {
23         return -1;
24     }
25
26     close(fd);
27     return 0;
28 }
29
30 void service_record::run_child_proc(const char * const *args, const char *working_dir,
31         const char *logfile, bool on_console, int wpipefd, int csfd, int socket_fd,
32         int notify_fd, int force_notify_fd, const char *notify_var,
33         uid_t uid, gid_t gid) noexcept
34 {
35     // Child process. Must not risk throwing any uncaught exception from here until exit().
36
37     // If the console already has a session leader, presumably it is us. On the other hand
38     // if it has no session leader, and we don't create one, then control inputs such as
39     // ^C will have no effect.
40     bool do_set_ctty = (tcgetsid(0) == -1);
41
42     // Copy signal mask, but unmask signals that we masked on startup. For the moment, we'll
43     // also block all signals, since apparently dup() can be interrupted (!!! really, POSIX??).
44     sigset_t sigwait_set;
45     sigset_t sigall_set;
46     sigfillset(&sigall_set);
47     sigprocmask(SIG_SETMASK, &sigall_set, &sigwait_set);
48     sigdelset(&sigwait_set, SIGCHLD);
49     sigdelset(&sigwait_set, SIGINT);
50     sigdelset(&sigwait_set, SIGTERM);
51     sigdelset(&sigwait_set, SIGQUIT);
52
53     constexpr int bufsz = 11 + ((CHAR_BIT * sizeof(pid_t) + 2) / 3) + 1;
54     // "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate
55     // on the maxiumum number of bytes required for LISTEN=nnn, including nul terminator,
56     // where nnn is a pid_t in decimal (i.e. one decimal digit is worth just over 3 bits).
57     char nbuf[bufsz];
58
59     // "DINIT_CS_FD=" - 12 bytes. (we -1 from sizeof(int) in account of sign bit).
60     constexpr int csenvbufsz = 12 + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
61     char csenvbuf[csenvbufsz];
62
63     int minfd = (socket_fd == -1) ? 3 : 4;
64
65     // Move wpipefd/csfd/notifyfd to another fd if necessary
66
67     // first allocate the forced notification fd, if specified:
68     if (force_notify_fd != -1) {
69         if (notify_fd != force_notify_fd) {
70             if (dup2(notify_fd, force_notify_fd) == -1) {
71                 goto failure_out;
72             }
73             close(notify_fd);
74             notify_fd = force_notify_fd;
75         }
76     }
77
78     if (wpipefd < minfd) {
79         wpipefd = fcntl(wpipefd, F_DUPFD_CLOEXEC, minfd);
80         if (wpipefd == -1) goto failure_out;
81     }
82
83     if (csfd != -1 && csfd < minfd) {
84         csfd = fcntl(csfd, F_DUPFD, minfd);
85         if (csfd == -1) goto failure_out;
86     }
87
88     if (notify_fd < minfd && notify_fd != force_notify_fd) {
89         notify_fd = fcntl(notify_fd, F_DUPFD, minfd);
90         if (notify_fd == -1) goto failure_out;
91     }
92
93     // Set up notify-fd variable
94     if (notify_var != nullptr && *notify_var != 0) {
95         // We need to do an allocation: the variable name length, '=', and space for the value,
96         // and nul terminator:
97         int notify_var_len = strlen(notify_var);
98         int req_sz = notify_var_len + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
99         char * var_str = (char *) malloc(req_sz);
100         if (var_str == nullptr) goto failure_out;
101         snprintf(var_str, req_sz, "%s=%d", notify_var, notify_fd);
102         if (putenv(var_str)) goto failure_out;
103     }
104
105     // Set up Systemd-style socket activation
106     if (socket_fd != -1) {
107         // If we passing a pre-opened socket, it has to be fd number 3. (Thanks, Systemd).
108         if (dup2(socket_fd, 3) == -1) goto failure_out;
109         if (socket_fd != 3) close(socket_fd);
110
111         if (putenv(const_cast<char *>("LISTEN_FDS=1"))) goto failure_out;
112         snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast<intmax_t>(getpid()));
113         if (putenv(nbuf)) goto failure_out;
114     }
115
116     if (csfd != -1) {
117         snprintf(csenvbuf, csenvbufsz, "DINIT_CS_FD=%d", csfd);
118         if (putenv(csenvbuf)) goto failure_out;
119     }
120
121     if (working_dir != nullptr && *working_dir != 0) {
122         if (chdir(working_dir) == -1) {
123             goto failure_out;
124         }
125     }
126
127     if (! on_console) {
128         // Re-set stdin, stdout, stderr
129         for (int i = 0; i < 3; i++) {
130             if (i != force_notify_fd) close(i);
131         }
132
133         if (notify_fd == 0 || move_fd(open("/dev/null", O_RDONLY), 0) == 0) {
134             // stdin = 0. That's what we should have; proceed with opening stdout and stderr. We have to
135             // take care not to clobber the notify_fd.
136             if (notify_fd != 1) {
137                 if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 1) != 0) {
138                     goto failure_out;
139                 }
140                 if (notify_fd != 2 && dup2(1, 2) != 2) {
141                     goto failure_out;
142                 }
143             }
144             else if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 2) != 0) {
145                 goto failure_out;
146             }
147         }
148         else goto failure_out;
149
150         // We have the option of creating a session and process group, or just a new process
151         // group. If we just create a new process group, the child process cannot make itself
152         // a session leader if it wants to do that (eg getty/login will generally want this).
153         // If we do neither, and we are running with a controlling terminal, a ^C or similar
154         // will also affect the child process (which probably isn't so bad, though since we
155         // will handle the shutdown ourselves it's not necessary). Creating a new session
156         // (and a new process group as part of that) seems like a safe bet, and has the
157         // advantage of letting us signal the process as part of a process group.
158         setsid();
159     }
160     else {
161         // "run on console" - run as a foreground job on the terminal/console device
162
163         // if do_set_ctty is false, we are the session leader; we are probably running
164         // as a user process. Don't create a new session leader in that case, and run
165         // as part of the parent session. Otherwise, the new session cannot claim the
166         // terminal as a controlling terminal (it is already claimed), meaning that it
167         // will not see control signals from ^C etc.
168
169         if (do_set_ctty) {
170             // Disable suspend (^Z) (and on some systems, delayed suspend / ^Y)
171             signal(SIGTSTP, SIG_IGN);
172
173             // Become session leader
174             setsid();
175             ioctl(0, TIOCSCTTY, 0);
176         }
177         setpgid(0,0);
178         tcsetpgrp(0, getpgrp());
179     }
180
181     if (uid != uid_t(-1)) {
182         if (setreuid(uid, uid) != 0) goto failure_out;
183         if (setregid(gid, gid) != 0) goto failure_out;
184     }
185
186     sigprocmask(SIG_SETMASK, &sigwait_set, nullptr);
187
188     execvp(args[0], const_cast<char **>(args));
189
190     // If we got here, the exec failed:
191     failure_out:
192     int exec_status = errno;
193     write(wpipefd, &exec_status, sizeof(int));
194     _exit(0);
195 }