monero.loveA little love for the network.
← All news

Monero / Security / Wallets

Monero Developers Address a Memory-Wiping Weakness in Sensitive Strings

Monero PR 11715 proposes wiping old sensitive bytes during string assignment. Our tests compare released code with the patch and explain wallet implications.

Paper-style memory chip illustration showing stored bytes being erased before a buffer is replaced

Monero memory wiping has a gap in epee::wipeable_string, a class used for sensitive text. Its assignment operators can replace an existing value without erasing the old contents first. Depending on the assignment, those bytes can remain in discarded storage or beyond the new string length. PR #11715, opened by contributor jpk68 on November 9, proposes correcting both copy and move assignment.

Our isolated tests reproduced residual dummy bytes in Monero 0.18.5.4 and the PR's base revision. The proposed operators erased the previous contents in the tested cases. This establishes a memory-handling defect. This investigation did not establish a remote attack or compromised funds.

At the November 9, 23:10 UTC research cutoff, the proposal was open, with an approval from selsta and a needs-backport label. The latest stable release remained 0.18.5.4, published October 17. Review approval is not a merge, and neither approval nor a backport request means users already have the fix.

What wipeable_string protects

The class stores characters in a std::vector<char>. Unlike an ordinary string that simply releases its allocation, its destructor calls wipe(). That method passes the current buffer address and length to Monero's memwipe function. The released code also takes care to erase truncated characters during resizing and to wipe old storage during its own growth procedure.

That matters because releasing memory does not necessarily overwrite it. A heap allocator can keep freed storage for later use by the same process. Until those bytes change, a separate ability to inspect memory could expose them. Erasure aims to reduce that residual window.

A destructor can only erase storage the object still owns. If an assignment has already replaced the buffer, the destructor can no longer erase the discarded allocation. If an assignment shortened the vector, wiping its new length can miss characters left beyond that length.

Copy assignment can leave a tail behind

Before the patch, copy assignment checked for self-assignment, then delegated to the vector's copy-assignment operator. It did not call the class's own grow() or wipe() methods.

Consider a destination containing 64 dummy bytes and a source containing eight. Vector assignment can reuse the destination's allocation, overwrite its first eight bytes and change its size to eight. Capacity, the amount of allocated space, can remain larger than size, the number of characters currently in the string.

In our libstdc++ test, 56 old dummy bytes remained immediately before deallocation. The destructor had zeroed the eight characters still counted by the vector. Assigning an empty string left all 64 previous dummy bytes untouched.

A longer incoming value presents another case. If it exceeds the existing capacity, vector assignment allocates replacement storage and releases the old allocation. Our test observed all 64 previous dummy bytes immediately before that release.

Allocator and standard-library behavior can change the amount and lifetime of residual data.

Move assignment replaces ownership

Move assignment transfers the source buffer to the destination. The original operator delegated directly to buffer = std::move(other.buffer) after its self-assignment check.

The destination's previous allocation is separate from the incoming one. Destroying or replacing its std::vector<char> member does not invoke the enclosing wipeable_string destructor. In our test, move assignment released the old destination allocation with its 64 dummy bytes intact.

The destination must already contain sensitive data for assignment to leave a secret behind. A newly created, empty object has no previous contents to erase.

How PR 11715 changes assignment

The complete patch changes one source file. Move assignment wipes the destination before taking ownership of the source buffer.

Copy assignment first constructs a separate vector from the source, then wipes the destination and moves the prepared copy into place. Preparing the copy first means an allocation failure can leave the original destination value intact. Erasing first would destroy it before the replacement allocation succeeds.

Both operators keep their self-assignment guard. Without that check, blindly wiping before copying from the same object would erase the source value.

Conceptual comparison of a 64-byte buffer, eight-byte copy assignment leaving a residual tail, and the proposed wipe-before-replacement sequence

The diagram uses dummy byte counts from the isolated test. It represents buffer ownership and erasure, not a wallet memory capture.

The patch wipes the old vector's size, not its entire capacity. That is appropriate to the previous live contents handled by these operators. Other allocations and copying paths need their own review.

Isolated tests with dummy bytes

We compiled the upstream class with a small allocation observer and non-sensitive repeated characters. The observer counted remaining dummy bytes immediately before freeing the selected allocation. It did not read freed memory, open a wallet, attach to another process or create a memory dump.

The comparison covered the stable source, the exact PR base and the proposed revision. Tests included shorter and longer copy assignment, move assignment, assignment from empty strings, copy and move self-assignment, shrinking, reserve-driven growth, and an injected allocation failure.

Operation on an initially 64-byte destination Original residual dummy bytes Proposed residual dummy bytes
Copy eight bytes, then destroy 56 0
Copy a longer value 64 0
Move an incoming value 64 0
Copy or move an empty value 64 0
Self-assignment, then destroy 0 0
Shrink or reserve through the class methods 0 0

We observed replacement cases when assignment released the old allocation. We inspected retained allocations during destruction. The table compares those specific boundaries rather than suggesting all operations release memory immediately.

Optimized builds and sanitizer checks test different properties. A sanitizer can detect invalid accesses, but it need not flag sensitive bytes remaining in otherwise valid storage. Our byte-count assertions supply that missing check. The matrix covered GCC without optimization, GCC O2, GCC O3 with link-time optimization, Clang O2, and GCC with AddressSanitizer/UBSan. We also selected the explicit_bzero path. All 180 final case checks passed, including assertions that reproduce the original defect. We excluded UBSan vptr checks for the unused logging dependency. The full wallet, other operating systems and alternative standard libraries were not tested.

The upstream PR does not add regression tests. Its existing unit-test file checks construction, explicit wiping and other string operations, but has no dedicated check of discarded assignment buffers. Our local probe is separate evidence, not an upstream test result.

Does the compiler preserve the wipe?

Monero's memwipe source uses memset_s or explicit_bzero when the corresponding build configuration selects them. Its fallback uses memset with compiler barriers intended to prevent removal of an apparently unnecessary write.

We inspected optimized assembly for the fallback and explicit_bzero paths. The generated code retained calls to the zeroing functions. The allocation observations also found zeroed old contents in the patched runs, including the optimized build with link-time optimization.

That is evidence for these compiler configurations. It does not guarantee erasure from registers, temporary copies, swap, hibernation images or recorded dumps. The memset_s branch was not exercised on this Linux test host. Calling a function named memwipe is not, by itself, a universal erasure guarantee.

Passwords and seed-word handling

The released password container stores its password in a wipeable_string. The CLI wallet also uses the class for seed-related text, seed passphrases and textual secret-key inputs. Password assignment into an initially empty local variable starts without an old password to erase.

A more specific repeated-assignment path appears in mnemonic language detection. The loop assigns each successive seed-word prefix to the same trimmed_word variable. After the first iteration, that destination can contain a previous prefix. This repeatedly replaces sensitive mnemonic material through the affected operator.

We did not run a full seed-restoration workflow or recover a complete mnemonic through it. A seed-word prefix is not automatically an entire seed, and the presence of this code does not establish practical recovery of a wallet. The source path connects the buffer defect to actual mnemonic processing.

This is separate from the Monero CLI logging issue. Logging writes information to a file. This proposal concerns bytes left in process memory after replacement.

Access required to read residual data

The reviewed report identifies no network endpoint that returns these residual bytes. An attacker would need a separate way to read the memory while useful remnants still exist. Access to a process dump or another memory-disclosure flaw could provide that capability.

Whether those remnants contain a complete useful value depends on the operation, allocation reuse and later writes. The reviewed material contains no confirmed exploitation or evidence of stolen funds. The author says an LLM found the issue. That attribution describes the discovery method. Our technical conclusion rests on source inspection and dummy-data tests.

What users can do now

No verified configuration switch repairs these assignment operators. Keep wallet processes and diagnostic dumps away from untrusted users and software. Treat crash dumps as potentially sensitive, and do not upload them casually to support services.

Follow the PR and official releases for a merged correction and an identified stable backport. Do not treat an approved patch branch as an official security release. Our download verification guide explains checking signed software releases.

For larger holdings, offline signing can reduce the exposure of spending material to an online system. It does not make the signing device's own memory handling irrelevant.

The confirmed affected source in this investigation is 0.18.5.4, plus the inspected development base. The earliest affected release and a complete version range remain unestablished. A future correction needs an identifiable merged commit and stable release before users can treat it as an available update.

Support Monero Love

Help keep the public nodes and packages running. Send any amount of XMR.

Scan with a Monero wallet to donate to Monero Love.89hGTKxRVA3AUeKAbCRBt5TMSzq3MQZooNNsezCpNFjp2oeJPRx8AJeGKA8ziHZNqab78f4Vr1ntxhPDxX5298nnGZjHkhm