spawn.c   [plain text]


/* 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 <errno.h>

#if HAVE_FCNTL_H
#include <fcntl.h>
#else
#if HAVE_SYS_FILE_H
#include <sys/file.h>
#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;
}