Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// Copyright (C) 2022 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qfactoryloader_p.h"
#include "private/qcoreapplication_p.h"
#include "private/qloggingregistry_p.h"
#include "qcborarray.h"
#include "qcbormap.h"
#include "qcborstreamreader.h"
#include "qcborvalue.h"
#include "qdirlisting.h"
#include "qfileinfo.h"
#include "qjsonarray.h"
#include "qjsondocument.h"
#include "qjsonobject.h"
#include "qplugin.h"
#include "qpluginloader.h"
#include <qtcore_tracepoints_p.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
Q_TRACE_POINT(qtcore, QFactoryLoader_update, const QString &fileName);
namespace {
struct IterationResult
{
enum Result {
FinishedSearch = 0,
ContinueSearch,
// parse errors
ParsingError = -1,
InvalidMetaDataVersion = -2,
InvalidTopLevelItem = -3,
InvalidHeaderItem = -4,
};
Result result;
QCborError error = { QCborError::NoError };
Q_IMPLICIT IterationResult(Result r) : result(r) {}
Q_IMPLICIT IterationResult(QCborError e) : result(ParsingError), error(e) {}
};
struct QFactoryLoaderIidSearch
{
QLatin1StringView iid;
bool matchesIid = false;
QFactoryLoaderIidSearch(QLatin1StringView iid) : iid(iid)
{ Q_ASSERT(!iid.isEmpty()); }
static IterationResult::Result skip(QCborStreamReader &reader)
{
// skip this, whatever it is
reader.next();
return IterationResult::ContinueSearch;
}
IterationResult::Result operator()(QtPluginMetaDataKeys key, QCborStreamReader &reader)
{
if (key != QtPluginMetaDataKeys::IID)
return skip(reader);
matchesIid = (reader.readAllString() == iid);
return IterationResult::FinishedSearch;
}
IterationResult::Result operator()(QUtf8StringView, QCborStreamReader &reader)
{
return skip(reader);
}
};
struct QFactoryLoaderMetaDataKeysExtractor : QFactoryLoaderIidSearch
{
QCborArray keys;
QFactoryLoaderMetaDataKeysExtractor(QLatin1StringView iid)
: QFactoryLoaderIidSearch(iid)
{}
IterationResult::Result operator()(QtPluginMetaDataKeys key, QCborStreamReader &reader)
{
if (key == QtPluginMetaDataKeys::IID) {
QFactoryLoaderIidSearch::operator()(key, reader);
return IterationResult::ContinueSearch;
}
if (key != QtPluginMetaDataKeys::MetaData)
return skip(reader);
if (!matchesIid)
return IterationResult::FinishedSearch;
if (!reader.isMap() || !reader.isLengthKnown())
return IterationResult::InvalidHeaderItem;
if (!reader.enterContainer())
return IterationResult::ParsingError;
while (reader.isValid()) {
// the metadata is JSON, so keys are all strings
QByteArray key = reader.readAllUtf8String();
if (key == "Keys") {
if (!reader.isArray() || !reader.isLengthKnown())
return IterationResult::InvalidHeaderItem;
keys = QCborValue::fromCbor(reader).toArray();
break;
}
skip(reader);
}
// warning: we may not have finished iterating over the header
return IterationResult::FinishedSearch;
}
using QFactoryLoaderIidSearch::operator();
};
} // unnamed namespace
template <typename F> static IterationResult iterateInPluginMetaData(QByteArrayView raw, F &&f)
{
QPluginMetaData::Header header;
Q_ASSERT(raw.size() >= qsizetype(sizeof(header)));
memcpy(&header, raw.data(), sizeof(header));
if (Q_UNLIKELY(header.version > QPluginMetaData::CurrentMetaDataVersion))
return IterationResult::InvalidMetaDataVersion;
// use fromRawData to keep QCborStreamReader from copying
raw = raw.sliced(sizeof(header));
QByteArray ba = QByteArray::fromRawData(raw.data(), raw.size());
QCborStreamReader reader(ba);
if (reader.isInvalid())
return reader.lastError();
if (!reader.isMap())
return IterationResult::InvalidTopLevelItem;
if (!reader.enterContainer())
return reader.lastError();
while (reader.isValid()) {
IterationResult::Result r;
if (reader.isInteger()) {
// integer key, one of ours
qint64 value = reader.toInteger();
auto key = QtPluginMetaDataKeys(value);
if (qint64(key) != value)
return IterationResult::InvalidHeaderItem;
if (!reader.next())
return reader.lastError();
r = f(key, reader);
} else if (reader.isString()) {
QByteArray key = reader.readAllUtf8String();
if (key.isNull())
return reader.lastError();
r = f(QUtf8StringView(key), reader);
} else {
return IterationResult::InvalidTopLevelItem;
}
if (QCborError e = reader.lastError())
return e;
if (r != IterationResult::ContinueSearch)
return r;
}
if (!reader.leaveContainer())
return reader.lastError();
return IterationResult::FinishedSearch;
}
static bool isIidMatch(QByteArrayView raw, QLatin1StringView iid)
{
QFactoryLoaderIidSearch search(iid);
iterateInPluginMetaData(raw, search);
return search.matchesIid;
}
bool QPluginParsedMetaData::parse(QByteArrayView raw)
{
QCborMap map;
auto r = iterateInPluginMetaData(raw, [&](const auto &key, QCborStreamReader &reader) {
QCborValue item = QCborValue::fromCbor(reader);
if (item.isInvalid())
return IterationResult::ParsingError;
if constexpr (std::is_enum_v<std::decay_t<decltype(key)>>)
map[int(key)] = item;
else
map[QString::fromUtf8(key)] = item;
return IterationResult::ContinueSearch;
});
switch (r.result) {
case IterationResult::FinishedSearch:
case IterationResult::ContinueSearch:
break;
// parse errors
case IterationResult::ParsingError:
return setError(QFactoryLoader::tr("Metadata parsing error: %1").arg(r.error.toString()));
case IterationResult::InvalidMetaDataVersion:
return setError(QFactoryLoader::tr("Invalid metadata version"));
case IterationResult::InvalidTopLevelItem:
case IterationResult::InvalidHeaderItem:
return setError(QFactoryLoader::tr("Unexpected metadata contents"));
}
// header was validated
auto header = qFromUnaligned<QPluginMetaData::Header>(raw.data());
DecodedArchRequirements archReq =
header.version == 0 ? decodeVersion0ArchRequirements(header.plugin_arch_requirements)
: decodeVersion1ArchRequirements(header.plugin_arch_requirements);
// insert the keys not stored in the top-level CBOR map
map[int(QtPluginMetaDataKeys::QtVersion)] =
QT_VERSION_CHECK(header.qt_major_version, header.qt_minor_version, 0);
map[int(QtPluginMetaDataKeys::IsDebug)] = archReq.isDebug;
map[int(QtPluginMetaDataKeys::Requirements)] = archReq.level;
data = std::move(map);
return true;
}
QJsonObject QPluginParsedMetaData::toJson() const
{
// convert from the internal CBOR representation to an external JSON one
QJsonObject o;
for (auto it : data.toMap()) {
QString key;
if (it.first.isInteger()) {
switch (it.first.toInteger()) {
#define CONVERT_TO_STRING(IntKey, StringKey, Description) \
case int(IntKey): key = QStringLiteral(StringKey); break;
QT_PLUGIN_FOREACH_METADATA(CONVERT_TO_STRING)
}
} else {
key = it.first.toString();
}
if (!key.isEmpty())
o.insert(key, it.second.toJsonValue());
}
return o;
}
#if QT_CONFIG(library)
Q_STATIC_LOGGING_CATEGORY_WITH_ENV_OVERRIDE(lcFactoryLoader, "QT_DEBUG_PLUGINS",
"qt.core.plugin.factoryloader")
namespace {
struct QFactoryLoaderGlobals
{
// needs to be recursive because loading one plugin could cause another
// factory to be initialized
QRecursiveMutex mutex;
QList<QFactoryLoader *> loaders;
};
}
Q_GLOBAL_STATIC(QFactoryLoaderGlobals, qt_factoryloader_global)
inline void QFactoryLoader::Private::updateSinglePath(const QString &path)
{
struct LibraryReleaser {
void operator()(QLibraryPrivate *library)
{ if (library) library->release(); }
};
// If we've already loaded, skip it...
if (loadedPaths->hasSeen(path))
return;
qCDebug(lcFactoryLoader) << "checking directory path" << path << "...";
QDirListing plugins(path,
#if defined(Q_OS_WIN) || defined(Q_OS_CYGWIN)
QStringList(QStringLiteral("*.dll")),
#elif defined(Q_OS_ANDROID)
QStringList("libplugins_%1_*.so"_L1.arg(suffix)),
#endif
QDirListing::IteratorFlag::FilesOnly | QDirListing::IteratorFlag::ResolveSymlinks);
auto versionFromLib = [](const QLibraryPrivate *lib) {
return lib->metaData.value(QtPluginMetaDataKeys::QtVersion).toInteger();
};
for (const auto &dirEntry : plugins) {
const QString &fileName = dirEntry.fileName();
#if defined(Q_PROCESSOR_X86)
if (fileName.endsWith(".avx2"_L1) || fileName.endsWith(".avx512"_L1)) {
// ignore AVX2-optimized file, we'll do a bait-and-switch to it later
continue;
}
#endif
qCDebug(lcFactoryLoader) << "looking at" << fileName;
Q_TRACE(QFactoryLoader_update, fileName);
QLibraryPrivate::UniquePtr library;
library.reset(QLibraryPrivate::findOrCreate(dirEntry.canonicalFilePath()));
if (!library->isPlugin()) {
qCDebug(lcFactoryLoader) << library->errorString << Qt::endl
<< " not a plugin";
continue;
}
QStringList keys;
bool metaDataOk = false;
QString iid = library->metaData.value(QtPluginMetaDataKeys::IID).toString();
if (iid == QLatin1StringView(this->iid.constData(), this->iid.size())) {
QCborMap object = library->metaData.value(QtPluginMetaDataKeys::MetaData).toMap();
metaDataOk = true;
const QCborArray k = object.value("Keys"_L1).toArray();
for (QCborValueConstRef v : k)
keys += cs ? v.toString() : v.toString().toLower();
}
qCDebug(lcFactoryLoader) << "Got keys from plugin meta data" << keys;
if (!metaDataOk)
continue;
static constexpr qint64 QtVersionNoPatch = QT_VERSION_CHECK(QT_VERSION_MAJOR, QT_VERSION_MINOR, 0);
qint64 thisVersion = versionFromLib(library.get());
if (iid.startsWith(QStringLiteral("org.qt-project.Qt.QPA"))) {
// QPA plugins must match Qt Major.Minor
if (thisVersion != QtVersionNoPatch) {
qCDebug(lcFactoryLoader) << "Ignoring QPA plugin due to mismatching Qt versions" << QtVersionNoPatch << thisVersion;
continue;
}
}
int keyUsageCount = 0;
for (const QString &key : std::as_const(keys)) {
QLibraryPrivate *&keyMapEntry = keyMap[key];
if (QLibraryPrivate *existingLibrary = keyMapEntry) {
static constexpr bool QtBuildIsDebug = QT_CONFIG(debug);
bool existingIsDebug = existingLibrary->metaData.value(QtPluginMetaDataKeys::IsDebug).toBool();
bool thisIsDebug = library->metaData.value(QtPluginMetaDataKeys::IsDebug).toBool();
bool configsAreDifferent = thisIsDebug != existingIsDebug;
bool thisConfigDoesNotMatchQt = thisIsDebug != QtBuildIsDebug;
if (configsAreDifferent && thisConfigDoesNotMatchQt)
continue; // Existing library matches Qt's build config
// If the existing library was built with a future Qt version,
// whereas the one we're considering has a Qt version that fits
// better, we prioritize the better match.
qint64 existingVersion = versionFromLib(existingLibrary);
if (existingVersion == QtVersionNoPatch)
continue; // Prefer exact Qt version match
if (existingVersion < QtVersionNoPatch && thisVersion > QtVersionNoPatch)
continue; // Better too old than too new
if (existingVersion < QtVersionNoPatch && thisVersion < existingVersion)
continue; // Otherwise prefer newest
}
keyMapEntry = library.get();
++keyUsageCount;
}
if (keyUsageCount || keys.isEmpty()) {
library->setLoadHints(QLibrary::PreventUnloadHint); // once loaded, don't unload
QMutexLocker locker(&mutex);
libraries.push_back(std::move(library));
}
};
loadedLibraries.resize(libraries.size());
}
void QFactoryLoader::setLoadHints(QLibrary::LoadHints loadHints)
{
d->loadHints = loadHints;
}
void QFactoryLoader::update()
{
#ifdef QT_SHARED
if (!d->extraSearchPath.isEmpty())
d->updateSinglePath(d->extraSearchPath);
const QStringList paths = QCoreApplication::libraryPaths();
for (const QString &pluginDir : paths) {
#ifdef Q_OS_ANDROID
QString path = pluginDir;
#else
QString path = pluginDir + d->suffix;
#endif
d->updateSinglePath(path);
}
#else
qCDebug(lcFactoryLoader) << "ignoring" << d->iid
<< "since plugins are disabled in static builds";
#endif
}
QFactoryLoader::~QFactoryLoader()
{
if (!qt_factoryloader_global.isDestroyed()) {
QMutexLocker locker(&qt_factoryloader_global->mutex);
qt_factoryloader_global->loaders.removeOne(this);
}
#if QT_CONFIG(library)
for (qsizetype i = 0; i < d->loadedLibraries.size(); ++i) {
if (d->loadedLibraries.at(i)) {
auto &plugin = d->libraries[i];
delete plugin->inst.data();
plugin->unload();
}
}
#endif
for (QtPluginInstanceFunction staticInstance : std::as_const(d->usedStaticInstances)) {
if (staticInstance)
delete staticInstance();
}
}
#if defined(Q_OS_UNIX) && !defined (Q_OS_DARWIN)
QLibraryPrivate *QFactoryLoader::library(const QString &key) const
{
const auto it = d->keyMap.find(d->cs ? key : key.toLower());
if (it == d->keyMap.cend())
return nullptr;
return it->second;
}
#endif
void QFactoryLoader::refreshAll()
{
if (qt_factoryloader_global.exists()) {
QMutexLocker locker(&qt_factoryloader_global->mutex);
for (QFactoryLoader *loader : std::as_const(qt_factoryloader_global->loaders))
loader->update();
}
}
#endif // QT_CONFIG(library)
QFactoryLoader::QFactoryLoader(const char *iid,
const QString &suffix,
Qt::CaseSensitivity cs)
{
Q_ASSERT_X(suffix.startsWith(u'/'), "QFactoryLoader",
"For historical reasons, the suffix must start with '/' (and it can't be empty)");
d->iid = iid;
#if QT_CONFIG(library)
d->cs = cs;
d->suffix = suffix;
# ifdef Q_OS_ANDROID
if (!d->suffix.isEmpty() && d->suffix.at(0) == u'/')
d->suffix.remove(0, 1);
# endif
QMutexLocker locker(&qt_factoryloader_global->mutex);
update();
qt_factoryloader_global->loaders.append(this);
#else
Q_UNUSED(suffix);
Q_UNUSED(cs);
#endif
}
void QFactoryLoader::setExtraSearchPath(const QString &path)
{
#if QT_CONFIG(library)
if (d->extraSearchPath == path)
return; // nothing to do
QMutexLocker locker(&qt_factoryloader_global->mutex);
QString oldPath = std::exchange(d->extraSearchPath, path);
if (oldPath.isEmpty()) {
// easy case, just update this directory
d->updateSinglePath(d->extraSearchPath);
} else {
// must re-scan everything
for (qsizetype i = 0; i < d->loadedLibraries.size(); ++i) {
if (d->loadedLibraries.at(i)) {
auto &plugin = d->libraries[i];
delete plugin->inst.data();
}
}
d->loadedLibraries.fill(false);
d->loadedPaths.emplace(); // hard-reset QDuplicateTracker
d->libraries.clear();
d->keyMap.clear();
update();
}
#else
Q_UNUSED(path);
#endif
}
QFactoryLoader::MetaDataList QFactoryLoader::metaData() const
{
QList<QPluginParsedMetaData> metaData;
#if QT_CONFIG(library)
QMutexLocker locker(&d->mutex);
metaData.reserve(qsizetype(d->libraries.size()));
for (const auto &library : d->libraries)
metaData.append(library->metaData);
locker.unlock();
#endif
QLatin1StringView iid(d->iid.constData(), d->iid.size());
const auto staticPlugins = QPluginLoader::staticPlugins();
for (const QStaticPlugin &plugin : staticPlugins) {
QByteArrayView pluginData(static_cast<const char *>(plugin.rawMetaData), plugin.rawMetaDataSize);
QPluginParsedMetaData parsed(pluginData);
if (parsed.isError() || parsed.value(QtPluginMetaDataKeys::IID) != iid)
continue;
metaData.append(std::move(parsed));
}
// other portions of the code will cast to int (e.g., keyMap())
Q_ASSERT(metaData.size() <= std::numeric_limits<int>::max());
return metaData;
}
QList<QCborArray> QFactoryLoader::metaDataKeys() const
{
QList<QCborArray> metaData;
#if QT_CONFIG(library)
QMutexLocker locker(&d->mutex);
metaData.reserve(qsizetype(d->libraries.size()));
for (const auto &library : d->libraries) {
const QCborValue md = library->metaData.value(QtPluginMetaDataKeys::MetaData);
metaData.append(md["Keys"_L1].toArray());
}
locker.unlock();
#endif
QLatin1StringView iid(d->iid.constData(), d->iid.size());
const auto staticPlugins = QPluginLoader::staticPlugins();
for (const QStaticPlugin &plugin : staticPlugins) {
QByteArrayView pluginData(static_cast<const char *>(plugin.rawMetaData),
plugin.rawMetaDataSize);
QFactoryLoaderMetaDataKeysExtractor extractor{ iid };
iterateInPluginMetaData(pluginData, extractor);
if (extractor.matchesIid)
metaData += std::move(extractor.keys);
}
// other portions of the code will cast to int (e.g., keyMap())
Q_ASSERT(metaData.size() <= std::numeric_limits<int>::max());
return metaData;
}
QObject *QFactoryLoader::instance(int index) const
{
if (index < 0)
return nullptr;
QMutexLocker lock(&d->mutex);
QObject *obj = instanceHelper_locked(index);
if (obj && !obj->parent())
obj->moveToThread(QCoreApplicationPrivate::mainThread());
return obj;
}
inline QObject *QFactoryLoader::instanceHelper_locked(int index) const
{
#if QT_CONFIG(library)
if (size_t(index) < d->libraries.size()) {
QLibraryPrivate *library = d->libraries[index].get();
d->loadedLibraries[index] = true;
library->setLoadHints(d->loadHints);
return library->pluginInstance();
}
// we know d->libraries.size() <= index <= numeric_limits<decltype(index)>::max() → no overflow
index -= static_cast<int>(d->libraries.size());
#endif
QLatin1StringView iid(d->iid.constData(), d->iid.size());
const QList<QStaticPlugin> staticPlugins = QPluginLoader::staticPlugins();
qsizetype i = 0;
for (QStaticPlugin plugin : staticPlugins) {
QByteArrayView pluginData(static_cast<const char *>(plugin.rawMetaData), plugin.rawMetaDataSize);
if (!isIidMatch(pluginData, iid))
continue;
if (i == index) {
if (d->usedStaticInstances.size() <= i)
d->usedStaticInstances.resize(i + 1);
d->usedStaticInstances[i] = plugin.instance;
return plugin.instance();
}
++i;
}
return nullptr;
}
QMultiMap<int, QString> QFactoryLoader::keyMap() const
{
QMultiMap<int, QString> result;
const QList<QCborArray> metaDataList = metaDataKeys();
for (int i = 0; i < int(metaDataList.size()); ++i) {
const QCborArray &keys = metaDataList[i];
for (QCborValueConstRef key : keys)
result.insert(i, key.toString());
}
return result;
}
int QFactoryLoader::indexOf(const QString &needle) const
{
const QList<QCborArray> metaDataList = metaDataKeys();
for (int i = 0; i < int(metaDataList.size()); ++i) {
const QCborArray &keys = metaDataList[i];
for (QCborValueConstRef key : keys) {
if (key.toString().compare(needle, Qt::CaseInsensitive) == 0)
return i;
}
}
return -1;
}
QT_END_NAMESPACE