]> git.sesse.net Git - plocate/blob - bind-mount.cpp
Remove some globals from the bind mount code.
[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 <atomic>
29 #include <fcntl.h>
30 #include <limits.h>
31 #include <map>
32 #include <poll.h>
33 #include <stddef.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <string>
38 #include <sys/stat.h>
39 #include <sys/time.h>
40 #include <thread>
41 #include <optional>
42
43 using namespace std;
44
45 /* mountinfo handling */
46
47 /* A single mountinfo entry */
48 struct mount {
49         int id, parent_id;
50         unsigned dev_major, dev_minor;
51         string root;
52         string mount_point;
53         string fs_type;
54         string source;
55 };
56
57 /* Path to mountinfo */
58 static atomic<bool> mountinfo_updated{ false };
59
60 // Keyed by device major/minor.
61 using MountEntries = multimap<pair<int, int>, mount>;
62
63 /* Read a line from F.
64    Return a string, or empty string on error. */
65 static string read_mount_line(FILE *f)
66 {
67         string line;
68
69         for (;;) {
70                 char buf[LINE_MAX];
71
72                 if (fgets(buf, sizeof(buf), f) == nullptr) {
73                         if (feof(f))
74                                 break;
75                         return "";
76                 }
77                 size_t chunk_length = strlen(buf);
78                 if (chunk_length > 0 && buf[chunk_length - 1] == '\n') {
79                         line.append(buf, chunk_length - 1);
80                         break;
81                 }
82                 line.append(buf, chunk_length);
83         }
84         return line;
85 }
86
87 /* Parse a space-delimited entry in STR, decode octal escapes, write it to
88    DEST if it is not nullptr.  Return 0 if OK, -1 on error. */
89 static int parse_mount_string(string *dest, const char **str)
90 {
91         const char *src = *str;
92         while (*src == ' ' || *src == '\t') {
93                 src++;
94         }
95         if (*src == 0) {
96                 return -1;
97         }
98         string mount_string;
99         for (;;) {
100                 char c = *src;
101
102                 switch (c) {
103                 case 0:
104                 case ' ':
105                 case '\t':
106                         goto done;
107
108                 case '\\':
109                         if (src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' && src[3] <= '7') {
110                                 unsigned v;
111
112                                 v = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
113                                 if (v <= UCHAR_MAX) {
114                                         mount_string.push_back(v);
115                                         src += 4;
116                                         break;
117                                 }
118                         }
119                         /* Else fall through */
120
121                 default:
122                         mount_string.push_back(c);
123                         src++;
124                 }
125         }
126
127 done:
128         *str = src;
129         if (dest != nullptr) {
130                 *dest = move(mount_string);
131         }
132         return 0;
133 }
134
135 /* Read a single entry from F. Return true if succesful. */
136 static bool read_mount_entry(FILE *f, mount *me)
137 {
138         string line = read_mount_line(f);
139         if (line.empty()) {
140                 return false;
141         }
142         size_t offset;
143         if (sscanf(line.c_str(), "%d %d %u:%u%zn", &me->id, &me->parent_id, &me->dev_major,
144                    &me->dev_minor, &offset) != 4) {
145                 return false;
146         }
147         const char *ptr = line.c_str() + offset;
148         if (parse_mount_string(&me->root, &ptr) != 0 ||
149             parse_mount_string(&me->mount_point, &ptr) != 0 ||
150             parse_mount_string(nullptr, &ptr) != 0) {
151                 return false;
152         }
153         bool separator_found;
154         do {
155                 string option;
156                 if (parse_mount_string(&option, &ptr) != 0) {
157                         return false;
158                 }
159                 separator_found = strcmp(option.c_str(), "-") == 0;
160         } while (!separator_found);
161
162         if (parse_mount_string(&me->fs_type, &ptr) != 0 ||
163             parse_mount_string(&me->source, &ptr) != 0 ||
164             parse_mount_string(nullptr, &ptr) != 0) {
165                 return false;
166         }
167         return true;
168 }
169
170 /* Read mount information from mountinfo_path, update mount_entries and
171    num_mount_entries.
172    Return std::nullopt on error. */
173 static optional<MountEntries> read_mount_entries(void)
174 {
175         FILE *f = fopen(MOUNTINFO_PATH, "r");
176         if (f == nullptr) {
177                 return {};
178         }
179
180         MountEntries mount_entries;
181
182         mount me;
183         while (read_mount_entry(f, &me)) {
184                 if (conf_debug_pruning) {
185                         fprintf(stderr,
186                                 " `%s' (%d on %d) is `%s' of `%s' (%u:%u), type `%s'\n",
187                                 me.mount_point.c_str(), me.id, me.parent_id, me.root.c_str(), me.source.c_str(),
188                                 me.dev_major, me.dev_minor, me.fs_type.c_str());
189                 }
190                 mount_entries.emplace(make_pair(me.dev_major, me.dev_minor), me);
191         }
192         fclose(f);
193         return mount_entries;
194 }
195
196 /* Bind mount path list maintenace and top-level interface. */
197
198 /* mountinfo_path file descriptor, or -1 */
199 static int mountinfo_fd;
200
201 /* Known bind mount paths */
202 static struct vector<string> bind_mount_paths; /* = { 0, }; */
203
204 /* Next bind_mount_paths entry */
205 static size_t bind_mount_paths_index; /* = 0; */
206
207 /* Rebuild bind_mount_paths */
208 static void rebuild_bind_mount_paths(void)
209 {
210         if (conf_debug_pruning) {
211                 fprintf(stderr, "Rebuilding bind_mount_paths:\n");
212         }
213         optional<MountEntries> mount_entries = read_mount_entries();
214         if (!mount_entries.has_value()) {
215                 return;
216         }
217         if (conf_debug_pruning) {
218                 fprintf(stderr, "Matching bind_mount_paths:\n");
219         }
220
221         bind_mount_paths.clear();
222
223         for (const auto &[dev_id, me] : *mount_entries) {
224                 const auto &[first, second] = mount_entries->equal_range(make_pair(me.dev_major, me.dev_minor));
225                 for (auto it = first; it != second; ++it) {
226                         const mount &other = it->second;
227                         if (other.id == me.id) {
228                                 // Don't compare an element to itself.
229                                 continue;
230                         }
231                         // We have two mounts from the same device. Is one a prefix of the other?
232                         // If there are two that are equal, prefer the one with lowest ID.
233                         if (me.root.size() > other.root.size() && me.root.find(other.root) == 0) {
234                                 if (conf_debug_pruning) {
235                                         fprintf(stderr, " => adding `%s' (root `%s' is a child of `%s', mounted on `%s')\n",
236                                                 me.mount_point.c_str(), me.root.c_str(), other.root.c_str(), other.mount_point.c_str());
237                                 }
238                                 bind_mount_paths.push_back(me.mount_point);
239                                 break;
240                         }
241                         if (me.root == other.root && me.id > other.id) {
242                                 if (conf_debug_pruning) {
243                                         fprintf(stderr, " => adding `%s' (duplicate of mount point `%s')\n",
244                                                 me.mount_point.c_str(), other.mount_point.c_str());
245                                 }
246                                 bind_mount_paths.push_back(me.mount_point);
247                                 break;
248                         }
249                 }
250         }
251         if (conf_debug_pruning) {
252                 fprintf(stderr, "...done\n");
253         }
254         string_list_dir_path_sort(&bind_mount_paths);
255 }
256
257 /* Return true if PATH is a destination of a bind mount.
258    (Bind mounts "to self" are ignored.) */
259 bool is_bind_mount(const char *path)
260 {
261         if (mountinfo_updated.exchange(false)) {  // Atomic test-and-clear.
262                 rebuild_bind_mount_paths();
263                 bind_mount_paths_index = 0;
264         }
265         return string_list_contains_dir_path(&bind_mount_paths,
266                                              &bind_mount_paths_index, path);
267 }
268
269 /* Initialize state for is_bind_mount(), to read data from MOUNTINFO. */
270 void bind_mount_init()
271 {
272         mountinfo_fd = open(MOUNTINFO_PATH, O_RDONLY);
273         if (mountinfo_fd == -1)
274                 return;
275         rebuild_bind_mount_paths();
276
277         // mlocate re-polls this for each and every directory it wants to check,
278         // for unclear reasons; it's possible that it's worried about a new recursive
279         // bind mount being made while updatedb is running, causing an infinite loop?
280         // Since it's probably for some good reason, we do the same, but we don't
281         // want the barrage of syscalls. It's not synchronous, but the poll signal
282         // isn't either; there's a slight race condition, but one that could only
283         // be exploited by root.
284         //
285         // The thread is forcibly terminated on exit(), so we just let it loop forever.
286         thread poll_thread([&] {
287                 for (;;) {
288                         struct pollfd pfd;
289                         /* Unfortunately (mount --bind $path $path/subdir) would leave st_dev
290                            unchanged between $path and $path/subdir, so we must keep reparsing
291                            mountinfo_path each time it changes. */
292                         pfd.fd = mountinfo_fd;
293                         pfd.events = POLLPRI;
294                         if (poll(&pfd, 1, /*timeout=*/-1) == -1) {
295                                 perror("poll()");
296                                 exit(1);
297                         }
298                         if ((pfd.revents & POLLPRI) != 0) {
299                                 mountinfo_updated = true;
300                         }
301                 }
302         });
303         poll_thread.detach();
304 }