diff options
author | Thiago Macieira <thiago.macieira@intel.com> | 2021-11-14 10:08:21 -0800 |
---|---|---|
committer | Thiago Macieira <thiago.macieira@intel.com> | 2021-11-19 22:51:03 -0800 |
commit | 892d5607d0b1c9e010ea10a1123e68741c46c21e (patch) | |
tree | e23b8ba93194383cede7da24b3409a24f9c795ca | |
parent | 235173175b64ea4a5571373a4c37c35b4e18d14f (diff) |
QPluginLoader: add COFF PE file parser
Fixes: QTBUG-67461
Docs: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
Change-Id: I5e52dc5b093c43a3b678fffd16b77bf9a8f2b17e
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
-rw-r--r-- | src/corelib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/corelib/plugin/qcoffpeparser.cpp | 416 | ||||
-rw-r--r-- | src/corelib/plugin/qcoffpeparser_p.h | 70 | ||||
-rw-r--r-- | src/corelib/plugin/qlibrary.cpp | 3 |
4 files changed, 490 insertions, 0 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index a14239e9ae..cd79a9f0c2 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1017,6 +1017,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_library ) qt_internal_extend_target(Core CONDITION QT_FEATURE_library AND WIN32 SOURCES + plugin/qcoffpeparser.cpp plugin/qcoffpeparser_p.h plugin/qlibrary_win.cpp ) qt_internal_extend_target(Core CONDITION QT_FEATURE_library AND APPLE diff --git a/src/corelib/plugin/qcoffpeparser.cpp b/src/corelib/plugin/qcoffpeparser.cpp new file mode 100644 index 0000000000..4bd0a5516f --- /dev/null +++ b/src/corelib/plugin/qcoffpeparser.cpp @@ -0,0 +1,416 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qcoffpeparser_p.h" + +#include <qendian.h> +#include <qloggingcategory.h> +#include <qnumeric.h> + +#include <optional> + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#undef min +#undef max + +QT_BEGIN_NAMESPACE + +// Whether we include some extra validity checks +// (checks to ensure we don't read out-of-bounds are always included) +static constexpr bool IncludeValidityChecks = true; + +static constexpr char rawSectionName[] = ".qtmetadata"; +static constexpr QLatin1String metadataSectionName(rawSectionName, sizeof(rawSectionName) - 1); +static constexpr QLatin1String truncatedSectionName = + metadataSectionName.left(sizeof(IMAGE_SECTION_HEADER::Name)); + +#ifdef QT_BUILD_INTERNAL +# define QCOFFPEPARSER_DEBUG +#endif +#if defined(QCOFFPEPARSER_DEBUG) +static Q_LOGGING_CATEGORY(lcCoffPeParser, "qt.core.plugin.coffpeparser") +# define peDebug qCDebug(lcCoffPeParser) << reinterpret_cast<const char16_t *>(error.errMsg->constData()) << ':' +#else +# define peDebug if (false) {} else QNoDebug() +#endif + +QT_WARNING_PUSH +QT_WARNING_DISABLE_CLANG("-Wunused-const-variable") + +static const WORD ExpectedMachine = +#if 0 + // nothing, just so everything is #elf +#elif defined(Q_PROCESSOR_ARM_32) + IMAGE_FILE_MACHINE_ARM +#elif defined(Q_PROCESSOR_ARM_64) + IMAGE_FILE_MACHINE_ARM64 +#elif defined(Q_PROCESSOR_IA64) + IMAGE_FILE_MACHINE_IA64 +#elif defined(Q_PROCESSOR_RISCV_32) + 0x5032 // IMAGE_FILE_MACHINE_RISCV32 +#elif defined(Q_PROCESSOR_RISCV_64) + 0x5064 // IMAGE_FILE_MACHINE_RISCV64 +#elif defined(Q_PROCESSOR_X86_32) + IMAGE_FILE_MACHINE_I386 +#elif defined(Q_PROCESSOR_X86_64) + IMAGE_FILE_MACHINE_AMD64 +#else +# error "Unknown Q_PROCESSOR_xxx macro, please update." + IMAGE_FILE_MACHINE_UNKNOWN +#endif + ; + +static const WORD ExpectedOptionalHeaderSignature = + sizeof(void*) == sizeof(quint64) ? IMAGE_NT_OPTIONAL_HDR64_MAGIC : + IMAGE_NT_OPTIONAL_HDR32_MAGIC; + +namespace { +struct ErrorMaker +{ + QString *errMsg; + constexpr ErrorMaker(QString *errMsg) : errMsg(errMsg) {} + + Q_DECL_COLD_FUNCTION QLibraryScanResult operator()(QString &&text) const + { + *errMsg = QLibrary::tr("'%1' is not a valid Windows DLL (%2)").arg(*errMsg, std::move(text)); + return {}; + } + + Q_DECL_COLD_FUNCTION QLibraryScanResult toosmall() const + { + *errMsg = QLibrary::tr("'%1' is too small").arg(*errMsg); + return {}; + } + + Q_DECL_COLD_FUNCTION QLibraryScanResult notplugin(QString &&explanation) const + { + *errMsg = QLibrary::tr("'%1' is not a Qt plugin (%2)").arg(*errMsg, explanation); + return {}; + } + + Q_DECL_COLD_FUNCTION QLibraryScanResult notfound() const + { + return notplugin(QLibrary::tr("metadata not found")); + } +}; + +struct HeaderDebug { const IMAGE_NT_HEADERS *h; }; +Q_DECL_UNUSED static QDebug &operator<<(QDebug &d, HeaderDebug h) +{ + switch (h.h->Signature & 0xffff) { + case IMAGE_OS2_SIGNATURE: return d << "NE executable"; + case IMAGE_VXD_SIGNATURE: return d << "LE executable"; + default: return d << "Unknown file type"; + case IMAGE_NT_SIGNATURE: break; + } + + // the FileHeader and the starting portion of OptionalHeader are the same + // for 32- and 64-bit + switch (h.h->OptionalHeader.Magic) { + case IMAGE_NT_OPTIONAL_HDR32_MAGIC: d << "COFF PE"; break; + case IMAGE_NT_OPTIONAL_HDR64_MAGIC: d << "COFF PE+"; break; + default: return d << "Unknown COFF PE type"; + } + + QDebugStateSaver saver(d); + d.nospace() << Qt::hex << Qt::showbase; + + switch (h.h->FileHeader.Machine) { + case IMAGE_FILE_MACHINE_I386: d << "i386"; break; + case IMAGE_FILE_MACHINE_ARM: d << "ARM"; break; + case IMAGE_FILE_MACHINE_ARMNT: d << "ARM Thumb-2"; break;; + case IMAGE_FILE_MACHINE_THUMB: d << "Thumb"; break; + case IMAGE_FILE_MACHINE_IA64: d << "IA-64"; break; + case IMAGE_FILE_MACHINE_MIPS16: d << "MIPS16"; break; + case IMAGE_FILE_MACHINE_MIPSFPU: d << "MIPS with FPU"; break; + case IMAGE_FILE_MACHINE_MIPSFPU16: d << "MIPS16 with FPU"; break; + case IMAGE_FILE_MACHINE_AMD64: d << "x86-64"; break; + case 0xaa64: d << "AArch64"; break; + default: d << h.h->FileHeader.Machine; break; + } + + // this usually prints "executable DLL" + if (h.h->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) + d << " executable"; + if (h.h->FileHeader.Characteristics & IMAGE_FILE_DLL) + d << " DLL"; + if (h.h->FileHeader.Characteristics & IMAGE_FILE_LARGE_ADDRESS_AWARE) + d << " large-address aware"; + + d << ", " << Qt::dec << h.h->FileHeader.NumberOfSections << " sections, "; + if (h.h->FileHeader.SizeOfOptionalHeader < sizeof(IMAGE_OPTIONAL_HEADER32)) + return d; + + auto optDebug = [&d](const auto *hdr) { + d << "(Windows " << hdr->MajorSubsystemVersion + << '.' << hdr->MinorSubsystemVersion; + switch (hdr->Subsystem) { + case IMAGE_SUBSYSTEM_NATIVE: d << " native)"; break; + case IMAGE_SUBSYSTEM_WINDOWS_GUI: d << " GUI)"; break; + case IMAGE_SUBSYSTEM_WINDOWS_CUI: d << " CUI)"; break; + default: d << " subsystem " << hdr->Subsystem << ')'; break; + } + + d.space() << Qt::hex << hdr->SizeOfHeaders << "header bytes," + << Qt::dec << hdr->NumberOfRvaAndSizes << "RVA entries"; + }; + + if (h.h->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + optDebug(reinterpret_cast<const IMAGE_OPTIONAL_HEADER64 *>(&h.h->OptionalHeader)); + else + optDebug(reinterpret_cast<const IMAGE_OPTIONAL_HEADER32 *>(&h.h->OptionalHeader)); + return d; +} + +struct SectionDebug { const IMAGE_SECTION_HEADER *s; }; +Q_DECL_UNUSED static QDebug &operator<<(QDebug &d, SectionDebug s) +{ + QDebugStateSaver saver(d); + d << Qt::hex << Qt::showbase; + d << "contains"; + if (s.s->Characteristics & IMAGE_SCN_CNT_CODE) + d << "CODE"; + if (s.s->Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) + d << "DATA"; + if (s.s->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) + d << "BSS"; + + d << "flags"; + d.nospace(); + if (s.s->Characteristics & IMAGE_SCN_MEM_READ) + d << 'R'; + if (s.s->Characteristics & IMAGE_SCN_MEM_WRITE) + d << 'W'; + if (s.s->Characteristics & IMAGE_SCN_MEM_EXECUTE) + d << 'X'; + if (s.s->Characteristics & IMAGE_SCN_MEM_SHARED) + d << 'S'; + if (s.s->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) + d << 'D'; + + d.space() << "offset" << s.s->PointerToRawData << "size" << s.s->SizeOfRawData; + return d; +} +} // unnamed namespace + +QT_WARNING_POP + +const IMAGE_NT_HEADERS *checkNtHeaders(QByteArrayView data, const ErrorMaker &error) +{ + if (size_t(data.size()) < qMax(sizeof(IMAGE_DOS_HEADER), sizeof(IMAGE_NT_HEADERS))) { + peDebug << "file too small:" << size_t(data.size()); + return error.toosmall(), nullptr; + } + + // check if there's a DOS image header (almost everything does) + size_t off = 0; + auto dosHeader = reinterpret_cast<const IMAGE_DOS_HEADER *>(data.data()); + if (dosHeader->e_magic == IMAGE_DOS_SIGNATURE) { + off = dosHeader->e_lfanew; + // peDebug << "DOS file header redirects to offset" << Qt::hex << Qt::showbase << off; + if (size_t end; qAddOverflow<sizeof(IMAGE_NT_HEADERS)>(off, &end) + || end > size_t(data.size())) { + peDebug << "file too small:" << size_t(data.size()); + return error.toosmall(), nullptr; + } + } + + // now check the NT headers + auto ntHeader = reinterpret_cast<const IMAGE_NT_HEADERS *>(data.data() + off); + peDebug << HeaderDebug{ntHeader}; + if (ntHeader->Signature != IMAGE_NT_SIGNATURE) // "PE\0\0" + return error(QLibrary::tr("invalid signature")), nullptr; + if (ntHeader->FileHeader.Machine != ExpectedMachine) + return error(QLibrary::tr("file is for a different processor")), nullptr; + if (ntHeader->FileHeader.NumberOfSections == 0) + return error(QLibrary::tr("file has no sections")), nullptr; + + WORD requiredCharacteristics = + IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_DLL; + if ((ntHeader->FileHeader.Characteristics & requiredCharacteristics) != requiredCharacteristics) + return error(QLibrary::tr("wrong characteristics")), nullptr; + + // the optional header is not optional + if (ntHeader->OptionalHeader.Magic != ExpectedOptionalHeaderSignature) + return error(QLibrary::tr("file is for a different word size")), nullptr; + if (ntHeader->OptionalHeader.SizeOfCode == 0) + return error.notplugin(QLibrary::tr("file has no code")), nullptr; + + return ntHeader; +} + +static const IMAGE_SECTION_HEADER * +findSectionTable(QByteArrayView data, const IMAGE_NT_HEADERS *ntHeader, const ErrorMaker &error) +{ + // macro IMAGE_FIRST_SECTION can't overflow due to limited range + // of type, but adding to the offset from the DOS header could + // overflow on 32-bit + static_assert(sizeof(ntHeader->FileHeader.SizeOfOptionalHeader) < sizeof(size_t)); + static_assert(sizeof(ntHeader->FileHeader.NumberOfSections) < sizeof(size_t)); + + size_t off = offsetof(IMAGE_NT_HEADERS, OptionalHeader); + off += ntHeader->FileHeader.SizeOfOptionalHeader; + if (qAddOverflow<size_t>(off, reinterpret_cast<const char *>(ntHeader) - data.data(), &off)) + return error.toosmall(), nullptr; + + size_t end = ntHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); + + // validate that the file is big enough for all sections we're + // supposed to have + if (qAddOverflow(end, off, &end) || end > size_t(data.size())) + return error.toosmall(), nullptr; + + peDebug << "contains" << ntHeader->FileHeader.NumberOfSections << "sections at offset" << off; + return reinterpret_cast<const IMAGE_SECTION_HEADER *>(data.data() + off); +} + +static std::optional<QByteArrayView> +findStringTable(QByteArrayView data, const IMAGE_NT_HEADERS *ntHeader, const ErrorMaker &error) +{ + // first, find the symbol table + size_t off = ntHeader->FileHeader.PointerToSymbolTable; + if (off == 0) + return QByteArrayView(); + + // skip the symbol table to find the string table after it + constexpr size_t SymbolEntrySize = 18; + size_t size = ntHeader->FileHeader.NumberOfSymbols; + if (qMulOverflow<SymbolEntrySize>(size, &size) + || qAddOverflow(off, size, &off) + || qAddOverflow(off, sizeof(DWORD), &off) + || off > size_t(data.size())) + return error.toosmall(), std::nullopt; + + off -= sizeof(DWORD); + + // we've found the string table, ensure it's big enough + size = qFromUnaligned<DWORD>(data.data() + off); + if (size_t end; qAddOverflow(off, size, &end) || end > size_t(data.size())) + return error.toosmall(), std::nullopt; + + // the conversion to signed is fine because we checked above it wasn't + // bigger than data.size() + return data.sliced(off, size); +} + +static QLatin1String findSectionName(const IMAGE_SECTION_HEADER *section, QByteArrayView stringTable) +{ + auto ptr = reinterpret_cast<const char *>(section->Name); + qsizetype n = qstrnlen(ptr, sizeof(section->Name)); + if (ptr[0] == '/' && !stringTable.isEmpty()) { + // long section name + // Microsoft's link.exe does not use these and will truncate the + // section name to fit section->Name. GNU binutils' ld does use long + // section names on executable image files by default (can be disabled + // using --disable-long-section-names). LLVM's lld does generate a + // string table in MinGW mode, but does not use it for our section. + + static_assert(sizeof(section->Name) - 1 < std::numeric_limits<uint>::digits10); + bool ok; + qsizetype offset = QByteArrayView(ptr + 1, n - 1).toUInt(&ok); + if (!ok || offset >= stringTable.size()) + return QLatin1String(); + + ptr = stringTable.data() + offset; + n = qstrnlen(ptr, stringTable.size() - offset); + } + + return QLatin1String(ptr, n); +} + +QLibraryScanResult QCoffPeParser::parse(QByteArrayView data, QString *errMsg) +{ + ErrorMaker error(errMsg); + auto ntHeaders = checkNtHeaders(data, error); + if (!ntHeaders) + return {}; + + auto section = findSectionTable(data, ntHeaders, error); + if (!ntHeaders) + return {}; + + QByteArrayView stringTable; + if (auto optional = findStringTable(data, ntHeaders, error); optional) + stringTable = *optional; + else + return {}; + + // scan the sections now + const auto sectionTableEnd = section + ntHeaders->FileHeader.NumberOfSections; + for ( ; section < sectionTableEnd; ++section) { + QLatin1String sectionName = findSectionName(section, stringTable); + peDebug << "section" << sectionName << SectionDebug{section}; + if (IncludeValidityChecks && sectionName.isEmpty()) + return error(QLibrary::tr("a section name is empty or extends past the end of the file")); + + size_t offset = section->PointerToRawData; + if (size_t end; qAddOverflow<size_t>(offset, section->SizeOfRawData, &end) + || end > size_t(data.size())) + return error(QLibrary::tr("a section data extends past the end of the file")); + + DWORD type = section->Characteristics + & (IMAGE_SCN_CNT_CODE | IMAGE_SCN_CNT_INITIALIZED_DATA + | IMAGE_SCN_CNT_UNINITIALIZED_DATA); + if (type != IMAGE_SCN_CNT_INITIALIZED_DATA) + continue; + + // if we do have a string table, the name may be complete + if (sectionName != truncatedSectionName && sectionName != metadataSectionName) + continue; + peDebug << "found .qtmetadata section"; + + if (IncludeValidityChecks) { + if (section->Characteristics & IMAGE_SCN_MEM_WRITE) + return error(QLibrary::tr(".qtmetadata section is writable")); + if (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) + return error(QLibrary::tr(".qtmetadata section is executable")); + } + size_t size = qMin(section->SizeOfRawData, section->Misc.VirtualSize); + if (size < sizeof(QPluginMetaData::MagicHeader)) + return error(QLibrary::tr("section .qtmetadata is too small")); + + return { qsizetype(offset + sizeof(QPluginMetaData::MagicString)), + qsizetype(size - sizeof(QPluginMetaData::MagicString)) }; + } + + return error.notfound(); +} + +QT_END_NAMESPACE diff --git a/src/corelib/plugin/qcoffpeparser_p.h b/src/corelib/plugin/qcoffpeparser_p.h new file mode 100644 index 0000000000..91de6a416b --- /dev/null +++ b/src/corelib/plugin/qcoffpeparser_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// no, this is not a misspelling of "coffeeparser" +#ifndef QCOFFPEPARSER_H +#define QCOFFPEPARSER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlibrary_p.h" + +#if defined(Q_OS_WIN) + +QT_BEGIN_NAMESPACE + +struct QCoffPeParser +{ + static QLibraryScanResult parse(QByteArrayView data, QString *errMsg); +}; + +QT_END_NAMESPACE + +#endif // defined(Q_OF_ELF) && defined(Q_CC_GNU) + +#endif // QCOFFPEPARSER_H diff --git a/src/corelib/plugin/qlibrary.cpp b/src/corelib/plugin/qlibrary.cpp index 5a8c647c27..d4ee833554 100644 --- a/src/corelib/plugin/qlibrary.cpp +++ b/src/corelib/plugin/qlibrary.cpp @@ -59,6 +59,7 @@ #include <private/qloggingregistry_p.h> #include <private/qsystemerror_p.h> +#include "qcoffpeparser_p.h" #include "qelfparser_p.h" #include "qfactoryloader_p.h" #include "qmachparser_p.h" @@ -206,6 +207,8 @@ static QLibraryScanResult qt_find_pattern(const char *s, qsizetype s_len, QStrin return QElfParser::parse({s, s_len}, errMsg); #elif defined(Q_OF_MACH_O) return QMachOParser::parse(s, s_len, errMsg); +#elif defined(Q_OS_WIN) + return QCoffPeParser::parse({s, s_len}, errMsg); #endif QByteArrayView pattern = QPluginMetaData::MagicString; static const QByteArrayMatcher matcher(pattern.toByteArray()); |