From 3646620430c77f74f88ec213e0e057d1f1b3925d Mon Sep 17 00:00:00 2001 From: Kai Uwe Broulik Date: Tue, 16 Aug 2022 16:04:10 +0200 Subject: [PATCH] utils: Introduce RamFile class for memfd This class can be used to create an anonymous file, for instance to pass data between compositor and clients, through means of a file descriptor, as is done in various Wayland protocols, notably the keymap exchange. It also implements sealing the file, so that it can be shared between multiple clients without them being able to modify it. If supported, memfd_create is used, otherwise a `QTemporaryFile` is used. Signed-off-by: Victoria Fischer --- CMakeLists.txt | 13 ++ autotests/CMakeLists.txt | 11 ++ autotests/test_utils.cpp | 71 ++++++++ src/config-kwin.h.cmake | 1 + src/utils/CMakeLists.txt | 1 + src/utils/ramfile.cpp | 163 ++++++++++++++++++ src/utils/ramfile.h | 117 +++++++++++++ .../autotests/client/test_wayland_seat.cpp | 6 +- 8 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 autotests/test_utils.cpp create mode 100644 src/utils/ramfile.cpp create mode 100644 src/utils/ramfile.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f921615de..335ededef6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,19 @@ if (epoxy_HAS_GLX) endif() endif() +check_cxx_source_compiles(" +#include +#include +#include + +int main() { + const int size = 10; + int fd = memfd_create(\"test\", MFD_CLOEXEC | MFD_ALLOW_SEALING); + ftruncate(fd, size); + fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL); + mmap(nullptr, size, PROT_WRITE, MAP_SHARED, fd, 0); +}" HAVE_MEMFD) + find_package(Wayland 1.2 OPTIONAL_COMPONENTS Egl) set_package_properties(Wayland PROPERTIES TYPE REQUIRED diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index fa7e248458..b5d01ddd5f 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -238,3 +238,14 @@ target_link_libraries(testFtrace ) add_test(NAME kwin-testFtrace COMMAND testFtrace) ecm_mark_as_test(testFtrace) + +######################################################## +# Test KWin Utils +######################################################## +add_executable(testUtils test_utils.cpp) +target_link_libraries(testUtils + Qt::Test + kwin +) +add_test(NAME kwin-testUtils COMMAND testUtils) +ecm_mark_as_test(testUtils) diff --git a/autotests/test_utils.cpp b/autotests/test_utils.cpp new file mode 100644 index 0000000000..36e4388d3b --- /dev/null +++ b/autotests/test_utils.cpp @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "utils/ramfile.h" + +#include + +using namespace KWin; + +class TestUtils : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testRamFile(); + void testSealedRamFile(); +}; + +static const QByteArray s_testByteArray = QByteArrayLiteral("Test Data \0\1\2\3"); +static const char s_writeTestArray[] = "test"; + +void TestUtils::testRamFile() +{ + KWin::RamFile file("test", s_testByteArray.constData(), s_testByteArray.size()); + QVERIFY(file.isValid()); + QCOMPARE(file.size(), s_testByteArray.size()); + + QVERIFY(file.fd() != -1); + + char buf[20]; + int num = read(file.fd(), buf, sizeof buf); + QCOMPARE(num, file.size()); + + QCOMPARE(qstrcmp(s_testByteArray.constData(), buf), 0); +} + +void TestUtils::testSealedRamFile() +{ +#if HAVE_MEMFD + KWin::RamFile file("test", s_testByteArray.constData(), s_testByteArray.size(), KWin::RamFile::Flag::SealWrite); + QVERIFY(file.isValid()); + QVERIFY(file.effectiveFlags().testFlag(KWin::RamFile::Flag::SealWrite)); + + // Writing should not work. + auto written = write(file.fd(), s_writeTestArray, strlen(s_writeTestArray)); + QCOMPARE(written, -1); + + // Cannot use MAP_SHARED on sealed file descriptor. + void *data = mmap(nullptr, file.size(), PROT_READ, MAP_SHARED, file.fd(), 0); + QCOMPARE(data, MAP_FAILED); + + data = mmap(nullptr, file.size(), PROT_READ, MAP_PRIVATE, file.fd(), 0); + QVERIFY(data != MAP_FAILED); +#else + QSKIP("Sealing requires memfd suport."); +#endif +} + +QTEST_MAIN(TestUtils) +#include "test_utils.moc" diff --git a/src/config-kwin.h.cmake b/src/config-kwin.h.cmake index 82aba0a2a6..a99a195fd7 100644 --- a/src/config-kwin.h.cmake +++ b/src/config-kwin.h.cmake @@ -16,6 +16,7 @@ #cmakedefine01 HAVE_X11_XCB #cmakedefine01 HAVE_X11_XINPUT #cmakedefine01 HAVE_GBM_BO_GET_FD_FOR_PLANE +#cmakedefine01 HAVE_MEMFD #cmakedefine01 HAVE_WAYLAND_EGL #cmakedefine01 HAVE_BREEZE_DECO #cmakedefine01 HAVE_SCHED_RESET_ON_FORK diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index e84ea1c3b9..f333817dd2 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(kwin PRIVATE edid.cpp egl_context_attribute_builder.cpp filedescriptor.cpp + ramfile.cpp realtime.cpp subsurfacemonitor.cpp udev.cpp diff --git a/src/utils/ramfile.cpp b/src/utils/ramfile.cpp new file mode 100644 index 0000000000..e78000b255 --- /dev/null +++ b/src/utils/ramfile.cpp @@ -0,0 +1,163 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ramfile.h" +#include "common.h" // for logging + +#include + +#include +#include +#include +#include + +namespace KWin +{ + +RamFile::RamFile(const char *name, const void *inData, int size, RamFile::Flags flags) + : m_size(size) + , m_flags(flags) +{ + auto guard = qScopeGuard([this] { + cleanup(); + }); + +#if HAVE_MEMFD + m_fd = FileDescriptor(memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING)); + if (!m_fd.isValid()) { + qCWarning(KWIN_CORE).nospace() << name << ": Can't create memfd: " << strerror(errno); + return; + } + + if (ftruncate(m_fd.get(), size) < 0) { + qCWarning(KWIN_CORE).nospace() << name << ": Failed to ftruncate memfd: " << strerror(errno); + return; + } + + void *data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd.get(), 0); + if (data == MAP_FAILED) { + qCWarning(KWIN_CORE).nospace() << name << ": mmap failed: " << strerror(errno); + return; + } +#else + m_tmp = std::make_unique(); + if (!m_tmp->open()) { + qCWarning(KWIN_CORE).nospace() << name << ": Can't open temporary file"; + return; + } + + if (unlink(m_tmp->fileName().toUtf8().constData()) != 0) { + qCWarning(KWIN_CORE).nospace() << name << ": Failed to remove temporary file from filesystem: " << strerror(errno); + } + + if (!m_tmp->resize(size)) { + qCWarning(KWIN_CORE).nospace() << name << ": Failed to resize temporary file"; + return; + } + + uchar *data = m_tmp->map(0, size); + if (!data) { + qCWarning(KWIN_CORE).nospace() << name << ": map failed"; + return; + } +#endif + + memcpy(data, inData, size); + +#if HAVE_MEMFD + munmap(data, size); +#else + m_tmp->unmap(data); +#endif + + int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL; + if (flags.testFlag(RamFile::Flag::SealWrite)) { + seals |= F_SEAL_WRITE; + } + // This can fail for QTemporaryFile based on the underlying file system. + if (fcntl(fd(), F_ADD_SEALS, seals) != 0) { + qCDebug(KWIN_CORE).nospace() << name << ": Failed to seal RamFile: " << strerror(errno); + } + + guard.dismiss(); +} + +RamFile::RamFile(RamFile &&other) Q_DECL_NOEXCEPT + : m_size(std::exchange(other.m_size, 0)) + , m_flags(std::exchange(other.m_flags, RamFile::Flags{})) +#if HAVE_MEMFD + , m_fd(std::exchange(other.m_fd, KWin::FileDescriptor{})) +#else + , m_tmp(std::exchange(other.m_tmp, {})) +#endif +{ +} + +RamFile &RamFile::operator=(RamFile &&other) Q_DECL_NOEXCEPT +{ + cleanup(); + m_size = std::exchange(other.m_size, 0); + m_flags = std::exchange(other.m_flags, RamFile::Flags{}); +#if HAVE_MEMFD + m_fd = std::exchange(other.m_fd, KWin::FileDescriptor{}); +#else + m_tmp = std::exchange(other.m_tmp, {}); +#endif + return *this; +} + +RamFile::~RamFile() +{ + cleanup(); +} + +void RamFile::cleanup() +{ +#if HAVE_MEMFD + m_fd = KWin::FileDescriptor(); +#else + m_tmp.reset(); +#endif +} + +bool RamFile::isValid() const +{ + return fd() != -1; +} + +RamFile::Flags RamFile::effectiveFlags() const +{ + Flags flags = {}; + + const int seals = fcntl(fd(), F_GET_SEALS); + if (seals > 0) { + if (seals & F_SEAL_WRITE) { + flags.setFlag(Flag::SealWrite); + } + } + + return flags; +} + +int RamFile::fd() const +{ +#if HAVE_MEMFD + return m_fd.get(); +#else + return m_tmp->handle(); +#endif +} + +int RamFile::size() const +{ + return m_size; +} + +} // namespace KWin diff --git a/src/utils/ramfile.h b/src/utils/ramfile.h new file mode 100644 index 0000000000..a2a35ef6d3 --- /dev/null +++ b/src/utils/ramfile.h @@ -0,0 +1,117 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#if HAVE_MEMFD +#include "filedescriptor.h" +#else +#include +#include +#endif + +#include + +class QByteArray; + +namespace KWin +{ + +/** + * @brief Creates a file in memory. + * + * This is useful for passing larger data to clients, + * for example the xkeymap. + * + * If memfd is supported, it is used, otherwise + * a temporary file is created. + * + * @note It is advisable not to send the same file + * descriptor out to multiple clients unless it + * is sealed for writing. Check which flags actually + * apply before handing out the file descriptor. + * + * @sa effectiveFlags() + */ +class KWIN_EXPORT RamFile +{ +public: + /** + * Flags to use when creating the file. + * + * @note Check with effectiveFlags() which flags + * actually apply after the file was created. + */ + enum class Flag { + SealWrite = 1 << 0, ///< Seal the file descriptor for writing. + }; + Q_DECLARE_FLAGS(Flags, Flag) + + RamFile() = default; + /** + * Create a file of given size with given data. + * + * @note You should call seal() after copying the data into the file. + * + * @param name The file name, useful for debugging. + * @param data The data to store in the file. + * @param size The size of the file. + * @param flags The flags to use when creating the file. + */ + RamFile(const char *name, const void *inData, int size, Flags flags = {}); + + RamFile(RamFile &&other) Q_DECL_NOEXCEPT; + RamFile &operator=(RamFile &&other) Q_DECL_NOEXCEPT; + + /** + * Destroys the file. + */ + ~RamFile(); + + /** + * Whether this instance contains a valid file descriptor. + */ + bool isValid() const; + /** + * The flags that are effectively applied. + * + * For instance, even though SealWrite was passed in the constructor, + * it might not be supported. + */ + Flags effectiveFlags() const; + + /** + * The underlying file descriptor + * + * @return The fd, or -1 if there is none. + */ + int fd() const; + /** + * The size of the file + */ + int size() const; + +private: + void cleanup(); + + int m_size = 0; + Flags m_flags = {}; + +#if HAVE_MEMFD + KWin::FileDescriptor m_fd; +#else + std::unique_ptr m_tmp; +#endif +}; + +} // namespace KWin diff --git a/src/wayland/autotests/client/test_wayland_seat.cpp b/src/wayland/autotests/client/test_wayland_seat.cpp index 72490224de..c863810ad8 100644 --- a/src/wayland/autotests/client/test_wayland_seat.cpp +++ b/src/wayland/autotests/client/test_wayland_seat.cpp @@ -2084,7 +2084,8 @@ void TestWaylandSeat::testKeymap() QVERIFY(keymapChangedSpy.wait()); int fd = keymapChangedSpy.first().first().toInt(); QVERIFY(fd != -1); - QCOMPARE(keymapChangedSpy.first().last().value(), 3u); + // Account for null terminator. + QCOMPARE(keymapChangedSpy.first().last().value(), 4u); QFile file; QVERIFY(file.open(fd, QIODevice::ReadOnly)); const char *address = reinterpret_cast(file.map(0, keymapChangedSpy.first().last().value())); @@ -2098,7 +2099,8 @@ void TestWaylandSeat::testKeymap() QVERIFY(keymapChangedSpy.wait()); fd = keymapChangedSpy.first().first().toInt(); QVERIFY(fd != -1); - QCOMPARE(keymapChangedSpy.first().last().value(), 3u); + // Account for null terminator. + QCOMPARE(keymapChangedSpy.first().last().value(), 4u); QVERIFY(file.open(fd, QIODevice::ReadWrite)); address = reinterpret_cast(file.map(0, keymapChangedSpy.first().last().value())); QVERIFY(address);