| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <sys/ioctl.h> |
| |
| #include "btrfs-util.h" |
| #include "chattr-util.h" |
| #include "errno-util.h" |
| #include "fd-util.h" |
| #include "fs-util.h" |
| #include "install-file.h" |
| #include "missing_syscall.h" |
| #include "rm-rf.h" |
| #include "sync-util.h" |
| |
| int fs_make_very_read_only(int fd) { |
| struct stat st; |
| int r; |
| |
| assert(fd >= 0); |
| |
| /* Tries to make the specified fd "comprehensively" read-only. Primary usecase for this is OS images, |
| * i.e. either loopback files or larger directory hierarchies. Depending on the inode type and |
| * backing file system this means something different: |
| * |
| * 1. If the fd refers to a btrfs subvolume we'll mark it read-only as a whole |
| * 2. If the fd refers to any other directory we'll set the FS_IMMUTABLE_FL flag on it |
| * 3. If the fd refers to a regular file we'll drop the w bits. |
| * 4. If the fd refers to a block device, use BLKROSET to set read-only state |
| * |
| * You might wonder why not drop the x bits for directories. That's because we want to guarantee that |
| * everything "inside" the image remains largely the way it is, in case you mount it. And since the |
| * mode of the root dir of the image is pretty visible we don't want to modify it. btrfs subvol flags |
| * and the FS_IMMUTABLE_FL otoh are much less visible. Changing the mode of regular files should be |
| * OK though, since after all this is supposed to be used for disk images, i.e. the fs in the disk |
| * image doesn't make the mode of the loopback file it is stored in visible. */ |
| |
| if (fstat(fd, &st) < 0) |
| return -errno; |
| |
| switch (st.st_mode & S_IFMT) { |
| |
| case S_IFDIR: |
| if (btrfs_might_be_subvol(&st)) { |
| r = btrfs_subvol_set_read_only_fd(fd, true); |
| if (r >= 0) |
| return 0; |
| |
| if (!ERRNO_IS_NOT_SUPPORTED(r) && r != -EINVAL) |
| return r; |
| } |
| |
| r = chattr_fd(fd, FS_IMMUTABLE_FL, FS_IMMUTABLE_FL, NULL); |
| if (r < 0) |
| return r; |
| |
| break; |
| |
| case S_IFREG: |
| if ((st.st_mode & 0222) != 0) |
| if (fchmod(fd, st.st_mode & 07555) < 0) |
| return -errno; |
| |
| break; |
| |
| case S_IFBLK: { |
| int ro = 1; |
| |
| if (ioctl(fd, BLKROSET, &ro) < 0) |
| return -errno; |
| |
| break; |
| } |
| |
| default: |
| return -EBADFD; |
| } |
| |
| return 0; |
| } |
| |
| static int unlinkat_maybe_dir(int dirfd, const char *pathname) { |
| |
| /* Invokes unlinkat() for regular files first, and if this fails with EISDIR tries again with |
| * AT_REMOVEDIR */ |
| |
| if (unlinkat(dirfd, pathname, 0) < 0) { |
| if (errno != EISDIR) |
| return -errno; |
| |
| if (unlinkat(dirfd, pathname, AT_REMOVEDIR) < 0) |
| return -errno; |
| } |
| |
| return 0; |
| } |
| |
| int install_file(int source_atfd, const char *source_name, |
| int target_atfd, const char *target_name, |
| InstallFileFlags flags) { |
| |
| _cleanup_close_ int rofd = -EBADF; |
| int r; |
| |
| /* Moves a file or directory tree into place, with some bells and whistles: |
| * |
| * 1. Optionally syncs before/after to ensure file installation can be used as barrier |
| * 2. Optionally marks the file/directory read-only using fs_make_very_read_only() |
| * 3. Optionally operates in replacing or in non-replacing mode. |
| * 4. If it replaces will remove the old tree if needed. |
| */ |
| |
| assert(source_atfd >= 0 || source_atfd == AT_FDCWD); |
| assert(source_name); |
| assert(target_atfd >= 0 || target_atfd == AT_FDCWD); |
| |
| /* If target_name is specified as NULL no renaming takes place. Instead it is assumed the file is |
| * already in place, and only the syncing/read-only marking shall be applied. Note that with |
| * target_name=NULL and flags=0 this call is a NOP */ |
| |
| if ((flags & (INSTALL_FSYNC|INSTALL_FSYNC_FULL|INSTALL_SYNCFS|INSTALL_READ_ONLY)) != 0) { |
| _cleanup_close_ int pfd = -EBADF; |
| struct stat st; |
| |
| /* Open an O_PATH fd for the source if we need to sync things or mark things read only. */ |
| |
| pfd = openat(source_atfd, source_name, O_PATH|O_CLOEXEC|O_NOFOLLOW); |
| if (pfd < 0) |
| return -errno; |
| |
| if (fstat(pfd, &st) < 0) |
| return -errno; |
| |
| switch (st.st_mode & S_IFMT) { |
| |
| case S_IFREG: { |
| _cleanup_close_ int regfd = -EBADF; |
| |
| regfd = fd_reopen(pfd, O_RDONLY|O_CLOEXEC); |
| if (regfd < 0) |
| return regfd; |
| |
| if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { |
| /* If this is just a regular file (as oppose to a fully populated directory) |
| * let's downgrade INSTALL_SYNCFS to INSTALL_FSYNC_FULL, after all this is |
| * going to be a single inode we install */ |
| r = fsync_full(regfd); |
| if (r < 0) |
| return r; |
| } else if (flags & INSTALL_FSYNC) { |
| if (fsync(regfd) < 0) |
| return -errno; |
| } |
| |
| if (flags & INSTALL_READ_ONLY) |
| rofd = TAKE_FD(regfd); |
| |
| break; |
| } |
| |
| case S_IFDIR: { |
| _cleanup_close_ int dfd = -EBADF; |
| |
| dfd = fd_reopen(pfd, O_RDONLY|O_DIRECTORY|O_CLOEXEC); |
| if (dfd < 0) |
| return dfd; |
| |
| if (flags & INSTALL_SYNCFS) { |
| if (syncfs(dfd) < 0) |
| return -errno; |
| } else if (flags & INSTALL_FSYNC_FULL) { |
| r = fsync_full(dfd); |
| if (r < 0) |
| return r; |
| } else if (flags & INSTALL_FSYNC) { |
| if (fsync(dfd) < 0) |
| return -errno; |
| } |
| |
| if (flags & INSTALL_READ_ONLY) |
| rofd = TAKE_FD(dfd); |
| |
| break; |
| } |
| |
| default: |
| /* Other inodes: char/block device inodes, fifos, symlinks, sockets don't need |
| * syncing themselves, as they only exist in the directory, and have no contents on |
| * disk */ |
| |
| if (target_name && (flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { |
| r = fsync_directory_of_file(pfd); |
| if (r < 0) |
| return r; |
| } |
| |
| break; |
| } |
| } |
| |
| if (target_name) { |
| /* Rename the file */ |
| |
| if (flags & INSTALL_REPLACE) { |
| /* First, try a simple renamat(), maybe that's enough */ |
| if (renameat(source_atfd, source_name, target_atfd, target_name) < 0) { |
| _cleanup_close_ int dfd = -EBADF; |
| |
| if (!IN_SET(errno, EEXIST, ENOTDIR, ENOTEMPTY, EISDIR, EBUSY)) |
| return -errno; |
| |
| /* Hmm, the target apparently existed already. Let's try to use |
| * RENAME_EXCHANGE. But let's first open the inode if it's a directory, so |
| * that we can later remove its contents if it's a directory. Why do this |
| * before the rename()? Mostly because if we have trouble opening the thing |
| * we want to know before we start actually modifying the file system. */ |
| |
| dfd = openat(target_atfd, target_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC, 0); |
| if (dfd < 0 && errno != ENOTDIR) |
| return -errno; |
| |
| if (renameat2(source_atfd, source_name, target_atfd, target_name, RENAME_EXCHANGE) < 0) { |
| |
| if (!ERRNO_IS_NOT_SUPPORTED(errno) && errno != EINVAL) |
| return -errno; |
| |
| /* The exchange didn't work, let's remove the target first, and try again */ |
| |
| if (dfd >= 0) |
| (void) rm_rf_children(TAKE_FD(dfd), REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, NULL); |
| |
| r = unlinkat_maybe_dir(target_atfd, target_name); |
| if (r < 0) |
| return log_debug_errno(r, "Failed to remove target directory: %m"); |
| |
| if (renameat(source_atfd, source_name, target_atfd, target_name) < 0) |
| return -errno; |
| } else { |
| /* The exchange worked, hence let's remove the source (i.e. the old target) */ |
| if (dfd >= 0) |
| (void) rm_rf_children(TAKE_FD(dfd), REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, NULL); |
| |
| r = unlinkat_maybe_dir(source_atfd, source_name); |
| if (r < 0) |
| return log_debug_errno(r, "Failed to remove replaced target directory: %m"); |
| } |
| } |
| } else { |
| r = rename_noreplace(source_atfd, source_name, target_atfd, target_name); |
| if (r < 0) |
| return r; |
| } |
| } |
| |
| if (rofd >= 0) { |
| r = fs_make_very_read_only(rofd); |
| if (r < 0) |
| return r; |
| } |
| |
| if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { |
| if (target_name) |
| r = fsync_parent_at(target_atfd, target_name); |
| else |
| r = fsync_parent_at(source_atfd, source_name); |
| if (r < 0) |
| return r; |
| } |
| |
| return 0; |
| } |