https://bugs.kde.org/show_bug.cgi?id=523471
Bug ID: 523471
Summary: Use-after-free crash after a few decryptions in the
Notepad: clearAddedRecipients() deletes
mUnknownWidgets entries without clearing the list
Classification: Applications
Product: kleopatra
Version First git master
Reported In:
Platform: Microsoft Windows
OS: Microsoft Windows
Status: REPORTED
Severity: crash
Priority: NOR
Component: general
Assignee: [email protected]
Reporter: [email protected]
CC: [email protected], [email protected],
[email protected]
Target Milestone: ---
SUMMARY
Kleopatra crashes with a use-after-free after a few decryptions in the Notepad.
SignEncryptWidget::clearAddedRecipients() deletes every widget in
d->mUnknownWidgets but never clears the list, leaving it full of dangling
pointers. PadWidget::doDecryptVerify() calls it on every decrypt, so the freed
pointers are deleted a second time on a later decrypt.
The bug is still present in current master; the two functions involved are
byte-identical between 5.0.2 and master.
STEPS TO REPRODUCE
The most reliable trigger is a message encrypted to a hidden recipient
(--hidden-recipient / --throw-keyids). Such a message carries key ID
0000000000000000, which can never be found in any keyring, so every decrypt of
it registers an unknown recipient.
gpg --throw-keyids -r <your-key> -e -a message.txt
1. Kleopatra -> Notepad -> paste the ciphertext -> Decrypt/Verify.
It decrypts correctly ("anonymous recipient", keyid 0000000000000000).
2. Repeat step 1 in the SAME Kleopatra session.
3. Kleopatra crashes on roughly the third decrypt.
A message encrypted to any ordinary key you do not hold also works, but the
hidden-recipient case is deterministic and needs no second keypair.
This matters in practice: any automated sender using --throw-keyids for
recipient privacy (contact forms, mailers) produces a stream of messages that
each advance the counter, so Kleopatra dies every few messages.
OBSERVED RESULT
Access violation, identical Windows Error Reporting bucket every time:
Faulting application name: kleopatra.exe, version: 5.0.2.0
Faulting module name: kleopatra.exe, version: 5.0.2.0
Exception code: 0xc0000005
Fault offset: 0x0000000000163538
Four consecutive crashes were captured at that same offset.
EXPECTED RESULT
No crash.
SOFTWARE/OS VERSIONS
Kleopatra: 5.0.2 (as shipped in Gpg4win 5.0.2, released 2026-03-16)
Source rev: 782c464a9801d6189e3d3be494922f42857bd26c
(kleopatra snapshot 2026-03-02, per Gpg4win versioninfo.txt)
Qt / KF: 6.10.1 / 6.20.0
GnuPG: 2.5.18, gpgme 2.0.1
OS: Windows 11 (x86_64)
Reproduced on master @ f80768bc98026b73d0d225d6fde5363c0d6a440c (2026-07-22)
ROOT CAUSE
src/crypto/gui/signencryptwidget.cpp (line numbers from the 5.0.2 revision;
master is identical at 597-602):
void SignEncryptWidget::clearAddedRecipients()
{
for (auto w : std::as_const(d->mUnknownWidgets)) {
d->mRecpLayout->removeWidget(w);
delete w;
}
// <-- d->mUnknownWidgets still contains every freed pointer
for (auto &key : std::as_const(d->mAddedKeys)) {
removeRecipient(key);
}
...
}
The list is never emptied anywhere else. Every reference in the file:
143: QList<UnknownRecipientWidget *> mUnknownWidgets; // declaration
615: for (auto w : std::as_const(d->mUnknownWidgets)) // deletes, no
clear
632: d->mUnknownWidgets << unknownWidget; // append
642: for (auto w : std::as_const(d->mUnknownWidgets)) //
keysMayHaveChanged handler
657: d->mUnknownWidgets.removeAll(w); // only removal
WHY AN ORDINARY DECRYPT REACHES IT
- PadWidget::doDecryptVerify() (src/view/padwidget.cpp:396) calls
clearAddedRecipients() at line 399 -- on every decrypt.
- PadWidget::updateRecipientsFromResult() (line 273) calls
addUnknownRecipient(recipient.keyID()) at line 295 for every recipient of
the
decrypted message whose key is not in the keyring.
decrypt #1 -> clearAddedRecipients(): list empty
addUnknownRecipient() -> [w1]
decrypt #2 -> clearAddedRecipients(): delete w1, list is STILL [w1]
(dangling)
addUnknownRecipient() -> [w1(freed), w2]
decrypt #3 -> clearAddedRecipients(): "delete w1" a second time -> UAF
Because a freed QWidget may still look plausible to the allocator, the second
delete can also succeed silently and corrupt the heap, so the visible crash may
come a decrypt or two later.
SCOPE
Notepad-only. The unknown-recipient list is touched from nowhere else:
addUnknownRecipient() <- only src/view/padwidget.cpp:295
clearAddedRecipients() <- only src/view/padwidget.cpp:399
updateRecipientsFromResult() <- private to PadWidget (padwidget.cpp:273)
SignEncryptWidget is also instantiated by signencryptfilesdialog.cpp:131 and
signencryptclipboarddialog.cpp:55, but neither calls those methods, so
decrypting via the file or clipboard dialogs does not hit this bug.
EVIDENCE FOR THE CRASH SITE
The shipped binary is stripped, so the fault offset was resolved by hand.
Disassembly at the faulting instruction (ImageBase 0x140000000, fault
0x140163538):
mov 0x28(%rcx),%rax ; rax = d (unique_ptr<Private>)
mov 0x48(%rax),%rbx ; begin \
mov 0x50(%rax),%rdx ; size / QList<UnknownRecipientWidget*> @ 0x40
lea (%rbx,%rdx,8),%rbp ; end -- 8-byte elements
loop:
mov (%rbx),%rdi ; w = *it
mov 0x88(%rax),%rcx ; QVBoxLayout* @ 0x88
mov %rdi,%rdx
call *%r12 ; -> QLayout::removeWidget(QWidget*) [IAT,
Qt6Widgets.dll]
test %rdi,%rdi
je ... ; the NULL check that `delete` emits
mov (%rdi),%rax ; load vtable pointer (succeeds)
add $0x8,%rbx
mov %rdi,%rcx
call *0x20(%rax) ; <<< FAULT: virtual deleting destructor = `delete
w`
mov 0x28(%rsi),%rax
cmp %rbx,%rbp
jne loop
; then ptr @ 0x60, size @ 0x68, shl $0x4 -> QList<GpgME::Key> @ 0x58
- call *0x20(%rax) is the deleting destructor, not qt_metacall: the Q_OBJECT
macro sits at the top of the class body, so it declares metaObject /
qt_metacast / qt_metacall at vtable slots 0x00/0x08/0x10 BEFORE
virtual ~QObject(), putting the complete/deleting destructors at 0x18/0x20.
UnknownRecipientWidget is "class UnknownRecipientWidget : public QWidget"
with Q_OBJECT (unknownrecipientwidget.h:14-16).
- The pointer is explicitly NULL-checked and the vtable load itself succeeds,
so this is a non-NULL dangling pointer, not a NULL dereference.
- Every Private member offset matches the declaration order at
signencryptwidget.cpp:138-146:
0x28 std::vector<RecipientWidgets> mRecpWidgets (24 bytes)
0x40 QList<UnknownRecipientWidget *> mUnknownWidgets (ptr 0x48, size
0x50)
0x58 QList<GpgME::Key> mAddedKeys (ptr 0x60, size 0x68, 16-byte
elems)
0x70 QList<KeyGroup> mAddedGroups (24 bytes)
0x88 QVBoxLayout *mRecpLayout (passed as QLayout* to
removeWidget)
RULING OUT THE ENVIRONMENT
The reporting machine also had an unrelated GnuPG fault ("gpg: sending fd ...
to
keyboxd: Input/output error", every key operation exiting 2). That was cleared
before the most recent crash, and the crash still occurred with a healthy
keyboxd and gpg exiting 0 -- so this defect does not depend on it.
POSSIBLY RELATED
dev.gnupg.org T7476 -- "Kleopatra crashes when decrypting file that was only
encrypted for a hidden recipient" (closed fixed for gpg4win5, Jan 2025). Same
trigger scenario, different defect; the use-after-free described here is still
present in 5.0.2 and in master.
PROPOSED PATCH
Attached: kleopatra-signencryptwidget-uaf.patch -- verified to apply cleanly
(git apply --check, exit 0) to both 782c464a98 (the 5.0.2 revision) and
origin/master @ f80768bc98.
The primary fix is one line, "d->mUnknownWidgets.clear();". The patch also
hardens two related defects in the same area:
1. The keysMayHaveChanged handler iterates d->mUnknownWidgets while its body
calls d->mUnknownWidgets.removeAll(w) on that same container, deletes w,
and
calls addRecipient() which may modify it again. Mutating a container while
range-iterating it invalidates the cached begin/end. Fixed by iterating a
copy.
2. connect() ran on every addUnknownRecipient() call, so N unknown recipients
installed N identical handlers and one keysMayHaveChanged emission executed
the loop N times. The handler already re-checks every widget, so a single
connection suffices.
CAVEAT: the patch has NOT been compiled or run -- no Qt 6 / KF 6 build
environment was available. It is a source-level fix derived from reading the
code; please review accordingly.
--
You are receiving this mail because:
You are watching all bug changes.