diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,16 @@ ) option(PAM_REQUIRED "Require building with PAM" ON) + +find_package(Seccomp) +set_package_properties(Seccomp PROPERTIES + TYPE + OPTIONAL + PURPOSE + "Used for putting the look'n'feel package in the greeter into a sandbox." +) +set(HAVE_SECCOMP ${Seccomp_FOUND}) + include(ConfigureChecks.cmake) configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) diff --git a/cmake/FindSeccomp.cmake b/cmake/FindSeccomp.cmake new file mode 100644 --- /dev/null +++ b/cmake/FindSeccomp.cmake @@ -0,0 +1,80 @@ +#============================================================================= +# Copyright 2017 Martin Gräßlin +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindSeccomp.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindSeccomp.cmake") +endif() + +find_package(PkgConfig) +pkg_check_modules(PKG_Libseccomp QUIET libseccomp) + +set(Seccomp_DEFINITIONS ${PKG_Libseccomp_CFLAGS_OTHER}) +set(Seccomp_VERSION ${PKG_Libseccomp_VERSION}) + +find_path(Seccomp_INCLUDE_DIR + NAMES + seccomp.h + HINTS + ${PKG_Libseccomp_INCLUDE_DIRS} +) +find_library(Seccomp_LIBRARY + NAMES + seccomp + HINTS + ${PKG_Libseccomp_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Seccomp + FOUND_VAR + Seccomp_FOUND + REQUIRED_VARS + Seccomp_LIBRARY + Seccomp_INCLUDE_DIR + VERSION_VAR + Seccomp_VERSION +) + +if (Seccomp_FOUND AND NOT TARGET Seccomp::Seccomp) + add_library(Seccomp::Seccomp UNKNOWN IMPORTED) + set_target_properties(Seccomp::Seccomp PROPERTIES + IMPORTED_LOCATION "${Seccomp_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Seccomp_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Seccomp_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Seccomp_LIBRARY Seccomp_INCLUDE_DIR) + +include(FeatureSummary) +set_package_properties(Seccomp PROPERTIES + URL "https://github.com/seccomp/libseccomp" + DESCRIPTION "The enhanced seccomp library." +) diff --git a/config-kscreenlocker.h.cmake b/config-kscreenlocker.h.cmake --- a/config-kscreenlocker.h.cmake +++ b/config-kscreenlocker.h.cmake @@ -12,3 +12,4 @@ #cmakedefine01 HAVE_PR_SET_DUMPABLE #cmakedefine01 HAVE_SYS_PROCCTL_H #cmakedefine01 HAVE_PROC_TRACE_CTL +#cmakedefine01 HAVE_SECCOMP diff --git a/greeter/CMakeLists.txt b/greeter/CMakeLists.txt --- a/greeter/CMakeLists.txt +++ b/greeter/CMakeLists.txt @@ -15,6 +15,10 @@ wallpaper_integration.cpp ) +if(HAVE_SECCOMP) + set(kscreenlocker_greet_SRCS ${kscreenlocker_greet_SRCS} seccomp_filter.cpp) +endif() + qt5_add_resources(kscreenlocker_greet_SRCS fallbacktheme.qrc) kconfig_add_kcfg_files(kscreenlocker_greet_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../kcfg/kscreensaversettings.kcfgc) @@ -40,6 +44,9 @@ KF5::WaylandClient Wayland::Client ) +if(HAVE_SECCOMP) + target_link_libraries(kscreenlocker_greet Qt5::DBus Seccomp::Seccomp) +endif() install(TARGETS kscreenlocker_greet DESTINATION ${KDE_INSTALL_LIBEXECDIR}) diff --git a/greeter/autotests/CMakeLists.txt b/greeter/autotests/CMakeLists.txt --- a/greeter/autotests/CMakeLists.txt +++ b/greeter/autotests/CMakeLists.txt @@ -26,3 +26,18 @@ add_executable(killTest killtest.cpp) ecm_mark_as_test(killTest) target_link_libraries(killTest Qt5::Test) + +####################################### +# Seccomp Test +####################################### +if(HAVE_SECCOMP) + add_executable(seccompTest seccomp_test.cpp ../seccomp_filter.cpp) + ecm_mark_as_test(seccompTest) + target_link_libraries(seccompTest + Qt5::Test + Qt5::Gui + Qt5::DBus + Qt5::Network + Seccomp::Seccomp + ) +endif() diff --git a/greeter/autotests/seccomp_test.cpp b/greeter/autotests/seccomp_test.cpp new file mode 100644 --- /dev/null +++ b/greeter/autotests/seccomp_test.cpp @@ -0,0 +1,106 @@ +/******************************************************************** + KSld - the KDE Screenlocker Daemon + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include +#include "../seccomp_filter.h" + +#include +#include +#include + +#include +#include +#include + +#include + +class SeccompTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testCreateFile(); + void testOpenFile(); + void testStartProcess(); + void testNetworkAccess_data(); + void testNetworkAccess(); +}; + +void SeccompTest::initTestCase() +{ + ScreenLocker::SecComp::init(); +} + +void SeccompTest::testCreateFile() +{ + QTemporaryFile file; + QVERIFY(!file.open()); +} + +void SeccompTest::testOpenFile() +{ + QFile file(QStringLiteral(KCHECKPASS_BIN)); + QVERIFY(file.exists()); + QVERIFY(!file.open(QIODevice::WriteOnly)); + QVERIFY(!file.open(QIODevice::ReadWrite)); + QVERIFY(file.open(QIODevice::ReadOnly)); +} + +void SeccompTest::testStartProcess() +{ + // QProcess fails already using pipe + QProcess p; + p.start(QStringLiteral(KCHECKPASS_BIN)); + QVERIFY(!p.waitForStarted()); + QCOMPARE(p.error(), QProcess::ProcessError::FailedToStart); + + // using glibc fork succeeds as it uses clone + // we don't forbid clone as it's needed to start a new thread + // so only test that exec fails + QCOMPARE(execl(KCHECKPASS_BIN, "fakekcheckpass", (char*)0), -1); + QCOMPARE(errno, EPERM); +} + +void SeccompTest::testNetworkAccess_data() +{ + QTest::addColumn("url"); + + // TODO: maybe resolve the IP addresses prior to installing seccomp? + QTest::newRow("domain") << QStringLiteral("https://www.kde.org"); + QTest::newRow("ip4") << QStringLiteral("http://91.189.93.5"); + // phabricator.kde.org + QTest::newRow("ip6") << QStringLiteral("http://[2a01:4f8:171:2687::4]/"); +} + +void SeccompTest::testNetworkAccess() +{ + QNetworkAccessManager manager; + QFETCH(QString, url); + QNetworkRequest request(url); + auto reply = manager.get(request); + QVERIFY(reply); + QSignalSpy finishedSpy(reply, &QNetworkReply::finished); + QVERIFY(finishedSpy.isValid()); + QVERIFY(finishedSpy.wait()); + QVERIFY(reply->error() != QNetworkReply::NoError); +} + + +QTEST_MAIN(SeccompTest) +#include "seccomp_test.moc" diff --git a/greeter/greeterapp.h b/greeter/greeterapp.h --- a/greeter/greeterapp.h +++ b/greeter/greeterapp.h @@ -67,6 +67,10 @@ void updateCanSuspend(bool set); void updateCanHibernate(bool set); + bool supportsSeccomp() const { + return m_supportsSeccomp; + } + public Q_SLOTS: void desktopResized(); @@ -86,6 +90,7 @@ void initializeWayland(); void shareEvent(QEvent *e, KQuickAddons::QuickViewSharedEngine *from); void loadWallpaperPlugin(KQuickAddons::QuickViewSharedEngine *view); + Authenticator *createAuthenticator(); QString m_packageName; QUrl m_mainQmlPath; @@ -112,6 +117,7 @@ KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; WallpaperIntegration *m_wallpaperIntegration; + bool m_supportsSeccomp = false; }; } // namespace diff --git a/greeter/greeterapp.cpp b/greeter/greeterapp.cpp --- a/greeter/greeterapp.cpp +++ b/greeter/greeterapp.cpp @@ -23,6 +23,7 @@ #include "authenticator.h" #include "noaccessnetworkaccessmanagerfactory.h" #include "wallpaper_integration.h" +#include // KDE #include @@ -67,6 +68,11 @@ // #include +#if HAVE_SECCOMP +#include +#include +#endif + // this is usable to fake a "screensaver" installation for testing // *must* be "0" for every public commit! #define TEST_SCREENSAVER 0 @@ -98,12 +104,12 @@ , m_testing(false) , m_ignoreRequests(false) , m_immediateLock(false) - , m_authenticator(new Authenticator(AuthenticationMode::Direct, this)) , m_graceTime(0) , m_noLock(false) , m_defaultToSwitchUser(false) , m_wallpaperIntegration(new WallpaperIntegration(this)) { + m_authenticator = createAuthenticator(); connect(m_authenticator, &Authenticator::succeeded, this, &QCoreApplication::quit); initialize(); connect(this, &UnlockApp::screenAdded, this, &UnlockApp::desktopResized); @@ -131,6 +137,19 @@ } } +Authenticator *UnlockApp::createAuthenticator() +{ +#if HAVE_SECCOMP + struct stat buf; + stat(KCHECKPASS_BIN, &buf); + if (!(buf.st_mode & S_ISUID)) { + m_supportsSeccomp = true; + return new Authenticator(AuthenticationMode::Delayed, this); + } +#endif + return new Authenticator(AuthenticationMode::Direct, this); +} + void UnlockApp::initialize() { initializeWayland(); diff --git a/greeter/main.cpp b/greeter/main.cpp --- a/greeter/main.cpp +++ b/greeter/main.cpp @@ -36,6 +36,9 @@ #include #include #endif +#if HAVE_SECCOMP +#include "seccomp_filter.h" +#endif static void signalHandler(int signum) { @@ -166,6 +169,14 @@ app.setKsldSocket(fd); } } + + // init the sandbox +#if HAVE_SECCOMP + if (app.supportsSeccomp()) { + ScreenLocker::SecComp::init(); + } +#endif + app.desktopResized(); // This allow ksmserver to know when the applicaion has actually finished setting itself up. diff --git a/greeter/seccomp_filter.h b/greeter/seccomp_filter.h new file mode 100644 --- /dev/null +++ b/greeter/seccomp_filter.h @@ -0,0 +1,36 @@ +/******************************************************************** + KSld - the KDE Screenlocker Daemon + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of +the License or (at your option) version 3 or any later version +accepted by the membership of KDE e.V. (or its successor approved +by the membership of KDE e.V.), which shall act as a proxy +defined in Section 14 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#ifndef SCREENLOCKER_SECCOMP_FILTER_H +#define SCREENLOCKER_SECCOMP_FILTER_H + +namespace ScreenLocker +{ +namespace SecComp +{ + +void init(); + +} +} + +#endif diff --git a/greeter/seccomp_filter.cpp b/greeter/seccomp_filter.cpp new file mode 100644 --- /dev/null +++ b/greeter/seccomp_filter.cpp @@ -0,0 +1,84 @@ +/******************************************************************** + KSld - the KDE Screenlocker Daemon + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of +the License or (at your option) version 3 or any later version +accepted by the membership of KDE e.V. (or its successor approved +by the membership of KDE e.V.), which shall act as a proxy +defined in Section 14 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "seccomp_filter.h" + +#include +#include + +#include +#include +#include + +namespace ScreenLocker +{ +namespace SecComp +{ + +void init() +{ + // trigger OpenGL context creation + // we need this to ensure that all required files are opened for write + QOpenGLContext::supportsThreadedOpenGL(); + + // access DBus to have the socket open + QDBusConnection::sessionBus(); + + // default action: allow + // we cannot use a whitelist approach of syscalls + // Qt, OpenGL, DBus just need to much and too broad + auto context = seccomp_init(SCMP_ACT_ALLOW); + if (!context) { + return; + } + // add a filter to prevent that the password gets written to a file + // we cannot disallow write syscall. That one is needed to wake up threads + // Qt and OpenGL might create additional threads and then it would fail as we have an fd which + // is not allowed to write to + + // instead disallow opening new files for writing + // they should fail with EPERM error + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(open), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, O_WRONLY, O_WRONLY)); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(open), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, O_RDWR, O_RDWR)); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(open), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, O_CREAT, O_CREAT)); + + // disallow going to a socket + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 0); + + // disallow fork+exec + // note glibc seems to use clone which is allowed for threads + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fork), 0); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(vfork), 0); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(execve), 0); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(execveat), 0); + + // disallow pipe, that should destroy copy and paste on Wayland + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(pipe), 0); + seccomp_rule_add(context, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(pipe2), 0); + + // and activate our rules + seccomp_load(context); + seccomp_release(context); +} + +} +}