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 <victoria.fischer@mbition.io>
master
Kai Uwe Broulik 2 years ago committed by Kai Uwe Broulik
parent e5aeb674c0
commit 3646620430

@ -180,6 +180,19 @@ if (epoxy_HAS_GLX)
endif() endif()
endif() endif()
check_cxx_source_compiles("
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
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) find_package(Wayland 1.2 OPTIONAL_COMPONENTS Egl)
set_package_properties(Wayland PROPERTIES set_package_properties(Wayland PROPERTIES
TYPE REQUIRED TYPE REQUIRED

@ -238,3 +238,14 @@ target_link_libraries(testFtrace
) )
add_test(NAME kwin-testFtrace COMMAND testFtrace) add_test(NAME kwin-testFtrace COMMAND testFtrace)
ecm_mark_as_test(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)

@ -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 <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kwin.h>
#include <sys/mman.h>
#include <unistd.h>
#include "utils/ramfile.h"
#include <QtTest>
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"

@ -16,6 +16,7 @@
#cmakedefine01 HAVE_X11_XCB #cmakedefine01 HAVE_X11_XCB
#cmakedefine01 HAVE_X11_XINPUT #cmakedefine01 HAVE_X11_XINPUT
#cmakedefine01 HAVE_GBM_BO_GET_FD_FOR_PLANE #cmakedefine01 HAVE_GBM_BO_GET_FD_FOR_PLANE
#cmakedefine01 HAVE_MEMFD
#cmakedefine01 HAVE_WAYLAND_EGL #cmakedefine01 HAVE_WAYLAND_EGL
#cmakedefine01 HAVE_BREEZE_DECO #cmakedefine01 HAVE_BREEZE_DECO
#cmakedefine01 HAVE_SCHED_RESET_ON_FORK #cmakedefine01 HAVE_SCHED_RESET_ON_FORK

@ -4,6 +4,7 @@ target_sources(kwin PRIVATE
edid.cpp edid.cpp
egl_context_attribute_builder.cpp egl_context_attribute_builder.cpp
filedescriptor.cpp filedescriptor.cpp
ramfile.cpp
realtime.cpp realtime.cpp
subsurfacemonitor.cpp subsurfacemonitor.cpp
udev.cpp udev.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 <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "ramfile.h"
#include "common.h" // for logging
#include <QScopeGuard>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <utility>
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<QTemporaryFile>();
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

@ -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 <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <config-kwin.h>
#include <kwin_export.h>
#if HAVE_MEMFD
#include "filedescriptor.h"
#else
#include <QTemporaryFile>
#include <memory>
#endif
#include <QFlag>
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<QTemporaryFile> m_tmp;
#endif
};
} // namespace KWin

@ -2084,7 +2084,8 @@ void TestWaylandSeat::testKeymap()
QVERIFY(keymapChangedSpy.wait()); QVERIFY(keymapChangedSpy.wait());
int fd = keymapChangedSpy.first().first().toInt(); int fd = keymapChangedSpy.first().first().toInt();
QVERIFY(fd != -1); QVERIFY(fd != -1);
QCOMPARE(keymapChangedSpy.first().last().value<quint32>(), 3u); // Account for null terminator.
QCOMPARE(keymapChangedSpy.first().last().value<quint32>(), 4u);
QFile file; QFile file;
QVERIFY(file.open(fd, QIODevice::ReadOnly)); QVERIFY(file.open(fd, QIODevice::ReadOnly));
const char *address = reinterpret_cast<char *>(file.map(0, keymapChangedSpy.first().last().value<quint32>())); const char *address = reinterpret_cast<char *>(file.map(0, keymapChangedSpy.first().last().value<quint32>()));
@ -2098,7 +2099,8 @@ void TestWaylandSeat::testKeymap()
QVERIFY(keymapChangedSpy.wait()); QVERIFY(keymapChangedSpy.wait());
fd = keymapChangedSpy.first().first().toInt(); fd = keymapChangedSpy.first().first().toInt();
QVERIFY(fd != -1); QVERIFY(fd != -1);
QCOMPARE(keymapChangedSpy.first().last().value<quint32>(), 3u); // Account for null terminator.
QCOMPARE(keymapChangedSpy.first().last().value<quint32>(), 4u);
QVERIFY(file.open(fd, QIODevice::ReadWrite)); QVERIFY(file.open(fd, QIODevice::ReadWrite));
address = reinterpret_cast<char *>(file.map(0, keymapChangedSpy.first().last().value<quint32>())); address = reinterpret_cast<char *>(file.map(0, keymapChangedSpy.first().last().value<quint32>()));
QVERIFY(address); QVERIFY(address);

Loading…
Cancel
Save