/* Authors: Jakub Hrozek 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 . */ /* * 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 #include #include #include #include #include #include #include "util/util.h" #include "tools/tools_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); int 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); 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) { 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); 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: [%d][%s]\n", ret, strerror(ret)); goto fail; } } } ret = closedir(rootdir); rootdir = NULL; if (ret != 0) { ret = errno; goto fail; } 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; } /* 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; char buf[1024]; ssize_t cnt, written; 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; } 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) { 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 of '%s': %s\n", full_path, strerror(ret)); goto done; } /* Set the desired mode. */ ret = fchmod(ofd, statp->st_mode); if (ret == -1) { ret = errno; DEBUG(SSSDBG_OP_FAILURE, "Error changing owner of '%s': %s\n", full_path, strerror(ret)); 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 */ } close(ofd); ofd = -1; ret = EOK; done: 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", 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", 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", 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", 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", 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", 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 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; }