libreoffice-online/common/JailUtil.cpp
Ashod Nakashian 29a5a1f1e9 wsd: move jail setup to the script to support readonly systemplate
We now gracefully fallback to copying when/if systemplate
is readonly.

The bulk of the change is to support proper cleanup in
both cases.

First, we had to move as much of the jail bootstrapping
into the loolwsd-systemplate-setup script, so systemplate
will be as complete as possible before it is locked down.
Next, we needed to update the jail with graceful fallback
to linking/copying upon failure. For that, the jail setup
logic in Kit.cpp has been reworked to support not just
update failures, but also more comprehensive mounting
failures as well.

Finally, jail cleanup now is seamless. To support proper
cleanup when we had mounting enabled but had to fallback,
we mark jails that aren't mounted so we can 'rm -rf' the
contents safely and without fear or causing undue damage
(as unlikely as that is, technically we wouldn't want to
rm systemplate files, if mounting read-only had failed).

There are a few minor refactorings of JailUtil to make
it cleaner and more robust.

Change-Id: Iac34869cb84f45acf64fbbc46d46898367b496d2
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/101260
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Andras Timar <andras.timar@collabora.com>
2020-08-25 07:58:30 +02:00

419 lines
14 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This file is part of the LibreOffice project.
*
* 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 <config.h>
#include "FileUtil.hpp"
#include "JailUtil.hpp"
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#ifdef __linux
#include <sys/sysmacros.h>
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "Log.hpp"
namespace JailUtil
{
bool loolmount(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(), "loolmount").toString() + ' '
+ arg + ' ' + source + ' ' + target;
LOG_TRC("Executing loolmount command: " << cmd);
return !system(cmd.c_str());
}
bool bind(const std::string& source, const std::string& target)
{
Poco::File(target).createDirectory();
const bool res = loolmount("-b", source, target);
if (res)
LOG_TRC("Bind-mounted [" << source << "] -> [" << target << "].");
else
LOG_ERR("Failed to bind-mount [" << source << "] -> [" << target << "].");
return res;
}
bool remountReadonly(const std::string& source, const std::string& target)
{
Poco::File(target).createDirectory();
const bool res = loolmount("-r", source, target);
if (res)
LOG_TRC("Mounted [" << source << "] -> [" << target << "] readonly.");
else
LOG_ERR("Failed to mount [" << source << "] -> [" << target << "] readonly.");
return res;
}
bool unmount(const std::string& target)
{
LOG_DBG("Unmounting [" << target << "].");
const bool res = loolmount("-u", "", target);
if (res)
LOG_TRC("Unmounted [" << target << "] successfully.");
else
LOG_ERR("Failed to unmount [" << target << "].");
return res;
}
// This file signifies that we copied instead of mounted.
// NOTE: jail cleanup helpers are called from forkit and
// loolwsd, 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, we if 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();
}
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;
FileUtil::removeFile(path, recursive);
return true;
}
void removeJail(const std::string& root)
{
LOG_INF("Removing jail [" << root << "].");
// Unmount the tmp directory. Don't care if we fail.
const std::string tmpPath = Poco::Path(root, "tmp").toString();
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/delete the jail (sysTemplate).
safeRemoveDir(root);
}
/// 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 directory or doesn't exist.");
return;
}
//FIXME: technically, the loTemplate directory may have any name.
if (FileUtil::Stat(root + "/lo").exists())
{
// This is a jail.
removeJail(root);
}
else
{
// Not a jail, recurse. UnitTest creates sub-directories.
LOG_TRC("Directory [" << root << "] is not a jail, recursing.");
std::vector<std::string> jails;
Poco::File(root).list(jails);
for (const auto& jail : jails)
{
const Poco::Path path(root, jail);
if (jail == "tmp") // Delete tmp with prejeduce.
FileUtil::removeFile(path.toString(), true);
else
cleanupJails(path.toString());
}
}
// Remove empty directories.
if (FileUtil::isEmptyDirectory(root))
safeRemoveDir(root);
else
LOG_WRN("Jails root directory [" << root << "] is not empty. Will not remove it.");
}
void setupJails(bool bindMount, const std::string& jailRoot, const std::string& sysTemplate)
{
// Start with a clean slate.
cleanupJails(jailRoot);
Poco::File(jailRoot).createDirectories();
disableBindMounting(); // Clear to avoid surprises.
if (bindMount)
{
// Test mounting to verify it actually works,
// as it might not function in some systems.
const std::string target = Poco::Path(jailRoot, "lool_test_mount").toString();
if (bind(sysTemplate, target))
{
enableBindMounting();
safeRemoveDir(target);
LOG_INF("Enabling Bind-Mounting of jail contents for better performance per "
"mount_jail_tree config in loolwsd.xml.");
}
else
LOG_ERR("Bind-Mounting fails and will be disabled for this run. To disable permanently "
"set mount_jail_tree config entry in loolwsd.xml to false.");
}
else
LOG_INF("Disabling Bind-Mounting of jail contents per "
"mount_jail_tree config in loolwsd.xml.");
}
// 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)
{
// Create the urandom and random devices
Poco::File(Poco::Path(root, "/dev")).createDirectory();
if (!Poco::File(root + "/dev/random").exists())
{
LOG_DBG("Making /dev/random node in [" << root << "/dev].");
if (mknod((root + "/dev/random").c_str(),
S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH,
makedev(1, 8))
!= 0)
{
LOG_SYS("mknod(" << root << "/dev/random) failed. Mount must not use nodev flag.");
}
}
if (!Poco::File(root + "/dev/urandom").exists())
{
LOG_DBG("Making /dev/urandom node in [" << root << "/dev].");
if (mknod((root + "/dev/urandom").c_str(),
S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH,
makedev(1, 9))
!= 0)
{
LOG_SYS("mknod(" << root << "/dev/urandom) failed. Mount must not use nodev flag.");
}
}
}
/// The envar name used to control bind-mounting of systemplate/jails.
constexpr const char* BIND_MOUNTING_ENVAR_NAME = "LOOL_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;
}
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 << "].");
for (const auto& srcFilename : DynamicFilePaths)
{
const Poco::File srcFilePath(srcFilename);
FileUtil::Stat srcStat(srcFilename);
if (!srcStat.exists())
continue;
const std::string dstFilename = Poco::Path(sysTemplate, srcFilename).toString();
FileUtil::Stat dstStat(dstFilename);
// Is it outdated?
if (dstStat.isUpToDate(srcStat))
{
LOG_INF("File [" << dstFilename << "] is already up-to-date.");
continue;
}
LOG_INF("File [" << dstFilename << "] needs to be updated.");
if (LinkDynamicFiles)
{
LOG_INF("Linking [" << srcFilename << "] -> [" << dstFilename << "].");
// Link or copy.
if (link(srcFilename, dstFilename.c_str()) == 0)
continue;
// Hard-linking failed, try symbolic linking.
if (symlink(srcFilename, 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.
}
}
}
}
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/";
Poco::File(path).createDirectories();
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 symlink(\"" << target << "\", \"" << linkpath << "\")");
}
// 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: */