1 /* Bind mount detection. Note: if you change this, change tmpwatch as well.
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
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.
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.
16 Author: Miloslav Trmac <mitr@redhat.com>
18 plocate modifications: Copyright (C) 2020 Steinar H. Gunderson.
19 plocate parts and modifications are licensed under the GPLv2 or, at your option,
23 #include "bind-mount.h"
43 #include <unordered_map>
47 /* mountinfo handling */
49 /* A single mountinfo entry */
52 unsigned dev_major, dev_minor;
58 // Derived properties.
59 bool pruned_due_to_fs_type;
63 /* Path to mountinfo */
64 static atomic<bool> mountinfo_updated{ false };
66 // Keyed by device major/minor.
67 using MountEntries = multimap<pair<int, int>, mount>;
69 /* Read a line from F.
70 Return a string, or empty string on error. */
71 static string read_mount_line(FILE *f)
78 if (fgets(buf, sizeof(buf), f) == nullptr) {
83 size_t chunk_length = strlen(buf);
84 if (chunk_length > 0 && buf[chunk_length - 1] == '\n') {
85 line.append(buf, chunk_length - 1);
88 line.append(buf, chunk_length);
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)
97 const char *src = *str;
98 while (*src == ' ' || *src == '\t') {
115 if (src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' && src[3] <= '7') {
118 v = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
119 if (v <= UCHAR_MAX) {
120 mount_string.push_back(v);
125 /* Else fall through */
128 mount_string.push_back(c);
135 if (dest != nullptr) {
136 *dest = move(mount_string);
141 /* Read a single entry from F. Return true if succesful. */
142 static bool read_mount_entry(FILE *f, mount *me)
144 string line = read_mount_line(f);
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) {
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) {
159 bool separator_found;
162 if (parse_mount_string(&option, &ptr) != 0) {
165 separator_found = strcmp(option.c_str(), "-") == 0;
166 } while (!separator_found);
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) {
176 static bool find_whether_under_pruned(
178 const unordered_map<int, mount *> &id_to_mount,
179 unordered_map<int, bool> *id_to_pruned_cache)
181 auto cache_it = id_to_pruned_cache->find(id);
182 if (cache_it != id_to_pruned_cache->end()) {
183 return cache_it->second;
185 auto mount_it = id_to_mount.find(id);
186 if (mount_it == id_to_mount.end()) {
187 // Should not happen.
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);
199 /* Read mount information from mountinfo_path, update mount_entries and
201 Return std::nullopt on error. */
202 static optional<MountEntries> read_mount_entries(void)
204 FILE *f = fopen(MOUNTINFO_PATH, "r");
209 MountEntries mount_entries;
213 while (read_mount_entry(f, &me)) {
214 string fs_type_upper = me.fs_type;
215 for (char &c : fs_type_upper) {
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) {
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);
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;
237 unordered_map<int, bool> id_to_pruned_cache;
238 for (auto &[key, me] : mount_entries) {
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());
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);
258 return mount_entries;
261 /* Bind mount path list maintenace and top-level interface. */
263 /* mountinfo_path file descriptor, or -1 */
264 static int mountinfo_fd;
266 /* Known bind mount paths */
267 static struct vector<string> bind_mount_paths; /* = { 0, }; */
269 /* Next bind_mount_paths entry */
270 static size_t bind_mount_paths_index; /* = 0; */
272 /* Rebuild bind_mount_paths */
273 static void rebuild_bind_mount_paths(void)
275 if (conf_debug_pruning) {
276 fprintf(stderr, "Rebuilding bind_mount_paths:\n");
278 optional<MountEntries> mount_entries = read_mount_entries();
279 if (!mount_entries.has_value()) {
282 if (conf_debug_pruning) {
283 fprintf(stderr, "Matching bind_mount_paths:\n");
286 bind_mount_paths.clear();
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.
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());
303 bind_mount_paths.push_back(me.mount_point);
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());
311 bind_mount_paths.push_back(me.mount_point);
316 if (conf_debug_pruning) {
317 fprintf(stderr, "...done\n");
319 string_list_dir_path_sort(&bind_mount_paths);
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)
326 if (mountinfo_updated.exchange(false)) { // Atomic test-and-clear.
327 rebuild_bind_mount_paths();
328 bind_mount_paths_index = 0;
330 return string_list_contains_dir_path(&bind_mount_paths,
331 &bind_mount_paths_index, path);
334 /* Initialize state for is_bind_mount(), to read data from MOUNTINFO. */
335 void bind_mount_init()
337 mountinfo_fd = open(MOUNTINFO_PATH, O_RDONLY);
338 if (mountinfo_fd == -1)
340 rebuild_bind_mount_paths();
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.
350 // The thread is forcibly terminated on exit(), so we just let it loop forever.
351 thread poll_thread([&] {
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) {
363 if ((pfd.revents & POLLPRI) != 0) {
364 mountinfo_updated = true;
368 poll_thread.detach();