/* spawn.c Spawn a program securely. Copyright (C) 1992, 1993, 1994, 1995 Ian Lance Taylor This file is part of the Taylor UUCP package. 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 of the License, 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. The author of the program may be contacted at ian@airs.com. */ #include "uucp.h" #include "uudefs.h" #include "sysdep.h" #include #if HAVE_FCNTL_H #include #else #if HAVE_SYS_FILE_H #include #endif #endif #ifndef O_RDONLY #define O_RDONLY 0 #define O_WRONLY 1 #define O_RDWR 2 #endif #ifndef FD_CLOEXEC #define FD_CLOEXEC 1 #endif #ifndef environ extern char **environ; #endif /* Spawn a child in a fairly secure fashion. This returns the process ID of the child or -1 on error. It takes far too many arguments: pazargs -- arguments (element 0 is command) aidescs -- file descriptors for stdin, stdout and stderr fkeepuid -- TRUE if euid should be left unchanged fkeepenv -- TRUE if environment should be left unmodified zchdir -- directory to chdir to fnosigs -- TRUE if child should ignore SIGHUP, SIGINT and SIGQUIT fshell -- TRUE if should try /bin/sh if execve gets ENOEXEC zpath -- value for environment variable PATH zuu_machine -- value for environment variable UU_MACHINE zuu_user -- value for environment variable UU_USER The aidescs array is three elements long. 0 is stdin, 1 is stdout and 2 is stderr. The array may contain either file descriptor numbers to dup appropriately, or one of the following: SPAWN_NULL -- set descriptor to /dev/null SPAWN_READ_PIPE -- set aidescs element to pipe for parent to read SPAWN_WRITE_PIPE -- set aidescs element to pipe for parent to write If fkeepenv is FALSE, a standard environment is created. The environment arguments (zpath, zuu_machine and zuu_user) are only used if fkeepenv is FALSE; any of them may be NULL. This routine expects that all file descriptors have been set to close-on-exec, so it doesn't have to worry about closing them explicitly. It sets the close-on-exec flag for the new pipe descriptors it returns. */ pid_t ixsspawn (pazargs, aidescs, fkeepuid, fkeepenv, zchdir, fnosigs, fshell, zpath, zuu_machine, zuu_user) const char **pazargs; int aidescs[3]; boolean fkeepuid; boolean fkeepenv; const char *zchdir; boolean fnosigs; boolean fshell; const char *zpath; const char *zuu_machine; const char *zuu_user; { char *zshcmd; int i; char *azenv[9]; char **pazenv; boolean ferr; #if HAVE_FULLDUPLEX_PIPES boolean ffullduplex; #endif int ierr = 0; int onull; int aichild_descs[3]; int cpar_close; int aipar_close[4]; int cchild_close; int aichild_close[3]; pid_t iret = 0; const char *zcmd; /* If we might have to use the shell, allocate enough space for the quoted command before forking. Otherwise the allocation would modify the data segment and we could not safely use vfork. */ zshcmd = NULL; if (fshell) { size_t clen; clen = 0; for (i = 0; pazargs[i] != NULL; i++) clen += strlen (pazargs[i]); zshcmd = zbufalc (2 * clen + i); } /* Set up a standard environment. This is again done before forking because it will modify the data segment. */ if (fkeepenv) pazenv = environ; else { const char *zterm, *ztz; char *zspace; int ienv; if (zpath == NULL) zpath = CMDPATH; azenv[0] = zbufalc (sizeof "PATH=" + strlen (zpath)); sprintf (azenv[0], "PATH=%s", zpath); zspace = azenv[0] + sizeof "PATH=" - 1; while ((zspace = strchr (zspace, ' ')) != NULL) *zspace = ':'; azenv[1] = zbufalc (sizeof "HOME=" + strlen (zSspooldir)); sprintf (azenv[1], "HOME=%s", zSspooldir); zterm = getenv ("TERM"); if (zterm == NULL) zterm = "unknown"; azenv[2] = zbufalc (sizeof "TERM=" + strlen (zterm)); sprintf (azenv[2], "TERM=%s", zterm); azenv[3] = zbufcpy ("SHELL=/bin/sh"); azenv[4] = zbufalc (sizeof "USER=" + strlen (OWNER)); sprintf (azenv[4], "USER=%s", OWNER); ienv = 5; ztz = getenv ("TZ"); if (ztz != NULL) { azenv[ienv] = zbufalc (sizeof "TZ=" + strlen (ztz)); sprintf (azenv[ienv], "TZ=%s", ztz); ++ienv; } if (zuu_machine != NULL) { azenv[ienv] = zbufalc (sizeof "UU_MACHINE=" + strlen (zuu_machine)); sprintf (azenv[ienv], "UU_MACHINE=%s", zuu_machine); ++ienv; } if (zuu_user != NULL) { azenv[ienv] = zbufalc (sizeof "UU_USER=" + strlen (zuu_user)); sprintf (azenv[ienv], "UU_USER=%s", zuu_user); ++ienv; } azenv[ienv] = NULL; pazenv = azenv; } /* Set up any needed pipes. */ ferr = FALSE; onull = -1; cpar_close = 0; cchild_close = 0; #if HAVE_FULLDUPLEX_PIPES ffullduplex = (aidescs[0] == SPAWN_WRITE_PIPE && aidescs[1] == SPAWN_READ_PIPE); #endif for (i = 0; i < 3; i++) { if (aidescs[i] == SPAWN_NULL) { if (onull < 0) { onull = open ((char *) "/dev/null", O_RDWR); if (onull < 0 || fcntl (onull, F_SETFD, fcntl (onull, F_GETFD, 0) | FD_CLOEXEC) < 0) { ierr = errno; (void) close (onull); ferr = TRUE; break; } aipar_close[cpar_close] = onull; ++cpar_close; } aichild_descs[i] = onull; } else if (aidescs[i] != SPAWN_READ_PIPE && aidescs[i] != SPAWN_WRITE_PIPE) aichild_descs[i] = aidescs[i]; else { int aipipe[2]; #if HAVE_FULLDUPLEX_PIPES if (ffullduplex && i == 1) { /* Just use the fullduplex pipe. */ aidescs[i] = aidescs[0]; aichild_descs[i] = aichild_descs[0]; continue; } #endif if (pipe (aipipe) < 0) { ierr = errno; ferr = TRUE; break; } if (aidescs[i] == SPAWN_READ_PIPE) { aidescs[i] = aipipe[0]; aichild_close[cchild_close] = aipipe[0]; aichild_descs[i] = aipipe[1]; aipar_close[cpar_close] = aipipe[1]; } else { aidescs[i] = aipipe[1]; aichild_close[cchild_close] = aipipe[1]; aichild_descs[i] = aipipe[0]; aipar_close[cpar_close] = aipipe[0]; } ++cpar_close; ++cchild_close; if (fcntl (aipipe[0], F_SETFD, fcntl (aipipe[0], F_GETFD, 0) | FD_CLOEXEC) < 0 || fcntl (aipipe[1], F_SETFD, fcntl (aipipe[1], F_GETFD, 0) | FD_CLOEXEC) < 0) { ierr = errno; ferr = TRUE; break; } } } #if DEBUG > 1 if (! ferr && FDEBUGGING (DEBUG_EXECUTE)) { ulog (LOG_DEBUG_START, "Forking %s", pazargs[0]); for (i = 1; pazargs[i] != NULL; i++) ulog (LOG_DEBUG_CONTINUE, " %s", pazargs[i]); ulog (LOG_DEBUG_END, "%s", ""); } #endif if (! ferr) { /* This should really be vfork if available. */ iret = ixsfork (); if (iret < 0) { ferr = TRUE; ierr = errno; } } if (ferr) { for (i = 0; i < cchild_close; i++) (void) close (aichild_close[i]); iret = -1; } if (iret != 0) { /* The parent. Close the child's ends of the pipes and return the process ID, or an error. */ for (i = 0; i < cpar_close; i++) (void) close (aipar_close[i]); ubuffree (zshcmd); if (! fkeepenv) { char **pz; for (pz = azenv; *pz != NULL; pz++) ubuffree (*pz); } errno = ierr; return iret; } /* The child. */ #ifdef STDIN_FILENO #if STDIN_FILENO != 0 || STDOUT_FILENO != 1 || STDERR_FILENO != 2 #error The following code makes invalid assumptions #endif #endif for (i = 0; i < 3; i++) { if (aichild_descs[i] != i) (void) dup2 (aichild_descs[i], i); /* This should only be necessary if aichild_descs[i] == i, but some systems copy the close-on-exec flag for a dupped descriptor, which is wrong according to POSIX. */ (void) fcntl (i, F_SETFD, fcntl (i, F_GETFD, 0) &~ FD_CLOEXEC); } zcmd = pazargs[0]; pazargs[0] = strrchr (zcmd, '/'); if (pazargs[0] == NULL) pazargs[0] = zcmd; else ++pazargs[0]; if (! fkeepuid) { /* Return to the uid of the invoking user. */ (void) setuid (getuid ()); (void) setgid (getgid ()); } else { /* Try to force the UUCP uid to be both real and effective user ID, in order to present a consistent environment regardless of the invoking user. This won't work on older System V based systems, where it can cause trouble if ordinary users wind up executing uuxqt, perhaps via uucico; any program which uuxqt executes will have an arbitrary real user ID, so if the program is itself a setuid program, any security checks it does based on the real user ID will be incorrect. Fixing this problem would seem to require a special setuid root program; I have not used this approach because modern systems should not suffer from it. */ #if HAVE_SETREUID (void) setreuid (geteuid (), -1); (void) setregid (getegid (), -1); #else (void) setuid (geteuid ()); (void) setgid (getegid ()); #endif } if (zchdir != NULL) { (void) chdir (zchdir); } if (fnosigs) { #ifdef SIGHUP (void) signal (SIGHUP, SIG_IGN); #endif #ifdef SIGINT (void) signal (SIGINT, SIG_IGN); #endif #ifdef SIGQUIT (void) signal (SIGQUIT, SIG_IGN); #endif } #ifdef isc386 #ifdef _POSIX_SOURCE /* ISC has a remarkably stupid notion of environments. If a program is compiled in the POSIX environment, it sets a process state. If you then exec a program which expects the USG environment, the process state is not reset, so the execed program fails. The __setostype call is required to change back to the USG environment. This ought to be a switch in policy.h, but it seems too trivial, so I will leave this code here and wait for it to break in some fashion in the next version of ISC. */ __setostype (0); #endif #endif (void) execve ((char *) zcmd, (char **) pazargs, pazenv); /* The exec failed. If permitted, try using /bin/sh to execute a shell script. */ if (errno == ENOEXEC && fshell) { char *zto; const char *azshargs[4]; pazargs[0] = zcmd; zto = zshcmd; for (i = 0; pazargs[i] != NULL; i++) { const char *zfrom; for (zfrom = pazargs[i]; *zfrom != '\0'; zfrom++) { /* Some versions of /bin/sh appear to have a bug such that quoting a '/' sometimes causes an error. I don't know exactly when this happens (I can recreate it on Ultrix 4.0), but in any case it is harmless to not quote a '/'. */ if (*zfrom != '/') *zto++ = '\\'; *zto++ = *zfrom; } *zto++ = ' '; } *(zto - 1) = '\0'; azshargs[0] = "sh"; azshargs[1] = "-c"; azshargs[2] = zshcmd; azshargs[3] = NULL; (void) execve ((char *) "/bin/sh", (char **) azshargs, pazenv); } _exit (EXIT_FAILURE); /* Avoid compiler warning. */ return -1; }