1 // Copyright 2010 The Kyua Authors. 2 // All rights reserved. 3 // 4 // Redistribution and use in source and binary forms, with or without 5 // modification, are permitted provided that the following conditions are 6 // met: 7 // 8 // * Redistributions of source code must retain the above copyright 9 // notice, this list of conditions and the following disclaimer. 10 // * Redistributions in binary form must reproduce the above copyright 11 // notice, this list of conditions and the following disclaimer in the 12 // documentation and/or other materials provided with the distribution. 13 // * Neither the name of Google Inc. nor the names of its contributors 14 // may be used to endorse or promote products derived from this software 15 // without specific prior written permission. 16 // 17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 #include "utils/fs/operations.hpp" 30 31 #if defined(HAVE_CONFIG_H) 32 # include "config.h" 33 #endif 34 35 extern "C" { 36 #include <sys/param.h> 37 #if defined(HAVE_SYS_MOUNT_H) 38 # include <sys/mount.h> 39 #endif 40 #include <sys/stat.h> 41 #if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS) 42 # include <sys/statvfs.h> 43 #endif 44 #if defined(HAVE_SYS_VFS_H) 45 # include <sys/vfs.h> 46 #endif 47 #include <sys/wait.h> 48 49 #include <unistd.h> 50 } 51 52 #include <cerrno> 53 #include <cstdlib> 54 #include <cstring> 55 #include <fstream> 56 #include <iostream> 57 #include <sstream> 58 #include <string> 59 60 #include "utils/auto_array.ipp" 61 #include "utils/defs.hpp" 62 #include "utils/env.hpp" 63 #include "utils/format/macros.hpp" 64 #include "utils/fs/directory.hpp" 65 #include "utils/fs/exceptions.hpp" 66 #include "utils/fs/path.hpp" 67 #include "utils/logging/macros.hpp" 68 #include "utils/optional.ipp" 69 #include "utils/sanity.hpp" 70 #include "utils/units.hpp" 71 72 namespace fs = utils::fs; 73 namespace units = utils::units; 74 75 using utils::optional; 76 77 78 namespace { 79 80 81 /// Operating systems recognized by the code below. 82 enum os_type { 83 os_unsupported = 0, 84 os_freebsd, 85 os_linux, 86 os_netbsd, 87 os_sunos, 88 }; 89 90 91 /// The current operating system. 92 static enum os_type current_os = 93 #if defined(__FreeBSD__) 94 os_freebsd 95 #elif defined(__linux__) 96 os_linux 97 #elif defined(__NetBSD__) 98 os_netbsd 99 #elif defined(__SunOS__) 100 os_sunos 101 #else 102 os_unsupported 103 #endif 104 ; 105 106 107 /// Error code returned by subprocess to indicate a controlled failure. 108 const int exit_known_error = 123; 109 110 111 static void run_mount_tmpfs [[noreturn]] (const fs::path&, const uint64_t); 112 113 114 /// Executes 'mount -t tmpfs' (or a similar variant). 115 /// 116 /// This function must be called from a subprocess as it never returns. 117 /// 118 /// \param mount_point Location on which to mount a tmpfs. 119 /// \param size The size of the tmpfs to mount. If 0, use unlimited. 120 static void 121 run_mount_tmpfs(const fs::path& mount_point, const uint64_t size) 122 { 123 const char* mount_args[16]; 124 std::string size_arg; 125 126 std::size_t last = 0; 127 switch (current_os) { 128 case os_freebsd: 129 mount_args[last++] = "mount"; 130 mount_args[last++] = "-ttmpfs"; 131 if (size > 0) { 132 size_arg = F("-osize=%s") % size; 133 mount_args[last++] = size_arg.c_str(); 134 } 135 mount_args[last++] = "tmpfs"; 136 mount_args[last++] = mount_point.c_str(); 137 break; 138 139 case os_linux: 140 mount_args[last++] = "mount"; 141 mount_args[last++] = "-ttmpfs"; 142 if (size > 0) { 143 size_arg = F("-osize=%s") % size; 144 mount_args[last++] = size_arg.c_str(); 145 } 146 mount_args[last++] = "tmpfs"; 147 mount_args[last++] = mount_point.c_str(); 148 break; 149 150 case os_netbsd: 151 mount_args[last++] = "mount"; 152 mount_args[last++] = "-ttmpfs"; 153 if (size > 0) { 154 size_arg = F("-o-s%s") % size; 155 mount_args[last++] = size_arg.c_str(); 156 } 157 mount_args[last++] = "tmpfs"; 158 mount_args[last++] = mount_point.c_str(); 159 break; 160 161 case os_sunos: 162 mount_args[last++] = "mount"; 163 mount_args[last++] = "-Ftmpfs"; 164 if (size > 0) { 165 size_arg = F("-o-s%s") % size; 166 mount_args[last++] = size_arg.c_str(); 167 } 168 mount_args[last++] = "tmpfs"; 169 mount_args[last++] = mount_point.c_str(); 170 break; 171 172 default: 173 std::cerr << "Don't know how to mount a temporary file system in this " 174 "host operating system\n"; 175 std::exit(exit_known_error); 176 } 177 mount_args[last] = NULL; 178 179 const char** arg; 180 std::cout << "Mounting tmpfs onto " << mount_point << " with:"; 181 for (arg = &mount_args[0]; *arg != NULL; arg++) 182 std::cout << " " << *arg; 183 std::cout << "\n"; 184 185 const int ret = ::execvp(mount_args[0], 186 UTILS_UNCONST(char* const, mount_args)); 187 INV(ret == -1); 188 std::cerr << "Failed to exec " << mount_args[0] << "\n"; 189 std::exit(EXIT_FAILURE); 190 } 191 192 193 /// Unmounts a file system using unmount(2) or umount(2). 194 /// 195 /// This is a thin wrapper around the filesystem implementation for the actual 196 /// underlying system call which implements filesystem unmounting function. 197 /// 198 /// \param mount_point The file system to unmount. 199 /// 200 /// \throw fs::system_error The unmount operation fails. 201 static void 202 unmount_impl(const fs::path& mount_point) 203 { 204 205 #if defined(HAVE_UNMOUNT) 206 if (::unmount(mount_point.c_str(), 0) == 0) 207 return; 208 #elif defined(HAVE_UMOUNT) 209 if (::umount(mount_point.c_str()) == 0) 210 return; 211 #endif 212 const int original_errno = errno; 213 throw fs::system_error(F("unmount(%s) failed") % mount_point, 214 original_errno); 215 } 216 217 /// Stats a file, without following links. 218 /// 219 /// \param path The file to stat. 220 /// 221 /// \return The stat structure on success. 222 /// 223 /// \throw system_error An error on failure. 224 static struct ::stat 225 safe_stat(const fs::path& path) 226 { 227 struct ::stat sb; 228 if (::lstat(path.c_str(), &sb) == -1) { 229 const int original_errno = errno; 230 throw fs::system_error(F("Cannot get information about %s") % path, 231 original_errno); 232 } 233 return sb; 234 } 235 236 237 } // anonymous namespace 238 239 240 /// Copies a file. 241 /// 242 /// \param source The file to copy. 243 /// \param target The destination of the new copy; must be a file name, not a 244 /// directory. 245 /// 246 /// \throw error If there is a problem copying the file. 247 void 248 fs::copy(const fs::path& source, const fs::path& target) 249 { 250 std::ifstream input(source.c_str()); 251 if (!input) 252 throw error(F("Cannot open copy source %s") % source); 253 254 std::ofstream output(target.c_str()); 255 if (!output) 256 throw error(F("Cannot create copy target %s") % target); 257 258 char buffer[1024]; 259 while (input.good()) { 260 input.read(buffer, sizeof(buffer)); 261 if (input.good() || input.eof()) 262 output.write(buffer, input.gcount()); 263 } 264 if (!input.good() && !input.eof()) 265 throw error(F("Error while reading input file %s") % source); 266 } 267 268 269 /// Queries the path to the current directory. 270 /// 271 /// \return The path to the current directory. 272 /// 273 /// \throw fs::error If there is a problem querying the current directory. 274 fs::path 275 fs::current_path(void) 276 { 277 char* cwd; 278 #if defined(HAVE_GETCWD_DYN) 279 cwd = ::getcwd(NULL, 0); 280 #else 281 cwd = ::getcwd(NULL, MAXPATHLEN); 282 #endif 283 if (cwd == NULL) { 284 const int original_errno = errno; 285 throw fs::system_error(F("Failed to get current working directory"), 286 original_errno); 287 } 288 289 try { 290 const fs::path result(cwd); 291 std::free(cwd); 292 return result; 293 } catch (...) { 294 std::free(cwd); 295 throw; 296 } 297 } 298 299 300 /// Checks if a file exists. 301 /// 302 /// Be aware that this is racy in the same way as access(2) is. 303 /// 304 /// \param path The file to check the existance of. 305 /// 306 /// \return True if the file exists; false otherwise. 307 bool 308 fs::exists(const fs::path& path) 309 { 310 return ::access(path.c_str(), F_OK) == 0; 311 } 312 313 314 /// Locates a file in the PATH. 315 /// 316 /// \param name The file to locate. 317 /// 318 /// \return The path to the located file or none if it was not found. The 319 /// returned path is always absolute. 320 optional< fs::path > 321 fs::find_in_path(const char* name) 322 { 323 const optional< std::string > current_path = utils::getenv("PATH"); 324 if (!current_path || current_path.get().empty()) 325 return none; 326 327 std::istringstream path_input(current_path.get() + ":"); 328 std::string path_component; 329 while (std::getline(path_input, path_component, ':').good()) { 330 const fs::path candidate = path_component.empty() ? 331 fs::path(name) : (fs::path(path_component) / name); 332 if (exists(candidate)) { 333 if (candidate.is_absolute()) 334 return utils::make_optional(candidate); 335 else 336 return utils::make_optional(candidate.to_absolute()); 337 } 338 } 339 return none; 340 } 341 342 343 /// Calculates the free space in a given file system. 344 /// 345 /// \param path Path to a file in the file system for which to check the free 346 /// disk space. 347 /// 348 /// \return The amount of free space usable by a non-root user. 349 /// 350 /// \throw system_error If the call to statfs(2) fails. 351 utils::units::bytes 352 fs::free_disk_space(const fs::path& path) 353 { 354 #if defined(HAVE_STATVFS) 355 struct ::statvfs buf; 356 if (::statvfs(path.c_str(), &buf) == -1) { 357 const int original_errno = errno; 358 throw fs::system_error(F("Failed to stat file system for %s") % path, 359 original_errno); 360 } 361 return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); 362 #elif defined(HAVE_STATFS) 363 struct ::statfs buf; 364 if (::statfs(path.c_str(), &buf) == -1) { 365 const int original_errno = errno; 366 throw fs::system_error(F("Failed to stat file system for %s") % path, 367 original_errno); 368 } 369 return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); 370 #else 371 # error "Don't know how to query free disk space" 372 #endif 373 } 374 375 376 /// Checks if the given path is a directory or not. 377 /// 378 /// \return True if the path is a directory; false otherwise. 379 bool 380 fs::is_directory(const fs::path& path) 381 { 382 const struct ::stat sb = safe_stat(path); 383 return S_ISDIR(sb.st_mode); 384 } 385 386 387 /// Creates a directory. 388 /// 389 /// \param dir The path to the directory to create. 390 /// \param mode The permissions for the new directory. 391 /// 392 /// \throw system_error If the call to mkdir(2) fails. 393 void 394 fs::mkdir(const fs::path& dir, const int mode) 395 { 396 if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { 397 const int original_errno = errno; 398 throw fs::system_error(F("Failed to create directory %s") % dir, 399 original_errno); 400 } 401 } 402 403 404 /// Creates a directory and any missing parents. 405 /// 406 /// This is separate from the fs::mkdir function to clearly differentiate the 407 /// libc wrapper from the more complex algorithm implemented here. 408 /// 409 /// \param dir The path to the directory to create. 410 /// \param mode The permissions for the new directories. 411 /// 412 /// \throw system_error If any call to mkdir(2) fails. 413 void 414 fs::mkdir_p(const fs::path& dir, const int mode) 415 { 416 try { 417 fs::mkdir(dir, mode); 418 } catch (const fs::system_error& e) { 419 if (e.original_errno() == ENOENT) { 420 fs::mkdir_p(dir.branch_path(), mode); 421 fs::mkdir(dir, mode); 422 } else if (e.original_errno() != EEXIST) 423 throw e; 424 } 425 } 426 427 428 /// Creates a temporary directory that is world readable/accessible. 429 /// 430 /// The temporary directory is created using mkdtemp(3) using the provided 431 /// template. This should be most likely used in conjunction with 432 /// fs::auto_directory. 433 /// 434 /// The temporary directory is given read and execute permissions to everyone 435 /// and thus should not be used to protect data that may be subject to snooping. 436 /// This goes together with the assumption that this function is used to create 437 /// temporary directories for test cases, and that those test cases may 438 /// sometimes be executed as an unprivileged user. In those cases, we need to 439 /// support two different things: 440 /// 441 /// - Allow the unprivileged code to write to files in the work directory by 442 /// name (e.g. to write the results file, whose name is provided by the 443 /// monitor code running as root). This requires us to grant search 444 /// permissions. 445 /// 446 /// - Allow the test cases themselves to call getcwd(3) at any point. At least 447 /// on NetBSD 7.x, getcwd(3) requires both read and search permissions on all 448 /// path components leading to the current directory. This requires us to 449 /// grant both read and search permissions. 450 /// 451 /// TODO(jmmv): A cleaner way to support this would be for the test executor to 452 /// create two work directory hierarchies directly rooted under TMPDIR: one for 453 /// root and one for the unprivileged user. However, that requires more 454 /// bookkeeping for no real gain, because we are not really trying to protect 455 /// the data within our temporary directories against attacks. 456 /// 457 /// \param path_template The template for the temporary path, which is a 458 /// basename that is created within the TMPDIR. Must contain the XXXXXX 459 /// pattern, which is atomically replaced by a random unique string. 460 /// 461 /// \return The generated path for the temporary directory. 462 /// 463 /// \throw fs::system_error If the call to mkdtemp(3) fails. 464 fs::path 465 fs::mkdtemp_public(const std::string& path_template) 466 { 467 PRE(path_template.find("XXXXXX") != std::string::npos); 468 469 const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); 470 const fs::path full_template = tmpdir / path_template; 471 472 utils::auto_array< char > buf(new char[full_template.str().length() + 1]); 473 std::strcpy(buf.get(), full_template.c_str()); 474 if (::mkdtemp(buf.get()) == NULL) { 475 const int original_errno = errno; 476 throw fs::system_error(F("Cannot create temporary directory using " 477 "template %s") % full_template, 478 original_errno); 479 } 480 const fs::path path(buf.get()); 481 482 if (::chmod(path.c_str(), 0755) == -1) { 483 const int original_errno = errno; 484 485 try { 486 rmdir(path); 487 } catch (const fs::system_error& e) { 488 // This really should not fail. We just created the directory and 489 // have not written anything to it so there is no reason for this to 490 // fail. But better handle the failure just in case. 491 LW(F("Failed to delete just-created temporary directory %s") 492 % path); 493 } 494 495 throw fs::system_error(F("Failed to grant search permissions on " 496 "temporary directory %s") % path, 497 original_errno); 498 } 499 500 return path; 501 } 502 503 504 /// Creates a temporary file. 505 /// 506 /// The temporary file is created using mkstemp(3) using the provided template. 507 /// This should be most likely used in conjunction with fs::auto_file. 508 /// 509 /// \param path_template The template for the temporary path, which is a 510 /// basename that is created within the TMPDIR. Must contain the XXXXXX 511 /// pattern, which is atomically replaced by a random unique string. 512 /// 513 /// \return The generated path for the temporary directory. 514 /// 515 /// \throw fs::system_error If the call to mkstemp(3) fails. 516 fs::path 517 fs::mkstemp(const std::string& path_template) 518 { 519 PRE(path_template.find("XXXXXX") != std::string::npos); 520 521 const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); 522 const fs::path full_template = tmpdir / path_template; 523 524 utils::auto_array< char > buf(new char[full_template.str().length() + 1]); 525 std::strcpy(buf.get(), full_template.c_str()); 526 if (::mkstemp(buf.get()) == -1) { 527 const int original_errno = errno; 528 throw fs::system_error(F("Cannot create temporary file using template " 529 "%s") % full_template, original_errno); 530 } 531 return fs::path(buf.get()); 532 } 533 534 535 /// Mounts a temporary file system with unlimited size. 536 /// 537 /// \param in_mount_point The path on which the file system will be mounted. 538 /// 539 /// \throw fs::system_error If the attempt to mount process fails. 540 /// \throw fs::unsupported_operation_error If the code does not know how to 541 /// mount a temporary file system in the current operating system. 542 void 543 fs::mount_tmpfs(const fs::path& in_mount_point) 544 { 545 mount_tmpfs(in_mount_point, units::bytes()); 546 } 547 548 549 /// Mounts a temporary file system. 550 /// 551 /// \param in_mount_point The path on which the file system will be mounted. 552 /// \param size The size of the tmpfs to mount. If 0, use unlimited. 553 /// 554 /// \throw fs::system_error If the attempt to mount process fails. 555 /// \throw fs::unsupported_operation_error If the code does not know how to 556 /// mount a temporary file system in the current operating system. 557 void 558 fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size) 559 { 560 // SunOS's mount(8) requires paths to be absolute. To err on the side of 561 // caution, let's make the mount point absolute in all cases. 562 const fs::path mount_point = in_mount_point.is_absolute() ? 563 in_mount_point : in_mount_point.to_absolute(); 564 565 const pid_t pid = ::fork(); 566 if (pid == -1) { 567 const int original_errno = errno; 568 throw fs::system_error("Cannot fork to execute mount tool", 569 original_errno); 570 } 571 if (pid == 0) 572 run_mount_tmpfs(mount_point, size); 573 574 int status; 575 retry: 576 if (::waitpid(pid, &status, 0) == -1) { 577 const int original_errno = errno; 578 if (errno == EINTR) 579 goto retry; 580 throw fs::system_error("Failed to wait for mount subprocess", 581 original_errno); 582 } 583 584 if (WIFEXITED(status)) { 585 if (WEXITSTATUS(status) == exit_known_error) 586 throw fs::unsupported_operation_error( 587 "Don't know how to mount a tmpfs on this operating system"); 588 else if (WEXITSTATUS(status) == EXIT_SUCCESS) 589 return; 590 else 591 throw fs::error(F("Failed to mount tmpfs on %s; returned exit " 592 "code %s") % mount_point % WEXITSTATUS(status)); 593 } else { 594 throw fs::error(F("Failed to mount tmpfs on %s; mount tool " 595 "received signal") % mount_point); 596 } 597 } 598 599 600 /// Recursively removes a directory. 601 /// 602 /// This operation simulates a "rm -r". No effort is made to forcibly delete 603 /// files and no attention is paid to mount points. 604 /// 605 /// \param directory The directory to remove. 606 /// 607 /// \throw fs::error If there is a problem removing any directory or file. 608 void 609 fs::rm_r(const fs::path& directory) 610 { 611 const fs::directory dir(directory); 612 613 ::chmod(directory.c_str(), 0700); 614 for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); 615 ++iter) { 616 if (iter->name == "." || iter->name == "..") 617 continue; 618 619 const fs::path entry = directory / iter->name; 620 621 if (fs::is_directory(entry)) { 622 LD(F("Descending into %s") % entry); 623 ::chmod(entry.c_str(), 0700); 624 fs::rm_r(entry); 625 } else { 626 LD(F("Removing file %s") % entry); 627 fs::unlink(entry); 628 } 629 } 630 631 LD(F("Removing empty directory %s") % directory); 632 fs::rmdir(directory); 633 } 634 635 636 /// Removes an empty directory. 637 /// 638 /// \param file The directory to remove. 639 /// 640 /// \throw fs::system_error If the call to rmdir(2) fails. 641 void 642 fs::rmdir(const path& file) 643 { 644 if (::rmdir(file.c_str()) == -1) { 645 const int original_errno = errno; 646 throw fs::system_error(F("Removal of %s failed") % file, 647 original_errno); 648 } 649 } 650 651 652 /// Obtains all the entries in a directory. 653 /// 654 /// \param path The directory to scan. 655 /// 656 /// \return The set of all directory entries in the given directory. 657 /// 658 /// \throw fs::system_error If reading the directory fails for any reason. 659 std::set< fs::directory_entry > 660 fs::scan_directory(const fs::path& path) 661 { 662 std::set< fs::directory_entry > contents; 663 664 fs::directory dir(path); 665 for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); 666 ++iter) { 667 contents.insert(*iter); 668 } 669 670 return contents; 671 } 672 673 674 /// Removes a file. 675 /// 676 /// \param file The file to remove. 677 /// 678 /// \throw fs::system_error If the call to unlink(2) fails. 679 void 680 fs::unlink(const path& file) 681 { 682 if (::unlink(file.c_str()) == -1) { 683 const int original_errno = errno; 684 throw fs::system_error(F("Removal of %s failed") % file, 685 original_errno); 686 } 687 } 688 689 690 /// Unmounts a file system. 691 /// 692 /// \param in_mount_point The file system to unmount. 693 /// 694 /// \throw fs::error If the unmount fails. 695 void 696 fs::unmount(const fs::path& in_mount_point) 697 { 698 // FreeBSD's unmount(2) requires paths to be absolute. To err on the side 699 // of caution, let's make it absolute in all cases. 700 const fs::path mount_point = in_mount_point.is_absolute() ? 701 in_mount_point : in_mount_point.to_absolute(); 702 703 static const int unmount_retries = 3; 704 static const int unmount_retry_delay_seconds = 1; 705 706 int retries = unmount_retries; 707 retry: 708 try { 709 unmount_impl(mount_point); 710 } catch (const fs::system_error& error) { 711 if (error.original_errno() == EBUSY && retries > 0) { 712 LW(F("%s busy; unmount retries left %s") % mount_point % retries); 713 retries--; 714 ::sleep(unmount_retry_delay_seconds); 715 goto retry; 716 } 717 throw; 718 } 719 } 720