/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * Copyright the Collabora Online contributors. * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include "FileUtil.hpp" #include "JailUtil.hpp" #include #include #include #ifdef __linux__ #include #endif #include #include #include #include #include "Log.hpp" #include namespace JailUtil { bool coolmount(const std::string& arg, std::string source, std::string target) { source = Util::trim(source, '/'); target = Util::trim(target, '/'); const std::string cmd = Poco::Path(Util::getApplicationPath(), "coolmount").toString() + ' ' + arg + ' ' + source + ' ' + target; LOG_TRC("Executing coolmount command: " << cmd); return !system(cmd.c_str()); } bool bind(const std::string& source, const std::string& target) { LOG_DBG("Mounting [" << source << "] -> [" << target << ']'); try { Poco::File(target).createDirectory(); const bool res = coolmount("-b", source, target); if (res) LOG_TRC("Bind-mounted [" << source << "] -> [" << target << ']'); else LOG_ERR("Failed to bind-mount [" << source << "] -> [" << target << ']'); return res; } catch (const std::exception& exc) { LOG_ERR("Failed to mount [" << source << "] -> [" << target << "]: " << exc.what()); } return false; } bool remountReadonly(const std::string& source, const std::string& target) { LOG_DBG("Remounting [" << source << "] -> [" << target << ']'); try { Poco::File(target).createDirectory(); const bool res = coolmount("-r", source, target); if (res) LOG_TRC("Mounted [" << source << "] -> [" << target << "] readonly"); else LOG_ERR("Failed to mount [" << source << "] -> [" << target << "] readonly"); return res; } catch (const std::exception& exc) { LOG_ERR("Failed to remount [" << source << "] -> [" << target << "]: " << exc.what()); } return false; } /// Unmount a bind-mounted jail directory. static bool unmount(const std::string& target) { LOG_DBG("Unmounting [" << target << ']'); const bool res = coolmount("-u", "", target); if (res) LOG_TRC("Unmounted [" << target << "] successfully."); else { // If bind-mounting is enabled, noisily log failures. // Otherwise, it's a cleanup attempt of earlier mounts, // which may be left-over and now the config has changed. // This happens more often in dev labs than in prod. if (JailUtil::isBindMountingEnabled()) LOG_ERR("Failed to unmount [" << target << ']'); else LOG_DBG("Failed to unmount [" << target << ']'); } return res; } // This file signifies that we copied instead of mounted. // NOTE: jail cleanup helpers are called from forkit and // coolwsd, and they may have bind-mounting enabled, but the // kit could have had it removed when falling back to copying. // In such cases, we cannot safely know whether the jail was // copied or not, since the bind envar will be present and // assuming it was mounted, would leak them. // Alternatively, if we remove the files when mounted // we could destroy systemplate if remounting read-only had // failed (and it wasn't owned by root). constexpr const char* COPIED_JAIL_MARKER_FILE = "delete.me"; void markJailCopied(const std::string& root) { // The reason we should be able to create this file // is because the jail must be writable. // Failing this will cause an exception, signaling an error. Poco::File(root + '/' + COPIED_JAIL_MARKER_FILE).createFile(); } bool isJailCopied(const std::string& root) { // If the marker file exists, the jail was copied. FileUtil::Stat delFileStat(root + '/' + COPIED_JAIL_MARKER_FILE); return delFileStat.exists(); } static bool safeRemoveDir(const std::string& path) { // Always unmount, just in case. unmount(path); // Regardless of the bind flag, check if the jail is marked as copied. const bool copied = isJailCopied(path); // We must be empty if we had mounted. if (!copied && JailUtil::isBindMountingEnabled() && !FileUtil::isEmptyDirectory(path)) { LOG_WRN("Path [" << path << "] is not empty. Will not remove it."); return false; } // Recursively remove if link/copied. const bool recursive = copied; //FIXME: do not delete the 'copied' marker until the very end. FileUtil::removeFile(path, recursive); return true; } void removeAuxFolders(const std::string &root) { FileUtil::removeFile(Poco::Path(root, "tmp").toString(), true); FileUtil::removeFile(Poco::Path(root, "linkable").toString(), true); } bool tryRemoveJail(const std::string& root) { if (!FileUtil::Stat(root + '/' + LO_JAIL_SUBPATH).exists()) return false; // not a jail. LOG_TRC("Do remove of jail [" << root << ']'); // Unmount the tmp directory. Don't care if we fail. const std::string tmpPath = Poco::Path(root, "tmp").toString(); #ifdef __FreeBSD__ unmount(tmpPath + "/dev"); #endif FileUtil::removeFile(tmpPath, true); // Delete tmp contents with prejudice. unmount(tmpPath); // Unmount the loTemplate directory. //FIXME: technically, the loTemplate directory may have any name. unmount(Poco::Path(root, "lo").toString()); // Unmount the test-mount directory too. const std::string testMountPath = Poco::Path(root, "cool_test_mount").toString(); if (FileUtil::Stat(testMountPath).exists()) unmount(testMountPath); // Unmount/delete the jail (sysTemplate). safeRemoveDir(root); return true; } /// This cleans up the jails directories. /// Note that we assume the templates are mounted /// and we unmount first. This is critical, because /// otherwise when mounting is disabled we may /// inadvertently delete the contents of the mount-points. void cleanupJails(const std::string& root) { LOG_INF("Cleaning up childroot directory [" << root << "]."); FileUtil::Stat stRoot(root); if (!stRoot.exists() || !stRoot.isDirectory()) { LOG_TRC("Directory [" << root << "] is not a jail directory or doesn't exist."); return; } std::vector jails; Poco::File(root).list(jails); // legacy jails at the top-level for (const auto& jail : jails) { std::string childDir = Poco::Path(root, jail).toString(); FileUtil::Stat stChild(childDir); if (stChild.exists() && !stChild.isLink() && stChild.isDirectory()) { // Modern jails should look like this: // jails/-// size_t pidSepPos = jail.find('-'); if (pidSepPos != std::string::npos) { bool skip = false; std::string pidStr = jail.substr(0, pidSepPos); try { int pid = std::stoi(pidStr); LOG_TRC("Checking pid for jail " << pid << " " << root); if (pid != getpid() && kill(pid, 0) == 0) { LOG_TRC("Skipping cleaning jails directory for running coolwsd with pid " << pid); skip = true; } } catch(...) { // Problematic - may delete a jail that is not ours then ... LOG_WRN("Exception parsing pid '" << pidStr << "' from '" << jail << "'"); } if (!skip) { std::vector newJails; Poco::File(childDir).list(newJails); // legacy jails at the top-level for (const auto& newJail : newJails) { tryRemoveJail(Poco::Path(childDir, newJail).toString()); } // top level linkable and tmp mount point. removeAuxFolders(childDir); // top level per-coolwsd jails directory. safeRemoveDir(childDir); } } // Remove legacy things that look like jails else if (tryRemoveJail(childDir)) { static size_t warned = 0; if (!(warned++)) LOG_WRN("Cleaned legacy jail without pid prefix after upgrade " << childDir); } // else legacy tmp or linkable } } // Cleanup legacy top-level 'tmp' and 'linkable' directories if empty removeAuxFolders(root); // Cleanup top-level 'jails' directory if empty if (FileUtil::isEmptyDirectory(root)) safeRemoveDir(root); else LOG_WRN("Jails root directory [" << root << "] is not empty. Will not remove it."); } void createJailPath(const std::string& path) { LOG_INF("Creating jail path (if missing): " << path); Poco::File(path).createDirectories(); if (chmod(path.c_str(), S_IXUSR | S_IWUSR | S_IRUSR) != 0) LOG_WRN("chmod(\"" << path << "\") failed: " << strerror(errno)); } void setupChildRoot(bool bindMount, const std::string& childRoot, const std::string& sysTemplate) { // Start with a clean slate. cleanupJails(childRoot); createJailPath(childRoot + CHILDROOT_TMP_INCOMING_PATH); disableBindMounting(); // Clear to avoid surprises. // Try to enable bind-mounting if requested (via config). if (bindMount) { // Test mounting to verify it actually works, // as it might not function in some systems. const std::string target = Poco::Path(childRoot, "cool_test_mount").toString(); // Make sure that we can both mount and unmount before enabling bind-mounting. if (bind(sysTemplate, target) && unmount(target)) { enableBindMounting(); safeRemoveDir(target); LOG_INF("Enabling Bind-Mounting of jail contents for better performance per " "mount_jail_tree config in coolwsd.xml."); } else LOG_ERR("Bind-Mounting fails and will be disabled for this run. To disable permanently " "set mount_jail_tree config entry in coolwsd.xml to false."); } else LOG_INF("Disabling Bind-Mounting of jail contents per " "mount_jail_tree config in coolwsd.xml."); } /// Create a random device, either via mknod or by bind-mounting. bool createRandomDeviceInJail(const std::string& root, const std::string& devicePath, dev_t dev) { const std::string absPath = root + devicePath; if (FileUtil::Stat(absPath).exists()) { LOG_DBG("Random device [" << devicePath << "] already exits"); return true; } LOG_DBG("Making [" << devicePath << "] node in [" << root << "/dev]"); if (mknod((absPath).c_str(), S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, dev) == 0) { LOG_DBG("Created random device [" << absPath << ']'); return true; } const auto mknodErrno = errno; if (isBindMountingEnabled()) { static bool warned = false; if (!warned) { warned = true; LOG_WRN("Performance issue: nodev mount permission or mknod fails. Have to bind mount " "random devices"); } Poco::File(absPath).createFile(); if (coolmount("-b", devicePath, absPath)) { LOG_DBG("Bind mounted [" << devicePath << "] -> [" << absPath << ']'); return true; } LOG_INF("Failed to bind mount [" << devicePath << "] -> [" << absPath << ']'); } else { LOG_INF("Failed to create random device via mknod(" << absPath << "). Mount must not use nodev flag, or bind-mount must be enabled: " << strerror(mknodErrno)); } static bool warned = false; if (!warned) { warned = true; LOG_ERR("Failed to create random device [" << devicePath << "] at [" << absPath << "]. Please either allow creating devices or enable bind-mounting. Some " "features, such us password-protection and document-signing, might not work"); } return false; } // This is the second stage of setting up /dev/[u]random // in the jails. Here we create the random devices in // /tmp/dev/ in the jail chroot. See setupRandomDeviceLinks(). void setupJailDevNodes(const std::string& root) { if (!FileUtil::isWritable(root)) { LOG_WRN("Path [" << root << "] is read-only. Will not create the random device nodes."); return; } const auto pathDev = Poco::Path(root, "/dev"); try { // Create the path first. Poco::File(pathDev).createDirectory(); } catch (const std::exception& ex) { LOG_ERR("Failed to create [" << pathDev.toString() << "]: " << ex.what()); return; } #ifndef __FreeBSD__ // Create the random and urandom devices. createRandomDeviceInJail(root, "/dev/random", makedev(1, 8)); createRandomDeviceInJail(root, "/dev/urandom", makedev(1, 9)); #else if (!FileUtil::Stat(root + "/dev/random").exists()) { const bool res = coolmount("-d", "", root + "/dev"); if (res) LOG_TRC("Mounted devfs hierarchy -> [" << root << "/dev]."); else LOG_ERR("Failed to mount devfs -> [" << root << "/dev]."); } #endif } /// The envar name used to control bind-mounting of systemplate/jails. constexpr const char* BIND_MOUNTING_ENVAR_NAME = "COOL_BIND_MOUNT"; void enableBindMounting() { // Set the envar to enable. setenv(BIND_MOUNTING_ENVAR_NAME, "1", 1); } void disableBindMounting() { // Remove the envar to disable. unsetenv(BIND_MOUNTING_ENVAR_NAME); } bool isBindMountingEnabled() { // Check if we have a valid envar set. return std::getenv(BIND_MOUNTING_ENVAR_NAME) != nullptr; } namespace SysTemplate { /// The network and other system files we need to keep up-to-date in jails. /// These must be up-to-date, as they can change during /// the long lifetime of our process. Also, it's unlikely /// that systemplate will get re-generated after installation. static const auto DynamicFilePaths = { "/etc/passwd", "/etc/group", "/etc/host.conf", "/etc/hosts", "/etc/nsswitch.conf", "/etc/resolv.conf", "/etc/timezone", "/etc/localtime" }; /// Copy (false) by default for KIT_IN_PROCESS. static bool LinkDynamicFiles = false; static bool updateDynamicFilesImpl(const std::string& sysTemplate); void setupDynamicFiles(const std::string& sysTemplate) { LOG_INF("Setting up systemplate dynamic files in [" << sysTemplate << "]."); const std::string etcSysTemplatePath = Poco::Path(sysTemplate, "etc").toString(); LinkDynamicFiles = true; // Prefer linking, unless it fails. if (!updateDynamicFilesImpl(sysTemplate)) { // Can't copy! LOG_WRN("Failed to update the dynamic files in [" << sysTemplate << "]. Will disable bind-mounting in this run and clone systemplate into the " "jails, which is more resource intensive."); disableBindMounting(); // We can't mount from incomplete systemplate. LinkDynamicFiles = false; } FileUtil::Stat copiedFileStat(Poco::Path(sysTemplate, "etc/copied").toString()); if (copiedFileStat.exists()) { // At least one file is copied, we must check for changes before each jail setup. LinkDynamicFiles = false; } LOG_INF("Systemplate dynamic files in [" << sysTemplate << "] " << (LinkDynamicFiles ? "are linked and will remain" : "will be copied to keep them") << " up-to-date."); } bool updateDynamicFilesImpl(const std::string& sysTemplate) { LOG_INF("Updating systemplate dynamic files in [" << sysTemplate << ']'); bool checkWritableSysTemplate = true; for (const auto& dynFilename : DynamicFilePaths) { if (!FileUtil::Stat(dynFilename).exists()) { LOG_INF("Dynamic file [" << dynFilename << "] does not exist. Some functionality may be affected."); continue; } const std::string srcFilename = FileUtil::realpath(dynFilename); if (srcFilename != dynFilename) { LOG_TRC("Dynamic file [" << dynFilename << "] points to real path [" << srcFilename << "], which will be used instead."); } const Poco::File srcFilePath(srcFilename); FileUtil::Stat srcStat(srcFilename); if (!srcStat.exists()) continue; const std::string dstFilename = Poco::Path(sysTemplate, dynFilename).toString(); FileUtil::Stat dstStat(dstFilename); // Is it outdated? if (dstStat.isUpToDate(srcStat)) { LOG_TRC("File [" << dstFilename << "] is already up-to-date."); continue; } if (checkWritableSysTemplate && !FileUtil::isWritable(sysTemplate)) { disableBindMounting(); // We can't mount from incomplete systemplate that can't be updated. LinkDynamicFiles = false; LOG_WRN("The systemplate directory [" << sysTemplate << "] is read-only, and at least [" << dstFilename << "] is out-of-date. Will have to copy sysTemplate to jails. To restore " "optimal performance, make sure the files in [" << sysTemplate << "/etc] are up-to-date."); return false; } checkWritableSysTemplate = false; // We've checked and is writable. LOG_INF("File [" << dstFilename << "] needs to be updated."); if (LinkDynamicFiles) { LOG_INF("Linking [" << srcFilename << "] -> [" << dstFilename << "]."); // Link or copy. if (link(srcFilename.c_str(), dstFilename.c_str()) == 0) continue; // Hard-linking failed, try symbolic linking. if (symlink(srcFilename.c_str(), dstFilename.c_str()) == 0) continue; const int linkerr = errno; // With parallel tests, another test might have linked already. FileUtil::Stat dstStat2(dstFilename); if (dstStat2.isUpToDate(srcStat)) { LOG_INF("File [" << dstFilename << "] now seems to be up-to-date."); continue; } // Failed to link a file. Disable linking and copy instead. LOG_WRN("Failed to link [" << srcFilename << "] -> [" << dstFilename << "] (" << strerror(linkerr) << "). Will copy and disable linking dynamic system files in this run."); LinkDynamicFiles = false; } // Linking failed, just copy. if (!LinkDynamicFiles) { LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilename << ']'); if (!FileUtil::copyAtomic(srcFilename, dstFilename, true)) { FileUtil::Stat dstStat2(dstFilename); // Stat again. if (!dstStat2.isUpToDate(srcStat)) { return false; // No point in trying the remaining files. } } // Create the 'copied' file so we keep the files up-to-date. Poco::File(Poco::Path(sysTemplate, "etc/copied").toString()).createFile(); } } return true; } bool updateDynamicFiles(const std::string& sysTemplate) { // If the files are linked, they are always up-to-date. return LinkDynamicFiles ? true : updateDynamicFilesImpl(sysTemplate); } void setupRandomDeviceLink(const std::string& sysTemplate, const std::string& name) { const std::string path = sysTemplate + "/dev/"; try { // Create the path first. Poco::File(path).createDirectories(); } catch (const std::exception& ex) { LOG_ERR("Failed to create [" << path << "]: " << ex.what()); return; } const std::string linkpath = path + name; const std::string target = "../tmp/dev/" + name; LOG_DBG("Linking symbolically [" << linkpath << "] to [" << target << "]."); const FileUtil::Stat stLink(linkpath, true); // The file is a link. if (stLink.exists()) { if (!stLink.isLink()) LOG_WRN("Random device link [" << linkpath << "] exists but isn't a link."); else LOG_TRC("Random device link [" << linkpath << "] already exists."); return; } if (symlink(target.c_str(), linkpath.c_str()) == -1) { LOG_SYS( "Failed to create symlink to [" << name << "] device at [" << target << "] pointing to source [" << linkpath << "]. Some features, such us password-protection and document-signing might not work"); } } // The random devices are setup in two stages. // This is the first stage, where we create symbolic links // in sysTemplate/dev/[u]random pointing to ../tmp/dev/[u]random // when we setup sysTemplate in forkit. // In the second stage, during jail creation, we create the dev // nodes in /tmp/dev/[u]random inside the jail chroot. void setupRandomDeviceLinks(const std::string& sysTemplate) { setupRandomDeviceLink(sysTemplate, "random"); setupRandomDeviceLink(sysTemplate, "urandom"); } } // namespace SysTemplate } // namespace JailUtil /* vim:set shiftwidth=4 softtabstop=4 expandtab: */