/* $Id$ * filesystem sandbox server stub for libcaptive * Copyright (C) 2003 Jan Kratochvil * * 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; exactly version 2 of June 1991 is required * * 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 "config.h" #include #include #include #include #include #include #include #include #include "captive/options.h" #include #include "captive/macros.h" #include #include #include #include #include #include "../../libcaptive/sandbox/split.h" /* for captive_sandbox_fd_closeup(); FIXME */ #include #include #include #include /* CONFIG: */ #define CHROOT_PATH_HASHKEY_LENGTH (64) GQuark sandbox_server_main_error_quark(void) { GQuark r=0; if (!r) r=g_quark_from_static_string("sandbox-server"); return r; } static gchar *optarg_setuid=CAPTIVE_SANDBOX_SETUID; static gchar *optarg_setgid=CAPTIVE_SANDBOX_SETGID; static gchar *optarg_chroot=CAPTIVE_SANDBOX_CHROOT; static const struct poptOption popt_table[]={ #define SANDBOX_SERVER_POPT(longname,argInfoP,argP,descripP,argDescripP) \ { \ longName: (longname), \ shortName: 0, \ argInfo: (argInfoP), \ arg: (void *)argP, \ val: 0, \ descrip: (descripP), \ argDescrip: (argDescripP), \ } SANDBOX_SERVER_POPT("setuid" ,POPT_ARG_STRING,&optarg_setuid,N_("Username or UID to become; \"-\" for disable"),N_("UID")), SANDBOX_SERVER_POPT("setgid" ,POPT_ARG_STRING,&optarg_setgid,N_("Groupname or GID to become; \"-\" for disable"),N_("GID")), SANDBOX_SERVER_POPT("chroot" ,POPT_ARG_STRING,&optarg_chroot,N_("Pathname to directory for chroot(2); \"-\" for disable"),N_("directory")), #undef SANDBOX_SERVER_POPT POPT_AUTOHELP POPT_TABLEEND }; static gchar *fatal_argv0; static void fatal(const char *fmt,...) { va_list ap; fprintf(stderr,"%s: ",fatal_argv0); va_start(ap,fmt); vfprintf(stderr,fmt,ap); va_end(ap); fprintf(stderr,"!\nAborting!\n"); exit(EXIT_FAILURE); /* NOTREACHED */ for (;;); } static void check_dir_safety(const gchar *dir) { gchar *local_dir; const gchar *cs; static gint depth=0; if (++depth>=1000) fatal("Loop count >=%d during check_dir_safety(\"%s\")",depth,dir); if (*dir!='/') fatal("chroot path \"%s\" not absolute",dir); dir=captive_printf_alloca("%s/",dir); local_dir=(gchar *)captive_strdup_alloca(dir); for (cs=dir;cs;cs=strchr(cs+1,'/')) { struct stat statbuf; g_assert(*cs=='/'); /* Include the trailing '/' to resolve the root directory as "/". */ memcpy(local_dir,dir,cs+1-dir); local_dir[cs+1-dir]=0; if (lstat(local_dir,&statbuf)) fatal("lstat(\"%s\") of chroot path component: %m",local_dir); if (S_ISLNK(statbuf.st_mode)) { char linkbuf[PATH_MAX]; int linkbuflen; if (0>(linkbuflen=readlink(local_dir,linkbuf,sizeof(linkbuf)-1))) fatal("readlink(\"%s\") of chroot path component: %m",local_dir); linkbuf[linkbuflen]=0; check_dir_safety(linkbuf); if (stat(local_dir,&statbuf)) /* NOT lstat(2) */ fatal("stat(\"%s\") of chroot path component: %m",local_dir); } if (!S_ISDIR(statbuf.st_mode)) fatal("lstat/stat(\"%s\") of chroot path component is !S_ISDIR",local_dir); if (statbuf.st_uid!=0) fatal("lstat/stat(\"%s\") of chroot path component has UID %d !=0",local_dir,(int)statbuf.st_uid); if (statbuf.st_gid!=0) fatal("lstat/stat(\"%s\") of chroot path component has GID %d !=0",local_dir,(int)statbuf.st_gid); if ((statbuf.st_mode&(S_IFDIR|S_ISVTX|0111)) != (S_IFDIR|0111)) fatal("lstat/stat(\"%s\") of chroot path component has mode 0%o !=04[01]111",local_dir,(int)statbuf.st_mode); } depth--; } static void chrooted_unlink_recursive(const gchar *pathname) { DIR *dir; struct dirent *dirent; static gint depth=0; struct stat statbuf; if (++depth>=1000) fatal("Loop count >=%d during chrooted_unlink_recursive(\"%s\")",depth,pathname); /* Security: Do not allow anyone to escape the sandbox directory by symlinks. */ if (lstat(pathname,&statbuf)) fatal("Cannot lstat(\"%s\") to delete leftover sandbox files: %m",pathname); if (!S_ISDIR(statbuf.st_mode)) { if (unlink(pathname)) fatal("Cannot unlink(\"%s\") to delete leftover sandbox files: %m",pathname); goto done; } if (!(dir=opendir(pathname))) fatal("Cannot opendir(\"%s\") to delete leftover sandbox files: %m",pathname); while (errno=0,(dirent=readdir(dir))) { gchar *dirent_path; if (!strcmp(dirent->d_name,".") || !strcmp(dirent->d_name,"..")) continue; dirent_path=g_strdup_printf("%s/%s",pathname,dirent->d_name); chrooted_unlink_recursive(dirent_path); g_free(dirent_path); } if (errno) fatal("Cannot readdir(\"%s\") during delete of leftover sandbox files: %m",pathname); if (closedir(dir)) fatal("Cannot closedir(\"%s\") during delete of leftover sandbox files: %m",pathname); if (rmdir(pathname)) fatal("Cannot rmdir(\"%s\") during delete of leftover sandbox files: %m",pathname); done: depth--; } static void chrooted_cleanuplockeddirs(const gchar *pathname) { DIR *dir; struct dirent *dirent; if (!(dir=opendir(pathname))) { if (errno!=ENOTDIR) fatal("Cannot opendir(\"%s\") to delete leftover sandbox files: %m",pathname); /* errno==ENOTDIR, a regular file */ if (unlink(pathname)) fatal("Cannot unlink(\"%s\") to delete leftover sandbox files: %m",pathname); return; } while (errno=0,(dirent=readdir(dir))) { gchar *dirent_path; int direntfd; if (!strcmp(dirent->d_name,".") || !strcmp(dirent->d_name,"..")) continue; dirent_path=g_strdup_printf("%s/%s",pathname,dirent->d_name); if (-1==(direntfd=open(dirent_path,O_RDONLY))) { if (errno==ENOENT) /* It could disappear in the meantime. */ goto next_dirent_free_dirent_path; fatal("Cannot open(\"%s\") as the child directory during delete of leftover sandbox files: %m",dirent_path); } if (flock(direntfd,LOCK_EX|LOCK_NB)) { if (errno==EWOULDBLOCK) /* Valid directory in use. */ goto next_dirent_close_direntfd; fatal("Cannot flock(\"%s\",LOCK_EX|LOCK_NB) child directory during delete of leftover sandbox files: %m",dirent_path); } chrooted_unlink_recursive(dirent_path); next_dirent_close_direntfd: if (close(direntfd)) fatal("Cannot close(\"%s\") child directory during delete of leftover sandbox files: %m",dirent_path); next_dirent_free_dirent_path: g_free(dirent_path); } if (errno) fatal("Cannot readdir(\"%s\") during delete of leftover sandbox files: %m",pathname); if (closedir(dir)) fatal("Cannot closedir(\"%s\") during delete of leftover sandbox files: %m",pathname); } static void chrooted_createdir(const gchar *dir,uid_t uid,gid_t gid,gboolean lock) { gint retries; for (retries=0;retries<10;retries++) { struct stat statbuf; int dirfd; if (mkdir(dir,0711)) { if (errno!=EEXIST) fatal("Failed to create chroot directory \"%s\": %m",dir); chrooted_unlink_recursive(dir); if (mkdir(dir,0711)) fatal("Failed to create chroot directory \"%s\" after attempted unlink: %m",dir); } if (!lock) break; dirfd=open(dir,O_RDONLY); if (dirfd==-1) { if (errno!=ENOENT) fatal("Failed to open created chroot directory \"%s\" to lock it: %m",dir); continue; } /* Do not use 'LOCK_NB' here as the garbage collector should release it soon. */ if (flock(dirfd,LOCK_EX)) fatal("Failed to lock created chroot directory \"%s\": %m",dir); if (lstat(dir,&statbuf)) { if (errno!=ENOENT) fatal("Failed to lstat(2) created chroot directory \"%s\": %m",dir); if (close(dirfd)) fatal("Failed to close created and locked chroot directory \"%s\": %m",dir); continue; } /* Leave 'dirfd' open to leave it LOCK_EX-ed. */ break; } if (chown(dir,uid,gid)) fatal("Failed to chown(\"%s\",%d,%d): %m",dir,uid,gid); if (chmod(dir,0711)) /* Just to be safe after chown(2); should be already done by mkdir(2). */ fatal("Failed to chmod(\"%s\",0%o): %m",dir,0711); } static const gchar *chrooted_orbit_dir; static void chroot_setup(gboolean fragile) { uid_t want_uid=0; const gchar *want_uid_name=NULL; gid_t want_gid=0; char *endptr; if (fragile) { captive_sandbox_fd_closeup(2 /* STDERR */ +1); clearenv(); } #define CLEANEMPTY(var) G_STMT_START { \ if ((var) && (!*(var) || *(var)=='-')) \ (var)=NULL; \ } G_STMT_END CLEANEMPTY(optarg_setgid); CLEANEMPTY(optarg_setuid); CLEANEMPTY(optarg_chroot); #undef CLEANEMPTY if (optarg_setgid) { long want_gidl; want_gidl=strtol(optarg_setgid,&endptr,10); if (!endptr || !*endptr) { want_gid=want_gidl; if (want_gidl<=0 || want_gid!=(gid_t)want_gidl) fatal("Numeric setgid not parsable: %s",optarg_setgid); } else { struct group *want_gid_group=NULL; if (!(want_gid_group=getgrnam(optarg_setgid))) fatal("Unable to query setgid group name \"%s\"",optarg_setgid); want_gid=want_gid_group->gr_gid; } } if (optarg_setuid) { long want_uidl; struct passwd *want_uid_passwd; want_uidl=strtol(optarg_setuid,&endptr,10); if (!endptr || !*endptr) { want_uid=want_uidl; if (want_uidl<=0 || want_uid!=(gid_t)want_uidl) fatal("Numeric setuid not parsable: %s",optarg_setuid); } else { if (!(want_uid_passwd=getpwnam(optarg_setuid))) fatal("Unable to query setuid user name \"%s\"",optarg_setuid); want_uid=want_uid_passwd->pw_uid; } if (!want_uid) fatal("Unable to detect setuid UID"); if (!(want_uid_passwd=getpwuid(want_uid))) fatal("Unable to query name of UID %d",(int)want_uid); want_uid_name=captive_strdup_alloca(want_uid_passwd->pw_name); } if (fragile && !optarg_chroot) fatal("Fragile setuid/root environment but no --chroot set"); if (optarg_chroot) { const gchar *chroot_pid_dir,*chroot_pid_hashkey_dir; GRand *grand; gchar chroot_hashkey[CHROOT_PATH_HASHKEY_LENGTH+1],*s; gint gi; check_dir_safety(optarg_chroot); if (!(grand=g_rand_new())) /* I hope g_rand_new() is security-safe. It looks so. */ fatal("Cannot initialize random number generator g_rand_new()"); for (s=chroot_hashkey;s=0 && gi<10) *s='0'+gi-(0); else if (gi>=10+0 && gi<10+26) *s='a'+gi-(10); else if (gi>=10+26+0 && gi<10+26+26) *s='A'+gi-(10+26); else g_assert_not_reached(); } g_rand_free(grand); *s=0; if (geteuid()==0) /* Not 'fragile' as we can be native 'root'. */ chrooted_cleanuplockeddirs(optarg_chroot); chroot_pid_dir=captive_printf_alloca("%s/sandbox-server-%d",optarg_chroot,(int)getpid()); chrooted_createdir(chroot_pid_dir,(!optarg_setuid ? (uid_t)-1 : want_uid),(!optarg_setgid ? (gid_t)-1 : want_gid), TRUE); /* lock */ chroot_pid_hashkey_dir=captive_printf_alloca("%s/%s",chroot_pid_dir,chroot_hashkey); chrooted_createdir(chroot_pid_hashkey_dir,(!optarg_setuid ? (uid_t)-1 : want_uid),(!optarg_setgid ? (gid_t)-1 : want_gid), FALSE); /* lock */ if (chroot(chroot_pid_hashkey_dir)) fatal("Failed to chroot(\"%s\"): %m",chroot_pid_hashkey_dir); if (chdir("/")) fatal("Failed to chdir(\"%s\"): %m","/"); /* Now it is safe to set umask(0000) as we are protected by 'chroot_hashkey'. * We need it to permit our spawning parent to hardlink its sockets to us. */ umask(0000); if (umask(0000)!=0000) fatal("Failed to set umask(0%o): %m",0000); if (want_uid_name) { printf("chroot_pid_hashkey_dir=%s\n",chroot_pid_hashkey_dir); chrooted_orbit_dir=g_strdup_printf("/tmp/orbit-%s",want_uid_name); printf("chrooted_orbit_dir=%s\n",chrooted_orbit_dir); } } if (fragile && !optarg_setgid) fatal("Fragile setuid/root environment but no --setgid set"); if (optarg_setgid) { if (!want_gid || setgid(want_gid)) fatal("Failed to setgid(%d)",(!want_gid ? -1 : (int)want_gid)); if (setgroups(1 /* size */,&want_gid)) fatal("Failed to setgroups(1,[%d])",(!want_gid ? -1 : (int)want_gid)); } if (fragile && !optarg_setuid) fatal("Fragile setuid/root environment but no --setuid set"); if (optarg_setuid) { if (!want_uid || setuid(want_uid)) fatal("Failed to setuid(%d)",(!want_uid ? -1 : (int)want_uid)); } /* Prepare /tmp for /tmp/orbit-$username directories for ORBit2 * and also for parent's hardlink to its /tmp/captive-orbit-$pid directory. */ if (optarg_chroot) { if (mkdir("/tmp",S_ISVTX|0777)) fatal("Failed to mkdir(\"%s\"): %m","/tmp"); if (mkdir("/etc",0700)) fatal("Failed to mkdir(\"%s\"): %m","/etc"); if (want_uid_name && want_uid && want_gid) { FILE *f; if (!(f=fopen("/etc/passwd","w"))) fatal("Failed to fopen(\"%s\",\"w\"): %m","/etc/passwd"); if (0>fprintf(f,"%s:*:%d:%d:%s:%s:/bin/false",want_uid_name,(int)want_uid,(int)want_gid,want_uid_name,optarg_chroot)) fatal("Failed to fprintf(\"%s\"): %m","/etc/passwd"); if (fclose(f)) fatal("Failed to fclose(\"%s\"): %m","/etc/passwd"); } } if (fragile) { gid_t gid_list[2]; int gid_list_size,i; if (getuid()!=want_uid) fatal("getuid()=%d != want_uid=%d",(int)getuid(),(int)want_uid); if (geteuid()!=want_uid) fatal("geteuid()=%d != want_uid=%d",(int)geteuid(),(int)want_uid); if (getgid()!=want_gid) fatal("getgid()=%d != want_gid=%d",(int)getgid(),(int)want_gid); if (getegid()!=want_gid) fatal("getegid()=%d != want_gid=%d",(int)getegid(),(int)want_gid); gid_list_size=getgroups(G_N_ELEMENTS(gid_list),gid_list); for (i=0;i