From 5becda752d18c40cdb80b8eca759df5e19ab31d3 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 14 Dec 2017 11:05:00 -0500 Subject: [PATCH] rofiles: Add --copyup option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sadly https://sourceware.org/bugzilla/show_bug.cgi?id=22089 is I think going to actually force us to cave here. Even if we got the glibc patch in today, we need to support the RHEL glibc. See also discussion about fish as part of the general Fedora tracker. This is basically needed to unblock rpm-ostree unified core 🌐: https://github.com/projectatomic/rpm-ostree/issues/729 Closes: https://github.com/ostreedev/ostree/issues/1377 --- src/rofiles-fuse/Makefile-inc.am | 5 +- src/rofiles-fuse/main.c | 122 +++++++++++++++++++++++-------- tests/test-rofiles-fuse.sh | 21 +++++- 3 files changed, 116 insertions(+), 32 deletions(-) diff --git a/src/rofiles-fuse/Makefile-inc.am b/src/rofiles-fuse/Makefile-inc.am index 5510a2bd45..623aa6c090 100644 --- a/src/rofiles-fuse/Makefile-inc.am +++ b/src/rofiles-fuse/Makefile-inc.am @@ -19,5 +19,6 @@ bin_PROGRAMS += rofiles-fuse rofiles_fuse_SOURCES = src/rofiles-fuse/main.c -rofiles_fuse_CFLAGS = $(AM_CFLAGS) -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 $(BUILDOPT_FUSE_CFLAGS) $(OT_INTERNAL_GIO_UNIX_CFLAGS) -I$(srcdir)/libglnx $(NULL) -rofiles_fuse_LDADD = libglnx.la $(BUILDOPT_FUSE_LIBS) $(OT_INTERNAL_GIO_UNIX_LIBS) +rofiles_fuse_CFLAGS = $(AM_CFLAGS) -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 $(BUILDOPT_FUSE_CFLAGS) \ + $(OT_INTERNAL_GIO_UNIX_CFLAGS) -I $(srcdir)/src/libostree -I$(srcdir)/libglnx +rofiles_fuse_LDADD = libglnx.la $(BUILDOPT_FUSE_LIBS) $(OT_INTERNAL_GIO_UNIX_LIBS) libostree-1.la diff --git a/src/rofiles-fuse/main.c b/src/rofiles-fuse/main.c index 9e04274bec..61a260b262 100644 --- a/src/rofiles-fuse/main.c +++ b/src/rofiles-fuse/main.c @@ -35,9 +35,14 @@ #include #include "libglnx.h" +#include "ostree.h" // Global to store our read-write path static int basefd = -1; +/* Whether or not to automatically "copyup" (in overlayfs terms). + * What we're really doing is breaking hardlinks. + */ +static gboolean opt_copyup; static inline const char * ENSURE_RELPATH (const char *path) @@ -200,52 +205,97 @@ callback_link (const char *from, const char *to) /* Check whether @stbuf refers to a hardlinked regfile or symlink, and if so * return -EROFS. Otherwise return 0. */ -static int -can_write_stbuf (struct stat *stbuf) +static gboolean +can_write_stbuf (const struct stat *stbuf) { /* If it's not a regular file or symlink, ostree won't hardlink it, so allow * writes - it might be a FIFO or device that somehow * ended up underneath our mount. */ if (!(S_ISREG (stbuf->st_mode) || S_ISLNK (stbuf->st_mode))) - return 0; + return TRUE; /* If the object isn't hardlinked, it's OK to write */ if (stbuf->st_nlink <= 1) - return 0; + return TRUE; /* Otherwise, it's a hardlinked file or symlink; it must be * immutable. */ - return -EROFS; + return FALSE; } -/* Check whether @path refers to a hardlinked regfile or symlink, and if so - * return -EROFS. Otherwise return 0. - */ static int -can_write (const char *path) +gioerror_to_errno (GIOErrorEnum e) { - struct stat stbuf; - if (fstatat (basefd, path, &stbuf, AT_SYMLINK_NOFOLLOW) == -1) + /* It's obviously crappy to have to do this but + * we also don't want to try to have "raw errno" versions + * of everything down in ostree_break_hardlink() so... + * let's just reverse map a few ones I think are going to be common. + */ + switch (e) { - if (errno == ENOENT) - return 0; + case G_IO_ERROR_NOT_FOUND: + return ENOENT; + case G_IO_ERROR_IS_DIRECTORY: + return EISDIR; + case G_IO_ERROR_PERMISSION_DENIED: + return EPERM; + case G_IO_ERROR_NO_SPACE: + return ENOSPC; + default: + return EIO; + } +} + +static int +verify_write_or_copyup (const char *path, const struct stat *stbuf) +{ + struct stat stbuf_local; + + /* If a stbuf wasn't provided, gather it now */ + if (!stbuf) + { + if (fstatat (basefd, path, &stbuf_local, AT_SYMLINK_NOFOLLOW) == -1) + { + if (errno == ENOENT) + return 0; + else + return -errno; + } + stbuf = &stbuf_local; + } + + /* Verify writability, if that fails, perform copy-up if enabled */ + if (!can_write_stbuf (stbuf)) + { + if (opt_copyup) + { + g_autoptr(GError) tmp_error = NULL; + if (!ostree_break_hardlink (basefd, path, FALSE, NULL, &tmp_error)) + return -gioerror_to_errno ((GIOErrorEnum)tmp_error->code); + } else - return -errno; + return -EROFS; } - return can_write_stbuf (&stbuf); + + return 0; } -#define VERIFY_WRITE(path) do { \ - int r = can_write (path); \ - if (r != 0) \ - return r; \ +/* Given a path (which is absolute), convert it + * to a relative path (even for the caller) and + * perform either write verification or copy-up. + */ +#define PATH_WRITE_ENTRYPOINT(path) do { \ + path = ENSURE_RELPATH (path); \ + int r = verify_write_or_copyup (path, NULL); \ + if (r != 0) \ + return r; \ } while (0) static int callback_chmod (const char *path, mode_t mode) { - path = ENSURE_RELPATH (path); - VERIFY_WRITE(path); + PATH_WRITE_ENTRYPOINT (path); + /* Note we can't use AT_SYMLINK_NOFOLLOW yet; * https://marc.info/?l=linux-kernel&m=148830147803162&w=2 * https://marc.info/?l=linux-fsdevel&m=149193779929561&w=2 @@ -258,8 +308,8 @@ callback_chmod (const char *path, mode_t mode) static int callback_chown (const char *path, uid_t uid, gid_t gid) { - path = ENSURE_RELPATH (path); - VERIFY_WRITE(path); + PATH_WRITE_ENTRYPOINT (path); + if (fchownat (basefd, path, uid, gid, AT_SYMLINK_NOFOLLOW) != 0) return -errno; return 0; @@ -268,12 +318,9 @@ callback_chown (const char *path, uid_t uid, gid_t gid) static int callback_truncate (const char *path, off_t size) { - glnx_autofd int fd = -1; + PATH_WRITE_ENTRYPOINT (path); - path = ENSURE_RELPATH (path); - VERIFY_WRITE(path); - - fd = openat (basefd, path, O_NOFOLLOW|O_WRONLY); + glnx_autofd int fd = openat (basefd, path, O_NOFOLLOW|O_WRONLY); if (fd == -1) return -errno; @@ -286,6 +333,9 @@ callback_truncate (const char *path, off_t size) static int callback_utimens (const char *path, const struct timespec tv[2]) { + /* This one isn't write-verified, we support changing times + * even for hardlinked files. + */ path = ENSURE_RELPATH (path); if (utimensat (basefd, path, tv, AT_SYMLINK_NOFOLLOW) == -1) @@ -324,13 +374,22 @@ do_open (const char *path, mode_t mode, struct fuse_file_info *finfo) return -errno; } - int r = can_write_stbuf (&stbuf); + int r = verify_write_or_copyup (path, &stbuf); if (r != 0) { (void) close (fd); return r; } + /* In the copyup case, we need to re-open */ + if (opt_copyup) + { + (void) close (fd); + fd = openat (basefd, path, finfo->flags & ~O_TRUNC, mode); + if (fd == -1) + return -errno; + } + /* Handle O_TRUNC here only after verifying hardlink state */ if (finfo->flags & O_TRUNC) { @@ -521,6 +580,7 @@ struct fuse_operations callback_oper = { enum { KEY_HELP, KEY_VERSION, + KEY_COPYUP, }; static void @@ -565,6 +625,9 @@ rofs_parse_opt (void *data, const char *arg, int key, case KEY_HELP: usage (outargs->argv[0]); exit (EXIT_SUCCESS); + case KEY_COPYUP: + opt_copyup = TRUE; + return 0; default: fprintf (stderr, "see `%s -h' for usage\n", outargs->argv[0]); exit (EXIT_FAILURE); @@ -577,6 +640,7 @@ static struct fuse_opt rofs_opts[] = { FUSE_OPT_KEY ("--help", KEY_HELP), FUSE_OPT_KEY ("-V", KEY_VERSION), FUSE_OPT_KEY ("--version", KEY_VERSION), + FUSE_OPT_KEY ("--copyup", KEY_COPYUP), FUSE_OPT_END }; diff --git a/tests/test-rofiles-fuse.sh b/tests/test-rofiles-fuse.sh index 6222929e56..533eb35748 100755 --- a/tests/test-rofiles-fuse.sh +++ b/tests/test-rofiles-fuse.sh @@ -26,7 +26,7 @@ skip_without_user_xattrs setup_test_repository "bare" -echo "1..8" +echo "1..10" cd ${test_tmpdir} mkdir mnt @@ -117,3 +117,22 @@ echo "ok checkout copy fallback" # check that O_RDONLY|O_CREAT is handled correctly; used by flock(1) at least flock mnt/nonexistent-file echo "ok create file in ro mode" +echo "ok flock" + +# And now with --copyup enabled + +fusermount -u ${test_tmpdir}/mnt +assert_not_has_file mnt/firstfile +rofiles-fuse --copyup checkout-test2 mnt +assert_file_has_content mnt/firstfile first +echo "ok copyup mount" + +firstfile_orig_inode=$(stat -c %i checkout-test2/firstfile) +for path in firstfile{,-link}; do + echo truncating > mnt/${path} + assert_file_has_content mnt/${path} truncating +done +firstfile_new_inode=$(stat -c %i checkout-test2/firstfile) +assert_not_streq "${firstfile_orig_inode}" "${firstfile_new_inode}" + +echo "ok copyup"