Skip to content

Commit

Permalink
qt6.qtdeclarative: invalidate qml caches created from mismatched stor…
Browse files Browse the repository at this point in the history
…e paths

Includes the application's store path in the verification metadata of
qml cache files and prevents them from being loaded if mismatched.
  • Loading branch information
outfoxxed committed Sep 16, 2024
1 parent a7b76fc commit 4952687
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pkgs/development/libraries/qt-6/modules/qtdeclarative.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ qtModule {
strictDeps = true;

patches = [
# invalidates qml caches created from nix applications at different
# store paths and disallows saving caches of bare qml files in the store.
../patches/qtdeclarative-invalidate-caches-from-mismatched-store-paths.patch
# add version specific QML import path
../patches/0002-qtdeclarative-also-use-versioned-qml-paths.patch
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
diff --git a/src/qml/jsruntime/qv4executablecompilationunit.cpp b/src/qml/jsruntime/qv4executablecompilationunit.cpp
index 2ab38b70d9..8634b07e41 100644
--- a/src/qml/jsruntime/qv4executablecompilationunit.cpp
+++ b/src/qml/jsruntime/qv4executablecompilationunit.cpp
@@ -33,6 +33,8 @@
#include <QtCore/qcryptographichash.h>
#include <QtCore/QScopedValueRollback>

+#include <QtCore/qcoreapplication.h>
+
static_assert(QV4::CompiledData::QmlCompileHashSpace > QML_COMPILE_HASH_LENGTH);

#if defined(QML_COMPILE_HASH) && defined(QML_COMPILE_HASH_LENGTH) && QML_COMPILE_HASH_LENGTH > 0
@@ -44,6 +46,36 @@ __attribute__((section(".qml_compile_hash")))
const char qml_compile_hash[QV4::CompiledData::QmlCompileHashSpace] = QML_COMPILE_HASH;
static_assert(sizeof(QV4::CompiledData::Unit::libraryVersionHash) > QML_COMPILE_HASH_LENGTH,
"Compile hash length exceeds reserved size in data structure. Please adjust and bump the format version");
+
+bool nix__isNixApplication() {
+ static const bool value = QCoreApplication::applicationFilePath().startsWith(QStringLiteral("/nix/store/"));
+ return value;
+}
+
+static_assert(sizeof(QV4::CompiledData::Unit::libraryVersionHash) >
+ /*sha1*/ 20 + /*NIX:*/ 4,
+ "Nix compile hash length exceeds the reserved space in data "
+ "structure. Please review the patch.");
+
+const QByteArray &nix__applicationHash() {
+ static const QByteArray value = [](){
+ QCryptographicHash applicationHash(QCryptographicHash::Sha1);
+ applicationHash.addData(QByteArrayView(qml_compile_hash, QML_COMPILE_HASH_LENGTH));
+
+ // We only care about the package, not the specific file path.
+ auto view = QCoreApplication::applicationFilePath().sliced(11); // len("/nix/store/")
+ auto pkgEndIdx = view.indexOf(QStringLiteral("/"));
+ if (pkgEndIdx != -1) view = view.sliced(0, pkgEndIdx);
+
+ QCryptographicHash::hashLength(QCryptographicHash::Sha1);
+ applicationHash.addData(view.toUtf8());
+
+ return QByteArray("NIX:") + applicationHash.result();
+ }();
+
+ return value;
+}
+
#else
# error "QML_COMPILE_HASH must be defined for the build of QtDeclarative to ensure version checking for cache files"
#endif
@@ -909,6 +941,28 @@ bool ExecutableCompilationUnit::saveToDisk(const QUrl &unitUrl, QString *errorSt
return false;
}

+#if defined(QML_COMPILE_HASH) && defined(QML_COMPILE_HASH_LENGTH) && QML_COMPILE_HASH_LENGTH > 0
+ if (nix__isNixApplication() && unitUrl.scheme() == QStringLiteral("qrc")) {
+ // If the application is running from the nix store, we can safely save
+ // bytecode for its embedded QML files as long as we hash the
+ // application path into the version.
+ const auto &applicationHash = nix__applicationHash();
+
+ memcpy(const_cast<char *>(unitData()->libraryVersionHash),
+ applicationHash.constData(), applicationHash.length());
+ } else if (unitUrl.path().startsWith(QStringLiteral("/nix/store/"))) {
+ // We don't store bytecode for bare QML files in the nix store as the
+ // paths will change every time the application updates, filling caches
+ // endlessly with junk.
+ *errorString = QStringLiteral("Refusing to save bytecode for bare /nix/store/ path.");
+ return false;
+ } else {
+ // If the QML file is loaded from a normal file path it doesn't matter
+ // if the application itself is running from a nix path, so we fall back
+ // to the default Qt behavior.
+ }
+#endif
+
return CompiledData::SaveableUnitPointer(unitData()).saveToDisk<char>(
[&unitUrl, errorString](const char *data, quint32 size) {
const QString cachePath = localCacheFilePath(unitUrl);
@@ -1038,13 +1092,29 @@ bool ExecutableCompilationUnit::verifyHeader(
}

#if defined(QML_COMPILE_HASH) && defined(QML_COMPILE_HASH_LENGTH) && QML_COMPILE_HASH_LENGTH > 0
- if (qstrncmp(qml_compile_hash, unit->libraryVersionHash, QML_COMPILE_HASH_LENGTH) != 0) {
+ const bool nixUnit = qstrncmp("NIX:", unit->libraryVersionHash, 4) == 0;
+
+ if (nixUnit && !nix__isNixApplication()) {
+ *errorString = QStringLiteral("QML compile hash is for a nix store application.");
+ return false;
+ }
+
+ const char *targetHash = qml_compile_hash;
+ size_t targetHashLength = QML_COMPILE_HASH_LENGTH;
+
+ if (nixUnit) {
+ const auto &applicationHash = nix__applicationHash();
+ targetHash = applicationHash.constData();
+ targetHashLength = applicationHash.length();
+ }
+
+ if (qstrncmp(targetHash, unit->libraryVersionHash, targetHashLength) != 0) {
*errorString = QStringLiteral("QML compile hashes don't match. Found %1 expected %2")
.arg(QString::fromLatin1(
- QByteArray(unit->libraryVersionHash, QML_COMPILE_HASH_LENGTH)
+ QByteArray(unit->libraryVersionHash, targetHashLength)
.toPercentEncoding()),
QString::fromLatin1(
- QByteArray(qml_compile_hash, QML_COMPILE_HASH_LENGTH)
+ QByteArray(targetHash, targetHashLength)
.toPercentEncoding()));
return false;
}

0 comments on commit 4952687

Please sign in to comment.