1 /* Bind mount detection.
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;
60 bool pruned_due_to_path;
64 /* Path to mountinfo */
65 static atomic<bool> mountinfo_updated{ false };
67 // Keyed by device major/minor.
68 using MountEntries = multimap<pair<int, int>, mount>;
70 /* Read a line from F.
71 Return a string, or empty string on error. */
72 static string read_mount_line(FILE *f)
79 if (fgets(buf, sizeof(buf), f) == nullptr) {
84 size_t chunk_length = strlen(buf);
85 if (chunk_length > 0 && buf[chunk_length - 1] == '\n') {
86 line.append(buf, chunk_length - 1);
89 line.append(buf, chunk_length);
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)
98 const char *src = *str;
99 while (*src == ' ' || *src == '\t') {
116 if (src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' && src[3] <= '7') {
119 v = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
120 if (v <= UCHAR_MAX) {
121 mount_string.push_back(v);
126 /* Else fall through */
129 mount_string.push_back(c);
136 if (dest != nullptr) {
137 *dest = move(mount_string);
142 /* Read a single entry from F. Return true if succesful. */
143 static bool read_mount_entry(FILE *f, mount *me)
145 string line = read_mount_line(f);
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) {
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) {
160 bool separator_found;
163 if (parse_mount_string(&option, &ptr) != 0) {
166 separator_found = strcmp(option.c_str(), "-") == 0;
167 } while (!separator_found);
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) {
177 static bool find_whether_under_pruned(
179 const unordered_map<int, mount *> &id_to_mount,
180 unordered_map<int, bool> *id_to_pruned_cache)
182 auto cache_it = id_to_pruned_cache->find(id);
183 if (cache_it != id_to_pruned_cache->end()) {
184 return cache_it->second;
186 auto mount_it = id_to_mount.find(id);
187 if (mount_it == id_to_mount.end()) {
188 // Should not happen.
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);
202 /* Read mount information from mountinfo_path, update mount_entries and
204 Return std::nullopt on error. */
205 static optional<MountEntries> read_mount_entries(void)
207 FILE *f = fopen(MOUNTINFO_PATH, "r");
212 MountEntries mount_entries;
216 while (read_mount_entry(f, &me)) {
217 string fs_type_upper = me.fs_type;
218 for (char &c : fs_type_upper) {
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) {
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);
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;
244 unordered_map<int, bool> id_to_pruned_cache;
245 for (auto &[key, me] : mount_entries) {
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());
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);
265 return mount_entries;
268 /* Bind mount path list maintenace and top-level interface. */
270 /* mountinfo_path file descriptor, or -1 */
271 static int mountinfo_fd;
273 /* Known bind mount paths */
274 static struct vector<string> bind_mount_paths; /* = { 0, }; */
276 /* Next bind_mount_paths entry */
277 static size_t bind_mount_paths_index; /* = 0; */
279 /* Rebuild bind_mount_paths */
280 static void rebuild_bind_mount_paths(void)
282 if (conf_debug_pruning) {
283 fprintf(stderr, "Rebuilding bind_mount_paths:\n");
285 optional<MountEntries> mount_entries = read_mount_entries();
286 if (!mount_entries.has_value()) {
289 if (conf_debug_pruning) {
290 fprintf(stderr, "Matching bind_mount_paths:\n");
293 bind_mount_paths.clear();
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.
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());
310 bind_mount_paths.push_back(me.mount_point);
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());
318 bind_mount_paths.push_back(me.mount_point);
323 if (conf_debug_pruning) {
324 fprintf(stderr, "...done\n");
326 string_list_dir_path_sort(&bind_mount_paths);
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)
333 if (mountinfo_updated.exchange(false)) { // Atomic test-and-clear.
334 rebuild_bind_mount_paths();
335 bind_mount_paths_index = 0;
337 return string_list_contains_dir_path(&bind_mount_paths,
338 &bind_mount_paths_index, path);
341 /* Initialize state for is_bind_mount(), to read data from MOUNTINFO. */
342 void bind_mount_init()
344 mountinfo_fd = open(MOUNTINFO_PATH, O_RDONLY);
345 if (mountinfo_fd == -1)
347 rebuild_bind_mount_paths();
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.
357 // The thread is forcibly terminated on exit(), so we just let it loop forever.
358 thread poll_thread([&] {
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) {
370 if ((pfd.revents & POLLPRI) != 0) {
371 mountinfo_updated = true;
375 poll_thread.detach();