]> git.sesse.net Git - plocate/blob - bind-mount.cpp
tmpwatch is not kept in sync with plocate.
[plocate] / bind-mount.cpp
1 /* Bind mount detection.
2
3 Copyright (C) 2005, 2007, 2008, 2012 Red Hat, Inc. All rights reserved.
4 This copyrighted material is made available to anyone wishing to use, modify,
5 copy, or redistribute it subject to the terms and conditions of the GNU General
6 Public License v.2.
7
8 This program is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
12 You should have received a copy of the GNU General Public License along with
13 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
14 Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
16 Author: Miloslav Trmac <mitr@redhat.com>
17
18 plocate modifications: Copyright (C) 2020 Steinar H. Gunderson.
19 plocate parts and modifications are licensed under the GPLv2 or, at your option,
20 any later version.
21 */
22
23 #include "bind-mount.h"
24
25 #include "conf.h"
26 #include "lib.h"
27
28 #include <algorithm>
29 #include <atomic>
30 #include <fcntl.h>
31 #include <limits.h>
32 #include <map>
33 #include <poll.h>
34 #include <stddef.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <string>
39 #include <sys/stat.h>
40 #include <sys/time.h>
41 #include <thread>
42 #include <optional>
43 #include <unordered_map>
44
45 using namespace std;
46
47 /* mountinfo handling */
48
49 /* A single mountinfo entry */
50 struct mount {
51         int id, parent_id;
52         unsigned dev_major, dev_minor;
53         string root;
54         string mount_point;
55         string fs_type;
56         string source;
57
58         // Derived properties.
59         bool pruned_due_to_fs_type;
60         bool pruned_due_to_path;
61         bool to_remove;
62 };
63
64 /* Path to mountinfo */
65 static atomic<bool> mountinfo_updated{ false };
66
67 // Keyed by device major/minor.
68 using MountEntries = multimap<pair<int, int>, mount>;
69
70 /* Read a line from F.
71    Return a string, or empty string on error. */
72 static string read_mount_line(FILE *f)
73 {
74         string line;
75
76         for (;;) {
77                 char buf[LINE_MAX];
78
79                 if (fgets(buf, sizeof(buf), f) == nullptr) {
80                         if (feof(f))
81                                 break;
82                         return "";
83                 }
84                 size_t chunk_length = strlen(buf);
85                 if (chunk_length > 0 && buf[chunk_length - 1] == '\n') {
86                         line.append(buf, chunk_length - 1);
87                         break;
88                 }
89                 line.append(buf, chunk_length);
90         }
91         return line;
92 }
93
94 /* Parse a space-delimited entry in STR, decode octal escapes, write it to
95    DEST if it is not nullptr.  Return 0 if OK, -1 on error. */
96 static int parse_mount_string(string *dest, const char **str)
97 {
98         const char *src = *str;
99         while (*src == ' ' || *src == '\t') {
100                 src++;
101         }
102         if (*src == 0) {
103                 return -1;
104         }
105         string mount_string;
106         for (;;) {
107                 char c = *src;
108
109                 switch (c) {
110                 case 0:
111                 case ' ':
112                 case '\t':
113                         goto done;
114
115                 case '\\':
116                         if (src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' && src[3] <= '7') {
117                                 unsigned v;
118
119                                 v = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
120                                 if (v <= UCHAR_MAX) {
121                                         mount_string.push_back(v);
122                                         src += 4;
123                                         break;
124                                 }
125                         }
126                         /* Else fall through */
127
128                 default:
129                         mount_string.push_back(c);
130                         src++;
131                 }
132         }
133
134 done:
135         *str = src;
136         if (dest != nullptr) {
137                 *dest = move(mount_string);
138         }
139         return 0;
140 }
141
142 /* Read a single entry from F. Return true if succesful. */
143 static bool read_mount_entry(FILE *f, mount *me)
144 {
145         string line = read_mount_line(f);
146         if (line.empty()) {
147                 return false;
148         }
149         size_t offset;
150         if (sscanf(line.c_str(), "%d %d %u:%u%zn", &me->id, &me->parent_id, &me->dev_major,
151                    &me->dev_minor, &offset) != 4) {
152                 return false;
153         }
154         const char *ptr = line.c_str() + offset;
155         if (parse_mount_string(&me->root, &ptr) != 0 ||
156             parse_mount_string(&me->mount_point, &ptr) != 0 ||
157             parse_mount_string(nullptr, &ptr) != 0) {
158                 return false;
159         }
160         bool separator_found;
161         do {
162                 string option;
163                 if (parse_mount_string(&option, &ptr) != 0) {
164                         return false;
165                 }
166                 separator_found = strcmp(option.c_str(), "-") == 0;
167         } while (!separator_found);
168
169         if (parse_mount_string(&me->fs_type, &ptr) != 0 ||
170             parse_mount_string(&me->source, &ptr) != 0 ||
171             parse_mount_string(nullptr, &ptr) != 0) {
172                 return false;
173         }
174         return true;
175 }
176
177 static bool find_whether_under_pruned(
178         int id,
179         const unordered_map<int, mount *> &id_to_mount,
180         unordered_map<int, bool> *id_to_pruned_cache)
181 {
182         auto cache_it = id_to_pruned_cache->find(id);
183         if (cache_it != id_to_pruned_cache->end()) {
184                 return cache_it->second;
185         }
186         auto mount_it = id_to_mount.find(id);
187         if (mount_it == id_to_mount.end()) {
188                 // Should not happen.
189                 return false;
190         }
191
192         bool result =
193                 mount_it->second->pruned_due_to_fs_type ||
194                 mount_it->second->pruned_due_to_path ||
195                 (mount_it->second->parent_id != 0 &&
196                  find_whether_under_pruned(mount_it->second->parent_id,
197                                            id_to_mount, id_to_pruned_cache));
198         id_to_pruned_cache->emplace(id, result);
199         return result;
200 }
201
202 /* Read mount information from mountinfo_path, update mount_entries and
203    num_mount_entries.
204    Return std::nullopt on error. */
205 static optional<MountEntries> read_mount_entries(void)
206 {
207         FILE *f = fopen(MOUNTINFO_PATH, "r");
208         if (f == nullptr) {
209                 return {};
210         }
211
212         MountEntries mount_entries;
213
214         {
215                 mount me;
216                 while (read_mount_entry(f, &me)) {
217                         string fs_type_upper = me.fs_type;
218                         for (char &c : fs_type_upper) {
219                                 c = toupper(c);
220                         }
221                         me.pruned_due_to_fs_type =
222                                 (find(conf_prunefs.begin(), conf_prunefs.end(), fs_type_upper) != conf_prunefs.end());
223                         size_t prunepath_index = 0;  // Search the entire list every time.
224                         me.pruned_due_to_path =
225                                 string_list_contains_dir_path(&conf_prunepaths, &prunepath_index, me.mount_point.c_str());
226                         mount_entries.emplace(make_pair(me.dev_major, me.dev_minor), me);
227                         if (conf_debug_pruning) {
228                                 fprintf(stderr,
229                                         " `%s' (%d on %d) is `%s' of `%s' (%u:%u), type `%s' (pruned_fs=%d, pruned_path=%d)\n",
230                                         me.mount_point.c_str(), me.id, me.parent_id, me.root.c_str(), me.source.c_str(),
231                                         me.dev_major, me.dev_minor, me.fs_type.c_str(), me.pruned_due_to_fs_type,
232                                         me.pruned_due_to_path);
233                         }
234                 }
235                 fclose(f);
236         }
237
238         // Now propagate pruned status recursively through parent links
239         // (e.g. if /run is tmpfs, then /run/foo should also be pruned).
240         unordered_map<int, mount *> id_to_mount;
241         for (auto &[key, me] : mount_entries) {
242                 id_to_mount[me.id] = &me;
243         }
244         unordered_map<int, bool> id_to_pruned_cache;
245         for (auto &[key, me] : mount_entries) {
246                 me.to_remove =
247                         find_whether_under_pruned(me.id, id_to_mount, &id_to_pruned_cache);
248                 if (conf_debug_pruning && me.to_remove) {
249                         fprintf(stderr, " `%s' is, or is under, a pruned file system; removing\n",
250                                 me.mount_point.c_str());
251                 }
252         }
253
254         // Now take out those that we won't see due to file system type anyway,
255         // so that we don't inadvertently prefer them to others during bind mount
256         // duplicate detection.
257         for (auto it = mount_entries.begin(); it != mount_entries.end(); ) {
258                 if (it->second.to_remove) {
259                         it = mount_entries.erase(it);
260                 } else {
261                         ++it;
262                 }
263         }
264
265         return mount_entries;
266 }
267
268 /* Bind mount path list maintenace and top-level interface. */
269
270 /* mountinfo_path file descriptor, or -1 */
271 static int mountinfo_fd;
272
273 /* Known bind mount paths */
274 static struct vector<string> bind_mount_paths; /* = { 0, }; */
275
276 /* Next bind_mount_paths entry */
277 static size_t bind_mount_paths_index; /* = 0; */
278
279 /* Rebuild bind_mount_paths */
280 static void rebuild_bind_mount_paths(void)
281 {
282         if (conf_debug_pruning) {
283                 fprintf(stderr, "Rebuilding bind_mount_paths:\n");
284         }
285         optional<MountEntries> mount_entries = read_mount_entries();
286         if (!mount_entries.has_value()) {
287                 return;
288         }
289         if (conf_debug_pruning) {
290                 fprintf(stderr, "Matching bind_mount_paths:\n");
291         }
292
293         bind_mount_paths.clear();
294
295         for (const auto &[dev_id, me] : *mount_entries) {
296                 const auto &[first, second] = mount_entries->equal_range(make_pair(me.dev_major, me.dev_minor));
297                 for (auto it = first; it != second; ++it) {
298                         const mount &other = it->second;
299                         if (other.id == me.id) {
300                                 // Don't compare an element to itself.
301                                 continue;
302                         }
303                         // We have two mounts from the same device. Is one a prefix of the other?
304                         // If there are two that are equal, prefer the one with lowest ID.
305                         if (me.root.size() > other.root.size() && me.root.find(other.root) == 0) {
306                                 if (conf_debug_pruning) {
307                                         fprintf(stderr, " => adding `%s' (root `%s' is a child of `%s', mounted on `%s')\n",
308                                                 me.mount_point.c_str(), me.root.c_str(), other.root.c_str(), other.mount_point.c_str());
309                                 }
310                                 bind_mount_paths.push_back(me.mount_point);
311                                 break;
312                         }
313                         if (me.root == other.root && me.id > other.id) {
314                                 if (conf_debug_pruning) {
315                                         fprintf(stderr, " => adding `%s' (duplicate of mount point `%s')\n",
316                                                 me.mount_point.c_str(), other.mount_point.c_str());
317                                 }
318                                 bind_mount_paths.push_back(me.mount_point);
319                                 break;
320                         }
321                 }
322         }
323         if (conf_debug_pruning) {
324                 fprintf(stderr, "...done\n");
325         }
326         string_list_dir_path_sort(&bind_mount_paths);
327 }
328
329 /* Return true if PATH is a destination of a bind mount.
330    (Bind mounts "to self" are ignored.) */
331 bool is_bind_mount(const char *path)
332 {
333         if (mountinfo_updated.exchange(false)) {  // Atomic test-and-clear.
334                 rebuild_bind_mount_paths();
335                 bind_mount_paths_index = 0;
336         }
337         return string_list_contains_dir_path(&bind_mount_paths,
338                                              &bind_mount_paths_index, path);
339 }
340
341 /* Initialize state for is_bind_mount(), to read data from MOUNTINFO. */
342 void bind_mount_init()
343 {
344         mountinfo_fd = open(MOUNTINFO_PATH, O_RDONLY);
345         if (mountinfo_fd == -1)
346                 return;
347         rebuild_bind_mount_paths();
348
349         // mlocate re-polls this for each and every directory it wants to check,
350         // for unclear reasons; it's possible that it's worried about a new recursive
351         // bind mount being made while updatedb is running, causing an infinite loop?
352         // Since it's probably for some good reason, we do the same, but we don't
353         // want the barrage of syscalls. It's not synchronous, but the poll signal
354         // isn't either; there's a slight race condition, but one that could only
355         // be exploited by root.
356         //
357         // The thread is forcibly terminated on exit(), so we just let it loop forever.
358         thread poll_thread([&] {
359                 for (;;) {
360                         struct pollfd pfd;
361                         /* Unfortunately (mount --bind $path $path/subdir) would leave st_dev
362                            unchanged between $path and $path/subdir, so we must keep reparsing
363                            mountinfo_path each time it changes. */
364                         pfd.fd = mountinfo_fd;
365                         pfd.events = POLLPRI;
366                         if (poll(&pfd, 1, /*timeout=*/-1) == -1) {
367                                 perror("poll()");
368                                 exit(1);
369                         }
370                         if ((pfd.revents & POLLPRI) != 0) {
371                                 mountinfo_updated = true;
372                         }
373                 }
374         });
375         poll_thread.detach();
376 }