]> git.sesse.net Git - plocate/blob - bind-mount.cpp
b3c1bd29194fe9f945499ce5db652f1cba9c5343
[plocate] / bind-mount.cpp
1 /* Bind mount detection.  Note: if you change this, change tmpwatch as well.
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 to_remove;
61 };
62
63 /* Path to mountinfo */
64 static atomic<bool> mountinfo_updated{ false };
65
66 // Keyed by device major/minor.
67 using MountEntries = multimap<pair<int, int>, mount>;
68
69 /* Read a line from F.
70    Return a string, or empty string on error. */
71 static string read_mount_line(FILE *f)
72 {
73         string line;
74
75         for (;;) {
76                 char buf[LINE_MAX];
77
78                 if (fgets(buf, sizeof(buf), f) == nullptr) {
79                         if (feof(f))
80                                 break;
81                         return "";
82                 }
83                 size_t chunk_length = strlen(buf);
84                 if (chunk_length > 0 && buf[chunk_length - 1] == '\n') {
85                         line.append(buf, chunk_length - 1);
86                         break;
87                 }
88                 line.append(buf, chunk_length);
89         }
90         return line;
91 }
92
93 /* Parse a space-delimited entry in STR, decode octal escapes, write it to
94    DEST if it is not nullptr.  Return 0 if OK, -1 on error. */
95 static int parse_mount_string(string *dest, const char **str)
96 {
97         const char *src = *str;
98         while (*src == ' ' || *src == '\t') {
99                 src++;
100         }
101         if (*src == 0) {
102                 return -1;
103         }
104         string mount_string;
105         for (;;) {
106                 char c = *src;
107
108                 switch (c) {
109                 case 0:
110                 case ' ':
111                 case '\t':
112                         goto done;
113
114                 case '\\':
115                         if (src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' && src[3] <= '7') {
116                                 unsigned v;
117
118                                 v = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
119                                 if (v <= UCHAR_MAX) {
120                                         mount_string.push_back(v);
121                                         src += 4;
122                                         break;
123                                 }
124                         }
125                         /* Else fall through */
126
127                 default:
128                         mount_string.push_back(c);
129                         src++;
130                 }
131         }
132
133 done:
134         *str = src;
135         if (dest != nullptr) {
136                 *dest = move(mount_string);
137         }
138         return 0;
139 }
140
141 /* Read a single entry from F. Return true if succesful. */
142 static bool read_mount_entry(FILE *f, mount *me)
143 {
144         string line = read_mount_line(f);
145         if (line.empty()) {
146                 return false;
147         }
148         size_t offset;
149         if (sscanf(line.c_str(), "%d %d %u:%u%zn", &me->id, &me->parent_id, &me->dev_major,
150                    &me->dev_minor, &offset) != 4) {
151                 return false;
152         }
153         const char *ptr = line.c_str() + offset;
154         if (parse_mount_string(&me->root, &ptr) != 0 ||
155             parse_mount_string(&me->mount_point, &ptr) != 0 ||
156             parse_mount_string(nullptr, &ptr) != 0) {
157                 return false;
158         }
159         bool separator_found;
160         do {
161                 string option;
162                 if (parse_mount_string(&option, &ptr) != 0) {
163                         return false;
164                 }
165                 separator_found = strcmp(option.c_str(), "-") == 0;
166         } while (!separator_found);
167
168         if (parse_mount_string(&me->fs_type, &ptr) != 0 ||
169             parse_mount_string(&me->source, &ptr) != 0 ||
170             parse_mount_string(nullptr, &ptr) != 0) {
171                 return false;
172         }
173         return true;
174 }
175
176 static bool find_whether_under_pruned(
177         int id,
178         const unordered_map<int, mount *> &id_to_mount,
179         unordered_map<int, bool> *id_to_pruned_cache)
180 {
181         auto cache_it = id_to_pruned_cache->find(id);
182         if (cache_it != id_to_pruned_cache->end()) {
183                 return cache_it->second;
184         }
185         auto mount_it = id_to_mount.find(id);
186         if (mount_it == id_to_mount.end()) {
187                 // Should not happen.
188                 return false;
189         }
190
191         bool result = mount_it->second->pruned_due_to_fs_type ||
192                 (mount_it->second->parent_id != 0 &&
193                  find_whether_under_pruned(mount_it->second->parent_id,
194                                            id_to_mount, id_to_pruned_cache));
195         id_to_pruned_cache->emplace(id, result);
196         return result;
197 }
198
199 /* Read mount information from mountinfo_path, update mount_entries and
200    num_mount_entries.
201    Return std::nullopt on error. */
202 static optional<MountEntries> read_mount_entries(void)
203 {
204         FILE *f = fopen(MOUNTINFO_PATH, "r");
205         if (f == nullptr) {
206                 return {};
207         }
208
209         MountEntries mount_entries;
210
211         {
212                 mount me;
213                 while (read_mount_entry(f, &me)) {
214                         string fs_type_upper = me.fs_type;
215                         for (char &c : fs_type_upper) {
216                                 c = toupper(c);
217                         }
218                         me.pruned_due_to_fs_type =
219                                 (find(conf_prunefs.begin(), conf_prunefs.end(), fs_type_upper) != conf_prunefs.end());
220                         mount_entries.emplace(make_pair(me.dev_major, me.dev_minor), me);
221                         if (conf_debug_pruning) {
222                                 fprintf(stderr,
223                                         " `%s' (%d on %d) is `%s' of `%s' (%u:%u), type `%s' (pruned_fs=%d)\n",
224                                         me.mount_point.c_str(), me.id, me.parent_id, me.root.c_str(), me.source.c_str(),
225                                         me.dev_major, me.dev_minor, me.fs_type.c_str(), me.pruned_due_to_fs_type);
226                         }
227                 }
228                 fclose(f);
229         }
230
231         // Now propagate pruned status recursively through parent links
232         // (e.g. if /run is tmpfs, then /run/foo should also be pruned).
233         unordered_map<int, mount *> id_to_mount;
234         for (auto &[key, me] : mount_entries) {
235                 id_to_mount[me.id] = &me;
236         }
237         unordered_map<int, bool> id_to_pruned_cache;
238         for (auto &[key, me] : mount_entries) {
239                 me.to_remove =
240                         find_whether_under_pruned(me.id, id_to_mount, &id_to_pruned_cache);
241                 if (conf_debug_pruning && me.to_remove) {
242                         fprintf(stderr, " `%s' is, or is under, a pruned file system; removing\n",
243                                 me.mount_point.c_str());
244                 }
245         }
246
247         // Now take out those that we won't see due to file system type anyway,
248         // so that we don't inadvertently prefer them to others during bind mount
249         // duplicate detection.
250         for (auto it = mount_entries.begin(); it != mount_entries.end(); ) {
251                 if (it->second.to_remove) {
252                         it = mount_entries.erase(it);
253                 } else {
254                         ++it;
255                 }
256         }
257
258         return mount_entries;
259 }
260
261 /* Bind mount path list maintenace and top-level interface. */
262
263 /* mountinfo_path file descriptor, or -1 */
264 static int mountinfo_fd;
265
266 /* Known bind mount paths */
267 static struct vector<string> bind_mount_paths; /* = { 0, }; */
268
269 /* Next bind_mount_paths entry */
270 static size_t bind_mount_paths_index; /* = 0; */
271
272 /* Rebuild bind_mount_paths */
273 static void rebuild_bind_mount_paths(void)
274 {
275         if (conf_debug_pruning) {
276                 fprintf(stderr, "Rebuilding bind_mount_paths:\n");
277         }
278         optional<MountEntries> mount_entries = read_mount_entries();
279         if (!mount_entries.has_value()) {
280                 return;
281         }
282         if (conf_debug_pruning) {
283                 fprintf(stderr, "Matching bind_mount_paths:\n");
284         }
285
286         bind_mount_paths.clear();
287
288         for (const auto &[dev_id, me] : *mount_entries) {
289                 const auto &[first, second] = mount_entries->equal_range(make_pair(me.dev_major, me.dev_minor));
290                 for (auto it = first; it != second; ++it) {
291                         const mount &other = it->second;
292                         if (other.id == me.id) {
293                                 // Don't compare an element to itself.
294                                 continue;
295                         }
296                         // We have two mounts from the same device. Is one a prefix of the other?
297                         // If there are two that are equal, prefer the one with lowest ID.
298                         if (me.root.size() > other.root.size() && me.root.find(other.root) == 0) {
299                                 if (conf_debug_pruning) {
300                                         fprintf(stderr, " => adding `%s' (root `%s' is a child of `%s', mounted on `%s')\n",
301                                                 me.mount_point.c_str(), me.root.c_str(), other.root.c_str(), other.mount_point.c_str());
302                                 }
303                                 bind_mount_paths.push_back(me.mount_point);
304                                 break;
305                         }
306                         if (me.root == other.root && me.id > other.id) {
307                                 if (conf_debug_pruning) {
308                                         fprintf(stderr, " => adding `%s' (duplicate of mount point `%s')\n",
309                                                 me.mount_point.c_str(), other.mount_point.c_str());
310                                 }
311                                 bind_mount_paths.push_back(me.mount_point);
312                                 break;
313                         }
314                 }
315         }
316         if (conf_debug_pruning) {
317                 fprintf(stderr, "...done\n");
318         }
319         string_list_dir_path_sort(&bind_mount_paths);
320 }
321
322 /* Return true if PATH is a destination of a bind mount.
323    (Bind mounts "to self" are ignored.) */
324 bool is_bind_mount(const char *path)
325 {
326         if (mountinfo_updated.exchange(false)) {  // Atomic test-and-clear.
327                 rebuild_bind_mount_paths();
328                 bind_mount_paths_index = 0;
329         }
330         return string_list_contains_dir_path(&bind_mount_paths,
331                                              &bind_mount_paths_index, path);
332 }
333
334 /* Initialize state for is_bind_mount(), to read data from MOUNTINFO. */
335 void bind_mount_init()
336 {
337         mountinfo_fd = open(MOUNTINFO_PATH, O_RDONLY);
338         if (mountinfo_fd == -1)
339                 return;
340         rebuild_bind_mount_paths();
341
342         // mlocate re-polls this for each and every directory it wants to check,
343         // for unclear reasons; it's possible that it's worried about a new recursive
344         // bind mount being made while updatedb is running, causing an infinite loop?
345         // Since it's probably for some good reason, we do the same, but we don't
346         // want the barrage of syscalls. It's not synchronous, but the poll signal
347         // isn't either; there's a slight race condition, but one that could only
348         // be exploited by root.
349         //
350         // The thread is forcibly terminated on exit(), so we just let it loop forever.
351         thread poll_thread([&] {
352                 for (;;) {
353                         struct pollfd pfd;
354                         /* Unfortunately (mount --bind $path $path/subdir) would leave st_dev
355                            unchanged between $path and $path/subdir, so we must keep reparsing
356                            mountinfo_path each time it changes. */
357                         pfd.fd = mountinfo_fd;
358                         pfd.events = POLLPRI;
359                         if (poll(&pfd, 1, /*timeout=*/-1) == -1) {
360                                 perror("poll()");
361                                 exit(1);
362                         }
363                         if ((pfd.revents & POLLPRI) != 0) {
364                                 mountinfo_updated = true;
365                         }
366                 }
367         });
368         poll_thread.detach();
369 }