Skip to content

security(tw_memory): Zeroize Rust TWData contents before deallocation #4841

Description

@lanecole

Summary

The Rust implementation of TWData releases its underlying Vec<u8> without securely erasing its contents.

This can leave sensitive data—including decrypted wallet plaintext and derived key material—in allocator-managed memory after TWData is deleted. The equivalent C++ issue was fixed in PR #4667, but Rust TWData is an independent implementation and is not covered by that fix.

Details

Rust TWData directly owns a regular Vec<u8>:

#[derive(Clone, Debug, Default)]
pub struct TWData(Data);

Its deletion function reconstructs the owner and relies on the default Drop implementation:

pub unsafe extern "C" fn tw_data_delete(data: *mut TWData) {
    let _ = TWData::from_ptr(data);
}

Dropping a regular Vec<u8> deallocates its buffer without securely erasing its contents.

One affected path is cloud backup wallet restoration:

let plaintext = try_or_else!(
    session
        .access(|session| session.restore_wallet(wid, ep.as_slice()))
        .and_then(Result::ok),
    std::ptr::null_mut
);
TWData::from(plaintext.to_vec()).into_ptr()

Although plaintext is a Zeroizing<Vec<u8>>, plaintext.to_vec() creates a separate copy. That copy is moved into TWData and is not zeroized when tw_data_delete is called.

The problem applies globally to sensitive values stored in Rust TWData. It also affects implicit destruction paths, such as TWData instances owned by TWDataVector, because zeroization is not enforced by the TWData type itself.

The independent C++ implementation already establishes the expected invariant by calling memzero before deallocation:

void TWDataDelete(TWData *_Nonnull data) {
    auto* vConst = reinterpret_cast<const Data*>(data);
    if (!vConst->empty()) {
        auto* v = const_cast<Data*>(vConst);
        memzero(v->data(), v->size());
    }
    delete vConst;
}

Rust TWData should enforce the same invariant through its destruction semantics, covering every direct and container-owned destruction path.

PoC

  1. Create a Rust TWData containing a recognizable sensitive pattern:
let data = TWData::from(b"TW_DATA_SENSITIVE_PLAINTEXT".to_vec());
let ptr = data.into_ptr();
  1. Obtain the underlying buffer address and length:
let bytes = unsafe { tw_data_bytes(ptr) };
let size = unsafe { tw_data_size(ptr) };
  1. Call the Rust deletion function:
unsafe { tw_data_delete(ptr) };
  1. Observe the buffer immediately before allocator deallocation with a debugger or instrumented allocator.

The bytes passed to the allocator still contain:

TW_DATA_SENSITIVE_PLAINTEXT

No zeroization operation occurs between tw_data_delete taking ownership and Vec<u8> deallocating its buffer.

Reading the buffer after deallocation would itself be undefined behavior; allocator/debugger instrumentation should therefore inspect it at the deallocation boundary.

Impact

This is a sensitive-memory-remanence vulnerability.

Applications using Rust-backed wallet-core APIs may leave decrypted wallet plaintext, derived keys, passwords, or other sensitive TWData contents in process heap memory after deletion. The residual data may be exposed through:

  • process memory dumps;
  • crash reports or core dumps;
  • debugger or forensic inspection;
  • allocator reuse following another memory-safety vulnerability.

The issue affects every caller that stores sensitive material in the Rust TWData implementation. It is not limited to the cloud backup restore path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions