/* $Id: child.c,v 1.27 2006/06/01 04:34:52 dm Exp $ */ /* * * Copyright (C) 2004 David Mazieres (dm@uun.org) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * */ #include "local.h" #include #include #include #ifdef HAVE_SETUSERCONTEXT # ifdef HAVE_LOGIN_CAP_H # include # endif /* HAVE_LOGIN_CAP_H */ #endif /* HAVE_SETUSERCONTEXT */ #if HAVE_GETSPNAM # include #endif /* HAVE_GETSPNAM */ #include #include char *getusershell(void); void setusershell(void); void endusershell(void); static int validshell (const char *shell) { const char *s; setusershell (); while ((s = getusershell ())) if (!strcmp (s, shell)) { endusershell (); return 1; } endusershell (); return 0; } struct passwd * validuser (const char *user) { char *avdir; struct stat sb; int expired = 0; #ifdef HAVE_GETSPNAM struct spwd *spe; #endif /* HAVE_GETSPNAM */ struct passwd *pw = getpwnam (user); if (!pw || !pw->pw_uid || !validshell (pw->pw_shell) || pw->pw_dir[0] != '/') return NULL; #ifdef HAVE_GETSPNAM spe = getspnam (pw->pw_name); if (spe && spe->sp_expire > 0 && spe->sp_expire <= (time (NULL) / (24 * 60 * 60))) expired = 1; #elif defined (HAVE_PASSWD_PW_EXPIRE) if (pw->pw_expire > 0 && pw->pw_expire <= time (NULL)) expired = 1; #endif /* HAVE_PASSWD_PW_EXPIRE */ if (expired) return NULL; avdir = xmalloc (strlen (pw->pw_dir) + sizeof ("/.avenger")); strcpy (avdir, pw->pw_dir); strcat (avdir, "/.avenger"); if (lstat (avdir, &sb)) { if (tmperr (errno)) { perror (avdir); exit (EX_OSERR); } pw = NULL; } else if (!S_ISDIR (sb.st_mode)) pw = NULL; free (avdir); return pw; } #ifndef NGROUPS_MAX # define NGROUPS_MAX 16 #endif /* !NGROUPS_MAX */ int become_user (struct passwd *pw, int grouplist, int cd) { int root = getuid () <= 0; u_int flags __attribute__ ((unused)); if (setsid () == -1) perror ("setsid"); if (!grouplist && setgroups (0, NULL)) { GETGROUPS_T gid = getgid (); if (setgroups (1, &gid) && root) { perror ("fatal: setgroups"); return (EX_OSERR); } } #ifdef HAVE_SETUSERCONTEXT flags = LOGIN_SETALL; flags &= ~LOGIN_SETUMASK; if (!grouplist) { flags &= ~LOGIN_SETGROUP; if (setgid (pw->pw_gid)) { perror ("setgid"); if (root) return (EX_OSERR); } } if (root && setusercontext (NULL, pw, pw->pw_uid, flags)) { perror ("setusercontext"); return (EX_OSERR); } #else /* !HAVE_SETUSERCONTEXT */ # if HAVE_SETLOGIN if (setlogin (pw->pw_name)) perror ("setlogin"); # endif /* HAVE_SETLOGIN */ # ifdef HAVE_INITGROUPS if (grouplist && initgroups (pw->pw_name, pw->pw_gid)) { fprintf (stderr, "initgroups failed\n"); if (root) return (EX_OSERR); } # else /* !HAVE_INITGROUPS */ if (grouplist) { GETGROUPS_T gl[NGROUPS_MAX]; int gn = 0; struct group *gr; # ifdef HAVE_EGID_IN_GROUPLIST gl[gn++] = pw->pw_gid; # endif /* HAVE_EGID_IN_GROUPLIST */ setgrent (); while (gn < NGROUPS_MAX && (gr = getgrent ())) { int i; char **mp; if (gr->gr_gid == pw->pw_gid) continue; for (i = 0; i < gn; i++) if (gr->gr_gid == gl[i]) goto next; for (mp = gr->gr_mem; *mp; mp++) if (!strcmp (*mp, pw->pw_name)) { gl[gn++] = gr->gr_gid; goto next; } next:; } if (setgroups (gn, gl)) { perror ("setgroups"); if (root) return (EX_OSERR); } } # endif /* !HAVE_INITGROUPS */ if (setgid (pw->pw_gid)) { perror ("setgid"); if (root) return (EX_OSERR); } if (setuid (pw->pw_uid)) perror ("setuid"); #endif /* !HAVE_SETUSERCONTEXT */ if (root && pw->pw_uid && !getuid ()) { fprintf (stderr, "bacome_user: failed to drop privileges\n"); return (EX_OSERR); } mysetenv ("HOME", pw->pw_dir, -1); if (cd && chdir (pw->pw_dir)) { perror (pw->pw_dir); return tmperr (errno) ? EX_OSERR : EX_CONFIG; } /* Who knows what the system libraries are doing. Wouldn't want to * inherit a file descriptor to the shadow file across an exec. */ endpwent (); #ifdef HAVE_GETSPNAM endspent (); #endif /* HAVE_GETSPNAM */ return 0; } static int linefromprog (char **resp, const char *prog, int mfd) { pid_t pid = 0; int status = -1; int fds[2]; static char buf[1025]; int n, nn; *resp = NULL; while (*prog && isspace (*prog)) prog++; if (!*prog) return 0; if (pipe (fds)) { perror ("pipe"); return EX_OSERR; } if (lseek (mfd, 0, SEEK_SET) == -1 || (pid = fork ()) == -1) { perror (pid ? "fork" : "lseek"); close (fds[0]); close (fds[1]); return EX_OSERR; } if (!pid) { close (fds[0]); errno = 0; if (mfd && dup2 (mfd, 0)) { perror ("dup2 (mfd)"); _exit (EX_OSERR); } else if (fds[1] != 1 && dup2 (fds[1], 1) != 1) { perror ("dup2 (pipe)"); _exit (EX_OSERR); } execl ("/bin/sh", "/bin/sh", "-c", prog, (char *) NULL); perror ("/bin/sh"); _exit (EX_OSFILE); } close (fds[1]); n = 0; while ((nn = read (fds[0], buf + n, sizeof (buf) - n))) if (nn < 0 || (size_t) (n += nn) >= sizeof (buf)) { close (fds[0]); waitpid (pid, &status, 0); fprintf (stderr, "command '%s' produced too much output\n", prog); return EX_SOFTWARE; } close (fds[0]); waitpid (pid, &status, 0); if (!status) { if (n > 0 && memchr (buf, '\n', n) == buf + n - 1) { buf[n - 1] = '\0'; *resp = buf; } else fprintf (stderr, "command '%s' didn't output exactly one line\n", prog); } if (WIFEXITED (status)) { status = WEXITSTATUS (status); if (status < 64 || (status > 68 && status != 99 && status != 100 && status != 111 && status != 112)) status = 0; return status; } if (WIFSIGNALED (status)) fprintf (stderr, "command '%s' exited with signal %d\n", prog, WTERMSIG (status)); return EX_TEMPFAIL; } static int do_line (char *line, size_t len, FILE *parent, int mfd) { char *in; int n; if (len > 0) line[--len] = '\0'; switch (line[0]) { case '\n': case '\0': case '#': return 0; case '&': fprintf (parent, "%s\n", line); return 0; case '<': if (line[1] == '!') { n = linefromprog (&in, line + 2, mfd); if (in) fprintf (parent, "<%s\n", in); return n; } else fprintf (parent, "<%s\n", line + 1); return 0; case '!': n = linefromprog (&in, line + 1, mfd); if (!in || !*in) return n; if (in[0] != '.' && in[0] != '/') fprintf (stderr, "invalid mailbox name '%s' from '%s'\n", in, line); line = in; len = strlen (line); /* cascade */ case '.': case '/': if (len < 2) { fprintf (stderr, "invalid line '%s' in .avenger/local* file\n", line); return 0; } if (line[len - 1] == '/') { line[len - 1] = '\0'; return deliver_maildir (line, mfd, 1); } return deliver_mbox (line, mfd, 1); case '|': { pid_t pid = 0; int status = -1; while (*++line && isspace (*line)) ; if (!*line) return 0; if (lseek (mfd, 0, SEEK_SET) == -1 || (pid = fork ()) == -1) { perror (pid ? "fork" : "lseek"); return EX_OSERR; } if (!pid) { if (dup2 (mfd, 0)) { perror ("dup2"); _exit (EX_OSERR); } execl ("/bin/sh", "/bin/sh", "-c", line, (char *) NULL); perror ("/bin/sh"); _exit (EX_OSFILE); } if (waitpid (pid, &status, 0) != pid) { perror ("waitpid"); return EX_OSFILE; } if (WIFEXITED (status)) { status = WEXITSTATUS (status); if (status > 0 && status < 64) fprintf (stderr, "command '%s' exited with status %d\n", line, status); return status; } if (WIFSIGNALED (status)) fprintf (stderr, "command '%s' exited with signal %d\n", line, WTERMSIG (status)); return EX_TEMPFAIL; } default: fprintf (stderr, "invalid line '%s' in .avenger/local* file\n", line); return 0; } } static int open_local (const char *extra, char **path) { char buf[300]; const char *p; if (!extra) { int fd = open (".avenger/local", O_RDONLY); if (fd < 0) fd = open ("/dev/null", O_RDONLY); if (fd < 0) errno = EAGAIN; return fd; } p = extra + strlen (extra); snprintf (buf, sizeof (buf), ".avenger/local%c%s", opt_separator, extra); for (;;) { int fd = open (buf, O_RDONLY); if (fd >= 0) { if (!*p) mysetenv ("PREFIX", extra, -1); else { int i; char sufn[80]; mysetenv ("PREFIX", extra, p - extra - 1); mysetenv ("SUFFIX", p, -1); for (i = 1; (p = strchr (p, opt_separator)); i++) { snprintf (sufn, sizeof (sufn), "SUFFIX%d", i); mysetenv (sufn, ++p, -1); } } if (path) *path = xstrdup (buf); return fd; } if (p <= extra || tmperr (errno)) return -1; do { p--; } while (p > extra && p[-1] != opt_separator); snprintf (buf, sizeof (buf), ".avenger/local%c%.*sdefault", opt_separator, (int) (p - extra), extra); } } static void nothing (int sig) { } int child (struct passwd *pw, int pfd, int mfd) { FILE *parent = fdopen (pfd, "w"); FILE *local; int err; int fd; struct lnbuf buf; int gotone = 0; char *localpath = NULL; /* XXX - initialize to work around gcc4 */ if (getenv ("FORKDEBUG")) { signal (SIGCONT, nothing); fprintf (stderr, "%s: pid %d waiting for debugger\n", progname, getpid ()); pause (); } if (!parent) { perror ("fdopen"); return EX_OSERR; } setvbuf (parent, NULL, _IOLBF, 0); if ((err = become_user (pw, 1, 1))) return err; fd = open_local (opt_extra, &localpath); if (fd < 0) { if (tmperr (errno)) return EX_OSERR; else if (errno == ENOENT) return EX_NOUSER; else return EX_CONFIG; } local = fdopen (fd, "r"); if (!local) { perror ("fdopen"); close (fd); return EX_OSERR; } bzero (&buf, sizeof (buf)); while ((err = readln (&buf, local, 8192))) { switch (err) { case LNBUF_OK: if (!gotone && buf.buf[0] == '#' && buf.buf[1] == '!') { struct stat sb; if (fstat (fd, &sb) || !(sb.st_mode & 0111)) { fprintf (stderr, "%s starts '#!' but not executable\n", localpath); return EX_TEMPFAIL; } if (lseek (mfd, 0, SEEK_SET) == -1) { perror ("lseek"); return EX_OSERR; } if (mfd != 0 && (dup2 (mfd, 0) == -1 || close (mfd))) { perror ("dup2"); return EX_OSERR; } fclose (parent); execl (localpath, localpath, (char *) NULL); perror (localpath); return EX_TEMPFAIL; } gotone = 1; err = do_line (buf.buf, buf.size, parent, mfd); if (err) return err; break; case LNBUF_EOFNL: fprintf (stderr, "ignoring incomplete line in %s\n", localpath); break; case LNBUF_NOMEM: fprintf (stderr, "out of memory reading line from %s\n", localpath); return EX_OSERR; case LNBUF_TOOBIG: fprintf (stderr, "line too long in %s file\n", localpath); return EX_CONFIG; case LNBUF_IOERR: fprintf (stderr, "error reading %s file\n", localpath); return EX_OSERR; } } if (gotone) return 0; else { char *dl = xmalloc (strlen (opt_default) + 2); sprintf (dl, "%s\n", opt_default); err = do_line (dl, strlen (dl), parent, mfd); free (dl); return err; } }