• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 23795245986

31 Mar 2026 11:36AM UTC coverage: 18.476% (+0.2%) from 18.252%
23795245986

push

github

web-flow
Fix memory leak in QR code popup dialog (#863)

* Fix memory leak in QR code popup dialog

The QDialog created in showTextAsQRCode() was never deleted after
exec() returned, causing a memory leak.

Fix by adding Qt::WA_DeleteOnClose attribute to ensure the dialog
is deleted when closed.

* 📝 CodeRabbit Chat: Add unit tests for changes

* test: remove flaky QR popup auto-deletion test

The qrCodePopupDeletesItselfOnClose test was unreliable - WA_DeleteOnClose
does not guarantee immediate deletion on Qt::Popup windows. The remaining
tests verify:
- Attribute is correctly set (qrCodePopupHasDeleteOnCloseAttribute)
- Without attribute, no auto-deletion occurs

Also fixed clang-format formatting on CodeRabbit's test additions.

* test: add QR popup auto-deletion test using QTRY_VERIFY

The QTRY_VERIFY macro handles the asynchronous nature of Qt::WA_DeleteOnClose
deletion, making the test reliable across different platforms.

Test verifies that a QDialog with Qt::Popup | Qt::FramelessWindowHint
and WA_DeleteOnClose attribute is properly deleted when close() is called.

* test: add codecov coverage for QR popup createQRCodePopup

Extract createQRCodePopup as a static public method to enable direct
testing. This provides codecov coverage for the memory leak fix line
popup->setAttribute(Qt::WA_DeleteOnClose) in showTextAsQRCode.

The test verifies that QtPass::createQRCodePopup creates a QDialog
with Qt::WA_DeleteOnClose attribute set.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

12 of 13 new or added lines in 1 file covered. (92.31%)

58 existing lines in 1 file now uncovered.

931 of 5039 relevant lines covered (18.48%)

7.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.84
/src/qtpass.cpp
1
// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "qtpass.h"
4
#include "mainwindow.h"
5
#include "qtpasssettings.h"
6
#include "util.h"
7
#include <QApplication>
8
#include <QClipboard>
9
#include <QDialog>
10
#include <QLabel>
11
#include <QPixmap>
12
#include <QVBoxLayout>
13

14
#ifndef Q_OS_WIN
15
#include <QInputDialog>
16
#include <QLineEdit>
17
#include <utility>
18
#else
19
#define WIN32_LEAN_AND_MEAN /*_KILLING_MACHINE*/
20
#define WIN32_EXTRA_LEAN
21
#include <windows.h>
22
#include <winnetwk.h>
23
#undef DELETE
24
#endif
25

26
#ifdef QT_DEBUG
27
#include "debughelper.h"
28
#endif
29

30
QtPass::QtPass(MainWindow *mainWindow)
×
31
    : m_mainWindow(mainWindow), freshStart(true) {
×
32
  setClipboardTimer();
×
33
  clearClipboardTimer.setSingleShot(true);
×
34
  connect(&clearClipboardTimer, &QTimer::timeout, this,
×
35
          &QtPass::clearClipboard);
×
36

37
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
38
#pragma GCC diagnostic push
39
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
40
#endif
41
  QObject::connect(qApp, &QApplication::aboutToQuit, this,
×
42
                   &QtPass::clearClipboard);
×
43
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
44
#pragma GCC diagnostic pop
45
#endif
46

47
  setMainWindow();
×
48
}
×
49

50
/**
51
 * @brief QtPass::~QtPass destroy!
52
 */
53
QtPass::~QtPass() {
×
54
#ifdef Q_OS_WIN
55
  if (QtPassSettings::isUseWebDav())
56
    WNetCancelConnection2A(QtPassSettings::getPassStore().toUtf8().constData(),
57
                           0, 1);
58
#else
59
  if (fusedav.state() == QProcess::Running) {
×
60
    fusedav.terminate();
×
61
    fusedav.waitForFinished(2000);
×
62
  }
63
#endif
64
}
×
65

66
/**
67
 * @brief QtPass::init make sure we are ready to go as soon as
68
 * possible
69
 */
70
auto QtPass::init() -> bool {
×
71
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
×
72
  QtPassSettings::setPassStore(passStore);
×
73

74
  QtPassSettings::initExecutables();
×
75

76
  QString version = QtPassSettings::getVersion();
×
77
  // dbg()<< version;
78

79
  // Config updates
80
  if (version.isEmpty()) {
×
81
#ifdef QT_DEBUG
82
    dbg() << "assuming fresh install";
83
#endif
84

85
    if (QtPassSettings::getAutoclearSeconds() < 5) {
×
86
      QtPassSettings::setAutoclearSeconds(10);
×
87
    }
88
    if (QtPassSettings::getAutoclearPanelSeconds() < 5) {
×
89
      QtPassSettings::setAutoclearPanelSeconds(10);
×
90
    }
91
    if (!QtPassSettings::getPwgenExecutable().isEmpty()) {
×
92
      QtPassSettings::setUsePwgen(true);
×
93
    } else {
94
      QtPassSettings::setUsePwgen(false);
×
95
    }
96
    QtPassSettings::setPassTemplate("login\nurl");
×
97
  } else {
98
    // QStringList ver = version.split(".");
99
    // dbg()<< ver;
100
    // if (ver[0] == "0" && ver[1] == "8") {
101
    //// upgrade to 0.9
102
    // }
103
    if (QtPassSettings::getPassTemplate().isEmpty()) {
×
104
      QtPassSettings::setPassTemplate("login\nurl");
×
105
    }
106
  }
107

108
  QtPassSettings::setVersion(VERSION);
×
109

110
  if (Util::checkConfig()) {
×
111
    m_mainWindow->config();
×
112
    if (freshStart && Util::checkConfig()) {
×
113
      return false;
114
    }
115
  }
116

117
  // TODO(annejan): this needs to be before we try to access the store,
118
  // but it would be better to do it after the Window is shown,
119
  // as the long delay it can cause is irritating otherwise.
120
  if (QtPassSettings::isUseWebDav()) {
×
121
    mountWebDav();
×
122
  }
123

124
  freshStart = false;
×
125
  // startupPhase = false;
126
  return true;
×
127
}
128

129
void QtPass::setMainWindow() {
×
130
  m_mainWindow->restoreWindow();
×
131

132
  fusedav.setParent(m_mainWindow);
×
133

134
  // TODO(bezet): this should be reconnected dynamically when pass changes
135
  connectPassSignalHandlers(QtPassSettings::getRealPass());
×
136
  connectPassSignalHandlers(QtPassSettings::getImitatePass());
×
137

138
  connect(m_mainWindow, &MainWindow::passShowHandlerFinished, this,
×
139
          &QtPass::passShowHandlerFinished);
×
140

141
  // only for ipass
142
  connect(QtPassSettings::getImitatePass(), &ImitatePass::startReencryptPath,
×
143
          m_mainWindow, &MainWindow::startReencryptPath);
×
144
  connect(QtPassSettings::getImitatePass(), &ImitatePass::endReencryptPath,
×
145
          m_mainWindow, &MainWindow::endReencryptPath);
×
146

147
  connect(m_mainWindow, &MainWindow::passGitInitNeeded, [=]() -> void {
×
148
#ifdef QT_DEBUG
149
    dbg() << "Pass git init called";
150
#endif
151
    QtPassSettings::getPass()->GitInit();
×
152
  });
×
153

154
  connect(m_mainWindow, &MainWindow::generateGPGKeyPair, m_mainWindow,
×
155
          [=](const QString &batch) -> void {
×
156
            QtPassSettings::getPass()->GenerateGPGKeys(batch);
×
157
            m_mainWindow->showStatusMessage(tr("Generating GPG key pair"),
×
158
                                            60000);
159
          });
×
160
}
×
161

162
void QtPass::connectPassSignalHandlers(Pass *pass) {
×
163
  connect(pass, &Pass::error, this, &QtPass::processError);
×
164
  connect(pass, &Pass::processErrorExit, this, &QtPass::processErrorExit);
×
165
  connect(pass, &Pass::critical, m_mainWindow, &MainWindow::critical);
×
166
  connect(pass, &Pass::startingExecuteWrapper, m_mainWindow,
×
167
          &MainWindow::executeWrapperStarted);
×
168
  connect(pass, &Pass::statusMsg, m_mainWindow, &MainWindow::showStatusMessage);
×
169
  connect(pass, &Pass::finishedShow, m_mainWindow,
×
170
          &MainWindow::passShowHandler);
×
171
  connect(pass, &Pass::finishedOtpGenerate, m_mainWindow,
×
172
          &MainWindow::passOtpHandler);
×
173

174
  connect(pass, &Pass::finishedGitInit, this, &QtPass::passStoreChanged);
×
175
  connect(pass, &Pass::finishedGitPull, this, &QtPass::processFinished);
×
176
  connect(pass, &Pass::finishedGitPush, this, &QtPass::processFinished);
×
177
  connect(pass, &Pass::finishedInsert, this, &QtPass::finishedInsert);
×
178
  connect(pass, &Pass::finishedRemove, this, &QtPass::passStoreChanged);
×
179
  connect(pass, &Pass::finishedInit, this, &QtPass::passStoreChanged);
×
180
  connect(pass, &Pass::finishedMove, this, &QtPass::passStoreChanged);
×
181
  connect(pass, &Pass::finishedCopy, this, &QtPass::passStoreChanged);
×
182
  connect(pass, &Pass::finishedGenerateGPGKeys, this,
×
183
          &QtPass::onKeyGenerationComplete);
×
184
}
×
185

186
/**
187
 * @brief QtPass::mountWebDav is some scary voodoo magic
188
 */
189
void QtPass::mountWebDav() {
×
190
#ifdef Q_OS_WIN
191
  char dst[20] = {0};
192
  NETRESOURCEA netres;
193
  memset(&netres, 0, sizeof(netres));
194
  netres.dwType = RESOURCETYPE_DISK;
195
  netres.lpLocalName = 0;
196
  netres.lpRemoteName = QtPassSettings::getWebDavUrl().toUtf8().data();
197
  DWORD size = sizeof(dst);
198
  DWORD r = WNetUseConnectionA(
199
      reinterpret_cast<HWND>(m_mainWindow->effectiveWinId()), &netres,
200
      QtPassSettings::getWebDavPassword().toUtf8().constData(),
201
      QtPassSettings::getWebDavUser().toUtf8().constData(),
202
      CONNECT_TEMPORARY | CONNECT_INTERACTIVE | CONNECT_REDIRECT, dst, &size,
203
      0);
204
  if (r == NO_ERROR) {
205
    QtPassSettings::setPassStore(dst);
206
  } else {
207
    char message[256] = {0};
208
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, r, 0, message,
209
                   sizeof(message), 0);
210
    m_mainWindow->flashText(tr("Failed to connect WebDAV:\n") + message +
211
                                " (0x" + QString::number(r, 16) + ")",
212
                            true);
213
  }
214
#else
215
  fusedav.start("fusedav", QStringList()
×
216
                               << "-o"
×
217
                               << "nonempty"
×
218
                               << "-u"
×
219
                               << "\"" + QtPassSettings::getWebDavUser() + "\""
×
220
                               << QtPassSettings::getWebDavUrl()
×
221
                               << "\"" + QtPassSettings::getPassStore() + "\"");
×
222
  fusedav.waitForStarted();
×
223
  if (fusedav.state() == QProcess::Running) {
×
224
    QString pwd = QtPassSettings::getWebDavPassword();
×
225
    bool ok = true;
×
226
    if (pwd.isEmpty()) {
×
227
      pwd = QInputDialog::getText(m_mainWindow, tr("QtPass WebDAV password"),
×
228
                                  tr("Enter password to connect to WebDAV:"),
×
229
                                  QLineEdit::Password, "", &ok);
230
    }
231
    if (ok && !pwd.isEmpty()) {
×
232
      fusedav.write(pwd.toUtf8() + '\n');
×
233
      fusedav.closeWriteChannel();
×
234
      fusedav.waitForFinished(2000);
×
235
    } else {
236
      fusedav.terminate();
×
237
    }
238
  }
239
  QString error = fusedav.readAllStandardError();
×
240
  int prompt = error.indexOf("Password:");
×
241
  if (prompt >= 0) {
×
242
    error.remove(0, prompt + 10);
×
243
  }
244
  if (fusedav.state() != QProcess::Running) {
×
245
    error = tr("fusedav exited unexpectedly\n") + error;
×
246
  }
247
  if (error.size() > 0) {
×
248
    m_mainWindow->flashText(
×
249
        tr("Failed to start fusedav to connect WebDAV:\n") + error, true);
×
250
  }
251
#endif
252
}
×
253

254
/**
255
 * @brief QtPass::processError something went wrong
256
 * @param error
257
 */
258
void QtPass::processError(QProcess::ProcessError error) {
×
259
  QString errorString;
×
260
  switch (error) {
×
261
  case QProcess::FailedToStart:
×
262
    errorString = tr("QProcess::FailedToStart");
×
263
    break;
×
264
  case QProcess::Crashed:
×
265
    errorString = tr("QProcess::Crashed");
×
266
    break;
×
267
  case QProcess::Timedout:
×
268
    errorString = tr("QProcess::Timedout");
×
269
    break;
×
270
  case QProcess::ReadError:
×
271
    errorString = tr("QProcess::ReadError");
×
272
    break;
×
273
  case QProcess::WriteError:
×
274
    errorString = tr("QProcess::WriteError");
×
275
    break;
×
276
  case QProcess::UnknownError:
×
277
    errorString = tr("QProcess::UnknownError");
×
278
    break;
×
279
  }
280

281
  m_mainWindow->flashText(errorString, true);
×
282
  m_mainWindow->setUiElementsEnabled(true);
×
283
}
×
284

285
void QtPass::processErrorExit(int exitCode, const QString &p_error) {
×
286
  if (nullptr != m_mainWindow->getKeygenDialog()) {
×
287
    m_mainWindow->cleanKeygenDialog();
×
288
    if (exitCode != 0) {
×
289
      m_mainWindow->showStatusMessage(tr("GPG key pair generation failed"),
×
290
                                      10000);
291
    }
292
  }
293

294
  if (!p_error.isEmpty()) {
×
295
    QString output;
×
296
    QString error = p_error.toHtmlEscaped();
×
297
    if (exitCode == 0) {
×
298
      //  https://github.com/IJHack/qtpass/issues/111
299
      output = "<span style=\"color: darkgray;\">" + error + "</span><br />";
×
300
    } else {
301
      output = "<span style=\"color: red;\">" + error + "</span><br />";
×
302
    }
303

304
    output.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
305
    output.replace(QStringLiteral("\n"), "<br />");
×
306

307
    m_mainWindow->flashText(output, false, true);
×
308
  }
309

310
  m_mainWindow->setUiElementsEnabled(true);
×
311
}
×
312

313
/**
314
 * @brief QtPass::processFinished background process has finished
315
 * @param exitCode
316
 * @param exitStatus
317
 * @param output    stdout from a process
318
 * @param errout    stderr from a process
319
 */
320
void QtPass::processFinished(const QString &p_output, const QString &p_errout) {
×
321
  showInTextBrowser(p_output);
×
322
  //    Sometimes there is error output even with 0 exit code, which is
323
  //    assumed in this function
324
  processErrorExit(0, p_errout);
×
325

326
  m_mainWindow->setUiElementsEnabled(true);
×
327
}
×
328

329
void QtPass::passStoreChanged(const QString &p_out, const QString &p_err) {
×
330
  processFinished(p_out, p_err);
×
331
  doGitPush();
×
332
}
×
333

334
void QtPass::finishedInsert(const QString &p_output, const QString &p_errout) {
×
335
  processFinished(p_output, p_errout);
×
336
  doGitPush();
×
337
  m_mainWindow->on_treeView_clicked(m_mainWindow->getCurrentTreeViewIndex());
×
338
}
×
339

340
void QtPass::onKeyGenerationComplete(const QString &p_output,
×
341
                                     const QString &p_errout) {
342
  if (nullptr != m_mainWindow->getKeygenDialog()) {
×
343
#ifdef QT_DEBUG
344
    qDebug() << "Keygen Done";
345
#endif
346

347
    m_mainWindow->cleanKeygenDialog();
×
348
    m_mainWindow->showStatusMessage(tr("GPG key pair generated successfully"),
×
349
                                    10000);
350
  }
351

352
  processFinished(p_output, p_errout);
×
353
}
×
354

355
void QtPass::passShowHandlerFinished(QString output) {
×
356
  showInTextBrowser(std::move(output));
×
357
}
×
358

359
void QtPass::showInTextBrowser(QString output, const QString &prefix,
×
360
                               const QString &postfix) {
361
  output = output.toHtmlEscaped();
×
362

363
  output.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
364
  output.replace(QStringLiteral("\n"), "<br />");
×
365
  output = prefix + output + postfix;
×
366

367
  m_mainWindow->flashText(output, false, true);
×
368
}
×
369

370
void QtPass::doGitPush() {
×
371
  if (QtPassSettings::isAutoPush()) {
×
372
    m_mainWindow->onPush();
×
373
  }
374
}
×
375

376
void QtPass::setClippedText(const QString &password, const QString &p_output) {
×
377
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER &&
×
378
      !p_output.isEmpty()) {
379
    clippedText = password;
×
380
    if (QtPassSettings::getClipBoardType() == Enums::CLIPBOARD_ALWAYS) {
×
381
      copyTextToClipboard(password);
×
382
    }
383
  }
384
}
×
385
void QtPass::clearClippedText() { clippedText = ""; }
×
386

387
void QtPass::setClipboardTimer() {
×
388
  clearClipboardTimer.setInterval(MS_PER_SECOND *
×
389
                                  QtPassSettings::getAutoclearSeconds());
×
390
}
×
391

392
/**
393
 * @brief MainWindow::clearClipboard remove clipboard contents.
394
 */
395
void QtPass::clearClipboard() {
×
396
  QClipboard *clipboard = QApplication::clipboard();
×
397
  bool cleared = false;
398
  if (this->clippedText == clipboard->text(QClipboard::Selection)) {
×
399
    clipboard->clear(QClipboard::Selection);
×
400
    clipboard->setText(QString(""), QClipboard::Selection);
×
401
    cleared = true;
402
  }
403
  if (this->clippedText == clipboard->text(QClipboard::Clipboard)) {
×
404
    clipboard->clear(QClipboard::Clipboard);
×
405
    cleared = true;
406
  }
407
  if (cleared) {
×
408
    m_mainWindow->showStatusMessage(tr("Clipboard cleared"));
×
409
  } else {
410
    m_mainWindow->showStatusMessage(tr("Clipboard not cleared"));
×
411
  }
412

413
  clippedText.clear();
×
414
}
×
415

416
/**
417
 * @brief MainWindow::copyTextToClipboard copies text to your clipboard
418
 * @param text
419
 */
420
void QtPass::copyTextToClipboard(const QString &text) {
×
421
  QClipboard *clip = QApplication::clipboard();
×
422
  if (!QtPassSettings::isUseSelection()) {
×
423
    clip->setText(text, QClipboard::Clipboard);
×
424
  } else {
425
    clip->setText(text, QClipboard::Selection);
×
426
  }
427

428
  clippedText = text;
×
429
  m_mainWindow->showStatusMessage(tr("Copied to clipboard"));
×
430
  if (QtPassSettings::isUseAutoclear()) {
×
431
    clearClipboardTimer.start();
×
432
  }
433
}
×
434

435
/**
436
 * @brief displays the text as qrcode
437
 * @param text
438
 */
439
void QtPass::showTextAsQRCode(const QString &text) {
×
440
  QProcess qrencode;
×
441
  qrencode.start(QtPassSettings::getQrencodeExecutable("/usr/bin/qrencode"),
×
442
                 QStringList() << "-o-"
×
443
                               << "-tPNG");
×
444
  qrencode.write(text.toUtf8());
×
445
  qrencode.closeWriteChannel();
×
446
  qrencode.waitForFinished();
×
447
  QByteArray output(qrencode.readAllStandardOutput());
×
448

449
  if (qrencode.exitStatus() || qrencode.exitCode()) {
×
450
    QString error(qrencode.readAllStandardError());
×
451
    m_mainWindow->showStatusMessage(error);
×
452
  } else {
453
    QPixmap image;
×
454
    image.loadFromData(output, "PNG");
×
NEW
455
    QDialog *popup = createQRCodePopup(image);
×
456
    popup->exec();
×
457
  }
×
458
}
×
459

460
/**
461
 * @brief QtPass::createQRCodePopup creates a popup dialog with the given QR
462
 * code image. This is extracted for testability. The caller is responsible
463
 * for showing and managing the popup lifecycle.
464
 * @param image The QR code pixmap to display
465
 * @return The created popup dialog
466
 */
467
QDialog *QtPass::createQRCodePopup(const QPixmap &image) {
1✔
468
  auto *popup = new QDialog(nullptr, Qt::Popup | Qt::FramelessWindowHint);
1✔
469
  popup->setAttribute(Qt::WA_DeleteOnClose);
1✔
470
  auto *layout = new QVBoxLayout;
1✔
471
  auto *popupLabel = new QLabel();
1✔
472
  layout->addWidget(popupLabel);
1✔
473
  popupLabel->setPixmap(image);
1✔
474
  popupLabel->setScaledContents(true);
1✔
475
  popupLabel->show();
1✔
476
  popup->setLayout(layout);
1✔
477
  popup->move(QCursor::pos());
1✔
478
  return popup;
1✔
479
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc