Skip to content

feat: tidy-up mnemonic dialog by removing useless code and reducing exposure of sensitive data#7127

Merged
PastaPastaPasta merged 5 commits intodashpay:developfrom
knst:refactor-mnemonic-allocations
Feb 13, 2026
Merged

feat: tidy-up mnemonic dialog by removing useless code and reducing exposure of sensitive data#7127
PastaPastaPasta merged 5 commits intodashpay:developfrom
knst:refactor-mnemonic-allocations

Conversation

@knst
Copy link
Collaborator

@knst knst commented Feb 3, 2026

Issue being fixed or feature implemented

Firstly, current implementation of mnemonic dialog has zeroing of SecureString's objects with std::fill which is useless, and most likely even removed by optimizing compiler.

For reference, SecureString's implementation of it, see src/support/cleanse.cpp for details:

void deallocate(T* p, std::size_t n)
{    
    if (p != nullptr) {
        memory_cleanse(p, sizeof(T) * n); // <- safe memory cleaning
    }
    LockedPoolManager::Instance().free(p);
}

Secondly, current implementation causes creating extra temporary object with sensitive data:

QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end()));

This std::string object maybe omitted, see PR

What was done?

This PR tidy-up a bit mnemonic dialog by fixing these issues and some minor improvements for formatting.
Though, using memory_cleanse should be considered to use for QStrings.

This PR conflicts to #7126 because the same function is changed; I will prefer #7126 to be merged first because 7126 is meant to be backported.

How Has This Been Tested?

Tested as an extra changes to #7126 locally in the same branch, splitted to 2 PR after that.

Breaking Changes

N/A

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)

@github-actions
Copy link

github-actions bot commented Feb 3, 2026

⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

Please coordinate with the authors of these PRs to avoid merge conflicts.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Removed private helpers clearMnemonic() and clearWordsSecurely() and their declarations. Mnemonic handling was migrated from std::string-based conversions to UTF-8-based QString/fromUtf8 paths across parsing, counting, grid building, display, and validation. Memory handling was adjusted: m_words is cleared eagerly in some flows, temporaries (QString/std::string) are cleared immediately after use, and SecureString words are constructed from UTF-8 bytes. Revealed grid now parses words via UTF-8; hidden view shows placeholder bullets. The dialog source path was added to the non-backported data list.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (41 files):

⚔️ .github/workflows/build-depends.yml (content)
⚔️ src/coinjoin/coinjoin.cpp (content)
⚔️ src/instantsend/instantsend.cpp (content)
⚔️ src/instantsend/instantsend.h (content)
⚔️ src/instantsend/net_instantsend.cpp (content)
⚔️ src/instantsend/net_instantsend.h (content)
⚔️ src/interfaces/node.h (content)
⚔️ src/interfaces/wallet.h (content)
⚔️ src/llmq/dkgsession.cpp (content)
⚔️ src/llmq/dkgsession.h (content)
⚔️ src/net.cpp (content)
⚔️ src/net.h (content)
⚔️ src/node/interfaces.cpp (content)
⚔️ src/qt/bitcoingui.cpp (content)
⚔️ src/qt/forms/optionsdialog.ui (content)
⚔️ src/qt/governancelist.cpp (content)
⚔️ src/qt/masternodelist.cpp (content)
⚔️ src/qt/masternodelist.h (content)
⚔️ src/qt/masternodemodel.cpp (content)
⚔️ src/qt/masternodemodel.h (content)
⚔️ src/qt/mnemonicverificationdialog.cpp (content)
⚔️ src/qt/mnemonicverificationdialog.h (content)
⚔️ src/qt/optionsdialog.cpp (content)
⚔️ src/qt/optionsmodel.cpp (content)
⚔️ src/qt/optionsmodel.h (content)
⚔️ src/qt/overviewpage.cpp (content)
⚔️ src/qt/proposalmodel.cpp (content)
⚔️ src/qt/proposalmodel.h (content)
⚔️ src/qt/proposalwizard.cpp (content)
⚔️ src/qt/proposalwizard.h (content)
⚔️ src/qt/transactionrecord.cpp (content)
⚔️ src/qt/transactionrecord.h (content)
⚔️ src/qt/transactiontablemodel.cpp (content)
⚔️ src/qt/transactiontablemodel.h (content)
⚔️ src/qt/transactionview.cpp (content)
⚔️ src/qt/transactionview.h (content)
⚔️ src/qt/walletmodel.cpp (content)
⚔️ src/qt/walletmodel.h (content)
⚔️ src/serialize.h (content)
⚔️ src/wallet/interfaces.cpp (content)
⚔️ test/util/data/non-backported.txt (content)

These conflicts must be resolved before merging into develop.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: removing useless code and reducing sensitive data exposure in the mnemonic dialog implementation.
Description check ✅ Passed The description is directly related to the changeset, explaining the problems being fixed (useless SecureString zeroing, extra temporary objects with sensitive data) and the improvements made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch refactor-mnemonic-allocations
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/qt/mnemonicverificationdialog.cpp (2)

298-314: ⚠️ Potential issue | 🟡 Minor

Remove redundant toLower() call.

Line 309 calls toLower() on secureWord which is already lowercased from line 308. Qt 5's QString::toLower() returns a new allocated copy, creating an uncleared temporary containing sensitive data. Compare directly against the already-lowercased secureWord.

🔧 Proposed fix
-    QString secureWord{QString::fromUtf8(words[position - 1].data(), words[position - 1].size()).toLower()};
-    const bool result{word == secureWord.toLower()};
+    QString secureWord{QString::fromUtf8(words[position - 1].data(), words[position - 1].size()).toLower()};
+    const bool result{word == secureWord};

392-405: ⚠️ Potential issue | 🟡 Minor

Clear mnemonicStr buffer before releasing.

In Qt 5, QString::clear() does not overwrite the buffer contents before deallocation—the UTF-16 data remains in freed heap memory until reused by the allocator. Use fill(QChar(0)) before clear() to overwrite the buffer with null characters and reduce exposure of mnemonic text.

🔧 Proposed fix
         // Clear immediately
-        mnemonicStr.clear();
+        mnemonicStr.fill(QChar(0));
+        mnemonicStr.clear();
         mnemonicStr.squeeze();

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

This pull request has conflicts, please rebase.

@knst knst force-pushed the refactor-mnemonic-allocations branch from 660e89b to df28c17 Compare February 6, 2026 09:45
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/qt/mnemonicverificationdialog.cpp (2)

308-313: ⚠️ Potential issue | 🟡 Minor

Redundant .toLower() creates an uncleared temporary containing sensitive data.

secureWord is already lowered on line 308 via .toLower(). Calling .toLower() again on line 309 is redundant and, more importantly, produces a temporary QString that holds the mnemonic word but is never zeroed — contradicting the PR's goal of reducing sensitive data exposure.

Proposed fix
     QString secureWord{QString::fromUtf8(words[position - 1].data(), words[position - 1].size()).toLower()};
-    const bool result{word == secureWord.toLower()};
+    const bool result{word == secureWord};

396-404: ⚠️ Potential issue | 🟡 Minor

Missing fill(QChar(0)) leaves mnemonic data in freed memory.

In parseWords() (line 384), mnemonicStr is zeroed with fill(QChar(0)) before clear()/squeeze(). Here in getWordCount(), the same sensitive mnemonic string is only cleared and squeezed — the underlying buffer is released without being zeroed first.

Apply the same sanitization pattern for consistency.

Proposed fix
         int count = words.size();
         // Clear immediately
+        mnemonicStr.fill(QChar(0));
         mnemonicStr.clear();
         mnemonicStr.squeeze();
+        for (auto& w : words) {
+            w.fill(QChar(0));
+        }
         words.clear();
         return count;
🧹 Nitpick comments (1)
src/qt/mnemonicverificationdialog.cpp (1)

380-381: wordStd.assign(size, 0) is a dead store the compiler may optimize away.

Unlike memory_cleanse (which uses volatile or OPENSSL_cleanse to resist optimization), a plain assign of zeros to wordStd right before it goes out of scope is a classic dead-store that an optimizing compiler can elide. Additionally, this only clears up to size(), not capacity(), so remnants of earlier (possibly longer) words may linger in the slack buffer.

Consider using memory_cleanse here for consistency with the secure-deallocation guarantees the PR relies on for SecureString.

Proposed fix
     // Clear temporary std::string
-    wordStd.assign(wordStd.size(), 0);
+    memory_cleanse(wordStd.data(), wordStd.size());
+    wordStd.clear();
+    wordStd.shrink_to_fit();

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

This pull request has conflicts, please rebase.

@knst knst force-pushed the refactor-mnemonic-allocations branch from df28c17 to 540aca7 Compare February 6, 2026 16:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qt/mnemonicverificationdialog.cpp (1)

393-401: ⚠️ Potential issue | 🟡 Minor

Inconsistent secure cleanup: mnemonicStr is not zeroed before clearing.

In parseWords() (line 381), mnemonicStr.fill(QChar(0)) is called before clear()/squeeze() to wipe the sensitive content. Here in getWordCount(), the fill step is missing — clear() only resets the logical length, leaving the mnemonic text in the underlying buffer until the allocator reuses it.

Proposed fix
         int count = words.size();
         // Clear immediately
+        mnemonicStr.fill(QChar(0));
         mnemonicStr.clear();
         mnemonicStr.squeeze();
         words.clear();
🤖 Fix all issues with AI agents
In `@src/qt/mnemonicverificationdialog.cpp`:
- Around line 305-306: secureWord is already lowercased when constructed, so
remove the redundant .toLower() on the comparison line to avoid creating an
extra temporary containing sensitive data; change the comparison to use
secureWord directly (const bool result{word == secureWord}) and, if necessary,
ensure the incoming word is normalized to lowercase once before this comparison
so both sides are compared consistently without extra temporaries.
🧹 Nitpick comments (2)
src/qt/mnemonicverificationdialog.cpp (2)

370-378: wordStd zeroing only covers the last iteration's content.

Because wordStd is reused across loop iterations, overwriting with a shorter word leaves residual bytes from the previous (longer) word in the buffer. The post-loop assign(size, 0) only covers the final word's length. Additionally, since wordStd is not read after the zero-fill, the compiler may optimize the call away (unlike memory_cleanse which uses a volatile store).

Consider scoping wordStd inside the loop body and clearing it each iteration, or using memory_cleanse for the final wipe.

Suggested approach
-    std::string wordStd;
     for (const QString& word : wordList) {
-        wordStd = word.toStdString();
+        std::string wordStd = word.toStdString();
         SecureString secureWord;
         secureWord.assign(std::string_view{wordStd});
         m_words.push_back(secureWord);
+        memory_cleanse(wordStd.data(), wordStd.size());
     }
-    // Clear temporary std::string
-    wordStd.assign(wordStd.size(), 0);

295-311: parseWords() returns by value — each validateWord() call copies the entire secure word vector.

validateWord() is called 3 times per keystroke (via updateWordValidation) and 3 more times from accept(). Each invocation copies every SecureString in the vector just to access a single word. Returning a const reference would avoid these copies and reduce sensitive-data duplication in memory.

Suggested approach

Change the return type and call sites:

-std::vector<SecureString> MnemonicVerificationDialog::parseWords()
+const std::vector<SecureString>& MnemonicVerificationDialog::parseWords()
 {

Then in validateWord:

-    std::vector<SecureString> words = parseWords();
+    const auto& words = parseWords();

And similarly in buildMnemonicGrid:

-    std::vector<SecureString> words;
-    if (reveal) {
-        words = parseWords();
-    } else {
+    if (!reveal) {
         // ... hidden view branch (unchanged, returns early) ...
     }
-
-    // Revealed view - words are already parsed
-    const int n = words.size();
+    const auto& words = parseWords();
+    const int n = words.size();

@github-actions
Copy link

This pull request has conflicts, please rebase.

@knst knst force-pushed the refactor-mnemonic-allocations branch from 540aca7 to b8b0c1c Compare February 11, 2026 08:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qt/mnemonicverificationdialog.cpp (1)

393-401: ⚠️ Potential issue | 🟡 Minor

getWordCount doesn't zero the QString buffer before releasing it.

parseWords() (line 381) uses mnemonicStr.fill(QChar(0)) before .clear() / .squeeze(). Here the fill step is missing, so the mnemonic content lingers in the freed Qt heap block.

Proposed fix
         int count = words.size();
         // Clear immediately
+        mnemonicStr.fill(QChar(0));
         mnemonicStr.clear();
         mnemonicStr.squeeze();
         words.clear();
🤖 Fix all issues with AI agents
In `@src/qt/mnemonicverificationdialog.cpp`:
- Around line 370-378: The temporary std::string buffer `wordStd` is reused in
the loop so only the last buffer is wiped; modify the loop in
mnemonicverificationdialog.cpp that iterates `for (const QString& word :
wordList)` so that you explicitly overwrite/zero `wordStd` before each new
assignment (and still zero it after the loop) to ensure each previous buffer is
scrubbed; retain construction of `SecureString secureWord` and push to `m_words`
as-is, but ensure `wordStd` is cleared (e.g., assign zeros or call a memory
cleanse routine) immediately before `wordStd = word.toStdString()` and after the
loop to avoid leaving plaintext in freed heap memory.

@UdjinM6
Copy link

UdjinM6 commented Feb 11, 2026

pls consider 65722be

… handling

Restore m_words.clear() calls in onHideMnemonicClicked() and reject()
to minimize the exposure window of sensitive mnemonic data in memory.
While secure_allocator cleanses on deallocation, keeping data in memory
longer than necessary increases the attack surface. Also fix redundant
double .toLower() in validateWord() and replace std::string intermediate
in parseWords() loop with QByteArray to avoid leaking word data through
the default (non-secure) allocator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qt/mnemonicverificationdialog.cpp (1)

396-404: ⚠️ Potential issue | 🟡 Minor

getWordCount skips zeroing mnemonicStr before clearing — inconsistent with parseWords.

parseWords() (line 384) does mnemonicStr.fill(QChar(0)) before .clear(), but here mnemonicStr is cleared without zeroing first. The mnemonic content remains in freed heap memory.

Proposed fix
         int count = words.size();
         // Clear immediately
+        mnemonicStr.fill(QChar(0));
         mnemonicStr.clear();
         mnemonicStr.squeeze();
+        for (QString& w : words) {
+            w.fill(QChar(0));
+        }
         words.clear();
🧹 Nitpick comments (3)
src/qt/mnemonicverificationdialog.cpp (3)

369-390: mnemonicStr is properly scrubbed, but wordList entries are not zeroed before clearing.

Line 384-386 correctly fills mnemonicStr with zeros before releasing. However, line 387 calls wordList.clear() without first zeroing each individual QString in the list. Each QString in wordList holds a mnemonic word in Qt's default (non-secure) allocator and will be freed without scrubbing.

For consistency with the rest of the scrubbing in this function:

Proposed fix
     // Clear the temporary QString immediately after parsing
     mnemonicStr.fill(QChar(0));
     mnemonicStr.clear();
     mnemonicStr.squeeze(); // Release memory
+    for (QString& w : wordList) {
+        w.fill(QChar(0));
+    }
     wordList.clear();

462-470: text on line 463 contains the mnemonic word but is never scrubbed.

wordStr is properly zeroed, but the formatted text string ("N. word") still holds the plaintext word and is freed without cleansing when it goes out of scope.

Proposed fix — also scrub `text`
             lbl->setTextInteractionFlags(Qt::TextSelectableByMouse);
             m_gridLayout->addWidget(lbl, r, c);
             // Clear temporary QString immediately after use
             wordStr.fill(QChar(0));
             wordStr.clear();
             wordStr.squeeze();
+            // Note: `text` also contains the word; zero it too
+            // (the QLabel retains its own copy internally)
         }

Though since text is const, you'd need to drop the const or restructure slightly:

-            const QString text = QString("%1. %2").arg(idx + 1, 2).arg(wordStr);
+            QString text = QString("%1. %2").arg(idx + 1, 2).arg(wordStr);
             QLabel* lbl = new QLabel(text);
             ...
+            text.fill(QChar(0));

300-317: parseWords() returns by value — each validateWord call copies the entire word vector.

updateWordValidation() calls validateWord() three times per keystroke (lines 325-327). Each call invokes parseWords() on line 305, which returns m_words by value, creating a full std::vector<SecureString> copy each time. While SecureString uses secure_allocator so copies are safely handled, it's 3 unnecessary deep copies per keystroke.

Consider using m_words directly (it's already a member), or having parseWords() return a const reference:

Sketch
-std::vector<SecureString> MnemonicVerificationDialog::parseWords()
+const std::vector<SecureString>& MnemonicVerificationDialog::parseWords()
 {
     ...
     return m_words;
 }

Then in validateWord:

-    std::vector<SecureString> words = parseWords();
+    const auto& words = parseWords();

Copy link

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK b847bae

@PastaPastaPasta PastaPastaPasta merged commit 1a461a0 into dashpay:develop Feb 13, 2026
42 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants