-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 This patch looks good to me. acked. -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.11 (GNU/Linux) Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/ iEYEARECAAYFAk5yVRIACgkQrlYvE4MpobNTogCaA/XBPxA2GRq8Mux9nab3urM8 ivwAoKLZXJs4tVfbRBNTpQMXnlEgnDYs =3GvE -----END PGP SIGNATURE-----
>From 54ed5929b8f8ffac7bdc48d589c5cb38f6798530 Mon Sep 17 00:00:00 2001 From: Eric Paris <eparis@xxxxxxxxxx> Date: Mon, 15 Aug 2011 19:58:08 -0400 Subject: [PATCH 33/67] policycoreutils: sandbox: FIXME rewrite /tmp handling seunshare now creates a runtime temporary directory owned by root and with the sticky bit set properly. Files from the user-specified directory are copied to the runtime directory and the changes synced back (using rsync) at the end of the seunshare run. review needed to changelog correctness/completeness Signed-off-by: Eric Paris <eparis@xxxxxxxxxx> Acked-by: Dan Walsh <dwalsh@xxxxxxxxxx> --- policycoreutils/sandbox/sandbox | 8 +- policycoreutils/sandbox/seunshare.8 | 2 +- policycoreutils/sandbox/seunshare.c | 488 +++++++++++++++++++++++++++-------- 3 files changed, 386 insertions(+), 112 deletions(-) diff --git a/policycoreutils/sandbox/sandbox b/policycoreutils/sandbox/sandbox index 031fdc7..486cd4e 100644 --- a/policycoreutils/sandbox/sandbox +++ b/policycoreutils/sandbox/sandbox @@ -29,7 +29,6 @@ import commands import setools PROGNAME = "policycoreutils" -HOMEDIR=pwd.getpwuid(os.getuid()).pw_dir SEUNSHARE = "/usr/sbin/seunshare" SANDBOXSH = "/usr/share/sandbox/sandboxX.sh" import gettext @@ -374,23 +373,20 @@ sandbox [-h] [-c] [-l level ] [-[X|M] [-H homedir] [-T tempdir]] [-I includefile def __setup_dir(self): if self.__options.level or self.__options.session: return - sandboxdir = HOMEDIR + "/.sandbox" - if not os.path.exists(sandboxdir): - os.mkdir(sandboxdir) if self.__options.homedir: selinux.chcon(self.__options.homedir, self.__filecon, recursive=True) self.__homedir = self.__options.homedir else: selinux.setfscreatecon(self.__filecon) - self.__homedir = mkdtemp(dir=sandboxdir, prefix=".sandbox") + self.__homedir = mkdtemp(dir="/tmp", prefix=".sandbox_home_") if self.__options.tmpdir: selinux.chcon(self.__options.tmpdir, self.__filecon, recursive=True) self.__tmpdir = self.__options.tmpdir else: selinux.setfscreatecon(self.__filecon) - self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox") + self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox_tmp_") selinux.setfscreatecon(None) self.__copyfiles() diff --git a/policycoreutils/sandbox/seunshare.8 b/policycoreutils/sandbox/seunshare.8 index a9b846b..a1bf3fa 100644 --- a/policycoreutils/sandbox/seunshare.8 +++ b/policycoreutils/sandbox/seunshare.8 @@ -16,7 +16,7 @@ within the specified context, using the alternate home directory and /tmp direct Alternate homedir to be used by the application. Homedir must be owned by the user. .TP \fB\-t\ tmpdir -Use alternate temporary directory to mount on /tmp. tmpdir must be owned by the user. +Use alternate tempory directory to mount on /tmp. tmpdir must be owned by the user. .TP \fB\-c --cgroups\fR Use cgroups to control this copy of seunshare. Specify parameters in /etc/sysconfig/sandbox. Max memory usage and cpu usage are to be specified in percent. You can specify which CPUs to use by numbering them 0,1,2... etc. diff --git a/policycoreutils/sandbox/seunshare.c b/policycoreutils/sandbox/seunshare.c index 9645b23..a4d6cdc 100644 --- a/policycoreutils/sandbox/seunshare.c +++ b/policycoreutils/sandbox/seunshare.c @@ -10,6 +10,7 @@ #include <sys/wait.h> #include <syslog.h> #include <sys/mount.h> +#include <glob.h> #include <pwd.h> #include <sched.h> #include <libcgroup.h> @@ -99,7 +100,7 @@ static int set_signal_handles(void) (void)sigprocmask(SIG_SETMASK, &empty, NULL); - /* Terminate on SIGHUP. */ + /* Terminate on SIGHUP */ if (signal(SIGHUP, SIG_DFL) == SIG_ERR) { perror("Unable to set SIGHUP handler"); return -1; @@ -150,26 +151,6 @@ static int spawn_command(const char *cmd, uid_t uid){ } /** - * This function makes sure the mounted directory is owned by the user executing - * seunshare. - * If so, it returns 0. If it can not figure this out or they are different, it returns -1. - */ -static int verify_mount(const char *mntdir, struct passwd *pwd) { - struct stat sb; - if (stat(mntdir, &sb) == -1) { - fprintf(stderr, _("Invalid mount point %s: %s\n"), mntdir, strerror(errno)); - return -1; - } - if (sb.st_uid != pwd->pw_uid) { - errno = EPERM; - syslog(LOG_AUTHPRIV | LOG_ALERT, "%s attempted to mount an invalid directory, %s", pwd->pw_name, mntdir); - perror(_("Invalid mount point, reporting to administrator")); - return -1; - } - return 0; -} - -/** * Check file/directory ownership, struct stat * must be passed to the * functions. */ @@ -247,7 +228,7 @@ static int verify_shell(const char *shell_name) /* check the shell skipping newline char */ if (!strcmp(shell_name, buf)) { - rc = 1; + rc = 0; break; } } @@ -255,26 +236,60 @@ static int verify_shell(const char *shell_name) return rc; } -static int seunshare_mount(const char *src, const char *dst, struct passwd *pwd) { +/** + * Mount directory and check that we mounted the right directory. + */ +static int seunshare_mount(const char *src, const char *dst, struct stat *src_st) +{ + int flags = MS_REC; + int is_tmp = 0; + if (verbose) - printf("Mount %s on %s\n", src, dst); - if (mount(dst, dst, NULL, MS_BIND | MS_REC, NULL) < 0) { + printf(_("Mounting %s on %s\n"), src, dst); + + if (strcmp("/tmp", dst) == 0) { + flags = flags | MS_NODEV | MS_NOSUID | MS_NOEXEC; + is_tmp = 1; + } + + /* mount directory */ + if (mount(dst, dst, NULL, MS_BIND | flags, NULL) < 0) { fprintf(stderr, _("Failed to mount %s on %s: %s\n"), dst, dst, strerror(errno)); return -1; } - - if (mount(dst, dst, NULL, MS_PRIVATE | MS_REC, NULL) < 0) { + if (mount(dst, dst, NULL, MS_PRIVATE | flags, NULL) < 0) { fprintf(stderr, _("Failed to make %s private: %s\n"), dst, strerror(errno)); return -1; } - - if (mount(src, dst, NULL, MS_BIND | MS_REC, NULL) < 0) { + if (mount(src, dst, NULL, MS_BIND | flags, NULL) < 0) { fprintf(stderr, _("Failed to mount %s on %s: %s\n"), src, dst, strerror(errno)); return -1; } - if (verify_mount(dst, pwd) < 0) - return -1; + /* verify whether we mounted what we expected to mount */ + if (verify_directory(dst, src_st, NULL) < 0) return -1; + + /* bind mount /tmp on /var/tmp too */ + if (is_tmp) { + if (verbose) + printf(_("Mounting /tmp on /var/tmp\n")); + + if (mount("/var/tmp", "/var/tmp", NULL, MS_BIND | flags, NULL) < 0) { + fprintf(stderr, _("Failed to mount /var/tmp on /var/tmp: %s\n"), strerror(errno)); + return -1; + } + if (mount("/var/tmp", "/var/tmp", NULL, MS_PRIVATE | flags, NULL) < 0) { + fprintf(stderr, _("Failed to make /var/tmp private: %s\n"), strerror(errno)); + return -1; + } + if (mount("/tmp", "/var/tmp", NULL, MS_BIND | flags, NULL) < 0) { + fprintf(stderr, _("Failed to mount /tmp on /var/tmp: %s\n"), strerror(errno)); + return -1; + } + } + + return 0; + } /** @@ -285,7 +300,6 @@ static int sandbox_error(const char *string) fprintf(stderr, string); syslog(LOG_AUTHPRIV | LOG_ALERT, string); exit(-1); - } /** @@ -485,18 +499,266 @@ err: return rc; } +/* + If path is empy or ends with "/." or "/.. return -1 else return 0; + */ +static int bad_path(const char *path) { + const char *ptr; + ptr = path; + while (*ptr) ptr++; + if (ptr == path) return -1; // ptr null + ptr--; + if (ptr != path && *ptr == '.') { + ptr--; + if (*ptr == '/') return -1; // path ends in /. + if (*ptr == '.') { + if (ptr != path) { + ptr--; + if (*ptr == '/') return -1; // path ends in /.. + } + } + } + return 0; +} + +static int rsynccmd(const char * src, const char *dst, char **cmdbuf) +{ + char *buf = NULL; + char *newbuf = NULL; + glob_t fglob; + fglob.gl_offs = 0; + int flags = GLOB_PERIOD; + unsigned int i = 0; + int rc = -1; + + /* match glob for all files in src dir */ + if (asprintf(&buf, "%s/*", src) == -1) { + fprintf(stderr, "Out of memory\n"); + return -1; + } + + if (glob(buf, flags, NULL, &fglob) != 0) { + free(buf); buf = NULL; + return -1; + } + + free(buf); buf = NULL; + + for ( i=0; i < fglob.gl_pathc; i++) { + const char *path = fglob.gl_pathv[i]; + + if (bad_path(path)) continue; + + if (!buf) { + if (asprintf(&newbuf, "\'%s\'", path) == -1) { + fprintf(stderr, "Out of memory\n"); + goto err; + } + } else { + if (asprintf(&newbuf, "%s \'%s\'", buf, path) == -1) { + fprintf(stderr, "Out of memory\n"); + goto err; + } + } + + free(buf); buf = newbuf; + newbuf = NULL; + } + + if (buf) { + if (asprintf(&newbuf, "/usr/bin/rsync -trlHDq %s '%s'", buf, dst) == -1) { + fprintf(stderr, "Out of memory\n"); + goto err; + } + *cmdbuf=newbuf; + } + else { + *cmdbuf=NULL; + } + rc = 0; + +err: + free(buf); buf = NULL; + globfree(&fglob); + return rc; +} + +/** + * Clean up runtime temporary directory. Returns 0 if no problem was detected, + * >0 if some error was detected, but errors here are treated as non-fatal and + * left to tmpwatch to finish incomplete cleanup. + */ +static int cleanup_tmpdir(const char *tmpdir, const char *src, + struct passwd *pwd, int copy_content) +{ + char *cmdbuf = NULL; + int rc = 0; + + /* rsync files back */ + if (copy_content) { + if (asprintf(&cmdbuf, "/usr/bin/rsync --exclude=.X11-unix -utrlHDq --delete '%s/' '%s/'", tmpdir, src) == -1) { + fprintf(stderr, _("Out of memory\n")); + cmdbuf = NULL; + rc++; + } + if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) { + fprintf(stderr, _("Failed to copy files from the runtime temporary directory\n")); + rc++; + } + free(cmdbuf); cmdbuf = NULL; + } + + /* remove files from the runtime temporary directory */ + if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { + fprintf(stderr, _("Out of memory\n")); + cmdbuf = NULL; + rc++; + } + /* this may fail if there's root-owned file left in the runtime tmpdir */ + if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; + free(cmdbuf); cmdbuf = NULL; + + /* remove runtime temporary directory */ + setfsuid(0); + if (rmdir(tmpdir) == -1) + fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); + setfsuid(pwd->pw_uid); + + return 0; +} + +/** + * seunshare will create a tmpdir in /tmp, with root ownership. The parent + * process waits for it child to exit to attempt to remove the directory. If + * it fails to remove the directory, we will need to rely on tmpreaper/tmpwatch + * to clean it up. + */ +static char *create_tmpdir(const char *src, struct stat *src_st, + struct stat *out_st, struct passwd *pwd, security_context_t execcon) +{ + char *tmpdir = NULL; + char *cmdbuf = NULL; + int fd_t = -1, fd_s = -1; + struct stat tmp_st; + security_context_t con = NULL; + + /* get selinux context */ + if (execcon) { + setfsuid(pwd->pw_uid); + if ((fd_s = open(src, O_RDONLY)) < 0) { + fprintf(stderr, _("Failed to open directory %s: %s\n"), src, strerror(errno)); + goto err; + } + if (fstat(fd_s, &tmp_st) == -1) { + fprintf(stderr, _("Failed to stat directory %s: %s\n"), src, strerror(errno)); + goto err; + } + if (!equal_stats(src_st, &tmp_st)) { + fprintf(stderr, _("Error: %s was replaced by a different directory\n"), src); + goto err; + } + if (fgetfilecon(fd_s, &con) == -1) { + fprintf(stderr, _("Failed to get context of the directory %s: %s\n"), src, strerror(errno)); + goto err; + } + + /* ok to not reach this if there is an error */ + setfsuid(0); + } + + if (asprintf(&tmpdir, "/tmp/.sandbox-%s-XXXXXX", pwd->pw_name) == -1) { + fprintf(stderr, _("Out of memory\n")); + tmpdir = NULL; + goto err; + } + if (mkdtemp(tmpdir) == NULL) { + fprintf(stderr, _("Failed to create temporary directory: %s\n"), strerror(errno)); + goto err; + } + + /* temporary directory must be owned by root:user */ + if (verify_directory(tmpdir, NULL, out_st) < 0) { + goto err; + } + + if (check_owner_uid(0, tmpdir, out_st) < 0) + goto err; + + if (check_owner_gid(getgid(), tmpdir, out_st) < 0) + goto err; + + /* change permissions of the temporary directory */ + if ((fd_t = open(tmpdir, O_RDONLY)) < 0) { + fprintf(stderr, _("Failed to open directory %s: %s\n"), tmpdir, strerror(errno)); + goto err; + } + if (fstat(fd_t, &tmp_st) == -1) { + fprintf(stderr, _("Failed to stat directory %s: %s\n"), tmpdir, strerror(errno)); + goto err; + } + if (!equal_stats(out_st, &tmp_st)) { + fprintf(stderr, _("Error: %s was replaced by a different directory\n"), tmpdir); + goto err; + } + if (fchmod(fd_t, 01770) == -1) { + fprintf(stderr, _("Unable to change mode on %s: %s\n"), tmpdir, strerror(errno)); + goto err; + } + /* re-stat again to pick change mode */ + if (fstat(fd_t, out_st) == -1) { + fprintf(stderr, _("Failed to stat directory %s: %s\n"), tmpdir, strerror(errno)); + goto err; + } + + /* copy selinux context */ + if (execcon) { + if (fsetfilecon(fd_t, con) == -1) { + fprintf(stderr, _("Failed to set context of the directory %s: %s\n"), tmpdir, strerror(errno)); + goto err; + } + } + + setfsuid(pwd->pw_uid); + + if (rsynccmd(src, tmpdir, &cmdbuf) < 0) { + goto err; + } + + /* ok to not reach this if there is an error */ + setfsuid(0); + + if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) { + fprintf(stderr, _("Failed to populate runtime temporary directory\n")); + cleanup_tmpdir(tmpdir, src, pwd, 0); + goto err; + } + + goto good; +err: + free(tmpdir); tmpdir = NULL; +good: + free(cmdbuf); cmdbuf = NULL; + freecon(con); con = NULL; + if (fd_t >= 0) close(fd_t); + if (fd_s >= 0) close(fd_s); + return tmpdir; +} + int main(int argc, char **argv) { - int rc; int status = -1; + security_context_t execcon = NULL; - security_context_t scontext = NULL; - - int flag_index; /* flag index in argv[] */ int clflag; /* holds codes for command line flags */ - char *tmpdir_s = NULL; /* tmpdir spec'd by user in argv[] */ - char *homedir_s = NULL; /* homedir spec'd by user in argv[] */ int usecgroups = 0; + char *homedir_s = NULL; /* homedir spec'd by user in argv[] */ + char *tmpdir_s = NULL; /* tmpdir spec'd by user in argv[] */ + char *tmpdir_r = NULL; /* tmpdir created by seunshare */ + + struct stat st_homedir; + struct stat st_tmpdir_s; + struct stat st_tmpdir_r; + const struct option long_options[] = { {"homedir", 1, 0, 'h'}, {"tmpdir", 1, 0, 't'}, @@ -528,7 +790,7 @@ int main(int argc, char **argv) { } if (verify_shell(pwd->pw_shell) < 0) { - fprintf(stderr, _("Error! Shell is not valid.\n")); + fprintf(stderr, _("Error: User shell is not valid\n")); return -1; } @@ -539,22 +801,13 @@ int main(int argc, char **argv) { switch (clflag) { case 't': - if (!(tmpdir_s = realpath(optarg, NULL))) { - fprintf(stderr, _("Invalid mount point %s: %s\n"), optarg, strerror(errno)); - return -1; - } - if (verify_mount(tmpdir_s, pwd) < 0) return -1; + tmpdir_s = optarg; break; case 'h': - if (!(homedir_s = realpath(optarg, NULL))) { - fprintf(stderr, _("Invalid mount point %s: %s\n"), optarg, strerror(errno)); - return -1; - } - if (verify_mount(homedir_s, pwd) < 0) return -1; - if (verify_mount(pwd->pw_dir, pwd) < 0) return -1; + homedir_s = optarg; break; case 'v': - verbose = 1; + verbose++; break; case 'c': usecgroups = 1; @@ -563,7 +816,7 @@ int main(int argc, char **argv) { cap_set = CAPNG_SELECT_CAPS; break; case 'Z': - scontext = strdup(optarg); + execcon = optarg; break; default: fprintf(stderr, "%s\n", USAGE_STRING); @@ -572,13 +825,17 @@ int main(int argc, char **argv) { } if (! homedir_s && ! tmpdir_s) { - fprintf(stderr, _("Error: tmpdir and/or homedir required \n"), - "%s\n", USAGE_STRING); + fprintf(stderr, _("Error: tmpdir and/or homedir required\n %s\n"), USAGE_STRING); return -1; } if (argc - optind < 1) { - fprintf(stderr, _("Error: executable required \n %s \n"), USAGE_STRING); + fprintf(stderr, _("Error: executable required\n %s\n"), USAGE_STRING); + return -1; + } + + if (execcon && is_selinux_enabled() != 1) { + fprintf(stderr, _("Error: execution context specified, but SELinux is not enabled\n")); return -1; } @@ -588,84 +845,105 @@ int main(int argc, char **argv) { if (usecgroups && setup_cgroups() < 0) return -1; - if (unshare(CLONE_NEWNS) < 0) { - perror(_("Failed to unshare")); + /* set fsuid to ruid */ + /* Changing fsuid is usually required when user-specified directory is + * on an NFS mount. It's also desired to avoid leaking info about + * existence of the files not accessible to the user. */ + setfsuid(uid); + + /* verify homedir and tmpdir */ + if (homedir_s && ( + verify_directory(homedir_s, NULL, &st_homedir) < 0 || + check_owner_uid(uid, homedir_s, &st_homedir))) return -1; + if (tmpdir_s && ( + verify_directory(tmpdir_s, NULL, &st_tmpdir_s) < 0 || + check_owner_uid(uid, tmpdir_s, &st_tmpdir_s))) return -1; + setfsuid(0); + + /* create runtime tmpdir */ + if (tmpdir_s && (tmpdir_r = create_tmpdir(tmpdir_s, &st_tmpdir_s, + &st_tmpdir_r, pwd, execcon)) == NULL) { + fprintf(stderr, _("Failed to create runtime temporary directory\n")); return -1; } - if (homedir_s && tmpdir_s && (strncmp(pwd->pw_dir, tmpdir_s, strlen(pwd->pw_dir)) == 0)) { - if (seunshare_mount(tmpdir_s, "/tmp", pwd) < 0) - return -1; - if (seunshare_mount(homedir_s, pwd->pw_dir, pwd) < 0) - return -1; - } else { - if (homedir_s && seunshare_mount(homedir_s, pwd->pw_dir, pwd) < 0) - return -1; - - if (tmpdir_s && seunshare_mount(tmpdir_s, "/tmp", pwd) < 0) - return -1; - } - - if (drop_privs(uid)) - return -1; - + /* spawn child process */ int child = fork(); if (child == -1) { perror(_("Unable to fork")); - return -1; + goto err; } - if (!child) { - char *display=NULL; - /* Construct a new environment */ - char *d = getenv("DISPLAY"); - if (d) { - display = strdup(d); - if (!display) { - perror(_("Out of memory")); - exit(-1); - } - } + if (child == 0) { + char *display = NULL; + int rc = -1; - if ((rc = clearenv())) { - perror(_("Unable to clear environment")); - free(display); - exit(-1); + if (unshare(CLONE_NEWNS) < 0) { + perror(_("Failed to unshare")); + goto childerr; } - if (scontext) { - if (setexeccon(scontext)) { - fprintf(stderr, _("Could not set exec context to %s.\n"), - scontext); - free(display); - exit(-1); + /* assume fsuid==ruid after this point */ + setfsuid(uid); + + /* mount homedir and tmpdir, in this order */ + if (homedir_s && seunshare_mount(homedir_s, pwd->pw_dir, + &st_homedir) != 0) goto childerr; + if (tmpdir_s && seunshare_mount(tmpdir_r, "/tmp", + &st_tmpdir_r) != 0) goto childerr; + + if (drop_privs(uid) != 0) goto childerr; + + /* construct a new environment */ + if ((display = getenv("DISPLAY")) != NULL) { + if ((display = strdup(display)) == NULL) { + perror(_("Out of memory")); + goto childerr; } } - - if (display) + if ((rc = clearenv()) != 0) { + perror(_("Failed to clear environment")); + goto childerr; + } + if (display) rc |= setenv("DISPLAY", display, 1); rc |= setenv("HOME", pwd->pw_dir, 1); rc |= setenv("SHELL", pwd->pw_shell, 1); rc |= setenv("USER", pwd->pw_name, 1); rc |= setenv("LOGNAME", pwd->pw_name, 1); rc |= setenv("PATH", DEFAULT_PATH, 1); - + if (rc != 0) { + fprintf(stderr, _("Failed to construct environment\n")); + goto childerr; + } + + /* selinux context */ + if (execcon && setexeccon(execcon) != 0) { + fprintf(stderr, _("Could not set exec context to %s.\n"), execcon); + goto childerr; + } + if (chdir(pwd->pw_dir)) { perror(_("Failed to change dir to homedir")); - exit(-1); + goto childerr; } setsid(); execv(argv[optind], argv + optind); + fprintf(stderr, _("Failed to execute command %s: %s\n"), argv[optind], strerror(errno)); +childerr: free(display); - perror("execv"); exit(-1); - } else { - waitpid(child, &status, 0); } - free(tmpdir_s); - free(homedir_s); - free(scontext); + drop_caps(); + /* parent waits for child exit to do the cleanup */ + waitpid(child, &status, 0); + status_to_retval(status, status); + + if (tmpdir_r) cleanup_tmpdir(tmpdir_r, tmpdir_s, pwd, 1); + +err: + free(tmpdir_r); return status; } -- 1.7.6.2