files.c revision a3c8390d19593b1e5277d95bfb4ab206d4785150
/*
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"
#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;
}