1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <sys/ioctl.h>
4 
5 #include "btrfs-util.h"
6 #include "chattr-util.h"
7 #include "errno-util.h"
8 #include "fd-util.h"
9 #include "fs-util.h"
10 #include "install-file.h"
11 #include "missing_syscall.h"
12 #include "rm-rf.h"
13 #include "sync-util.h"
14 
fs_make_very_read_only(int fd)15 int fs_make_very_read_only(int fd) {
16         struct stat st;
17         int r;
18 
19         assert(fd >= 0);
20 
21         /* Tries to make the specified fd "comprehensively" read-only. Primary usecase for this is OS images,
22          * i.e. either loopback files or larger directory hierarchies. Depending on the inode type and
23          * backing file system this means something different:
24          *
25          * 1. If the fd refers to a btrfs subvolume we'll mark it read-only as a whole
26          * 2. If the fd refers to any other directory we'll set the FS_IMMUTABLE_FL flag on it
27          * 3. If the fd refers to a regular file we'll drop the w bits.
28          * 4. If the fd refers to a block device, use BLKROSET to set read-only state
29          *
30          * You might wonder why not drop the x bits for directories. That's because we want to guarantee that
31          * everything "inside" the image remains largely the way it is, in case you mount it. And since the
32          * mode of the root dir of the image is pretty visible we don't want to modify it. btrfs subvol flags
33          * and the FS_IMMUTABLE_FL otoh are much less visible. Changing the mode of regular files should be
34          * OK though, since after all this is supposed to be used for disk images, i.e. the fs in the disk
35          * image doesn't make the mode of the loopback file it is stored in visible. */
36 
37         if (fstat(fd, &st) < 0)
38                 return -errno;
39 
40         switch (st.st_mode & S_IFMT) {
41 
42         case S_IFDIR:
43                 if (btrfs_might_be_subvol(&st)) {
44                         r = btrfs_subvol_set_read_only_fd(fd, true);
45                         if (r >= 0)
46                                 return 0;
47 
48                         if (!ERRNO_IS_NOT_SUPPORTED(r) && r != -EINVAL)
49                                 return r;
50                 }
51 
52                 r = chattr_fd(fd, FS_IMMUTABLE_FL, FS_IMMUTABLE_FL, NULL);
53                 if (r < 0)
54                         return r;
55 
56                 break;
57 
58         case S_IFREG:
59                 if ((st.st_mode & 0222) != 0)
60                         if (fchmod(fd, st.st_mode & 07555) < 0)
61                                 return -errno;
62 
63                 break;
64 
65         case S_IFBLK: {
66                 int ro = 1;
67 
68                 if (ioctl(fd, BLKROSET, &ro) < 0)
69                         return -errno;
70 
71                 break;
72         }
73 
74         default:
75                 return -EBADFD;
76         }
77 
78         return 0;
79 }
80 
unlinkat_maybe_dir(int dirfd,const char * pathname)81 static int unlinkat_maybe_dir(int dirfd, const char *pathname) {
82 
83         /* Invokes unlinkat() for regular files first, and if this fails with EISDIR tries again with
84          * AT_REMOVEDIR */
85 
86         if (unlinkat(dirfd, pathname, 0) < 0) {
87                 if (errno != EISDIR)
88                         return -errno;
89 
90                 if (unlinkat(dirfd, pathname, AT_REMOVEDIR) < 0)
91                         return -errno;
92         }
93 
94         return 0;
95 }
96 
install_file(int source_atfd,const char * source_name,int target_atfd,const char * target_name,InstallFileFlags flags)97 int install_file(int source_atfd, const char *source_name,
98                  int target_atfd, const char *target_name,
99                  InstallFileFlags flags) {
100 
101         _cleanup_close_ int rofd = -1;
102         int r;
103 
104         /* Moves a file or directory tree into place, with some bells and whistles:
105          *
106          * 1. Optionally syncs before/after to ensure file installation can be used as barrier
107          * 2. Optionally marks the file/directory read-only using fs_make_very_read_only()
108          * 3. Optionally operates in replacing or in non-replacing mode.
109          * 4. If it replaces will remove the old tree if needed.
110          */
111 
112         assert(source_atfd >= 0 || source_atfd == AT_FDCWD);
113         assert(source_name);
114         assert(target_atfd >= 0 || target_atfd == AT_FDCWD);
115 
116         /* If target_name is specified as NULL no renaming takes place. Instead it is assumed the file is
117          * already in place, and only the syncing/read-only marking shall be applied. Note that with
118          * target_name=NULL and flags=0 this call is a NOP */
119 
120         if ((flags & (INSTALL_FSYNC|INSTALL_FSYNC_FULL|INSTALL_SYNCFS|INSTALL_READ_ONLY)) != 0) {
121                 _cleanup_close_ int pfd = -1;
122                 struct stat st;
123 
124                 /* Open an O_PATH fd for the source if we need to sync things or mark things read only. */
125 
126                 pfd = openat(source_atfd, source_name, O_PATH|O_CLOEXEC|O_NOFOLLOW);
127                 if (pfd < 0)
128                         return -errno;
129 
130                 if (fstat(pfd, &st) < 0)
131                         return -errno;
132 
133                 switch (st.st_mode & S_IFMT) {
134 
135                 case S_IFREG: {
136                         _cleanup_close_ int regfd = -1;
137 
138                         regfd = fd_reopen(pfd, O_RDONLY|O_CLOEXEC);
139                         if (regfd < 0)
140                                 return regfd;
141 
142                         if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) {
143                                 /* If this is just a regular file (as oppose to a fully populated directory)
144                                  * let's downgrade INSTALL_SYNCFS to INSTALL_FSYNC_FULL, after all this is
145                                  * going to be a single inode we install */
146                                 r = fsync_full(regfd);
147                                 if (r < 0)
148                                         return r;
149                         } else if (flags & INSTALL_FSYNC) {
150                                 if (fsync(regfd) < 0)
151                                         return -errno;
152                         }
153 
154                         if (flags & INSTALL_READ_ONLY)
155                                 rofd = TAKE_FD(regfd);
156 
157                         break;
158                 }
159 
160                 case S_IFDIR: {
161                         _cleanup_close_ int dfd = -1;
162 
163                         dfd = fd_reopen(pfd, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
164                         if (dfd < 0)
165                                 return dfd;
166 
167                         if (flags & INSTALL_SYNCFS) {
168                                 if (syncfs(dfd) < 0)
169                                         return -errno;
170                         } else if (flags & INSTALL_FSYNC_FULL) {
171                                 r = fsync_full(dfd);
172                                 if (r < 0)
173                                         return r;
174                         } else if (flags & INSTALL_FSYNC) {
175                                 if (fsync(dfd) < 0)
176                                         return -errno;
177                         }
178 
179                         if (flags & INSTALL_READ_ONLY)
180                                 rofd = TAKE_FD(dfd);
181 
182                         break;
183                 }
184 
185                 default:
186                         /* Other inodes: char/block device inodes, fifos, symlinks, sockets don't need
187                          * syncing themselves, as they only exist in the directory, and have no contents on
188                          * disk */
189 
190                         if (target_name && (flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) {
191                                 r = fsync_directory_of_file(pfd);
192                                 if (r < 0)
193                                         return r;
194                         }
195 
196                         break;
197                 }
198         }
199 
200         if (target_name) {
201                 /* Rename the file */
202 
203                 if (flags & INSTALL_REPLACE) {
204                         /* First, try a simple renamat(), maybe that's enough */
205                         if (renameat(source_atfd, source_name, target_atfd, target_name) < 0) {
206                                 _cleanup_close_ int dfd = -1;
207 
208                                 if (!IN_SET(errno, EEXIST, ENOTDIR, ENOTEMPTY, EISDIR, EBUSY))
209                                         return -errno;
210 
211                                 /* Hmm, the target apparently existed already. Let's try to use
212                                  * RENAME_EXCHANGE. But let's first open the inode if it's a directory, so
213                                  * that we can later remove its contents if it's a directory. Why do this
214                                  * before the rename()? Mostly because if we have trouble opening the thing
215                                  * we want to know before we start actually modifying the file system. */
216 
217                                 dfd = openat(target_atfd, target_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC, 0);
218                                 if (dfd < 0 && errno != ENOTDIR)
219                                         return -errno;
220 
221                                 if (renameat2(source_atfd, source_name, target_atfd, target_name, RENAME_EXCHANGE) < 0) {
222 
223                                         if (!ERRNO_IS_NOT_SUPPORTED(errno) && errno != EINVAL)
224                                                 return -errno;
225 
226                                         /* The exchange didn't work, let's remove the target first, and try again */
227 
228                                         if (dfd >= 0)
229                                                 (void) rm_rf_children(TAKE_FD(dfd), REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, NULL);
230 
231                                         r = unlinkat_maybe_dir(target_atfd, target_name);
232                                         if (r < 0)
233                                                 return log_debug_errno(r, "Failed to remove target directory: %m");
234 
235                                         if (renameat(source_atfd, source_name, target_atfd, target_name) < 0)
236                                                 return -errno;
237                                 } else {
238                                         /* The exchange worked, hence let's remove the source (i.e. the old target) */
239                                         if (dfd >= 0)
240                                                 (void) rm_rf_children(TAKE_FD(dfd), REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, NULL);
241 
242                                         r = unlinkat_maybe_dir(source_atfd, source_name);
243                                         if (r < 0)
244                                                 return log_debug_errno(r, "Failed to remove replaced target directory: %m");
245                                 }
246                         }
247                 } else {
248                         r = rename_noreplace(source_atfd, source_name, target_atfd, target_name);
249                         if (r < 0)
250                                 return r;
251                 }
252         }
253 
254         if (rofd >= 0) {
255                 r = fs_make_very_read_only(rofd);
256                 if (r < 0)
257                         return r;
258         }
259 
260         if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) {
261                 if (target_name)
262                         r = fsync_parent_at(target_atfd, target_name);
263                 else
264                         r = fsync_parent_at(source_atfd, source_name);
265                 if (r < 0)
266                         return r;
267         }
268 
269         return 0;
270 }
271