diff --git a/glnx-fdio.c b/glnx-fdio.c index 0046807ad8f589ac5a8a5360aa6b6b90a677c9f2..8093836c335f0b941f8abc22398ae6860ab2da8a 100644 --- a/glnx-fdio.c +++ b/glnx-fdio.c @@ -872,11 +872,15 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) * @cancellable: cancellable * @error: Error * - * Perform a full copy of the regular file or - * symbolic link from @src_subpath to @dest_subpath. + * Perform a full copy of the regular file or symbolic link from @src_subpath to + * @dest_subpath; if @src_subpath is anything other than a regular file or + * symbolic link, an error will be returned. * - * If @src_subpath is anything other than a regular - * file or symbolic link, an error will be returned. + * If the source is a regular file and the destination exists as a symbolic + * link, the symbolic link will not be followed; rather the link itself will be + * replaced. Related to this: for regular files, when `GLNX_FILE_COPY_OVERWRITE` + * is specified, this function always uses `O_TMPFILE` (if available) and does a + * rename-into-place rather than `open(O_TRUNC)`. */ gboolean glnx_file_copy_at (int src_dfd, @@ -888,31 +892,23 @@ glnx_file_copy_at (int src_dfd, GCancellable *cancellable, GError **error) { - gboolean ret = FALSE; - int r; - int dest_open_flags; - struct timespec ts[2]; - glnx_fd_close int src_fd = -1; - glnx_fd_close int dest_fd = -1; - struct stat local_stbuf; - - if (g_cancellable_set_error_if_cancelled (cancellable, error)) - goto out; - + /* Canonicalize dfds */ src_dfd = glnx_dirfd_canonicalize (src_dfd); dest_dfd = glnx_dirfd_canonicalize (dest_dfd); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + /* Automatically do stat() if no stat buffer was supplied */ + struct stat local_stbuf; if (!src_stbuf) { - if (fstatat (src_dfd, src_subpath, &local_stbuf, AT_SYMLINK_NOFOLLOW) != 0) - { - glnx_set_error_from_errno (error); - goto out; - } + if (!glnx_fstatat (src_dfd, src_subpath, &local_stbuf, AT_SYMLINK_NOFOLLOW, error)) + return FALSE; src_stbuf = &local_stbuf; } + /* For symlinks, defer entirely to copy_symlink_at() */ if (S_ISLNK (src_stbuf->st_mode)) { return copy_symlink_at (src_dfd, src_subpath, src_stbuf, @@ -924,47 +920,26 @@ glnx_file_copy_at (int src_dfd, { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Cannot copy non-regular/non-symlink file: %s", src_subpath); - goto out; + return FALSE; } - if (!glnx_openat_rdonly (src_dfd, src_subpath, FALSE, &src_fd, error)) - goto out; - - dest_open_flags = O_WRONLY | O_CREAT | O_CLOEXEC | O_NOCTTY; - if (!(copyflags & GLNX_FILE_COPY_OVERWRITE)) - dest_open_flags |= O_EXCL; - else - dest_open_flags |= O_TRUNC; - - dest_fd = TEMP_FAILURE_RETRY (openat (dest_dfd, dest_subpath, dest_open_flags, src_stbuf->st_mode)); - if (dest_fd == -1) - { - glnx_set_error_from_errno (error); - goto out; - } + /* Regular file path below here */ - r = glnx_regfile_copy_bytes (src_fd, dest_fd, (off_t) -1); - if (r < 0) - { - glnx_set_error_from_errno (error); - goto out; - } + glnx_fd_close int src_fd = -1; + if (!glnx_openat_rdonly (src_dfd, src_subpath, FALSE, &src_fd, error)) + return FALSE; - if (fchown (dest_fd, src_stbuf->st_uid, src_stbuf->st_gid) != 0) - { - glnx_set_error_from_errno (error); - goto out; - } + /* Open a tmpfile for dest */ + g_auto(GLnxTmpfile) tmp_dest = { 0, }; + if (!glnx_open_tmpfile_linkable_at (dest_dfd, ".", O_WRONLY | O_CLOEXEC, + &tmp_dest, error)) + return FALSE; - if (fchmod (dest_fd, src_stbuf->st_mode & 07777) != 0) - { - glnx_set_error_from_errno (error); - goto out; - } + if (glnx_regfile_copy_bytes (src_fd, tmp_dest.fd, (off_t) -1) < 0) + return glnx_throw_errno_prefix (error, "regfile copy"); - ts[0] = src_stbuf->st_atim; - ts[1] = src_stbuf->st_mtim; - (void) futimens (dest_fd, ts); + if (fchown (tmp_dest.fd, src_stbuf->st_uid, src_stbuf->st_gid) != 0) + return glnx_throw_errno_prefix (error, "fchown"); if (!(copyflags & GLNX_FILE_COPY_NOXATTRS)) { @@ -972,35 +947,40 @@ glnx_file_copy_at (int src_dfd, if (!glnx_fd_get_all_xattrs (src_fd, &xattrs, cancellable, error)) - goto out; + return FALSE; - if (!glnx_fd_set_all_xattrs (dest_fd, xattrs, + if (!glnx_fd_set_all_xattrs (tmp_dest.fd, xattrs, cancellable, error)) - goto out; + return FALSE; } + /* Always chmod after setting xattrs, in case the file has mode 0400 or less, + * like /etc/shadow. Linux currently allows write() on non-writable open files + * but not fsetxattr(). + */ + if (fchmod (tmp_dest.fd, src_stbuf->st_mode & 07777) != 0) + return glnx_throw_errno_prefix (error, "fchmod"); + + struct timespec ts[2]; + ts[0] = src_stbuf->st_atim; + ts[1] = src_stbuf->st_mtim; + (void) futimens (tmp_dest.fd, ts); + if (copyflags & GLNX_FILE_COPY_DATASYNC) { - if (fdatasync (dest_fd) < 0) - { - glnx_set_error_from_errno (error); - goto out; - } - } - - r = close (dest_fd); - dest_fd = -1; - if (r < 0) - { - glnx_set_error_from_errno (error); - goto out; + if (fdatasync (tmp_dest.fd) < 0) + return glnx_throw_errno_prefix (error, "fdatasync"); } - ret = TRUE; - out: - if (!ret) - (void) unlinkat (dest_dfd, dest_subpath, 0); - return ret; + const GLnxLinkTmpfileReplaceMode replacemode = + (copyflags & GLNX_FILE_COPY_OVERWRITE) ? + GLNX_LINK_TMPFILE_REPLACE : + GLNX_LINK_TMPFILE_NOREPLACE; + + if (!glnx_link_tmpfile_at (&tmp_dest, replacemode, dest_dfd, dest_subpath, error)) + return FALSE; + + return TRUE; } /** diff --git a/tests/test-libglnx-fdio.c b/tests/test-libglnx-fdio.c index 36ded80c395d54c7a72fd98e1aabf97b3e54865c..4047df646d25439b5d50a11404ad832345344f6b 100644 --- a/tests/test-libglnx-fdio.c +++ b/tests/test-libglnx-fdio.c @@ -211,6 +211,67 @@ test_fstatat (void) g_assert_no_error (local_error); } +static void +test_filecopy (void) +{ + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + g_auto(GLnxTmpfile) tmpf = { 0, }; + const char foo[] = "foo"; + + if (!glnx_file_replace_contents_at (AT_FDCWD, foo, (guint8*)foo, sizeof (foo), + GLNX_FILE_REPLACE_NODATASYNC, NULL, error)) + goto out; + + if (!glnx_file_copy_at (AT_FDCWD, foo, NULL, AT_FDCWD, "bar", + GLNX_FILE_COPY_NOXATTRS, NULL, error)) + goto out; + + if (glnx_file_copy_at (AT_FDCWD, foo, NULL, AT_FDCWD, "bar", + GLNX_FILE_COPY_NOXATTRS, NULL, error)) + g_assert_not_reached (); + g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS); + g_clear_error (&local_error); + + if (!glnx_file_copy_at (AT_FDCWD, foo, NULL, AT_FDCWD, "bar", + GLNX_FILE_COPY_NOXATTRS | GLNX_FILE_COPY_OVERWRITE, + NULL, error)) + goto out; + + if (symlinkat ("nosuchtarget", AT_FDCWD, "link") < 0) + { + glnx_throw_errno_prefix (error, "symlinkat"); + goto out; + } + + /* Shouldn't be able to overwrite a symlink without GLNX_FILE_COPY_OVERWRITE */ + if (glnx_file_copy_at (AT_FDCWD, foo, NULL, AT_FDCWD, "link", + GLNX_FILE_COPY_NOXATTRS, + NULL, error)) + g_assert_not_reached (); + g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS); + g_clear_error (&local_error); + + /* Test overwriting symlink */ + if (!glnx_file_copy_at (AT_FDCWD, foo, NULL, AT_FDCWD, "link", + GLNX_FILE_COPY_NOXATTRS | GLNX_FILE_COPY_OVERWRITE, + NULL, error)) + goto out; + + struct stat stbuf; + if (!glnx_fstatat_allow_noent (AT_FDCWD, "nosuchtarget", &stbuf, AT_SYMLINK_NOFOLLOW, error)) + goto out; + g_assert_cmpint (errno, ==, ENOENT); + g_assert_no_error (local_error); + + if (!glnx_fstatat (AT_FDCWD, "link", &stbuf, AT_SYMLINK_NOFOLLOW, error)) + goto out; + g_assert (S_ISREG (stbuf.st_mode)); + + out: + g_assert_no_error (local_error); +} + int main (int argc, char **argv) { int ret; @@ -219,6 +280,7 @@ int main (int argc, char **argv) g_test_add_func ("/tmpfile", test_tmpfile); g_test_add_func ("/stdio-file", test_stdio_file); + g_test_add_func ("/filecopy", test_filecopy); g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); g_test_add_func ("/fstat", test_fstatat);