chiark / gitweb /
get sense of ferror check right
[inn-innduct.git] / innd / inndstart.c
1 /*  $Id: inndstart.c 7749 2008-04-06 14:15:04Z iulius $
2 **
3 **  Open the privileged port, then exec innd.
4 **
5 **  inndstart, in a normal INN installation, is installed setuid root and
6 **  executable only by users in the news group.  Because it is setuid root,
7 **  it's very important to ensure that it be as simple and secure as
8 **  possible so that news access can't be leveraged into root access.
9 **
10 **  Fighting against this desire, as much of INN's operation as possible
11 **  should be configurable at run-time using inn.conf, and the news system
12 **  should be able to an alternate inn.conf by setting INNCONF to the path
13 **  to that file before starting any programs.  The configuration data
14 **  therefore can't be trusted.
15 **
16 **  Our security model is therefore:
17 **
18 **   - The only three operations performed while privileged are determining
19 **     the UID and GID of NEWSUSER and NEWSGRP, setting system limits, and
20 **     opening the privileged port we're binding to.
21 **
22 **   - We can only be executed by the NEWSUSER and NEWSGRP, both compile-
23 **     time constants; otherwise, we exit.  Similarly, we will only setuid()
24 **     to the NEWSUSER.  This is to prevent someone other than the NEWSUSER
25 **     but still able to execute inndstart for whatever reason from using it
26 **     to run innd as the news user with bogus configuration information,
27 **     thereby possibly compromising the news account.
28 **
29 **   - The only ports < 1024 that we'll bind to are 119 and 433, or a port
30 **     given at configure time with --with-innd-port.  This is to prevent
31 **     the news user from taking over a service such as telnet or POP and
32 **     potentially gaining access to user passwords.
33 **
34 **  This program therefore gives the news user the ability to revoke system
35 **  file descriptor limits and bind to the news port, and nothing else.
36 **
37 **  Note that we do use getpwnam() to determine the UID of NEWSUSER, which
38 **  potentially opens an exploitable hole on those systems that don't
39 **  correctly prevent a user running a setuid program from interfering with
40 **  the running process (replacing system calls, for example, or using
41 **  things like LD_PRELOAD).
42 */
43
44 #include "config.h"
45 #include "clibrary.h"
46 #include "portable/socket.h"
47 #include <errno.h>
48 #include <fcntl.h>
49 #include <grp.h>
50 #include <pwd.h>
51 #include <syslog.h>
52
53 #ifdef HAVE_INET6
54 # include <netdb.h>
55 #endif
56
57 #include "inn/innconf.h"
58 #include "inn/messages.h"
59 #include "libinn.h"
60 #include "paths.h"
61
62 /* Fake up a do-nothing setgroups for Cygwin. */
63 #if !HAVE_SETGROUPS
64 # define setgroups(n, list)     0
65 #endif
66
67 /* To run innd under the debugger, uncomment this and fix the path. */
68 /* #define DEBUGGER "/usr/ucb/dbx" */
69
70
71 int
72 main(int argc, char *argv[])
73 {
74     struct passwd *pwd;
75     struct group *grp;
76     uid_t news_uid, real_uid;
77     gid_t news_gid;
78     int snum = 0;
79     int port, s[MAX_SOCKETS + 1], i, j;
80 #ifdef HAVE_INET6
81     struct in6_addr address6;
82     bool addr6_specified = false;
83 #endif
84     struct in_addr address;
85     bool addr_specified = false;
86     char *p;
87     char **innd_argv;
88     char pflag[SMBUF];
89 #ifdef PURIFY
90     char *innd_env[11];
91 #else
92     char *innd_env[9];
93 #endif
94
95     /* Set up the error handlers.  Always print to stderr, and for warnings
96        also syslog with a priority of LOG_ERR.  For fatal errors, also
97        syslog with a priority of LOG_CRIT.  These priority levels are a
98        little high, but they're chosen to match innd. */
99     openlog("inndstart", LOG_CONS, LOG_INN_PROG);
100     message_handlers_warn(2, message_log_stderr, message_log_syslog_err);
101     message_handlers_die(2, message_log_stderr, message_log_syslog_crit);
102     message_program_name = "inndstart";
103
104     /* Convert NEWSUSER and NEWSGRP to a UID and GID.  getpwnam() and
105        getgrnam() don't set errno normally, so don't print strerror() on
106        failure; it probably contains garbage.*/
107     pwd = getpwnam(NEWSUSER);
108     if (!pwd)
109         die("can't getpwnam(%s)", NEWSUSER);
110     news_uid = pwd->pw_uid;
111     grp = getgrnam(NEWSGRP);
112     if (!grp)
113         die("can't getgrnam(%s)", NEWSGRP);
114     news_gid = grp->gr_gid;
115
116     /* Exit if run by any other user or group. */
117     real_uid = getuid();
118     if (real_uid != news_uid)
119         die("must be run by user %s (%lu), not %lu", NEWSUSER,
120                 (unsigned long)news_uid, (unsigned long)real_uid);
121
122     /* Drop all supplemental groups and drop privileges to read inn.conf.
123        setgroups() can only be invoked by root, so if inndstart isn't setuid
124        root this is where we fail. */
125     if (setgroups(1, &news_gid) < 0)
126         syswarn("can't setgroups (is inndstart setuid root?)");
127     if (seteuid(news_uid) < 0)
128         sysdie("can't seteuid to %lu", (unsigned long)news_uid);
129     if (!innconf_read(NULL))
130         exit(1);
131
132     /* Check for a bind address specified in inn.conf.  "any" or "all" will
133        cause inndstart to bind to INADDR_ANY. */
134     address.s_addr = htonl(INADDR_ANY);
135     p = innconf->bindaddress;
136     if (p && strcmp(p, "all") != 0 && strcmp(p, "any") != 0) {
137         if (!inet_aton(p, &address))
138             die("invalid bindaddress in inn.conf (%s)", p);
139         addr_specified = true;
140     }
141 #ifdef HAVE_INET6
142     address6 = in6addr_any;
143     p = innconf->bindaddress6;
144     if (p && strcmp(p, "all") != 0 && strcmp(p, "any") != 0) {
145         if (inet_pton(AF_INET6, p, &address6) < 1)
146             die("invalid bindaddress6 in inn.conf (%s)", p);
147         addr6_specified = true;
148     }
149 #endif
150
151     /* Parse our command-line options.  The only options we take are -P,
152        which specifies what port number to bind to, and -I, which specifies
153        what IP address to bind to.  Both override inn.conf.  Support both
154        "-P <port>" and "-P<port>".  All other options are passed through to
155        innd. */
156     port = innconf->port;
157     for (i = 1; i < argc; i++) {
158         if (strncmp("-P", argv[i], 2) == 0) {
159             if (strlen(argv[i]) > 2) {
160                 port = atoi(&argv[i][2]);
161             } else {
162                 i++;
163                 if (argv[i] == NULL)
164                     die("missing port after -P");
165                 port = atoi(argv[i]);
166             }
167             if (port == 0)
168                 die("invalid port %s (must be a number)", argv[i]);
169 #ifdef HAVE_INET6
170         } else if (strncmp("-6", argv[i], 2) == 0) {
171             if (strlen(argv[i]) > 2) {
172                 p = &argv[i][2];
173             } else {
174                 i++;
175                 if (argv[i] == NULL)
176                     die("missing address after -6");
177                 p = argv[i];
178             }
179             if (inet_pton(AF_INET6, p, &address6) < 1)
180                 die("invalid address %s", p);
181             addr6_specified = true;
182 #endif
183         } else if (strncmp("-I", argv[i], 2) == 0) {
184             if (strlen(argv[i]) > 2) {
185                 p = &argv[i][2];
186             } else {
187                 i++;
188                 if (argv[i] == NULL)
189                     die("missing address after -I");
190                 p = argv[i];
191             }
192             if (!inet_aton(p, &address))
193                 die("invalid address %s", p);
194             addr_specified = true;
195         }
196     }
197             
198     /* Make sure that the requested port is legitimate. */
199     if (port < 1024 && port != 119
200 #ifdef INND_PORT
201         && port != INND_PORT
202 #endif
203         && port != 433)
204         die("can't bind to restricted port %d", port);
205
206     /* Now, regain privileges so that we can change system limits and bind
207        to our desired port. */
208     if (seteuid(0) < 0)
209         sysdie("can't seteuid to 0");
210
211     /* innconf->rlimitnofile <= 0 says to leave it alone. */
212     if (innconf->rlimitnofile > 0 && setfdlimit(innconf->rlimitnofile) < 0)
213         syswarn("can't set file descriptor limit to %ld",
214                 innconf->rlimitnofile);
215
216 #if defined(HAVE_INET6) && defined(IPV6_V6ONLY)
217     /* If we have the IPV6_V6ONLY socket option, and it works,
218        always open separate IPv4 and IPv6 sockets. */
219     if (addr_specified == 0 && addr6_specified == 0) {
220         j = socket(PF_INET6, SOCK_STREAM, 0);
221         if (j >= 0) {
222             i = 1;
223             if (setsockopt (j, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&i,
224                              sizeof i) == 0) {
225                 addr_specified = 1;
226                 addr6_specified = 1;
227             }
228             close (j);
229         }
230     }
231 #endif
232
233     /* Create a socket and name it. */
234 #ifdef HAVE_INET6
235     if( ! (addr_specified || addr6_specified) ) {
236         struct addrinfo hints, *addr, *ressave;
237         char service[16];
238         int error;
239
240         memset(&hints, 0, sizeof hints);
241         hints.ai_family = PF_UNSPEC;
242         hints.ai_flags = AI_PASSIVE;
243         hints.ai_socktype = SOCK_STREAM;
244         snprintf(service, sizeof(service), "%d", port);
245         error = getaddrinfo(NULL, service, &hints, &addr);
246         if (error < 0)
247             die("getaddrinfo: %s", gai_strerror(error));
248
249         for (ressave = addr; addr; addr = addr->ai_next) {
250             if ((i = socket(addr->ai_family, addr->ai_socktype,
251                             addr->ai_protocol)) < 0)
252                 continue; /* ignore */
253 #ifdef SO_REUSEADDR
254             j = 1;
255             if (setsockopt(i, SOL_SOCKET, SO_REUSEADDR, (char *)&j,
256                         sizeof j) < 0)
257                 syswarn("can't set SO_REUSEADDR");
258 #endif
259             if (bind(i, addr->ai_addr, addr->ai_addrlen) < 0) {
260                 j = errno;
261                 close(i);
262                 errno = j;
263                 continue; /* ignore */
264             }
265             s[snum++] = i;
266             if (snum == MAX_SOCKETS)
267                 break;
268         }
269         freeaddrinfo(ressave);
270
271         if (snum == 0)
272             sysdie("can't bind socket");
273     } else {
274         if ( addr6_specified ) {
275             struct sockaddr_in6 server6;
276
277             s[snum] = socket(PF_INET6, SOCK_STREAM, 0);
278             if (s[snum] < 0)
279                 sysdie("can't open inet6 socket");
280 #ifdef SO_REUSEADDR
281             i = 1;
282             if (setsockopt(s[snum], SOL_SOCKET, SO_REUSEADDR, (char *)&i,
283                         sizeof i) < 0)
284                 syswarn("can't set SO_REUSEADDR");
285 #endif
286 #ifdef IPV6_V6ONLY
287             i = 1;
288             if (setsockopt(s[snum], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&i,
289                         sizeof i) < 0)
290                 syswarn("can't set IPV6_V6ONLY");
291 #endif
292             memset(&server6, 0, sizeof server6);
293             server6.sin6_port = htons(port);
294             server6.sin6_family = AF_INET6;
295             server6.sin6_addr = address6;
296 #ifdef HAVE_SOCKADDR_LEN
297             server6.sin6_len = sizeof server6;
298 #endif
299             if (bind(s[snum], (struct sockaddr *)&server6, sizeof server6) < 0)
300                 sysdie("can't bind inet6 socket");
301             snum++;
302         }
303         if ( addr_specified )
304 #endif /* HAVE_INET6 */
305         {
306             struct sockaddr_in server;
307
308             s[snum] = socket(PF_INET, SOCK_STREAM, 0);
309             if (s[snum] < 0)
310                 sysdie("can't open inet socket");
311 #ifdef SO_REUSEADDR
312             i = 1;
313             if (setsockopt(s[snum], SOL_SOCKET, SO_REUSEADDR, (char *) &i,
314                         sizeof i) < 0)
315                 syswarn("can't set SO_REUSEADDR");
316 #endif
317             memset(&server, 0, sizeof server);
318             server.sin_port = htons(port);
319             server.sin_family = AF_INET;
320 #ifdef HAVE_SOCKADDR_LEN
321             server.sin_len = sizeof server;
322 #endif
323             server.sin_addr = address;
324             if (bind(s[snum], (struct sockaddr *)&server, sizeof server) < 0)
325                 sysdie("can't bind inet socket");
326             snum++;
327         }
328 #ifdef HAVE_INET6
329     }
330 #endif
331     s[snum] = -1;
332
333     /* Now, permanently drop privileges. */
334     if (setgid(news_gid) < 0 || getgid() != news_gid)
335         sysdie("can't setgid to %lu", (unsigned long)news_gid);
336     if (setuid(news_uid) < 0 || getuid() != news_uid)
337         sysdie("can't setuid to %lu", (unsigned long)news_uid);
338
339     /* Build the argument vector for innd.  Pass -p<port> to innd to tell it
340        what port we just created and bound to for it. */
341     innd_argv = xmalloc((1 + argc + 1) * sizeof(char *));
342     i = 0;
343     strlcpy(pflag, "-p ", sizeof(pflag));
344     for (j = 0; s[j] > 0; j++) {
345         char temp[16];
346
347         snprintf(temp, sizeof(temp), "%d,", s[j]);
348         strlcat(pflag, temp, sizeof(pflag));
349     }
350     /* chop off the trailing , */
351     j = strlen(pflag) - 1;
352     pflag[j] = '\0';
353 #ifdef DEBUGGER
354     innd_argv[i++] = DEBUGGER;
355     innd_argv[i++] = concatpath(innconf->pathbin, "innd");
356     innd_argv[i] = 0;
357     printf("When starting innd, use -d %s\n", s, pflag);
358 #else /* DEBUGGER */
359     innd_argv[i++] = concatpath(innconf->pathbin, "innd");
360     innd_argv[i++] = pflag;
361
362     /* Don't pass along -p, -P, or -I.  Check the length of the argument
363        string, and if it == 2 (meaning there's nothing after the -p or -P or
364        -I), skip the next argument too, to support leaving a space between
365        the argument and the value. */
366     for (j = 1; j < argc; j++) {
367         if (argv[j][0] == '-' && strchr("pP6I", argv[j][1])) {
368             if (strlen(argv[j]) == 2)
369                 j++;
370             continue;
371         } else {
372             innd_argv[i++] = argv[j];
373         }
374     }
375     innd_argv[i] = 0;
376 #endif /* !DEBUGGER */
377
378     /* Set up the environment.  Note that we're trusting BIND_INADDR and TZ;
379        everything else is either from inn.conf or from configure.  These
380        should be sanity-checked before being propagated, but that requires
381        knowledge of the range of possible values.  Just limiting their
382        length doesn't necessarily do anything to prevent exploits and may
383        stop things from working that should.  We have to pass BIND_INADDR so
384        that it's set for programs, such as innfeed, that innd may spawn. */
385     innd_env[0] = concat("PATH=", innconf->pathbin, ":", innconf->pathetc,
386                          ":/bin:/usr/bin:/usr/ucb", (char *) 0);
387     innd_env[1] = concat( "TMPDIR=", innconf->pathtmp,  (char *) 0);
388     innd_env[2] = concat(  "SHELL=", _PATH_SH,          (char *) 0);
389     innd_env[3] = concat("LOGNAME=", NEWSMASTER,        (char *) 0);
390     innd_env[4] = concat(   "USER=", NEWSMASTER,        (char *) 0);
391     innd_env[5] = concat(   "HOME=", innconf->pathnews, (char *) 0);
392     i = 6;
393     p = getenv("BIND_INADDR");
394     if (p != NULL)
395         innd_env[i++] = concat("BIND_INADDR=", p, (char *) 0);
396     p = getenv("TZ");
397     if (p != NULL)
398         innd_env[i++] = concat("TZ=", p, (char *) 0);
399 #ifdef PURIFY
400     /* you have to compile with `purify cc -DPURIFY' to get this */
401     p = getenv("DISPLAY");
402     if (p != NULL)
403         innd_env[i++] = concat("DISPLAY=", p, (char *) 0);
404     p = getenv("PURIFYOPTIONS");
405     if (p != NULL)
406         innd_env[i++] = concat("PURIFYOPTIONS=", p, (char *) 0);
407 #endif
408     innd_env[i] = 0;
409
410     /* Go exec innd. */
411     execve(innd_argv[0], innd_argv, innd_env);
412     sysdie("can't exec %s", innd_argv[0]);
413
414     /* Not reached. */
415     return 1;
416 }