]>
Commit | Line | Data |
---|---|---|
235be6bc LP |
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" | |
bf819d3a | 13 | #include "sync-util.h" |
235be6bc | 14 | |
f3c5ea71 | 15 | static int fs_make_very_read_only(int fd) { |
235be6bc LP |
16 | struct stat st; |
17 | int r; | |
18 | ||
19 | assert(fd >= 0); | |
20 | ||
7227dd81 | 21 | /* Tries to make the specified fd "comprehensively" read-only. Primary use case for this is OS images, |
235be6bc LP |
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 | ||
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 | ||
97 | int install_file(int source_atfd, const char *source_name, | |
98 | int target_atfd, const char *target_name, | |
99 | InstallFileFlags flags) { | |
100 | ||
254d1313 | 101 | _cleanup_close_ int rofd = -EBADF; |
235be6bc LP |
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) { | |
254d1313 | 121 | _cleanup_close_ int pfd = -EBADF; |
235be6bc LP |
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: { | |
254d1313 | 136 | _cleanup_close_ int regfd = -EBADF; |
235be6bc LP |
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: { | |
254d1313 | 161 | _cleanup_close_ int dfd = -EBADF; |
235be6bc LP |
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) { | |
254d1313 | 206 | _cleanup_close_ int dfd = -EBADF; |
235be6bc LP |
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 | } |