/* cvsd - chroot wrapper to run `cvs pserver' more securely. cvsd was originally written by Chris Black , http://cblack.mokey.com/. That was until release 0.6 cvsd versions up till 0.8b3 were maintained by Philippe Kehl , http://guv.ethz.ch/~flip/cvsd/, http://www.oinkzwurgl.org/software/cvsd/. after that Arthur de Jong took up the work and did a complete rewrite, http://ch.tudelft.nl/~arthur/cvsd. Copyright (C) 1999 Chris Black. Copyright (C) 2000 Philippe Kehl. Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006 Arthur de Jong. 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #include #include #include #include #include #ifdef HAVE_GETOPT_H #include #endif /* HAVE_GETOPT_H */ #ifndef HAVE_GETOPT_LONG #include "getopt_long.h" #endif /* not HAVE_GETOPT_LONG */ #ifndef HAVE_DAEMON #include "daemon.h" #endif /* not HAVE_DAEMON */ #ifndef HAVE_GETADDRINFO #include "getinfos.h" #endif /* not HAVE_GETADDRINFO */ #include #include #include #include #include #include #include #include #ifdef HAVE_NETDB_H #include #endif /* HAVE_NETDB_H */ #ifdef HAVE_FCNTL_H #include #endif /* HAVE_FCNTL_H */ #ifdef USE_LIBWRAP #ifdef HAVE_TCPD_H #include #endif /* HAVE_TCPD_H */ #endif /* USE_LIBWRAP */ #ifdef HAVE_GRP_H #include #endif /* HAVE_GRP_H */ #ifdef USE_CAPABILITIES #include #include #endif /* USE_CAPABILITIES */ #include "xmalloc.h" #ifdef HAVE_SETRLIMIT #include "reslimit.h" #endif /* HAVE_SETRLIMIT */ #include "cfg.h" #include "cfgfile.h" #include "log.h" /* the definition of the environment */ extern char **environ; /* the number of children spawned */ static volatile int cvsd_numchildren=0; /* the exit flag to indicate that a signal was received */ static volatile int cvsd_exitsignal=0; /* the server sockets */ #define MAXSERVERSOCKETS 16 static int cvsd_serversockets[MAXSERVERSOCKETS]; static int cvsd_serversocketnum=0; /* the number of seconds to sleep when no more connections can be listened for */ #define SLEEPSECS 5 /* libwrap logging settings */ #ifdef USE_LIBWRAP int allow_severity=LOG_INFO; int deny_severity=LOG_NOTICE; #endif /* USE_LIBWRAP */ /* display version information */ static void display_version(FILE *fp) { fprintf(fp,"%s\n",PACKAGE_STRING); fprintf(fp,"Written by Chris Black, Philippe Kehl and Arthur de Jong.\n\n"); fprintf(fp,"Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006 Chris Black,\n" "Philippe Kehl and Arthur de Jong.\n" "This is free software; see the source for copying conditions. There is NO\n" "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); } /* display usage information to stdout and exit(status) */ static void display_usage(FILE *fp, const char *program_name) { fprintf(fp,"Usage: %s [OPTION]...\n",program_name); fprintf(fp,"chroot wrapper to run `cvs pserver' more securely.\n"); fprintf(fp," -f, --config=FILE use FILE as configfile (default %s)\n",DEFAULT_CONFIGFILE); fprintf(fp," -d, --debug don't fork and print debugging to stderr\n"); fprintf(fp," --help display this help and exit\n"); fprintf(fp," --version output version information and exit\n"); fprintf(fp,"\n" "Report bugs to <%s>.\n",PACKAGE_BUGREPORT); } /* the definition of options for getopt(). see getopt(2) */ static struct option const cvsd_options[] = { { "config", required_argument, NULL, 'f' }, { "debug", no_argument, NULL, 'd' }, { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { NULL, 0, NULL, 0 } }; #define CVSD_OPTIONSTRING "f:dhV" /* parse command line options and save settings in struct */ static void parse_cmdline(int argc,char *argv[],struct cvsd_cfg *cfg) { int optc; while ((optc=getopt_long(argc,argv,CVSD_OPTIONSTRING,cvsd_options,NULL))!=-1) { switch (optc) { case 'f': /* -f, --config=FILE use FILE as configfile */ if (optarg[0]!='/') { fprintf(stderr,"%s: configfile '%s' must be an absolute path\n", argv[0],optarg); exit(1); } cfg->configfile=xstrdup(optarg); break; case 'd': /* -d, --debug don't fork and print debugging to stderr */ cfg->debugging=1; log_setdefaultloglevel(LOG_DEBUG); break; case 'h': /* --help display this help and exit */ display_usage(stdout,argv[0]); exit(0); case 'V': /* --version output version information and exit */ display_version(stdout); exit(0); case ':': /* missing required parameter */ case '?': /* unknown option character or extraneous parameter */ default: fprintf(stderr,"Try `%s --help' for more information.\n", argv[0]); exit(1); } } /* check for remaining arguments */ if (optind0) { /* shouldn't have a race here because we don't have threads */ cvsd_numchildren--; if (WIFSIGNALED(status)) { log_log(LOG_INFO,"cvs command exited on signal %s (%d) with exit-status %d", signame((int)WTERMSIG(status)), (int)WTERMSIG(status), (int)WEXITSTATUS(status)); } else { log_log(LOG_INFO,"cvs command exited%s with exit-status %d", WIFEXITED(status)?"":" abnormally", (int)WEXITSTATUS(status)); } } } /* signal handler for dying children */ static RETSIGTYPE sigchld_handler(int signum) { } /* signal handler for closing down */ static RETSIGTYPE sigexit_handler(int signum) { cvsd_exitsignal=signum; } /* do cleaning up before terminating */ static void exithandler(void) { int i; for (i=0;i<=cvsd_serversocketnum;i++) if (close(cvsd_serversockets[i])) log_log(LOG_WARNING,"problem closing server socket (ignored): %s",strerror(errno)); log_log(LOG_INFO,"version %s bailing out",VERSION); } /* handle a connection by doing fork() and stuff */ static void handleconnection(int csock,const struct cvsd_cfg *cfg) { pid_t cpid; int i,m; /* we have a possible race here? (probably atomic) */ cvsd_numchildren++; /* fork off a child to handle the connection */ switch (cpid=fork()) { case 0: /* we are the child */ /* set limits */ setreslimits(); /* connect socket to stdin/stdout/stderr */ if ( (dup2(csock,0)<0) || /* stdin */ (dup2(csock,1)<0) || /* stdout */ (dup2(csock,2)<0) ) /* stderr */ { log_log(LOG_ERR,"dup failed: %s",strerror(errno)); _exit(1); } /* close all other file descriptors */ m=sysconf(_SC_OPEN_MAX); for (i=3;icvscmd,cfg->cvsargs,cfg->cvsenv); /* if we are here there has been an error */ /* we can't log since we don't have any useful file descriptors */ close(0); close(1); close(2); _exit(1); break; case -1: /* we are the parent, but have an error */ log_log(LOG_ERR,"fork() failed: %s",strerror(errno)); exit(1); break; } /* we are the parent and have a cpid */ log_log(LOG_DEBUG,"debug: fork() succeeded (child pid=%d)",(int)cpid); /* close the socket to prevent confusion */ if (close(csock)) log_log(LOG_WARNING,"problem closing connection socket in parent (ignored): %s",strerror(errno)); } /* create socket, bind and listen on the specified address */ static void listenonaddresses(const struct cvsd_addrs *addrs) { int flag; int sock; char host[80],serv[40]; int i; struct addrinfo *last=NULL; struct addrinfo *addr; int count=0; for (addr=addrs->addrs;addr!=NULL;addr=addr->ai_next) { switch (i=getnameinfo(addr->ai_addr,addr->ai_addrlen, host,80,serv,40, NI_NUMERICHOST|NI_NUMERICSERV)<0) { case 0: log_log(LOG_DEBUG,"debug: binding %s %s family=%d socktype=%d protocol=%d",host,serv, addr->ai_family,addr->ai_socktype,addr->ai_protocol); break; case EAI_SYSTEM: log_log(LOG_DEBUG,"debug: getnameinfo() failed: %s",strerror(errno)); break; default: log_log(LOG_DEBUG,"debug: getnameinfo() failed: %s",gai_strerror(i)); break; } /* ignore unix domain sockets (i.e. named sockets) */ if (addr->ai_family==PF_UNIX) { log_log(LOG_DEBUG,"debug: do not try unix domain socket (family %d)",addr->ai_family); continue; } /* create socket */ if ((sock=socket(addr->ai_family,addr->ai_socktype,addr->ai_protocol))<0) { if ((errno==EAFNOSUPPORT)||(errno==EINVAL)) { log_log(LOG_DEBUG,"debug: socket() failed (ignored): %s",strerror(errno)); continue; } log_log(LOG_ERR,"cannot create socket: %s",strerror(errno)); exit(1); } /* bind socket even if port is in close wait state */ flag=1; if (setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(int))!=0) { log_log(LOG_ERR,"setsockopt(SO_REUSEADDR) failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } #ifdef TODO /* close the socket fast (SO_LINGER) */ struct linger { int l_onoff; /* linger active */ int l_linger; /* how many seconds to linger for */ }; if (setsockopt(sock,SOL_SOCKET,SO_LINGER,&**,sizeof(**))!=0) { log_log(LOG_ERR,"setsockopt(SO_LINGER) failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } #endif /* TODO */ /* do not block on accept() */ if ((i=fcntl(sock,F_GETFL,0))<0) { log_log(LOG_ERR,"fctnl(F_GETFL) failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } if (fcntl(sock,F_SETFL,i|O_NONBLOCK)<0) { log_log(LOG_ERR,"fctnl(F_SETFL,O_NONBLOCK) failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } /* bind the socket to the specified port */ if (bind(sock,addr->ai_addr,addr->ai_addrlen)<0) { if ((errno==EADDRINUSE)&&(last!=NULL)&& (last->ai_family==AF_INET6)&& (addr->ai_family==AF_INET)) { log_log(LOG_DEBUG,"debug: bind() failed (ignored): %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket (ignored): %s",strerror(errno)); continue; } log_log(LOG_ERR,"bind() failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } /* start listening for connections */ if (listen(sock,1)<0) { log_log(LOG_ERR,"listen() failed: %s",strerror(errno)); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } /* add the allocated socket to the list of ok sockets */ if (cvsd_serversocketnum>=MAXSERVERSOCKETS) { log_log(LOG_ERR,"maximum number of listening serversockets reached: edit MAXSERVERSOCKETS in cvsd.c and recompile!"); if (close(sock)) log_log(LOG_WARNING,"problem closing socket: %s",strerror(errno)); exit(1); } cvsd_serversockets[cvsd_serversocketnum++]=sock; /* log stuff */ switch (i=getnameinfo(addr->ai_addr,addr->ai_addrlen, host,80,serv,40, NI_NUMERICHOST|NI_NUMERICSERV)<0) { case 0: log_log(LOG_INFO,"listening on %s %s",host,serv); break; case EAI_SYSTEM: log_log(LOG_ERR,"getnameinfo() failed (ignored): %s",strerror(errno)); break; default: log_log(LOG_ERR,"getnameinfo() failed (ignored): %s",gai_strerror(i)); break; } last=addr; count++; } /* next addr */ if (count<=0) { log_log(LOG_ERR,"failed binding any address for %s %s: %s", addrs->node,addrs->service,strerror(errno)); exit(1); } } /* accept a connection on the socket and spawn a child cvs */ static void acceptconnection(const struct cvsd_cfg *cfg) { int csock; fd_set fds; int i; int j; int max=0; struct sockaddr_storage addr; socklen_t alen; char hostbuf[80],serv[40]; char *host; #ifdef USE_LIBWRAP struct request_info rinfo; #endif /* USE_LIBWRAP */ /* clear the set */ FD_ZERO(&fds); for (i=0;imax) max=cvsd_serversockets[i]+1; } /* wait for something to change */ if (select(max,&fds,NULL,NULL,NULL)<0) { if (errno==EINTR) { log_log(LOG_DEBUG,"debug: select() failed (ignored): %s",strerror(errno)); return; } log_log(LOG_ERR,"select() failed: %s",strerror(errno)); exit(1); } for (i=0;icvsenv; /* parse the command line */ parse_cmdline(argc,argv,cfg); /* begin building command line */ cfg_addcvsarg(cfg,"cvs"); cfg_addcvsarg(cfg,"-f"); /* do not read ~/.cvsrc */ #ifdef DEBUG cfg_addcvsarg(cfg,"-t"); /* verbose */ #endif /* DEBUG */ /* read the configuration file */ read_configfile(cfg->configfile,cfg); /* specify cvs_command if not set earlier */ if (cfg->cvscmd==NULL) { cfg->cvscmd=( (cfg->rootjail!=NULL) && (strcmp(cfg->rootjail,"none")!=0) )?"/bin/cvs":CVS_LOCATION; } log_log(LOG_DEBUG,"debug: cvscmd: %s",cfg->cvscmd); /* add cvs action */ cfg_addcvsarg(cfg,"pserver"); /* run cvs as pserver */ /* dump all arguments */ for (i=0;cfg->cvsargs[i]!=NULL;i++) log_log(LOG_DEBUG,"debug: cvsargs[%d]: %s",i,cfg->cvsargs[i]); /* complete environment for cvs */ cfg->cvsenv[CVSUMASK_IDX]=(char *)xmalloc(13*sizeof(char)); i=snprintf(cfg->cvsenv[CVSUMASK_IDX],13,"CVSUMASK=%03o",cfg->umask); if ( (i<0) || (i>12) ) { log_log(LOG_ERR,"fatal error saving umask in environment (probably a bug)"); exit(1); } /* dump environment */ for (i=0;cfg->cvsenv[i]!=NULL;i++) log_log(LOG_DEBUG,"debug: cvsenv[%d]: %s",i,cfg->cvsenv[i]); /* default address */ if (cfg->addresses==NULL) cfg_addaddress(cfg,NULL,0,DEFAULT_ADDR,DEFAULT_PORT); /* daemonize */ if ((!cfg->debugging)&&(daemon(0,0)<0)) { log_log(LOG_ERR,"unable to daemonize: %s",strerror(errno)); exit(1); } /* intilialize logging */ if (!cfg->debugging) log_startlogging(); #ifdef DEBUG log_log(LOG_INFO,"version %s starting (debugging enabled)",VERSION); #else /* DEBUG */ log_log(LOG_INFO,"version %s starting",VERSION); #endif /* not DEBUG */ /* install handler to close stuff off on exit and log notice */ atexit(exithandler); /* write pidfile */ write_pidfile(cfg->pidfile); /* set the umask */ umask(cfg->umask); /* start listening on addresses from configfile */ for (tmp=cfg->addresses;tmp!=NULL;tmp=nxt) { listenonaddresses(tmp); nxt=tmp->next; freeaddrinfo(tmp->addrs); free(tmp->node); free(tmp->service); free(tmp); } cfg->addresses=NULL; /* do the chroot stuff */ if ( (cfg->rootjail!=NULL) && (strcmp(cfg->rootjail,"none")!=0) ) { if (chdir(cfg->rootjail)!=0) { log_log(LOG_ERR,"cannot chdir(%s): %s",cfg->rootjail,strerror(errno)); exit(1); } if (chroot(cfg->rootjail)!=0) { log_log(LOG_ERR,"cannot chroot(%s): %s",cfg->rootjail,strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: chroot(%s) done",cfg->rootjail); } /* just to be sure */ chdir("/"); /* renice */ errno=0; /* for strange nice implementations */ nice(cfg->nice); if (errno!=0) { log_log(LOG_ERR,"cannot nice(%d): %s",(int)cfg->nice,strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: nice(%d) done",(int)cfg->nice); #ifdef HAVE_SETGROUPS /* drop all supplemental groups */ if (setgroups(0,NULL)<0) { log_log(LOG_WARNING,"cannot setgroups(0,NULL) (ignored): %s",strerror(errno)); } else { log_log(LOG_DEBUG,"debug: setgroups(0,NULL) done"); } #else /* HAVE_SETGROUPS */ log_log(LOG_DEBUG,"debug: setgroups() not available"); #endif /* not HAVE_SETGROUPS */ #ifdef USE_CAPABILITIES /* if there are any capabilities defined, set them to be kept across setuid() calls so we can limit them later on */ if (cfg->capabilities!=NULL) { if (prctl(PR_SET_KEEPCAPS,1)) { log_log(LOG_ERR,"cannot prctl(PR_SET_KEEPCAPS,1): %s",strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: prctl(PR_SET_KEEPCAPS,1) done"); /* dump the current capabilities */ caps=cap_get_proc(); log_log(LOG_DEBUG,"debug: current capabilities: %s",cap_to_text(caps,NULL)); cap_free(caps); } #endif /* USE_CAPABILITIES */ /* change to cvs gid */ if (cfg->gid!=NOGID) { if (setgid(cfg->gid)!=0) { log_log(LOG_ERR,"cannot setgid(%d): %s",(int)cfg->gid,strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: setgid(%d) done",cfg->gid); } /* change to cvs uid */ if (cfg->uid!=NOUID) { if (setuid(cfg->uid)!=0) { log_log(LOG_ERR,"cannot setuid(%d): %s",(int)cfg->uid,strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: setuid(%d) done",cfg->uid); } #ifdef USE_CAPABILITIES /* if there are any capabilities defined, limit them now */ if (cfg->capabilities!=NULL) { /* dump the current capabilities */ caps=cap_get_proc(); log_log(LOG_DEBUG,"debug: current capabilities: %s",cap_to_text(caps,NULL)); cap_free(caps); /* limit the capabilities */ if (cap_set_proc(cfg->capabilities)!=0) { log_log(LOG_ERR,"cannot cap_set_proc(%s): %s",cap_to_text(cfg->capabilities,NULL),strerror(errno)); exit(1); } log_log(LOG_DEBUG,"debug: cap_set_proc(%2) done",cap_to_text(cfg->capabilities,NULL)); /* we no longer need this so we should free it */ cap_free(cfg->capabilities); /* dump the current capabilities */ caps=cap_get_proc(); log_log(LOG_DEBUG,"debug: current capabilities: %s",cap_to_text(caps,NULL)); cap_free(caps); } #endif /* USE_CAPABILITIES */ /* build cvs command line */ j=strlen(cfg->cvscmd)+1; for (i=1;cfg->cvsargs[i]!=NULL;i++) j+=strlen(cfg->cvsargs[i])+1; cvs_cmdline=(char *)xmalloc(j*sizeof(char)); strcpy(cvs_cmdline,cfg->cvscmd); for (i=1;cfg->cvsargs[i]!=NULL;i++) { strcat(cvs_cmdline," "); strcat(cvs_cmdline,cfg->cvsargs[i]); } log_log(LOG_DEBUG,"debug: cvs command to execute: '%s'",cvs_cmdline); /* install signalhandler for terminating children */ install_sighandler(SIGCHLD,sigchld_handler); /* install signalhandlers for some other signals */ install_sighandler(SIGHUP, sigexit_handler); install_sighandler(SIGINT, sigexit_handler); install_sighandler(SIGQUIT,sigexit_handler); install_sighandler(SIGILL, sigexit_handler); install_sighandler(SIGABRT,sigexit_handler); install_sighandler(SIGSEGV,sigexit_handler); install_sighandler(SIGPIPE,sigexit_handler); install_sighandler(SIGALRM,sigexit_handler); install_sighandler(SIGTERM,sigexit_handler); install_sighandler(SIGUSR1,sigexit_handler); install_sighandler(SIGUSR2,sigexit_handler); log_log(LOG_INFO,"accepting connections"); /* start waiting for incoming connections */ while (cvsd_exitsignal==0) { /* if we are ready to handle a connection do so */ if ((cfg->maxconnections==0)||(cvsd_numchildrenmaxconnections)) { /* wait for a new connection */ acceptconnection(cfg); /* handle exiting child processes */ reap_deadchildren(); } else /* not ready for connection */ { /* log that the connections are full */ log_log(LOG_INFO,"too many connections (%d), not accepting new connections",cvsd_numchildren); /* wait until children have died */ while (cvsd_numchildren>=cfg->maxconnections) { /* sleep unblocks on a signal */ sleep(SLEEPSECS); /* handle exiting child processes */ reap_deadchildren(); } /* log that the connections are full */ log_log(LOG_INFO,"resuming accepting new connections"); } } /* print something about received signals */ if (cvsd_exitsignal!=0) { log_log(LOG_INFO,"caught signal %s (%d), shutting down", signame(cvsd_exitsignal),cvsd_exitsignal); } return 1; }