--- configure.in +++ configure.in @@ -1399,7 +1399,7 @@ AC_PROG_GCC_TRADITIONAL AC_FUNC_SETPGRP AC_TYPE_SIGNAL AC_FUNC_VPRINTF -AC_CHECK_FUNCS(bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getpgid getpgrp nl_langinfo) +AC_CHECK_FUNCS(bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getpgid getpgrp mkdtemp nl_langinfo) AC_CHECK_FUNC(gai_strerror, AC_DEFINE(HAVE_GAI_STRERROR, 1, [Define if you have the gai_strerror() function]), --- configure +++ configure @@ -26526,7 +26526,8 @@ done -for ac_func in bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getpgid getpgrp nl_langinfo + +for ac_func in bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getpgid getpgrp mkdtemp nl_langinfo do as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh` { echo "$as_me:$LINENO: checking for $ac_func" >&5 --- config.h.in +++ config.h.in @@ -357,6 +357,9 @@ /* Define if you have the mkdir function. */ #undef HAVE_MKDIR +/* Define if you have the mkdtemp function. */ +#undef HAVE_MKDTEMP + /* Define if you have the mkstemp function. */ #undef HAVE_MKSTEMP --- contrib/mod_sftp/fxp.c +++ contrib/mod_sftp/fxp.c @@ -5498,7 +5498,7 @@ static int fxp_handle_mkdir(struct fxp_p (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "creating directory '%s' with mode 0%o", path, (unsigned int) dir_mode); - res = pr_fsio_mkdir(path, dir_mode); + res = pr_fsio_smkdir(fxp->pool, path, dir_mode, (uid_t) -1, (gid_t) -1); if (res < 0) { const char *reason; int xerrno = errno; --- contrib/mod_sftp/scp.c +++ contrib/mod_sftp/scp.c @@ -711,7 +711,7 @@ static int recv_finfo(pool *p, uint32_t if (xerrno == ENOENT) { pr_trace_msg(trace_channel, 5, "creating directory '%s'", sp->filename); - if (pr_fsio_mkdir(sp->filename, 0777) < 0) { + if (pr_fsio_smkdir(p, sp->filename, 0777, (uid_t) -1, (gid_t) -1) < 0) { xerrno = errno; (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, --- include/fsio.h +++ include/fsio.h @@ -129,6 +129,7 @@ struct fs_rec { int (*fchmod)(pr_fh_t *, int, mode_t); int (*chown)(pr_fs_t *, const char *, uid_t, gid_t); int (*fchown)(pr_fh_t *, int, uid_t, gid_t); + int (*lchown)(pr_fs_t *, const char *, uid_t, gid_t); int (*access)(pr_fs_t *, const char *, int, uid_t, gid_t, array_header *); int (*faccess)(pr_fh_t *, int, uid_t, gid_t, array_header *); int (*utimes)(pr_fs_t *, const char *, struct timeval *); @@ -249,6 +250,7 @@ int pr_fsio_mkdir(const char *, mode_t); int pr_fsio_rmdir(const char *); int pr_fsio_rename(const char *, const char *); int pr_fsio_rename_canon(const char *, const char *); +int pr_fsio_smkdir(pool *, const char *, mode_t, uid_t, gid_t); int pr_fsio_unlink(const char *); int pr_fsio_unlink_canon(const char *); pr_fh_t *pr_fsio_open(const char *, int); @@ -270,6 +272,7 @@ int pr_fsio_fchmod(pr_fh_t *, mode_t); int pr_fsio_chmod_canon(const char *, mode_t); int pr_fsio_chown(const char *, uid_t, gid_t); int pr_fsio_fchown(pr_fh_t *, uid_t, gid_t); +int pr_fsio_lchown(const char *, uid_t, gid_t); int pr_fsio_chown_canon(const char *, uid_t, gid_t); int pr_fsio_chroot(const char *); int pr_fsio_access(const char *, int, uid_t, gid_t, array_header *); @@ -278,6 +281,11 @@ int pr_fsio_utimes(const char *, struct int pr_fsio_futimes(pr_fh_t *, struct timeval *); off_t pr_fsio_lseek(pr_fh_t *, off_t, int); +/* Set a flag determining whether to use mkdtemp(3) (if available) or not. + * Returns the previously-set value. + */ +int pr_fsio_set_use_mkdtemp(int); + /* FS-related functions */ char *pr_fsio_getline(char *, int, pr_fh_t *, unsigned int *); --- modules/mod_core.c +++ modules/mod_core.c @@ -3744,7 +3744,7 @@ int core_chgrp(cmd_rec *cmd, char *dir, } cmd->argv[0] = cmd_name; - return pr_fsio_chown(dir, uid, gid); + return pr_fsio_lchown(dir, uid, gid); } int core_chmod(cmd_rec *cmd, char *dir, mode_t mode) { @@ -4002,7 +4002,6 @@ MODRET core_rmd(cmd_rec *cmd) { MODRET core_mkd(cmd_rec *cmd) { int res; char *dir; - struct stat st; CHECK_CMD_MIN_ARGS(cmd, 2); @@ -4043,7 +4042,8 @@ MODRET core_mkd(cmd_rec *cmd) { return PR_ERROR(cmd); } - if (pr_fsio_mkdir(dir, 0777) < 0) { + if (pr_fsio_smkdir(cmd->tmp_pool, dir, 0777, session.fsuid, + session.fsgid) < 0) { int xerrno = errno; (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): " @@ -4055,71 +4055,6 @@ MODRET core_mkd(cmd_rec *cmd) { return PR_ERROR(cmd); } - /* Check to see if we need to change the ownership (user and/or group) of - * the newly created directory. - */ - if (session.fsuid != (uid_t) -1) { - int err = 0, iserr = 0; - - pr_fsio_stat(dir, &st); - - PRIVS_ROOT - if (pr_fsio_chown(dir, session.fsuid, session.fsgid) == -1) { - iserr++; - err = errno; - } - PRIVS_RELINQUISH - - if (iserr) { - pr_log_pri(PR_LOG_WARNING, "chown() as root failed: %s", strerror(err)); - - } else { - if (session.fsgid != (gid_t) -1) { - pr_log_debug(DEBUG2, "root chown(%s) to uid %lu, gid %lu successful", - dir, (unsigned long) session.fsuid, (unsigned long) session.fsgid); - - } else { - pr_log_debug(DEBUG2, "root chown(%s) to uid %lu successful", dir, - (unsigned long) session.fsuid); - } - } - - } else if (session.fsgid != (gid_t) -1) { - register unsigned int i; - int use_root_privs = TRUE; - - pr_fsio_stat(dir, &st); - - /* Check if session.fsgid is in session.gids. If not, use root privs. */ - for (i = 0; i < session.gids->nelts; i++) { - gid_t *group_ids = session.gids->elts; - - if (group_ids[i] == session.fsgid) { - use_root_privs = FALSE; - break; - } - } - - if (use_root_privs) { - PRIVS_ROOT - } - - res = pr_fsio_chown(dir, (uid_t) -1, session.fsgid); - - if (use_root_privs) { - PRIVS_RELINQUISH - } - - if (res == -1) { - pr_log_pri(PR_LOG_WARNING, "%schown() failed: %s", - use_root_privs ? "root " : "", strerror(errno)); - - } else { - pr_log_debug(DEBUG2, "%schown(%s) to gid %lu successful", - use_root_privs ? "root " : "", dir, (unsigned long) session.fsgid); - } - } - pr_response_add(R_257, _("\"%s\" - Directory successfully created"), quote_dir(cmd, dir)); --- src/fsio.c +++ src/fsio.c @@ -29,6 +29,7 @@ */ #include "conf.h" +#include "privs.h" #ifdef HAVE_REGEX_H # include @@ -84,6 +85,13 @@ static unsigned char chk_fs_map = FALSE; static char vwd[PR_TUNABLE_PATH_MAX + 1] = "/"; static char cwd[PR_TUNABLE_PATH_MAX + 1] = "/"; +/* Runtime enabling/disabling of mkdtemp(3) use. */ +#ifdef HAVE_MKDTEMP +static int use_mkdtemp = TRUE; +#else +static int use_mkdtemp = FALSE; +#endif /* HAVE_MKDTEMP */ + /* Runtime enabling/disabling of encoding of paths. */ static int use_encoding = TRUE; @@ -179,6 +187,10 @@ static int sys_fchown(pr_fh_t *fh, int f return fchown(fd, uid, gid); } +static int sys_lchown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) { + return lchown(path, uid, gid); +} + /* We provide our own equivalent of access(2) here, rather than using * access(2) directly, because access(2) uses the real IDs, rather than * the effective IDs, of the process. @@ -2477,6 +2489,213 @@ int pr_fsio_mkdir(const char *path, mode return res; } +int pr_fsio_set_use_mkdtemp(int value) { + int prev_value; + + prev_value = use_mkdtemp; + +#ifdef HAVE_MKDTEMP + use_mkdtemp = value; +#endif /* HAVE_MKDTEMP */ + + return prev_value; +} + +/* "safe mkdir" variant of mkdir(2), uses mkdtemp(3), lchown(2), and + * rename(2) to create a directory which cannot be hijacked by a symlink + * race (hopefully) before the UserOwner/GroupOwner ownership changes are + * applied. + */ +int pr_fsio_smkdir(pool *p, const char *path, mode_t mode, uid_t uid, + gid_t gid) { + int res, use_root_privs = TRUE, xerrno = 0; + char *tmpl_path; + char *dst_dir, *tmpl; + size_t dst_dirlen, tmpl_len; + + if (path == NULL) { + errno = EINVAL; + return -1; + } + +#ifdef HAVE_MKDTEMP + if (use_mkdtemp == TRUE) { + char *ptr; + struct stat st; + + ptr = strrchr(path, '/'); + if (ptr == NULL) { + errno = EINVAL; + return -1; + } + + if (ptr != path) { + dst_dirlen = (ptr - path); + dst_dir = pstrndup(p, path, dst_dirlen); + + } else { + dst_dirlen = 1; + dst_dir = "/"; + } + + res = lstat(dst_dir, &st); + if (res < 0) { + return -1; + } + + if (!S_ISDIR(st.st_mode) && + !S_ISLNK(st.st_mode)) { + errno = EPERM; + return -1; + } + + /* Allocate enough space for the temporary name: the length of the + * destination directory, a slash, 9 X's, 3 for the prefix, and 1 for the + * trailing NUL. + */ + tmpl_len = strlen(dst_dir) + 14; + tmpl = pcalloc(p, tmpl_len); + snprintf(tmpl, tmpl_len-1, "%s/dstXXXXXXXXX", + dst_dirlen > 1 ? dst_dir : ""); + + /* Use mkdtemp(3) to create the temporary directory (in the same destination + * directory as the target path). + */ + tmpl_path = mkdtemp(tmpl); + if (tmpl_path == NULL) { + return -1; + } + + } else { + res = pr_fsio_mkdir(path, mode); + if (res < 0) { + return -1; + } + + tmpl_path = pstrdup(p, path); + } +#else + + res = pr_fsio_mkdir(path, mode); + if (res < 0) { + return -1; + } + + tmpl_path = pstrdup(p, path); +#endif /* HAVE_MKDTEMP */ + + if (uid != (uid_t) -1) { + PRIVS_ROOT + res = pr_fsio_lchown(tmpl_path, uid, gid); + xerrno = errno; + PRIVS_RELINQUISH + + if (res < 0) { + pr_log_pri(PR_LOG_WARNING, "lchown(%s) as root failed: %s", tmpl_path, + strerror(xerrno)); + + } else { + if (gid != (gid_t) -1) { + pr_log_debug(DEBUG2, "root lchown(%s) to UID %lu, GID %lu successful", + tmpl_path, (unsigned long) uid, (unsigned long) gid); + + } else { + pr_log_debug(DEBUG2, "root lchown(%s) to UID %lu successful", + tmpl_path, (unsigned long) uid); + } + } + + } else if (gid != (gid_t) -1) { + register unsigned int i; + + /* Check if session.fsgid is in session.gids. If not, use root privs. */ + for (i = 0; i < session.gids->nelts; i++) { + gid_t *group_ids = session.gids->elts; + + if (group_ids[i] == gid) { + use_root_privs = FALSE; + break; + } + } + + if (use_root_privs) { + PRIVS_ROOT + } + + res = pr_fsio_lchown(tmpl_path, (uid_t) -1, gid); + xerrno = errno; + + if (use_root_privs) { + PRIVS_RELINQUISH + } + + if (res < 0) { + pr_log_pri(PR_LOG_WARNING, "%slchown(%s) failed: %s", + use_root_privs ? "root " : "", tmpl_path, strerror(xerrno)); + + } else { + pr_log_debug(DEBUG2, "%slchown(%s) to GID %lu successful", + use_root_privs ? "root " : "", tmpl_path, (unsigned long) gid); + } + } + + if (use_mkdtemp == TRUE) { + mode_t mask, *dir_umask; + + /* Use chmod(2) to set the permission that we want. + * + * mkdtemp(3) creates a directory with 0700 perms; we are given the + * target mode (modulo the configured Umask). + */ + dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE); + if (dir_umask) { + mask = *dir_umask; + + } else { + mask = (mode_t) 0022; + } + + if (use_root_privs) { + PRIVS_ROOT + } + + res = chmod(tmpl_path, mode & ~mask); + xerrno = errno; + + if (use_root_privs) { + PRIVS_RELINQUISH + } + + if (res < 0) { + pr_log_pri(PR_LOG_WARNING, "%schmod(%s) failed: %s", + use_root_privs ? "root " : "", tmpl_path, strerror(xerrno)); + + (void) rmdir(tmpl_path); + + errno = xerrno; + return -1; + } + + /* Use rename(2) to move the temporary directory into place at the + * target path. + */ + res = rename(tmpl_path, path); + if (res < 0) { + xerrno = errno; + + pr_log_pri(PR_LOG_WARNING, "renaming '%s' to '%s' failed: %s", tmpl_path, + path, strerror(xerrno)); + + (void) rmdir(tmpl_path); + + errno = xerrno; + return -1; + } + } + + return 0; +} + int pr_fsio_rmdir(const char *path) { int res; pr_fs_t *fs; @@ -3336,6 +3555,33 @@ int pr_fsio_fchown(pr_fh_t *fh, uid_t ui return res; } +int pr_fsio_lchown(const char *name, uid_t uid, gid_t gid) { + int res; + pr_fs_t *fs; + + fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN); + if (fs == NULL) { + return -1; + } + + /* Find the first non-NULL custom lchown handler. If there are none, + * use the system chown. + */ + while (fs && fs->fs_next && !fs->lchown) { + fs = fs->fs_next; + } + + pr_trace_msg(trace_channel, 8, "using %s lchown() for path '%s'", + fs->fs_name, name); + res = (fs->lchown)(fs, name, uid, gid); + + if (res == 0) { + pr_fs_clear_cache(); + } + + return res; +} + int pr_fsio_access(const char *path, int mode, uid_t uid, gid_t gid, array_header *suppl_gids) { pr_fs_t *fs; @@ -3955,6 +4201,7 @@ int init_fs(void) { root_fs->fchmod = sys_fchmod; root_fs->chown = sys_chown; root_fs->fchown = sys_fchown; + root_fs->lchown = sys_lchown; root_fs->access = sys_access; root_fs->faccess = sys_faccess; root_fs->utimes = sys_utimes; @@ -4036,6 +4283,12 @@ static const char *get_fs_hooks_str(pool if (fs->chown) hooks = pstrcat(p, hooks, *hooks ? ", " : "", "chown(2)", NULL); + if (fs->fchown) + hooks = pstrcat(p, hooks, *hooks ? ", " : "", "fchown(2)", NULL); + + if (fs->lchown) + hooks = pstrcat(p, hooks, *hooks ? ", " : "", "lchown(2)", NULL); + if (fs->access) hooks = pstrcat(p, hooks, *hooks ? ", " : "", "access(2)", NULL); --- src/Makefile.in +++ src/Makefile.in @@ -294,7 +294,7 @@ filter.o: ../include/event.h ../include/ filter.o: ../include/trace.h ../include/encode.h ../include/compat.h filter.o: ../include/proctitle.h ../include/pidfile.h ../include/env.h filter.o: ../include/pr-syslog.h -fsio.o: ../include/conf.h ../include/version.h ../config.h +fsio.o: ../include/conf.h ../include/privs.h ../include/version.h ../config.h fsio.o: ../include/default_paths.h ../include/options.h ../include/pool.h fsio.o: ../include/str.h ../include/regexp.h ../include/table.h fsio.o: ../include/proftpd.h ../include/class.h ../include/netacl.h