/* Return the mode (including the file type) of the file pointed to by symlink * PATH, or 0 if it doesn't exist. Catch symlink loops using LAST_INODE and * RCOUNT. */ static mode_t _symlink(const char *path, ino_t last_inode, int rcount) { char buf[PR_TUNABLE_PATH_MAX + 1]; struct stat sbuf; int i; if (++rcount >= 32) { errno = ELOOP; return 0; } memset(buf, '\0', sizeof(buf)); i = pr_fsio_readlink(path, buf, sizeof(buf) - 1); if (i == -1) return (mode_t)0; buf[i] = '\0'; pr_fs_clear_cache2(buf); if (pr_fsio_lstat(buf, &sbuf) != -1) { if (sbuf.st_ino && (ino_t) sbuf.st_ino == last_inode) { errno = ELOOP; return 0; } if (S_ISLNK(sbuf.st_mode)) { return _symlink(buf, (ino_t) sbuf.st_ino, rcount); } return sbuf.st_mode; } return 0; }
static int copy_symlink(pool *p, const char *src_path, const char *dst_path) { char *link_path = pcalloc(p, PR_TUNABLE_BUFFER_SIZE); int len; len = pr_fsio_readlink(src_path, link_path, PR_TUNABLE_BUFFER_SIZE-1); if (len < 0) { int xerrno = errno; pr_log_pri(PR_LOG_WARNING, MOD_COPY_VERSION ": error reading link '%s': %s", src_path, strerror(xerrno)); errno = xerrno; return -1; } link_path[len] = '\0'; if (pr_fsio_symlink(link_path, dst_path) < 0) { int xerrno = errno; pr_log_pri(PR_LOG_WARNING, MOD_COPY_VERSION ": error symlinking '%s' to '%s': %s", link_path, dst_path, strerror(xerrno)); errno = xerrno; return -1; } return 0; }
static int copy_symlink(pool *p, const char *src_dir, const char *src_path, const char *dst_dir, const char *dst_path, uid_t uid, gid_t gid) { char *link_path = pcalloc(p, PR_TUNABLE_BUFFER_SIZE); int len; len = pr_fsio_readlink(src_path, link_path, PR_TUNABLE_BUFFER_SIZE-1); if (len < 0) { pr_log_pri(PR_LOG_WARNING, "CreateHome: error reading link '%s': %s", src_path, strerror(errno)); return -1; } link_path[len] = '\0'; /* If the target of the link lies within the src path, rename that portion * of the link to be the corresponding part of the dst path. */ if (strncmp(link_path, src_dir, strlen(src_dir)) == 0) { link_path = pdircat(p, dst_dir, link_path + strlen(src_dir), NULL); } if (pr_fsio_symlink(link_path, dst_path) < 0) { pr_log_pri(PR_LOG_WARNING, "CreateHome: error symlinking '%s' to '%s': %s", link_path, dst_path, strerror(errno)); return -1; } /* Make sure the new symlink has the proper ownership. */ if (pr_fsio_chown(dst_path, uid, gid) < 0) { pr_log_pri(PR_LOG_WARNING, "CreateHome: error chown'ing '%s' to %u/%u: %s", dst_path, (unsigned int) uid, (unsigned int) gid, strerror(errno)); } return 0; }
/* Return the mode (including the file type) of the file pointed to by symlink * PATH, or 0 if it doesn't exist. Catch symlink loops using LAST_INODE and * RCOUNT. */ static mode_t _symlink(const char *path, ino_t last_inode, int rcount) { char buf[PR_TUNABLE_PATH_MAX + 1]; struct stat st; int i; if (++rcount >= PR_FSIO_MAX_LINK_COUNT) { errno = ELOOP; return 0; } memset(buf, '\0', sizeof(buf)); i = pr_fsio_readlink(path, buf, sizeof(buf) - 1); if (i < 0) { return (mode_t) 0; } buf[i] = '\0'; pr_fs_clear_cache2(buf); if (pr_fsio_lstat(buf, &st) >= 0) { if (st.st_ino > 0 && (ino_t) st.st_ino == last_inode) { errno = ELOOP; return 0; } if (S_ISLNK(st.st_mode)) { return _symlink(buf, (ino_t) st.st_ino, rcount); } return st.st_mode; } return 0; }
END_TEST START_TEST (fsio_readlink_test) { int res; char buf[PR_TUNABLE_BUFFER_SIZE]; const char *link_path, *target_path, *path; res = pr_fsio_readlink(NULL, NULL, 0); fail_unless(res < 0, "Failed to handle null arguments"); fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno), errno); /* Read a non-symlink file */ path = "/"; res = pr_fsio_readlink(path, buf, sizeof(buf)-1); fail_unless(res < 0, "Failed to handle non-symlink path"); fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno), errno); /* Read a symlink file */ target_path = "/tmp"; link_path = fsio_link_path; res = pr_fsio_symlink(target_path, link_path); fail_unless(res == 0, "Failed to create symlink from '%s' to '%s': %s", link_path, target_path, strerror(errno)); memset(buf, '\0', sizeof(buf)); res = pr_fsio_readlink(link_path, buf, sizeof(buf)-1); fail_unless(res > 0, "Failed to read symlink '%s': %s", link_path, strerror(errno)); buf[res] = '\0'; fail_unless(strcmp(buf, target_path) == 0, "Expected '%s', got '%s'", target_path, buf); /* Read a symlink file using a zero-length buffer */ res = pr_fsio_readlink(link_path, buf, 0); fail_unless(res <= 0, "Expected length <= 0, got %d", res); (void) unlink(link_path); }
static int af_check_file(pool *p, const char *name, const char *path, int flags) { struct stat st; int res; const char *orig_path; orig_path = path; res = lstat(path, &st); if (res < 0) { int xerrno = errno; pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION ": unable to lstat %s '%s': %s", name, path, strerror(xerrno)); errno = xerrno; return -1; } if (S_ISLNK(st.st_mode)) { char buf[PR_TUNABLE_PATH_MAX+1]; /* Check the permissions on the parent directory; if they're world-writable, * then this symlink can be deleted/pointed somewhere else. */ res = af_check_parent_dir(p, name, path); if (res < 0) { return -1; } /* Follow the link to the target path; that path will then have its * parent directory checked. */ memset(buf, '\0', sizeof(buf)); res = pr_fsio_readlink(path, buf, sizeof(buf)-1); if (res > 0) { path = pstrdup(p, buf); } res = stat(orig_path, &st); if (res < 0) { int xerrno = errno; pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION ": unable to stat %s '%s': %s", name, orig_path, strerror(xerrno)); errno = xerrno; return -1; } } if (S_ISDIR(st.st_mode)) { int xerrno = EISDIR; pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION ": unable to use %s '%s': %s", name, orig_path, strerror(xerrno)); errno = xerrno; return -1; } /* World-readable files MAY be insecure, and are thus not usable/trusted. */ if ((st.st_mode & S_IROTH) && !(flags & PR_AUTH_FILE_FL_ALLOW_WORLD_READABLE)) { int xerrno = EPERM; pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION ": unable to use world-readable %s '%s' (perms %04o): %s", name, orig_path, st.st_mode & ~S_IFMT, strerror(xerrno)); errno = xerrno; return -1; } /* World-writable files are insecure, and are thus not usable/trusted. */ if (st.st_mode & S_IWOTH) { int xerrno = EPERM; pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION ": unable to use world-writable %s '%s' (perms %04o): %s", name, orig_path, st.st_mode & ~S_IFMT, strerror(xerrno)); errno = xerrno; return -1; } if (!S_ISREG(st.st_mode)) { pr_log_pri(PR_LOG_WARNING, MOD_AUTH_FILE_VERSION ": %s '%s' is not a regular file", name, orig_path); } /* Check the parent directory of this file. If the parent directory * is world-writable, that too is insecure. */ res = af_check_parent_dir(p, name, path); if (res < 0) { return -1; } return 0; }
/* Performs chroot-aware handling of symlinks. */ int dir_readlink(pool *p, const char *path, char *buf, size_t bufsz, int flags) { int is_abs_dst, clean_flags, len, res = -1; size_t chroot_pathlen = 0, adj_pathlen = 0; char *dst_path, *adj_path; pool *tmp_pool; if (p == NULL || path == NULL || buf == NULL) { errno = EINVAL; return -1; } if (bufsz == 0) { return 0; } len = pr_fsio_readlink(path, buf, bufsz); if (len < 0) { return -1; } if (len == 0 || len == bufsz) { /* If we read nothing in, OR if the given buffer was completely * filled WITHOUT terminating NUL, there's really nothing we can/should * be doing. */ return len; } is_abs_dst = FALSE; if (*buf == '/') { is_abs_dst = TRUE; } if (session.chroot_path != NULL) { chroot_pathlen = strlen(session.chroot_path); } if (chroot_pathlen <= 1) { char *ptr; if (is_abs_dst == TRUE || !(flags & PR_DIR_READLINK_FL_HANDLE_REL_PATH)) { return len; } /* Since we have a relative destination path, we will concat it * with the source path's directory, then clean up that path. */ ptr = strrchr(path, '/'); if (ptr != NULL && ptr != path) { char *parent_dir; tmp_pool = make_sub_pool(p); pr_pool_tag(tmp_pool, "dir_readlink pool"); parent_dir = pstrndup(tmp_pool, path, (ptr - path)); dst_path = pdircat(tmp_pool, parent_dir, buf, NULL); adj_pathlen = bufsz + 1; adj_path = pcalloc(tmp_pool, adj_pathlen); res = pr_fs_clean_path2(dst_path, adj_path, adj_pathlen-1, 0); if (res == 0) { pr_trace_msg("fsio", 19, "cleaned symlink path '%s', yielding '%s'", dst_path, adj_path); dst_path = adj_path; } pr_trace_msg("fsio", 19, "adjusted relative symlink path '%s', yielding '%s'", buf, dst_path); memset(buf, '\0', bufsz); sstrncpy(buf, dst_path, bufsz); len = strlen(buf); destroy_pool(tmp_pool); } return len; } if (is_abs_dst == FALSE) { /* If we are to ignore relative destination paths, return now. */ if (!(flags & PR_DIR_READLINK_FL_HANDLE_REL_PATH)) { return len; } } if (is_abs_dst == TRUE && len < chroot_pathlen) { /* If the destination path length is shorter than the chroot path, * AND the destination path is absolute, then by definition it CANNOT * point within the chroot. */ return len; } tmp_pool = make_sub_pool(p); pr_pool_tag(tmp_pool, "dir_readlink pool"); dst_path = pstrdup(tmp_pool, buf); if (is_abs_dst == FALSE) { char *ptr; /* Since we have a relative destination path, we will concat it * with the source path's directory, then clean up that path. */ ptr = strrchr(path, '/'); if (ptr != NULL && ptr != path) { char *parent_dir; parent_dir = pstrndup(tmp_pool, path, (ptr - path)); dst_path = pdircat(tmp_pool, parent_dir, dst_path, NULL); } else { dst_path = pdircat(tmp_pool, path, dst_path, NULL); } } adj_pathlen = bufsz + 1; adj_path = pcalloc(tmp_pool, adj_pathlen); clean_flags = PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH; res = pr_fs_clean_path2(dst_path, adj_path, adj_pathlen-1, clean_flags); if (res == 0) { pr_trace_msg("fsio", 19, "cleaned symlink path '%s', yielding '%s'", dst_path, adj_path); dst_path = adj_path; memset(buf, '\0', bufsz); sstrncpy(buf, dst_path, bufsz); len = strlen(dst_path); } if (strncmp(dst_path, session.chroot_path, chroot_pathlen) == 0 && *(dst_path + chroot_pathlen) == '/') { char *ptr; ptr = dst_path + chroot_pathlen; if (is_abs_dst == FALSE && res == 0) { /* If we originally had a relative destination path, AND we cleaned * that adjusted path, then we should try to re-adjust the path * back to being a relative path. Within reason. */ ptr = pstrcat(tmp_pool, ".", ptr, NULL); } /* Since we are making the destination path shorter, the given buffer * (which was big enough for the original destination path) should * always be large enough for this adjusted, shorter version. Right? */ pr_trace_msg("fsio", 19, "adjusted symlink path '%s' for chroot '%s', yielding '%s'", dst_path, session.chroot_path, ptr); memset(buf, '\0', bufsz); sstrncpy(buf, ptr, bufsz); len = strlen(buf); } destroy_pool(tmp_pool); return len; }