diff options
Diffstat (limited to 'src/util/files.c')
-rw-r--r-- | src/util/files.c | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/src/util/files.c b/src/util/files.c new file mode 100644 index 000000000..5827b29d8 --- /dev/null +++ b/src/util/files.c @@ -0,0 +1,809 @@ +/* + Authors: + Jakub Hrozek <jhrozek@redhat.com> + + Copyright (C) 2009 Red Hat + + 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; either version 3 of the License, or + (at your option) any later version. + + 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, see <http://www.gnu.org/licenses/>. +*/ + +/* + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2001, Marek Michałkiewicz + * Copyright (c) 2003 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2008, Nicolas François + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/time.h> +#include <dirent.h> +#include <fcntl.h> +#include <errno.h> +#include <talloc.h> + +#include "util/util.h" + +struct copy_ctx { + const char *src_orig; + const char *dst_orig; + dev_t src_dev; + uid_t uid; + gid_t gid; +}; + +static int sss_timeat_set(int dir_fd, const char *path, + const struct stat *statp, + int flags) +{ + int ret; + +#ifdef HAVE_UTIMENSAT + struct timespec timebuf[2]; + + timebuf[0] = statp->st_atim; + timebuf[1] = statp->st_mtim; + + ret = utimensat(dir_fd, path, timebuf, flags); +#else + struct timeval tv[2]; + + tv[0].tv_sec = statp->st_atime; + tv[0].tv_usec = 0; + tv[1].tv_sec = statp->st_mtime; + tv[1].tv_usec = 0; + + ret = futimesat(dir_fd, path, tv); +#endif + if (ret == -1) { + return errno; + } + + return EOK; +} + +static int sss_futime_set(int fd, const struct stat *statp) +{ + int ret; + +#ifdef HAVE_FUTIMENS + struct timespec timebuf[2]; + + timebuf[0] = statp->st_atim; + timebuf[1] = statp->st_mtim; + ret = futimens(fd, timebuf); +#else + struct timeval tv[2]; + + tv[0].tv_sec = statp->st_atime; + tv[0].tv_usec = 0; + tv[1].tv_sec = statp->st_mtime; + tv[1].tv_usec = 0; + + ret = futimes(fd, tv); +#endif + if (ret == -1) { + return errno; + } + + return EOK; +} + +/* wrapper in order not to create a temporary context in + * every iteration */ +static int remove_tree_with_ctx(TALLOC_CTX *mem_ctx, + int parent_fd, + const char *dir_name, + dev_t parent_dev, + bool keep_root_dir); + +int sss_remove_tree(const char *root) +{ + TALLOC_CTX *tmp_ctx = NULL; + int ret; + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) { + return ENOMEM; + } + + ret = remove_tree_with_ctx(tmp_ctx, AT_FDCWD, root, 0, false); + talloc_free(tmp_ctx); + return ret; +} + +int sss_remove_subtree(const char *root) +{ + TALLOC_CTX *tmp_ctx = NULL; + int ret; + + tmp_ctx = talloc_new(NULL); + if (!tmp_ctx) { + return ENOMEM; + } + + ret = remove_tree_with_ctx(tmp_ctx, AT_FDCWD, root, 0, true); + talloc_free(tmp_ctx); + return ret; +} + +/* + * The context is not freed in case of error + * because this is a recursive function, will be freed when we + * reach the top level remove_tree() again + */ +static int remove_tree_with_ctx(TALLOC_CTX *mem_ctx, + int parent_fd, + const char *dir_name, + dev_t parent_dev, + bool keep_root_dir) +{ + struct dirent *result; + struct stat statres; + DIR *rootdir = NULL; + int ret, err; + int dir_fd; + + dir_fd = sss_openat_cloexec(parent_fd, dir_name, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW, &ret); + if (dir_fd == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot open %s: [%d]: %s\n", + dir_name, ret, strerror(ret)); + return ret; + } + + rootdir = fdopendir(dir_fd); + if (rootdir == NULL) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot open directory: [%d][%s]\n", ret, strerror(ret)); + close(dir_fd); + goto fail; + } + + while ((result = readdir(rootdir)) != NULL) { + if (strcmp(result->d_name, ".") == 0 || + strcmp(result->d_name, "..") == 0) { + continue; + } + + ret = fstatat(dir_fd, result->d_name, + &statres, AT_SYMLINK_NOFOLLOW); + if (ret != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "stat failed: [%d][%s]\n", ret, strerror(ret)); + goto fail; + } + + if (S_ISDIR(statres.st_mode)) { + /* if directory, recursively descend, but check if on the same FS */ + if (parent_dev && parent_dev != statres.st_dev) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Directory %s is on different filesystem, " + "will not follow\n", result->d_name); + ret = EFAULT; + goto fail; + } + + ret = remove_tree_with_ctx(mem_ctx, dir_fd, result->d_name, + statres.st_dev, false); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Removing subdirectory failed: [%d][%s]\n", + ret, strerror(ret)); + goto fail; + } + } else { + ret = unlinkat(dir_fd, result->d_name, 0); + if (ret != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Removing file failed '%s': [%d][%s]\n", + result->d_name, ret, strerror(ret)); + goto fail; + } + } + } + + ret = closedir(rootdir); + rootdir = NULL; + if (ret != 0) { + ret = errno; + goto fail; + } + + if (!keep_root_dir) { + /* Remove also root directory. */ + ret = unlinkat(parent_fd, dir_name, AT_REMOVEDIR); + if (ret == -1) { + ret = errno; + } + } + + ret = EOK; +fail: + if (rootdir) { /* clean up on abnormal exit but retain return code */ + err = closedir(rootdir); + if (err) { + DEBUG(SSSDBG_CRIT_FAILURE, "closedir failed, bad dirp?\n"); + } + } + return ret; +} + +static char *talloc_readlinkat(TALLOC_CTX *mem_ctx, int dir_fd, + const char *filename) +{ + size_t size = 1024; + ssize_t nchars; + char *buffer; + char *new_buffer; + + buffer = talloc_array(mem_ctx, char, size); + if (!buffer) { + return NULL; + } + + while (1) { + nchars = readlinkat(dir_fd, filename, buffer, size); + if (nchars < 0) { + talloc_free(buffer); + return NULL; + } + + if ((size_t) nchars < size) { + /* The buffer was large enough */ + break; + } + + /* Try again with a bigger buffer */ + size *= 2; + new_buffer = talloc_realloc(mem_ctx, buffer, char, size); + if (!new_buffer) { + talloc_free(buffer); + return NULL; + } + buffer = new_buffer; + } + + /* readlink does not nul-terminate */ + buffer[nchars] = '\0'; + return buffer; +} + +static int +copy_symlink(int src_dir_fd, + int dst_dir_fd, + const char *file_name, + const char *full_path, + const struct stat *statp, + uid_t uid, gid_t gid) +{ + char *buf; + errno_t ret; + + buf = talloc_readlinkat(NULL, src_dir_fd, file_name); + if (!buf) { + return ENOMEM; + } + + ret = selinux_file_context(full_path); + if (ret != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to set SELinux context for [%s]\n", full_path); + /* Not fatal */ + } + + ret = symlinkat(buf, dst_dir_fd, file_name); + talloc_free(buf); + if (ret == -1) { + ret = errno; + if (ret == EEXIST) { + DEBUG(SSSDBG_MINOR_FAILURE, + "symlink pointing to already exists at '%s'\n", full_path); + return EOK; + } + + DEBUG(SSSDBG_CRIT_FAILURE, "symlinkat failed: %s\n", strerror(ret)); + return ret; + } + + ret = fchownat(dst_dir_fd, file_name, + uid, gid, AT_SYMLINK_NOFOLLOW); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "fchownat failed: %s\n", strerror(ret)); + return ret; + } + + ret = sss_timeat_set(dst_dir_fd, file_name, statp, + AT_SYMLINK_NOFOLLOW); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, "utimensat failed [%d]: %s\n", + ret, strerror(ret)); + /* Do not fail */ + } + + return EOK; +} + +static int +copy_file_contents(int ifd, + int ofd, + mode_t mode, + uid_t uid, gid_t gid) +{ + errno_t ret; + char buf[1024]; + ssize_t cnt, written; + + while ((cnt = sss_atomic_read_s(ifd, buf, sizeof(buf))) != 0) { + if (cnt == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot read() from source file: [%d][%s].\n", + ret, strerror(ret)); + goto done; + } + + errno = 0; + written = sss_atomic_write_s(ofd, buf, cnt); + if (written == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot write() to destination file: [%d][%s].\n", + ret, strerror(ret)); + goto done; + } + + if (written != cnt) { + ret = EINVAL; + DEBUG(SSSDBG_CRIT_FAILURE, + "Wrote %zd bytes, expected %zd\n", written, cnt); + goto done; + } + } + + /* Set the ownership; permissions are still + * restrictive. */ + ret = fchown(ofd, uid, gid); + if (ret == -1 && errno != EPERM) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, + "Error changing owner: %s\n", + strerror(ret)); + goto done; + } + + /* Set the desired mode. */ + ret = fchmod(ofd, mode); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, "Error changing mode: %s\n", + strerror(ret)); + goto done; + } + + ret = EOK; + +done: + return ret; +} + + +/* Copy bytes from input file descriptor ifd into file named + * dst_named under directory with dest_dir_fd. Own the new file + * by uid/gid + */ +static int +copy_file(int ifd, + int dest_dir_fd, + const char *file_name, + const char *full_path, + const struct stat *statp, + uid_t uid, gid_t gid) +{ + int ofd = -1; + errno_t ret; + + ret = selinux_file_context(full_path); + if (ret != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to set SELinux context for [%s]\n", full_path); + /* Not fatal */ + } + + /* Start with absolutely restrictive permissions */ + ofd = openat(dest_dir_fd, file_name, + O_EXCL | O_CREAT | O_WRONLY | O_NOFOLLOW, + 0); + if (ofd < 0 && errno != EEXIST) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, + "Cannot open() destination file '%s': [%d][%s].\n", + full_path, ret, strerror(ret)); + goto done; + } + + ret = copy_file_contents(ifd, ofd, statp->st_mode, uid, gid); + if (ret != EOK) goto done; + + + ret = sss_futime_set(ofd, statp); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, "sss_futime_set failed [%d]: %s\n", + ret, strerror(ret)); + /* Do not fail */ + } + ret = EOK; + +done: + if (ofd != -1) close(ofd); + return ret; +} + +int +sss_copy_file_secure(const char *src, + const char *dest, + mode_t mode, + uid_t uid, gid_t gid, + bool force) +{ + int ifd = -1; + int ofd = -1; + int dest_flags = 0; + errno_t ret; + + ret = selinux_file_context(dest); + if (ret != 0) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to set SELinux context for [%s]\n", dest); + /* Not fatal */ + } + + /* Start with absolutely restrictive permissions */ + dest_flags = O_CREAT | O_WRONLY | O_NOFOLLOW; + if (!force) { + dest_flags |= O_EXCL; + } + + ofd = open(dest, dest_flags, mode); + if (ofd < 0) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot open() destination file '%s': [%d][%s].\n", + dest, errno, strerror(errno)); + goto done; + } + + ifd = sss_open_cloexec(src, O_RDONLY | O_NOFOLLOW, &ret); + if (ifd < 0) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot open() source file '%s': [%d][%s].\n", + src, ret, strerror(ret)); + goto done; + } + + ret = copy_file_contents(ifd, ofd, mode, uid, gid); + +done: + if (ifd != -1) close(ifd); + if (ofd != -1) close(ofd); + return ret; +} + +static errno_t +copy_dir(struct copy_ctx *cctx, + int src_dir_fd, const char *src_dir_path, + int dest_parent_fd, const char *dest_dir_name, + const char *dest_dir_path, + mode_t mode, + const struct stat *src_dir_stat); + +static errno_t +copy_entry(struct copy_ctx *cctx, + int src_dir_fd, + const char *src_dir_path, + int dest_dir_fd, + const char *dest_dir_path, + const char *ent_name) +{ + char *src_ent_path = NULL; + char *dest_ent_path = NULL; + int ifd = -1; + errno_t ret; + struct stat st; + + /* Build the path of the source file or directory and its + * corresponding member in the new tree. */ + src_ent_path = talloc_asprintf(cctx, "%s/%s", src_dir_path, ent_name); + dest_ent_path = talloc_asprintf(cctx, "%s/%s", dest_dir_path, ent_name); + if (!src_ent_path || !dest_ent_path) { + ret = ENOMEM; + goto done; + } + + /* Open the input entry first, then we can fstat() it and be + * certain that it is still the same file. O_NONBLOCK protects + * us against FIFOs and perhaps side-effects of the open() of a + * device file if there ever was one here, and doesn't matter + * for regular files or directories. */ + ifd = sss_openat_cloexec(src_dir_fd, ent_name, + O_RDONLY | O_NOFOLLOW | O_NONBLOCK, &ret); + if (ifd == -1 && ret != ELOOP) { + /* openat error */ + DEBUG(SSSDBG_CRIT_FAILURE, "openat failed on '%s': %s\n", + src_ent_path, strerror(ret)); + goto done; + } else if (ifd == -1 && ret == ELOOP) { + /* Should be a symlink.. */ + ret = fstatat(src_dir_fd, ent_name, &st, AT_SYMLINK_NOFOLLOW); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "fstatat failed on '%s': %s\n", + src_ent_path, strerror(ret)); + goto done; + } + + /* Handle symlinks */ + ret = copy_symlink(src_dir_fd, dest_dir_fd, ent_name, + dest_ent_path, &st, cctx->uid, cctx->gid); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot copy '%s' to '%s'\n", + src_ent_path, dest_ent_path); + } + goto done; + } + + ret = fstat(ifd, &st); + if (ret != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "couldn't stat '%s': %s\n", src_ent_path, strerror(ret)); + goto done; + } + + if (S_ISDIR(st.st_mode)) { + /* If it's a directory, descend into it. */ + ret = copy_dir(cctx, ifd, src_ent_path, + dest_dir_fd, ent_name, + dest_ent_path, st.st_mode & 07777, + &st); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Couldn't recursively copy '%s' to '%s': %s\n", + src_ent_path, dest_ent_path, strerror(ret)); + goto done; + } + } else if (S_ISREG(st.st_mode)) { + /* Copy a regular file */ + ret = copy_file(ifd, dest_dir_fd, ent_name, dest_ent_path, + &st, cctx->uid, cctx->gid); + if (ret) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot copy '%s' to '%s'\n", + src_ent_path, dest_ent_path); + goto done; + } + } else { + /* Is a special file */ + DEBUG(SSSDBG_FUNC_DATA, "'%s' is a special file, skipping.\n", + src_ent_path); + } + + ret = EOK; +done: + talloc_free(src_ent_path); + talloc_free(dest_ent_path); + if (ifd != -1) close(ifd); + return ret; +} + +static errno_t +copy_dir(struct copy_ctx *cctx, + int src_dir_fd, const char *src_dir_path, + int dest_parent_fd, const char *dest_dir_name, + const char *dest_dir_path, + mode_t mode, + const struct stat *src_dir_stat) +{ + errno_t ret; + errno_t dret; + int dest_dir_fd = -1; + DIR *dir = NULL; + struct dirent *ent; + + if (!dest_dir_path) { + return EINVAL; + } + + dir = fdopendir(src_dir_fd); + if (dir == NULL) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Error reading '%s': %s\n", src_dir_path, strerror(ret)); + goto done; + } + + /* Create the directory. It starts owned by us (presumbaly root), with + * fairly restrictive permissions that still allow us to use the + * directory. + * */ + errno = 0; + ret = mkdirat(dest_parent_fd, dest_dir_name, S_IRWXU); + if (ret == -1 && errno != EEXIST) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Error reading '%s': %s\n", dest_dir_path, strerror(ret)); + goto done; + } + + dest_dir_fd = sss_openat_cloexec(dest_parent_fd, dest_dir_name, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW, &ret); + if (dest_dir_fd == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Error opening '%s': %s\n", dest_dir_path, strerror(ret)); + goto done; + } + + while ((ent = readdir(dir)) != NULL) { + /* Iterate through each item in the directory. */ + /* Skip over self and parent hard links. */ + if (strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0) { + continue; + } + + ret = copy_entry(cctx, + src_dir_fd, src_dir_path, + dest_dir_fd, dest_dir_path, + ent->d_name); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Could not copy [%s] to [%s]\n", + src_dir_path, dest_dir_path); + goto done; + } + } + + /* Set the ownership on the directory. Permissions are still + * fairly restrictive. */ + ret = fchown(dest_dir_fd, cctx->uid, cctx->gid); + if (ret == -1 && errno != EPERM) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, + "Error changing owner of '%s': %s\n", + dest_dir_path, strerror(ret)); + goto done; + } + + /* Set the desired mode. Do this explicitly to preserve S_ISGID and + * other bits. Do this after chown, because chown is permitted to + * reset these bits. */ + ret = fchmod(dest_dir_fd, mode); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, + "Error setting mode of '%s': %s\n", + dest_dir_path, strerror(ret)); + goto done; + } + + sss_futime_set(dest_dir_fd, src_dir_stat); + if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, "sss_futime_set failed [%d]: %s\n", + ret, strerror(ret)); + /* Do not fail */ + } + + ret = EOK; +done: + if (dir) { + dret = closedir(dir); + if (dret != 0) { + dret = errno; + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to close directory: %s.\n", strerror(dret)); + } + } + + if (dest_dir_fd != -1) { + close(dest_dir_fd); + } + return ret; +} + +/* NOTE: + * For several reasons, including the fact that we copy even special files + * (pipes, etc) from the skeleton directory, the skeldir needs to be trusted + */ +int sss_copy_tree(const char *src_root, + const char *dst_root, + mode_t mode_root, + uid_t uid, gid_t gid) +{ + int ret = EOK; + struct copy_ctx *cctx = NULL; + int fd = -1; + struct stat s_src; + + fd = sss_open_cloexec(src_root, O_RDONLY | O_DIRECTORY, &ret); + if (fd == -1) { + goto fail; + } + + ret = fstat(fd, &s_src); + if (ret == -1) { + ret = errno; + goto fail; + } + + cctx = talloc_zero(NULL, struct copy_ctx); + if (!cctx) { + ret = ENOMEM; + goto fail; + } + + cctx->src_orig = src_root; + cctx->dst_orig = dst_root; + cctx->src_dev = s_src.st_dev; + cctx->uid = uid; + cctx->gid = gid; + + ret = copy_dir(cctx, fd, src_root, AT_FDCWD, + dst_root, dst_root, mode_root, &s_src); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "copy_dir failed: [%d][%s]\n", ret, strerror(ret)); + goto fail; + } + +fail: + if (fd != -1) close(fd); + reset_selinux_file_context(); + talloc_free(cctx); + return ret; +} |