From c65fd911632dae6b9d3ffc10c06df2368006dd66 Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Tue, 22 Sep 2026 13:05:35 +0200 Subject: Fix bug when reading qtdf files with multiple textures If the pre-generated distance field cache spans multiple textures, then the bottom most glyphs will expand beyond what is reported as the maximum texture size, due to a hot fix made for QTBUG-76528. When fixing QTBUG-149508, a stronger restriction was added on the glyph rects, ensuring that they do not expand beyond the texture. This was to protect against malformed qtdf tables and the main purpose was to keep the values from overflowing into negative numbers. Checking the height against maxSize * 2 instead ensures that we also accept glyphs that expand beyond the texture size while keeping the fix for QTBUG-149508 for any reasonable texture size. This adds tests for both QTBUG-149508 and QTBUG-150766 to ensure both problems are covered. Fixes: QTBUG-150766 Change-Id: I59e793b6bee32f83999f597c92275ddbdba1bd38 Reviewed-by: Andy Nichols (cherry picked from commit c2a8d0ff939122a5c1e8e6c5c59dae53c7b30389) --- .../scenegraph/qsgrhidistancefieldglyphcache.cpp | 7 +- tests/auto/quick/scenegraph/data/neg_double.ttf | Bin 0 -> 32896 bytes tests/auto/quick/scenegraph/data/neg_single.ttf | Bin 0 -> 32876 bytes tests/auto/quick/scenegraph/data/ok_double.ttf | Bin 0 -> 32896 bytes tests/auto/quick/scenegraph/data/ok_single.ttf | Bin 0 -> 28780 bytes .../data/pregeneratedDistanceFieldCache.qml | 21 ++++++ .../scenegraph/data/qtdf_invalidgeometry_5_12.ttf | Bin 0 -> 32492 bytes .../scenegraph/data/qtdf_multitexture_5_12.ttf | Bin 0 -> 32476 bytes tests/auto/quick/scenegraph/data/wild.ttf | Bin 0 -> 32896 bytes tests/auto/quick/scenegraph/tst_scenegraph.cpp | 73 +++++++++++++++++++++ 10 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/auto/quick/scenegraph/data/neg_double.ttf create mode 100644 tests/auto/quick/scenegraph/data/neg_single.ttf create mode 100644 tests/auto/quick/scenegraph/data/ok_double.ttf create mode 100644 tests/auto/quick/scenegraph/data/ok_single.ttf create mode 100644 tests/auto/quick/scenegraph/data/pregeneratedDistanceFieldCache.qml create mode 100644 tests/auto/quick/scenegraph/data/qtdf_invalidgeometry_5_12.ttf create mode 100644 tests/auto/quick/scenegraph/data/qtdf_multitexture_5_12.ttf create mode 100644 tests/auto/quick/scenegraph/data/wild.ttf diff --git a/src/quick/scenegraph/qsgrhidistancefieldglyphcache.cpp b/src/quick/scenegraph/qsgrhidistancefieldglyphcache.cpp index ad0551433a..22fd972d53 100644 --- a/src/quick/scenegraph/qsgrhidistancefieldglyphcache.cpp +++ b/src/quick/scenegraph/qsgrhidistancefieldglyphcache.cpp @@ -441,8 +441,13 @@ bool QSGRhiDistanceFieldGlyphCache::loadPregeneratedCache(const QRawFont &font) const quint32 allocWidth = Qtdf::fetch(textureRecord, Qtdf::allocatedWidth); const quint32 allocHeight = Qtdf::fetch(textureRecord, Qtdf::allocatedHeight); const quint32 maxSize = quint32(m_maxTextureSize); + // maxSize is the slice height of the area allocator, and a glyph that crosses a slice + // boundary expands the texture it is assigned to past the slice, by up to one glyph + // height - and a glyph can be no taller than the slice itself. The + // tallest valid texture is therefore 2 * maxSize - 1. + const quint32 maxTexHeight = maxSize * 2 - 1; if (allocX > maxSize || allocY > maxSize - || allocWidth > maxSize || allocHeight > maxSize) { + || allocWidth > maxSize || allocHeight > maxTexHeight) { qWarning("Invalid texture geometry in qtdf table in font '%s'", qPrintable(font.familyName())); return false; diff --git a/tests/auto/quick/scenegraph/data/neg_double.ttf b/tests/auto/quick/scenegraph/data/neg_double.ttf new file mode 100644 index 0000000000..88e80a10c5 Binary files /dev/null and b/tests/auto/quick/scenegraph/data/neg_double.ttf differ diff --git a/tests/auto/quick/scenegraph/data/neg_single.ttf b/tests/auto/quick/scenegraph/data/neg_single.ttf new file mode 100644 index 0000000000..96d1861ad5 Binary files /dev/null and b/tests/auto/quick/scenegraph/data/neg_single.ttf differ diff --git a/tests/auto/quick/scenegraph/data/ok_double.ttf b/tests/auto/quick/scenegraph/data/ok_double.ttf new file mode 100644 index 0000000000..d95da04c1c Binary files /dev/null and b/tests/auto/quick/scenegraph/data/ok_double.ttf differ diff --git a/tests/auto/quick/scenegraph/data/ok_single.ttf b/tests/auto/quick/scenegraph/data/ok_single.ttf new file mode 100644 index 0000000000..7f16fcdb64 Binary files /dev/null and b/tests/auto/quick/scenegraph/data/ok_single.ttf differ diff --git a/tests/auto/quick/scenegraph/data/pregeneratedDistanceFieldCache.qml b/tests/auto/quick/scenegraph/data/pregeneratedDistanceFieldCache.qml new file mode 100644 index 0000000000..d7b72a2849 --- /dev/null +++ b/tests/auto/quick/scenegraph/data/pregeneratedDistanceFieldCache.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Rectangle { + width: 320 + height: 200 + color: "white" + + property alias fontFamily: text.font.family + + Text { + id: text + anchors.centerIn: parent + renderType: Text.QtRendering + text: "ABC" + color: "black" + font.pixelSize: 64 + } +} diff --git a/tests/auto/quick/scenegraph/data/qtdf_invalidgeometry_5_12.ttf b/tests/auto/quick/scenegraph/data/qtdf_invalidgeometry_5_12.ttf new file mode 100644 index 0000000000..57159c0c72 Binary files /dev/null and b/tests/auto/quick/scenegraph/data/qtdf_invalidgeometry_5_12.ttf differ diff --git a/tests/auto/quick/scenegraph/data/qtdf_multitexture_5_12.ttf b/tests/auto/quick/scenegraph/data/qtdf_multitexture_5_12.ttf new file mode 100644 index 0000000000..e9f336b5a5 Binary files /dev/null and b/tests/auto/quick/scenegraph/data/qtdf_multitexture_5_12.ttf differ diff --git a/tests/auto/quick/scenegraph/data/wild.ttf b/tests/auto/quick/scenegraph/data/wild.ttf new file mode 100644 index 0000000000..b2c92fbffa Binary files /dev/null and b/tests/auto/quick/scenegraph/data/wild.ttf differ diff --git a/tests/auto/quick/scenegraph/tst_scenegraph.cpp b/tests/auto/quick/scenegraph/tst_scenegraph.cpp index 4e0ebc2877..e32733fd4a 100644 --- a/tests/auto/quick/scenegraph/tst_scenegraph.cpp +++ b/tests/auto/quick/scenegraph/tst_scenegraph.cpp @@ -108,6 +108,8 @@ private slots: void resizeTextureFromImage(); void textureNativeInterface(); void distanceFieldCacheInvalidation(); + void pregeneratedDistanceFieldCache_data(); + void pregeneratedDistanceFieldCache(); void unexposeDuringPolish(); #ifdef QT_BUILD_INTERNAL @@ -909,6 +911,77 @@ void tst_SceneGraph::distanceFieldCacheInvalidation() } } +void tst_SceneGraph::pregeneratedDistanceFieldCache_data() +{ + QTest::addColumn("fontFileName"); + QTest::addColumn("expectLoaded"); + + // Various tests for + QTest::newRow("control, one 64x64 texture") << QStringLiteral("ok_single.ttf") << true; + QTest::newRow("control, two 64x64 textures") << QStringLiteral("ok_double.ttf") << true; + QTest::newRow("control, two 64x64 textures") << QStringLiteral("ok_double.ttf") << true; + QTest::newRow("allocatedWidth -1, height 256 -> size -256") << QStringLiteral("neg_single.ttf") << true; + QTest::newRow("allocatedWidth -1, height 1024 -> size -1024, pointer slides back 1 KB") << QStringLiteral("neg_double.ttf") << true; + QTest::newRow("allocatedWidth INT_MIN -> size -2147483648") << QStringLiteral("wild.ttf") << true; + + // In the 5.12 version of the qtdf format, the texture size in the header + // is the slice height of the stacked area allocator, and the texture + // holding a glyph that crosses a slice boundary is taller than this. + QTest::newRow("multiple textures, version 5.12") + << QStringLiteral("qtdf_multitexture_5_12.ttf") << true; + + // QTBUG-149508: an allocated height with the high bit set becomes + // negative when narrowed to int, defeating the payload bounds check. + // Such geometry must be rejected. + QTest::newRow("invalid geometry, version 5.12") + << QStringLiteral("qtdf_invalidgeometry_5_12.ttf") << false; +} + +void tst_SceneGraph::pregeneratedDistanceFieldCache() +{ + if (!isRunningOnRhi()) + QSKIP("Skipping complex rendering tests due to not running with QRhi"); + + QFETCH(QString, fontFileName); + QFETCH(bool, expectLoaded); + + const int fontId = QFontDatabase::addApplicationFont(testFile(fontFileName)); + QVERIFY(fontId >= 0); + const auto cleanup = qScopeGuard([fontId] { QFontDatabase::removeApplicationFont(fontId); }); + + const QStringList families = QFontDatabase::applicationFontFamilies(fontId); + QVERIFY(!families.isEmpty()); + + // Verify that the font resolves and carries a pregenerated cache, so that + // the text below cannot accidentally pass by using another font or by + // generating distance fields dynamically. + QCOMPARE(QFontInfo(QFont(families.first())).family(), families.first()); + { + const QRawFont rawFont(testFile(fontFileName), 32.0); + QVERIFY(rawFont.isValid()); + QVERIFY(!rawFont.fontTable("qtdf").isEmpty()); + } + + // The scene renders "ABC" with the given font. The pregenerated cache + // contains the glyphs for "A" and "B" in the first texture, where "B" + // crosses the boundary into the next slice, and "C" in the second + // texture. The distance field data marks the glyphs' texture rects as + // fully inside, so if (and only if) the pregenerated cache is loaded, the + // glyphs are rendered as solid boxes. + QQuickView view; + view.setInitialProperties({{ QStringLiteral("fontFamily"), families.first() }}); + view.setSource(testFileUrl(QLatin1String("pregeneratedDistanceFieldCache.qml"))); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + const QImage content = view.grabWindow(); + QVERIFY(!content.isNull()); + if (expectLoaded) + QVERIFY(containsSomethingOtherThanWhite(content)); + else + QVERIFY(!containsSomethingOtherThanWhite(content)); +} + class NotificationItem : public QQuickItem { Q_OBJECT -- cgit v1.2.3