| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "plugin_proc.h" |
| 4 | |
| 5 | // ---------------------------------------------------------------------------- |
| 6 | // taken from gnulib/mountlist.c |
| 7 | |
| 8 | #ifndef ME_REMOTE |
| 9 | /* A file system is "remote" if its Fs_name contains a ':' |
| 10 | or if (it is of type (smbfs or cifs) and its Fs_name starts with '//') |
| 11 | or Fs_name is equal to "-hosts" (used by autofs to mount remote fs). */ |
| 12 | # define ME_REMOTE(Fs_name, Fs_type) \ |
| 13 | (strchr (Fs_name, ':') != NULL \ |
| 14 | || ((Fs_name)[0] == '/' \ |
| 15 | && (Fs_name)[1] == '/' \ |
| 16 | && (strcmp (Fs_type, "smbfs") == 0 \ |
| 17 | || strcmp (Fs_type, "cifs") == 0)) \ |
| 18 | || (strcmp("-hosts", Fs_name) == 0)) |
| 19 | #endif |
| 20 | |
| 21 | #define ME_DUMMY_0(Fs_name, Fs_type) \ |
| 22 | (strcmp (Fs_type, "autofs") == 0 \ |
| 23 | || strcmp (Fs_type, "proc") == 0 \ |
| 24 | || strcmp (Fs_type, "subfs") == 0 \ |
| 25 | /* for Linux 2.6/3.x */ \ |
| 26 | || strcmp (Fs_type, "debugfs") == 0 \ |
| 27 | || strcmp (Fs_type, "devpts") == 0 \ |
| 28 | || strcmp (Fs_type, "fusectl") == 0 \ |
| 29 | || strcmp (Fs_type, "mqueue") == 0 \ |
| 30 | || strcmp (Fs_type, "rpc_pipefs") == 0 \ |
| 31 | || strcmp (Fs_type, "sysfs") == 0 \ |
| 32 | /* FreeBSD, Linux 2.4 */ \ |
| 33 | || strcmp (Fs_type, "devfs") == 0 \ |
| 34 | /* for NetBSD 3.0 */ \ |
| 35 | || strcmp (Fs_type, "kernfs") == 0 \ |
| 36 | /* for Irix 6.5 */ \ |
| 37 | || strcmp (Fs_type, "ignore") == 0) |
| 38 | |
| 39 | /* Historically, we have marked as "dummy" any file system of type "none", |
| 40 | but now that programs like du need to know about bind-mounted directories, |
| 41 | we grant an exception to any with "bind" in its list of mount options. |
| 42 | I.e., those are *not* dummy entries. */ |
| 43 | # define ME_DUMMY(Fs_name, Fs_type) \ |
| 44 | (ME_DUMMY_0 (Fs_name, Fs_type) || strcmp (Fs_type, "none") == 0) |
| 45 | |
| 46 | // ---------------------------------------------------------------------------- |
| 47 | |
| 48 | // find the mount info with the given major:minor |
| 49 | // in the supplied linked list of mountinfo structures |
| 50 | struct mountinfo *mountinfo_find(struct mountinfo *root, unsigned long major, unsigned long minor, char *device) { |
| 51 | struct mountinfo *mi; |
| 52 | |
| 53 | uint32_t hash = simple_hash(device); |
| 54 | |
| 55 | for(mi = root; mi ; mi = mi->next) |
| 56 | if (unlikely( |
| 57 | mi->major == major && |
| 58 | mi->minor == minor && |
| 59 | mi->mount_source_name_hash == hash && |
| 60 | !strcmp(mi->mount_source_name, device))) |
| 61 | return mi; |
| 62 | |
| 63 | return NULL; |
| 64 | } |
| 65 | |
| 66 | // find the mount info with the given filesystem and mount_source |
| 67 | // in the supplied linked list of mountinfo structures |
| 68 | struct mountinfo *mountinfo_find_by_filesystem_mount_source(struct mountinfo *root, const char *filesystem, const char *mount_source) { |
| 69 | struct mountinfo *mi; |
| 70 | uint32_t filesystem_hash = simple_hash(filesystem), mount_source_hash = simple_hash(mount_source); |
| 71 | |
| 72 | for(mi = root; mi ; mi = mi->next) |
| 73 | if(unlikely(mi->filesystem |
| 74 | && mi->mount_source |
| 75 | && mi->filesystem_hash == filesystem_hash |
| 76 | && mi->mount_source_hash == mount_source_hash |
| 77 | && !strcmp(mi->filesystem, filesystem) |
| 78 | && !strcmp(mi->mount_source, mount_source))) |
| 79 | return mi; |
| 80 | |
| 81 | return NULL; |
| 82 | } |
| 83 | |
| 84 | struct mountinfo *mountinfo_find_by_filesystem_super_option(struct mountinfo *root, const char *filesystem, const char *super_options) { |
| 85 | struct mountinfo *mi; |
| 86 | uint32_t filesystem_hash = simple_hash(filesystem); |
| 87 | |
| 88 | size_t solen = strlen(super_options); |
| 89 | |
| 90 | for(mi = root; mi ; mi = mi->next) |
| 91 | if(unlikely(mi->filesystem |
| 92 | && mi->super_options |
| 93 | && mi->filesystem_hash == filesystem_hash |
| 94 | && !strcmp(mi->filesystem, filesystem))) { |
| 95 | |
| 96 | // super_options is a comma separated list |
| 97 | char *s = mi->super_options, *e; |
| 98 | while(*s) { |
| 99 | e = s + 1; |
| 100 | while(*e && *e != ',') e++; |
| 101 | |
| 102 | size_t len = e - s; |
| 103 | if(unlikely(len == solen && !strncmp(s, super_options, len))) |
| 104 | return mi; |
| 105 | |
| 106 | if(*e == ',') s = ++e; |
| 107 | else s = e; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | return NULL; |
| 112 | } |
| 113 | |
| 114 | static void mountinfo_free(struct mountinfo *mi) { |
| 115 | freez(mi->root); |
| 116 | freez(mi->mount_point_stat_path); |
| 117 | freez(mi->mount_point); |
| 118 | freez(mi->mount_options); |
| 119 | freez(mi->persistent_id); |
| 120 | /* |
| 121 | if(mi->optional_fields_count) { |
| 122 | int i; |
| 123 | for(i = 0; i < mi->optional_fields_count ; i++) |
| 124 | free(*mi->optional_fields[i]); |
| 125 | } |
| 126 | free(mi->optional_fields); |
| 127 | */ |
| 128 | freez(mi->filesystem); |
| 129 | freez(mi->mount_source); |
| 130 | freez(mi->mount_source_name); |
| 131 | freez(mi->super_options); |
| 132 | freez(mi); |
| 133 | } |
| 134 | |
| 135 | // free a linked list of mountinfo structures |
| 136 | void mountinfo_free_all(struct mountinfo *mi) { |
| 137 | while(mi) { |
| 138 | struct mountinfo *t = mi; |
| 139 | mi = mi->next; |
| 140 | |
| 141 | mountinfo_free(t); |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | static char *strdupz_decoding_octal(const char *string) { |
| 146 | char *buffer = strdupz(string); |
| 147 | |
| 148 | char *d = buffer; |
| 149 | const char *s = string; |
| 150 | |
| 151 | while(*s) { |
| 152 | if(unlikely(*s == '\\')) { |
| 153 | s++; |
| 154 | if(likely(isdigit(*s) && isdigit(s[1]) && isdigit(s[2]))) { |
| 155 | char c = *s++ - '0'; |
| 156 | c <<= 3; |
| 157 | c |= *s++ - '0'; |
| 158 | c <<= 3; |
| 159 | c |= *s++ - '0'; |
| 160 | *d++ = c; |
| 161 | } |
| 162 | else *d++ = '_'; |
| 163 | } |
| 164 | else *d++ = *s++; |
| 165 | } |
| 166 | *d = '\0'; |
| 167 | |
| 168 | return buffer; |
| 169 | } |
| 170 | |
| 171 | static inline int is_read_only(const char *s) { |
| 172 | if(!s) return 0; |
| 173 | |
| 174 | size_t len = strlen(s); |
| 175 | if(len < 2) return 0; |
| 176 | if(len == 2) { |
| 177 | if(!strcmp(s, "ro")) return 1; |
| 178 | return 0; |
| 179 | } |
| 180 | if(!strncmp(s, "ro,", 3)) return 1; |
| 181 | if(!strncmp(&s[len - 3], ",ro", 3)) return 1; |
| 182 | if(strstr(s, ",ro,")) return 1; |
| 183 | return 0; |
| 184 | } |
| 185 | |
| 186 | // for the full list of protected mount points look at |
| 187 | // https://github.com/systemd/systemd/blob/1eb3ef78b4df28a9e9f464714208f2682f957e36/src/core/namespace.c#L142-L149 |
| 188 | // https://github.com/systemd/systemd/blob/1eb3ef78b4df28a9e9f464714208f2682f957e36/src/core/namespace.c#L180-L194 |
| 189 | static const char *systemd_protected_mount_points[] = { |
| 190 | "/home", |
| 191 | "/root", |
| 192 | "/usr", |
| 193 | "/boot", |
| 194 | "/efi", |
| 195 | "/etc", |
| 196 | "/run/user", |
| 197 | "/lib", |
| 198 | "/lib64", |
| 199 | "/bin", |
| 200 | "/sbin", |
| 201 | NULL |
| 202 | }; |
| 203 | |
| 204 | static inline int mount_point_is_protected(char *mount_point) |
| 205 | { |
| 206 | for (size_t i = 0; systemd_protected_mount_points[i] != NULL; i++) |
| 207 | if (!strcmp(mount_point, systemd_protected_mount_points[i])) |
| 208 | return 1; |
| 209 | |
| 210 | return 0; |
| 211 | } |
| 212 | |
| 213 | // read the whole mountinfo into a linked list |
| 214 | struct mountinfo *mountinfo_read(int do_statvfs) { |
| 215 | char filename[FILENAME_MAX + 1]; |
| 216 | |
| 217 | snprintfz(filename, FILENAME_MAX, "%s/root/proc/1/mountinfo", netdata_configured_host_prefix); |
| 218 | procfile *ff = procfile_open(filename, " \t", PROCFILE_FLAG_NO_ERROR_ON_FILE_IO); |
| 219 | |
| 220 | // Inside docker with '-v "/:/host/root:ro' |
| 221 | bool host_root_prefix = ff != NULL; |
| 222 | |
| 223 | if (!ff) { |
| 224 | snprintfz(filename, FILENAME_MAX, "%s/proc/self/mountinfo", netdata_configured_host_prefix); |
| 225 | ff = procfile_open(filename, " \t", PROCFILE_FLAG_DEFAULT); |
| 226 | } |
| 227 | if (!ff) { |
| 228 | snprintfz(filename, FILENAME_MAX, "%s/proc/1/mountinfo", netdata_configured_host_prefix); |
| 229 | ff = procfile_open(filename, " \t", PROCFILE_FLAG_DEFAULT); |
| 230 | } |
| 231 | if (!ff) |
| 232 | return NULL; |
| 233 | |
| 234 | ff = procfile_readall(ff); |
| 235 | if(unlikely(!ff)) |
| 236 | return NULL; |
| 237 | |
| 238 | struct mountinfo *root = NULL, *last = NULL, *mi = NULL; |
| 239 | |
| 240 | // create a dictionary to track uniqueness |
| 241 | DICTIONARY *dict = dictionary_create_advanced( |
| 242 | DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_NAME_LINK_DONT_CLONE, |
| 243 | &dictionary_stats_category_collectors, 0); |
| 244 | |
| 245 | unsigned long l, lines = procfile_lines(ff); |
| 246 | for(l = 0; l < lines ;l++) { |
| 247 | if(unlikely(procfile_linewords(ff, l) < 5)) |
| 248 | continue; |
| 249 | |
| 250 | // make sure we don't add the same item twice |
| 251 | char *v = (char *)dictionary_set(dict, procfile_lineword(ff, l, 4), "N", 2); |
| 252 | if(v) { |
| 253 | if(*v == 'O') continue; |
| 254 | *v = 'O'; |
| 255 | } |
| 256 | |
| 257 | mi = mallocz(sizeof(struct mountinfo)); |
| 258 | |
| 259 | unsigned long w = 0; |
| 260 | mi->id = str2ul(procfile_lineword(ff, l, w)); w++; |
| 261 | mi->parentid = str2ul(procfile_lineword(ff, l, w)); w++; |
| 262 | |
| 263 | char *major = procfile_lineword(ff, l, w), *minor; w++; |
| 264 | for(minor = major; *minor && *minor != ':' ;minor++) ; |
| 265 | |
| 266 | if(unlikely(!*minor)) { |
| 267 | collector_error("Cannot parse major:minor on '%s' at line %lu of '%s'", major, l + 1, filename); |
| 268 | freez(mi); |
| 269 | continue; |
| 270 | } |
| 271 | |
| 272 | *minor = '\0'; |
| 273 | minor++; |
| 274 | |
| 275 | mi->flags = 0; |
| 276 | |
| 277 | mi->major = str2ul(major); |
| 278 | mi->minor = str2ul(minor); |
| 279 | |
| 280 | mi->root = strdupz(procfile_lineword(ff, l, w)); w++; |
| 281 | mi->root_hash = simple_hash(mi->root); |
| 282 | |
| 283 | mi->mount_point = strdupz_decoding_octal(procfile_lineword(ff, l, w));w++; |
| 284 | mi->mount_point_hash = simple_hash(mi->mount_point); |
| 285 | |
| 286 | if (host_root_prefix) { |
| 287 | snprintfz(filename, FILENAME_MAX, "%s/root%s", netdata_configured_host_prefix, mi->mount_point); |
| 288 | mi->mount_point_stat_path = strdupz(filename); |
| 289 | } else { |
| 290 | mi->mount_point_stat_path = strdupz(mi->mount_point); |
| 291 | } |
| 292 | |
| 293 | mi->persistent_id = strdupz(mi->mount_point); |
| 294 | netdata_fix_chart_id(mi->persistent_id); |
| 295 | mi->persistent_id_hash = simple_hash(mi->persistent_id); |
| 296 | |
| 297 | mi->mount_options = strdupz(procfile_lineword(ff, l, w)); w++; |
| 298 | |
| 299 | if(unlikely(is_read_only(mi->mount_options))) |
| 300 | mi->flags |= MOUNTINFO_READONLY; |
| 301 | |
| 302 | if(unlikely(mount_point_is_protected(mi->mount_point))) |
| 303 | mi->flags |= MOUNTINFO_IS_IN_SYSD_PROTECTED_LIST; |
| 304 | |
| 305 | // count the optional fields |
| 306 | /* |
| 307 | unsigned long wo = w; |
| 308 | */ |
| 309 | mi->optional_fields_count = 0; |
| 310 | char *s = procfile_lineword(ff, l, w); |
| 311 | while(*s && *s != '-') { |
| 312 | w++; |
| 313 | s = procfile_lineword(ff, l, w); |
| 314 | mi->optional_fields_count++; |
| 315 | } |
| 316 | |
| 317 | /* |
| 318 | if(unlikely(mi->optional_fields_count)) { |
| 319 | // we have some optional fields |
| 320 | // read them into a new array of pointers; |
| 321 | |
| 322 | mi->optional_fields = mallocz(mi->optional_fields_count * sizeof(char *)); |
| 323 | |
| 324 | int i; |
| 325 | for(i = 0; i < mi->optional_fields_count ; i++) { |
| 326 | *mi->optional_fields[wo] = strdupz(procfile_lineword(ff, l, w)); |
| 327 | wo++; |
| 328 | } |
| 329 | } |
| 330 | else |
| 331 | mi->optional_fields = NULL; |
| 332 | */ |
| 333 | |
| 334 | if(likely(*s == '-')) { |
| 335 | w++; |
| 336 | |
| 337 | mi->filesystem = strdupz(procfile_lineword(ff, l, w)); w++; |
| 338 | mi->filesystem_hash = simple_hash(mi->filesystem); |
| 339 | |
| 340 | mi->mount_source = strdupz_decoding_octal(procfile_lineword(ff, l, w)); w++; |
| 341 | mi->mount_source_hash = simple_hash(mi->mount_source); |
| 342 | |
| 343 | mi->mount_source_name = strdupz(basename(mi->mount_source)); |
| 344 | mi->mount_source_name_hash = simple_hash(mi->mount_source_name); |
| 345 | |
| 346 | mi->super_options = strdupz(procfile_lineword(ff, l, w)); w++; |
| 347 | |
| 348 | if(unlikely(is_read_only(mi->super_options))) |
| 349 | mi->flags |= MOUNTINFO_READONLY; |
| 350 | |
| 351 | if(unlikely(ME_DUMMY(mi->mount_source, mi->filesystem))) |
| 352 | mi->flags |= MOUNTINFO_IS_DUMMY; |
| 353 | |
| 354 | if(unlikely(ME_REMOTE(mi->mount_source, mi->filesystem))) |
| 355 | mi->flags |= MOUNTINFO_IS_REMOTE; |
| 356 | |
| 357 | // mark as BIND the duplicates (i.e. same filesystem + same source) |
| 358 | if(do_statvfs) { |
| 359 | struct stat buf; |
| 360 | if(unlikely(stat(mi->mount_point_stat_path, &buf) == -1)) { |
| 361 | mi->st_dev = 0; |
| 362 | mi->flags |= MOUNTINFO_NO_STAT; |
| 363 | } |
| 364 | else { |
| 365 | mi->st_dev = buf.st_dev; |
| 366 | |
| 367 | struct mountinfo *mt; |
| 368 | for(mt = root; mt; mt = mt->next) { |
| 369 | if(unlikely(mt->st_dev == mi->st_dev && !(mt->flags & MOUNTINFO_IS_SAME_DEV))) { |
| 370 | if(strlen(mi->mount_point) < strlen(mt->mount_point)) |
| 371 | mt->flags |= MOUNTINFO_IS_SAME_DEV; |
| 372 | else |
| 373 | mi->flags |= MOUNTINFO_IS_SAME_DEV; |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | else { |
| 379 | mi->st_dev = 0; |
| 380 | } |
| 381 | |
| 382 | //try to detect devices with same minor and major modes. Within these, |
| 383 | //the larger mount point is considered a bind. |
| 384 | struct mountinfo *mt; |
| 385 | for(mt = root; mt; mt = mt->next) { |
| 386 | if(unlikely(mt->major == mi->major && mt->minor == mi->minor && !(mi->flags & MOUNTINFO_IS_BIND))) { |
| 387 | if(strlen(mi->root) < strlen(mt->root)) |
| 388 | mt->flags |= MOUNTINFO_IS_BIND; |
| 389 | else |
| 390 | mi->flags |= MOUNTINFO_IS_BIND; |
| 391 | } |
| 392 | } |
| 393 | } |
| 394 | else { |
| 395 | mi->filesystem = NULL; |
| 396 | mi->filesystem_hash = 0; |
| 397 | |
| 398 | mi->mount_source = NULL; |
| 399 | mi->mount_source_hash = 0; |
| 400 | |
| 401 | mi->mount_source_name = NULL; |
| 402 | mi->mount_source_name_hash = 0; |
| 403 | |
| 404 | mi->super_options = NULL; |
| 405 | |
| 406 | mi->st_dev = 0; |
| 407 | } |
| 408 | |
| 409 | // check if it has size |
| 410 | if(do_statvfs && !(mi->flags & MOUNTINFO_IS_DUMMY)) { |
| 411 | struct statvfs buff_statvfs; |
| 412 | if(unlikely(statvfs(mi->mount_point_stat_path, &buff_statvfs) < 0)) { |
| 413 | mi->flags |= MOUNTINFO_NO_STAT; |
| 414 | } |
| 415 | else if(unlikely(!buff_statvfs.f_blocks /* || !buff_statvfs.f_files */)) { |
| 416 | mi->flags |= MOUNTINFO_NO_SIZE; |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | // link it |
| 421 | if(unlikely(!root)) |
| 422 | root = mi; |
| 423 | else |
| 424 | last->next = mi; |
| 425 | |
| 426 | last = mi; |
| 427 | mi->next = NULL; |
| 428 | |
| 429 | /* |
| 430 | #ifdef NETDATA_INTERNAL_CHECKS |
| 431 | fprintf(stderr, "MOUNTINFO: %ld %ld %lu:%lu root '%s', persistent id '%s', mount point '%s', mount options '%s', filesystem '%s', mount source '%s', super options '%s'%s%s%s%s%s%s\n", |
| 432 | mi->id, |
| 433 | mi->parentid, |
| 434 | mi->major, |
| 435 | mi->minor, |
| 436 | mi->root, |
| 437 | mi->persistent_id, |
| 438 | (mi->mount_point)?mi->mount_point:"", |
| 439 | (mi->mount_options)?mi->mount_options:"", |
| 440 | (mi->filesystem)?mi->filesystem:"", |
| 441 | (mi->mount_source)?mi->mount_source:"", |
| 442 | (mi->super_options)?mi->super_options:"", |
| 443 | (mi->flags & MOUNTINFO_IS_DUMMY)?" DUMMY":"", |
| 444 | (mi->flags & MOUNTINFO_IS_BIND)?" BIND":"", |
| 445 | (mi->flags & MOUNTINFO_IS_REMOTE)?" REMOTE":"", |
| 446 | (mi->flags & MOUNTINFO_NO_STAT)?" NOSTAT":"", |
| 447 | (mi->flags & MOUNTINFO_NO_SIZE)?" NOSIZE":"", |
| 448 | (mi->flags & MOUNTINFO_IS_SAME_DEV)?" SAMEDEV":"" |
| 449 | ); |
| 450 | #endif |
| 451 | */ |
| 452 | } |
| 453 | |
| 454 | /* find if the mount options have "bind" in them |
| 455 | { |
| 456 | FILE *fp = setmntent(MOUNTED, "r"); |
| 457 | if (fp != NULL) { |
| 458 | struct mntent mntbuf; |
| 459 | struct mntent *mnt; |
| 460 | char buf[4096 + 1]; |
| 461 | |
| 462 | while ((mnt = getmntent_r(fp, &mntbuf, buf, 4096))) { |
| 463 | char *bind = hasmntopt(mnt, "bind"); |
| 464 | if(unlikely(bind)) { |
| 465 | struct mountinfo *mi; |
| 466 | for(mi = root; mi ; mi = mi->next) { |
| 467 | if(unlikely(strcmp(mnt->mnt_dir, mi->mount_point) == 0)) { |
| 468 | fprintf(stderr, "Mount point '%s' is BIND\n", mi->mount_point); |
| 469 | mi->flags |= MOUNTINFO_IS_BIND; |
| 470 | break; |
| 471 | } |
| 472 | } |
| 473 | |
| 474 | #ifdef NETDATA_INTERNAL_CHECKS |
| 475 | if(unlikely(!mi)) { |
| 476 | collector_error("Mount point '%s' not found in /proc/self/mountinfo", mnt->mnt_dir); |
| 477 | } |
| 478 | #endif |
| 479 | } |
| 480 | } |
| 481 | endmntent(fp); |
| 482 | } |
| 483 | } |
| 484 | */ |
| 485 | |
| 486 | dictionary_destroy(dict); |
| 487 | procfile_close(ff); |
| 488 | return root; |
| 489 | } |