summaryrefslogtreecommitdiff
path: root/common/JailUtil.cpp
blob: 7b313d4706448ed4229103c7dd22d9a3d374a8f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
/* -*- 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: */