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);