diff options
author | Robert Griebl <robert.griebl@pelagicore.com> | 2015-10-20 11:31:13 +0200 |
---|---|---|
committer | Robert Griebl <robert.griebl@pelagicore.com> | 2015-10-26 09:46:22 +0000 |
commit | 2491a2ba4b7c4bdc04566df84e1b6841c1393bb3 (patch) | |
tree | 8d87a4534b1d8abbaf6e358929b969168901304c /tests/sudo/tst_sudo.cpp | |
parent | 0c9e6ac0803ef00401ddfd08dc7558c20eefd0c4 (diff) |
Initial commit of Pelagicore's ApplicationManager with squashed history.
(minus all 3rd party stuff - we will re-add them later, potentially
changing the way some of them are handled completely)
Change-Id: I55f15b12f0dbe0c4f979206eaa163d4793ff7073
Reviewed-by: Lars Knoll <lars.knoll@theqtcompany.com>
Reviewed-by: Alex Blasche <alexander.blasche@theqtcompany.com>
Diffstat (limited to 'tests/sudo/tst_sudo.cpp')
-rw-r--r-- | tests/sudo/tst_sudo.cpp | 520 |
1 files changed, 520 insertions, 0 deletions
diff --git a/tests/sudo/tst_sudo.cpp b/tests/sudo/tst_sudo.cpp new file mode 100644 index 00000000..86537d2e --- /dev/null +++ b/tests/sudo/tst_sudo.cpp @@ -0,0 +1,520 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Pelagicore AG +** Contact: http://www.pelagicore.com/ +** +** This file is part of the Pelagicore Application Manager. +** +** SPDX-License-Identifier: GPL-3.0 +** +** $QT_BEGIN_LICENSE:GPL3$ +** Commercial License Usage +** Licensees holding valid commercial Pelagicore Application Manager +** licenses may use this file in accordance with the commercial license +** agreement provided with the Software or, alternatively, in accordance +** with the terms contained in a written agreement between you and +** Pelagicore. For licensing terms and conditions, contact us at: +** http://www.pelagicore.com. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3 requirements will be +** met: http://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtTest> +#include <qplatformdefs.h> + +#if !defined(Q_OS_LINUX) +# error "This test is Linux specific!" +#endif + +#include <linux/loop.h> +#include <sys/ioctl.h> + +#include "utilities.h" +#include "sudo.h" + +#include "../sudo-cleanup.h" + + +static bool startedSudoServer = false; +static QString sudoServerError; + +#define LOSETUP "/sbin/losetup" +#define MOUNT "/bin/mount" +#define UMOUNT "/bin/umount" +#define BLKID "/sbin/blkid" + +// sudo RAII style +class ScopedRootPrivileges +{ +public: + ScopedRootPrivileges() + { + m_uid = getuid(); + m_gid = getgid(); + if (setresuid(0, 0, 0) || setresgid(0, 0, 0)) + QFAIL("cannot re-gain root privileges"); + } + ~ScopedRootPrivileges() + { + if (setresgid(m_gid, m_gid, 0) || setresuid(m_uid, m_uid, 0)) + QFAIL("cannot drop root privileges"); + } +private: + uid_t m_uid; + gid_t m_gid; +}; + +#define SUB_QVERIFY2(statement, description) \ +do {\ + if (statement) {\ + if (!QTest::qVerify(true, #statement, (description), __FILE__, __LINE__))\ + return false;\ + } else {\ + if (!QTest::qVerify(false, #statement, (description), __FILE__, __LINE__))\ + return false;\ + }\ +} while (0) + + +class ScopedLoopbackManager +{ +public: + enum CreateMode + { + CreateFromLoopbackDevice, + CreateFromImageFile + }; + + ScopedLoopbackManager() + { } + + bool create(CreateMode mode, const QString &argument) + { + if (mode == CreateFromLoopbackDevice) { + m_loopbackDevice = argument; + } else if (mode == CreateFromImageFile) { + SUB_QVERIFY2(!argument.isEmpty(), ""); + + ScopedRootPrivileges sudo; + + QProcess p; + p.start(LOSETUP, { "--show", "-f", argument }); + SUB_QVERIFY2(p.waitForStarted(3000), qPrintable(p.errorString())); + SUB_QVERIFY2(p.waitForFinished(3000), qPrintable(p.errorString())); + SUB_QVERIFY2(p.exitCode() == 0, qPrintable(p.readAllStandardError())); + m_loopbackDevice = p.readAllStandardOutput().trimmed(); + } + SUB_QVERIFY2(m_loopbackDevice.startsWith("/dev/loop"), qPrintable(m_loopbackDevice)); + return true; + } + + void detach() + { + m_loopbackDevice.clear(); + } + + QString loopbackDevice() + { + return m_loopbackDevice; + } + + ~ScopedLoopbackManager() + { + if (!m_loopbackDevice.isEmpty()) { + ScopedRootPrivileges sudo; + + QProcess p; + + for (int tries = 2; tries; --tries) { + p.start(LOSETUP, { "-d", m_loopbackDevice }); + QVERIFY2(p.waitForStarted(3000), qPrintable(p.errorString())); + QVERIFY2(p.waitForFinished(3000), qPrintable(p.errorString())); + if (p.exitCode() == 0) + return; + + QTest::qWait(200); + } + QFAIL(qPrintable(p.readAllStandardError())); + } + } + + +private: + QString m_loopbackDevice; +}; + +class ScopedMountManager +{ +public: + enum CreateMode + { + CreateFromUnmounted, + CreateFromMounted + }; + + ScopedMountManager() + { } + + bool create(CreateMode mode, const QString &device, const QString &mountPoint) + { + m_device = device; + m_mountPoint = mountPoint; + + SUB_QVERIFY2(QFile::exists(device), ""); + SUB_QVERIFY2(QDir(mountPoint).exists(), ""); + + if (mode == CreateFromUnmounted) { + ScopedRootPrivileges sudo; + + QProcess p; + p.start(MOUNT, { "-t", "ext2", device, mountPoint}); + SUB_QVERIFY2(p.waitForStarted(3000), qPrintable(p.errorString())); + SUB_QVERIFY2(p.waitForFinished(3000), qPrintable(p.errorString())); + SUB_QVERIFY2(p.exitCode() == 0, qPrintable(p.readAllStandardError())); + } + return m_mounted = true; + } + + void detach() + { + m_mounted = false; + } + + ~ScopedMountManager() + { + if (m_mounted) { + ScopedRootPrivileges sudo; + + QProcess p; + for (int tries = 2; tries; --tries) { + p.start(UMOUNT, { "-lf", m_mountPoint }); + QVERIFY2(p.waitForStarted(3000), qPrintable(p.errorString())); + QVERIFY2(p.waitForFinished(3000), qPrintable(p.errorString())); + if (p.exitCode() == 0) + return; + + QTest::qWait(200); + } + QFAIL(qPrintable(p.readAllStandardError())); + } + } + + bool checkKernelMountStatus() + { + return mountedDirectories().values(m_mountPoint).contains(m_device); + } + +private: + QString m_device; + QString m_mountPoint; + bool m_mounted = false; +}; + +class tst_Sudo : public QObject +{ + Q_OBJECT + +public: + tst_Sudo(QObject *parent = 0); + ~tst_Sudo(); + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void loopback_data(); + void loopback(); + void mount_data(); + void mount(); + void unmount_data(); + void unmount(); + void checkMountPoints(); // not really part of Sudo, but can be ideally tested here + void image(); + +private: + QString pathTo(const QString &file) + { + return QDir(m_workDir.path()).absoluteFilePath(file); + } + + SudoClient *m_root = 0; + TemporaryDir m_workDir; +}; + +tst_Sudo::tst_Sudo(QObject *parent) + : QObject(parent) +{ } + +tst_Sudo::~tst_Sudo() +{ + if (m_workDir.isValid()) { + ScopedRootPrivileges sudo; + + detachLoopbacksAndUnmount(0, m_workDir.path()); + recursiveOperation(m_workDir.path(), SafeRemove()); + } +} + +void tst_Sudo::initTestCase() +{ + QVERIFY(QFile::exists(LOSETUP)); + QVERIFY(QFile::exists(MOUNT)); + QVERIFY(QFile::exists(UMOUNT)); + QVERIFY(QFile::exists(BLKID)); + + QVERIFY2(startedSudoServer, qPrintable(sudoServerError)); + m_root = SudoClient::instance(); + QVERIFY(m_root); + + QVERIFY(m_workDir.isValid()); + + for (int i = 0; i < 3; ++i) { + QVERIFY(QDir(m_workDir.path()).mkdir(QString::fromLatin1("mp-%1").arg(i))); + QVERIFY(QFile::copy(AM_TESTDATA_DIR "ext2image", QString::fromLatin1("%1/img-%2") + .arg(m_workDir.path()).arg(i))); + } +} + +void tst_Sudo::cleanupTestCase() +{ + // the real cleanup happens in ~tst_Installer, since we also need + // to call this cleanup from the crash handler +} + + +void tst_Sudo::loopback_data() +{ + QTest::addColumn<QString>("imageFile"); + QTest::addColumn<bool>("readOnly"); + QTest::addColumn<bool>("valid"); + + QTest::newRow("invalid-image") << pathTo("invalid") << false << false; + QTest::newRow("read-write") << pathTo("img-0") << false << true; + QTest::newRow("read-only") << pathTo("img-0") << true << true; +} + +void tst_Sudo::loopback() +{ + QFETCH(QString, imageFile); + QFETCH(bool, readOnly); + QFETCH(bool, valid); + + QString loopbackDevice = m_root->attachLoopback(imageFile, readOnly); + QVERIFY2(valid == !loopbackDevice.isEmpty(), qPrintable(m_root->lastError())); + if (!loopbackDevice.isEmpty()) { + ScopedLoopbackManager slm; + QVERIFY(slm.create(ScopedLoopbackManager::CreateFromLoopbackDevice, loopbackDevice)); + + if (valid) { + ScopedRootPrivileges sudo; + + struct loop_info64 loopInfo64; + QFile f(loopbackDevice); + QVERIFY(f.open(QFile::ReadOnly)); + QVERIFY2(::ioctl(f.handle(), LOOP_GET_STATUS64, &loopInfo64) == 0, strerror(errno)); + f.close(); + + QTest::qWait(200); + + QVERIFY2(m_root->detachLoopback(loopbackDevice), qPrintable(m_root->lastError())); + slm.detach(); + QVERIFY(!QFile::exists(slm.loopbackDevice())); + } + } +} + + +void tst_Sudo::mount_data() +{ + QTest::addColumn<QString>("imageFile"); + QTest::addColumn<QString>("mountPoint"); + QTest::addColumn<bool>("readOnly"); + + QTest::newRow("missing-mountpoint") << pathTo("img-0") << "" << false; + QTest::newRow("read-only") << pathTo("img-0") << pathTo("mp-0") << true; + QTest::newRow("read-write") << pathTo("img-0") << pathTo("mp-0") << false; +} + +void tst_Sudo::mount() +{ + QFETCH(QString, imageFile); + QFETCH(QString, mountPoint); + QFETCH(bool, readOnly); + + ScopedLoopbackManager slm; + QVERIFY(slm.create(ScopedLoopbackManager::CreateFromImageFile, imageFile)); + QString loopbackDevice = slm.loopbackDevice(); + QVERIFY(imageFile.isEmpty() == loopbackDevice.isEmpty()); + + bool mountOk = m_root->mount(loopbackDevice, mountPoint, readOnly); + + if (imageFile.isEmpty() || mountPoint.isEmpty()) { + QVERIFY(!mountOk); + } else { + QVERIFY2(mountOk, qPrintable(m_root->lastError())); + + ScopedMountManager smm; + QVERIFY(smm.create(ScopedMountManager::CreateFromMounted, loopbackDevice, mountPoint)); + QVERIFY(smm.checkKernelMountStatus()); + + { + ScopedRootPrivileges sudo; + + QFile file(mountPoint + "/write-test"); + bool canOpen = file.open(QIODevice::WriteOnly); + QCOMPARE(readOnly, !canOpen); + if (!readOnly) { + QVERIFY(canOpen); + QCOMPARE(file.write("foobar"), 6); + file.close(); + QCOMPARE(file.size(), 6); + QVERIFY(file.remove()); + } + } + } +} + + +void tst_Sudo::unmount_data() +{ + QTest::addColumn<bool>("openFile"); + QTest::addColumn<bool>("forceUnmount"); + QTest::addColumn<bool>("canUnmount"); + + QTest::newRow("normal-no-force") << false << false << true; + QTest::newRow("normal-force") << false << true << true; + QTest::newRow("file-no-force") << true << false << false; + QTest::newRow("file-force") << true << true << true; +} + +void tst_Sudo::unmount() +{ + QFETCH(bool, openFile); + QFETCH(bool, forceUnmount); + QFETCH(bool, canUnmount); + + ScopedLoopbackManager slm; + QVERIFY(slm.create(ScopedLoopbackManager::CreateFromImageFile, pathTo("img-0"))); + QVERIFY(!slm.loopbackDevice().isEmpty()); + ScopedMountManager smm; + QVERIFY(smm.create(ScopedMountManager::CreateFromUnmounted, slm.loopbackDevice(), pathTo("mp-0"))); + + QFile file(pathTo("mp-0/force-lock")); + if (openFile) { + ScopedRootPrivileges sudo; + + QVERIFY(file.open(QFile::WriteOnly)); + } + QTest::qWait(200); + + bool ok = m_root->unmount(pathTo("mp-0"), forceUnmount); + if (ok) + smm.detach(); + QVERIFY2(ok == canUnmount, qPrintable(m_root->lastError())); + QCOMPARE(!smm.checkKernelMountStatus(), canUnmount); +} + + +void tst_Sudo::checkMountPoints() +{ + ScopedLoopbackManager slm[3]; + + for (int i = 0; i < 3; ++i) { + QVERIFY(slm[i].create(ScopedLoopbackManager::CreateFromImageFile, pathTo(QString::fromLatin1("img-%1").arg(i)))); + QVERIFY(!slm[i].loopbackDevice().isEmpty()); + } + // we are mounting both img-1 and img-2 to mp-2 on purpose here! + ScopedMountManager smm[3]; + + QVERIFY(smm[0].create(ScopedMountManager::CreateFromUnmounted, slm[0].loopbackDevice(), pathTo("mp-1"))); + QVERIFY(smm[1].create(ScopedMountManager::CreateFromUnmounted, slm[1].loopbackDevice(), pathTo("mp-2"))); + QVERIFY(smm[2].create(ScopedMountManager::CreateFromUnmounted, slm[2].loopbackDevice(), pathTo("mp-2"))); + + // map mount-points to loopback devices + QList<int> mountPointList[3] = { { }, { 0 }, { 1, 2 } }; + + for (int i = 0; i < 3; ++i) + QVERIFY(smm[i].checkKernelMountStatus()); + + QMultiMap<QString, QString> allMounts = mountedDirectories(); + + for (int i = 0; i < 3; ++i) { + QString mountPoint = pathTo(QString::fromLatin1("mp-%1").arg(i)); + QStringList mounts = allMounts.values(mountPoint); + + QCOMPARE(mounts.count(), mountPointList[i].size()); + foreach (int index, mountPointList[i]) + QVERIFY(mounts.contains(slm[index].loopbackDevice())); + } +} + + +void tst_Sudo::image() +{ + QString imageFile = pathTo("img-new"); + + QFile f(imageFile); + f.remove(); + QVERIFY(!f.exists()); + QVERIFY2(f.open(QFile::WriteOnly | QFile::Truncate), qPrintable(f.errorString())); + QVERIFY2(f.resize(1024*1024), qPrintable(f.errorString())); + f.close(); + QCOMPARE(f.size(), qint64(1024*1024)); + + { + ScopedLoopbackManager slm; + QVERIFY(slm.create(ScopedLoopbackManager::CreateFromImageFile, imageFile)); + + QVERIFY2(m_root->mkfs(slm.loopbackDevice()), qPrintable(m_root->lastError())); + + ScopedRootPrivileges sudo; + + QProcess p; + p.start(BLKID, { "-s", "TYPE", "-o", "value", slm.loopbackDevice() }); + QVERIFY2(p.waitForStarted(3000), qPrintable(p.errorString())); + QVERIFY2(p.waitForFinished(3000), qPrintable(p.errorString())); + QVERIFY(p.exitCode() == 0); + QCOMPARE(p.readAllStandardOutput().trimmed(), QByteArray("ext2")); + } + QVERIFY(QFile::remove(imageFile)); +} + +static tst_Sudo *tstSudo = 0; + +int main(int argc, char **argv) +{ + startedSudoServer = forkSudoServer(DropPrivilegesRegainable, &sudoServerError); + + QCoreApplication a(argc, argv); + tstSudo = new tst_Sudo(&a); + +#ifdef Q_OS_LINUX + auto crashHandler = [](int sigNum) -> void { + // we are doing very unsafe things from a within a signal handler, but + // we've crashed anyway at this point and the alternative is that we are + // leaking mounts and attached loopback devices. + + tstSudo->~tst_Sudo(); + + if (sigNum != -1) + exit(1); + }; + + signal(SIGABRT, crashHandler); + signal(SIGSEGV, crashHandler); + signal(SIGINT, crashHandler); +#endif // Q_OS_LINUX + + return QTest::qExec(tstSudo, argc, argv); +} + +#include "tst_sudo.moc" |