<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on kqdu</title><link>https://kqdu.io/posts/</link><description>Recent content in Posts on kqdu</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 03 Aug 2026 13:37:37 +0000</lastBuildDate><atom:link href="https://kqdu.io/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Go's runtime/secret: wiping sensitive memory is harder than it looks</title><link>https://kqdu.io/posts/go-runtime-secret/</link><pubDate>Mon, 03 Aug 2026 13:37:37 +0000</pubDate><guid>https://kqdu.io/posts/go-runtime-secret/</guid><description>Go 1.26 ships an experimental runtime/secret package. How to use it, how to check it actually works, and why you probably don&amp;#39;t need it.</description><content:encoded><![CDATA[<p>The most expensive thing in a system I used to work on was 32 bytes long. Leak it before a certain moment and it would have taken millions ($) with it. Not the servers, not the data, not the code. Literally just the bytes. They sat in the memory of a Go process, waiting for the garbage collector to get around to them.</p>
<p>Eight years in tech, a good part of it around security, taught me enough to know my way around this problem, even if systems programming is a place I visit rather than call home. Still, the thought showed up uninvited: this secret is just sitting there. Shouldn&rsquo;t somebody wipe it?</p>
<p>Go 1.26 finally made &ldquo;somebody&rdquo; the runtime itself. The experimental <code>runtime/secret</code> package is two functions. Getting them took over eight years, and using them correctly is harder than it looks.</p>
<h2 id="how-secrets-leak-from-memory">How secrets leak from memory</h2>
<p>Process memory is not supposed to be readable by anyone else. &ldquo;Supposed&rdquo; carries a lot of weight there. Memory ends up in swap, in core dumps (which plenty of infra helpfully ships to third-party crash services), in cloud VM snapshots, in panic tracebacks that print raw argument words into logs. The crash dump path has a famous casualty: a Microsoft signing key slipped past redaction into a crash dump, the dump left the isolated production network, and <a href="https://www.microsoft.com/en-us/msrc/blog/2023/09/results-of-major-technical-investigations-for-storm-0558-key-acquisition/">Storm-0558</a> ended with forged tokens and government mailboxes read. Sometimes the isolation itself breaks: <a href="https://heartbleed.com/">Heartbleed</a> let strangers read server memory over TLS, and <a href="https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/">Cloudbleed</a> had Cloudflare&rsquo;s edge leaking fragments of process memory into cached HTTP responses.</p>
<p>How much this matters depends on how long the secret lives. A session key that exists for 200 milliseconds has a tiny exposure window; a key that sits in memory for hours is in every dump and snapshot taken during those hours. Mine was the second kind.</p>
<h2 id="why-a-zeroing-loop-doesnt-work">Why a zeroing loop doesn&rsquo;t work</h2>
<p>The obvious fix: overwrite the buffer when done. <code>for i := range key { key[i] = 0 }</code>.</p>
<p>Except the secret doesn&rsquo;t live in one buffer. It gets copied into function arguments, registers, and stack spill slots the compiler never told you about. When a goroutine stack grows, the runtime copies the whole thing and frees the old one without zeroing it, stale key included. Convert <code>[]byte</code> to <code>string</code> and you&rsquo;ve made an immutable copy nothing can scrub. Hand the key to an AEAD and the expanded key schedule lives in internal state you have no pointer to. Your loop cleans one copy out of many; an attacker reading memory can reassemble the rest.</p>
<p>C at least has <code>explicit_bzero</code> for the narrow problem of optimizers deleting &ldquo;useless&rdquo; writes. Go had nothing, so people coped with hacks. This is roughly what WireGuard&rsquo;s Go implementation did to clear an AEAD&rsquo;s key state:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">s</span><span class="w"> </span><span class="o">*</span><span class="nx">safeAEAD</span><span class="p">)</span><span class="w"> </span><span class="nb">clear</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">aead</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">reflect</span><span class="p">.</span><span class="nf">ValueOf</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nx">aead</span><span class="p">).</span><span class="nf">Elem</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">v</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="nx">reflect</span><span class="p">.</span><span class="nf">Zero</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nf">Type</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">s</span><span class="p">.</span><span class="nx">aead</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>If your security depends on <code>reflect.Zero</code>, something upstream has failed you.</p>
<h2 id="eight-years-of-issue-21865">Eight years of issue #21865</h2>
<p>That snippet is not a random example. In September 2017 Jason A. Donenfeld, the author of WireGuard, opened <a href="https://github.com/golang/go/issues/21865">golang/go#21865</a> showing exactly that hack, &ldquo;unholy&rdquo; by his own description, and asking for something better. WireGuard promises forward secrecy: past traffic stays safe even if the machine is compromised later. That only holds if ephemeral keys are actually gone from memory.</p>
<p>The thread ran for over eight years. Every simple idea (manual zeroing, mlock enclaves, <code>Clear()</code> methods on crypto APIs) broke against the same wall: in Go the runtime owns memory, so only the runtime knows where the copies went, and only the runtime can erase them. What finally landed, implemented by Daniel Morsing, might be the smallest API in the standard library:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Do</span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="kd">func</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Enabled</span><span class="p">()</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span></code></pre></div><p>Behind it, the scheduler, the garbage collector and stack management all had to learn a new mode. Each of those sounds routine until you hit the details. One of them: when a signal arrives, the kernel dumps every register onto the signal stack, and you can&rsquo;t erase that inside the handler, because the registers get restored from it on return. The fix waits: the thread gets flagged, and the runtime erases its signal stack later, from a safe point outside the handler. Smallest API, one of the hairiest implementations.</p>
<h2 id="what-do-promises">What Do promises</h2>
<p>Read <a href="https://pkg.go.dev/runtime/secret">the package docs</a> first. They&rsquo;re short and good; this is the summary plus the parts I&rsquo;d flag.</p>
<p><code>secret.Do(f)</code> runs <code>f</code>, meaning the whole call tree under it, in secret mode. When <code>Do</code> returns, every register and stack byte that tree touched has been zeroed. Heap allocations made inside get erased too, but only once the GC notices they&rsquo;re unreachable (more on this below). All of it survives panics and <code>runtime.Goexit</code>, though a panic will appear to originate from <code>Do</code> itself.</p>
<p>The fine print:</p>
<ul>
<li>Linux amd64/arm64 only. Everywhere else <code>Do</code> silently just calls <code>f</code>. Your Mac gives zero protection while behaving identically.</li>
<li>Global variables written by <code>f</code> are not protected.</li>
<li>In Go 1.26, goroutines started inside <code>Do</code> are not protected either. Own section below.</li>
<li>Don&rsquo;t encode secrets into pointer values; addresses can linger in the GC&rsquo;s own bookkeeping.</li>
</ul>
<p>The package only exists under <code>GOEXPERIMENT=runtimesecret</code>, so the import fails on a normal build. Hide it behind the build tag the experiment defines:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// secret_on.go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="cp">//go:build goexperiment.runtimesecret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">sealed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="s">&#34;runtime/secret&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Do</span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="kd">func</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">f</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// secret_off.go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="cp">//go:build !goexperiment.runtimesecret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">sealed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Do</span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="kd">func</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nf">f</span><span class="p">()</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><code>Do</code> returns nothing; errors go out through the closure, a pattern Keith Randall <a href="https://github.com/golang/go/issues/21865#issuecomment-3647103813">showed in the thread</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">out</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">deriveKey</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// handle it out here, in the ordinary world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><img alt="kitten tangled in unrolled toilet paper" loading="lazy" src="/images/cat-paper.jpg">
<em>runtime/secret, artist&rsquo;s impression.</em></p>
<h2 id="the-heap-asterisk">The heap asterisk</h2>
<p>Stack and registers are wiped before <code>Do</code> returns. That&rsquo;s a hard guarantee. Heap allocations are wiped when the GC notices they&rsquo;re unreachable, which is two conditions: you drop every reference, and the GC actually runs. An idle service might not collect for a couple of minutes (the runtime forces a GC about that often), so a &ldquo;wiped&rdquo; secret can survive in dead memory for minutes. Keep any reference alive (a slice header in some struct, a captured variable) and it is never erased, and nothing warns you. The docs are also upfront that allocating inside <code>Do</code> costs tracking memory and longer GC sweeps.</p>
<p>The annoying part: you can&rsquo;t check where a value went. Escape analysis is a compiler implementation detail, and there is no <code>if onHeap(x)</code>. So the discipline is fixed-size arrays passed by pointer (<code>[32]byte</code>, not <code>make([]byte, 32)</code>), results copied into caller-created storage, and a test pinning it down:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">TestDeriveOutcomeZeroAlloc</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">out</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">msg</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;client-input:42&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">allocs</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">testing</span><span class="p">.</span><span class="nf">AllocsPerRun</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">allocs</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;expected 0 allocations, got %v&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">allocs</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The test fails in CI the day a refactor or a new compiler version starts leaking the derivation onto the heap.</p>
<p><img alt="cat pushing a glass off a table" loading="lazy" src="/images/cat-glass.jpg">
<em>The garbage collector, deciding when your allocation gets to die.</em></p>
<h2 id="stack-only-code-hurts">Stack-only code hurts</h2>
<p>The natural way to derive an output from a secret seed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">mac</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">hmac</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">sha256</span><span class="p">.</span><span class="nx">New</span><span class="p">,</span><span class="w"> </span><span class="nx">seed</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">mac</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">mac</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Four lines, and it allocates: <code>hmac.New</code> builds two hash states on the heap, both full of key material. Inside <code>secret.Do</code> this works, the runtime eventually erases all of it. But &ldquo;eventually&rdquo; is the word we&rsquo;re trying to delete.</p>
<p>The stack-only version builds HMAC by hand from <code>sha256.Sum256</code>, which doesn&rsquo;t allocate:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="nx">maxMsgLen</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">64</span><span class="w"> </span><span class="c1">// plenty for &#34;client-input:nonce&#34; style messages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// deriveOutcome computes HMAC-SHA256(seed, msg) with no heap allocations.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Everything local stays on the stack, which is exactly the memory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// secret.Do wipes on return.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="nx">seed</span><span class="w"> </span><span class="o">*</span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">out</span><span class="w"> </span><span class="o">*</span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="nx">maxMsgLen</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nb">panic</span><span class="p">(</span><span class="s">&#34;deriveOutcome: message too long&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">inner</span><span class="w"> </span><span class="p">[</span><span class="mi">64</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">maxMsgLen</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">outer</span><span class="w"> </span><span class="p">[</span><span class="mi">64</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">sha256</span><span class="p">.</span><span class="nx">Size</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="mi">64</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">inner</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x36</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">outer</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x5c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">seed</span><span class="p">);</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">inner</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">^=</span><span class="w"> </span><span class="nx">seed</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">outer</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">^=</span><span class="w"> </span><span class="nx">seed</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">n</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">copy</span><span class="p">(</span><span class="nx">inner</span><span class="p">[</span><span class="mi">64</span><span class="p">:],</span><span class="w"> </span><span class="nx">msg</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">innerSum</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">sha256</span><span class="p">.</span><span class="nf">Sum256</span><span class="p">(</span><span class="nx">inner</span><span class="p">[:</span><span class="mi">64</span><span class="o">+</span><span class="nx">n</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nb">copy</span><span class="p">(</span><span class="nx">outer</span><span class="p">[</span><span class="mi">64</span><span class="p">:],</span><span class="w"> </span><span class="nx">innerSum</span><span class="p">[:])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="o">*</span><span class="nx">out</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">sha256</span><span class="p">.</span><span class="nf">Sum256</span><span class="p">(</span><span class="nx">outer</span><span class="p">[:])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>It matches <code>crypto/hmac</code> (I checked it against the stdlib output), passes the zero-alloc test, and is worse code by every normal standard. I wrote it, it works, and I still feel like I should apologize to someone.</p>
<p>That&rsquo;s the deal today. Tolerate &ldquo;erased at the next GC cycle&rdquo; and your code stays normal. Demand &ldquo;erased before Do returns&rdquo; and you&rsquo;re writing C-flavored Go: fixed buffers, out-parameters, no <code>fmt</code>, no strings. Fine for one small derivation function, a serious project to retrofit through a big call tree.</p>
<h2 id="my-32-bytes">My 32 bytes</h2>
<p>I worked on a backend where certain outcomes are derived from a server-side seed under a commit-reveal scheme. Before a session starts, the server publishes a hash of the seed. Outcomes during the session are derived from the seed plus client input, an HMAC like the one above. When the session ends, the seed is revealed and anyone can re-derive the outcomes to check nothing was manipulated.</p>
<p>The scheme holds only if nobody learns the seed before the reveal, and the seed has to live from commit to reveal. Sometimes that&rsquo;s hours; sometimes the reveal only comes with a rotation nobody is in a hurry to trigger, which makes it effectively open-ended. How to store and rotate such seeds is a big, opinionated topic for another day; in that system it sat encrypted at rest. What <code>runtime/secret</code> fixes is narrower: every touch of the plaintext smears copies across stacks and registers. So the I/O stays outside, and the plaintext touchpoints get wrapped:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">s</span><span class="w"> </span><span class="o">*</span><span class="nx">Server</span><span class="p">)</span><span class="w"> </span><span class="nf">Outcome</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">sessionID</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">clientInput</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">([</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">clientInput</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="nx">maxMsgLen</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="p">{},</span><span class="w"> </span><span class="nx">errInputTooLong</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">encSeed</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nf">EncryptedSeed</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">sessionID</span><span class="p">)</span><span class="w"> </span><span class="c1">// fetch ciphertext, no secrets yet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="p">{},</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">out</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">var</span><span class="w"> </span><span class="nx">seed</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">decryptSeed</span><span class="p">(</span><span class="nx">encSeed</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">seed</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">clientInput</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">out</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><code>out</code> is created by the caller and survives. The seed and every intermediate exist only inside the wiped region. A core dump taken between requests now contains the encrypted seed and nothing else, as long as <code>decryptSeed</code> keeps to the same stack-only discipline.</p>
<p>Notice what this turns the problem into: the long-lived secret never exists in memory as plaintext, and what <code>Do</code> protects is ephemeral, the copies alive during one derivation. That framing matters, since even the maintainer of Go&rsquo;s crypto libraries is on record in the thread as sceptical of wiping long-term secrets; ephemeral material is the case everyone agrees on.</p>
<h2 id="trust-but-core-dump">Trust, but core-dump</h2>
<p>Given the silent no-op on other platforms and the heap asterisk, don&rsquo;t believe this package. Catch it in the act. In a test build, use a seed with a recognizable canary pattern instead of random bytes, run the workload, dump, grep:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> gcore -o dump <span class="k">$(</span>pgrep myapp<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> grep -c --binary-files<span class="o">=</span>text <span class="s2">&#34;CANARY-7f3a&#34;</span> dump.*
</span></span><span class="line"><span class="cl"><span class="go">0
</span></span></span></code></pre></div><p>Run the same check on a build with the wrapper compiled out and you should get hits. That&rsquo;s the control proving the test can find the secret at all.</p>
<p>A dump is a snapshot, though. Taken after the request, it proves the secret didn&rsquo;t linger, but it can miss a transient leak that got overwritten before you dumped. For proper paranoia, scan <code>/proc/&lt;pid&gt;/mem</code> in a loop between requests. The runtime tests itself the same way from the inside; see <a href="https://go.dev/src/runtime/secret/secret_test.go">secret_test.go</a>.</p>
<h2 id="the-goroutine-hole">The goroutine hole</h2>
<p>In Go 1.26, secret mode does not extend to goroutines started inside <code>Do</code>. No panic, no vet warning, they just run unprotected. Since any library you call inside <code>Do</code> may use a goroutine internally, the protection can leak through a dependency you never see. Easy to check with <code>Enabled()</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;inside Do:&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Enabled</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">done</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kd">struct</span><span class="p">{})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">defer</span><span class="w"> </span><span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;inside goroutine:&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Enabled</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="o">&lt;-</span><span class="nx">done</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span></code></pre></div><p>On my linux/arm64 box:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nv">GOEXPERIMENT</span><span class="o">=</span>runtimesecret go run main.go        <span class="c1"># Go 1.26</span>
</span></span><span class="line"><span class="cl"><span class="go">inside Do: true
</span></span></span><span class="line"><span class="cl"><span class="go">inside goroutine: false
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">$</span> <span class="nv">GOEXPERIMENT</span><span class="o">=</span>runtimesecret go1.27rc2 run main.go
</span></span><span class="line"><span class="cl"><span class="go">inside Do: true
</span></span></span><span class="line"><span class="cl"><span class="go">inside goroutine: true
</span></span></span></code></pre></div><p>The second run is the good news. Go 1.27, due out around when this post goes up, makes goroutines inherit secret mode (<a href="https://github.com/golang/go/issues/76477">#76477</a>). Is the 1.26 behavior scandalous? Not really, and it wasn&rsquo;t an oversight: inheritance was debated in the thread and rejected, the argument being that implicit state makes it harder to audit which code actually runs protected. The direct call path is still wiped either way. But semantics reversing between releases is what &ldquo;experimental&rdquo; means.</p>
<h2 id="what-it-costs">What it costs</h2>
<p>Two currencies. Per call: entering secret mode plus zeroing the used stack on exit. Deferred: every tracked heap allocation adds bookkeeping and sweep work, paid later during GC, easy to miss in a microbenchmark.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">sink</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">BenchmarkDirect</span><span class="p">(</span><span class="nx">b</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">B</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">seed</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">msg</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;client-input:42&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nf">Loop</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">sink</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">BenchmarkInsideDo</span><span class="p">(</span><span class="nx">b</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">B</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">seed</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">msg</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;client-input:42&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nf">Loop</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nf">deriveOutcome</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">seed</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">sink</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">BenchmarkDoEmpty</span><span class="p">(</span><span class="nx">b</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">B</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nf">Loop</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// BenchmarkInsideDoHeap is the allocating hmac.New version, for the</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// &#34;heap tax&#34; comparison: watch allocs/op and B/op, and remember the</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// erase cost is paid later, during GC sweeps.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">BenchmarkInsideDoHeap</span><span class="p">(</span><span class="nx">b</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">B</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">seed</span><span class="w"> </span><span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">msg</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;client-input:42&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nf">Loop</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">secret</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">mac</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">hmac</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">sha256</span><span class="p">.</span><span class="nx">New</span><span class="p">,</span><span class="w"> </span><span class="nx">seed</span><span class="p">[:])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">mac</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">mac</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="nx">sink</span><span class="p">[:</span><span class="mi">0</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nv">GOEXPERIMENT</span><span class="o">=</span>runtimesecret go <span class="nb">test</span> -bench . -benchmem ./derive/
</span></span><span class="line"><span class="cl"><span class="go">goos: linux
</span></span></span><span class="line"><span class="cl"><span class="go">goarch: arm64
</span></span></span><span class="line"><span class="cl"><span class="go">pkg: go-runtime-secret-post/derive
</span></span></span><span class="line"><span class="cl"><span class="go">BenchmarkDirect-4         	 5604813	       226.9 ns/op	       0 B/op	       0 allocs/op
</span></span></span><span class="line"><span class="cl"><span class="go">BenchmarkInsideDo-4       	 4328854	       299.7 ns/op	       0 B/op	       0 allocs/op
</span></span></span><span class="line"><span class="cl"><span class="go">BenchmarkDoEmpty-4        	10411758	       100.2 ns/op	       0 B/op	       0 allocs/op
</span></span></span><span class="line"><span class="cl"><span class="go">BenchmarkInsideDoHeap-4   	 1000000	      1846 ns/op	     480 B/op	       5 allocs/op
</span></span></span><span class="line"><span class="cl"><span class="go">PASS
</span></span></span><span class="line"><span class="cl"><span class="go">ok  	go-runtime-secret-post/derive	5.464s
</span></span></span></code></pre></div><p>Whatever your hardware says, the shape holds: one HMAC per request disappears against any workload with a network in it. Wrap loops in a single <code>Do</code> rather than each iteration, and don&rsquo;t allocate inside.</p>
<h2 id="do-you-need-this">Do you need this?</h2>
<p>Almost certainly not. It&rsquo;s aimed at crypto library authors, so that <code>crypto/tls</code> and the WireGuards of the world can make forward secrecy real while you inherit the protection without importing anything. The exception is an application that itself holds a long-lived, high-value secret: a commit-reveal seed, a master key that unwraps other keys. If you recognize yours there, you knew before reading this. If you&rsquo;re unsure, regular hygiene (secrets out of logs, encrypted swap, core dumps disabled) buys far more per unit of effort.</p>
<p>And it&rsquo;s experimental in the full sense: gated behind <code>GOEXPERIMENT</code>, outside the compatibility promise, Linux-only with a silent fallback, still moving. There&rsquo;s already a proposal (<a href="https://github.com/golang/go/issues/76795">#76795</a>) for a <code>secret.Exempt</code> to mark allocations that should survive <code>Do</code>. Wrap it behind a build tag and expect to chase upstream.</p>
<p>A postscript for the maximalists: instead of shortening a secret&rsquo;s lifetime you can attack its exposure, keeping it encrypted even while resident in memory and decrypting only at the moment of use. Against Cloudbleed-class adjacency leaks that&rsquo;s arguably the only mitigation that helps. It&rsquo;s also a post of its own.</p>
<p>Eight years, two functions, one very tired issue thread. And for the first time, a Go process that can honestly say the secret is gone.</p>
]]></content:encoded></item></channel></rss>