skip to content

A long-standing piece of advice says to hold a password or key in a mutable byte or character buffer rather than an immutable string type. State the property that advice is really about, why immutable string types are the wrong container for a secret, and how the answer differs across runtimes with different memory management.

level: middleimportance: must knowfreq 55%

answer

  1. exposure = copies × dwell × observers
  2. immutable: drop reference ≠ erase bytes
  3. interned strings never die
  4. copying GC leaves stale copies behind
  5. native wipe deleted as a dead store

basics

~20 s

The property is the lifetime of plaintext in memory. Immutable strings cannot be erased — you can drop the reference but not the bytes, so the value lingers until collection and beyond. A mutable buffer gives you the ability to overwrite it now.

solid answer

~60 s

The rule is not about the type; it is about **who controls the storage and for how long the plaintext exists**. Exposure is roughly copies × dwell time × observers, and an immutable string maximises all three: you cannot overwrite it, so its bytes persist until the collector reclaims the object *and* the freed memory is reused — and any string constant or interned value may never die at all. A mutable buffer changes exactly one thing: it lets you overwrite the bytes at a moment you choose, shrinking dwell time from "until the runtime feels like it" to "until the next line." Runtimes diverge in ways that matter. A managed runtime with a *copying or compacting* collector relocates live objects, leaving stale copies at the old address that you have no reference to and cannot wipe. A reference-counted runtime frees promptly but still does not zero, and small strings may be interned. Native code can zero and lock pages against swap, but a compiler may delete the zeroing store as dead code — hence the dedicated secure-zero routines.

go deeper

for a junior

Say the core: an immutable string cannot be overwritten, so the password stays in memory until the runtime cleans up and might appear in a heap dump; a byte or character buffer can be zeroed as soon as you are done.

for a middle

Explain lifetime versus reference, mention interning making values permanent, and note that freed memory is not zeroed memory. Be able to say what the control actually protects against — dumps, core files, swap — and what it does not.

for a senior

Bring in the runtime divergence (relocating collectors leaving stale copies, reference counting freeing promptly without zeroing, compilers eliding native wipes) and place the control honestly as window-shortening rather than protection, with dump restrictions as the real bound.

for a principal

Argue the hierarchy: the strongest control is never materialising plaintext in the process — delegate to a keystore or hardware module, stream rather than park, fetch short-lived credentials — with wipeable containers as the fallback where the value must exist locally.

## The property behind the folklore "Use a character array, not a string" is repeated so often that candidates recite it without the reason, and the reason is the whole answer. The quantity being managed is **the lifetime of plaintext inside the process**, and the useful mental model is: > exposure ≈ (number of copies) × (how long each copy lives) × (who can observe process memory) Every control in this area moves one of those three factors. Choosing a wipeable container moves the second. It does not make the secret safe against someone who can read your process while it is in use — nothing at this layer does — it shortens the window during which a *later* observer (a heap dump written at 3am, a core file, a crash report, a debugger attached during an incident) finds the value sitting there. ## Why an immutable string is the wrong container An immutable string offers no operation that changes its contents. That single fact has several consequences: - **You cannot erase it.** Setting the variable to null or letting it fall out of scope removes your *reference*; the bytes stay exactly where they were until the memory is reclaimed and eventually reused for something else. "Reclaimed" is not "overwritten" — freed memory typically keeps its contents until the allocator hands it out again, which may be never. - **Its lifetime is not yours.** Collection happens when the runtime decides, under memory pressure you do not control. On a machine with plenty of headroom, a password can sit in the heap for the life of the process. - **Some strings are immortal by design.** Literal or interned strings live in a table that deliberately keeps one instance forever, so a secret that ever reaches that table never leaves memory. This is why hard-coded credentials are memory-resident as well as source-visible. - **It multiplies quietly.** Because strings are the universal interchange type, a secret in a string gets substringed, trimmed, concatenated into a log message, formatted, encoded, put in a map, serialised to JSON for an outbound call — each step making a new copy you did not plan and cannot track. A buffer is awkward to pass around, and that friction is itself a benefit: it discourages accidental copies. A mutable buffer fixes precisely one of these: it gives you a moment of your choosing at which the bytes become zeroes. That is worth having, and it is a smaller win than the folklore implies. ## How runtimes diverge This is where a strong answer separates itself, because the same advice has different force in different environments: - **Managed runtime with a copying/compacting collector** (typical JVM young-generation collection, and comparable designs elsewhere): live objects are *moved* to new addresses during collection. The old bytes remain at the vacated address with no reference pointing at them, so a wipe performed later touches only the surviving copy. Wiping still helps — it removes the copy you can reach — but it cannot promise the plaintext is gone from the address space. - **Managed runtime with a non-moving collector** (a mark-sweep design, as in some other managed languages): no relocation, so a wipe of the buffer you hold is more nearly complete — but strings remain immutable and unwipeable, and interning still applies to small values. - **Reference-counted runtime**: memory is freed deterministically at the last reference drop, which sounds better and is: the dwell time is short and predictable. But freeing is not zeroing, immutable string types are still unwipeable, and short strings are commonly interned for the process lifetime. - **Native, unmanaged code**: you have the most control and the sharpest footgun. You can overwrite the buffer and lock its pages so they are never written to swap. But an optimising compiler is entitled to delete a store to memory that is never read again — the wipe at the end of a function is textbook dead-store elimination — which is exactly why platforms provide dedicated secure-zero routines that the compiler is forbidden to remove. - **Runtimes that offered a dedicated "secure string" container**: several platforms shipped one and then advised against it for new code, on an honest argument worth repeating in an interview — it cannot protect a secret from an attacker who is already inside the process, and it encourages the belief that it can. The guidance that replaced it is the architectural one: do not hold the secret at all where you can avoid it. ## What the rule is worth, honestly Against an attacker with live code execution in the process, none of this matters; they read the buffer while it is populated. What it genuinely buys: - **Heap dumps and core files.** A dump written on out-of-memory, or captured by a profiler, an APM agent or an operator during an incident, contains every live string in plaintext. Those artefacts get copied into ticket systems and object storage and outlive the incident. Shortening the window during which a secret would appear in one is a real, frequently realised win. - **Swap and hibernation images**, which persist memory contents to disk outside the process's control. - **Later-stage disclosure bugs**, where an out-of-bounds read returns whatever happens to be nearby. ## The better move above the buffer The strongest version of the answer says the container choice is the *second* control, not the first. The first is not to materialise the plaintext in this process at all: verify a password by streaming it into a hashing routine rather than parking it; hold a handle or a reference to a key kept by a separate keystore or hardware module and ask that component to perform the operation; keep the secret out of any long-lived object, cache, session, or exception payload. If it never exists as a durable value, there is nothing to wipe and no dwell time to argue about. ## Answering the question Lead with lifetime, not with types. Say that immutable strings cannot be erased, that their lifetime belongs to the runtime, and that interning can make them permanent. Name the runtime divergence — relocating collectors leave stale copies, reference counting frees promptly without zeroing, native code needs a secure-zero routine because the compiler will delete an ordinary one. Then place the control honestly: it shrinks the window that shows up in dumps and swap; it is not protection against an attacker already executing in the process, and the stronger move is not holding the plaintext at all.

  • If a relocating collector can leave stale copies you cannot reach, is wiping the buffer pointless?
    No, it is bounded rather than pointless. Wiping removes the copy you hold, which is the one most likely to still be live and reachable when a dump is taken, and it caps how long that copy carries plaintext. What it cannot do is promise the process address space is clean, so you should not describe it as a guarantee. The honest framing is window-shortening plus defence in depth, with the real bound coming from restricting who can dump the process.
  • Where does this advice stop being worth the complexity?
    When the secret must live for the whole process anyway — a signing key loaded at startup and used on every request has no window to shorten, so wiping is theatre. It also stops paying when the awkwardness of buffers pushes teams into conversions back and forth, each producing extra copies. At that point the leverage moves to not holding the value at all: delegate the operation to a keystore or hardware module, or fetch a short-lived credential per use.

An immutable string is writing the secret in permanent ink on a page you hand to a librarian: you can forget which shelf it went on, but you cannot un-write it, and only the librarian decides when the page is pulped.

saying these in an interview costs you the question

  • "Setting the variable to null erases the password" — it drops the reference, not the bytes.
  • Believing a mutable buffer makes the secret safe against an attacker with code execution in the process.
  • Forgetting that string interning or literal pooling can keep a secret alive for the entire process.
  • Treating a wipe in native code as guaranteed, when a plain memory-set can be optimised away.
  • Claiming garbage collection zeroes reclaimed memory.

context