/* $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 #include #include #include #include #include #include "captive/client.h" #ifdef HAVE_ORBIT_LINK char *link_get_tmpdir(void); void link_set_tmpdir(const char *dir); #else #include /* for linc_set_tmpdir() */ #endif /* Do not: #include "../../libcaptive/sandbox/split.h" * for captive_sandbox_fd_closeup(); FIXME * * as it has libcaptive-dependent includes conditioned by ORBIT2. * FIXME: Unify this declaration: */ void captive_sandbox_fd_closeup(int fd_first_to_delete); void captive_corba_sandbox_child(const gchar *chrooted_orbit_dir); /* CONFIG: */ /* FIXME: We hit linc-1.0.1/src/linc-protocols.c/linc_protocol_get_sockaddr_unix() * limit of socket pathname 64 characters. * With CHROOT_PATH_HASHKEY_LENGTH 12 "linc-%x-%x-%x%x" still does not fit completely. */ #define CHROOT_PATH_HASHKEY_LENGTH (12) 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 gint optarg_no_rlimit=0; 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")), SANDBOX_SERVER_POPT("no-rlimit",POPT_ARG_NONE ,&optarg_no_rlimit, N_("Disable setrlimit(2) restrictions"),NULL), #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,const gchar *prefix) { 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; if (strncmp(dirent->d_name,prefix,strlen(prefix))) 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 void sandbox_server_rlimit(int resource,const gchar *resource_string,rlim_t rlim_max) { struct rlimit rlim; rlim.rlim_cur=rlim.rlim_max=rlim_max; if (setrlimit(resource,&rlim)) fatal("setrlimit(%s,%d): %m",resource_string,(int)rlim_max); if (getrlimit(resource,&rlim)) fatal("getrlimit(%s,%d): %m",resource_string,(int)rlim_max); if (rlim.rlim_cur!=rlim_max || rlim.rlim_max!=rlim_max) fatal("Unsuccessful setrlimit(%s)",resource_string); } static void sandbox_server_mkdir_p(const gchar *dirpathname) { gchar *pathname=(/* de-const */ gchar *)captive_strdup_alloca(dirpathname); gchar *gs,*gs2; /* Missing mkdir(2) of the last component path is intentional: */ for (gs=pathname;(gs2=strchr(gs,'/'));gs=gs2) { *gs2='\0'; if (*pathname && mkdir(pathname,S_ISVTX|0777)) { if (errno!=EEXIST) fatal("Failed to mkdir(\"%s\"): %m",pathname); } *gs2++='/'; } } 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; const gchar *chroot_pid_hashkey_dir=NULL; 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); } /* Prevent: GLib-WARNING **: getpwuid_r(): failed due to unknown user id (42) * Try to invoke GLib g_get_any_init() before possible chroot(2) below. */ g_get_user_name(); g_get_real_name(); g_get_home_dir(); g_get_tmp_dir(); /* Pre-resolve "link_get_tmpdir" symbol to prevent its later failed * resolving in chroot(2) mode in Debian dynamic build. */ #ifdef HAVE_ORBIT_LINK g_free(link_get_tmpdir()); /* returns g_strdup()ed string */ #else g_free(linc_get_tmpdir()); /* returns g_strdup()ed string */ #endif if (fragile && !optarg_chroot) fatal("Fragile setuid/root environment but no --chroot set"); if (optarg_chroot) { const gchar *chroot_pid_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,"s-"); chrooted_cleanuplockeddirs(captive_printf_alloca("%s/tmp",optarg_chroot),"captive-orbit-"); } chroot_pid_dir=captive_printf_alloca("%s/s-%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); printf("chroot_pid_hashkey_dir=%s\n",chroot_pid_hashkey_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 /t for /t/o-$PID directories for ORBit2 * and also for parent's hardlink to its /t/o-$pid directory. */ if (optarg_chroot) { gchar *chrooted_orbit_dir_old; if (mkdir("/t",S_ISVTX|0777)) { if (errno!=EEXIST) fatal("Failed to mkdir(\"%s\"): %m","/t"); } 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\n",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"); } g_assert(chroot_pid_hashkey_dir!=NULL); chrooted_orbit_dir=g_strdup_printf("%s/t/o-%d",chroot_pid_hashkey_dir,getpid()); /* Last pathname component is not created: */ sandbox_server_mkdir_p(chrooted_orbit_dir); /* Prepare '/tmp' for the initial CORBA_ORB_init() default path. * Workaround sandbox_server_mkdir_p() does not create last component. * Do not use '/tmp' directly as some distributions may set custom * tmpdir pathname by $ENV{"TMPDIR"} etc. */ sandbox_server_mkdir_p(captive_printf_alloca("%s/",g_get_tmp_dir())); /* Set '0700' to prevent: Wrong permissions for ... * by linc-1.0.1-1/src/linc-protocols.c/make_local_tmpdir() */ if (mkdir(chrooted_orbit_dir,0700)) { /* Do not: g_assert(errno==EEXIST); * as if 'optarg_chroot' the whole chroot(2)ed directory should be ours. */ fatal("Cannot created chrooted_orbit_dir \"%s\": %m",chrooted_orbit_dir); } /* Init 'orb' to pass through its linc_set_tmpdir() to not to be overriden below. */ { CORBA_ORB orb; CORBA_Environment ev; int orb_argc=1; gchar *orb_argv[]={ (gchar *)captive_strdup_alloca("captive-sandbox-server"), NULL}; CORBA_exception_init(&ev); /* libcaptive is single-threaded only, caller must lock it. * If thread A spawned the sandbox while currently doing its own work * and thread B calls the sandbox thread B waits on ORB_run() * while the sandbox waits for the response of thread A ORB. Deadlock. * "orbit-local-non-threaded-orb" requests thread unaware ORB. */ orb=CORBA_ORB_init(&orb_argc,orb_argv,"orbit-local-non-threaded-orb",&ev); if (orb==CORBA_OBJECT_NIL) fatal("Cannot initialize CORBA ORB (CORBA_OBJECT_NIL): %m"); if (ev._major!=CORBA_NO_EXCEPTION) fatal("Cannot initialize CORBA ORB (exception): %m"); } #ifdef HAVE_ORBIT_LINK chrooted_orbit_dir_old=link_get_tmpdir(); /* returns g_strdup()ed string */ #else chrooted_orbit_dir_old=linc_get_tmpdir(); /* returns g_strdup()ed string */ #endif g_assert(chrooted_orbit_dir_old!=NULL); #ifdef HAVE_ORBIT_LINK link_set_tmpdir(chrooted_orbit_dir); #else linc_set_tmpdir(chrooted_orbit_dir); #endif if (!*chrooted_orbit_dir_old) fatal("Cannot detect chrooted_orbit_dir: --with-orbit-line incompatible with ORBit2 version"); if (rmdir(chrooted_orbit_dir_old)) fatal("Cannot remove old chrooted_orbit_dir \"%s\": %m",chrooted_orbit_dir_old); g_free(chrooted_orbit_dir_old); /* chmod(2) it to prevent mode limitation by * active ulimit(2) of being executed by mount(8). */ /* Set '0777' as our parent does not have 'captive' user permissions. */ if (chmod(chrooted_orbit_dir,S_ISVTX|0777)) fatal("Cannot chmod 0%o chrooted_orbit_dir \"%s\": %m",S_ISVTX|0777,chrooted_orbit_dir); printf("chrooted_orbit_dir=%s\n",chrooted_orbit_dir); } if (fragile || !optarg_no_rlimit) { #define SANDBOX_SERVER_RLIMIT(what,how) sandbox_server_rlimit((what),G_STRINGIFY(what),(how)) SANDBOX_SERVER_RLIMIT(RLIMIT_NPROC,0); SANDBOX_SERVER_RLIMIT(RLIMIT_MEMLOCK,0); SANDBOX_SERVER_RLIMIT(RLIMIT_CORE,0); SANDBOX_SERVER_RLIMIT(RLIMIT_FSIZE,0); SANDBOX_SERVER_RLIMIT(RLIMIT_NOFILE,16); /* >=6; newer ORBit2/link require >6 */ /* FIXME: Why flock(dirfd,...) in chrooted_createdir() succeeds?: */ SANDBOX_SERVER_RLIMIT(RLIMIT_LOCKS,0); #undef SANDBOX_SERVER_RLIMIT } 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