Hi,
No TL;DR: You're supposed to read and understand all of it.
As I write this, the UB fix for QArrayDataObs/Pointer¹ that held up² adding
RHEL10 to the CI has merged to all branches, except the ESM ones (6.5 and
5.15), but it's just a keystroke of Jani's away there, too.
If the following reads like "I told you so", I'm sorry, but ... I did ;)
Act 1: Phantom Menace
When we were trying to enable RHEL 10 (using GCC 14.3) in the CI, we hit a FAIL
in tst_qarraydata. This is _good_. It means it didn't blow up at some customer
first (for all we know). Several highly-esteemed developers and two chat bots
concluded that this is a compiler bug. The problem is that the compiler isn't
very exotic, so what does it bring us if we know it's a compiler bug? Nothing.
We'd still need to work around it.
Act 2: Deus Ex Machina
It just so happens that this developer ran into a very similar issue last year,
and found, with the help of the GCC devs³, that the compiler is completely
correct, and it was my fault. I was casting a char*& to a reference to a
pointer to enum qchar8 : uchar {}, incremented it, and expected the original
pointer object to have been incremented. But this is not guaranteed by the
standard. Only char etc is allowed to alias other objects, _not_ char*, and
certainly not enum*. So the compiler was right in caching the old value of the
object and continue using it.
Side note: if this sounds familiar to you, it's because we've just talked about
that this is exactly how the UB in QObject::disconnect() would likely manifest.⁴
The analysis of the QArrayData assembly sounded eerily similar: one of the
three fields that QArrayDataPointer::swap() swaps wasn't swapped. Thiago found
the culprit⁵, I merely connected the dots: we were inheriting QArrayDataOps :
QArrayDataPointer, and static_cast<Ops*>(this) from QArrayDataPointer::op->().
The cast is UB, because *this isn't actually an object of type Ops, but only
QArrayDataPointer. Period. This code has been stitting there for 15 years
(since Qt 5.0, but unused until Qt 6.0)⁵, and not making any problems. Until an
innocent compiler upgrade.
Side note: Casting a Base to a Derived : Base is UB if the object isn't
actually of dynamic type Derived. It doesn't matter whether sizeof(Derived) ==
sizeof(Base). It _is_ always UB. I have lost count of the number of code
snippets I have fixed in tests, mostly, where ubsan complained about invalid
casts where it turned out that this "technique" was used. It was _very_
prevalent in Qt, as if somone somewhere put up the rule that, regardless of
what the std _actually_ says, casting to a non-actual derived type is ok as
long as the sizeof's are the same. Well, it's not.
Once the bug was identified, the fix was lengthy, but pretty straight-forward.
But since this was "actively exploited" UB in one of Qt's most central classes
(QList, QString, QByteArray depend on this, so I'd be hard-pressed to find
anything in Qt that does _not_ depend on QArrayData), we decided that we need
to pick this all the way back, incl. into ESM branches.
Act 3: Odyssey
This is where the vast amount of my work was sunk: cherry-picking through a
forest of cleanup and rewrite commits.
I have long said that, from a software-engineering POV, you do _not_ want your
active branches to diverge very much. If you find a security issue, speed is of
the essence, and you a) don't want to slow down by resolving conflicts (you
want to stage the change in dev and wake up the next morning with the change
merged to all branches by the bots) and b) don't want to introduce a lot of
risk by combining commit sets in a unique combination found nowhere else.
Well, this bug is as close to a security issue as you can have without an
actual security issue. And it ran full steam into one the worst violations of
that principle, which, I hasten to add, is _not_ standing Qt policy.
The end effect is that, the issue took a full week to make it to the ESM
branches, instead of a few hours. It didn't help that the cherry-picking has
its own weird ideas about where to pick to⁶, but that only manifests in
multi-commit fixing scenarios, so doesn't affect your typical security fix
(which tend to be one-liners more often than not).
Epilogue
To me, this issue reinforces what I have known to be true for years, to wit:
- the compiler is more clever than you, you MUST NOT fall into UB, regardless
of whether _you_ think it's benign (even if you think you can prove it)
- only features shouldn't be picked back, in particular
- test changes should always be picked back, with a XFAIL, if necessary
- refactorings should always be picked back (or not done at all)
- bugfixes should always be picked back
We have known issues in both categories. I'm pretty sure we have more classes
in Qt that are never instantiated, only cast to, and we have at least one
module (you know who you are), which is security-critical, and where a high
code turnover is paired with lack of cherry-picking. I have experienced the
same frustrating conflicts there than I did in shoveling QArrayData patches
though. That's not to single out said module or it's authors and maintainers.
They work by the book. It's the book that's wrong here, and I would hope we
have a discussion about changing it to aggressive picking going forward, for
security-critical components. We have the CRA markings now. I would say that
one goal should be that all security-critical components in Qt are at the same
level in all active branches. This is what we do with 3rd-party components
already. It seems odd to exclude Qt's own security-critical components, where
we have the added benefit of being able to exclude feature commits from
picking. In 3rd-party components, we always get both.
Hope this provides some food for thought (and change).
Thanks for reading this far,
Marc
¹ https://codereview.qt-project.org/c/qt/qtbase/+/736274 /
https://codereview.qt-project.org/q/topic:QArrayDataOps-2026
² https://qt-project.atlassian.net/browse/QTBUG-146652 /
https://qt-project.atlassian.net/browse/QTQAINFRA-7202
³ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=121593
⁴ https://lists.qt-project.org/pipermail/development/2026-March/047026.html
⁵ introduced, but not necessarily taken into use, by
https://codereview.qt-project.org/c/qt/qtbase/+/9847
⁶ https://qt-project.atlassian.net/browse/QTQAINFRA-7900 /
https://qt-project.atlassian.net/browse/QTQAINFRA-7903
--
Marc Mutz <[email protected]><mailto:[email protected]> (he/his)
Principal Software Engineer
The Qt Company
Erich-Thilo-Str. 10 12489
Berlin, Germany
www.qt.io<http://www.qt.io>
Geschäftsführer: Mika Pälsi, Juha Varelius, Juha Puputti
Sitz der Gesellschaft: Berlin,
Registergericht: Amtsgericht Charlottenburg,
HRB 144331 B
Public
--
Development mailing list
[email protected]
https://lists.qt-project.org/listinfo/development