<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Michael Stapelbergs Website: posts tagged golang</title>
  <link href="https://michael.stapelberg.ch/posts/tags/golang/feed.xml" rel="self"/>
  <link href="https://michael.stapelberg.ch/posts/tags/golang/"/>


  <id>https://michael.stapelberg.ch/posts/tags/golang/</id>
  <author>
    <name>Michael Stapelberg</name>
  </author>
  <generator>Hugo -- gohugo.io</generator>
  <entry>
    <title type="html"><![CDATA[How my minimal, memory-safe Go rsync steers clear of vulnerabilities]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/"/>
    <id>https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/</id>
    <published>2026-05-24T16:20:22+02:00</published>
    <content type="html"><![CDATA[<p>Back in January 2025, multiple different security researchers published a total
of <a href="https://www.openwall.com/lists/oss-security/2025/01/14/3">6 security vulnerabilities in
rsync</a>, some of which
allow arbitrary code execution and file leaks, so naturally I was wondering
whether/how my <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a> implementation
was affected. Did implementing my own (compatible, but minimal) rsync in Go, a
modern and memory-safe programming language, really rule out entire classes of
security vulnerabilities?</p>
<p>This deep dive article was in the making since January 2025, but was delayed
because we uncovered more unpublished vulnerabilities in the process! The
“Security Vulnerabilities” section now covers all 12 vulnerabilities from the
January 2025 batch and the May 2026 batch.</p>
<p><strong>If you are running (upstream, samba)
<a href="https://github.com/RsyncProject/rsync">rsync</a> in production, upgrade to version
3.4.3 or newer.</strong></p>
<p>If you are running <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a> in
production, upgrade to version v0.3.3 or newer.</p>
<p>Feel free to skip over the nitty-gritty security issue details and jump directly to:</p>
<ul>
<li>The <a href="#go-verdict">verdict on whether using Go has helped</a>.</li>
<li>The <a href="#gokrazy-rsync-verdict">verdict on whether a minimal re-implementation like gokrazy/rsync helps</a>.</li>
<li>My <a href="#openrsync">comparison with OpenBSD’s <code>openrsync</code></a> (written in C).</li>
<li><a href="#defenseindepth">Defense in depth mechanisms</a> one can use on Linux.</li>
<li>The <a href="#conclusion">conclusion</a>.</li>
</ul>
<h2 id="context-my-own-rsync">Context: My own rsync</h2>
<p>For context, I blogged about <a href="/posts/2022-06-18-rsync-overview/">rsync, how I use it, and how it
works</a> back in June 2022. See also <a href="/posts/tags/rsync/">all posts
tagged “rsync”</a>.</p>
<p>The original motivation for writing my own rsync (back then only a server, today
all directions are supported) was to provide the software packages of <a href="https://distr1.org/">distri,
my Linux distribution research project for fast package
management</a>, which I wanted to host on
<a href="https://router7.org/">router7</a>, my small home Linux+Go internet router, which
in turn is built on <a href="https://gokrazy.org/">gokrazy</a>, my Go appliance platform.</p>
<p>I am still running multiple gokrazy/rsync servers for this original purpose, and
also many others! Having rsync available as a primitive (that you can link into
your Go programs!) is really nice.</p>
<h2 id="vulns">Security Vulnerabilities</h2>
<p>This article covers the following security vulnerabilities:</p>
<ul>
<li>CVE-2024-12084 to 12088 <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">(original report)</a></li>
<li>CVE-2024-12747 (discovered separately by Aleksei Gorban &ldquo;loqpa&rdquo;)</li>
<li>CVE-2026-29518 (discovered by Damien Neil and myself! and independently by <a href="https://nullx3d.com/">Nullx3D</a>)</li>
<li>CVE-2026-43617 to 43620</li>
<li>CVE-2026-45232</li>
</ul>
<p>The first batch of the vulnerabilities above was announced on the <a href="https://www.openwall.com/lists/oss-security/2025/01/14/3">oss-security
mailing list</a>, but
note that the original report has more detail compared to the oss-security
summaries!</p>
<p>The later vulnerabilities were announced via <a href="https://github.com/RsyncProject/rsync/security/advisories">GitHub Security Advisories on the
rsync project</a>.</p>
<h3 id="jan2025">January 2025 batch</h3>
<h4 id="cve-2024-12084">CVE-2024-12084: Heap Buffer Overflow (9.8)</h4>
<p><strong>Summary:</strong></p>
<ul>
<li>rsync performed insufficient validation: It read the (attacker-controlled)
checksum length from the network and compared the length against
<code>MAX_DIGEST_LEN</code>.</li>
<li>However, rsync’s data structures always declared a 16 byte buffer: <code>char sum2[SUM_LENGTH]</code>
<ul>
<li><code>SUM_LENGTH</code> is always 16 (bytes), which is sufficient to hold an
<a href="https://en.wikipedia.org/wiki/MD4">MD4</a> or
<a href="https://en.wikipedia.org/wiki/MD5">MD5</a> checksum.</li>
<li><code>MAX_DIGEST_LEN</code> used to be 16 (bytes), but can be larger when rsync is
compiled with SHA256 or SHA512 checksum support.</li>
</ul>
</li>
<li>Hence, the bounds check was ineffective! An attacker could write out of bounds.</li>
<li>This issue was introduced with <a href="https://github.com/RsyncProject/rsync/commit/ae16850dc58e884eb9f5cb7f772342b2db28f471">commit <code>ae16850</code> in September
2022</a>,
which added SHA256/SHA512 checksum support.</li>
</ul>
<details>
<summary>Click to expand the <strong>full description of the improper checksum length validation</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</summary>
<blockquote>
<p>When the checksums are read by the daemon, two different checksums are read:</p>
<ol>
<li>A 32-bit Adler-CRC32 Checksum</li>
<li>A digest of the file chunk. The digest algorithm is determined at the beginning of the protocol negotiation.
The corresponding code can be seen below:
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/sender.c#L96-L100">sender.c</a>:</li>
</ol>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>s<span style="color:#666">-&gt;</span>sums <span style="color:#666">=</span> <span style="color:#06287e">new_array</span>(<span style="color:#007020;font-weight:bold">struct</span> sum_buf, s<span style="color:#666">-&gt;</span>count);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">for</span> (i <span style="color:#666">=</span> <span style="color:#40a070">0</span>; i <span style="color:#666">&lt;</span> s<span style="color:#666">-&gt;</span>count; i<span style="color:#666">++</span>) {
</span></span><span style="display:flex;"><span>    s<span style="color:#666">-&gt;</span>sums[i].sum1 <span style="color:#666">=</span> <span style="color:#06287e">read_int</span>(f);
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">read_buf</span>(f, s<span style="color:#666">-&gt;</span>sums[i].sum2, s<span style="color:#666">-&gt;</span>s2length);
</span></span></code></pre></div><p>Most importantly, note that <code>sum2</code> field is filled with <code>s-&gt;s2length</code> bytes. <code>sum2</code> always has a size of 16:
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/rsync.h#L955-L962">rsync.h</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020">#define SUM_LENGTH 16
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// …
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">struct</span> sum_buf {
</span></span><span style="display:flex;"><span>    OFF_T offset;           <span style="color:#60a0b0;font-style:italic">/**&lt; offset in file of this chunk */</span>
</span></span><span style="display:flex;"><span>    int32 len;              <span style="color:#60a0b0;font-style:italic">/**&lt; length of chunk of file */</span>
</span></span><span style="display:flex;"><span>    uint32 sum1;            <span style="color:#60a0b0;font-style:italic">/**&lt; simple checksum */</span>
</span></span><span style="display:flex;"><span>    int32 chain;            <span style="color:#60a0b0;font-style:italic">/**&lt; next hash-table collision */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#902000">short</span> flags;            <span style="color:#60a0b0;font-style:italic">/**&lt; flag bits */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#902000">char</span> sum2[SUM_LENGTH];  <span style="color:#60a0b0;font-style:italic">/**&lt; checksum  */</span>
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><code>s2length</code> is an attacker-controlled value and can have a value up to <code>MAX_DIGEST_LEN</code> bytes, as the next snipper shows:</p>
<p><a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/io.c#L1979-L1984">io.c</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">=</span> protocol_version <span style="color:#666">&lt;</span> <span style="color:#40a070">27</span> <span style="color:#666">?</span> <span style="color:#002070;font-weight:bold">csum_length</span> : (<span style="color:#902000">int</span>)<span style="color:#06287e">read_int</span>(f);
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">&lt;</span> <span style="color:#40a070">0</span> <span style="color:#666">||</span> sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">&gt;</span> MAX_DIGEST_LEN) {
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">rprintf</span>(FERROR, <span style="color:#4070a0">&#34;Invalid checksum length %d [%s]</span><span style="color:#4070a0;font-weight:bold">\n</span><span style="color:#4070a0">&#34;</span>,
</span></span><span style="display:flex;"><span>            sum<span style="color:#666">-&gt;</span>s2length, <span style="color:#06287e">who_am_i</span>());
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">exit_cleanup</span>(RERR_PROTOCOL);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The problem here is that <code>MAX_DIGEST_LEN</code> can be larger than 16 bytes, depending on the digest support the binary was compiled with:</p>
<p><a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/lib/md-defines.h#L11-L21">md-defines.h</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020">#define MD4_DIGEST_LEN 16
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#define MD5_DIGEST_LEN 16
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#if defined SHA512_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#elif defined SHA256_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA256_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#elif defined SHA_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#else
</span></span></span><span style="display:flex;"><span>  <span style="color:#007020">#define MAX_DIGEST_LEN MD5_DIGEST_LEN </span><span style="color:#60a0b0;font-style:italic">/* 16 bytes */</span><span style="color:#007020">
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#endif
</span></span></span></code></pre></div><p><code>SHA256</code> support is common and sets the <code>MAX_DIGEST_LENGTH</code> value to 64. As a
result, an attacker can write up to 48 bytes past the <code>sum2</code> buffer limit.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53">upstream fix for
CVE-2024-12084</a>
changes the <code>sum2</code> field to a dynamically-allocated <code>sum2_array</code> field, which is
allocated with <code>xfer_sum_len</code> length, and fixes the bounds check to check
against the <code>xfer_sum_len</code> (checksum length for this transfer’s algorithm).</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes: Missing or incorrect bounds checks will not result in a heap buffer
overflow in Go! Instead, attempting to write out of bounds will result in a
panic because the Go runtime performs bounds checks.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync also had insufficient validation! Our issue was different, though:
It wasn’t size confusion, we just were not doing any validation of the sum
header at all — oops!</p>
<p>We can confirm that the Go runtime’s bounds check triggers
on an attempt to <a href="https://github.com/gokrazy/rsync/blob/3a9d1ec136e049b2bffc9db15230eb70308ac90d/rsyncd/sender.go#L136">write out of
bounds</a>
by changing the code like so and running the tests:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/types.go w/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">index 5601697..899fcb8 100644
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">--- i/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+++ w/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -59,7 +59,7 @@ func (sh *SumHead) WriteTo(c *rsyncwire.Conn) error {
</span></span></span><span style="display:flex;"><span> 	var buf rsyncwire.Buffer
</span></span><span style="display:flex;"><span> 	buf.WriteInt32(sh.ChecksumCount)
</span></span><span style="display:flex;"><span> 	buf.WriteInt32(sh.BlockLength)
</span></span><span style="display:flex;"><span><span style="color:#a00000">-	buf.WriteInt32(sh.ChecksumLength)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	buf.WriteInt32(512 /*sh.ChecksumLength*/)
</span></span></span><span style="display:flex;"><span> 	buf.WriteInt32(sh.RemainderLength)
</span></span><span style="display:flex;"><span> 	return c.WriteString(buf.String())
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>As expected, the Go runtime panics with the following message:</p>
<div style="font-size: 80%">
<pre tabindex="0"><code>panic: runtime error: slice bounds out of range [:512] with length 16

goroutine 277 [running]:
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).receiveSums(0xc0000d7b68)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:136 +0x339
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).sendFiles(0xc0000d7b68, 0xc000120820)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:46 +0x134
github.com/gokrazy/rsync/rsyncd.(*Server).handleConnSender(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:397 +0x26a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleConn(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:351 +0x37a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleDaemonConn(0xc000476090, {0x94db80?, 0xc00018a040?}, {0x7fd15838b118, 0xc000428028}, {0xa2bd90, 0xc0002303c0})
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:307 +0xdbb
github.com/gokrazy/rsync/rsyncd.(*Server).Serve.func2()
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:450 +0xaf
created by github.com/gokrazy/rsync/rsyncd.(*Server).Serve in goroutine 260
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:448 +0xd2
</code></pre></div>
<p>Of course, crashing the entire server is not the best failure mode, so I <a href="https://github.com/gokrazy/rsync/commit/178216f10f2e05fd74bf865f2d0725fce4f907cd">added
the missing bounds checking to turn the panic into an
error</a>.</p>
<h4 id="cve-2024-12085">CVE-2024-12085: Stack Info Leak Defeats ASLR (7.5)</h4>
<p><strong>Summary:</strong></p>
<p>Because of the same lack of validation as in the previous CVE-2024-12084
vulnerability, an attacker could select a checksum algorithm with short
checksums (e.g. <code>xxhash64</code> with 8 byte checksums), but then claim they were
sending longer checksums (e.g. 9 bytes), making the victim leak one byte of
uninitialized stack content in the response.</p>
<p>Leaking one byte of stack content may seem benign, but as the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a> puts it:</p>
<blockquote>
<p>The first pair of vulnerabilities are a Heap Buffer Overflow and an Info Leak.
When combined, they allow a client to execute arbitrary code on the machine a
Rsync server is running on. The client only requires anonymous read-access to
the server.</p>
</blockquote>
<details>
<summary>Click to expand the <strong>full description of the info leak</strong>
(quoting the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a>)</summary>
<blockquote>
<p>The daemon matches checksums of chunks the client sent to the server against
the local file contents in
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/match.c#L140-L145"><code>hash_search()</code></a>. Part
of the function prologue is to allocate a buffer on the stack of
<code>MAX_DIGEST_LEN</code> bytes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">static</span> <span style="color:#902000">void</span> <span style="color:#06287e">hash_search</span>(<span style="color:#902000">int</span> f, <span style="color:#007020;font-weight:bold">struct</span> sum_struct <span style="color:#666">*</span>s,
</span></span><span style="display:flex;"><span>                        <span style="color:#007020;font-weight:bold">struct</span> map_struct <span style="color:#666">*</span>buf, OFF_T len)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    OFF_T offset, aligned_offset, end;
</span></span><span style="display:flex;"><span>    int32 k, want_i, aligned_i, backup;
</span></span><span style="display:flex;"><span>    <span style="color:#902000">char</span> sum2[MAX_DIGEST_LEN];
</span></span></code></pre></div><p>The daemon then iterates over the checksums the client sent and generates a
digest for each of the chunks and compares them to the remote digest:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (<span style="color:#666">!</span>done_csum2) {
</span></span><span style="display:flex;"><span>    map <span style="color:#666">=</span> (schar <span style="color:#666">*</span>)<span style="color:#06287e">map_ptr</span>(buf, offset, l);
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">get_checksum2</span>((<span style="color:#902000">char</span> <span style="color:#666">*</span>)map, l, sum2);
</span></span><span style="display:flex;"><span>    done_csum2 <span style="color:#666">=</span> <span style="color:#40a070">1</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (<span style="color:#06287e">memcmp</span>(sum2, s<span style="color:#666">-&gt;</span>sums[i].sum2, s<span style="color:#666">-&gt;</span>s2length) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>    false_alarms<span style="color:#666">++</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">continue</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notably, the number of bytes that are compared again are <code>s-&gt;s2length</code>
bytes. In this case, the comparison does not go out of bounds since
<code>s-&gt;s2length</code> can be a maximum of <code>MAX_DIGEST_LEN</code>.</p>
<p>However, the local <code>sum2</code> buffer, not to be confused with the
attacker-controlled <code>s-&gt;sums[i].sum2</code>, is a buffer on the stack that is not
cleared and thus contains uninitialized stack contents.</p>
<p>A malicious client can send a (known) <code>xxhash64</code> checksum for a given chunk of
a file, which leads to the daemon writing 8 bytes to the stack buffer
<code>sum2</code>. The attacker can then set <code>s-&gt;s2length</code> to 9 bytes. The result of such
a setup would be that the first 8 bytes match and an attacker-controlled 9th
byte is compared with an unknown value of uninitialized stack data.</p>
<p>An attacker can divide a file into 255 chunks and as a result leak one byte
per file download. An attacker can incrementally repeat the process, either in
the same connection or by resetting the connection.</p>
<p>As a result, they can leak <code>MAX_DIGEST_LEN - 8</code> bytes of uninitialized stack
data, which can contain pointers to Heap objects, Stack cookies, local
variables and pointers to global variables and return pointers. With those
pointers they can defeat ASLR.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>There are two relevant upstream fixes:</p>
<ul>
<li>The <a href="https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53">“Some checksum buffer
fixes”</a>
commit prevents this attack because the attacker-controlled <code>s-&gt;s2length</code> can
no longer be larger than the transfer’s checksum length.</li>
<li>The <a href="https://github.com/RsyncProject/rsync/commit/589b0691e59f761ccb05ddb8e1124991440db2c7">“prevent information leak off the
stack”</a>
commit initializes the <code>sum2</code> memory to zero, thereby making any stack leak
through <code>sum2</code> impossible.</li>
</ul>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes: By design, Go initializes all variables to the zero value. Go programmers
do not need to remember to explicitly initialize variables.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected by this vulnerability: Variables are always
initialized in Go.</p>
<p>Additionally, selecting checksums other than MD4 was only introduced in protocol
version 30 (gokrazy/rsync implements protocol version 27).</p>
<!--
Go does not do ASLR (https://github.com/golang/go/issues/27583#issuecomment-1002725264).
Not sure if it makes sense to mention that here.
-->
<h4 id="cve-2024-12087">CVE-2024-12087: Path Traversal using Symlinks (7.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</p>
<blockquote>
<p>When the syncing of symbolic links is enabled, either through the <code>-l</code> or <code>-a</code>
(<code>--archive</code>) flags, a malicious server can make the client write arbitrary
files outside of the destination directory.  A malicious server can send the
client a file list such as:</p>
<pre tabindex="0"><code>symlink -&gt; /arbitrary/directory
symlink/poc.txt
</code></pre><p>Symbolic links, by default, can be absolute or contain characters such as <code>../../</code>.</p>
<p>In practice, the client validates the file list and when it sees the
<code>symlink/poc.txt</code> entry, it will look for a directory called <code>symlink</code>,
otherwise it will error out. If the server sends <code>symlink</code> as [both, a
directory and a symbolic link], [the client] will only keep the directory
entry, thus the attack requires some more details to work.</p>
<p>In <code>inc_recurse</code> mode, which the server can enable for the client, the server
sends the client multiple file lists. The deduplication of the entries happens
on a per-file-list basis. As a result, a malicious server can send a client
multiple file lists, where:</p>
<pre tabindex="0"><code># file list 1:
.
./symlink (directory)
./symlink/poc.txt (regular file)

# file list 2:
./symlink -&gt; /arbitrary/path (symlink)
</code></pre><p>As a result, the <code>symlink</code> directory is created first and <code>symlink/poc.txt</code> is
considered a valid entry in the file list. Then, the attacker changes the type
of <code>symlink</code> to a symbolic link.</p>
<p>When the server then instructs the client to create the <code>symlink/poc.txt</code>
file, it will follow the symbolic link and thus files can be created outside
of the destination directory.</p>
</blockquote>
<p><strong>Can Go help prevent this?</strong></p>
<p>No. This vulnerability is caused by a logic error: when multiple file lists are
used, the merged file list needs to be re-verified.</p>
<p>But see <a href="#goosroot">Defense in depth: Go’s <code>os.Root</code></a></p>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/688f5c379a433038bde36897a156d589be373a98">upstream fix for
CVE-2024-12087</a>
adds the missing validation.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected by this vulnerability: gokrazy/rsync does not
implement the incremental recursion mode (<code>--inc-recursive</code>).</p>
<p>The trade-off here is implementation complexity vs. resource usage: the
incremental recursion mode allows working with the file set in a “windowed” way,
as opposed to having to scan the entire file set before any transfer can
begin. See also my <a href="/posts/2022-07-02-rsync-how-does-it-work/">How does rsync
work?</a> blog post.</p>
<h4 id="cve-2024-12088">CVE-2024-12088: Bypass for <code>--safe-links</code> (7.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</p>
<blockquote>
<p>The <code>--safe-links</code> CLI flag makes the client validate any symbolic links it
receives from the server. The desired behavior is that symbolic links target
can only be 1) relative to the destination directory and 2) never point
outside of the destination directory.</p>
<p>The <code>unsafe_symlink()</code> function is responsible for validating these symbolic
links. The function calculates the traversal depth of a symbolic link target,
relative to its position within the destination directory.</p>
<p>As an example, the following symbolic link is considered unsafe:</p>
<p><code>{DESTINATION}/foo -&gt; ../../</code></p>
<p>As it points outside the destination directory. On the other hand, the
following symbolic link is considered safe as it still points within the
destination directory:</p>
<p><code>{DESTINATION}/foo -&gt; a/b/c/d/e/f/../../</code></p>
<p>This function can be bypassed as it does not consider if the destination of a
symbolic link contains other symbolic links in the path. For example, take the
following two symbolic links:</p>
<p><code>{DESTINATION}/a -&gt; .</code>
<code>{DESTINATION}/foo -&gt; a/a/a/a/a/a/../../</code></p>
<p>In this case, foo would actually point outside the destination
directory. However, the <code>unsafe_symlink()</code> function assumes that <code>a/</code> is a
directory and that the symbolic link is safe.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/407c71c7ce562137230e8ba19149c81ccc47c387">upstream fix for
CVE-2024-12088</a>
makes <code>unsafe_symlink()</code> stricter by not allowing <code>../</code> anywhere within the
path, except at the very beginning.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>No. This vulnerability is caused by a logic error: the validation function was
incorrect. We could have implemented that same bug.</p>
<p>But see <a href="#goosroot">Defense in depth: Go’s <code>os.Root</code></a></p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable: The <code>--safe-links</code> feature is not yet
implemented in gokrazy/rsync.</p>
<h4 id="cve-2024-12086">CVE-2024-12086: Arbitrary File Leak (6.8)</h4>
<p><strong>Summary:</strong></p>
<p>The rsync receiver (in client mode) did not sanitize file names provided by the
rsync sender, or otherwise prevent opening files outside the destination tree. A
malicious sender could instruct a receiver to compare checksums of arbitrary
files outside the destination tree. By observing the receiver’s reaction to a
provided one-byte checksum, a malicious sender can leak arbitrary files.</p>
<details>
<summary>Click to expand the <strong>full description of the file leak</strong>
(quoting the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a>)</summary>
<blockquote>
<p>When a client connects to a malicious server the server is able to leak the
contents of an arbitrary file on the client’s machine.  In
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/rsync.c#L322C5-L322C23"><code>read_ndx_and_attrs()</code></a>
the client will read <code>fnamecmp</code> type as well as the <code>xname</code> from the server if
the server sets the appropriate flags. The flag <code>sanitize_paths</code> will not be
set for the client.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (iflags <span style="color:#666">&amp;</span> ITEM_BASIS_TYPE_FOLLOWS)
</span></span><span style="display:flex;"><span>    fnamecmp_type <span style="color:#666">=</span> <span style="color:#06287e">read_byte</span>(f_in);
</span></span><span style="display:flex;"><span><span style="color:#666">*</span>type_ptr <span style="color:#666">=</span> fnamecmp_type;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (iflags <span style="color:#666">&amp;</span> ITEM_XNAME_FOLLOWS) {
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> ((len <span style="color:#666">=</span> <span style="color:#06287e">read_vstring</span>(f_in, xname, MAXPATHLEN)) <span style="color:#666">&lt;</span> <span style="color:#40a070">0</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">exit_cleanup</span>(RERR_PROTOCOL);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (sanitize_paths) { <span style="color:#60a0b0;font-style:italic">/* not enabled when client receives */</span>
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">sanitize_path</span>(xname, xname, <span style="color:#4070a0">&#34;&#34;</span>, <span style="color:#40a070">0</span>, SP_DEFAULT);
</span></span><span style="display:flex;"><span>        len <span style="color:#666">=</span> <span style="color:#06287e">strlen</span>(buf);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>} <span style="color:#007020;font-weight:bold">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#666">*</span>buf <span style="color:#666">=</span> <span style="color:#4070a0">&#39;\0&#39;</span>;
</span></span><span style="display:flex;"><span>    len <span style="color:#666">=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#666">*</span>len_ptr <span style="color:#666">=</span> len;
</span></span></code></pre></div><p>The caller
(<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/receiver.c#L521"><code>recv_files()</code></a>)
then uses the server provided values to determine a file to compare the
incoming data with.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">case</span> <span style="color:#002070;font-weight:bold">FNAMECMP_FUZZY</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (file<span style="color:#666">-&gt;</span>dirname) {
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">pathjoin</span>(fnamecmpbuf, <span style="color:#007020;font-weight:bold">sizeof</span> fnamecmpbuf, file<span style="color:#666">-&gt;</span>dirname, xname);
</span></span><span style="display:flex;"><span>        fnamecmp <span style="color:#666">=</span> fnamecmpbuf;
</span></span><span style="display:flex;"><span>    } <span style="color:#007020;font-weight:bold">else</span>
</span></span><span style="display:flex;"><span>        fnamecmp <span style="color:#666">=</span> xname;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">break</span>;
</span></span><span style="display:flex;"><span><span style="">…</span>
</span></span><span style="display:flex;"><span>fd1 <span style="color:#666">=</span> <span style="color:#06287e">do_open</span>(fnamecmp, O_RDONLY, <span style="color:#40a070">0</span>);
</span></span></code></pre></div><p>In
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/receiver.c#L368"><code>receive_data()</code></a>
the contents of the file specified by <code>xname</code> are copied into the destination
file. This can be achieved by the server sending a negative token.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">while</span> ((i <span style="color:#666">=</span> <span style="color:#06287e">recv_token</span>(f_in, <span style="color:#666">&amp;</span>data)) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (i <span style="color:#666">&gt;</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (fd <span style="color:#666">!=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span> <span style="color:#666">&amp;&amp;</span> map <span style="color:#666">&amp;&amp;</span> <span style="color:#06287e">write_file</span>(fd, <span style="color:#40a070">0</span>, offset, map, len) <span style="color:#666">!=</span> (<span style="color:#902000">int</span>)len)
</span></span></code></pre></div><p>The server sends a checksum to compare. If they don&rsquo;t match, a 0 is returned.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (fd <span style="color:#666">!=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span> <span style="color:#666">&amp;&amp;</span> <span style="color:#06287e">memcmp</span>(file_sum1, sender_file_sum, xfer_sum_len) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">return</span> <span style="color:#40a070">0</span>;
</span></span></code></pre></div><p>When the return value is 0 the receiver will then send a <code>MSG_REDO</code> to the
generator. The generator will then write a message to the server.</p>
<p>The server can use this as a signal to determine if the checksum they sent was
correct. By starting off with a <code>blength</code> of 1 a malicious server is able to
determine the contents of the target file byte by byte.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c35e28331f10ba6eba370611abd78bde32d54da7">upstream fix for
CVE-2024-12086</a>
prevents opening files outside the destination tree by verifying the
sender-provided path.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable: the fuzzy matching feature was introduced with
rsync protocol version 29, but gokrazy/rsync implements protocol version 27.</p>
<h4 id="cve-2024-12747">CVE-2024-12747: Symlink Race Condition (5.6)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://access.redhat.com/security/cve/CVE-2024-12747">Red Hat Security
Advisory</a>)</p>
<blockquote>
<p>A flaw was found in rsync. This vulnerability arises from a race condition
during rsync&rsquo;s handling of symbolic links. Rsync&rsquo;s default behavior when
encountering symbolic links is to skip them. If an attacker replaced a regular
file with a symbolic link at the right time, it was possible to bypass the
default behavior and traverse symbolic links. Depending on the privileges of
the rsync process, an attacker could leak sensitive information, potentially
leading to privilege escalation.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0590b09d9a34ae72741b91ec0708a820650198b0">upstream fix for
CVE-2024-12747</a>
changes <code>open()</code> calls in the rsync sender to use the <code>O_NOFOLLOW</code> option. The
paths are not expected to be symlinks at that point in the algorithm (symlinks
would be handled with <a href="https://manpages.debian.org/readlink.2"><code>readlink(2)</code></a>
).</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync was vulnerable before <a href="https://github.com/gokrazy/rsync/commit/1b1fbf65b8ed696db464bf53445e8f9b97a37210">commit
<code>1b1fbf6</code></a>,
which introduces the same <code>O_NOFOLLOW</code> mitigation that upstream rsync uses.</p>
<details>
<summary>Click to expand the <strong>reproduction steps</strong> to trigger the
issue in <code>gokrazy/rsync</code></summary>
<p>To reproduce the issue, use the following steps:</p>
<ol>
<li>
<p>Check out gokrazy/rsync v0.2.7:</p>
<pre tabindex="0"><code>git clone https://github.com/gokrazy/rsync
cd rsync
git checkout v0.2.7
</code></pre></li>
<li>
<p>Patch the code as follows to undo the fix and execute the attack:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/internal/nofollow/nofollow_unix.go w/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">--- i/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+++ w/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -2,8 +2,6 @@
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> package nofollow
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a00000">-import &#34;golang.org/x/sys/unix&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-
</span></span></span><span style="display:flex;"><span> // Maybe resolves to unix.O_NOFOLLOW on unix systems,
</span></span><span style="display:flex;"><span> // 0 on other platforms.
</span></span><span style="display:flex;"><span><span style="color:#a00000">-const Maybe = unix.O_NOFOLLOW
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+const Maybe = 0 // unix.O_NOFOLLOW
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/internal/sender/do.go w/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">--- i/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+++ w/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -2,6 +2,8 @@ package sender
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> import (
</span></span><span style="display:flex;"><span> 	&#34;fmt&#34;
</span></span><span style="display:flex;"><span><span style="color:#00a000">+	&#34;os&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	&#34;path/filepath&#34;
</span></span></span><span style="display:flex;"><span> 	&#34;sort&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> 	&#34;github.com/gokrazy/rsync/internal/log&#34;
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -55,6 +57,15 @@ func (st *Transfer) Do(crd *rsyncwire.CountingReader, cwr *rsyncwire.CountingWri
</span></span></span><span style="display:flex;"><span> 		st.Logger.Printf(&#34;file list sent&#34;)
</span></span><span style="display:flex;"><span> 	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#00a000">+	// HACK: swap out the passwd file with a symlink to /etc/passwd
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	if err := os.Remove(filepath.Join(modPath, &#34;passwd&#34;)); err != nil {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+		return nil, err
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	if err := os.Symlink(&#34;../passwd&#34;, filepath.Join(modPath, &#34;passwd&#34;)); err != nil {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+		return nil, err
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	st.Logger.Printf(&#34;HACK: swapped passwd file for symlink&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span> 	// Sort the file list. The client sorts, so we need to sort, too (in the
</span></span><span style="display:flex;"><span> 	// same way!), otherwise our indices do not match what the client will
</span></span><span style="display:flex;"><span> 	// request.
</span></span></code></pre></div></li>
</ol>
<p>Running the <code>TestReceiverSymlinkTraversal</code> test now shows that the server
traversed the symlink:</p>
<pre tabindex="0"><code>    receiver_test.go:371: unexpected file contents: diff (-want +got):
          bytes.Join({
        - 	&#34;benign&#34;,
        + 	&#34;secret&#34;,
          }, &#34;&#34;)
</code></pre></details>
<p><strong>A surprising discovery</strong></p>
<p>When I shared a draft of this article with Damien Neil, member of the Go
Security Team and the author of <a href="#goosroot">the traversal-resistant <code>os.Root</code>
API</a>, he pointed out:</p>
<blockquote>
<p>I believe the <a href="https://github.com/gokrazy/rsync/commit/1b1fbf65b8ed696db464bf53445e8f9b97a37210">gokrazy fix for
CVE-2024-12747</a>
is insufficient. You&rsquo;re calling <code>os.Open</code> with <code>O_NOFOLLOW</code>, but <code>O_NOFOLLOW</code>
only prevents symlink traversal in the last path component.</p>
<p>This is probably still vulnerable to replacing an earlier path component so
<code>os.Open(&quot;dir/passwd&quot;)</code> can be redirected by symlinking <code>dir</code> to <code>/etc</code>.</p>
</blockquote>
<p>We reported this to the rsync security contact address in April 2025. In
December 2025 I learned that someone else had also independently discovered and
reported this issue.</p>
<p>Ultimately, this resulted in CVE-2026-29518, published on 2026-05-20.</p>
<h3 id="may2026">May 2026 batch</h3>
<h4 id="cve-2026-29518">CVE-2026-29518: Symlink Race Condition (7.0)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/pull/895/changes/8471fdd1561049ef5f58df44a1811a50bd9a531d">rsync 3.4.3 NEWS entry</a>)</p>
<blockquote>
<p>TOCTOU symlink race condition allowing local privilege escalation in
daemon mode without chroot.</p>
<p>An rsync daemon configured with <code>use chroot = no</code> is exposed
to a time-of-check / time-of-use race on parent path components. A local
attacker with write access to a module can replace a parent directory
component with a symlink between the receiver&rsquo;s check and its open(),
redirecting reads (basis-file disclosure) and writes (file overwrite)
outside the module. Under elevated daemon privilege this allows privilege
escalation.</p>
<p>Default <code>use chroot = yes</code> is not exposed.</p>
<p>Reach: local attacker on the daemon host, write access to a module path,
daemon configured with <code>use chroot = no</code>.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/859d44fa4f1420775e4ba050337ef32092f2894c">upstream fix for
CVE-2026-29518</a>
uses <code>secure_relative_open()</code>, which is similar to Go’s <code>os.Root</code> API.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync was vulnerable until I switched <a href="https://github.com/gokrazy/rsync/commit/b6d50e5e9fd1fdf196f09947a6bc796b1ac48f46">the
sender</a>
and <a href="https://github.com/gokrazy/rsync/commit/cc42e03cad132ba19e0905f149c7c34dab45cd3a">the
receiver</a>
to <a href="#goosroot">the traversal-resistant <code>os.Root</code> API</a>.</p>
<h4 id="cve-2026-43618">CVE-2026-43618: Integer overflow leaks remote memory (8.1)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-g37v-g3gj-pmwq">GitHub Security Advisory</a>)</p>
<blockquote>
<p>Description: The receiver&rsquo;s compressed-token decoder accumulated a 32-bit
signed counter without overflow checking. A malicious sender can trigger an
overflow that, with careful manipulation, leaks process memory contents to
the attacker &ndash; environment variables, passwords, heap and library pointers
&ndash; significantly weakening ASLR and facilitating further exploitation.</p>
<p>Reach: authenticated daemon connection with compression enabled (the
default for protocols &gt;= 30 when both peers advertise it). Disabling
compression on the daemon (&ldquo;refuse options = compress&rdquo; in rsyncd.conf) is
the available workaround.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c44c90e9460c666c965446a8c0957f0b9fa4c66a">upstream fix for
CVE-2026-43618</a>
introduces the missing checks.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable because it does not implement compression. See
<a href="https://github.com/gokrazy/rsync/issues/35#issuecomment-2988582190">gokrazy/rsync issue
#35</a> for
details on why compression support sounds simple, but is non-trivial.</p>
<h4 id="cve-2026-43620">CVE-2026-43620: DOS after Out-of-bounds read (6.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-28pw-r563-rxvm">GitHub Security Advisory</a>)</p>
<blockquote>
<p>The 2025 fix that added a <code>parent_ndx&lt;0</code> guard in <code>send_files()</code>
was not applied to the visually-identical block in <code>recv_files()</code>. A
malicious rsync server can drive any connecting client into a deterministic
<code>SIGSEGV</code> by setting <code>CF_INC_RECURSE</code> in the compatibility flags, sending a
flist whose first sorted entry is not a leading &ldquo;.&rdquo; directory (which causes
<code>recv_file_list()</code> to set <code>parent_ndx = -1</code>), then sending a transfer record
with <code>ndx=0</code> and a non-<code>ITEM_TRANSFER</code> iflag word. The receiver reads
<code>dir_flist-&gt;files[-1]</code> and dereferences the result. On glibc x86-64 the
dereferenced pointer is mmap chunk metadata that lands at an unmapped
address, hence a clean <code>SEGV_MAPERR</code>; non-glibc allocators have not been
audited.</p>
<p>Reach: any rsync client doing a normal pull from an attacker-controlled
URL. Works for both rsync:// URLs and remote-shell pulls. <code>inc_recurse</code> is the
protocol-30+ default; no special options are required on the victim.</p>
<p>Workaround: <code>--no-inc-recursive</code> on the client.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0cf200ecbb8baaf58070d825c4fbd892b4a63b69">upstream fix for
CVE-2026-43620</a>
adds the <code>parent_ndx&lt;0</code> guard to <code>recv_files()</code> as well.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>Just like for <a href="#cve-2024-12087">CVE-2024-12087</a>, gokrazy/rsync is not affected
by this vulnerability: gokrazy/rsync does not implement the incremental
recursion mode (<code>--inc-recursive</code>).</p>
<h4 id="cve-2026-43619">CVE-2026-43619: More symlink races (6.3)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-4h9m-w5ff-j735">GitHub Security
Advisory</a>)</p>
<blockquote>
<p>Description: Earlier fixes for symlink races on the receiver&rsquo;s open() call
(CVE-2026-29518) missed the same race class on every other path-based
system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink, mknod,
link, rmdir, lstat. On rsync daemons with &ldquo;use chroot = no&rdquo; a local
attacker with filesystem access on the daemon host can swap a symlink into
a parent directory component between the receiver&rsquo;s check and one of these
syscalls, redirecting it outside the exported module. The fix routes each
affected path-based syscall through a parent dirfd opened under
RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on Linux
5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+, per-component
O_NOFOLLOW walk elsewhere). Default &ldquo;use chroot = yes&rdquo; is not exposed.</p>
<p>Reach: local attacker on the daemon host, write access to a module path,
daemon configured with use chroot = no.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/30656c5e358b1c6033f4caf24f3e11b29d25c9f4">upstream fix for
CVE-2026-43619</a>
uses the <code>*at</code> family of syscalls, just like Go’s <code>os.Root</code>.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected, because it uses Go’s <code>os.Root</code> API throughout.</p>
<h4 id="cve-2026-43617">CVE-2026-43617: Hostname/ACL bypass (4.8)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-rjfm-3w2m-jf4f">GitHub Security
Advisory</a>)</p>
<blockquote>
<p>On an rsync daemon configured with the global <code>daemon chroot = /X</code> rsyncd.conf
setting, the reverse-DNS lookup of the connecting client was performed <em>after</em>
the daemon had chrooted into <code>/X</code>. If <code>/X</code> did not contain the files glibc
needs for resolution (<code>/etc/resolv.conf</code>, <code>/etc/nsswitch.conf</code>, <code>/etc/hosts</code>,
NSS service modules), the lookup failed and the connecting hostname was set to
&ldquo;UNKNOWN&rdquo;. Hostname-based deny rules (&ldquo;hosts deny = *.evil.example&rdquo;) therefore
could not match, and an attacker controlling their PTR record could connect
from a hostname the administrator had intended to deny. IP-based ACLs are
unaffected. The per-module <code>use chroot</code> setting is unrelated to this issue.</p>
<p>Reach: rsync daemon configured with <code>daemon chroot = /X</code> AND hostname-based
ACLs AND <code>/X</code> does not include the libc resolver fixtures.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c38f20c5ffabacd0c0c483786ab224a36e14bf43">upstream fix for
CVE-2026-43617</a>
moves the DNS lookup to an earlier point in the protocol.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable because we only implement IP-based allow/deny
lists, not hostname-based allow/deny lists.</p>
<h4 id="cve-2026-45232">CVE-2026-45232: stack out-of-bounds write (3.1)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-8f85-j2cv-59m8">GitHub Security Advisory</a>)</p>
<blockquote>
<p>The rsync client&rsquo;s HTTP <code>CONNECT</code> proxy support contains an off-by-one
out-of-bounds stack write in <code>establish_proxy_connection()</code> (<code>socket.c</code>). After
issuing the <code>CONNECT</code> request, rsync reads the proxy&rsquo;s first response line one
byte at a time into a 1024-byte stack buffer with the bound <code>cp &lt; &amp;buffer[sizeof buffer - 1]</code>, so the loop only ever writes <code>buffer[0..sizeof-2]</code>. If the proxy
(or a man-in-the-middle in front of it) returns 1023+ bytes on the first
response line without a <code>'\n'</code> terminator, the loop exits with <code>cp == &amp;buffer[sizeof buffer - 1]</code> — a slot the loop never wrote, so <code>*cp</code> holds stale
stack bytes left there by the earlier <code>snprintf()</code> that formatted the outgoing
<code>CONNECT</code> request. The post-loop code then does:</p>
<pre tabindex="0"><code>if (*cp != &#39;\n&#39;)   /* (*cp is uninitialised stack data) */
    cp++;          /* cp now &amp;buffer[sizeof]: one past end */
*cp-- = &#39;\0&#39;;      /* one-byte OOB write on the stack */
</code></pre><p>The <code>'\0'</code> lands one byte past the end of the on-stack <code>buffer[1024]</code>,
corrupting whatever lives in the adjacent stack slot. AddressSanitizer reports
<code>stack-buffer-overflow</code> at <code>socket.c:95</code> in the <code>establish_proxy_connection</code>
frame.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/a5fc5ebe7a8ef1aa72f6e344599f97fd4427ecba">upstream fix for
CVE-2026-45232</a>
validates the attacker-supplied data.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync does not implement such proxy support, so it is not vulnerable.</p>
<h3 id="go-verdict">Go verdict</h3>
<p>Let’s summarize how Go fares:</p>
<ul>
<li>The Go runtime’s bounds checks turn more serious security issues into a panic.
<ul>
<li>A panic is still a denial-of-service risk, but that’s much preferable.</li>
</ul>
</li>
<li>Go initializes memory to zero, making info leaks like CVE-2024-12085 impossible.</li>
<li>Go’s <code>os.Root</code> API prevents most of the remaining vulnerabilities.</li>
<li>Only one out of twelve vulnerabilities (CVE-2026-43617) is a proper bug in
the application logic that using Go could not have prevented.</li>
</ul>
<table>
	<thead>
			<tr>
					<th>CVE number</th>
					<th>Cause</th>
					<th>Risk (C)</th>
					<th>Does Go help?</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2024-12084</td>
					<td>insufficient validation</td>
					<td>Heap Buffer Overflow</td>
					<td>✅ bounds check (panics!)</td>
			</tr>
			<tr>
					<td>2024-12085</td>
					<td>insufficient validation</td>
					<td>Info Leak</td>
					<td>✅ zero init</td>
			</tr>
			<tr>
					<td>2024-12086</td>
					<td>missing validation</td>
					<td>Arbitrary File Leak</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2024-12087</td>
					<td>insufficient validation</td>
					<td>Write Arbitrary Files</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2024-12088</td>
					<td>insufficient validation</td>
					<td>Create Arbitrary Symlinks</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2024-12747</td>
					<td>TOCTOU</td>
					<td>Leak Privileged Files</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2026-29518</td>
					<td>TOCTOU</td>
					<td>Leak Privileged Files</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2026-43617</td>
					<td>missing validation</td>
					<td>Deny List Ineffective</td>
					<td>❌ logic bug</td>
			</tr>
			<tr>
					<td>2026-43618</td>
					<td>insufficient validation</td>
					<td>Info Leak</td>
					<td>✅ bounds check (panics!)</td>
			</tr>
			<tr>
					<td>2026-43619</td>
					<td>TOCTOU</td>
					<td>Leak Privileged Files</td>
					<td>✅ <code>os.Root</code></td>
			</tr>
			<tr>
					<td>2026-43620</td>
					<td>insufficient validation</td>
					<td>crash (DOS)</td>
					<td>✅ bounds check (panics!)</td>
			</tr>
			<tr>
					<td>2026-45232</td>
					<td>missing validation</td>
					<td>Memory Write</td>
					<td>✅ bounds check (panics!)</td>
			</tr>
	</tbody>
</table>
<h3 id="gokrazy-rsync-verdict">gokrazy/rsync verdict</h3>
<p>Aside from being written in Go, another key difference between gokrazy/rsync and
the official upstream rsync is that the gokrazy implementation is <strong>minimal</strong>:</p>
<ul>
<li>gokrazy/rsync is unaffected by many vulnerabilities because it does not
implement the feature in question, for example <code>--inc-recursive</code>.</li>
<li>Like all other wire protocol-compatible rsync implementations, gokrazy/rsync
targets protocol version 27, because later protocol versions introduce
significant complexity.</li>
<li>In some cases, features that would be good to implement come with significant
blockers, e.g. compression is tricky, see <a href="https://github.com/gokrazy/rsync/issues/35#issuecomment-2988582190">gokrazy/rsync issue
#35</a> for
details.</li>
</ul>
<p>Let’s have a look at whether gokrazy/rsync was affected by each CVE at the time
of publishing:</p>
<table>
	<thead>
			<tr>
					<th>CVE number</th>
					<th>Cause</th>
					<th>gokrazy/rsync impl?</th>
					<th>gokrazy/rsync affected?</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2024-12084</td>
					<td>insufficient validation</td>
					<td>yes</td>
					<td>⚠️ panic</td>
			</tr>
			<tr>
					<td>2024-12085</td>
					<td>insufficient validation</td>
					<td>no (proto 30)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2024-12086</td>
					<td>missing validation</td>
					<td>no (proto 29)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2024-12087</td>
					<td>insufficient validation</td>
					<td>no (<code>inc-rec</code>)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2024-12088</td>
					<td>insufficient validation</td>
					<td>no (<code>safe-links</code>)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2024-12747</td>
					<td>TOCTOU</td>
					<td>yes</td>
					<td>❌ vuln</td>
			</tr>
			<tr>
					<td>2026-29518</td>
					<td>TOCTOU</td>
					<td>yes</td>
					<td>⚠️ patched</td>
			</tr>
			<tr>
					<td>2026-43617</td>
					<td>missing validation</td>
					<td>no (host deny lists)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2026-43618</td>
					<td>insufficient validation</td>
					<td>no (compression)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2026-43619</td>
					<td>TOCTOU</td>
					<td>yes</td>
					<td>⚠️ patched</td>
			</tr>
			<tr>
					<td>2026-43620</td>
					<td>insufficient validation</td>
					<td>no (<code>inc-rec</code>)</td>
					<td>✅ not vuln</td>
			</tr>
			<tr>
					<td>2026-45232</td>
					<td>missing validation</td>
					<td>no (proxy)</td>
					<td>✅ not vuln</td>
			</tr>
	</tbody>
</table>
<p>To be clear: all known vulnerabilities are fixed in gokrazy/rsync! The table
above documents what the state was at the time when each CVE was published. In
other words:</p>
<p>When the January 2025 vulnerabilities were published, gokrazy/rsync panicked
(CVE-2024-12084) and was vulnerable to a TOCTOU race (CVE-2024-12747). In the
process of fixing the TOCTOU issue, we discovered CVE-2026-29518, which was
fixed in gokrazy/rsync before the CVE was published. CVE-2026-43619 was
discovered even later, but was also already fixed in gokrazy/rsync with the same
fix: using Go’s <code>os.Root</code> everywhere.</p>
<h3 id="terminology">Imprecise terminology</h3>
<p>As I was reading the vulnerability reports, I noticed that the reports were
slightly misleading by their choice of words: most reports just spoke of
“server” and “client”. However, in an rsync transfer, both sides, the rsync
client and the rsync server can assume either role: sender (upload files) or
receiver (download files)!</p>
<p>Some setups come with further restrictions that make certain attacks harder or
impossible to pull off. For example, when running in daemon mode, file system
access can be restricted to the pre-configured module paths (but not in command
mode!).</p>
<p>Here is a diagram to give you an overview of the 4 different setups and
role/protocol layering:</p>















<a href="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_fe6c47e615c529cc.png 2x,https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_2af07d046b10779e.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_676301528b69c19f.png"
  alt="diagram showing 4 different rsync setups: (1) rsync --daemon (2) rsync -e ssh; rsync://server/module/dir (3) rsync server:/some/path (4) rsync /src" title="diagram showing 4 different rsync setups: (1) rsync --daemon (2) rsync -e ssh; rsync://server/module/dir (3) rsync server:/some/path (4) rsync /src"
  width="600"
  height="547"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>In the context of our vulnerability reports, I would say that the Arbitrary File
Leak vulnerability (CVE-2024-12086)’s original title “Server leaks arbitrary
client files” can easily be misunderstood.</p>
<p>Instead, I would say: The <strong>rsync receiver</strong> will leak arbitrary files to a
<strong>malicious sender</strong>.</p>
<p>I have verified that a malicious client sender can make an unpatched remote
rsync open files outside the destination tree (e.g. the <code>/etc/shadow</code> system
password database) when running in command mode, for example over SSH. (But,
when running in daemon mode, the server enables additional path sanitization,
which prevents this attack.)</p>
<p>Similarly, the Symlink Path Traversal vulnerability (CVE-2024-12087) speaks
about a “malicious server”, but again, it should be “malicious sender”, which
can be either the client or the server.</p>
<h2 id="openrsync">Comparison with OpenBSD’s openrsync (C)</h2>
<p>The OpenBSD project is known for its security focus, so how does openrsync
compare?</p>
<p>openrsync is not affected by the Heap Buffer Overflow (CVE-2024-12084) and Stack
Info Leak (CVE-2024-12085) vulnerabilities because it validates the checksum
length and only supports one checksum size/algorithm (MD4).</p>
<p>openrsync is not affected by CVE-2024-12086, CVE-2024-12087 and CVE-2024-12088
because it does not implement the relevant features (like gokrazy/rsync). Even
if it was vulnerable, openrsync’s defense-in-depth measures like using
<a href="https://man.openbsd.org/unveil.2">OpenBSD’s <code>unveil(2)</code></a> and
<a href="https://man.openbsd.org/pledge.2"><code>pledge(2)</code></a> to restrict file system access
would have prevented successful exploitation — at least when running on OpenBSD.</p>
<p>openrsync is not affected by CVE-2024-12747 because it <a href="https://github.com/kristapsdz/openrsync/commit/8f1927c35021cd1fbf31cb8c5a6393cc37ba4de8">used <code>O_NOFOLLOW</code> from
the very moment they implemented symlink
support</a>. But,
because <code>O_NOFOLLOW</code> is not a sufficient fix for this issue, openrsync <strong>is</strong>
affected by CVE-2026-29518!</p>
<p>The above covers the January 2025 batch of vulnerabilities; the May 2026 batch
is similar in that most features just are not implemented.</p>
<p>Overall, I say: Well done, Kristaps and contributors! By diligently implementing
validation, restricting the attack surface and employing defense-in-depth
measures, openrsync manages to not be affected by almost all of the reported
vulnerabilities.</p>
<h2 id="defenseindepth">Defense in depth</h2>
<p>Which APIs and environments can we use on Linux for defense-in-depth measures?</p>
<p>I’ll go through the ones <code>gokrazy/rsync</code> supports, ordered by traditional to
modern.</p>
<h3 id="mountnamespaces">Linux mount namespaces</h3>
<p>Within a few weeks after starting the <code>gokrazy/rsync</code> project, I <a href="https://github.com/gokrazy/rsync/commit/d63aaed3be4f69ac98581c4b0ac2646c37f1f8c0">added support
for dropping privileges and using mount/pid namespaces on
Linux</a>
to restrict the file system objects that my rsync server could work with.</p>
<p>This approach works very well to mitigate path traversal attacks, but requires
privileges, meaning we need to run as <code>root</code> or <a href="https://github.com/gokrazy/rsync/commit/da9802e33c89af947d4958ec31e51b14f43ebfb2">in a Linux user
namespace</a>
(if enabled on your distribution / system).</p>
<p>That limitation makes mount namespaces well-suited for server setups, but
usually unavailable for interactive one-off transfers that are typically running
under a human’s user account.</p>
<h3 id="systemdhardening">systemd hardening</h3>
<p>In the same commit that introduced Linux mount/pid namespace support, I also
included a systemd service file that restricted file system access to home
directories and encouraged folks in the README to further restrict file system
access, depending on what their use-case allows.</p>
<p>These file system restrictions, if set up correctly, mitigate the File Leak
(CVE-2024-12086) and Path Traversal (CVE-2024-12087) vulnerabilities.</p>
<p>The Symlink Race Condition (CVE-2024-12747) relies on privilege escalation
through the rsync process, but thanks to the
<a href="https://0pointer.net/blog/dynamic-users-with-systemd.html">DynamicUser</a>
feature, our process has fewer privileges than other users.</p>
<p>Similarly to mount namespaces, these measures are great for server setups, but
too cumbersome to set up for interactive one-off usages.</p>
<h3 id="landlock">Linux Landlock</h3>
<p>I stumbled upon Justine’s blog post <a href="https://justine.lol/pledge/#paths">Porting OpenBSD pledge() to Linux
(2022)</a> and was reminded that Linux offers
the <a href="https://docs.kernel.org/userspace-api/landlock.html">Landlock API</a> for
unprivileged, per-process access control, similar to OpenBSD’s
<a href="https://man.openbsd.org/unveil"><code>unveil(2)</code></a> system call, which openrsync
uses. The basic idea is that once your program knows the directory it works
with, it makes a call like <code>unveil(&quot;/home/michael/backups&quot;, &quot;rw&quot;);</code> and no
longer has access to other file system locations.</p>
<p>I had previously heard of Landlock at a Go Meetup, so I knew there was Go
support for Landlock. Back in 2022, I enabled Landlock support in the gokrazy
kernel images.</p>
<p>So I gave it a shot in March 2025 and <a href="https://github.com/gokrazy/rsync/commit/f84d1e39ec6613599cd9610974e89be5ca183a6d">implemented Landlock support to restrict
file system
access</a>. It
took me a few hours, which seems a little longer than one might expect at
first. Making Landlock work (and/or skipping it) in our test environment ran
into a couple of road blocks: Our tests had defined many functions that get run
in the same process, but when repeatedly adding rulesets, we would exceed the
limit of 16 (!) policy layers per process.</p>
<p>Once I had it set up just right, it is a beautiful solution. Now we can restrict
rsync transfers to their sources (read-only) or destination directories
(read-write), even for unprivileged invocations of <code>gokrazy/rsync</code>! 🎉</p>
<hr>
<p>The downside to Landlock is that Landlock operates at the process level. This
means that Landlock policies must include the files that your program needs,
e.g. <code>gokrazy/rsync</code> needs to be able to read <code>/etc/passwd</code> for user id lookup,
so if the attacker is after the <code>/etc/passwd</code> file, Landlock does not help.</p>
<h3 id="goosroot">Go’s os.Root</h3>
<p>In February 2025, the Go 1.24 release introduced the
<a href="https://pkg.go.dev/os#Root"><code>os.Root</code></a> API, which is resistant against path
traversal, see <a href="https://go.dev/blog/osroot">The Go Blog: Traversal-resistant file
APIs</a> (by Damien Neil, March 2025). This API allows
more fine-grained control (per file system operation) compared to Landlock.</p>
<p>Go 1.25 (released in August 2025) added more methods to <code>os.Root</code>, making it a
convenient choice for most file system usage.</p>
<p>I have converted all of <code>gokrazy/rsync</code>’s file system usage to use <code>os.Root</code>,
which is a great fit: users configure input/output directories, but the
filenames received over the network are untrusted. That’s exactly what <code>os.Root</code>
was designed for!</p>
<hr>
<p>When I first looked into using <code>os.Root</code>, I thought that some system calls could
inherently not be made with this API, like for example <a href="https://manpages.debian.org/mknod.2"><code>mknod(2)</code></a>
 to create device node files. Damien explained:</p>
<blockquote>
<p>It won&rsquo;t support mknod, though.</p>
<p>However, you should be able to use it to enable a safe mknod:</p>
<ul>
<li>os.Root.OpenFile the parent directory of the target,</li>
<li>File.Fd to get the file descriptor for that directory,</li>
<li><a href="https://pkg.go.dev/golang.org/x/sys/unix#Mknodat">https://pkg.go.dev/golang.org/x/sys/unix#Mknodat</a> to create the file.</li>
</ul>
</blockquote>
<p>If you’re curious how that looks in practice, check out <code>gokrazy/rsync</code>’s usage
in <a href="https://github.com/gokrazy/rsync/blob/45444634288e753178af56c61e064ffb08636695/internal/receiver/generatormknod_linux.go#L15-L29"><code>internal/receiver/generatormknod_linux.go</code>, line
15-29</a>.</p>
<p>Another stumbling block was when I realized that unlike with <a href="https://manpages.debian.org/mknodat.2"><code>mknodat(2)</code></a>
, Linux only implements <a href="https://manpages.debian.org/bind.2"><code>bind(2)</code></a>
, but no <code>bindat</code> (as of Linux 7.0)!</p>
<p>Luckily, <a href="https://mastodon.social/@pid_eins/115869484548013992">Lennart Poettering pointed
out</a> that there’s a trick
to skip path resolution without <code>bindat</code>:</p>
<blockquote>
<p>you can probably bind to <code>/proc/self/&lt;fd&gt;/foobar</code> in the meantime&hellip;</p>
</blockquote>
<p>And indeed, this works! Path resolution is skipped because we only specify a
basename (last component of a path) after the known-safe <code>/proc/self/&lt;fd&gt;</code>, not
a path (see <a href="https://github.com/gokrazy/rsync/blob/45444634288e753178af56c61e064ffb08636695/internal/receiver/generatormknod_linux.go#L49-L56">line
49-56</a>).</p>
<p>With these two tips, <code>gokrazy/rsync</code> v0.3.1 and newer are fully using <code>os.Root</code>,
meaning all file system access is traversal-safe! 🥳</p>
<h2 id="conclusion">Conclusion</h2>
<p><strong>Lacking validation causes vulnerabilities</strong></p>
<p>It is interesting to note that aside from the TOCTOU vulnerabilities
(CVE-2024-12747, CVE-2026-29518 and CVE-2026-43619), all other vulnerabilities
were caused by missing or incorrect input validation. In three cases, there was
just no validation to begin with. In another case (CVE-2024-12088), the subject
matter of file system path resolution is tricky enough that the existing
validation did not cover all edge cases.</p>
<p>As the <a href="#go-verdict">Go verdict</a> section explains in more detail, the most
valuable structural fixes are to provide bounds checking (= always-on
validation) and safe-by-default APIs like Go’s <code>os.Root</code>.</p>
<p><strong>Too much complexity</strong></p>
<p>A few of the vulnerabilities came from evolution of the rsync protocol: The
code used to correctly perform sufficient validation, but then new features were
added. For example, when checksum algorithm negotiation was added (protocol
version 30), the validation was not correctly updated. When incremental
recursion was added (also protocol version 30), the validation that made sense
for individual file lists was not updated for the new processing approach of
merging incremental file lists.</p>
<p>Avoiding complexity avoids vulnerabilities! Both gokrazy/rsync and also
openrsync were not vulnerable to 8 out of the 12 security vulnerabilities simply
because they do not implement the feature with the vulnerability.</p>
<p>Of course, these features were added to rsync because they were valuable to
someone at some point, and of course I am not saying that we should just… not
develop software any further, ever.</p>
<p>But, I consider it ideal to <strong>use an implementation whose complexity is
appropriate for and proportional to the complexity of the use-case</strong>. In other
words: for simple use-cases, reach for a simple implementation. Only reach for
the fully-featured implementation where needed.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Stamp It! All Programs Must Report Their Version]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/"/>
    <id>https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/</id>
    <published>2026-04-05T16:20:00+02:00</published>
    <content type="html"><![CDATA[<p>Recently, during a production incident response, I guessed the root cause of an
outage correctly within less than an hour (cool!) and submitted a fix just to
rule it out, only to then spend many hours fumbling in the dark because we
lacked visibility into version numbers and rollouts… 😞</p>
<p>This experience made me think about software versioning again, or more
specifically about build info (build versioning, version stamping, however you
want to call it) and version reporting. I realized that for the i3 window
manager, I had solved this problem well over a decade ago, so it was really
unexpected that the problem was decidedly not solved at work.</p>
<p>In this article, I’ll explain how 3 simple steps (Stamp it! Plumb it! Report
it!) are sufficient to save you hours of delays and stress during incident
response.</p>
<h2 id="low-versioning-standards">Why are our versioning standards so low?!</h2>
<p>Every household appliance has incredibly detailed versioning! Consider this
dishwasher:</p>















<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_b0aacbc1f55d6a32.jpg 2x,https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_eda51a12209b7f34.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_c593ca2b227d5973.jpg"
  alt="a dishwasher, with many precise bits of version information" title="a dishwasher, with many precise bits of version information"
  width="600"
  height="450"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p><em>(Thank you Feuermurmel for sending me this lovely example!)</em></p>
<p>I observed a couple household appliance repairs and am under the impression that
if a repair person cannot identify the appliance, they would most likely refuse
to even touch it.</p>
<p>So why are our standards so low in computers, in comparison? Sure, consumer
products are typically versioned <em>somehow</em> and that’s typically good enough
(except for, say, USB 3.2 Gen 1×2!). But recently, I have encountered too many
developer builds that were not adequately versioned!</p>
<h2 id="software-versioning">Software Versioning</h2>
<p>Unlike a physical household appliance with a stamped metal plate, software is
constantly updated and runs in places and structures we often cannot even see.</p>
<p>Let’s dig into what we need to increase our versioning standard!</p>
<p>Usually, software has a <strong>name</strong> and some <strong>version number</strong> of varying granularity:</p>
<ul>
<li>Chrome</li>
<li>Chrome 146</li>
<li>Chrome 146.0.7680.80</li>
<li>Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34}</li>
</ul>
<p>All of these identify the Chrome browser on my computer, but each at different
granularity.</p>
<p>All are correct and useful, depending on the context. Here’s an example for each:</p>
<ol>
<li>“This works in Chrome for me, did you test in Firefox?”</li>
<li>“Chrome 146 contains broken middle-click-to-paste-and-navigate”</li>
<li>“I run Chrome 146.0.7680.80 and cannot reproduce your issue”</li>
<li>“Apply this patch on top of Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34} and follow these steps to reproduce: […]”</li>
</ol>
<p>After creating the <a href="https://i3wm.org">i3 window manager</a>, I quickly learned that
for user support, it is very valuable for programs to clearly identify
themselves. Let me illustrate with the following case study.</p>
<h2 id="i3-moreversion">Case Study: i3’s <code>--version</code> and <code>--moreversion</code></h2>
<p>When running <code>i3 --version</code>, you will see output like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% i3 --version
</span></span><span style="display:flex;"><span>i3 version 4.24 (2024-11-06) © 2009 Michael Stapelberg and contributors
</span></span></code></pre></div><p>Each word was carefully deliberated and placed. Let me dissect:</p>
<ol>
<li><code>i3 version 4.24</code>: I could have shortened this to <code>i3 4.24</code> or maybe <code>i3 v4.24</code>, but I figured it would be helpful to be explicit because <code>i3</code> is such
a short name. Users might mumble aloud “What’s an i-3-4-2-4?”, but when
putting “version” in there, the implication is that i3 is some computer thing
(→ a computer program) that exists in version 4.24.</li>
<li><code>(2024-11-06)</code> is the release date so that you can immediately tell if
“<code>4.24</code>” is recent.</li>
<li><code>© 2009 Michael Stapelberg</code> signals when the project was started and who is
the main person behind it.</li>
<li><code>and contributors</code> gives credit to the many people who helped. i3 was never a
one-person project; it was always a group effort.</li>
</ol>
<p>When doing user support, there are a couple of questions that are conceptually
easy to ask the affected user and produce very valuable answers for the
developer:</p>
<ol>
<li>Question: “Which version of i3 are you using?”
<ul>
<li>Since i3 is not a typical program that runs in a window (but a window
manager / desktop environment), there is no Help → About menu
option.</li>
<li>Instead, we started asking: What is the output of <code>i3 --version</code>?</li>
</ul>
</li>
<li>Question: “<em>Are you reporting a new issue or a preexisting issue? To confirm,
can you try going back to the version of i3 you used previously?</em>”. The
technical terms for “going back” are downgrade, rollback or revert.
<ul>
<li>Depending on the Linux distribution, this is either trivial or a nightmare.</li>
<li>With NixOS, it’s trivial: you just boot into an older system “generation”
by selecting that version in the bootloader. Or you revert in git, if your
configs are version-controlled.</li>
<li>With imperative Linux distributions like Debian Linux or Arch Linux, if
you did not take a file system-level snapshot, there is no easy and
reliable way to go back after upgrading your system. If you are lucky, you
can just <code>apt install</code> the older version of i3. But you might run into
dependency conflicts (“version hell”).</li>
<li>I know that it is <em>possible</em> to run older versions of Debian using
<a href="https://snapshot.debian.org/">snapshot.debian.org</a>, but it is just not
very practical, at least when I last tried.</li>
</ul>
</li>
<li>Can you check if the issue is still present in the latest i3 development version?
<ul>
<li>Of course, I could also try reproducing the user issue with the latest
release version, and <strong>then one additional time</strong> on the latest
development version.</li>
<li>But this way, the verification step moves to the affected user, which is
good because it filters for highly-motivated bug reporters (higher chance
the bug report actually results in a fix!) and it makes the user reproduce
the bug <em>twice</em>, figuring out if it’s a flaky issue, hard-to-reproduce, if
the reproduction instructions are correct, etc.</li>
<li>A natural follow-up question: “<em>Does this code change make the issue go
away?</em>” This is easy to test for the affected user who now has a
development environment.</li>
</ul>
</li>
</ol>
<p>Based on my experiences with asking these questions many times, I noticed a few
patterns in how these debugging sessions went. In response, I introduced another
way for i3 to report its version in i3 v4.3 (released in September 2012): a
<code>--moreversion</code> flag! Now I could ask users a small variation of the first
question: What is the output of <code>i3 --moreversion</code>? Note how this also transfers
well over spoken word, for example at a computer meetup:</p>
<blockquote>
<p><strong>Michael:</strong> Which version are you using?</p>
<p><strong>User:</strong> How can I check?</p>
<p><strong>Michael:</strong> Run this command: <code>i3 --version</code></p>
<p><strong>User:</strong> It says 4.24.</p>
<p><strong>Michael:</strong> Good, that is recent enough to include the bug fix. Now, we need
more version info! Run <code>i3 --moreversion</code> please and tell me what you see.</p>
</blockquote>
<p>When you run <code>i3 --moreversion</code>, it does not just report the version of the i3
program you called, it also connects to the running i3 window manager process in
your X11 session using <a href="https://i3wm.org/docs/ipc.html">its IPC (interprocess communication)
interface</a> and reports the running i3 process’s
version, alongside other key details that are helpful to show the user, like
which configuration file is loaded and when it was last changed:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% i3 --moreversion
</span></span><span style="display:flex;"><span>Binary i3 version:  4.24 (2024-11-06) © 2009 Michael Stapelberg and…
</span></span><span style="display:flex;"><span>Running i3 version: 4.24 (2024-11-06) (pid 2521)
</span></span><span style="display:flex;"><span>Loaded i3 config:
</span></span><span style="display:flex;"><span>  /home/michael/.config/i3/config (main)
</span></span><span style="display:flex;"><span>  (last modified: 2026-03-15T23:09:27 CET, 1101585 seconds ago)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The i3 binary you just called:
</span></span><span style="display:flex;"><span>/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
</span></span><span style="display:flex;"><span>The i3 binary you are running: i3
</span></span></code></pre></div><p>This might look like a lot of detail on first glance, but let me spell out why
this output is such a valuable debugging tool:</p>
<ol>
<li>
<p>Connecting to i3 via the IPC interface is an interesting test in and of
itself. If a user sees <code>i3 --moreversion</code> output, that implies they will also
be able to run debugging commands like (for example) <code>i3-msg -t get_tree &gt; /tmp/tree.json</code> to capture the full layout state.</p>
</li>
<li>
<p>During a debugging session, running <code>i3 --moreversion</code> is an easy check to
see if the version you just built is actually effective (see the <code>Running i3 version</code> line).</p>
<ul>
<li>Note that this is the same check that is relevant during production
incidents: verifying that <em>effectively running</em> matches <em>supposed to be
running</em> versions.</li>
</ul>
</li>
<li>
<p>Showing the full path to the loaded config file will make it obvious if the
user has been editing the wrong file. If the path alone is not sufficient,
the modification time (displayed both absolute and relative) will flag
editing the wrong file.</p>
</li>
</ol>
<p>I use NixOS, BTW, so I automatically get a stable identifier
(<code>0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24</code>) for <em>the specific build</em> of i3.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% ls -l $(which i3)
</span></span><span style="display:flex;"><span>lrwxrwxrwx 1 root root 58 1970-01-01 01:00 /run/current-system/sw/bin/i3
</span></span><span style="display:flex;"><span>-&gt; /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
</span></span></code></pre></div><p>To see the build recipe (“derivation” in Nix terminology) which produced this
Nix store output (<code>0zn9r4263…-i3-4.24</code>), I can run <code>nix derivation show</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  &#34;/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv&#34;: {
</span></span><span style="display:flex;"><span>[…]
</span></span></code></pre></div><details>
<summary>Click here to expand the full <code>nix derivation show</code> output if you are curious</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  &#34;/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv&#34;: {
</span></span><span style="display:flex;"><span>    &#34;args&#34;: [
</span></span><span style="display:flex;"><span>      &#34;-e&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh&#34;
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    &#34;builder&#34;: &#34;/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash&#34;,
</span></span><span style="display:flex;"><span>    &#34;env&#34;: {
</span></span><span style="display:flex;"><span>      &#34;NIX_MAIN_PROGRAM&#34;: &#34;i3&#34;,
</span></span><span style="display:flex;"><span>      &#34;__structuredAttrs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;buildInputs&#34;: &#34;/nix/store/58q0dn2lbm2p04qmds0aymwdd1fr5j67-libxcb-1.17.0-dev /nix/store/3fcfw014z5i05ay1ag0hfr6p81mb1kzw-libxcb-keysyms-0.4.1-dev /nix/store/2cdrqvd3av1dmxna9xjqv1jccibpvg6m-libxcb-util-0.4.1-dev /nix/store/256alp82fhdgbxx475dp7mk8m29y53rh-libxcb-wm-0.4.2-dev /nix/store/nr44nfhj48abr3s6afqy1fjq4qmr23lz-xcb-util-xrm-1.3 /nix/store/ml4cfhhw6af6qq6g3dn7g5j5alrnii88-libxkbcommon-1.11.0-dev /nix/store/6hnzjg09fd5xkkrdj437wyaj952nlg45-libstartup-notification-0.12 /nix/store/9m0938zahq7kcfzzix4kkpm8d1iz3nmq-libx11-1.8.12-dev /nix/store/vz5gd0rv0m2kjca50gacz0zq9qh7i8xf-pcre2-10.46-dev /nix/store/334cvqpqc9f0plv0aks71g352w6hai0c-libev-4.33 /nix/store/6s3fw10c0441wv53bybjg50fh8ag1561-yajl-2.1.0-unstable-2024-02-01 /nix/store/d6aw2004h90dwlsfcsygzzj4pzm1s31a-libxcb-cursor-0.1.6-dev /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/l6bslkrp59gaknypf1jrs5vbb2xmcwym-pango-1.57.0-dev /nix/store/7s7by82nq8bahsh195qr0mnn9ac8ljmm-perl5.40.0-AnyEvent-I3-0.19 /nix/store/9ml0p4x1cx5k1lla91bxgramc0amsfkf-perl5.40.0-X11-XCB-0.20 /nix/store/67j1sx7qcn6f7qvq1kh3z8i5mpajgq3r-perl5.40.0-IPC-Run-20231003.0 /nix/store/859x84mz38bcq0r7hwksk4b5apcsmf2w-perl5.40.0-ExtUtils-PkgConfig-1.16 /nix/store/q1qydg6frfpq9jkhnymfsjzf71x9jswr-perl5.40.0-Inline-C-0.82&#34;,
</span></span><span style="display:flex;"><span>      &#34;builder&#34;: &#34;/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash&#34;,
</span></span><span style="display:flex;"><span>      &#34;checkPhase&#34;: &#34;runHook preCheck\n\ntest_failed=\n# \&#34;| cat\&#34; disables fancy progress reporting which makes the log unreadable.\n./complete-run.pl -p 1 --keep-xserver-output | cat || test_failed=\&#34;complete-run.pl returned $?\&#34;\nif [ -z \&#34;$test_failed\&#34; ]; then\n  # Apparently some old versions of `complete-run.pl` did not return a\n  # proper exit code, so check the log for signs of errors too.\n  grep -q &#39;^not ok&#39; latest/complete-run.log &amp;&amp; test_failed=\&#34;test log contains errors\&#34; ||:\nfi\nif [ -n \&#34;$test_failed\&#34; ]; then\n  echo \&#34;***** Error: $test_failed\&#34;\n  echo \&#34;===== Test log =====\&#34;\n  cat latest/complete-run.log\n  echo \&#34;===== End of test log =====\&#34;\n  false\nfi\n\nrunHook postCheck\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;cmakeFlags&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;configureFlags&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;debug&#34;: &#34;/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildBuild&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildBuildPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildTarget&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildTargetPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsHostHost&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsHostHostPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsTargetTarget&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsTargetTargetPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;doCheck&#34;: &#34;1&#34;,
</span></span><span style="display:flex;"><span>      &#34;doInstallCheck&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;mesonFlags&#34;: &#34;-Ddocs=true -Dmans=true&#34;,
</span></span><span style="display:flex;"><span>      &#34;name&#34;: &#34;i3-4.24&#34;,
</span></span><span style="display:flex;"><span>      &#34;nativeBuildInputs&#34;: &#34;/nix/store/x06h0jfzv99c3dmb8pj8wbmy0v9wj6bd-pkg-config-wrapper-0.29.2 /nix/store/pcdnznc797nmf9svii18k3c5v22sqihs-make-shell-wrapper-hook /nix/store/nzg469dkg5dj7lv4p50pi8zmwzxx73hr-meson-1.9.1 /nix/store/rlcn0x0j22nbhhf8wfp8cwfxgh65l82r-ninja-1.13.1 /nix/store/hs4pgi40k5nbl0fpf0jx8i5f6zrdv63v-install-shell-files /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/xiqlw1h0i6a6v59skrg9a7rg3qpanqy7-asciidoc-10.2.1 /nix/store/300facd5m37fwqrypjcikn09vqs488zv-xmlto-0.0.29 /nix/store/yk7avh2szvm6bi5dwgzz4c2iciaipj2p-docbook-xml-4.5 /nix/store/d5qdxn0rjl9s7xfc1rca33gya0fhcvkm-docbook-xsl-nons-1.79.2 /nix/store/2y1r1cpza3lpk7v6y9mf75ak0pswilwi-find-xml-catalogs-hook /nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh /nix/store/xlhipdkyqksxvp73cznnij5q6ilbbqd9-xorg-server-21.1.21-dev /nix/store/i8nxxmw5rzhxlx3n12s3lvplwwap6mpc-xvfb-run-1+g87f6705 /nix/store/a198i9cnhn6y5cajkdxg0hhcrmalazjr-xdotool-3.20211022.1 /nix/store/b4dnjyq2i4kjg8xswkjd7lwfcdps94j8-setxkbmap-1.3.4 /nix/store/cxdbw6iqj1a1r69wb55xl5nwi7abfllb-xrandr-1.5.3 /nix/store/5k4mv2a1qrciv12wywlkgpslc6swyv58-which-2.23&#34;,
</span></span><span style="display:flex;"><span>      &#34;out&#34;: &#34;/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24&#34;,
</span></span><span style="display:flex;"><span>      &#34;outputs&#34;: &#34;out debug&#34;,
</span></span><span style="display:flex;"><span>      &#34;patches&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;pname&#34;: &#34;i3&#34;,
</span></span><span style="display:flex;"><span>      &#34;postInstall&#34;: &#34;wrapProgram \&#34;$out/bin/i3-save-tree\&#34; --prefix PERL5LIB \&#34;:\&#34; \&#34;$PERL5LIB\&#34;\nfor program in $out/bin/i3-sensible-*; do\n  sed -i &#39;s/which/command -v/&#39; $program\ndone\n\ninstallManPage man/*.1\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;postPatch&#34;: &#34;patchShebangs .\n\n# This testcase generates a Perl executable file with a shebang, and\n# patchShebangs can&#39;t replace a shebang in the middle of a file.\nif [ -f testcases/t/318-i3-dmenu-desktop.t ]; then\n  substituteInPlace testcases/t/318-i3-dmenu-desktop.t \\\n    --replace-fail \&#34;#!/usr/bin/env perl\&#34; \&#34;#!/nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0/bin/perl\&#34;\nfi\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;propagatedBuildInputs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;propagatedNativeBuildInputs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;separateDebugInfo&#34;: &#34;1&#34;,
</span></span><span style="display:flex;"><span>      &#34;src&#34;: &#34;/nix/store/qx48i7zf9n69yla8gfbif6dskysk0l1w-source&#34;,
</span></span><span style="display:flex;"><span>      &#34;stdenv&#34;: &#34;/nix/store/43dbh9z6v997g6njz4yqmcrj26zic9ds-stdenv-linux&#34;,
</span></span><span style="display:flex;"><span>      &#34;strictDeps&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;system&#34;: &#34;x86_64-linux&#34;,
</span></span><span style="display:flex;"><span>      &#34;version&#34;: &#34;4.24&#34;
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;inputDrvs&#34;: {
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0h97zzsaf4ggiiwi0rbdjl3fzjj8vhj0-meson-1.9.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0r073sy0685h3gycpl8kpkgmv5p87rw4-libxcb-1.17.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0rjr80q4lpigwjwaxw089wcrrag7p46m-xmlto-0.0.29.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/14wsbyw3j1h9blcxr16c9663w0piq0p2-bash-5.3p3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/165y3ip2cqlnqd6qrgh6lzklv21xy11w-make-shell-wrapper-hook.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/1abxvpwsry6q5pijb2j91aryh2ilp929-pango-1.57.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/2sjcj6l2959dvd5vlicmkf1sdr0hwqx5-perl5.40.0-ExtUtils-PkgConfig-1.16.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/3jnvpbpi95g6zp8vjq1qafh20lz6kwi3-perl5.40.0-X11-XCB-0.20.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/45szhbhybqh4fkcpmx7sqpcrpwpadvgv-pkg-config-wrapper-0.29.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/4r5bd9g98fq40hjbfc7sbnp42jhnzg5h-yajl-2.1.0-unstable-2024-02-01.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/4yw0g3zqw4gn1szw8bqrvgmz5b6qm8s5-stdenv-linux.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/53gin0imc257fibkbyvl0jsi0pm1zvbl-docbook-xml-4.5.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/54q42ddy9jb24v4mbx0f19faqqsw5jga-libxkbcommon-1.11.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/56dg95jlnwp6kkifyqh94f548r5cha9b-xrandr-1.5.3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/6srgz2k17vc6x85s3paccdbgg9rv0bia-asciidoc-10.2.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/7xpmbw1xzzwxcd1rnx6qid7zhqnzq3jh-setxkbmap-1.3.4.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/87b385i529h64dzrycf16ksv0jcbzs29-libev-4.33.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/9l94a5gr0wbhaq6zyl30wpqygp1cffrx-pcre2-10.46.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/b8hhyx6rpy47hkbq5wlhrvfrfv3yn7j8-xvfb-run-1+g87f6705.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/bxrnxv90lrpvq06rja47986h057rhwcc-libxcb-cursor-0.1.6.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/cgdz2idkz91w2k7hpb2dymv80938cz9w-libxcb-wm-0.4.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ddvlvaj43mls902nay7ddjrg01d6c2la-perl-5.40.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ddxlvkpjlg6ycayb6az23ldjdr21xlnf-which-2.23.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ds5ss96inhkj9x2gbd7shinvbiid6v6b-xorg-server-21.1.21.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/f0yqdlwz2vwsx51wlgmi9pjqpdhbprkx-ninja-1.13.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/gm613dry4hkv26m7ml49fq60z8p0r0gf-perl5.40.0-IPC-Run-20231003.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/h3sjzf7hg9ghbh4hzdg6c4byfky2fjng-libx11-1.8.12.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/j5ji7yjwizrma9h72h2pqgi8ir6ah6q8-libstartup-notification-0.12.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/k2jxg4mck2f4pqlisp6slwhyd3pva8wz-source.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/n19ll9p9ivkni2y9l9i2rypyi5gi8z58-perl5.40.0-Inline-C-0.82.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/nm7v937f2z7srs54idjwc7sl6azc1slj-xdotool-3.20211022.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/qzg3b7p4gf4izfjbkc42bjyrvp8vz99k-xcb-util-xrm-1.3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/rjmh0kp3w170bii9i57z5anlshzm2gll-install-shell-files.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/rrsm8jbqqf58k30cm2lxmgk43fkxsgqp-find-xml-catalogs-hook.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/s4wl1ny41k50rkxw0x0wdjf9l5mjqyv0-libxcb-util-0.4.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/vxckbgl5kwf5ikz0ma0fkavsnh683ry0-libxcb-keysyms-0.4.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/xxb7x7j73p3sxf03hb1hzaz588avd3yw-docbook-xsl-nons-1.79.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/yik59jhh69af5fcvddmxlhfwya69pnzw-perl5.40.0-AnyEvent-I3-0.19.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;inputSrcs&#34;: [
</span></span><span style="display:flex;"><span>      &#34;/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh&#34;
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    &#34;name&#34;: &#34;i3-4.24&#34;,
</span></span><span style="display:flex;"><span>    &#34;outputs&#34;: {
</span></span><span style="display:flex;"><span>      &#34;debug&#34;: {
</span></span><span style="display:flex;"><span>        &#34;path&#34;: &#34;/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug&#34;
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;out&#34;: {
</span></span><span style="display:flex;"><span>        &#34;path&#34;: &#34;/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24&#34;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;system&#34;: &#34;x86_64-linux&#34;
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div></details>
<p>Unfortunately, I am not aware of a way to go from the derivation to the <code>.nix</code>
source, but at least one can check that a certain source results in an identical
derivation.</p>
<h3 id="developer-builds">Developer builds</h3>
<p>The versioning I have described so far is sufficient for most users, who will
not be interested in tracking intermediate versions of software, but only the
released versions.</p>
<p>But what about developers, or any kind of user who needs more precision?</p>
<p>When building i3 from git, it reports the git revision it was built from, using
<a href="https://manpages.debian.org/git-describe.1"><code>git-describe(1)</code></a>
:</p>
<pre tabindex="0"><code>~/i3/build % git describe
4.25-23-g98f23f54
~/i3/build % ninja
[110/110] Linking target i3
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54 © 2009 Michael Stapelberg and contributors
</code></pre><p>A modified working copy gets represented by a <code>+</code> after the revision:</p>
<pre tabindex="0"><code>~/i3/build % echo &#39;// dirty working copy&#39; &gt;&gt; ../src/main.c &amp;&amp; ninja
[104/104] Linking target i3bar
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54+ © 2009 Michael Stapelberg and contributors
</code></pre><p>Reporting the git revision (or VCS revision, generally speaking) is the most
useful choice.</p>
<p>This way, we catch the following common mistakes:</p>
<ul>
<li>People build from the wrong revision.</li>
<li>People build, but forget to install.</li>
<li>People install, but their session does not pick it up (wrong location?).</li>
</ul>
<h2 id="useful-stamp-vcs-rev">Most Useful: Stamp The VCS Revision</h2>
<p>As we have seen above, the single most useful piece of version information is
the VCS revision. We can fetch all other details (version numbers, dates,
authors, …) from the VCS repository.</p>
<p>Now, let’s demonstrate the best case scenario by looking at how Go does it!</p>
<h3 id="go-vcs">Go always stamps! 🥳</h3>
<p>Go has become my favorite programming language over the years, in big part
because of the good taste and style of the Go developers, and of course also
because of the high-quality tooling:</p>



  <div class="postlink">
      <div>
	<a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/"><h3>Why Go is my favorite programming language</h3></a>
      </div>
      <div class="summary">
	
	
	
	
	<p>
	  <a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/">
	  I strive to respect everybody’s personal preferences, so I usually steer clear of debates about which is the best programming language, text editor or operating system.
However, recently I was asked a couple of times why I like and use a lot of Go, so here is a coherent article to fill in the blanks of my ad-hoc in-person ramblings :-).

	  <span class="readmore"><a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/">Read more →</a></span>
	  </a>
	</p>
      </div>
  </div>


<p>Therefore, I am pleased to say that Go implements the gold standard with regard
to software versioning: it stamps VCS buildinfo by default! 🥳 This was
introduced in <a href="https://go.dev/doc/go1.18#debug_buildinfo">Go 1.18 (March 2022)</a>:</p>
<blockquote>
<p>Additionally, the go command embeds information about the build, including
build and tool tags (set with -tags), compiler, assembler, and linker flags
(like -gcflags), whether cgo was enabled, and if it was, the values of the cgo
environment variables (like CGO_CFLAGS).</p>
<p>Both VCS and build information may be read together with module information
using <code>go version -m file</code> or
<a href="https://pkg.go.dev/runtime/debug#ReadBuildInfo">runtime/debug.ReadBuildInfo</a>
(for the currently running binary) or the new
<a href="https://pkg.go.dev/debug/buildinfo">debug/buildinfo</a> package.</p>
</blockquote>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> Before Go 1.18, the standard approach was to use <code>-ldflags -X main.version=$(git describe)</code> or similar explicit injection. This setup works
(and can still be seen in many places) but requires making changes to the
application code, whereas the Go 1.18+ stamping requires no extra steps.</div>
  </div>
</aside>

<p>What does this mean in practice? Here is a diagram for the common case: building
from git:</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-common-case-build-from-git.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-common-case-build-from-git.svg"
  alt="diagram showing going from git repository to binary by invoking go build / go install" title="diagram showing going from git repository to binary by invoking go build / go install"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<p>This covers most of my hobby projects!</p>
<p>Many tools I just <code>go install</code>, or <code>CGO_ENABLED=0 go install</code> if I want to
easily copy them around to other computers. Although, I am managing more and
more of my software in NixOS.</p>
<p>When I find a program that is not yet fully managed, I can use <code>gops</code> and the
<code>go</code> tool to identify it:</p>
<div style="font-size: 85%">
<pre tabindex="0"><code>root@ax52 ~ % nix run nixpkgs#gops
2573594 1       dcs-package-importer  go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-package-importer
2573576 1       dcs-source-backend    go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-source-backend
2573566 1       debiman               go1.25.5 /srv/man/bin/debiman
[…]
root@ax52 ~ % nix run nixpkgs#go -- version -m /srv/man/bin/debiman
/srv/man/bin/debiman: go1.25.5
  path	github.com/Debian/debiman/cmd/debiman
  mod	github.com/Debian/debiman	v0.0.0-20251230101540-ac8f5391b43b+dirty
  […]
  dep	pault.ag/go/debian	v0.18.0	h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
  dep	pault.ag/go/topsort	v0.1.1	h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
  build	-buildmode=exe
  build	-compiler=gc
  build	DefaultGODEBUG=containermaxprocs=0,decoratemappings=0,tlssha1=1,updatemaxprocs=0,x509sha256skid=0
  build	CGO_ENABLED=0
  build	GOARCH=amd64
  build	GOOS=linux
  build	GOAMD64=v1
  build	vcs=git
  build	vcs.revision=ac8f5391b43bc1a9dbdc99f6179e2fb7d7414a04
  build	vcs.time=2025-12-30T10:15:40Z
  build	vcs.modified=true
root@ax52 ~ %
</code></pre></div>
<p>It’s very cool that Go does the right thing by default!</p>
<p>Systems that consist of 100% Go software (like my <a href="https://gokrazy.org/">gokrazy Go appliance
platform</a>) are fully stamped! For example, the gokrazy web
interface shows me exactly which version and dependencies went into the
<code>gokrazy/rsync</code> build on my <a href="https://github.com/stapelberg/scan2drive">scan2drive
appliance</a>.</p>
<p>Despite being fully stamped, note that gokrazy only shows the module versions,
and no VCS buildinfo, because it currently suffers from the same gap as Nix:</p>















<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_bdda2b3931bb0cee.png 2x,https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_f5be822c1490d069.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_8f704022ad0a233b.png"
  alt="gokrazy scan2drive rsync" title="gokrazy scan2drive rsync"
  width="600"
  height="433"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h3 id="go-version-reporting">Go Version Reporting</h3>
<p>For the gokrazy packer, which follows a rolling release model (no version
numbers), I ended up with a few lines of Go code (see below) to display a git
revision, no matter if you installed the packer from a Go module or from a git
working copy.</p>
<p>The code either displays <code>vcs.revision</code> (the easy case; built from git) or
extracts the revision from the Go module version of the main module
(<a href="https://pkg.go.dev/runtime/debug#BuildInfo"><code>BuildInfo.Main.Version</code></a>):</p>
<p>What are the other cases? These examples illustrate the scenarios I usually deal
with:</p>
<table>
	<thead>
			<tr>
					<th>source (built from)</th>
					<th>buildinfo (stamped into program)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>directory (no git)</td>
					<td>module <code>(devel)</code></td>
			</tr>
			<tr>
					<td>Go module</td>
					<td>module <code>v0.3.1-0.20260105212325-5347ac5f5bcb</code></td>
			</tr>
			<tr>
					<td>directory (git)</td>
					<td>module <code>v0.0.0-20260131174001-ccb1d233f2a4+dirty</code></td>
			</tr>
			<tr>
					<td></td>
					<td><code>vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551</code></td>
			</tr>
			<tr>
					<td></td>
					<td><code>vcs.time=2026-01-31T17:40:01Z</code></td>
			</tr>
			<tr>
					<td></td>
					<td><code>vcs.modified=true</code></td>
			</tr>
	</tbody>
</table>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-go-install-git-vs-module.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-go-install-git-vs-module.svg"
  alt="diagram showing the two cases with go build info stamping: building from a git checkout or installing from a Go module" title="diagram showing the two cases with go build info stamping: building from a git checkout or installing from a Go module"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<details>
<summary>Go code to programmatically read the version</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>version<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;runtime/debug&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;strings&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">readParts</span>()<span style="color:#bbb"> </span>(revision<span style="color:#bbb"> </span><span style="color:#902000">string</span>,<span style="color:#bbb"> </span>modified,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#902000">bool</span>)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>info,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>debug.<span style="color:#06287e">ReadBuildInfo</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>!ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>settings<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">make</span>(<span style="color:#007020;font-weight:bold">map</span>[<span style="color:#902000">string</span>]<span style="color:#902000">string</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>s<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">range</span><span style="color:#bbb"> </span>info.Settings<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>settings[s.Key]<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>s.Value<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// When built from a local VCS directory, we can use vcs.revision directly.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>rev,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>settings[<span style="color:#4070a0">&#34;vcs.revision&#34;</span>];<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>rev,<span style="color:#bbb"> </span>settings[<span style="color:#4070a0">&#34;vcs.modified&#34;</span>]<span style="color:#bbb"> </span><span style="color:#666">==</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;true&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// When built as a Go module (not from a local VCS directory),</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// info.Main.Version is something like v0.0.0-20230107144322-7a5757f46310.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>v<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>info.Main.Version<span style="color:#bbb"> </span><span style="color:#60a0b0;font-style:italic">// for convenience</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>idx<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>strings.<span style="color:#06287e">LastIndexByte</span>(v,<span style="color:#bbb"> </span><span style="color:#4070a0">&#39;-&#39;</span>);<span style="color:#bbb"> </span>idx<span style="color:#bbb"> </span>&gt;<span style="color:#bbb"> </span><span style="color:#666">-</span><span style="color:#40a070">1</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>v[idx<span style="color:#666">+</span><span style="color:#40a070">1</span>:],<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&lt;BUG&gt;&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">Read</span>()<span style="color:#bbb"> </span><span style="color:#902000">string</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>revision,<span style="color:#bbb"> </span>modified,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">readParts</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>!ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&lt;not okay&gt;&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>modifiedSuffix<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>modified<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>modifiedSuffix<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#4070a0">&#34; (modified)&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;https://github.com/gokrazy/tools/commit/&#34;</span><span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>revision<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>modifiedSuffix<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div></details>
<p>This is what it looks like in practice:</p>
<pre tabindex="0"><code>% go install github.com/gokrazy/tools/cmd/gok@latest
% gok --version
https://github.com/gokrazy/tools/commit/8ed49b4fafc7
</code></pre><p>But a version built from git has the full revision available (→ you can tell them apart):</p>
<pre tabindex="0"><code>% (cd ~gokrazy/../tools &amp;&amp; go install ./cmd/...)
% gok --version
https://github.com/gokrazy/tools/commit/ba6a8936f4a88ddcf20a3b8f625e323e65664aa6 (modified)
</code></pre><h2 id="vcs-rev-with-nixos">VCS rev with NixOS</h2>
<p>When packaging Go software with Nix, it’s easy to lose Go VCS revision stamping:</p>
<ol>
<li>Nix fetchers like <code>fetchFromGitHub</code> are implemented by fetching an archive
(<code>.tar.gz</code>) file from GitHub — the full <code>.git</code> repository is not transferred,
which is more efficient.</li>
<li>Even if a <code>.git</code> repository is present, Nix usually intentionally removes it
for reproducibility: <code>.git</code> directories contain packed objects that change
across <code>git gc</code> runs (for example), which would break reproducible builds
(different hash for the same source).</li>
</ol>
<p>So the fundamental tension here is between reproducibility and VCS stamping.</p>
<p>Luckily, there is a solution that works for both: I created the
<a href="https://github.com/stapelberg/nix"><code>stapelberg/nix/go-vcs-stamping</code> Nix overlay
module</a> that you can import to get working Go
VCS revision stamping by default for your <code>buildGoModule</code> Nix expressions!</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-vcs-rev-with-nix.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-vcs-rev-with-nix.svg"
  alt="diagram from Git repo to go build without and with my go-vcs-stamping overlay workaround" title="diagram from Git repo to go build without and with my go-vcs-stamping overlay workaround"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<h3 id="nix-go-build-detail">The Nix Go build situation in detail</h3>
<p><strong>Tip:</strong> If you are not a Nix user, feel free to skip over this section. I
included it in this article so that you have a full example of making VCS
stamping work in the most complicated environments.</p>
<hr>
<p>Packaging Go software in Nix is pleasantly straightforward.</p>
<p>For example, the Go Protobuf generator plugin <code>protoc-gen-go</code> is packaged in Nix
with &lt;30 lines: <a href="https://github.com/NixOS/nixpkgs/blob/e347ac28905f77edcd1e9855dedcfb61e517f265/pkgs/by-name/pr/protoc-gen-go/package.nix">official nixpkgs <code>protoc-gen-go</code>
package.nix</a>. You
call
<a href="https://nixos.org/manual/nixpkgs/stable/#ssec-language-go"><code>buildGoModule</code></a>,
supply as <code>src</code> the result from
<a href="https://nixos.org/manual/nixpkgs/stable/#fetchfromgithub"><code>fetchFromGitHub</code></a>
and add a few lines of metadata.</p>
<p>But getting developer builds fully stamped is not straightforward at all!</p>
<p>When packaging my own software, I want to package individual revisions
(developer builds), not just released versions. I use the same <code>buildGoModule</code>,
or <code>buildGoLatestModule</code> if I need the latest Go version. Instead of using
<code>fetchFromGitHub</code>, I provide my sources using Flakes, usually also from GitHub
or from another Git repository. For example, I package <code>gokrazy/bull</code> like so:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  bullsrc<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Use buildGoLatestModule to build with Go 1.26</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># even before NixOS 26.05 Yarara is released</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># (NixOS 25.11 contains Go 1.25).</span>
</span></span><span style="display:flex;"><span>pkgs-unstable<span style="color:#666">.</span>buildGoLatestModule {
</span></span><span style="display:flex;"><span>  pname <span style="color:#666">=</span> <span style="color:#4070a0">&#34;bull&#34;</span>;
</span></span><span style="display:flex;"><span>  version <span style="color:#666">=</span> <span style="color:#4070a0">&#34;unstable&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  src <span style="color:#666">=</span> bullsrc;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Needs changing whenever `go mod vendor` changes,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># i.e. whenever go.mod is updated to use different versions.</span>
</span></span><span style="display:flex;"><span>  vendorHash <span style="color:#666">=</span> <span style="color:#4070a0">&#34;sha256-sU5j2dji5bX2rp+qwwSFccXNpK2LCpWJq4Omz/jmaXU=&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>bullsrc</code> comes from my <code>flake.nix</code>:</p>
<details>
<summary>Click here to expand the full <code>flake.nix</code></summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.11&#34;</span>;
</span></span><span style="display:flex;"><span>    nixpkgs-unstable<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-unstable&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    disko <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nix-community/disko&#34;</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#60a0b0;font-style:italic"># Use the same version as nixpkgs</span>
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    stapelbergnix<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/nix&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    zkjnastools<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/zkj-nas-tools&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    configfiles <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/configfiles&#34;</span>;
</span></span><span style="display:flex;"><span>      flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>; <span style="color:#60a0b0;font-style:italic"># repo is not a flake</span>
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bullsrc <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:gokrazy/bull&#34;</span>;
</span></span><span style="display:flex;"><span>      flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    sops-nix <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:Mic92/sops-nix&#34;</span>;
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      disko<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      zkjnastools<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      bullsrc<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      configfiles<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      sops-nix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>    }:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>      system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      pkgs-unstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixosConfigurations<span style="color:#666">.</span>keep <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> pkgs;
</span></span><span style="display:flex;"><span>        specialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> configfiles; };
</span></span><span style="display:flex;"><span>        modules <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>          disko<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>disko
</span></span><span style="display:flex;"><span>          sops-nix<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>sops
</span></span><span style="display:flex;"><span>          <span style="color:#235388">./configuration.nix</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>zshConfig
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Use systemd for network configuration</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdNetwork
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Use systemd-boot as bootloader</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdBoot
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Run prometheus node exporter in tailnet</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>prometheusNode
</span></span><span style="display:flex;"><span>          zkjnastools<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>zkjbackup
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          {
</span></span><span style="display:flex;"><span>            nixpkgs<span style="color:#666">.</span>overlays <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>              (final: prev: {
</span></span><span style="display:flex;"><span>                bull <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./bull-pkg.nix</span> {
</span></span><span style="display:flex;"><span>                  pkgs <span style="color:#666">=</span> final;
</span></span><span style="display:flex;"><span>                  pkgs-unstable <span style="color:#666">=</span> pkgs-unstable;
</span></span><span style="display:flex;"><span>                  <span style="color:#007020;font-weight:bold">inherit</span> bullsrc;
</span></span><span style="display:flex;"><span>                };
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>            ];
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      formatter<span style="color:#666">.</span><span style="color:#70a0d0">${</span>system<span style="color:#70a0d0">}</span> <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>nixfmt-tree;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></details>
<p>Go stamps all builds, but it does not have much to stamp here:</p>
<ul>
<li>We build from a directory, not a Go module, so the module version is <code>(devel)</code>.</li>
<li>The stamped buildinfo does not contain any <code>vcs</code> information.</li>
</ul>
<p>Here’s a full example of gokrazy/bull:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;display:grid;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% go version -m \
</span></span><span style="display:flex;"><span>  /nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull
</span></span><span style="display:flex;"><span>/nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull: go1.26.1
</span></span><span style="display:flex;"><span>        path    github.com/gokrazy/bull/cmd/bull
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        mod     github.com/gokrazy/bull (devel) 
</span></span><span style="display:flex;"><span>        dep     github.com/BurntSushi/toml      v1.4.1-0.20240526193622-a339e1f7089c    
</span></span><span style="display:flex;"><span>        dep     github.com/fsnotify/fsnotify    v1.8.0  
</span></span><span style="display:flex;"><span>        dep     github.com/google/renameio/v2   v2.0.2  
</span></span><span style="display:flex;"><span>        dep     github.com/yuin/goldmark        v1.7.8  
</span></span><span style="display:flex;"><span>        dep     go.abhg.dev/goldmark/wikilink   v0.5.0  
</span></span><span style="display:flex;"><span>        dep     golang.org/x/image      v0.23.0 
</span></span><span style="display:flex;"><span>        dep     golang.org/x/sync       v0.10.0 
</span></span><span style="display:flex;"><span>        dep     golang.org/x/sys        v0.28.0 
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -buildmode=exe
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -compiler=gc
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -trimpath=true
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   CGO_ENABLED=0
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOARCH=amd64
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOOS=linux
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOAMD64=v1</span></span></code></pre></div>
<p>To fix VCS stamping, add my <code>goVcsStamping</code> overlay to your <code>nixosSystem.modules</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  nixpkgs<span style="color:#666">.</span>overlays <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    stapelbergnix<span style="color:#666">.</span>overlays<span style="color:#666">.</span>goVcsStamping
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>(If you are using <code>nixpkgs-unstable</code>, like I am, you need to apply the overlay in both places.)</p>
<p>After rebuilding, your Go binaries should newly be stamped with <code>vcs</code> buildinfo:</p>
<pre tabindex="0"><code>% go version -m /nix/store/z8mgsf10pkc6dgvi8pfnbb7cs23pqfkn-bull-unstable/bin/bull
[…]
  build   vcs=git
  build   vcs.revision=c0134ef21d37e4ca8346bdcb7ce492954516aed5
  build   vcs.time=2026-03-22T08:32:55Z
  build   vcs.modified=false
</code></pre><p>Nice! 🥳 But… how does it work? When does it apply? How do you know how to fix
your config?</p>
<p>I’ll show you <strong>the full diagram</strong> first, and then explain how to read it:</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-nix-big-picture.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-nix-big-picture.svg"
  alt="a big diagram showing all the ways from .nix expression to a stamped binary or a binary where VCS info got lost" title="a big diagram showing all the ways from .nix expression to a stamped binary or a binary where VCS info got lost"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<p>There are 3 relevant parts of the Nix stack that you can end up in, depending on
what you write into your <code>.nix</code> files:</p>
<ol>
<li>Fetchers. These are what Flakes use, but also non-Flake use-cases.</li>
<li>Fixed-output derivations (FOD). This is how <code>pkgs.fetchgit</code> is implemented,
but the constant hash churn (updating the <code>sha256</code> line) inherent to FODs is
annoying.</li>
<li>Copiers. These just copy files into the Nix store and are not git-aware.</li>
</ol>
<p>For the purpose of VCS revision stamping, you should:</p>
<ul>
<li>Avoid the Copiers! If you use Flakes:
<ul>
<li>❌ do not use <code>url = &quot;/home/michael/dcs&quot;</code> as a Flake input</li>
<li>✅ use <code>url = &quot;git+file:///home/michael/dcs&quot;</code> instead for git awareness</li>
</ul>
</li>
<li>I avoid the fixed-output derivation (FOD) as well.
<ul>
<li>Fetching the git repository at build time is slow and inefficient.</li>
<li>Enabling <code>leaveDotGit</code>, which is needed for VCS revision stamping with this
approach, is even more inefficient because a new Git repository must be
constructed deterministically to keep the FOD reproducible.</li>
</ul>
</li>
</ul>
<p>Hence, we will stick to the left-most column: fetchers.</p>
<p>Unfortunately, by default, with fetchers, the VCS revision information, which is
stored in a Nix attrset (in-memory, during the build process), does not make it
into the Nix store, hence, when the Nix derivation is evaluated and Go compiles
the source code, Go does not see any VCS revision.</p>
<p>My <a href="https://github.com/stapelberg/nix"><code>stapelberg/nix/go-vcs-stamping</code> Nix overlay
module</a> fixes this, and enabling the overlay
is how you end up in the left-most lane of the above diagram: the happy path,
where your Go binaries are now stamped!</p>
<h3 id="nixos-buildinfo-overlay">My workaround: Nix git buildinfo overlay</h3>
<p>How does the <code>go-vcs-stamping</code> overlay work? It functions as an adapter between
Nix and Go:</p>
<ul>
<li>Nix tracks the VCS revision in the <code>.rev</code> in-memory attrset.</li>
<li>Go expects to find the VCS revision in a <code>.git</code> repository, accessed via
<code>.git/HEAD</code> file access and <a href="https://manpages.debian.org/git.1"><code>git(1)</code></a>
 commands.</li>
</ul>
<p>So the overlay implements 3 steps to get Go to stamp the correct info:</p>
<ol>
<li>It synthesizes a <code>.git/HEAD</code> file so that Go’s <code>vcs.FromDir()</code> detects a git
repository.</li>
<li>It injects a <code>git</code> command into the <code>PATH</code> that implements exactly the two
commands used by Go and fails loudly on anything else (in case Go updates its
implementation).</li>
<li>It sets <code>-buildvcs=true</code> in the <code>GOFLAGS</code> environment variable.</li>
</ol>
<p>For the full source, see
<a href="https://github.com/stapelberg/nix/blob/main/go-vcs-stamping.nix"><code>go-vcs-stamping.nix</code></a>.</p>
<h3 id="clean-fix">The clean fix</h3>
<p>See <a href="https://github.com/golang/go/issues/77020">Go issue #77020</a> and <a href="https://github.com/golang/go/issues/64162">Go issue
#64162</a> for a cleaner approach to
fixing this gap: allowing package managers to invoke the Go tool with the
correct VCS information injected.</p>
<p>This would allow Nix (or also gokrazy) to pass along buildinfo cleanly, without
the need for <a href="#nixos-buildinfo-overlay">workarounds like my <code>go-vcs-stamping</code>
adapter</a>.</p>
<p>At the time of writing, issue #77020 does not seem to have much traction and is
still open.</p>
<h2 id="conclusion-stamp-it-plumb-it-report-it">Conclusion: Stamp it! Plumb it! Report it!</h2>
<p>My argument is simple:</p>
<p><strong>Stamping the VCS revision is conceptually easy, but very important!</strong></p>
<p>For example, if the production system from the incident I mentioned had reported
its version, we would have saved multiple hours of mitigation time!</p>
<p>Unfortunately, many environments only identify the build output (useful, but
orthogonal), but do not plumb the VCS revision (much more useful!), or at least
not by default.</p>
<p>Your action plan to fix it is just 3 simple steps:</p>
<ol>
<li>Stamp it! Include the source VCS revision in your programs.
<ul>
<li>This is not a new idea: <a href="https://i3wm.org">i3</a> builds include their <a href="https://manpages.debian.org/git-describe.1"><code>git-describe(1)</code></a>
 revision since 2012!</li>
</ul>
</li>
<li>Plumb it! When building / packaging, ensure the VCS revision does not get lost.
<ul>
<li>My <a href="#vcs-rev-with-nixos">“VCS rev with NixOS”</a> case study section above
illustrates several reasons why the VCS rev could get lost, which paths
can work and how to fix the missing plumbing.</li>
</ul>
</li>
<li>Report it! Make your software print its VCS revision on every relevant
surface, for example:
<ul>
<li><strong>Executable programs:</strong> Report the VCS revision when run with <code>--version</code>
<ul>
<li>For Go programs, you can always use <code>go version -m</code></li>
</ul>
</li>
<li><strong>Services and batch jobs:</strong> Include the VCS revision in the startup logs.</li>
<li><strong>Outgoing HTTP requests:</strong> Include the VCS revision in the <code>User-Agent</code></li>
<li><strong>HTTP responses:</strong> Include the VCS revision in a header (internally)</li>
<li><strong>Remote Procedure Calls (RPCs):</strong> Include the revision in RPC metadata</li>
<li><strong>User Interfaces:</strong> Expose the revision somewhere visible for debugging.</li>
</ul>
</li>
</ol>
<p>Implementing “version observability” throughout your system is a one-day
high-ROI project.</p>
<p>With my Nix example, you saw how the VCS revision is available throughout the
stack, but can get lost in the middle. Hopefully my resources help you quickly
fix your stack(s), too:</p>
<ul>
<li><a href="https://github.com/stapelberg/nix">My <code>stapelberg/nix/go-vcs-stamping</code>
overlay</a> for Nix / NixOS</li>
<li><a href="https://github.com/stapelberg/stampit">My <code>stampit</code> repository</a> is a
community resource to collect examples (as markdown content) and includes a Go
module with a few helpers to make version reporting trivial.</li>
</ul>
<p><em><strong>Now go stamp your programs and data transfers! 🚀</strong></em></p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[In praise of grobi for auto-configuring X11 monitors]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/"/>
    <id>https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/</id>
    <published>2025-05-10T08:24:00+02:00</published>
    <content type="html"><![CDATA[<p>I have recently started using the <a href="https://github.com/fd0/grobi/"><code>grobi</code> program by Alexander
Neumann</a> again and was delighted to discover that
it makes using my fiddly (but wonderful) <a href="/posts/2017-12-11-dell-up3218k/">Dell 32-inch 8K monitor
(UP3218K)</a> monitor much more convenient — I get
a signal more quickly than with my previous, sleep-based approach.</p>
<p>Previously, when my PC woke up from suspend-to-RAM, there were two scenarios:</p>
<ol>
<li>The monitor was connected. My <a href="#zleep">sleep program</a> would power on the
monitor (if needed), sleep a little while and then run <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a>
 to (hopefully) configure the monitor correctly.</li>
<li>The monitor was not connected, for example because it was still connected to
my work PC.</li>
</ol>
<p>In scenario ②, or if the one-shot configuration attempt in scenario ① fails, I
would need to SSH in from a different computer and run <code>xrandr</code> manually so that
the monitor would show a signal:</p>
<pre tabindex="0"><code>% DISPLAY=:0 xrandr \
  --output DP-4 --mode 3840x4320 --panning 0x0+0+0 \
  --output DP-2 --right-of DP-4 --mode 3840x4320 --panning 0x0+3840+0
</code></pre><h2 id="automatic-monitor-configuration-with-grobi">Automatic monitor configuration with grobi</h2>
<p>I have now completely solved this problem by creating the following
<code>~/.config/grobi.conf</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">rules</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>UP3218K<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">outputs_connected</span>:<span style="color:#bbb"> </span>[DP-2, DP-4]<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic"># DP-4 is left, DP-2 is right</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">configure_row</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>- DP-4@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>- DP-2@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#60a0b0;font-style:italic"># atomic instructs grobi to only call xrandr once and configure all the</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#60a0b0;font-style:italic"># outputs. This does not always work with all graphic cards, but is</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic"># needed to successfully configure the UP3218K monitor.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">atomic</span>:<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>…and installing / enabling <code>grobi</code> (on Arch Linux) using:</p>
<pre tabindex="0"><code>% sudo pacman -S grobi
% systemctl --user enable --now grobi
</code></pre><p>Whenever <code>grobi</code> detects that my monitor is connected (it listens for <a href="https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt">X11
RandR</a>
output change events), it will run <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a>
 to
configure the monitor resolution and positioning.</p>
<p>To check what <code>grobi</code> is seeing/doing, you can use:</p>
<pre tabindex="0"><code>% systemctl --user status grobi
% journalctl --user -u grob
</code></pre><p>For example, on my system, I see:</p>
<pre tabindex="0"><code>grobi: 18:31:48.823765 outputs: [HDMI-0 (primary) DP-0 DP-1 DP-2 (connected) 3840x2160+ [DEL-16711-808727372-DELL UP3218K-D2HP805I043L] DP-3 DP-4 (connected) 3840x21&gt;
grobi: 18:31:48.823783 new rule found: UP3218K
grobi: 18:31:48.823785 enable outputs: [DP-4@3840x4320 DP-2@3840x4320]
grobi: 18:31:48.823789 using one atomic call to xrandr
grobi: 18:31:48.823806 running command /usr/bin/xrandr xrandr --output DP-4 --mode 3840x4320 --output DP-2 --mode 3840x4320 --right-of DP-4
grobi: 18:31:49.285944 new RANDR change event received
</code></pre><p>Notably, the instructions for getting out of a bad state (no signal) are now to
power off the monitor and power it back on again. This will result in RandR
output change events, which will trigger <code>grobi</code>, which will run <code>xrandr</code>, which
configures the monitor. Nice!</p>
<h2 id="why-not-autorandr">Why not autorandr?</h2>
<p>No particular reason. I knew <code>grobi</code>.</p>
<p>If nothing else, <code>grobi</code> is written in Go, so it’s likely to keep working
smoothly over the years.</p>
<h2 id="does-grobi-work-on-wayland">Does grobi work on Wayland?</h2>
<p>Probably not. There is no mention of Wayland over on the <a href="https://github.com/fd0/grobi/">grobi
repository</a>.</p>
<h2 id="zleep">Bonus: my Suspend-to-RAM setup</h2>
<p>As a bonus, this section describes the other half of my monitor-related
automation.</p>
<p>When I suspend my PC to RAM, I either want to wake it up manually later, for
example by pressing a key on the keyboard or by sending a Wake-on-LAN packet, or
I want it to wake up automatically each morning at 6:50 — that way, daily cron
jobs have some time to run before I start using the computer.</p>
<p>To accomplish this, I use <code>zleep</code>, a wrapper program around <a href="https://manpages.debian.org/rtcwake.8"><code>rtcwake(8)</code></a>
 and <code>systemctl suspend</code> that integrates with the
myStrom switch smart plug to turn off power to the monitor entirely. This is
worthwhile because the monitor draws 30W even in standby!</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;context&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;flag&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;fmt&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;log&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;net/http&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;net/url&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os/exec&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;time&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>resume<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>flag.<span style="color:#06287e">Bool</span>(<span style="color:#4070a0">&#34;resume&#34;</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#4070a0">&#34;run resume behavior only (turn on monitor via smart plug)&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>noMonitor<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>flag.<span style="color:#06287e">Bool</span>(<span style="color:#4070a0">&#34;no_monitor&#34;</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#4070a0">&#34;disable turning off/on monitor&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx<span style="color:#bbb"> </span>context.Context,<span style="color:#bbb"> </span>method,<span style="color:#bbb"> </span>cmnd<span style="color:#bbb"> </span><span style="color:#902000">string</span>)<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span><span style="color:#666">*</span>noMonitor<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] skipping because -no_monitor flag is set&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] command: %v&#34;</span>,<span style="color:#bbb"> </span>cmnd)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>u,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>url.<span style="color:#06287e">Parse</span>(<span style="color:#4070a0">&#34;http://myStrom-Switch-A46FD0/&#34;</span><span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>cmnd)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>ctx.<span style="color:#06287e">Err</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>req,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>http.<span style="color:#06287e">NewRequest</span>(method,<span style="color:#bbb"> </span>u.<span style="color:#06287e">String</span>(),<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>ctx,<span style="color:#bbb"> </span>canc<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">WithTimeout</span>(ctx,<span style="color:#bbb"> </span><span style="color:#40a070">5</span><span style="color:#666">*</span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#06287e">canc</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>req<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>req.<span style="color:#06287e">WithContext</span>(ctx)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>resp,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>http.DefaultClient.<span style="color:#06287e">Do</span>(req)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>time.<span style="color:#06287e">Sleep</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">continue</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>resp.StatusCode<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span>http.StatusOK<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;unexpected HTTP status code: got %v, want %v&#34;</span>,<span style="color:#bbb"> </span>resp.Status,<span style="color:#bbb"> </span>http.StatusOK)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>time.<span style="color:#06287e">Sleep</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">continue</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] request succeeded&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">nextWakeup</span>(now<span style="color:#bbb"> </span>time.Time)<span style="color:#bbb"> </span>time.Time<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>midnight<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>time.<span style="color:#06287e">Date</span>(now.<span style="color:#06287e">Year</span>(),<span style="color:#bbb"> </span>now.<span style="color:#06287e">Month</span>(),<span style="color:#bbb"> </span>now.<span style="color:#06287e">Day</span>(),<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span>time.Local)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>now.<span style="color:#06287e">Hour</span>()<span style="color:#bbb"> </span>&lt;<span style="color:#bbb"> </span><span style="color:#40a070">6</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// wake up today</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>midnight.<span style="color:#06287e">Add</span>(<span style="color:#40a070">6</span><span style="color:#666">*</span>time.Hour<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span><span style="color:#40a070">50</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// wake up tomorrow</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>midnight.<span style="color:#06287e">Add</span>(<span style="color:#40a070">24</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Hour).<span style="color:#06287e">Add</span>(<span style="color:#40a070">6</span><span style="color:#666">*</span>time.Hour<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span><span style="color:#40a070">50</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">runResume</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Retry for up to one minute to give the network some time to come up</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>ctx,<span style="color:#bbb"> </span>canc<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">WithTimeout</span>(context.<span style="color:#06287e">Background</span>(),<span style="color:#bbb"> </span><span style="color:#40a070">1</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#06287e">canc</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;GET&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;relay?state=1&#34;</span>);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">zleep</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>ctx<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">Background</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>now<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>time.<span style="color:#06287e">Now</span>().<span style="color:#06287e">Truncate</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wakeup<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">nextWakeup</span>(now)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;now   : %v&#34;</span>,<span style="color:#bbb"> </span>now)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;wakeup: %v&#34;</span>,<span style="color:#bbb"> </span>wakeup)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;wakeup: %v (timestamp)&#34;</span>,<span style="color:#bbb"> </span>wakeup.<span style="color:#06287e">Unix</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// assumes hwclock is running in UTC (see timedatectl | grep local)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Power the monitor off in 15 seconds.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// mode=on is intentional: https://api.mystrom.ch/#e532f952-36ea-40fb-a180-a57b835f550e</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will be turned on (already on, so this is a no-op)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will wait for 15 seconds</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will be turned off</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;POST&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;timer?mode=on&amp;time=15&#34;</span>);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>exec.<span style="color:#06287e">Command</span>(<span style="color:#4070a0">&#34;sh&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;-c&#34;</span>,<span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Sprintf</span>(<span style="color:#4070a0">&#34;sudo rtcwake -m no --verbose --utc -t %v &amp;&amp; sudo systemctl suspend&#34;</span>,<span style="color:#bbb"> </span>wakeup.<span style="color:#06287e">Unix</span>()))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep.Stdout<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stdout<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep.Stderr<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stderr<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>fmt.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;running %v\n&#34;</span>,<span style="color:#bbb"> </span>sleep.Args)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>sleep.<span style="color:#06287e">Run</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Errorf</span>(<span style="color:#4070a0">&#34;%v: %v&#34;</span>,<span style="color:#bbb"> </span>sleep.Args,<span style="color:#bbb"> </span>err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>flag.<span style="color:#06287e">Parse</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span><span style="color:#666">*</span>resume<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">runResume</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">else</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">zsleep</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>To turn power to the monitor on after resuming, I placed the following shell
script in <code>/lib/systemd/system-sleep/zleep.sh</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#007020">#!/bin/sh
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">case</span> <span style="color:#4070a0">&#34;</span><span style="color:#bb60d5">$1</span><span style="color:#4070a0">&#34;</span> in
</span></span><span style="display:flex;"><span>	pre<span style="color:#666">)</span>	<span style="color:#007020">exit</span> <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span>	post<span style="color:#666">)</span>	/usr/local/bin/zleep -resume
</span></span><span style="display:flex;"><span>		<span style="color:#007020">exit</span> <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span> 	*<span style="color:#666">)</span>	<span style="color:#007020">exit</span> <span style="color:#40a070">1</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">esac</span>
</span></span></code></pre></div><p>Once power is on, grobi will detect and configure the monitor.</p>
<p>Here is the program in action:</p>
<pre tabindex="0"><code>2025/05/06 21:58:32 now   : 2025-05-06 21:58:32 +0200 CEST
2025/05/06 21:58:32 wakeup: 2025-05-07 06:50:00 +0200 CEST
2025/05/06 21:58:32 wakeup: 1746593400 (timestamp)
2025/05/06 21:58:32 [monitor power] command: timer?mode=on&amp;time=15
2025/05/06 21:58:32 [monitor power] request succeeded
running [sh -c sudo rtcwake -m no --verbose --utc -t 1746593400 &amp;&amp; sudo systemctl suspend]
Using UTC time.
	delta   = 0
	tzone   = 0
	tzname  = UTC
	systime = 1746561512, (UTC) Tue May  6 19:58:32 2025
	rtctime = 1746561512, (UTC) Tue May  6 19:58:32 2025
alarm 1746593400, sys_time 1746561512, rtc_time 1746561512, seconds 0
rtcwake: wakeup using /dev/rtc0 at Wed May  7 04:50:00 2025
suspend mode: no; leaving
</code></pre>]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Tips to debug hanging Go programs]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/"/>
    <id>https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/</id>
    <published>2025-02-27T17:51:38+01:00</published>
    <content type="html"><![CDATA[<p>I was helping someone get my <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a>
implementation set up to synchronize <a href="https://en.wikipedia.org/wiki/Resource_Public_Key_Infrastructure">RPKI
data</a> (used
for securing BGP routing infrastructure), when we discovered that with the right
invocation, my rsync receiver would just hang indefinitely.</p>
<p>This was a quick problem to solve, but in the process, I realized that I should
probably write down a few Go debugging tips I have come to appreciate over the
years!</p>
<h2 id="scenario-hanging-go-program">Scenario: hanging Go program</h2>
<p>If you want to follow along, you can reproduce the issue by building an older
version of gokrazy/rsync, just before the bug fix commit (you’ll need <a href="https://go.dev/dl/">Go 1.22
or newer</a>):</p>
<pre tabindex="0"><code>git clone https://github.com/gokrazy/rsync
cd rsync
git reset --hard 6c89d4dda3be055f19684c0ed56d623da458194e^
go install ./cmd/...
</code></pre><p>Now we can try to sync the repository:</p>
<pre tabindex="0"><code>% gokr-rsync \
  -rtO \
  --delete \
  rsync://rsync.paas.rpki.ripe.net/repository/ \
  /tmp/rpki-repo
[…]
2025/02/08 09:35:10 Opening TCP connection to rsync.paas.rpki.ripe.net:873
2025/02/08 09:35:10 rsync module &#34;repo&#34;, path &#34;repo/&#34;
2025/02/08 09:35:10 (Client) Protocol versions: remote=31, negotiated=27
2025/02/08 09:35:10 Client checksum: md4
2025/02/08 09:35:10 sending daemon args: [--server --sender -tr . repo/]
2025/02/08 09:35:10 exclusion list sent
2025/02/08 09:35:10 receiving file list
2025/02/08 09:35:11 [Receiver] i=0 ? . mode=40755 len=4096 uid=0 gid=0 flags=?
[…]
2025/02/08 09:35:11 [Receiver] i=89 ? clonoth/1/3139332e33322e3130302e302f32342d3234203d3e203537313936.roa mode=100644 len=1747 uid=0 gid=0 flags=?
</code></pre><p>…and then the program just sits there.</p>
<h2 id="sigquit-stack-trace">Tip 1: Press Ctrl+\ (SIGQUIT) to print a stack trace</h2>
<p>The easiest way to look at where a Go program is hanging is to press <code>Ctrl+\</code>
(backslash) to <a href="https://en.wikipedia.org/wiki/Signal_(IPC)#SIGQUIT">make the terminal send it a <code>SIGQUIT</code>
signal</a>. When the Go runtime
receives <code>SIGQUIT</code>, it prints a stack trace to the terminal before exiting the
process. This behavior is enabled by default and can be customized via the
<code>GOTRACEBACK</code> environment variable, see the <a href="https://pkg.go.dev/runtime"><code>runtime</code> package
docs</a>.</p>
<p>Here is what the output looks like in our case. I have made the font small so
that you can recognize the shape of the output (the details are not important,
continue reading below):</p>
<div style="font-size: 60%">
<pre tabindex="0"><code>^\SIGQUIT: quit
PC=0x47664e m=0 sigcode=128

goroutine 0 gp=0x6e6020 m=0 mp=0x6e6ec0 [idle]:
internal/runtime/syscall.Syscall6()
	/home/michael/sdk/go1.23.0/src/internal/runtime/syscall/asm_linux_amd64.s:36 +0xe fp=0x7ffc58665090 sp=0x7ffc58665088 pc=0x47664e
internal/runtime/syscall.EpollWait(0x586651e0?, {0x7ffc5866511c?, 0x3000000018?, 0x7ffc586651f0?}, 0x58665110?, 0x7ffc?)
	/home/michael/sdk/go1.23.0/src/internal/runtime/syscall/syscall_linux.go:32 +0x45 fp=0x7ffc586650e0 sp=0x7ffc58665090 pc=0x4765e5
runtime.netpoll(0xc0000000c0?)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll_epoll.go:116 +0xd2 fp=0x7ffc58665768 sp=0x7ffc586650e0 pc=0x432332
runtime.findRunnable()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:3580 +0x8c5 fp=0x7ffc586658e0 sp=0x7ffc58665768 pc=0x43f045
runtime.schedule()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:3995 +0xb1 fp=0x7ffc58665918 sp=0x7ffc586658e0 pc=0x4405b1
runtime.park_m(0xc0000061c0)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:4102 +0x1eb fp=0x7ffc58665970 sp=0x7ffc58665918 pc=0x4409cb
runtime.mcall()
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:459 +0x4e fp=0x7ffc58665988 sp=0x7ffc58665970 pc=0x470e2e

goroutine 1 gp=0xc0000061c0 m=nil [IO wait]:
runtime.gopark(0x452658?, 0x0?, 0x98?, 0xb3?, 0xb?)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:424 +0xce fp=0xc0000eb358 sp=0xc0000eb338 pc=0x46bc0e
runtime.netpollblock(0x4a01b8?, 0x4058e6?, 0x0?)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll.go:575 +0xf7 fp=0xc0000eb390 sp=0xc0000eb358 pc=0x4318f7
internal/poll.runtime_pollWait(0x7ef586628808, 0x72)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll.go:351 +0x85 fp=0xc0000eb3b0 sp=0xc0000eb390 pc=0x46af05
internal/poll.(*pollDesc).wait(0xc0000ce180?, 0xc00020e99c?, 0x0)
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_poll_runtime.go:84 +0x27 fp=0xc0000eb3d8 sp=0xc0000eb3b0 pc=0x4b0ce7
internal/poll.(*pollDesc).waitRead(...)
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Read(0xc0000ce180, {0xc00020e99c, 0x4, 0x4})
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_unix.go:165 +0x27a fp=0xc0000eb470 sp=0xc0000eb3d8 pc=0x4b17da
net.(*netFD).Read(0xc0000ce180, {0xc00020e99c?, 0x6eeea0?, 0x1?})
	/home/michael/sdk/go1.23.0/src/net/fd_posix.go:55 +0x25 fp=0xc0000eb4b8 sp=0xc0000eb470 pc=0x4f7e85
net.(*conn).Read(0xc000206000, {0xc00020e99c?, 0xc000212000?, 0x6e6ec0?})
	/home/michael/sdk/go1.23.0/src/net/net.go:189 +0x45 fp=0xc0000eb500 sp=0xc0000eb4b8 pc=0x5001a5
net.(*TCPConn).Read(0x0?, {0xc00020e99c?, 0xc0000eb568?, 0x46d449?})
	&lt;autogenerated&gt;:1 +0x25 fp=0xc0000eb530 sp=0xc0000eb500 pc=0x50bb25
io.ReadAtLeast({0x5d9640, 0xc000206000}, {0xc00020e99c, 0x4, 0x4}, 0x4)
	/home/michael/sdk/go1.23.0/src/io/io.go:335 +0x90 fp=0xc0000eb578 sp=0xc0000eb530 pc=0x4957d0
io.ReadFull(...)
	/home/michael/sdk/go1.23.0/src/io/io.go:354
encoding/binary.Read({0x5d9640, 0xc000206000}, {0x5da8b0, 0x7059a0}, {0x55e7c0, 0xc0000eb6a0})
	/home/michael/sdk/go1.23.0/src/encoding/binary/binary.go:244 +0xa5 fp=0xc0000eb670 sp=0xc0000eb578 pc=0x5102a5
github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).ReadMsg(0xc00020a100)
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:50 +0x48 fp=0xc0000eb6e8 sp=0xc0000eb670 pc=0x514428
github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).Read(0x7ef5869b9a68?, {0xc000280000, 0x40000, 0x4dd4fb?})
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:72 +0x2f fp=0xc0000eb788 sp=0xc0000eb6e8 pc=0x5145af
bufio.(*Reader).Read(0xc0002020c0, {0xc00020e998, 0x4, 0x40ece5?})
	/home/michael/sdk/go1.23.0/src/bufio/bufio.go:241 +0x197 fp=0xc0000eb7c0 sp=0xc0000eb788 pc=0x4d5a57
io.ReadAtLeast({0x5d93e0, 0xc0002020c0}, {0xc00020e998, 0x4, 0x4}, 0x4)
	/home/michael/sdk/go1.23.0/src/io/io.go:335 +0x90 fp=0xc0000eb808 sp=0xc0000eb7c0 pc=0x4957d0
io.ReadFull(...)
	/home/michael/sdk/go1.23.0/src/io/io.go:354
github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32(0xc000208060)
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:163 +0x4a fp=0xc0000eb850 sp=0xc0000eb808 pc=0x51490a
github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1(0xc000202120, 0x5a9b58)
	/home/michael/kr/rsync/internal/receiver/uidlist.go:16 +0x3d fp=0xc0000eb8c0 sp=0xc0000eb850 pc=0x51fc7d
github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList(0xc000202120)
	/home/michael/kr/rsync/internal/receiver/uidlist.go:52 +0x1dd fp=0xc0000eba08 sp=0xc0000eb8c0 pc=0x51ffbd
github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList(0xc000202120)
	/home/michael/kr/rsync/internal/receiver/flist.go:229 +0x378 fp=0xc0000ebb10 sp=0xc0000eba08 pc=0x51c5b8
github.com/gokrazy/rsync/internal/receivermaincmd.clientRun({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0x7ef53d47efc8, 0xc000206000}, {0x7ffc5866600e, ...}, ...)
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341 +0x5cd fp=0xc0000ebc10 sp=0xc0000ebb10 pc=0x550c2d
github.com/gokrazy/rsync/internal/receivermaincmd.socketClient({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0x7ffc58665ff4?, 0x1?}, {0x7ffc5866600e, ...})
	/home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44 +0x425 fp=0xc0000ebcd0 sp=0xc0000ebc10 pc=0x54c205
github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0xc00007e440, 0x1, 0x2}, ...)
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160 +0x5d7 fp=0xc0000ebdf0 sp=0xc0000ebcd0 pc=0x54f697
github.com/gokrazy/rsync/internal/receivermaincmd.Main({0xc0000160a0, 0x5, 0x5}, {0x5d9280?, 0xc000078058?}, {0x5d92a0?, 0xc000078060?}, {0x5d92a0?, 0xc000078068?})
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394 +0x272 fp=0xc0000ebee8 sp=0xc0000ebdf0 pc=0x5510d2
main.main()
	/home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12 +0x4e fp=0xc0000ebf50 sp=0xc0000ebee8 pc=0x5515ae
runtime.main()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:272 +0x28b fp=0xc0000ebfe0 sp=0xc0000ebf50 pc=0x438d4b
runtime.goexit({})
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000ebfe8 sp=0xc0000ebfe0 pc=0x472e61

goroutine 2 gp=0xc000006c40 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:424 +0xce fp=0xc000074fa8 sp=0xc000074f88 pc=0x46bc0e
runtime.goparkunlock(...)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:430
runtime.forcegchelper()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:337 +0xb3 fp=0xc000074fe0 sp=0xc000074fa8 pc=0x439093
runtime.goexit({})
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000074fe8 sp=0xc000074fe0 pc=0x472e61
created by runtime.init.7 in goroutine 1
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:325 +0x1a
</code></pre></div>
<p>Phew! This output is pretty dense.</p>
<p>We can use the <a href="https://github.com/maruel/panicparse">https://github.com/maruel/panicparse</a> program to present this
stack trace in a more colorful and much shorter version:</p>















<a href="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_4c5f7b3d7df4ad22.jpg 2x,https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_ad1e8c2d9905e0.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_a426e16585061786.jpg"
  
  width="600"
  height="394"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>The functions helpfully highlighted in red are where the problem lies: My rsync
receiver implementation was incorrectly expecting the server to send a uid/gid
list, despite the PreserveUid and PreserveGid options not being enabled. <a href="https://github.com/gokrazy/rsync/commit/6c89d4dda3be055f19684c0ed56d623da458194e">Commit
<code>6c89d4d</code></a>
fixes the issue.</p>
<h2 id="attach-dlv">Tip 2: Attach the delve debugger to the process</h2>
<p>If dumping the stack trace in the moment is not sufficient to diagnose the
problem, you can go one step further and reach for an interactive debugger.</p>
<p>The most well-known Linux debugger is probably GDB, but when working with Go, I
recommend using <a href="https://github.com/go-delve/delve">the delve debugger</a> instead
as it typically works better. Install delve if you haven’t already:</p>
<pre tabindex="0"><code>% go install github.com/go-delve/delve/cmd/dlv@latest
</code></pre><p>In this article, I am using delve v1.24.0.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><p><strong>Note:</strong> If you want to explore local variables, you should rebuild your
program without optimizations and inlining (see the <a href="https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv_exec.md"><code>dlv exec</code>
docs</a>):</p>
<pre tabindex="0"><code>% go install -gcflags=all=&#34;-N -l&#34; ./cmd/...
</code></pre></div>
  </div>
</aside>

<p>While you can run a new child process in a debugger (use <code>dlv exec</code>) without any
special permissions, attaching existing processes in a debugger is <a href="https://www.kernel.org/doc/Documentation/security/Yama.txt">disabled by
default in Linux</a>
for security reasons. We can allow this feature (remember to turn it off later!)
using:</p>
<pre tabindex="0"><code>% sudo sysctl -w kernel.yama.ptrace_scope=0
kernel.yama.ptrace_scope = 0
</code></pre><p>…and then we can just <code>dlv attach</code> to the hanging <code>gokr-rsync</code> process:</p>
<pre tabindex="0"><code>% dlv attach $(pidof gokr-rsync)
Type &#39;help&#39; for list of commands.
(dlv)
</code></pre><p>Great. But if we just print a stack trace, we only see functions from the
<code>runtime</code> package:</p>
<pre tabindex="0"><code>(dlv) bt
0  0x000000000047bb83 in runtime.futex
   at /home/michael/sdk/go1.23.6/src/runtime/sys_linux_amd64.s:558
1  0x00000000004374d0 in runtime.futexsleep
   at /home/michael/sdk/go1.23.6/src/runtime/os_linux.go:69
2  0x000000000040d89d in runtime.notesleep
   at /home/michael/sdk/go1.23.6/src/runtime/lock_futex.go:170
3  0x000000000044123e in runtime.mPark
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:1866
4  0x000000000044290d in runtime.stopm
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:2886
5  0x00000000004433d0 in runtime.findRunnable
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:3623
6  0x0000000000444e1d in runtime.schedule
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:3996
7  0x00000000004451cb in runtime.park_m
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:4103
8  0x0000000000477eee in runtime.mcall
   at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:459
</code></pre><p>The reason is that no goroutine is running (the program is waiting indefinitely
to receive data from the server), so we see one of the OS threads waiting in the
Go scheduler.</p>
<p>We first need to switch to the goroutine we are interested in (<code>grs</code> prints all
goroutines), and then the stack trace looks like what we expect:</p>
<pre tabindex="0"><code>(dlv) gr 1
Switched from 0 to 1 (thread 414327)
(dlv) bt
 0  0x0000000000474ebc in runtime.gopark
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:425
 1  0x000000000043819e in runtime.netpollblock
    at /home/michael/sdk/go1.23.6/src/runtime/netpoll.go:575
 2  0x000000000047435c in internal/poll.runtime_pollWait
    at /home/michael/sdk/go1.23.6/src/runtime/netpoll.go:351
 3  0x00000000004ed15a in internal/poll.(*pollDesc).wait
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_poll_runtime.go:84
 4  0x00000000004ed1f1 in internal/poll.(*pollDesc).waitRead
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_poll_runtime.go:89
 5  0x00000000004ee351 in internal/poll.(*FD).Read
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_unix.go:165
 6  0x0000000000569bb3 in net.(*netFD).Read
    at /home/michael/sdk/go1.23.6/src/net/fd_posix.go:55
 7  0x000000000057a025 in net.(*conn).Read
    at /home/michael/sdk/go1.23.6/src/net/net.go:189
 8  0x000000000058fcc5 in net.(*TCPConn).Read
    at &lt;autogenerated&gt;:1
 9  0x00000000004b72e8 in io.ReadAtLeast
    at /home/michael/sdk/go1.23.6/src/io/io.go:335
10  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
11  0x0000000000598d5f in encoding/binary.Read
    at /home/michael/sdk/go1.23.6/src/encoding/binary/binary.go:244
12  0x00000000005a0b7a in github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).ReadMsg
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:50
13  0x00000000005a0f17 in github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).Read
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:72
14  0x0000000000528de8 in bufio.(*Reader).Read
    at /home/michael/sdk/go1.23.6/src/bufio/bufio.go:241
15  0x00000000004b72e8 in io.ReadAtLeast
    at /home/michael/sdk/go1.23.6/src/io/io.go:335
16  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
17  0x00000000005a19ef in github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:163
18  0x00000000005b77d2 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:16
19  0x00000000005b7ea8 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:52
20  0x00000000005b18db in github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList
    at /home/michael/kr/rsync/internal/receiver/flist.go:229
21  0x0000000000605390 in github.com/gokrazy/rsync/internal/receivermaincmd.clientRun
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341
22  0x00000000005fe572 in github.com/gokrazy/rsync/internal/receivermaincmd.socketClient
    at /home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44
23  0x0000000000602f10 in github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160
24  0x0000000000605e7e in github.com/gokrazy/rsync/internal/receivermaincmd.Main
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394
25  0x0000000000606653 in main.main
    at /home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12
26  0x000000000043fa47 in runtime.main
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:272
27  0x000000000047bd01 in runtime.goexit
    at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:1700
</code></pre><h2 id="save-core-dump">Tip 3: Save a core dump for later</h2>
<p>If you don’t have time to poke around in the debugger now, you can save a core
dump for later.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Tip:</strong> Check out my <a href="/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/">debugging Go core dumps with
delve</a> blog post from
2024 for more details! This section just explains how to collect core dumps.</div>
  </div>
</aside>

<p>In addition to printing the stack trace on <code>SIGQUIT</code>, we can make the Go runtime
crash the program, which in turn makes the Linux kernel write a core dump, by
running our program with the environment variable
<a href="https://pkg.go.dev/runtime"><code>GOTRACEBACK=crash</code></a>.</p>
<p>Modern Linux systems typically include <a href="https://manpages.debian.org/systemd-coredump.8"><code>systemd-coredump(8)</code></a>
 (but you might need to explicitly install it, for example on
Ubuntu) to collect core dumps (and remove old ones). You can use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to list and work with them. On macOS,
<a href="https://developer.apple.com/forums/thread/694233#695943022">collecting cores is more
involved</a>. I don’t
know about Windows.</p>
<p>In case your Linux system does not use <code>systemd-coredump</code>, you can use <code>ulimit -c unlimited</code> and set the kernel’s <code>kernel.core_pattern</code> sysctl setting. You can
find more details and options in the <a href="https://go.dev/wiki/CoreDumpDebugging">CoreDumpDebugging page of the Go
wiki</a>. For this article, we will stick to
<code>coredumpctl</code>:</p>
<pre tabindex="0"><code>% GOTRACEBACK=crash gokr-rsync -rtO --delete rsync://rsync.paas.rpki.ripe.net/repo/ /tmp/rpki-repo
[…]
^\SIGQUIT: quit
[…]
zsh: IOT instruction (core dumped)  GOTRACEBACK=crash gokr-rsync -rtO […]
</code></pre><p>The last line is what we want to see: it should say “core dumped”.</p>
<p>This core should now show up in <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
:</p>
<pre tabindex="0"><code>% coredumpctl info
           PID: 414607 (gokr-rsync)
           UID: 1000 (michael)
           GID: 1000 (michael)
        Signal: 6 (ABRT)
     Timestamp: Sat 2025-02-08 10:18:27 CET (12s ago)
  Command Line: gokr-rsync -rtO --delete rsync://rsync.paas.rpki.ripe.net/repo/ /tmp/rpki-repo
    Executable: /bin/gokr-rsync
 Control Group: /user.slice/user-1000.slice/session-1.scope
          Unit: session-1.scope
         Slice: user-1000.slice
       Session: 1
     Owner UID: 1000 (michael)
       Boot ID: 6158dd3b52af4b8384c103a8a336fc02
    Machine ID: ecb5a44f1a5846ad871566e113bf8937
      Hostname: midna
       Storage: /var/lib/systemd/coredump/core.gokr-rsync.1000.6158dd3b52af4b8384c103a8a336fc02.414607.1739006307000000.zst (present)
  Size on Disk: 158.3K
       Message: Process 414607 (gokr-rsync) of user 1000 dumped core.
                
    Module [dso] without build-id.
    Module [dso]
    Stack trace of thread 1604447:
    #0  0x0000000000475a41 runtime.raise.abi0 (/bin/gokr-rsync + 0x75a41)
    #1  0x0000000000451d85 runtime.dieFromSignal (/bin/gokr-rsync + 0x51d85)
    #2  0x00000000004522e6 runtime.sigfwdgo (/bin/gokr-rsync + 0x522e6)
    #3  0x0000000000450c45 runtime.sigtrampgo (/bin/gokr-rsync + 0x50c45)
    #4  0x0000000000475d26 runtime.sigtramp.abi0 (/bin/gokr-rsync + 0x75d26)
    #5  0x0000000000475e20 n/a (/bin/gokr-rsync + 0x75e20)
    ELF object binary architecture: AMD x86-64
</code></pre><p>If you see only hexadecimal addresses followed by <code>n/a (n/a + 0x0)</code>, that means
<code>systemd-coredump</code> could not symbolize (= resolve addresses to function names)
your core dump. Here are a few possible reasons for missing symbolization:</p>
<ul>
<li>Linux 6.12 and 6.13 <a href="https://sourceware.org/bugzilla/show_bug.cgi?id=32713">produced core dumps that elfutils cannot
symbolize</a>. <code>systemd-coredump</code>
uses elfutils for symbolization, so avoid 6.12/6.13 in favor of using 6.14 or
newer.</li>
<li>With systemd v234-v256, <code>systemd-coredump</code> did not have permission to look
into programs living in the <code>/home</code> directory (fixed with <a href="https://github.com/systemd/systemd/commit/4ac1755be2d6c141fae7e57c42936e507c5b54e3">commit
<code>4ac1755</code></a>
in systemd v257+).
<ul>
<li>Similarly, <code>systemd-coredump</code> runs with
<a href="http://manpages.debian.org/systemd.exec"><code>PrivateTmp=yes</code></a>, meaning it
won’t be able to access programs you place in <code>/tmp</code>.</li>
</ul>
</li>
<li>Go builds with debug symbols by default, but maybe you are explicitly
stripping debug symbols in your build, by building with <code>-ldflags=-w</code>?</li>
</ul>
<p>We can now use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to launch delve for
this program + core dump:</p>
<pre tabindex="0"><code>% coredumpctl debug --debugger=dlv --debugger-arguments=core
[…]
Type &#39;help&#39; for list of commands.
(dlv) gr 1
Switched from 0 to 1 (thread 414607)
(dlv) bt
[…]
16  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
17  0x00000000005a19ef in github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:163
18  0x00000000005b77d2 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:16
19  0x00000000005b7ea8 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:52
20  0x00000000005b18db in github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList
    at /home/michael/kr/rsync/internal/receiver/flist.go:229
21  0x0000000000605390 in github.com/gokrazy/rsync/internal/receivermaincmd.clientRun
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341
22  0x00000000005fe572 in github.com/gokrazy/rsync/internal/receivermaincmd.socketClient
    at /home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44
23  0x0000000000602f10 in github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160
24  0x0000000000605e7e in github.com/gokrazy/rsync/internal/receivermaincmd.Main
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394
25  0x0000000000606653 in main.main
    at /home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12
26  0x000000000043fa47 in runtime.main
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:272
27  0x000000000047bd01 in runtime.goexit
    at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:1700
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>In my experience, in the medium to long term, it always pays off to set up your
environment such that you can debug your programs conveniently. I strongly
encourage every programmer (and even users!) to invest time into your
development and debugging setup.</p>
<p>Luckily, Go comes with stack printing functionality by default (just press
<code>Ctrl+\</code>) and we can easily get a core dump out of our Go programs by running
them with <code>GOTRACEBACK=crash</code> — provided the system is set up to collect core
dumps.</p>
<p>Together with the delve debugger, this gives us all we need to effectively and
efficiently diagnose problems in Go programs.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Go Protobuf: The new Opaque API]]></title>
    <link href="https://michael.stapelberg.ch/posts/2024-12-21-go-protobuf-opaque/"/>
    <id>https://michael.stapelberg.ch/posts/2024-12-21-go-protobuf-opaque/</id>
    <published>2024-12-21T11:06:00+01:00</published>
    <content type="html"><![CDATA[<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content">I originally published this post in <a href="https://go.dev/blog">the Go blog</a>, but am
publishing this copy of it in my own blog as well for readers who don’t follow
the Go blog.</div>
  </div>
</aside>

<p>[<a href="https://en.wikipedia.org/wiki/Protocol_Buffers">Protocol Buffers (Protobuf)</a>
is Google&rsquo;s language-neutral data interchange format. See
<a href="https://protobuf.dev/">protobuf.dev</a>.]</p>
<p>Back in March 2020, we released <a href="https://go.dev/blog/protobuf-apiv2">a major overhaul of the Go Protobuf
API</a>. The <code>google.golang.org/protobuf</code>
package introduced first-class <a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect">support for
reflection</a>,
a <a href="https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb"><code>dynamicpb</code></a>
implementation and the
<a href="https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp"><code>protocmp</code></a>
package for easier testing.</p>
<p>That release introduced a new protobuf module with a new API. Today, we are
releasing an additional API for generated code, meaning the Go code in the
<code>.pb.go</code> files created by the protocol compiler (<code>protoc</code>). This blog post
explains our motivation for creating a new API and shows you how to use it in
your projects.</p>
<p>To be clear: We are not removing anything. We will continue to support the
existing API for generated code, just like we still support the older protobuf
module (by wrapping the <code>google.golang.org/protobuf</code> implementation). Go is
<a href="https://go.dev/blog/compat">committed to backwards compatibility</a> and this
applies to Go Protobuf, too!</p>
<h2 id="background">Background: the (existing) Open Struct API</h2>
<p>We now call the existing API the Open Struct API, because generated struct types
are open to direct access. In the next section, we will see how it differs from
the new Opaque API.</p>
<p>To work with protocol buffers, you first create a <code>.proto</code> definition file like
this one:</p>
<pre><code>edition = &quot;2023&quot;;  // successor to proto2 and proto3

package log;

message LogEntry {
  string backend_server = 1;
  uint32 request_size = 2;
  string ip_address = 3;
}
</code></pre>
<p>Then, you <a href="https://protobuf.dev/getting-started/gotutorial/">run the protocol compiler
(<code>protoc</code>)</a> to generate code
like the following (in a <code>.pb.go</code> file):</p>
<pre><code>package logpb

type LogEntry struct {
  BackendServer *string
  RequestSize   *uint32
  IPAddress     *string
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) GetRequestSize() uint32   { … }
func (l *LogEntry) GetIPAddress() string     { … }
</code></pre>
<p>Now you can import the generated <code>logpb</code> package from your Go code and call
functions like
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Marshal"><code>proto.Marshal</code></a>
to encode <code>logpb.LogEntry</code> messages into protobuf wire format.</p>
<p>You can find more details in the <a href="https://protobuf.dev/reference/go/go-generated/">Generated Code API
documentation</a>.</p>
<h3 id="presence">(Existing) Open Struct API: Field Presence</h3>
<p>An important aspect of this generated code is how <em>field presence</em> (whether a
field is set or not) is modeled. For instance, the above example models presence
using pointers, so you could set the <code>BackendServer</code> field to:</p>
<ol>
<li><code>proto.String(&quot;zrh01.prod&quot;)</code>: the field is set and contains &ldquo;zrh01.prod&rdquo;</li>
<li><code>proto.String(&quot;&quot;)</code>: the field is set (non-<code>nil</code> pointer) but contains an
empty value</li>
<li><code>nil</code> pointer: the field is not set</li>
</ol>
<p>If you are used to generated code not having pointers, you are probably using
<code>.proto</code> files that start with <code>syntax = &quot;proto3&quot;</code>. The field presence behavior
changed over the years:</p>
<ul>
<li><code>syntax = &quot;proto2&quot;</code> uses <em>explicit presence</em> by default</li>
<li><code>syntax = &quot;proto3&quot;</code> used <em>implicit presence</em> by default (where cases 2 and 3
cannot be distinguished and are both represented by an empty string), but was
later extended to allow <a href="https://protobuf.dev/programming-guides/proto3/#field-labels">opting into explicit presence with the <code>optional</code>
keyword</a></li>
<li><code>edition = &quot;2023&quot;</code>, the <a href="https://protobuf.dev/editions/overview/">successor to both proto2 and
proto3</a>, uses <a href="https://protobuf.dev/programming-guides/field_presence/"><em>explicit
presence</em></a> by default</li>
</ul>
<h2 id="opaqueapi">The new Opaque API</h2>
<p>We created the new <em>Opaque API</em> to uncouple the <a href="https://protobuf.dev/reference/go/go-generated/">Generated Code
API</a> from the underlying
in-memory representation. The (existing) Open Struct API has no such separation:
it allows programs direct access to the protobuf message memory. For example,
one could use the <code>flag</code> package to parse command-line flag values into protobuf
message fields:</p>
<pre><code>var req logpb.LogEntry
flag.StringVar(&amp;req.BackendServer, &quot;backend&quot;, os.Getenv(&quot;HOST&quot;), &quot;…&quot;)
flag.Parse() // fills the BackendServer field from -backend flag
</code></pre>
<p>The problem with such a tight coupling is that we can never change how we lay
out protobuf messages in memory. Lifting this restriction enables many
implementation improvements, which we&rsquo;ll see below.</p>
<p>What changes with the new Opaque API? Here is how the generated code from the
above example would change:</p>
<pre><code>package logpb

type LogEntry struct {
  xxx_hidden_BackendServer *string // no longer exported
  xxx_hidden_RequestSize   uint32  // no longer exported
  xxx_hidden_IPAddress     *string // no longer exported
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) HasBackendServer() bool   { … }
func (l *LogEntry) SetBackendServer(string)  { … }
func (l *LogEntry) ClearBackendServer()      { … }
// …
</code></pre>
<p>With the Opaque API, the struct fields are hidden and can no longer be
directly accessed. Instead, the new accessor methods allow for getting, setting,
or clearing a field.</p>
<h3 id="lessmemory">Opaque structs use less memory</h3>
<p>One change we made to the memory layout is to model field presence for
elementary fields more efficiently:</p>
<ul>
<li>The (existing) Open Struct API uses pointers, which adds a 64-bit word to the
space cost of the field.</li>
<li>The Opaque API uses <a href="https://en.wikipedia.org/wiki/Bit_field">bit
fields</a>, which require one bit per
field (ignoring padding overhead).</li>
</ul>
<p>Using fewer variables and pointers also lowers load on the allocator and on the
garbage collector.</p>
<p>The performance improvement depends heavily on the shapes of your protocol
messages: The change only affects elementary fields like integers, bools, enums,
and floats, but not strings, repeated fields, or submessages (because it is
<a href="https://protobuf.dev/reference/go/opaque-faq/#memorylayout">less
profitable</a>
for those types).</p>
<p>Our benchmark results show that messages with few elementary fields exhibit
performance that is as good as before, whereas messages with more elementary
fields are decoded with significantly fewer allocations:</p>
<pre><code>             │ Open Struct API │             Opaque API             │
             │    allocs/op    │  allocs/op   vs base               │
Prod#1          360.3k ± 0%       360.3k ± 0%  +0.00% (p=0.002 n=6)
Search#1       1413.7k ± 0%       762.3k ± 0%  -46.08% (p=0.002 n=6)
Search#2        314.8k ± 0%       132.4k ± 0%  -57.95% (p=0.002 n=6)
</code></pre>
<p>Reducing allocations also makes decoding protobuf messages more efficient:</p>
<pre><code>             │ Open Struct API │             Opaque API            │
             │   user-sec/op   │ user-sec/op  vs base              │
Prod#1         55.55m ± 6%        55.28m ± 4%  ~ (p=0.180 n=6)
Search#1       324.3m ± 22%       292.0m ± 6%  -9.97% (p=0.015 n=6)
Search#2       67.53m ± 10%       45.04m ± 8%  -33.29% (p=0.002 n=6)
</code></pre>
<p>(All measurements done on an AMD Castle Peak Zen 2. Results on ARM and Intel
CPUs are similar.)</p>
<p>Note: proto3 with implicit presence similarly does not use pointers, so you will
not see a performance improvement if you are coming from proto3. If you were
using implicit presence for performance reasons, forgoing the convenience of
being able to distinguish empty fields from unset ones, then the Opaque API now
makes it possible to use explicit presence without a performance penalty.</p>
<h3 id="lazydecoding">Motivation: Lazy Decoding</h3>
<p>Lazy decoding is a performance optimization where the contents of a submessage
are decoded when first accessed instead of during
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Unmarshal"><code>proto.Unmarshal</code></a>. Lazy
decoding can improve performance by avoiding unnecessarily decoding fields which
are never accessed.</p>
<p>Lazy decoding can&rsquo;t be supported safely by the (existing) Open Struct API. While
the Open Struct API provides getters, leaving the (un-decoded) struct fields
exposed would be extremely error-prone. To ensure that the decoding logic runs
immediately before the field is first accessed, we must make the field private
and mediate all accesses to it through getter and setter functions.</p>
<p>This approach made it possible to implement lazy decoding with the Opaque
API. Of course, not every workload will benefit from this optimization, but for
those that do benefit, the results can be spectacular: We have seen logs
analysis pipelines that discard messages based on a top-level message condition
(e.g. whether <code>backend_server</code> is one of the machines running a new Linux kernel
version) and can skip decoding deeply nested subtrees of messages.</p>
<p>As an example, here are the results of the micro-benchmark we included,
demonstrating how lazy decoding saves over 50% of the work and over 87% of
allocations!</p>
<pre><code>                  │   nolazy    │                lazy                │
                  │   sec/op    │   sec/op     vs base               │
Unmarshal/lazy-24   6.742µ ± 0%   2.816µ ± 0%  -58.23% (p=0.002 n=6)

                  │    nolazy    │                lazy                 │
                  │     B/op     │     B/op      vs base               │
Unmarshal/lazy-24   3.666Ki ± 0%   1.814Ki ± 0%  -50.51% (p=0.002 n=6)

                  │   nolazy    │               lazy                │
                  │  allocs/op  │ allocs/op   vs base               │
Unmarshal/lazy-24   64.000 ± 0%   8.000 ± 0%  -87.50% (p=0.002 n=6)
</code></pre>
<h3 id="pointercomparison">Motivation: reduce pointer comparison mistakes</h3>
<p>Modeling field presence with pointers invites pointer-related bugs.</p>
<p>Consider an enum, declared within the <code>LogEntry</code> message:</p>
<pre><code>message LogEntry {
  enum DeviceType {
    DESKTOP = 0;
    MOBILE = 1;
    VR = 2;
  };
  DeviceType device_type = 1;
}
</code></pre>
<p>A simple mistake is to compare the <code>device_type</code> enum field like so:</p>
<pre><code>if cv.DeviceType == logpb.LogEntry_DESKTOP.Enum() { // incorrect!
</code></pre>
<p>Did you spot the bug? The condition compares the memory address instead of the
value. Because the <code>Enum()</code> accessor allocates a new variable on each call, the
condition can never be true. The check should have read:</p>
<pre><code>if cv.GetDeviceType() == logpb.LogEntry_DESKTOP {
</code></pre>
<p>The new Opaque API prevents this mistake: Because fields are hidden, all access
must go through the getter.</p>
<h3 id="accidentalsharing">Motivation: reduce accidental sharing mistakes</h3>
<p>Let&rsquo;s consider a slightly more involved pointer-related bug. Assume you are
trying to stabilize an RPC service that fails under high load. The following
part of the request middleware looks correct, but still the entire service goes
down whenever just one customer sends a high volume of requests:</p>
<pre><code>logEntry.IPAddress = req.IPAddress
logEntry.BackendServer = proto.String(hostname)
// The redactIP() function redacts IPAddress to 127.0.0.1,
// unexpectedly not just in logEntry *but also* in req!
go auditlog(redactIP(logEntry))
if quotaExceeded(req) {
	// BUG: All requests end up here, regardless of their source.
	return fmt.Errorf(&quot;server overloaded&quot;)
}
</code></pre>
<p>Did you spot the bug? The first line accidentally copied the pointer (thereby
sharing the pointed-to variable between the <code>logEntry</code> and <code>req</code> messages)
instead of its value. It should have read:</p>
<pre><code>logEntry.IPAddress = proto.String(req.GetIPAddress())
</code></pre>
<p>The new Opaque API prevents this problem as the setter takes a value
(<code>string</code>) instead of a pointer:</p>
<pre><code>logEntry.SetIPAddress(req.GetIPAddress())
</code></pre>
<h3 id="reflection">Motivation: Fix Sharp Edges: reflection</h3>
<p>To write code that works not only with a specific message type
(e.g. <code>logpb.LogEntry</code>), but with any message type, one needs some kind of
reflection. The previous example used a function to redact IP addresses. To work
with any type of message, it could have been defined as <code>func redactIP(proto.Message) proto.Message { … }</code>.</p>
<p>Many years ago, your only option to implement a function like <code>redactIP</code> was to
reach for <a href="https://go.dev/blog/laws-of-reflection">Go&rsquo;s <code>reflect</code> package</a>,
which resulted in very tight coupling: you had only the generator output and had
to reverse-engineer what the input protobuf message definition might have looked
like. The <a href="https://go.dev/blog/protobuf-apiv2"><code>google.golang.org/protobuf</code> module
release</a> (from March 2020) introduced
<a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect">Protobuf
reflection</a>,
which should always be preferred: Go&rsquo;s <code>reflect</code> package traverses the data
structure&rsquo;s representation, which should be an implementation detail. Protobuf
reflection traverses the logical tree of protocol messages without regard to its
representation.</p>
<p>Unfortunately, merely <em>providing</em> protobuf reflection is not sufficient and
still leaves some sharp edges exposed: In some cases, users might accidentally
use Go reflection instead of protobuf reflection.</p>
<p>For example, encoding a protobuf message with the <code>encoding/json</code> package (which
uses Go reflection) was technically possible, but the result is not <a href="https://protobuf.dev/programming-guides/proto3/#json">canonical
Protobuf JSON
encoding</a>. Use the
<a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson"><code>protojson</code></a>
package instead.</p>
<p>The new Opaque API prevents this problem because the message struct fields are
hidden: accidental usage of Go reflection will see an empty message. This is
clear enough to steer developers towards protobuf reflection.</p>
<h3 id="idealmemory">Motivation: Making the ideal memory layout possible</h3>
<p>The benchmark results from the <a href="#lessmemory">More Efficient Memory
Representation</a> section have already shown that protobuf
performance heavily depends on the specific usage: How are the messages defined?
Which fields are set?</p>
<p>To keep Go Protobuf as fast as possible for <em>everyone</em>, we cannot implement
optimizations that help only one program, but hurt the performance of other
programs.</p>
<p>The Go compiler used to be in a similar situation, up until <a href="https://go.dev/blog/go1.20">Go 1.20 introduced
Profile-Guided Optimization (PGO)</a>. By recording the
production behavior (through <a href="https://go.dev/blog/pprof">profiling</a>) and feeding
that profile back to the compiler, we allow the compiler to make better
trade-offs <em>for a specific program or workload</em>.</p>
<p>We think using profiles to optimize for specific workloads is a promising
approach for further Go Protobuf optimizations. The Opaque API makes those
possible: Program code uses accessors and does not need to be updated when the
memory representation changes, so we could, for example, move rarely set fields
into an overflow struct.</p>
<h2 id="migration">Migration</h2>
<p>You can migrate on your own schedule, or even not at all—the (existing) Open
Struct API will not be removed. But, if you’re not on the new Opaque API, you
won’t benefit from its improved performance, or future optimizations that target
it.</p>
<p>We recommend you select the Opaque API for new development. Protobuf Edition
2024 (see <a href="https://protobuf.dev/editions/overview/">Protobuf Editions Overview</a>
if you are not yet familiar) will make the Opaque API the default.</p>
<h3 id="hybridapi">The Hybrid API</h3>
<p>Aside from the Open Struct API and Opaque API, there is also the Hybrid API,
which keeps existing code working by keeping struct fields exported, but also
enabling migration to the Opaque API by adding the new accessor methods.</p>
<p>With the Hybrid API, the protobuf compiler will generate code on two API levels:
the <code>.pb.go</code> is on the Hybrid API, whereas the <code>_protoopaque.pb.go</code> version is
on the Opaque API and can be selected by building with the <code>protoopaque</code> build
tag.</p>
<h3 id="rewriting">Rewriting Code to the Opaque API</h3>
<p>See the <a href="https://protobuf.dev/reference/go/opaque-migration/">migration
guide</a>
for detailed instructions. The high-level steps are:</p>
<ol>
<li>Enable the Hybrid API.</li>
<li>Update existing code using the <code>open2opaque</code> migration tool.</li>
<li>Switch to the Opaque API.</li>
</ol>
<h3 id="publishing">Advice for published generated code: Use Hybrid API</h3>
<p>Small usages of protobuf can live entirely within the same repository, but
usually, <code>.proto</code> files are shared between different projects that are owned by
different teams. An obvious example is when different companies are involved: To
call Google APIs (with protobuf), use the <a href="https://github.com/googleapis/google-cloud-go">Google Cloud Client Libraries for
Go</a> from your project. Switching
the Cloud Client Libraries to the Opaque API is not an option, as that would be
a breaking API change, but switching to the Hybrid API is safe.</p>
<p>Our advice for such packages that publish generated code (<code>.pb.go</code> files) is to
switch to the Hybrid API please! Publish both the <code>.pb.go</code> and the
<code>_protoopaque.pb.go</code> files, please. The <code>protoopaque</code> version allows your
consumers to migrate on their own schedule.</p>
<h3 id="enablelazy">Enabling Lazy Decoding</h3>
<p>Lazy decoding is available (but not enabled) once you migrate to the Opaque API!
🎉</p>
<p>To enable: in your <code>.proto</code> file, annotate your message-typed fields with the
<code>[lazy = true]</code> annotation.</p>
<p>To opt out of lazy decoding (despite <code>.proto</code> annotations), the <a href="https://pkg.go.dev/google.golang.org/protobuf/runtime/protolazy"><code>protolazy</code>
package
documentation</a>
describes the available opt-outs, which affect either an individual Unmarshal
operation or the entire program.</p>
<h2 id="nextsteps">Next Steps</h2>
<p>By using the open2opaque tool in an automated fashion over the last few years,
we have converted the vast majority of Google’s <code>.proto</code> files and Go code to
the Opaque API. We continuously improved the Opaque API implementation as we
moved more and more production workloads to it.</p>
<p>Therefore, we expect you should not encounter problems when trying the Opaque
API. In case you do encounter any issues after all, please <a href="https://github.com/golang/protobuf/issues/">let us know on the
Go Protobuf issue tracker</a>.</p>
<p>Reference documentation for Go Protobuf can be found on <a href="https://protobuf.dev/reference/go/">protobuf.dev → Go
Reference</a>.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Testing with Go and PostgreSQL: ephemeral DBs]]></title>
    <link href="https://michael.stapelberg.ch/posts/2024-11-19-testing-with-go-and-postgresql-ephemeral-dbs/"/>
    <id>https://michael.stapelberg.ch/posts/2024-11-19-testing-with-go-and-postgresql-ephemeral-dbs/</id>
    <published>2024-11-19T17:04:00+01:00</published>
    <content type="html"><![CDATA[<p><a href="https://en.wikipedia.org/wiki/PostgreSQL" target="_blank"><img
src="postgresql-elephant-featured.png" align="right" width="125"
style="margin-left: 1.5em" alt="PostgreSQL elephant logo"></a></p>
<p>Let’s say you created a Go program that stores data in PostgreSQL — you
installed PostgreSQL, wrote the Go code, and everything works; great!</p>
<p>But after writing a test for your code, you wonder: how do you best provide
PostgreSQL to your automated tests? Do you start a separate PostgreSQL in a
Docker container, for example, or do you maybe reuse your development PostgreSQL
instance?</p>
<p>I have come to like using <strong>ephemeral PostgreSQL instances</strong> for their many benefits:</p>
<ul>
<li>Easier development setup: no need to <em>configure</em> a database, installation is enough.
<br>
I recommend installing PostgreSQL from your package manager, e.g. <code>apt install postgresql</code> (Debian) or <code>brew install postgresql</code> (macOS). No need for Docker :)</li>
<li>No risk of “works on my machine” (but nowhere else) problems: every test run
starts with an empty database instance, so your test <em>must</em> set up the database
correctly.</li>
<li>The same approach works locally and on CI systems like GitHub Actions.</li>
</ul>
<p>In this article, I want to show how to integrate ephemeral PostgreSQL instances
into your test setup. The examples are all specific to Go, but I expect that
users of other programming languages and environments can benefit from some of
these techniques as well.</p>
<h2 id="single-package-tests">Single-package tests</h2>
<p>When you are in the very early stages of your project, you might start out with
just a single test file (say, <code>app_test.go</code>), containing one or more test
functions (say, <code>TestSignupForm</code>).</p>
<p>In this scenario, all tests will run in the same process. While it’s easy enough
to write a few lines of code to start and stop PostgreSQL, I recommend reaching
for an existing test helper package.</p>
<p>Throughout this article, I will be using the
<a href="https://pkg.go.dev/github.com/stapelberg/postgrestest"><code>github.com/stapelberg/postgrestest</code></a>
package, which is based on <a href="https://pkg.go.dev/zombiezen.com/go/postgrestest">Roxy Light’s <code>postgrestest</code>
package</a> but was extended to
work well in the scenarios this article explains.</p>
<p>To start an ephemeral PostgreSQL instance before your test functions run, you
would <a href="https://pkg.go.dev/testing#hdr-Main">declare a custom <code>TestMain</code>
function</a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>pgt<span style="color:#bbb"> </span><span style="color:#666">*</span>postgrestest.Server<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">TestMain</span>(m<span style="color:#bbb"> </span><span style="color:#666">*</span>testing.M)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>pgt,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>postgrestest.<span style="color:#06287e">Start</span>(context.<span style="color:#06287e">Background</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020">panic</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span>pgt.<span style="color:#06287e">Cleanup</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>m.<span style="color:#06287e">Run</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>Starting a PostgreSQL instance takes about:</p>
<ul>
<li>300ms on my <a href="/posts/2022-01-15-high-end-linux-pc/">Intel Core i9 12900K CPU</a> (from 2022)</li>
<li>800ms on my <a href="/posts/2021-11-28-macbook-air-m1/">MacBook Air M1</a> (from 2020)</li>
</ul>
<p>Then, you can create a separate database for each test on this ephemeral
Postgres instance:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">TestSignupForm</span>(t<span style="color:#bbb"> </span><span style="color:#666">*</span>testing.T)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>pgurl,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>pgt.<span style="color:#06287e">CreateDatabase</span>(context.<span style="color:#06287e">Background</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>t.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// test goes here…</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>Each CreateDatabase call takes about:</p>
<ul>
<li>5-10ms on my <a href="/posts/2022-01-15-high-end-linux-pc/">Intel Core i9 12900K CPU</a> (from 2022)</li>
<li>20ms on my <a href="/posts/2021-11-28-macbook-air-m1/">MacBook Air M1</a> (from 2020)</li>
</ul>
<p>Usually, most projects quickly grow beyond just a single <code>_test.go</code> file.</p>
<p>In one project if mine, I eventually reached over 50 test functions in 25 Go
packages. I stuck to the above approach of adding a custom <code>TestMain</code> to each
package in which my tests needed PostgreSQL, and my test runtimes eventually
looked like this:</p>
<pre tabindex="0"><code># Intel Core i9 12900K
CGO_ENABLED=0 GOGC=off go test -count=1 -fullpath ./...
14,24s user 4,11s system 709% cpu 2,586 total

# MacBook Air M1
CGO_ENABLED=0 GOGC=off go test -count=1 -fullpath ./...
20,23s user 8,67s system 350% cpu 8,257 total
</code></pre><p>That’s not <em>terrible</em>, but not great either.</p>
<p>If you happen to open a process monitor while running tests, you might have
noticed that there are quite a number of PostgreSQL instances running. This
seems like something to optimize! Shouldn’t one PostgreSQL instance be enough
for all tests of a test run?</p>
<p>Let’s review the process model of <code>go test</code> before we can talk about how to
integrate with it.</p>
<h2 id="go-test-process-model">go test process model</h2>
<p>The usual command to run all tests of a Go project is <code>go test ./...</code> (see <a href="https://pkg.go.dev/cmd/go/internal/help#HelpPackages"><code>go help packages</code></a> for
details on the <code>/...</code> pattern syntax), which matches the Go package in the
current directory and all Go packages in its subdirectories.</p>
<p>Each Go package (≈ directory), including <code>_test.go</code> files, is compiled into a
<em>separate test binary:</em></p>
<pre tabindex="0"><code>% go help test
[…]
&#39;Go test&#39; recompiles each package along with any files with names matching
the file pattern &#34;*_test.go&#34;.
[…]
Each listed package causes the execution of a separate test binary.
[…]
</code></pre><p>These test binaries are then run in parallel. In fact, there are two levels of
parallelism at play here:</p>
<ol>
<li>All test functions (within a single test binary) that call <code>t.Parallel()</code> will be
run in parallel (in batches of size <code>-parallel</code>).</li>
<li><code>go test</code> will run different test binaries in parallel.</li>
</ol>
<p>The documentation explains that the <code>-parallel</code> test flag defaults to
<code>GOMAXPROCS</code> and references the <code>go test</code> parallelism:</p>
<pre tabindex="0"><code>% go help testflag
[…]
-parallel n
    Allow parallel execution of test functions that call t.Parallel, and
    fuzz targets that call t.Parallel when running the seed corpus.
    The value of this flag is the maximum number of tests to run
    simultaneously.
[…]
    By default, -parallel is set to the value of GOMAXPROCS.
    Setting -parallel to values higher than GOMAXPROCS may cause degraded
    performance due to CPU contention, especially when fuzzing.
    Note that -parallel only applies within a single test binary.
    The &#39;go test&#39; command may run tests for different packages
    in parallel as well, according to the setting of the -p flag
    (see &#39;go help build&#39;).
</code></pre><p>The <code>go test</code> parallelism is controlled by the <code>-p</code> flag, which also defaults to
<code>GOMAXPROCS</code>:</p>
<pre tabindex="0"><code>% go help build
[…]
-p n
	the number of programs, such as build commands or
	test binaries, that can be run in parallel.
	The default is GOMAXPROCS, normally the number of CPUs available.
[…]
</code></pre><p>To print <code>GOMAXPROCS</code> on a given machine, we can run a test program like this
<code>gomaxprocs.go</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;runtime&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020">print</span>(runtime.<span style="color:#06287e">GOMAXPROCS</span>(<span style="color:#40a070">0</span>))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>For me, <code>GOMAXPROCS</code> defaults to the <a href="https://ark.intel.com/content/www/us/en/ark/products/134597/intel-core-i9-12900-processor-30m-cache-up-to-5-10-ghz.html">24 <em>threads</em> of my Intel Core i9 12900K
CPU</a>,
which has 16 <em>cores</em> (8 Performance, 8 Efficiency; only the Performance cores
have Hyper Threading):</p>
<pre tabindex="0"><code>% go run gomaxprocs.go
24
% grep &#39;model name&#39; /proc/cpuinfo | wc -l
24
</code></pre><p>So with a single <code>go test ./...</code> command, we can expect 24 parallel processes
each running 24 tests in parallel. With our current approach, we would start up
to 24 concurrent ephemeral PostgreSQL instances (if we have that many packages),
which seems wasteful to me.</p>
<p>Starting one ephemeral PostgreSQL instance per <code>go test</code> run seems better.</p>
<h2 id="sharing-one-postgresql-among-all-tests">Sharing one PostgreSQL among all tests</h2>
<p>How can we go from starting 24 Postgres instances to starting just one?</p>
<p>First, we need to update our test setup code to work with a passed-in database
URL. For that, we switch from calling
<a href="https://pkg.go.dev/github.com/stapelberg/postgrestest#Server.CreateDatabase"><code>CreateDatabase</code></a>
to using a
<a href="https://pkg.go.dev/github.com/stapelberg/postgrestest#DBCreator"><code>DBCreator</code></a>
for a database identified by a URL. The old code still needs to remain so that
you can run a single test without bothering with <code>PGURL</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;display:grid;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>dbc<span style="color:#bbb"> </span><span style="color:#666">*</span>postgrestest.DBCreator<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">TestMain</span>(m<span style="color:#bbb"> </span><span style="color:#666">*</span>testing.M)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// It is best to specify the PGURL environment variable so that only</span><span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// one PostgreSQL instance is used for all tests.</span><span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>pgurl<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>os.<span style="color:#06287e">Getenv</span>(<span style="color:#4070a0">&#34;PGURL&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>pgurl<span style="color:#bbb"> </span><span style="color:#666">==</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&#34;</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// &#39;go test&#39; was started directly, start one Postgres per process:</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>pgt,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>postgrestest.<span style="color:#06287e">Start</span>(context.<span style="color:#06287e">Background</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020">panic</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span>pgt.<span style="color:#06287e">Cleanup</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>pgurl<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>pgt.<span style="color:#06287e">DefaultDatabase</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>dbc,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>postgrestest.<span style="color:#06287e">NewDBCreator</span>(pgurl)<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">		</span><span style="color:#007020">panic</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>m.<span style="color:#06287e">Run</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Inside the test function(s), we only need to update the <code>CreateDatabase</code>
receiver name:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;display:grid;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">TestSignupForm</span>(t<span style="color:#bbb"> </span><span style="color:#666">*</span>testing.T)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>pgurl,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>dbc.<span style="color:#06287e">CreateDatabase</span>(context.<span style="color:#06287e">Background</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>t.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// test goes here…</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Then, we create a new wrapper program (e.g. <code>internal/cmd/initpg/initpg.go</code>)
which calls <code>postgrestest.Start</code> and passes the <code>PGURL</code> environment variable to
the process(es) it starts:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// initpg is a small test helper command which starts a Postgres</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// instance and makes it available to the wrapped &#39;go test&#39; command.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;context&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;fmt&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;log&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os/exec&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;github.com/stapelberg/postgrestest&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Use the same database driver as in the rest of your project.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>_<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;github.com/lib/pq&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">runWrappedCommand</span>(pgurl<span style="color:#bbb"> </span><span style="color:#902000">string</span>)<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// os.Args[0] is initpg</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// os.Args[1] is --</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// os.Args[2] is go</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// os.Args[3] is test</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// etc.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wrapped<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>exec.<span style="color:#06287e">Command</span>(os.Args[<span style="color:#40a070">2</span>],<span style="color:#bbb"> </span>os.Args[<span style="color:#40a070">3</span>:]<span style="color:#666">...</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wrapped.Stdin<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stdin<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wrapped.Stdout<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stdout<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wrapped.Stderr<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stderr<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wrapped.Env<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#007020">append</span>(os.<span style="color:#06287e">Environ</span>(),<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;PGURL=&#34;</span><span style="color:#666">+</span>pgurl)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>wrapped.<span style="color:#06287e">Run</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Errorf</span>(<span style="color:#4070a0">&#34;%v: %v&#34;</span>,<span style="color:#bbb"> </span>wrapped.Args,<span style="color:#bbb"> </span>err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">initpg</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>pgt,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>postgrestest.<span style="color:#06287e">Start</span>(context.<span style="color:#06287e">Background</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// NOTE: keep reading the article, do not submit as-is</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span>pgt.<span style="color:#06287e">Cleanup</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Run the wrapped command (&#39;go test&#39;, typically)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#06287e">runWrappedCommand</span>(pgt.<span style="color:#06287e">DefaultDatabase</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">initpg</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><h3 id="running-the-initpg-wrapper-program">Running the initpg wrapper program</h3>
<p>While we could use <code>go run ./internal/cmd/initpg</code> to compile and run this
wrapper program, it is a bit wasteful to recompile this program over and over
when it rarely changes.</p>
<p>One alternative is to use <code>go install</code> instead of <code>go run</code>. I have two minor
concerns with that:</p>
<ol>
<li>
<p><code>go install</code> installs into the bin directory, which is <code>~/go/bin</code> by default.</p>
<ul>
<li>This means we need to rely on the <code>PATH</code> environment variable containing
the bin directory to run the installed program. Unfortunately, influencing
or determining the <code>go install</code> destination path is tricky.</li>
<li>It would be nice to not litter the user’s bin directory. I think the bin
directory should contain programs which the user explicitly requested to
install, not helper programs that are only necessary to run tests.</li>
</ul>
</li>
<li>
<p>On my machine, <code>go install</code> takes about 100ms, even when nothing has changed.</p>
</li>
</ol>
<p>I like to define a <code>Makefile</code> in each of my projects with a set of targets that
are consistently named, e.g. <code>make test</code>, <code>make push</code>, etc. Given that I already
use <code>make</code>, I like to set up my <code>Makefile</code> to build initpg in the <code>_bin</code>
directory:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-Makefile" data-lang="Makefile"><span style="display:flex;"><span><span style="color:#06287e">.PHONY</span><span style="color:#666">:</span> test
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#06287e">_bin/initpg</span><span style="color:#666">:</span> internal/cmd/initpg/initpg.go
</span></span><span style="display:flex;"><span>	mkdir -p _bin
</span></span><span style="display:flex;"><span>	go build -o _bin/initpg ./internal/cmd/initpg
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#06287e">test</span><span style="color:#666">:</span> _bin/initpg
</span></span><span style="display:flex;"><span>	./_bin/initpg -- go <span style="color:#007020">test</span> ./...
</span></span></code></pre></div><p>Because <code>initpg.go</code> rarely changes, the program will typically not need to be
recompiled.</p>
<p>Note that this <code>Makefile</code> is only approximately correct: <code>initpg</code>’s dependency
on <code>postgrestest</code> is not modeled, so you need to delete <code>_bin/initpg</code> to pick up
changes to <code>postgrestest</code>.</p>
<h2 id="performance">Performance</h2>
<p>Let’s compare the before and after test runtimes on the Intel Core i9 12900K:</p>
<pre tabindex="0"><code># Intel Core i9 12900K: one Postgres for each test
CGO_ENABLED=0 GOGC=off go test -count=1 -fullpath ./...
14,24s user 4,11s system 709% cpu 2,586 total

# Intel Core i9 12900K: one Postgres shared among all tests
CGO_ENABLED=0 GOGC=off ./_bin/initpg -- go test -count=1 -fullpath ./...
11,40s user 3,10s system 659% cpu 2,199 total
</code></pre><p>For comparison, the effect is more pronounced on the MacBook Air M1:</p>
<pre tabindex="0"><code># MacBook Air M1: one Postgres for each test
CGO_ENABLED=0 GOGC=off go test -count=1 -fullpath ./...
20,23s user 8,67s system 350% cpu 8,257 total

# MacBook Air M1: one Postgres shared among all tests
CGO_ENABLED=0 GOGC=off ./_bin/initpg -- go test -count=1 -fullpath ./...
14,25s user 4,36s system 275% cpu 6,752 total
</code></pre><p>Sharing one PostgreSQL instance has reduced the total test runtime for a full
run by about 20%!</p>
<h3 id="why-is-it-sometimes-slower">Why is it sometimes slower?</h3>
<p>We have measurably reduced the runtime of a full test run, but if you pay close
attention during development you will notice that now <strong>every test run is a full
test run</strong>, even when you only change a single package!</p>
<p>Why can Go no longer cache any of the test results? The problem is that the
<code>PGURL</code> environment variable has a different value on each run: the name of the
temporary directory that the <code>postgrestest</code> package uses for its ephemeral
database instance changes on each run.</p>
<p>The documentation on the <code>go test</code> caching behavior explains this in the last
paragraph:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;display:grid;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% go help test
</span></span><span style="display:flex;"><span>[…]
</span></span><span style="display:flex;"><span>In package list mode only, go test caches successful package test
</span></span><span style="display:flex;"><span>results to avoid unnecessary repeated running of tests. When the
</span></span><span style="display:flex;"><span>result of a test can be recovered from the cache, go test will
</span></span><span style="display:flex;"><span>redisplay the previous output instead of running the test binary
</span></span><span style="display:flex;"><span>again. When this happens, go test prints &#39;(cached)&#39; in place of the
</span></span><span style="display:flex;"><span>elapsed time in the summary line.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The rule for a match in the cache is that the run involves the same
</span></span><span style="display:flex;"><span>test binary and the flags on the command line come entirely from a
</span></span><span style="display:flex;"><span>restricted set of &#39;cacheable&#39; test flags, defined as -benchtime, -cpu,
</span></span><span style="display:flex;"><span>-list, -parallel, -run, -short, -timeout, -failfast, -fullpath and -v.
</span></span><span style="display:flex;"><span>If a run of go test has any test or non-test flags outside this set,
</span></span><span style="display:flex;"><span>the result is not cached. To disable test caching, use any test flag
</span></span><span style="display:flex;"><span>or argument other than the cacheable flags. The idiomatic way to disable
</span></span><span style="display:flex;"><span>test caching explicitly is to use -count=1.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Tests that open files within the package&#39;s source root (usually $GOPATH)
</span></span><span style="display:flex; background-color:#d8d8d8"><span>or that consult environment variables only match future runs in which
</span></span><span style="display:flex; background-color:#d8d8d8"><span>the files and environment variables are unchanged.
</span></span><span style="display:flex;"><span>[…]</span></span></code></pre></div>
<p>(See also <a href="https://github.com/golang/go/issues/22593">Go issue #22593</a> for more details.)</p>
<h3 id="fixing-go-test-caching-env-vars">Fixing Go test caching (env vars)</h3>
<p>For the Go test caching to work, all environment variables our tests access
(including <code>PGURL</code>) need to contain the same value between runs. For us, this
means we cannot use a randomly generated name for the Postgres data directory,
but instead need to use a fixed name.</p>
<p>My <code>postgrestest</code> package offers convenient support for specifying the desired
directory:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;display:grid;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">initpg</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>cacheDir,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>os.<span style="color:#06287e">UserCacheDir</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">	</span>pgt,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>postgrestest.<span style="color:#06287e">Start</span>(context.<span style="color:#06287e">Background</span>(),<span style="color:#bbb">
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#bbb">		</span>postgrestest.<span style="color:#06287e">WithDir</span>(filepath.<span style="color:#06287e">Join</span>(cacheDir,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;initpg.gus&#34;</span>)))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span>pgt.<span style="color:#06287e">Cleanup</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Run the wrapped command (&#39;go test&#39;, typically)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#06287e">runWrappedCommand</span>(pgt.<span style="color:#06287e">DefaultDatabase</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>When running the tests now, starting with the second run (without any changes),
you should see a “ (cached)” suffix printed behind tests that were successfully
cached, and the test runtime should be much shorter — under a second in my
project:</p>
<pre tabindex="0"><code>% time ./_bin/initpg -- go test -fullpath ./...
ok  	example/internal/handlers/adminhandler	(cached)
[…]
./_bin/initpg -- go test -fullpath ./...
1,30s user 0,88s system 288% cpu 0,756 total
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>In this article, I have shown how to integrate PostgreSQL into your test
environment in a way that is convenient for developers, light on system
resources and measurably reduces total test time.</p>
<p>Adopting <code>postgrestest</code> seems easy enough to me. If you want to see a complete
example, see <a href="https://github.com/gokrazy/gus/commit/b97c652fd03754ba817bd3c13f18ea6e2e154ef4">how I converted the <code>gokrazy/gus</code> repository to use
<code>postgrestest</code></a>.</p>
<h2 id="further-optimization-potential">Further optimization potential</h2>
<p>Now that we have a detailed understanding of the <code>go test</code> process model and
PostgreSQL startup, we can consider further optimizations. I won’t actually
implement them in this article, which is already long enough, but maybe you want
to go further in your project…</p>
<h3 id="hide-postgres-startup">Hide Postgres startup</h3>
<p>My journey into ephemeral PostgreSQL instances started with <a href="https://eradman.com/ephemeralpg/">Eric Radman’s
<code>pg_tmp</code> shell script</a>. Ultimately, I ended up
with the <code>postgrestest</code> Go solution that I much prefer: I don’t need to ship (or
require) the <code>pg_tmp</code> shell script with my projects. The fewer languages, the
better.</p>
<p>Also, <code>pg_tmp</code> is not a wrapper program, which resulted in problems regarding
cleanup: A wrapper program can reliably trigger cleanup when tests are done,
whereas <code>pg_tmp</code> has to poll for activity. Polling is prone to running too
quickly (cleaning up a database before tests were even started) or too slowly,
requiring constant tuning.</p>
<p>But, <code>pg_tmp</code> does have quite a clever concept of preparing PostgreSQL instances
in the background and thereby amortizing startup costs between test runs.</p>
<p>There might be an even simpler approach that could amount to the same startup
latency hiding behavior: Turning the sequential startup (<code>initpg</code> needs to wait
for PostgreSQL to start and only then can begin running <code>go test</code>) into parallel
startup using Socket Activation.</p>
<p>Note that PostgreSQL does not seem to support Socket Activation natively, so
probably one would need to implement a program-agnostic solution into <code>initpg</code>
as described in this <a href="https://unix.stackexchange.com/questions/352495/systemd-on-demand-start-of-services-like-postgresql-and-mysql-that-do-not-yet-s">Unix Stack Exchange
question</a>
or <a href="https://andreas.rammhold.de/posts/postgresql-tmpfs-with-sytemdsocket-activation-for-local-ephemeral-data-during-development/">Andreas Rammhold’s blog
post</a>.</p>
<h3 id="de-duplicate-schema-creation-cost">De-duplicate schema creation cost</h3>
<p>For isolation, we use a different PostgreSQL database for every test. This means
we need to initialize the database schema for each of these per-test databases.</p>
<p>We can eliminate this duplicative work by <strong>sharing the same database</strong> across
all tests, provided we have another way of isolating the tests from each other.</p>
<p>The <a href="https://github.com/DATA-DOG/go-txdb"><code>txdb</code> package</a> provides a standard
<code>database/sql.Driver</code> which runs all queries of an entire test in a single
transaction. Using <code>txdb</code> means we can now safely share the same database
between tests without running into conflicts, failing tests, or needing extra
locking.</p>
<p>Be sure to initialize the database schema <em>before</em> using <code>txdb</code> to share the
database: long-running transactions needs to lock the PostgreSQL catalog as soon
as you change the database schema (i.e. create or modify tables), meaning only
one test can run at a time. (Using <a href="https://sourcegraph.com/blog/go/an-introduction-to-go-tool-trace-rhys-hiltner"><code>go tool trace</code></a>
is a great way to understand such performance issues.)</p>
<p>I am aware that some people don’t like the transaction isolation approach. For
example, <a href="https://gajus.com/blog/setting-up-postgre-sql-for-running-integration-tests">Gajus Kuizinas’s blog post “Setting up PostgreSQL for running
integration
tests”</a>
finds that transactions don’t work in their (JavaScript) setup. I don’t share
this experience at all: In Go, the <a href="https://github.com/DATA-DOG/go-txdb"><code>txdb</code>
package</a> works well, even with nested
transactions. I have used <code>txdb</code> for months without problems.</p>
<p>In my tests, eliminating this duplicative schema initialization work saves
about:</p>
<ul>
<li>0.5s on my Intel Core i9 12900K</li>
<li>1s on the MacBook Air M1</li>
</ul>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Debug Go core dumps with delve: export byte slices]]></title>
    <link href="https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/"/>
    <id>https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/</id>
    <published>2024-10-22T17:22:23+02:00</published>
    <content type="html"><![CDATA[<p>Not all bugs can easily be reproduced — sometimes, all you have is a core dump
from a crashing program, but no idea about the triggering conditions of the bug
yet.</p>
<p>When using Go, we can use <a href="https://github.com/go-delve/delve">the delve
debugger</a> for core dump debugging, but I had
trouble figuring out how to save byte slice contents (for example: the incoming
request causing the crash) from memory into a file for further analysis, so this
article walks you through how to do it.</p>
<h2 id="simple-example">Simple Example</h2>
<p>Let’s imagine the following scenario: You are working on a performance
optimization in <a href="https://pkg.go.dev/google.golang.org/protobuf">Go Protobuf</a> and
have accidentally badly broken the <a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Marshal"><code>proto.Marshal</code>
function</a>. The
function is now returning an error, so let’s run one of the failing tests with
delve:</p>
<pre tabindex="0"><code>~/protobuf/proto master % dlv test
(dlv) b ExampleMarshal
(dlv) c
&gt; [Breakpoint 1] google.golang.org/protobuf/proto_test.ExampleMarshal() ./encode_test.go:293 (hits goroutine(1):1 total:1) (PC: 0x9d6c96)
(dlv) next 4
&gt; google.golang.org/protobuf/proto_test.ExampleMarshal() ./encode_test.go:297 (PC: 0xb54495)
   292: // [google.golang.org/protobuf/types/known/durationpb.New].
   293: func ExampleMarshal() {
   294: b, err := proto.Marshal(&amp;durationpb.Duration{
   295: Nanos: 125,
   296: })
=&gt; 297: if err != nil {
   298: panic(err)
   299: }
   300:
   301: fmt.Printf(&#34;125ns encoded into %d bytes of Protobuf wire format:\n% x\n&#34;, len(b), b)
   302:
</code></pre><p>Go Protobuf happens to return the already encoded bytes even when returning an
error, so we can inspect the <code>b</code> byte slice to see how far the encoding got
before the error happened:</p>
<pre tabindex="0"><code>(dlv) print b
[]uint8 len: 2, cap: 2, [16,125]
</code></pre><p>In this case, we can see that the entire (trivial) message was encoded, so our
error must happen at a later stage — this allows us to rule out a large chunk of
code in our search for the bug.</p>
<p>But what would we do if a longer part of the message was displayed and we wanted
to load it into a different tool for further analysis, e.g. the excellent
<a href="https://github.com/protocolbuffers/protoscope">protoscope</a>?</p>
<p>The low-tech approach is to print the contents and copy&amp;paste from the delve
output into an editor or similar. This stops working as soon as your data
contains non-printable characters.</p>
<p>We have multiple options to export the byte slice to a file:</p>
<ol>
<li>
<p>We could add <code>os.WriteFile(&quot;/tmp/b.raw&quot;, b, 0644)</code> to the source code and
re-run the test. This is definitely the simplest option, as it works with or
without a debugger.</p>
</li>
<li>
<p>As long as delve is connected to a running program, we can use delve’s call
command to just execute the same code without having to add it to our source:</p>
<pre tabindex="0"><code>(dlv) call os.WriteFile(&#34;/tmp/b.raw&#34;, b, 0644)
(dlv)
</code></pre></li>
</ol>
<p>Notably, both options only work when you can debug interactively. For the first
option, you need to be able to change the source. The second option requires
that delve is attached to a running process that you can afford to pause and
interactively control.</p>
<p>These are trivial requirements when running a unit tests on your local machine,
but get much harder when debugging an RPC service that crashes with specific
requests, as you need to only run your changed debugging code for the
troublesome requests, skipping the unproblematic requests that should still be
handled normally.</p>
<h2 id="core-dump-debugging-with-go">Core dump debugging with Go</h2>
<p>So let’s switch example: we are no longer working on Go Protobuf. Instead, we
now need to debug an RPC service where certain requests crash the service. We’ll
use core dump debugging!</p>















<a href="https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/core-memory-featured.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/core-memory-featured_hu_8e666702f0357e4d.jpg 2x,https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/core-memory-featured_hu_a1e1faf114299629.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/core-memory-featured_hu_e4c52f9dcb473cf9.jpg"
  alt="Core memory" title="Core memory"
  width="600"
  height="516"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>In case you’re wondering: The name “<a href="https://en.wikipedia.org/wiki/Core_dump">core
dump</a>” comes from <a href="https://en.wikipedia.org/wiki/Magnetic-core_memory">magnetic-core
memory</a>. These days we
should probably say “memory dump” instead. The picture above shows an exhibit
from the <a href="https://mitmuseum.mit.edu/">MIT Museum</a> (<em>Core Memory Unit, Bank C
(from Project Whirlwind, 1953-1959))</em>, a core memory unit with 4 KB of capacity.</p>
<p>To make Go write a core dump when panicing, run your program with the
environment variable <code>GOTRACEBACK=crash</code> set (all possible values are documented
<a href="https://pkg.go.dev/runtime">in the <code>runtime</code> package</a>).</p>
<p>You also need to ensure your system is set up to collect core dumps, as they are
typically discarded by default:</p>
<ul>
<li>On Linux, the easiest way is to install <a href="https://manpages.debian.org/systemd-coredump.8"><code>systemd-coredump(8)</code></a>
, after which core dumps will automatically be collected. You
can use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to list and work with them.</li>
<li>On macOS, you can enable core dump collection, but <a href="https://github.com/go-delve/delve/issues/2026">delve cannot open macOS
core dumps</a>. Luckily, macOS is
rarely used for production servers.</li>
<li>I don’t know about Windows and other systems.</li>
</ul>
<p>You can find more details and options in the <a href="https://go.dev/wiki/CoreDumpDebugging">CoreDumpDebugging page of the Go
wiki</a>. For this article, we will stick to
the <code>coredumpctl</code> route:</p>
<p>We’ll use the <a href="https://grpc.io/docs/languages/go/quickstart/">gRPC Go Quick start
example</a>, a greeter client/server
program, and add a <code>panic()</code> call to the server <code>SayHello</code> handler:</p>
<pre tabindex="0"><code>% cd greeter_server
% go build -gcflags=all=&#34;-N -l&#34;  # disable optimizations
% GOTRACEBACK=crash ./greeter_server
2024/10/19 21:48:01 server listening at [::]:50051
2024/10/19 21:48:03 Received: world
panic: oh no!

goroutine 5 gp=0xc000007c00 m=5 mp=0xc000100008 [running]:
panic({0x83ca60?, 0x9a3710?})
	/home/michael/sdk/go1.23.0/src/runtime/panic.go:804 +0x168 fp=0xc000169850 sp=0xc0001697a0 pc=0x46fe88
main.(*server).SayHello(0xcbb840?, {0x877200?, 0xc000094900?}, 0x4a6f25?)
	/home/michael/go/src/github.com/grpc/grpc-go/examples/helloworld/greeter_server/main.go:45 +0xbf fp=0xc0001698c0 sp=0xc000169850 pc=0x8037ff
[…]
signal: aborted (core dumped)
</code></pre><p>The last line is what we want to see: it should say “core dumped”.</p>
<p>We can now use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to launch delve for
this program + core dump:</p>
<pre tabindex="0"><code>% coredumpctl debug --debugger=dlv --debugger-arguments=core
           PID: 1729467 (greeter_server)
           UID: 1000 (michael)
           GID: 1000 (michael)
        Signal: 6 (ABRT)
     Timestamp: Sat 2024-10-19 21:50:12 CEST (1min 49s ago)
  Command Line: ./greeter_server
    Executable: /home/michael/go/src/github.com/grpc/grpc-go/examples/helloworld/greeter_server/greeter_server
 Control Group: /user.slice/user-1000.slice/session-1.scope
          Unit: session-1.scope
         Slice: user-1000.slice
       Session: 1
     Owner UID: 1000 (michael)
       Storage: /var/lib/systemd/coredump/core.greeter_server.1000.zst (present)
  Size on Disk: 204.7K
       Message: Process 1729467 (greeter_server) of user 1000 dumped core.
                
                Module /home/michael/go/src/github.com/grpc/grpc-go/examples/helloworld/greeter_server/greeter_server without build-id.
                Stack trace of thread 1729470:
                #0  0x0000000000479461 n/a (greeter_server + 0x79461)
[…]
                ELF object binary architecture: AMD x86-64

Type &#39;help&#39; for list of commands.
(dlv) bt
 0  0x0000000000479461 in runtime.raise
    at /home/michael/sdk/go1.23.0/src/runtime/sys_linux_amd64.s:154
 1  0x0000000000451a85 in runtime.dieFromSignal
    at /home/michael/sdk/go1.23.0/src/runtime/signal_unix.go:942
 2  0x00000000004520e6 in runtime.sigfwdgo
    at /home/michael/sdk/go1.23.0/src/runtime/signal_unix.go:1154
 3  0x0000000000450a85 in runtime.sigtrampgo
    at /home/michael/sdk/go1.23.0/src/runtime/signal_unix.go:432
 4  0x0000000000479461 in runtime.raise
    at /home/michael/sdk/go1.23.0/src/runtime/sys_linux_amd64.s:153
 5  0x0000000000451a85 in runtime.dieFromSignal
    at /home/michael/sdk/go1.23.0/src/runtime/signal_unix.go:942
 6  0x0000000000439551 in runtime.crash
    at /home/michael/sdk/go1.23.0/src/runtime/signal_unix.go:1031
 7  0x0000000000439551 in runtime.fatalpanic
    at /home/michael/sdk/go1.23.0/src/runtime/panic.go:1290
 8  0x000000000046fe88 in runtime.gopanic
    at /home/michael/sdk/go1.23.0/src/runtime/panic.go:804
 9  0x00000000008037ff in main.(*server).SayHello
    at ./main.go:45
10  0x00000000008033a6 in google.golang.org/grpc/examples/helloworld/helloworld._Greeter_SayHello_Handler
    at /home/michael/go/src/github.com/grpc/grpc-go/examples/helloworld/helloworld/helloworld_grpc.pb.go:115
11  0x00000000007edeeb in google.golang.org/grpc.(*Server).processUnaryRPC
    at /home/michael/go/src/github.com/grpc/grpc-go/server.go:1394
12  0x00000000007f2eab in google.golang.org/grpc.(*Server).handleStream
    at /home/michael/go/src/github.com/grpc/grpc-go/server.go:1805
13  0x00000000007ebbff in google.golang.org/grpc.(*Server).serveStreams.func2.1
    at /home/michael/go/src/github.com/grpc/grpc-go/server.go:1029
14  0x0000000000477c21 in runtime.goexit
    at /home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:1700
(dlv) 
</code></pre><p>Alright! Now let’s switch to frame 9 (our server’s <code>SayHello</code> handler) and
inspect the <code>Name</code> field of the incoming RPC request:</p>
<pre tabindex="0"><code>(dlv) frame 9
&gt; runtime.raise() /home/michael/sdk/go1.23.0/src/runtime/sys_linux_amd64.s:154 (PC: 0x482681)
Warning: debugging optimized function
Frame 9: ./main.go:45 (PC: aaabf8)
    40:	}
    41:	
    42:	// SayHello implements helloworld.GreeterServer
    43:	func (s *server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    44:		log.Printf(&#34;Received: %v&#34;, in.GetName())
=&gt;  45:		panic(&#34;oh no!&#34;)
    46:		return &amp;pb.HelloReply{Message: &#34;Hello &#34; + in.GetName()}, nil
    47:	}
    48:	
    49:	func main() {
    50:		flag.Parse()
(dlv) p in
(&#34;*google.golang.org/grpc/examples/helloworld/helloworld.HelloRequest&#34;)(0xc000120100)
*google.golang.org/grpc/examples/helloworld/helloworld.HelloRequest {
[…]
	unknownFields: []uint8 len: 0, cap: 0, nil,
	Name: &#34;world&#34;,}
</code></pre><p>In this case, it’s easy to see that the <code>Name</code> field was set to <code>world</code> in the
incoming request, but let’s assume the request contained lots of binary data
that was not as easy to read or copy.</p>
<p>How do we write the byte slice contents to a file? In this scenario, we cannot
modify the source code and delve’s <code>call</code> command does not work on core dumps
(only when delve is attached to a running process):</p>
<pre tabindex="0"><code>(dlv) call os.WriteFile(&#34;/tmp/name.raw&#34;, in.Name, 0644)
&gt; runtime.raise() /home/michael/sdk/go1.23.0/src/runtime/sys_linux_amd64.s:154 (PC: 0x482681)
Warning: debugging optimized function
Command failed: can not continue execution of core process
</code></pre><p>Luckily, we can extend delve with a custom Starlark function to write byte slice
contents to a file.</p>
<h2 id="exporting-byte-slices-with-writebytestofile">Exporting byte slices with writebytestofile</h2>
<p>You need a version of dlv that contains <a href="https://github.com/go-delve/delve/commit/52405ba86bd9e14a2e643db391cbdebdcbdb3368">commit
52405ba</a>. Until
the commit is part of a released version, you can install the latest dlv
directly from git:</p>
<pre tabindex="0"><code>% go install github.com/go-delve/delve/cmd/dlv@master
</code></pre><p>Save the following Starlark code to a file, for example <code>~/dlv_writebytestofile.star</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Syntax: writebytestofile &lt;byte slice var&gt; &lt;output file path&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">def</span> <span style="color:#06287e">command_writebytestofile</span>(args):
</span></span><span style="display:flex;"><span>	var_name, filename <span style="color:#666">=</span> args<span style="color:#666">.</span>split(<span style="color:#4070a0">&#34; &#34;</span>)
</span></span><span style="display:flex;"><span>	s <span style="color:#666">=</span> <span style="color:#007020">eval</span>(<span style="color:#007020;font-weight:bold">None</span>, var_name)<span style="color:#666">.</span>Variable
</span></span><span style="display:flex;"><span>	mem <span style="color:#666">=</span> examine_memory(s<span style="color:#666">.</span>Base, s<span style="color:#666">.</span>Len)<span style="color:#666">.</span>Mem
</span></span><span style="display:flex;"><span>	write_file(filename, mem)
</span></span></code></pre></div><p>Then, in delve, load the Starlark code and run the function to export the byte
slice contents of <code>in.Name</code> to <code>/tmp/name.raw</code>:</p>
<pre tabindex="0"><code>% coredumpctl debug --debugger=dlv --debugger-arguments=core
(dlv) frame 9
(dlv) source ~/dlv_writebytestofile.star
(dlv) writebytestofile in.Name /tmp/name.raw
</code></pre><p>Let’s verify that we got the right contents:</p>
<pre tabindex="0"><code>% hexdump -C /tmp/name.raw
00000000  77 6f 72 6c 64                                    |world|
00000005
</code></pre><h2 id="core-dump-debugging-with-nethttp-servers">Core dump debugging with <code>net/http</code> servers</h2>
<p>When you want to apply the core dump debugging technique on a <code>net/http</code> server
(instead of a gRPC server, as above), you will notice that panics in your HTTP
handlers do not actually result in a core dump! This code in
<code>go/src/net/http/server.go</code> recovers panics and logs a stack trace:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">func</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">recover</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span><span style="color:#666">&amp;&amp;</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span>ErrAbortHandler<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span><span style="color:#007020;font-weight:bold">const</span><span style="color:#bbb"> </span>size<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#40a070">64</span><span style="color:#bbb"> </span><span style="color:#666">&lt;&lt;</span><span style="color:#bbb"> </span><span style="color:#40a070">10</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>buf<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">make</span>([]<span style="color:#902000">byte</span>,<span style="color:#bbb"> </span>size)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>buf<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>buf[:runtime.<span style="color:#06287e">Stack</span>(buf,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>)]<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>c.server.<span style="color:#06287e">logf</span>(<span style="color:#4070a0">&#34;http: panic serving %v: %v\n%s&#34;</span>,<span style="color:#bbb"> </span>c.remoteAddr,<span style="color:#bbb"> </span>err,<span style="color:#bbb"> </span>buf)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}()<span style="color:#bbb">
</span></span></span></code></pre></div><p>Or, in other words: the <code>GOTRACEBACK=crash</code> environment variable configures what
happens for unhandled signals, but this signal is handled with the <code>recover()</code>
call, so no core is dumped.</p>
<p>This default behavior of <code>net/http</code> servers <a href="https://github.com/golang/go/issues/25245">is now considered regrettable but
cannot be changed for
compatibility</a>. (We probably can add
a struct field to optionally not recover panics, though. I’ll update this
paragraph once there is a proposal.)</p>
<p>So, what options do we have in the meantime?</p>
<p>We could recover panics in our own code (before <code>net/http</code>’s panic handler is
called), but then how do we produce a core dump from our own handler?</p>
<p>A closer look reveals that the Go runtime’s <code>crash</code> function is defined in
<code>signal_unix.go</code> and <a href="https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/runtime/signal_unix.go;l=938">sends signal <code>SIGABRT</code> with the <code>dieFromSignal</code>
function</a>
to the current thread:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020">//go:nosplit</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">crash</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span><span style="color:#06287e">dieFromSignal</span>(_SIGABRT)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>The default action for <code>SIGABRT</code> is to “terminate the process and dump core”,
see <a href="https://manpages.debian.org/signal.7"><code>signal(7)</code></a>
.</p>
<p>We can follow the same strategy and send <code>SIGABRT</code> to our process:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>http.<span style="color:#06287e">HandleFunc</span>(<span style="color:#4070a0">&#34;/&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">func</span>(w<span style="color:#bbb"> </span>http.ResponseWriter,<span style="color:#bbb"> </span>r<span style="color:#bbb"> </span><span style="color:#666">*</span>http.Request)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">func</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">recover</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span>proc,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>os.<span style="color:#06287e">FindProcess</span>(syscall.<span style="color:#06287e">Getpid</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">					</span><span style="color:#007020">panic</span>(fmt.<span style="color:#06287e">Sprintf</span>(<span style="color:#4070a0">&#34;could not find own process (pid %d): %v&#34;</span>,<span style="color:#bbb"> </span>syscall.<span style="color:#06287e">Getpid</span>(),<span style="color:#bbb"> </span>err))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span>proc.<span style="color:#06287e">Signal</span>(syscall.SIGABRT)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span><span style="color:#60a0b0;font-style:italic">// Ensure the stack triggering the core dump sticks around</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">				</span>proc.<span style="color:#06287e">Wait</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// …buggy handler code goes here; for illustration we panic</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020">panic</span>(<span style="color:#4070a0">&#34;this should result in a core dump&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>})<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Fatal</span>(http.<span style="color:#06287e">ListenAndServe</span>(<span style="color:#4070a0">&#34;:8080&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span>))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>There is one caveat: If you have any non-Go threads running in your program,
e.g. by using cgo, they might pick up the signal, so ensure they do not install
a <code>SIGABRT</code> handler (see also: <a href="https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG">cgo-related documentation in
<code>os/signal</code></a>).</p>
<p>If this is a concern, you can make the above code more platform-specific and use
the <a href="https://manpages.debian.org/tgkill.2"><code>tgkill(2)</code></a>
 syscall to direct the signal to the
current thread, as <a href="https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/runtime/sys_linux_amd64.s;l=143">the Go runtime
does</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Core dump debugging can be a very useful technique to quickly make progress on
otherwise hard-to-debug problems. In small environments (single to few Linux
servers), core dumps are easy enough to turn on and work with, but in larger
environments you might need to invest into central core dump collection.</p>
<p>I hope the technique shown above comes in handy when you need to work with core
dumps.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[gokrazy: instance-centric configuration released 🎉]]></title>
    <link href="https://michael.stapelberg.ch/posts/2023-01-15-gokrazy-instance-centric-config/"/>
    <id>https://michael.stapelberg.ch/posts/2023-01-15-gokrazy-instance-centric-config/</id>
    <published>2023-01-15T19:38:00+01:00</published>
    <content type="html"><![CDATA[<p><a href="https://gokrazy.org/">gokrazy</a> is an appliance platform for Go programs: with
just a few commands, you can deploy your Go program(s) on a Raspberry Pi or a
(typically small) PC.</p>
<p>I’m excited to let you know that gokrazy now comes with a re-designed <code>gok</code>
command line tool and gokrazy instance configuration mechanism!</p>
<h2 id="context-gokrazy-in-a-few-words">Context: gokrazy in a few words</h2>
<img src="gokrazy-logo.png" align="right" width="200" style="margin-left: 1.5em" alt="gokrazy logo">
<p>The traditional way to run Go software on a Raspberry Pi would be to install
Raspbian or some other Linux distribution onto the SD card, copy over your
program(s) and then maintain that installation (do regular updates).</p>
<p>I thought it would be nicer to run my Raspberry Pis such that <strong>only Go
software</strong> is run by the Linux kernel on it, without any traditional Linux
distribution programs like package managers or even the usual <a href="https://en.wikipedia.org/wiki/GNU_Core_Utilities">GNU Core
Utilities</a>.</p>
<p>gokrazy builds Go programs into a read-only SquashFS root file system
image. When that image is started on a Raspberry Pi, a minimal init system
supervises the Go programs, and a DHCP and NTP client configure the IP address
and synchronize the time, respectively. After the first installation, all
subsequent updates can be done over the network, with an A/B partitioning
scheme.</p>
<p>I use gokrazy to, for example:</p>
<ul>
<li>
<p><a href="https://github.com/stapelberg/scan2drive">Scan incoming paper mail into PDF files on Google
Drive</a>.</p>
</li>
<li>
<p>Connect to the internet using <a href="https://router7.org/">router7, my small home internet
router</a> written in Go, running on a <a href="/posts/2021-07-10-linux-25gbit-internet-router-pc-build/">fast router PC
build</a> that handles a
<a href="/posts/2022-04-23-fiber7-25gbit-upgrade/">25 Gbit/s Fiber To The Home
connection</a>.</p>
</li>
<li>
<p><a href="/posts/2021-01-10-mqtt-introduction/">Automate the lights in my home</a>, and
<a href="https://github.com/stapelberg/hmgo">control and monitor the heating</a>.</p>
</li>
<li>
<p>Offer <a href="https://gokrazy.org/packages/tailscale/">Tailscale access</a> to a
Raspberry Pi Zero 2 W in my home network to then send Wake On Lan (WOL)
packets before SSH&rsquo;ing into my normally-suspended computers. See also my post
<a href="/posts/2022-08-27-out-of-band-remote-console/">DIY out-of-band management: remote console server
(2022)</a>.</p>
</li>
</ul>
<h2 id="before-and-after">Before and after</h2>
<p>Previously, the concept of gokrazy instance configuration was only a
convention. Each gokrazy build was created using the <code>gokr-packer</code> CLI tool, and
configured by the packer’s command-line flags, parameters, config files in
<code>~/.config</code> and per-package config files in the current directory
(e.g. <code>flags/github.com/gokrazy/breakglass/flags.txt</code>).</p>
<p>Now, all gokrazy commands and tools understand the <code>--instance</code> flag (or <code>-i</code>
for short), which determines the directory from which the <a href="https://gokrazy.org/userguide/instance-config/">Instance
Config</a> is read. For a gokrazy
instance named “hello”, the default directory is <code>~/gokrazy/hello</code>, which
contains the <code>config.json</code> file.</p>
<h2 id="example-creating-an-instance-for-a-go-working-copy">Example: creating an instance for a Go working copy</h2>
<p>Let’s say I have <a href="https://github.com/evcc-io/evcc">the evcc repository</a> cloned
as <code>~/src/evcc</code>. evcc is an electric vehicle charge controller with PV
integration, written in Go.</p>
<p>Now I want to run <code>evcc</code> on my Raspberry Pi using gokrazy. First, I create a new
instance:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>% gok -i evcc new
</span></span><span style="display:flex;"><span>gokrazy instance configuration created in /home/michael/gokrazy/evcc/config.json
</span></span><span style="display:flex;"><span><span style="color:#666">(</span>Use <span style="color:#4070a0">&#39;gok -i evcc edit&#39;</span> to edit the configuration interactively.<span style="color:#666">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Use <span style="color:#4070a0">&#39;gok -i evcc add&#39;</span> to add packages to this instance
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>To deploy this gokrazy instance, see <span style="color:#4070a0">&#39;gok help overwrite&#39;</span>
</span></span></code></pre></div><p>Now let’s add our working copy of <code>evcc</code> to the instance:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>% gok -i evcc add .
</span></span><span style="display:flex;"><span>2023/01/15 18:55:39 Adding the following package to gokrazy instance <span style="color:#4070a0">&#34;evcc&#34;</span>:
</span></span><span style="display:flex;"><span>  Go package  : github.com/evcc-io/evcc
</span></span><span style="display:flex;"><span>  in Go module: github.com/evcc-io/evcc
</span></span><span style="display:flex;"><span>  in <span style="color:#007020">local</span> dir: /tmp/evcc
</span></span><span style="display:flex;"><span>2023/01/15 18:55:39 Creating gokrazy builddir <span style="color:#007020;font-weight:bold">for</span> package github.com/evcc-io/evcc
</span></span><span style="display:flex;"><span>2023/01/15 18:55:39 Creating go.mod with replace directive
</span></span><span style="display:flex;"><span>go: creating new go.mod: module gokrazy/build/github.com/evcc-io/evcc
</span></span><span style="display:flex;"><span>2023/01/15 18:55:39 Adding package to gokrazy config
</span></span><span style="display:flex;"><span>2023/01/15 18:55:39 All <span style="color:#007020;font-weight:bold">done</span>! Next, use <span style="color:#4070a0">&#39;gok overwrite&#39;</span> <span style="color:#666">(</span>first deployment<span style="color:#666">)</span>, <span style="color:#4070a0">&#39;gok update&#39;</span> <span style="color:#666">(</span>following deployments<span style="color:#666">)</span> or <span style="color:#4070a0">&#39;gok run&#39;</span> <span style="color:#666">(</span>run on running instance temporarily<span style="color:#666">)</span>
</span></span></code></pre></div><p>We might want to monitor this Raspberry Pi’s stats later, so let’s add the
Prometheus node exporter to our gokrazy instance, too:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>% gok -i evcc add github.com/prometheus/node_exporter
</span></span><span style="display:flex;"><span>2023/01/15 19:04:05 Adding github.com/prometheus/node_exporter as a <span style="color:#666">(</span>non-local<span style="color:#666">)</span> package to gokrazy instance evcc
</span></span><span style="display:flex;"><span>2023/01/15 19:04:05 Creating gokrazy builddir <span style="color:#007020;font-weight:bold">for</span> package github.com/prometheus/node_exporter
</span></span><span style="display:flex;"><span>2023/01/15 19:04:05 Creating go.mod before calling go get
</span></span><span style="display:flex;"><span>go: creating new go.mod: module gokrazy/build/github.com/prometheus/node_exporter
</span></span><span style="display:flex;"><span>2023/01/15 19:04:05 running <span style="color:#666">[</span>go get github.com/prometheus/node_exporter@latest<span style="color:#666">]</span>
</span></span><span style="display:flex;"><span>go: downloading github.com/prometheus/node_exporter v1.5.0
</span></span><span style="display:flex;"><span><span style="color:#666">[</span>…<span style="color:#666">]</span>
</span></span><span style="display:flex;"><span>2023/01/15 19:04:07 Adding package to gokrazy config
</span></span></code></pre></div><p>It’s time to insert an SD card (<code>/dev/sdx</code>), which we will overwrite with a
gokrazy build:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>% gok -i evcc overwrite --full /dev/sdx
</span></span></code></pre></div><p>See <a href="https://gokrazy.org/quickstart/">gokrazy quickstart</a> for more detailed instructions.</p>
<h2 id="automation">Automation</h2>
<p>The new <code>gok</code> subcommands (<code>add</code>, <code>update</code>, etc.) are much easier to manage than
long <code>gokr-packer</code> command lines.</p>
<p>The new <a href="https://gokrazy.org/userguide/automation/">Automation page</a> shows how
to automate common tasks, be it daily updates via <code>cron</code>, or automated building
in Continuous Integration environments like GitHub Actions.</p>
<h2 id="migration">Migration</h2>
<p>Are you already a gokrazy user? If so, see <a href="https://gokrazy.org/userguide/migration-guide/">the Instance Config Migration
Guide</a> for how to switch from
the old <code>gokr-packer</code> tool to the new <code>gok</code> command.</p>
<h2 id="feedback--questions">Feedback / Questions?</h2>
<p>If you have any questions, please feel free to reach out at <a href="https://github.com/gokrazy/gokrazy/discussions">gokrazy GitHub
Discussions</a> 👋</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Readiness notifications in Go]]></title>
    <link href="https://michael.stapelberg.ch/posts/2020-02-02-readiness-notifications-in-golang/"/>
    <id>https://michael.stapelberg.ch/posts/2020-02-02-readiness-notifications-in-golang/</id>
    <published>2020-02-02T00:00:00+00:00</published>
    <content type="html"><![CDATA[<p>When spawning a child program, for example in an integration test, it is often
helpful to know when the child program is ready to receive requests.</p>
<h3 id="delaying">Delaying</h3>
<p>A brittle strategy is to just add a delay (say, <code>time.Sleep(2 * time.Second)</code>)
and hope the child program finishes initialization in that time. This is brittle
because it depends on timing, so when the computer running the test is slow for
whichever reason, your test starts failing. Many CI/CD systems have less
capacity (and/or are more heavily utilized) than developer machines, so timeouts
frequently need to be adjusted.</p>
<p>Also, relying on timing is a race to the bottom: your delay needs to work on the
slowest machine that runs your code. Ergo, tests waste valuable developer time
on your high-end workstation, just so that they pass on some under-powered
machine.</p>
<h3 id="polling">Polling</h3>
<p>A slightly better strategy is polling, i.e. repeatedly checking whether the
child program is ready. As an example, in the <code>dnsmasq_exporter</code> test, <a href="https://github.com/google/dnsmasq_exporter/blob/646ded9be82e26a4c6450da8d7128d12e0e11e3a/dnsmasq_test.go#L46-L61">I need
to
poll</a>
to find out when <a href="https://manpages.debian.org/dnsmasq.8"><code>dnsmasq(8)</code></a>
 is ready.</p>
<p>This approach is better because it automatically works well on both high-end and
under-powered machines, without wasting time on either.</p>
<p>Finding a good frequency with which to poll is a bit of an art, though: the more
often you poll, the less time you waste, but also the more resources you spend
on polling instead of letting your program initialize. The overhead may be
barely noticeable, but when starting lots of programs (e.g. in a microservice
architecture) or when individual polls are costly, the overhead can add up.</p>
<h3 id="readiness-notifications">Readiness notifications</h3>
<p>The most elegant approach is to use readiness notifications: you don’t waste any
time or resources.</p>
<p>It only takes a few lines of code to integrate this approach into your
application. The specifics might vary depending on your environment,
e.g. whether an environment variable is preferable to a command-line flag; my
goal with this article is to explain the approach in general, and you can take
care of the details.</p>
<p>The key idea is: the child program inherits a pipe file descriptor from the
parent and closes it once ready. The parent program knows the child program is
ready because an otherwise blocking read from the pipe returns once the pipe is
closed.</p>
<p>This is similar to using a <code>chan struct{}</code> in Go and closing it. It doesn’t have
to remain this simple, though: you can also send arbitrary data over the pipe,
ranging from a simple string being sent in one direction and culminating in
speaking a framed protocol in a client/server fashion. In <a href="https://codesearch.debian.net/">Debian Code
Search</a>, I’m <a href="https://github.com/Debian/dcs/blob/3baaecabca2d6c56799012c40c1245fc389cb6e6/internal/addrfd/addrfd.go">writing the chosen network
address</a>
before closing the pipe, so that the parent program knows where to connect to.</p>
<h4 id="parent-program">Parent Program</h4>
<p>So, how do we go about readiness notifications in Go? We create a new pipe and
specify the write end in the <code>ExtraFiles</code> field of <code>(os/exec).Cmd</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>r,<span style="color:#bbb"> </span>w,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>os.<span style="color:#06287e">Pipe</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>child<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>exec.<span style="color:#06287e">Command</span>(<span style="color:#4070a0">&#34;child&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>child.Stderr<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stderr<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>child.ExtraFiles<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>[]<span style="color:#666">*</span>os.File{w}<span style="color:#bbb">
</span></span></span></code></pre></div><p>It is good practice to explicitly specify the file descriptor number that we
passed via some sort of signaling, so that the child program does not need to be
modified when we add new file descriptors in the parent, and also because this
behavior is usually opt-in.</p>
<p>In this case, we’ll do that via an environment variable and start the child
program:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// Go dup2()’s ExtraFiles to file descriptor 3 and counting.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// File descriptors 0, 1, 2 are stdin, stdout and stderr.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>child.Env<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#007020">append</span>(os.<span style="color:#06287e">Environ</span>(),<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;CHILD_READY_FD=3&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// Note child.Start(), not child.Run():</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>child.<span style="color:#06287e">Start</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Errorf</span>(<span style="color:#4070a0">&#34;%v: %v&#34;</span>,<span style="color:#bbb"> </span>child.Args,<span style="color:#bbb"> </span>err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>At this point, both the parent and the child process have a file descriptor
referencing the write end of the pipe. Since the pipe will only be closed once
<em>all</em> processes have closed the write end, we need to close the write end in the
parent program:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// Close the write end of the pipe in the parent:</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>w.<span style="color:#06287e">Close</span>()<span style="color:#bbb">
</span></span></span></code></pre></div><p>Now, we can blockingly read from the pipe, and know that once the read call
returns, the child program is ready to receive requests:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// Avoid hanging forever in case the child program never becomes ready;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// this is easier to diagnose than an unspecified CI/CD test timeout.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// This timeout should be much much longer than initialization takes.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>r.<span style="color:#06287e">SetReadDeadline</span>(time.<span style="color:#06287e">Now</span>().<span style="color:#06287e">Add</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Minute))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>ioutil.<span style="color:#06287e">ReadAll</span>(r);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Errorf</span>(<span style="color:#4070a0">&#34;awaiting readiness: %v&#34;</span>,<span style="color:#bbb"> </span>err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// …send requests…</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic">// …tear down child program…</span><span style="color:#bbb">
</span></span></span></code></pre></div><h4 id="child-program">Child Program</h4>
<p>In the child program, we need to recognize that the parent program requests a
readiness notification, and ensure our signaling doesn’t leak to child programs
of the child program:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>readyFile<span style="color:#bbb"> </span><span style="color:#666">*</span>os.File<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">init</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>fd,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>strconv.<span style="color:#06287e">Atoi</span>(os.<span style="color:#06287e">Getenv</span>(<span style="color:#4070a0">&#34;CHILD_READY_FD&#34;</span>));<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">==</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>readyFile<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.<span style="color:#06287e">NewFile</span>(<span style="color:#007020">uintptr</span>(fd),<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;readyfd&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>os.<span style="color:#06287e">Unsetenv</span>(<span style="color:#4070a0">&#34;CHILD_READY_FD&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#60a0b0;font-style:italic">// …initialize…</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>readyFile<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>readyFile.<span style="color:#06287e">Close</span>()<span style="color:#bbb"> </span><span style="color:#60a0b0;font-style:italic">// signal readiness</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>readyFile<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">   </span><span style="color:#60a0b0;font-style:italic">// just to be prudent</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span>}<span style="color:#bbb">
</span></span></span></code></pre></div><h3 id="conclusion">Conclusion</h3>
<p>Depending on what you’re communicating from the child to the parent, and how
your system is architected, it might be a good idea to use <a href="http://0pointer.de/blog/projects/socket-activation.html">systemd socket
activation</a> (<a href="https://vincent.bernat.ch/en/blog/2018-systemd-golang-socket-activation">socket
activation in
Go</a>). It
works similarly in concept, but passes a listening socket and readiness is
determined by the child process answering requests. We introduced this technique
in the <a href="https://i3wm.org/docs/testsuite.html#_appendix_b_socket_activation">i3
testsuite</a>
and reduced the total wallclock time from &gt;100 seconds to a mere 16 seconds back
then (even faster today).</p>
<p>The technique described in this blog post is a bit more generic than systemd’s
socket activation. In general, passing file descriptors between processes is a
powerful idea. For example, in debiman, we’re <a href="https://github.com/Debian/debiman/blob/32eac1bc6182f68c7443a56b85c33522dc3d5d70/internal/convert/mandoc.go#L118">passing individual pipe file
descriptors</a>
to a persistent <a href="https://manpages.debian.org/mandocd.8"><code>mandocd(8)</code></a>
 process to quickly
convert lots of man pages without encurring process creation overhead.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[distri: 20x faster initramfs (initrd) from scratch]]></title>
    <link href="https://michael.stapelberg.ch/posts/2020-01-21-initramfs-from-scratch-golang/"/>
    <id>https://michael.stapelberg.ch/posts/2020-01-21-initramfs-from-scratch-golang/</id>
    <published>2020-01-21T17:50:00+01:00</published>
    <content type="html"><![CDATA[<p>In case you are not yet familiar with why an initramfs (or initrd, or initial
ramdisk) is typically used when starting Linux, let me quote the <a href="https://en.wikipedia.org/wiki/Initial_ramdisk">wikipedia
definition</a>:</p>
<p>“[…] initrd is a scheme for loading a temporary root file system into memory,
which may be used as part of the Linux startup process […] to make preparations
before the real root file system can be mounted.”</p>
<p>Many Linux distributions do not compile all file system drivers into the kernel,
but instead load them on-demand from an initramfs, which saves memory.</p>
<p>Another common scenario, in which an initramfs is required, is full-disk
encryption: the disk must be unlocked from userspace, but since userspace is
encrypted, an initramfs is used.</p>
<h2 id="motivation">Motivation</h2>
<p>Thus far, building a <a href="https://distr1.org/">distri</a> disk image was quite slow:</p>
<p>This is on an AMD Ryzen 3900X 12-core processor (2019):</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
80.29s user 13.56s system 186% cpu 50.419 total # 19s image, 31s initrd
</code></pre><p>Of these 50 seconds,
<a href="https://en.wikipedia.org/wiki/Dracut_(software)"><code>dracut</code></a>’s initramfs
generation accounts for 31 seconds (62%)!</p>
<p>Initramfs generation time drops to 8.7 seconds once <code>dracut</code> no longer needs to
use the single-threaded <a href="https://manpages.debian.org/gzip.1"><code>gzip(1)</code></a>
, but the
multi-threaded replacement <a href="https://manpages.debian.org/pigz.1"><code>pigz(1)</code></a>
:</p>
<p>This brings the total time to build a distri disk image down to:</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
76.85s user 13.23s system 327% cpu 27.509 total # 19s image, 8.7s initrd
</code></pre><p>Clearly, when you use <code>dracut</code> on any modern computer, you should make pigz
available. <code>dracut</code> should fail to compile unless one explicitly opts into the
known-slower gzip. For more thoughts on optional dependencies, see <a href="/posts/2019-05-23-optional-dependencies/">“Optional
dependencies don’t work”</a>.</p>
<p>But why does it take 8.7 seconds still? Can we go faster?</p>
<p>The answer is <strong>Yes</strong>! I recently built a distri-specific initramfs I’m calling
<code>minitrd</code>. I wrote both big parts from scratch:</p>
<ol>
<li>the initramfs generator program (<a href="https://github.com/distr1/distri/blob/master/cmd/distri/initrd.go"><code>distri initrd</code></a>)</li>
<li>a custom Go userland (<a href="https://github.com/distr1/distri/blob/master/cmd/minitrd/minitrd.go"><code>cmd/minitrd</code></a>), running as <code>/init</code> in the initramfs.</li>
</ol>
<p><code>minitrd</code> generates the initramfs image in ≈400ms, bringing the total time down
to:</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
50.09s user 8.80s system 314% cpu 18.739 total # 18s image, 400ms initrd
</code></pre><p>(The remaining time is spent in preparing the file system, then installing and
configuring the distri system, i.e. preparing a disk image you can <a href="https://distr1.org/#run-distri-on-real-hardware">run on real
hardware</a>.)</p>
<p>How can <code>minitrd</code> be 20 times faster than <code>dracut</code>?</p>
<p><code>dracut</code> is mainly written in shell, with a C helper program. It drives the
generation process by spawning lots of external dependencies (e.g. <code>ldd</code> or the
<code>dracut-install</code> helper program). I assume that the combination of using an
interpreted language (shell) that spawns lots of processes and precludes a
concurrent architecture is to blame for the poor performance.</p>
<p><code>minitrd</code> is written in Go, with speed as a goal. It leverages concurrency and
uses no external dependencies; everything happens within a single process (but
with enough threads to saturate modern hardware).</p>
<p>Measuring early boot time using qemu, I measured the <code>dracut</code>-generated
initramfs taking 588ms to display the full disk encryption passphrase prompt,
whereas <code>minitrd</code> took only 195ms.</p>
<p>The rest of this article dives deeper into how <code>minitrd</code> works.</p>
<h2 id="what-does-an-initramfs-do">What does an initramfs do?</h2>
<p>Ultimately, the job of an initramfs is to make the root file system available
and continue booting the system from there. Depending on the system setup, this
involves the following 5 steps:</p>
<h3 id="1-load-kernel-modules-to-access-the-block-devices-with-the-root-file-system">1. Load kernel modules to access the block devices with the root file system</h3>
<p>Depending on the system, the block devices with the root file system might
already be present when the initramfs runs, or some kernel modules might need to
be loaded first. On my Dell XPS 9360 laptop, the NVMe system disk is already
present when the initramfs starts, whereas in qemu, we need to load the
<code>virtio_pci</code> module, followed by the <code>virtio_scsi</code> module.</p>
<p>How will our userland program know which kernel modules to load? Linux kernel
modules declare patterns for their supported hardware as an alias, e.g.:</p>
<pre tabindex="0"><code>initrd# grep virtio_pci lib/modules/5.4.6/modules.alias
alias pci:v00001AF4d*sv*sd*bc*sc*i* virtio_pci
</code></pre><p>Devices in <code>sysfs</code> have a <code>modalias</code> file whose content can be matched against
these declarations to identify the module to load:</p>
<pre tabindex="0"><code>initrd# cat /sys/devices/pci0000:00/*/modalias
pci:v00001AF4d00001005sv00001AF4sd00000004bc00scFFi00
pci:v00001AF4d00001004sv00001AF4sd00000008bc01sc00i00
[…]
</code></pre><p>Hence, for the initial round of module loading, it is sufficient to locate all
<code>modalias</code> files within <code>sysfs</code> and load the responsible modules.</p>
<p>Loading a kernel module can result in new devices appearing. When that happens,
the kernel sends a
<a href="https://stackoverflow.com/questions/22803469/uevent-sent-from-kernel-to-user-space-udev">uevent</a>,
which the uevent consumer in userspace receives via a netlink socket. Typically,
this consumer is <a href="https://manpages.debian.org/udev.7"><code>udev(7)</code></a>
, but in our case, it’s
<code>minitrd</code>.</p>
<p>For each uevent messages that comes with a <code>MODALIAS</code> variable, <code>minitrd</code> will
load the relevant kernel module(s).</p>
<p>When loading a kernel module, its dependencies need to be loaded
first. Dependency information is stored in the <code>modules.dep</code> file in a
<code>Makefile</code>-like syntax:</p>
<pre tabindex="0"><code>initrd# grep virtio_pci lib/modules/5.4.6/modules.dep
kernel/drivers/virtio/virtio_pci.ko: kernel/drivers/virtio/virtio_ring.ko kernel/drivers/virtio/virtio.ko
</code></pre><p>To load a module, we can open its file and then call the Linux-specific <a href="https://manpages.debian.org/finit_module.2"><code>finit_module(2)</code></a>
 system call. Some modules are expected to
return an error code, e.g. <code>ENODEV</code> or <code>ENOENT</code> when some hardware device is not
actually present.</p>
<p>Side note: next to the textual versions, there are also binary versions of the
<code>modules.alias</code> and <code>modules.dep</code> files. Presumably, those can be queried more
quickly, but for simplicitly, I have not (yet?) implemented support in
<code>minitrd</code>.</p>
<h3 id="2-console-settings-font-keyboard-layout">2. Console settings: font, keyboard layout</h3>
<p>Setting a legible font is necessary for hi-dpi displays. On my Dell XPS 9360
(3200 x 1800 QHD+ display), the following works well:</p>
<pre tabindex="0"><code>initrd# setfont latarcyrheb-sun32
</code></pre><p>Setting the user’s keyboard layout is necessary for entering the LUKS full-disk
encryption passphrase in their preferred keyboard layout. I use the <a href="https://www.neo-layout.org">NEO
layout</a>:</p>
<pre tabindex="0"><code>initrd# loadkeys neo
</code></pre><h3 id="3-block-device-identification">3. Block device identification</h3>
<p>In the Linux kernel, block device enumeration order is not necessarily the same
on each boot. Even if it was deterministic, device order could still be changed
when users modify their computer’s device topology (e.g. connect a new disk to a
formerly unused port).</p>
<p>Hence, it is good style to refer to disks and their partitions with stable
identifiers. This also applies to boot loader configuration, and so most
distributions will set a kernel parameter such as
<code>root=UUID=1fa04de7-30a9-4183-93e9-1b0061567121</code>.</p>
<p>Identifying the block device or partition with the specified <code>UUID</code> is the
initramfs’s job.</p>
<p>Depending on what the device contains, the UUID comes from a different
place. For example, <code>ext4</code> file systems have a UUID field in their file system
superblock, whereas LUKS volumes have a UUID in their LUKS header.</p>
<p>Canonically, probing a device to extract the UUID is done by <code>libblkid</code> from the
<code>util-linux</code> package, but the logic can easily be <a href="https://github.com/distr1/distri/blob/master/cmd/minitrd/blkid.go">re-implemented in other
languages</a>
and changes rarely. <code>minitrd</code> comes with its own implementation to avoid
<a href="https://golang.org/cmd/cgo/">cgo</a> or running the <a href="https://manpages.debian.org/blkid.8"><code>blkid(8)</code></a>
 program.</p>
<h3 id="4-luks-full-disk-encryption-unlocking-only-on-encrypted-systems">4. LUKS full-disk encryption unlocking (only on encrypted systems)</h3>
<p>Unlocking a
<a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup">LUKS</a>-encrypted volume
is done in userspace. The kernel handles the crypto, but reading the metadata,
obtaining the passphrase (or e.g. key material from a file) and setting up the
device mapper table entries are done in user space.</p>
<pre tabindex="0"><code>initrd# modprobe algif_skcipher
initrd# cryptsetup luksOpen /dev/sda4 cryptroot1
</code></pre><p>After the user entered their passphrase, the root file system can be mounted:</p>
<pre tabindex="0"><code>initrd# mount /dev/dm-0 /mnt
</code></pre><h3 id="5-continuing-the-boot-process-switch_root">5. Continuing the boot process (switch_root)</h3>
<p>Now that everything is set up, we need to pass execution to the init program on
the root file system with a careful sequence of <a href="https://manpages.debian.org/chdir.2"><code>chdir(2)</code></a>
, <a href="https://manpages.debian.org/mount.2"><code>mount(2)</code></a>
, <a href="https://manpages.debian.org/chroot.2"><code>chroot(2)</code></a>
, <a href="https://manpages.debian.org/chdir.2"><code>chdir(2)</code></a>
 and <a href="https://manpages.debian.org/execve.2"><code>execve(2)</code></a>
 system calls that is explained in <a href="https://github.com/mirror/busybox/blob/9ec836c033fc6e55e80f3309b3e05acdf09bb297/util-linux/switch_root.c#L297">this busybox switch_root
comment</a>.</p>
<pre tabindex="0"><code>initrd# mount -t devtmpfs dev /mnt/dev
initrd# exec switch_root -c /dev/console /mnt /init
</code></pre><p>To conserve RAM, the files in the temporary file system to which the initramfs
archive is extracted are typically deleted.</p>
<h2 id="how-is-an-initramfs-generated">How is an initramfs generated?</h2>
<p>An initramfs “image” (more accurately: archive) is a compressed
<a href="https://en.wikipedia.org/wiki/Cpio">cpio</a> archive. Typically, gzip compression
is used, but the kernel supports a bunch of different algorithms and
distributions such as <a href="https://www.phoronix.com/scan.php?page=news_item&amp;px=LZ4-Initramfs-Ubuntu-Go-Ahead">Ubuntu are switching to lz4</a>.</p>
<p>Generators typically prepare a temporary directory and feed it to the <a href="https://manpages.debian.org/cpio.1"><code>cpio(1)</code></a>
 program. In <code>minitrd</code>, we read the files into memory
and generate the cpio archive using the
<a href="https://github.com/cavaliercoder/go-cpio">go-cpio</a> package. We use the
<a href="https://github.com/klauspost/pgzip">pgzip</a> package for parallel gzip
compression.</p>
<p>The following files need to go into the cpio archive:</p>
<h3 id="minitrd-go-userland">minitrd Go userland</h3>
<p>The <code>minitrd</code> binary is copied into the cpio archive as <code>/init</code> and will be run
by the kernel after extracting the archive.</p>
<p>Like the rest of distri, <code>minitrd</code> is built statically without cgo, which means
it can be copied as-is into the cpio archive.</p>
<h3 id="linux-kernel-modules">Linux kernel modules</h3>
<p>Aside from the <code>modules.alias</code> and <code>modules.dep</code> metadata files, the kernel
modules themselves reside in e.g. <code>/lib/modules/5.4.6/kernel</code> and need to be
copied into the cpio archive.</p>
<p>Copying all modules results in a ≈80 MiB archive, so it is common to only copy
modules that are relevant to the initramfs’s features. This reduces archive size
to ≈24 MiB.</p>
<p>The filtering relies on hard-coded patterns and module names. For example, disk
encryption related modules are all kernel modules underneath <code>kernel/crypto</code>,
plus <code>kernel/drivers/md/dm-crypt.ko</code>.</p>
<p>When generating a host-only initramfs (works on precisely the computer that
generated it), some initramfs generators look at the currently loaded modules
and just copy those.</p>
<h3 id="console-fonts-and-keymaps">Console Fonts and Keymaps</h3>
<p>The <code>kbd</code> package’s <a href="https://manpages.debian.org/setfont.8"><code>setfont(8)</code></a>
 and <a href="https://manpages.debian.org/loadkeys.1"><code>loadkeys(1)</code></a>
 programs load console fonts and keymaps from
<code>/usr/share/consolefonts</code> and <code>/usr/share/keymaps</code>, respectively.</p>
<p>Hence, these directories need to be copied into the cpio archive. Depending on
whether the initramfs should be generic (work on many computers) or host-only
(works on precisely the computer/settings that generated it), the entire
directories are copied, or only the required font/keymap.</p>
<h3 id="cryptsetup-setfont-loadkeys">cryptsetup, setfont, loadkeys</h3>
<p>These programs are (currently) required because <code>minitrd</code> does not implement
their functionality.</p>
<p>As they are dynamically linked, not only the programs themselves need to be
copied, but also the ELF dynamic linking loader (path stored in the <code>.interp</code>
ELF section) and any ELF library dependencies.</p>
<p>For example, <code>cryptsetup</code> in distri declares the ELF interpreter
<code>/ro/glibc-amd64-2.27-3/out/lib/ld-linux-x86-64.so.2</code> and declares dependencies
on shared libraries <code>libcryptsetup.so.12</code>, <code>libblkid.so.1</code> and others. Luckily,
in distri, packages contain a <code>lib</code> subdirectory containing symbolic links to
the resolved shared library paths (hermetic packaging), so it is sufficient to
mirror the lib directory into the cpio archive, recursing into shared library
dependencies of shared libraries.</p>
<p><code>cryptsetup</code> also requires the GCC runtime library <code>libgcc_s.so.1</code> to be present
at runtime, and will abort with an error message about not being able to call
<a href="https://manpages.debian.org/pthread_cancel.3"><code>pthread_cancel(3)</code></a>
 if it is unavailable.</p>
<h3 id="time-zone-data">time zone data</h3>
<p>To print log messages in the correct time zone, we copy <code>/etc/localtime</code> from
the host into the cpio archive.</p>
<h2 id="minitrd-outside-of-distri">minitrd outside of distri?</h2>
<p>I currently have no desire to make <code>minitrd</code> available outside of
<a href="https://distr1.org/">distri</a>. While the technical challenges (such as extending
the generator to not rely on distri’s hermetic packages) are surmountable, I
don’t want to support people’s initramfs remotely.</p>
<p>Also, I think that people’s efforts should in general be spent on rallying
behind <code>dracut</code> and making it work faster, thereby benefiting all Linux
distributions that use dracut (increasingly more). With <code>minitrd</code>, I have
demonstrated that significant speed-ups are achievable.</p>
<h2 id="conclusion">Conclusion</h2>
<p>It was interesting to dive into how an initramfs really works. I had been
working with the concept for many years, from small tasks such as “debug why the
encrypted root file system is not unlocked” to more complicated tasks such as
“set up a root file system on DRBD for a high-availability setup”. But even with
that sort of experience, I didn’t know all the details, until I was forced to
implement every little thing.</p>
<p>As I suspected going into this exercise, <code>dracut</code> is much slower than it needs
to be. Re-implementing its generation stage in a modern language instead of
shell helps a lot.</p>
<p>Of course, my <code>minitrd</code> does a bit less than <code>dracut</code>, but not drastically
so. The overall architecture is the same.</p>
<p>I hope my effort helps with two things:</p>
<ol>
<li>
<p>As a teaching implementation: instead of wading through the various
components that make up a modern initramfs (udev, systemd, various shell
scripts, …), people can learn about how an initramfs works in a single place.</p>
</li>
<li>
<p>I hope the significant time difference motivates people to improve <code>dracut</code>.</p>
</li>
</ol>
<h2 id="appendix-qemu-development-environment">Appendix: qemu development environment</h2>
<p>Before writing any Go code, I did some manual prototyping. Learning how other
people prototype is often immensely useful to me, so I’m sharing my notes here.</p>
<p>First, I copied all kernel modules and a statically built busybox binary:</p>
<pre tabindex="0"><code>% mkdir -p lib/modules/5.4.6
% cp -Lr /ro/lib/modules/5.4.6/* lib/modules/5.4.6/
% cp ~/busybox-1.22.0-amd64/busybox sh
</code></pre><p>To generate an initramfs from the current directory, I used:</p>
<pre tabindex="0"><code>% find . | cpio -o -H newc | pigz &gt; /tmp/initrd
</code></pre><p>In distri’s <code>Makefile</code>, I append these flags to the <code>QEMU</code> invocation:</p>
<pre tabindex="0"><code>-kernel /tmp/kernel \
-initrd /tmp/initrd \
-append &#34;root=/dev/mapper/cryptroot1 rdinit=/sh ro console=ttyS0,115200 rd.luks=1 rd.luks.uuid=63051f8a-54b9-4996-b94f-3cf105af2900 rd.luks.name=63051f8a-54b9-4996-b94f-3cf105af2900=cryptroot1 rd.vconsole.keymap=neo rd.vconsole.font=latarcyrheb-sun32 init=/init systemd.setenv=PATH=/bin rw vga=836&#34;
</code></pre><p>The <code>vga=</code> mode parameter is required for loading font <code>latarcyrheb-sun32</code>.</p>
<p>Once in the <code>busybox</code> shell, I manually prepared the required mount points and
kernel modules:</p>
<pre tabindex="0"><code>ln -s sh mount
ln -s sh lsmod
mkdir /proc /sys /run /mnt
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
modprobe virtio_pci
modprobe virtio_scsi
</code></pre><p>As a next step, I copied <code>cryptsetup</code> and dependencies into the initramfs directory:</p>
<pre tabindex="0"><code>% for f in /ro/cryptsetup-amd64-2.0.4-6/lib/*; do full=$(readlink -f $f); rel=$(echo $full | sed &#39;s,^/,,g&#39;); mkdir -p $(dirname $rel); install $full $rel; done
% ln -s ld-2.27.so ro/glibc-amd64-2.27-3/out/lib/ld-linux-x86-64.so.2
% cp /ro/glibc-amd64-2.27-3/out/lib/ld-2.27.so ro/glibc-amd64-2.27-3/out/lib/ld-2.27.so
% cp -r /ro/cryptsetup-amd64-2.0.4-6/lib ro/cryptsetup-amd64-2.0.4-6/
% mkdir -p ro/gcc-libs-amd64-8.2.0-3/out/lib64/
% cp /ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1 ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1
% ln -s /ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1 ro/cryptsetup-amd64-2.0.4-6/lib
% cp -r /ro/lvm2-amd64-2.03.00-6/lib ro/lvm2-amd64-2.03.00-6/
</code></pre><p>In <code>busybox</code>, I used the following commands to unlock the root file system:</p>
<pre tabindex="0"><code>modprobe algif_skcipher
./cryptsetup luksOpen /dev/sda4 cryptroot1
mount /dev/dm-0 /mnt
</code></pre>]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Debian Code Search: positional index, TurboPFor-compressed]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-09-29-dcs-positional-turbopfor-index/"/>
    <id>https://michael.stapelberg.ch/posts/2019-09-29-dcs-positional-turbopfor-index/</id>
    <published>2019-09-29T00:00:00+00:00</published>
    <content type="html"><![CDATA[<style type="text/css">
.bar {
  display: inline-block;
  padding: 0.25em;
  text-align: center;
  vertical-align: middle;
}

.barcon {
  width: 40em;
  display: flex;
}
</style>
<p>See the <a href="#conclusion">Conclusion</a> for a summary if you’re impatient :-)</p>
<h3 id="motivation">Motivation</h3>
<p>Over the last few months, I have been developing a new index format for Debian
Code Search. This required a lot of careful refactoring, re-implementation,
debug tool creation and debugging.</p>
<p>Multiple factors motivated my work on a new index format:</p>
<ol>
<li>
<p>The existing index format has a 2G size limit, into which we have bumped a
few times, requiring manual intervention to keep the system running.</p>
</li>
<li>
<p>Debugging the existing system required creating ad-hoc debugging tools, which
made debugging sessions unnecessarily lengthy and painful.</p>
</li>
<li>
<p>I wanted to check whether <a href="https://github.com/Debian/dcs/issues/85">switching to a different integer compression
format</a> would improve performance
(it does not).</p>
</li>
<li>
<p>I wanted to check whether storing positions with the posting lists would
improve performance of identifier queries (= queries which are not using any
regular expression features), which make up 78.2% of all Debian Code Search
queries (it does).</p>
</li>
</ol>
<p>I figured building a new index from scratch was the easiest approach, compared
to refactoring the existing index to increase the size limit (point ①).</p>
<p>I also figured it would be a good idea to develop the debugging tool in lock
step with the index format so that I can be sure the tool works and is useful
(point ②).</p>
<h3 id="integer-compression-turbopfor">Integer compression: TurboPFor</h3>
<p>As a quick refresher, search engines typically store document IDs (representing
source code files, in our case) in an ordered list (“posting list”). It usually
makes sense to apply at least a rudimentary level of compression: our existing
system used variable integer encoding.</p>
<p><a href="https://github.com/powturbo/TurboPFor">TurboPFor</a>, the self-proclaimed “Fastest
Integer Compression” library, combines an advanced on-disk format with a
carefully tuned SIMD implementation to reach better speeds (in micro benchmarks)
at less disk usage than <a href="https://github.com/google/codesearch/blob/4fe90b597ae534f90238f82c7b5b1bb6d6d52dff/index/write.go#L561">Russ Cox’s varint implementation in
<code>github.com/google/codesearch</code></a>.</p>
<p>If you are curious about its inner workings, check out my “<a href="/posts/2019-02-05-turbopfor-analysis/">TurboPFor: an
analysis</a>”.</p>
<p>Applied on the Debian Code Search index, TurboPFor indeed compresses integers better:</p>
<h4 id="disk-space">Disk space</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">8.9G</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 61%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">5.5G</span>
TurboPFor index
<p>Switching to TurboPFor (via cgo) for storing and reading the index results in a
slight speed-up of a <code>dcs replay</code> benchmark, which is more pronounced the more
i/o is required.</p>
<h4 id="query-speed-regexp-cold-page-cache">Query speed (regexp, cold page cache)</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">18s</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 77.7%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">14s</span>
TurboPFor index (cgo)
<h4 id="query-speed-regexp-warm-page-cache">Query speed (regexp, warm page cache)</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">15s</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 93.3%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">14s</span>
TurboPFor index (cgo)
<p>Overall, TurboPFor is an all-around improvement in efficiency, albeit with a
high cost in implementation complexity.</p>
<h3 id="positional-index-trade-more-disk-for-faster-queries">Positional index: trade more disk for faster queries</h3>
<p>This section builds on the previous section: all figures come from the TurboPFor
index, which can optionally support positions.</p>
<p>Conceptually, we’re going from:</p>
<pre tabindex="0"><code>type docid uint32
type index map[trigram][]docid
</code></pre><p>…to:</p>
<pre tabindex="0"><code>type occurrence struct {
    doc docid
    pos uint32 // byte offset in doc
}
type index map[trigram][]occurrence
</code></pre><p>The resulting index consumes more disk space, but can be queried faster:</p>
<ol>
<li>
<p>We can do fewer queries: instead of reading all the posting lists for all
the trigrams, we can read the posting lists for the query’s first and last
trigram only.
<br>
This is one of the tricks described in the paper
“<a href="https://cedric.cnam.fr/fichiers/art_3216.pdf">AS-Index: A
Structure For String Search Using n-grams and Algebraic Signatures</a>”
(PDF), and goes a long way without incurring the complexity, computational
cost and additional disk usage of calculating algebraic signatures.</p>
</li>
<li>
<p>Verifying the delta between the last and first position matches the length
of the query term significantly reduces the number of files to read (lower
false positive rate).</p>
</li>
<li>
<p>The matching phase is quicker: instead of locating the query term in the
file, we only need to compare a few bytes at a known offset for equality.</p>
</li>
<li>
<p>More data is read sequentially (from the index), which is faster.</p>
</li>
</ol>
<h4 id="disk-space-1">Disk space</h4>
<p>A positional index consumes significantly more disk space, but not so much as
to pose a challenge: a Hetzner EX61-NVME dedicated server (≈ 64 €/month)
provides 1 TB worth of fast NVMe flash storage.</p>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 5.2%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">&nbsp;6.5G</span>
non-positional
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">123G</span>
positional
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 75.6%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">&nbsp;&nbsp;93G</span>
positional (posrel)
<p>The idea behind the positional index (posrel) is to not store a <code>(doc,pos)</code>
tuple on disk, but to store positions, accompanied by a stream of doc/pos
relationship bits: 1 means this position belongs to the next document, 0 means
this position belongs to the current document.</p>
<p>This is an easy way of saving some space without modifying the TurboPFor
on-disk format: the posrel technique reduces the index size to about ¾.</p>
<p>With the increase in size, the Linux page cache hit ratio will be lower for
the positional index, i.e. more data will need to be fetched from disk for
querying the index.</p>
<p>As long as the disk can deliver data as fast as you can decompress posting
lists, this only translates into one disk seek’s worth of additional
latency. This is the case with modern NVMe disks that deliver thousands of MB/s,
e.g. the Samsung 960 Pro (used in Hetzner’s aforementioned EX61-NVME server).</p>
<p>The values were measured by running <code>dcs du -h /srv/dcs/shard*/full</code>
without and with the <code>-pos</code> argument.</p>
<h4 id="bytes-read">Bytes read</h4>
<p>A positional index requires fewer queries: reading only the first and last
trigram’s posting lists and positions is sufficient to achieve a lower (!) false
positive rate than evaluating <strong>all</strong> trigram’s posting lists in a
non-positional index.</p>
<p>As a consequence, fewer files need to be read, resulting in fewer bytes required
to read from disk overall.</p>
<p>As an additional bonus, in a positional index, more data is read sequentially
(index), which is faster than random i/o, regardless of the underlying disk.</p>
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 1.2em); background-color: blue; color: white">
  1.2G
</div>
<div class="bar" style="width: calc(2 * 19.8em); background-color: green; color: white">
  19.8G
</div>
</div>
</div>
<span style="margin-right: 2em">21.0G</span>
regexp queries
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 4.2em); background-color: blue; color: white">
  4.2G (index)
</div>
<div class="bar" style="width: calc(2 * 10.8em); background-color: green; color: white">
  10.8G (files)
</div>
</div>
</div>
<span style="margin-right: 2em">15.0G</span>
identifier queries
<p>The values were measured by running <code>iostat -d 25</code> just before running
<a href="https://codesearch.debian.net/research/2019-08-03-dcs-new-index/"><code>bench.zsh</code></a>
on an otherwise idle system.</p>
<h4 id="query-speed">Query speed</h4>
<p>Even though the positional index is larger and requires more data to be read at
query time (see above), thanks to the C TurboPFor library, the 2 queries on a
positional index are roughly as fast as the n queries on a non-positional index
(≈4s instead of ≈3s).</p>
<p>This is more than made up for by the combined i/o matching stage, which shrinks
from ≈18.5s (7.1s i/o + 11.4s matching) to ≈1.3s.</p>
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 3.3em); background-color: blue; color: white">
  3.3s (index)
</div>
<div class="bar" style="width: calc(2 * 7.1em); background-color: green; color: white">
  7.1s (i/o)
</div>
<div class="bar" style="width: calc(2 * 11.4em); background-color: purple; color: white">
  11.4s (matching)
</div>
</div>
</div>
<span style="margin-right: 2em">21.8s</span>
regexp queries
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 3.92em); background-color: blue; color: white">
  3.92s (index)
</div>
<div class="bar" style="width: calc(2 * 1.3em); background-color: green; color: white">
  ≈1.3s
</div>
</div>
</div>
<span style="margin-right: 2em">5.22s</span>
identifier queries
<p>Note that identifier query i/o was sped up not just by needing to read fewer
bytes, but also by only having to verify bytes at a known offset instead of
needing to locate the identifier within the file.</p>
<h3 id="conclusion">Conclusion</h3>
<p>The new index format is overall slightly more efficient. This disk space
efficiency allows us to introduce a positional index section for the first
time.</p>
<p>Most Debian Code Search queries are positional queries (78.2%) and will be
answered much quicker by leveraging the positions.</p>
<p>Bottomline, it is beneficial to use a positional index on disk over a
non-positional index in RAM.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[distri: a Linux distribution to research fast package management]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-08-17-introducing-distri/"/>
    <id>https://michael.stapelberg.ch/posts/2019-08-17-introducing-distri/</id>
    <published>2019-08-17T18:36:00+02:00</published>
    <content type="html"><![CDATA[<p>Over the last year or so I have worked on a research linux distribution in my
spare time. It’s not a distribution for researchers (like <a href="https://en.wikipedia.org/wiki/Scientific_Linux">Scientific
Linux</a>), but my personal
playground project to research linux distribution development, i.e. try out
fresh ideas.</p>
<p>This article focuses on the package format and its advantages, but there is
more to distri, which I will <a href="#more-to-come">cover in upcoming blog posts</a>.</p>
<h3 id="motivation">Motivation</h3>
<p>I was a Debian Developer for the 7 years from 2012 to 2019, but using the
distribution often left me frustrated, ultimately <a href="/posts/2019-03-10-debian-winding-down/">resulting in me winding down
my Debian work</a>.</p>
<p>Frequently, I was noticing a large gap between the actual speed of an operation
(e.g. doing an update) and the possible speed based on back of the envelope
calculations. I wrote more about this in my blog post <a href="/posts/2019-08-17-linux-package-managers-are-slow/">“Package managers are
slow”</a>.</p>
<p>To me, this observation means that either there is potential to optimize the
package manager itself (e.g. <code>apt</code>), or what the system does is just too
complex. While I remember seeing some low-hanging fruit¹, through my work on
distri, I wanted to explore whether all the complexity we currently have in
Linux distributions such as Debian or Fedora is inherent to the problem space.</p>
<p>I have completed enough of the experiment to conclude that the complexity is not
inherent: I can build a Linux distribution for general-enough purposes which is
much less complex than existing ones.</p>
<p>① Those were low-hanging fruit from a user perspective. I’m not saying that
fixing them is easy in the technical sense; I know too little about <code>apt</code>’s code
base to make such a statement.</p>
<h3 id="key-idea-packages-are-images-not-archives">Key idea: packages are images, not archives</h3>
<p>One key idea is to switch from using archives to using <strong>images</strong> for package
contents. Common package managers such as <a href="https://manpages.debian.org/dpkg.1"><code>dpkg(1)</code></a>

use <a href="https://manpages.debian.org/tar.1"><code>tar(1)</code></a>
 archives with various compression
algorithms.</p>
<p>distri uses <a href="https://en.wikipedia.org/wiki/SquashFS">SquashFS images</a>, a
comparatively simple file system image format that I happen to be familiar with
from my work on the <a href="https://gokrazy.org">gokrazy Raspberry Pi 3 Go platform</a>.</p>
<p>This idea is not novel: <a href="https://en.wikipedia.org/wiki/AppImage">AppImage</a> and
<a href="https://en.wikipedia.org/wiki/Snappy_(package_manager)">snappy</a> also use
images, but only for individual, self-contained applications. distri however
uses images for distribution packages with dependencies. In particular, there is
no duplication of shared libraries in distri.</p>
<p>A nice side effect of using read-only image files is that applications are
immutable and can hence not be broken by accidental (or malicious!)
modification.</p>
<h3 id="key-idea-separate-hierarchies">Key idea: separate hierarchies</h3>
<p>Package contents are made available under a fully-qualified path. E.g., all
files provided by package <code>zsh-amd64-5.6.2-3</code> are available under
<code>/ro/zsh-amd64-5.6.2-3</code>. The mountpoint <code>/ro</code> stands for read-only, which is
short yet descriptive.</p>
<p>Perhaps surprisingly, building software with custom <code>prefix</code> values of
e.g. <code>/ro/zsh-amd64-5.6.2-3</code> is widely supported, thanks to:</p>
<ol>
<li>
<p>Linux distributions, which build software with <code>prefix</code> set to <code>/usr</code>,
whereas FreeBSD (and the autotools default), which build with <code>prefix</code> set to
<code>/usr/local</code>.</p>
</li>
<li>
<p>Enthusiast users in corporate or research environments, who install software
into their home directories.</p>
</li>
</ol>
<p>Because using a custom <code>prefix</code> is a common scenario, upstream awareness for
<code>prefix</code>-correctness is generally high, and the rarely required patch will be
quickly accepted.</p>
<h3 id="key-idea-exchange-directories">Key idea: exchange directories</h3>
<p>Software packages often exchange data by placing or locating files in well-known
directories. Here are just a few examples:</p>
<ul>
<li><a href="https://manpages.debian.org/gcc.1"><code>gcc(1)</code></a>
 locates the <a href="https://manpages.debian.org/libusb.3"><code>libusb(3)</code></a>
 headers via <code>/usr/include</code></li>
<li><a href="https://manpages.debian.org/man.1"><code>man(1)</code></a>
 locates the <a href="https://manpages.debian.org/nginx.1"><code>nginx(1)</code></a>
 manpage via <code>/usr/share/man</code>.</li>
<li><a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
 locates executable programs via <code>PATH</code> components such as <code>/bin</code></li>
</ul>
<p>In distri, these locations are called <strong>exchange directories</strong> and are provided
via FUSE in <code>/ro</code>.</p>
<p>Exchange directories come in two different flavors:</p>
<ol>
<li>
<p>global. The exchange directory, e.g. <code>/ro/share</code>, provides the union of the
<code>share</code> sub directory of all packages in the package store.
<br>
Global exchange directories are largely used for compatibility, <a href="#fhs-compat">see
below</a>.</p>
</li>
<li>
<p>per-package. Useful for tight coupling: e.g. <a href="https://manpages.debian.org/irssi.1"><code>irssi(1)</code></a>
 does not provide any ABI guarantees, so plugins such as <code>irssi-robustirc</code>
can declare that they want
e.g. <code>/ro/irssi-amd64-1.1.1-1/out/lib/irssi/modules</code> to be a per-package
exchange directory and contain files from their <code>lib/irssi/modules</code>.</p>
</li>
</ol>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> Only a few exchange directories are also available in the package build
environment (as opposed to run-time).</div>
  </div>
</aside>

<h4 id="search-paths-sometimes-need-to-be-fixed">Search paths sometimes need to be fixed</h4>
<p>Programs which use exchange directories sometimes use search paths to access
multiple exchange directories. In fact, the examples above were taken from <a href="https://manpages.debian.org/gcc.1"><code>gcc(1)</code></a>
’s <code>INCLUDEPATH</code>, <a href="https://manpages.debian.org/man.1"><code>man(1)</code></a>
’s <code>MANPATH</code> and <a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
’s <code>PATH</code>. These are
prominent ones, but more examples are easy to find: <a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
 loads completion functions from its <code>FPATH</code>.</p>
<p>Some search path values are derived from <code>--datadir=/ro/share</code> and require no
further attention, but others might derive from
e.g. <code>--prefix=/ro/zsh-amd64-5.6.2-3/out</code> and need to be pointed to an exchange
directory via a specific command line flag.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> To create the illusion of a writable search path at package
build-time, <code>$DESTDIR/ro/share</code> and <code>$DESTDIR/ro/lib</code> are
diverted to <code>$DESTDIR/$PREFIX/share</code> and
<code>$DESTDIR/$PREFIX/lib</code>, respectively.</div>
  </div>
</aside>

<h4 id="fhs-compat">FHS compatibility</h4>
<p>Global exchange directories are used to make distri provide enough of the
<a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">Filesystem Hierarchy Standard
(FHS)</a> that
third-party software largely just works. This includes a C development
environment.</p>
<p>I successfully ran a few programs from their binary packages such as Google
Chrome, Spotify, or Microsoft’s Visual Studio Code.</p>
<h3 id="fast-package-manager">Fast package manager</h3>
<p>I previously wrote about how <a href="/posts/2019-08-17-linux-package-managers-are-slow/">Linux distribution package managers are too slow</a>.</p>
<p>distri’s package manager is extremely fast. Its main bottleneck is typically the network link, even at high speed links (I tested with a 100 Gbps link).</p>
<p>Its speed comes largely from an architecture which allows the package manager to
do less work. Specifically:</p>
<ol>
<li>
<p>Package images can be added atomically to the package store, so we can safely
skip <a href="https://manpages.debian.org/fsync.2"><code>fsync(2)</code></a>
. Corruption will be cleaned up
automatically, and durability is not important: if an interactive
installation is interrupted, the user can just repeat it, as it will be fresh
on their mind.</p>
</li>
<li>
<p>Because all packages are co-installable thanks to separate hierarchies, there
are no conflicts at the package store level, and no dependency resolution (an
optimization problem requiring <a href="https://research.swtch.com/version-sat">SAT
solving</a>) is required at all.
<br>
In exchange directories, we resolve conflicts by selecting the package with the
highest monotonically increasing distri revision number.</p>
</li>
<li>
<p>distri proves that we can build a useful Linux distribution <a href="/posts/2019-07-20-hooks-and-triggers/">entirely without
hooks and triggers</a>. Not having to
serialize hook execution allows us to download packages into the package
store with maximum concurrency.</p>
</li>
<li>
<p>Because we are using images instead of archives, we do not need to unpack
anything. This means installing a package is really just writing its package
image and metadata to the package store. Sequential writes are typically the
fastest kind of storage usage pattern.</p>
</li>
</ol>
<p>Fast installation also make other use-cases more bearable, such as creating disk
images, be it for testing them in <a href="https://manpages.debian.org/qemu.1"><code>qemu(1)</code></a>
, booting
them on real hardware from a USB drive, or for cloud providers such as Google
Cloud.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> To saturate links above 1 Gbps, transfer packages without compression.</div>
  </div>
</aside>

<h3 id="fast-package-builder">Fast package builder</h3>
<p>Contrary to how distribution package builders are usually implemented, the
distri package builder does not actually install any packages into the build
environment.</p>
<p>Instead, distri makes available a filtered view of the package store (only
declared dependencies are available) at <code>/ro</code> in the build environment.</p>
<p>This means that even for large dependency trees, setting up a build environment
happens in a fraction of a second! Such a low latency really makes a difference
in how comfortable it is to iterate on distribution packages.</p>
<h3 id="package-stores">Package stores</h3>
<p>In distri, package images are installed from a remote <strong>package store</strong> into the
local system package store <code>/roimg</code>, which backs the <code>/ro</code> mount.</p>
<p>A package store is implemented as a directory of package images and their
associated metadata files.</p>
<p>You can easily make available a package store by using <code>distri export</code>.</p>
<p>To provide a mirror for your local network, you can periodically <code>distri update</code>
from the package store you want to mirror, and then <code>distri export</code> your local
copy. Special tooling (e.g. <code>debmirror</code> in Debian) is not required because
<code>distri install</code> is atomic (and <code>update</code> uses <code>install</code>).</p>
<p>Producing derivatives is easy: just add your own packages to a copy of the
package store.</p>
<p>The package store is intentionally kept simple to manage and distribute. Its
files could be exchanged via peer-to-peer file systems, or synchronized from an
offline medium.</p>
<h3 id="distris-first-release">distri’s first release</h3>
<p>distri works well enough to demonstrate the ideas explained above. I have
branched this state into <a href="https://github.com/distr1/distri/tree/jackherer">branch
<code>jackherer</code></a>, distri’s first
release code name. This way, I can keep experimenting in the distri repository
without breaking your installation.</p>
<p>From the branch contents, our autobuilder creates:</p>
<ol>
<li><a href="https://repo.distr1.org/distri/jackherer/img/">disk images</a>, which…</li>
</ol>
<ul>
<li>can be <a href="https://github.com/distr1/distri#run-distri-on-real-hardware">tested on real hardware</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-qemu">tested in qemu</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-virtualbox">tested in virtualbox</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-docker">tested in docker</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-on-google-cloud">tested on Google Cloud</a></li>
</ul>
<ol>
<li>
<p>a <a href="https://repo.distr1.org/distri/jackherer/pkg/">package repository</a>. Installations can pick up new packages with
<code>distri update</code>.</p>
</li>
<li>
<p><a href="https://repo.distr1.org/distri/jackherer/docs/">documentation for the release</a>.</p>
</li>
</ol>
<ul>
<li>Definitely check out the <a href="https://distr1.org/things-to-try/">“Cool things to
try”</a> README section.</li>
</ul>
<p>The project website can be found at <a href="https://distr1.org">https://distr1.org</a>. The website is just the
README for now, but we can improve that later.</p>
<p>The repository can be found at <a href="https://github.com/distr1/distri">https://github.com/distr1/distri</a></p>
<h3 id="project-outlook">Project outlook</h3>
<p>Right now, distri is mainly a vehicle for my spare-time Linux distribution
research. <strong>I don’t recommend anyone use distri for anything but research,</strong> and
there are no medium-term plans of that changing. At the very least, please
contact me before basing anything serious on distri so that we can talk about
limitations and expectations.</p>
<p>I expect the distri project to live for as long as I have blog posts to publish,
and we’ll see what happens afterwards. Note that this is a hobby for me: I will
continue to explore, at my own pace, parts that I find interesting.</p>
<p>My hope is that established distributions might get a useful idea or two from
distri.</p>
<h3 id="more-to-come">There’s more to come: subscribe to the distri feed</h3>
<p>I don’t want to make this post too long, but there is much more!</p>
<p>Please subscribe to the following URL in your feed reader to get all posts about
distri:</p>
<p><a href="https://michael.stapelberg.ch/posts/tags/distri/feed.xml">https://michael.stapelberg.ch/posts/tags/distri/feed.xml</a></p>
<p>Next in my queue are articles about hermetic packages and good package
maintainer experience (including declarative packaging).</p>
<h3 id="feedback-or-questions">Feedback or questions?</h3>
<p>I’d love to discuss these ideas in case you’re interested!</p>
<p>Please send feedback to the <a href="https://www.freelists.org/list/distri">distri mailing
list</a> so that everyone can participate!</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Which VCS do Debian’s Go package upstreams use?]]></title>
    <link href="https://michael.stapelberg.ch/posts/2017-10-22-pkg-go-upstreams/"/>
    <id>https://michael.stapelberg.ch/posts/2017-10-22-pkg-go-upstreams/</id>
    <published>2017-10-22T13:20:00+02:00</published>
    <content type="html"><![CDATA[<p>
  In the pkg-go team, we are currently discussing which workflows we should
  standardize on.
</p>
<p>
  One of the considerations is what goes into the “upstream” Git branch of our
  repositories: should it track the upstream Git repository, or should it
  contain orig tarball imports?
</p>
<p>
  Now, tracking the upstream Git repository only works if upstream actually uses
  Git. The go tool, which is widely used within the Go community for managing Go
  packages, supports Git, Mercurial, Bazaar and Subversion. But which of these
  are actually used in practice?
</p>
<p>
  Let’s find out!
</p>
<h3>Option 1: If you have the sources lists of all suites locally anyway</h3>
<pre>
/usr/lib/apt/apt-helper cat-file \
  $(apt-get indextargets --format '$(FILENAME)' 'ShortDesc: Sources' 'Origin: Debian') \
  | sed -n 's,Go-Import-Path: ,,gp' \
  | sort -u
</pre>
<h3>Option 2: If you prefer to use a relational database over textfiles</h3>
<p>
  This is the harder option, but also the more complete one.
</p>
<p>
  First, we’ll need the Go package import paths of all Go packages which are in
  Debian. We can get them from
  the <a href="https://wiki.debian.org/ProjectB">ProjectB</a> database, Debian’s
  main PostgreSQL database containing all of the state about the Debian archive.
</p>
<p>
  Unfortunately, only Debian Developers have SSH access to a mirror of ProjectB
  at the moment. I contacted DSA to ask about providing public ProjectB access.
</p>
<pre>
  ssh mirror.ftp-master.debian.org "echo \"SELECT value FROM source_metadata \
  LEFT JOIN metadata_keys ON (source_metadata.key_id = metadata_keys.key_id) \
  WHERE metadata_keys.key = 'Go-Import-Path' GROUP BY value\" | \
    psql -A -t service=projectb" > go_import_path.txt
</pre>
<p>
  I
  uploaded <a href="https://people.debian.org/~stapelberg/2017-10-22-go_import_path.txt">a
  copy of resulting <code>go_import_path.txt</code></a>, if you’re curious.
</p>
<p>
  Now, let’s come up with a little bit of Go to print the VCS responsible for
  each specified Go import path:
</p>
<pre>
go get -u golang.org/x/tools/go/vcs
cat >vcs4.go <<'EOT'
package main

import (
	"fmt"
	"log"
	"os"
	"sync"

	"golang.org/x/tools/go/vcs"
)

func main() {
	var wg sync.WaitGroup
	for _, arg := range os.Args[1:] {
		wg.Add(1)
		go func(arg string) {
			defer wg.Done()
			rr, err := vcs.RepoRootForImportPath(arg, false)
			if err != nil {
				log.Println(err)
				return
			}
			fmt.Println(rr.VCS.Name)
		}(arg)
	}
	wg.Wait()
}
EOT
</pre>
<p>
  Lastly, run it in combination
  with <a href="https://manpages.debian.org/stretch/coreutils/uniq.1"><code>uniq(1)</code></a>
  to discover…
</p>
<pre>
go run vcs4.go $(tr '\n' ' ' < go_import_path.txt) | sort | uniq -c
    760 Git
      1 Mercurial
</pre>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Why Go is my favorite programming language]]></title>
    <link href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/"/>
    <id>https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/</id>
    <published>2017-08-19T13:00:00+02:00</published>
    <content type="html"><![CDATA[<p>I strive to respect everybody’s personal preferences, so I usually steer clear
of debates about which is the best programming language, text editor or
operating system.</p>
<p>However, recently I was asked a couple of times why I like and use a lot of <a
href="https://golang.org">Go</a>, so here is a coherent article to fill in the
blanks of my ad-hoc in-person ramblings :-).</p>
<h2 id="my-background">My background</h2>
<p>I have used C and Perl for a number of decently sized projects. I have written
programs in Python, Ruby, C++, CHICKEN Scheme, Emacs Lisp, Rust and Java (for
Android only). I understand a bit of Lua, PHP, Erlang and Haskell. In a previous
life, I developed a number of programs using
<a href="https://en.wikipedia.org/wiki/Delphi_(programming_language)">Delphi</a>.</p>
<p>I had a brief look at Go in 2009, when it was first released. I seriously
started using the language when Go 1.0 was released in 2012, featuring the <a href="https://golang.org/doc/go1compat">Go 1
compatibility guarantee</a>. I still have
<a href="https://github.com/stapelberg/greetbot">code</a> running in production which I
authored in 2012, largely untouched.</p>
<h2 id="1-clarity">1. Clarity</h2>
<h3 id="formatting">Formatting</h3>
<p>Go code, by convention, is formatted using the
<a href="https://golang.org/cmd/gofmt/"><code>gofmt</code></a> tool. Programmatically formatting code
is not a new idea, but contrary to its predecessors, <code>gofmt</code> supports precisely
one canonical style.</p>
<p>Having all code formatted the same way makes reading code easier; the code feels
familiar. This helps not only when reading the standard library or Go compiler,
but also when working with many code bases — think Open Source, or big
companies.</p>
<p>Further, auto-formatting is a huge time-saver during code reviews, as it
eliminates an entire dimension in which code could be reviewed before: now, you
can just let your continuous integration system verify that <code>gofmt</code> produces no
diffs.</p>
<p>Interestingly enough, having my editor apply <code>gofmt</code> when saving a file has
changed the way I write code. I used to attempt to match what the formatter
would enforce, then have it correct my mistakes. Nowadays, I express my thought
as quickly as possible and trust <code>gofmt</code> to make it pretty
(<a href="https://play.golang.org/p/I6GJwiT77v">example</a> of what I would type, click
Format).</p>
<h3 id="high-quality-code">High-quality code</h3>
<p>I use the standard library (<a href="https://golang.org/pkg/">docs</a>,
<a href="https://github.com/golang/go/tree/master/src">source</a>) quite a bit, see below.</p>
<p>All standard library code which I have read so far was of extremely high quality.</p>
<p>One example is the <a href="https://golang.org/pkg/image/jpeg/"><code>image/jpeg</code></a> package: I
didn’t know how JPEG worked at the time, but it was easy to pick up by switching
between the <a href="https://en.wikipedia.org/wiki/JPEG">Wikipedia JPEG article</a> and the
<code>image/jpeg</code> code. If the package had a few more comments, I would qualify it as
a teaching implementation.</p>
<h3 id="opinions">Opinions</h3>
<p>I have come to agree with many opinions the Go community holds, such as:</p>
<ul>
<li><a href="https://github.com/golang/go/wiki/CodeReviewComments#variable-names">Variable names</a> should be short by default, and become more descriptive the further from its declaration a name is used.</li>
<li>Keep the dependency tree small (to a reasonable degree): <a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&amp;t=9m28s">a little copying is better than a little dependency</a></li>
<li>There is a cost to introducing an abstraction layer. Go code is usually rather clear, at the cost of being a bit repetitive at times.</li>
<li>See <a href="https://github.com/golang/go/wiki/CodeReviewComments">CodeReviewComments</a> and <a href="https://go-proverbs.github.io/">Go Proverbs</a> for more.</li>
</ul>
<h3 id="few-keywords-and-abstraction-layers">Few keywords and abstraction layers</h3>
<p>The Go specification lists only <a href="https://golang.org/ref/spec#Keywords">25
keywords</a>, which I can easily keep in my
head.</p>
<p>The same is true for <a href="https://golang.org/pkg/builtin/">builtin functions</a> and
<a href="https://golang.org/ref/spec#Types">types</a>.</p>
<p>In my experience, the small number of abstraction layers and concepts makes the
language easy to pick up and quickly feel comfortable in.</p>
<p>While we’re talking about it: I was surprised about how readable the <a href="https://golang.org/ref/spec">Go
specification</a> is. It really seems to target
programmers (rather than standards committees?).</p>
<h2 id="2-speed">2. Speed</h2>
<h3 id="quick-feedback--low-latency">Quick feedback / low latency</h3>
<p>I love quick feedback: I appreciate websites which load quickly, I prefer fluent
User Interfaces which don’t lag, and I will choose a quick tool over a more
powerful tool any day. <a href="https://blog.gigaspaces.com/amazon-found-every-100ms-of-latency-cost-them-1-in-sales/">The
findings</a>
of large web properties confirm that this behavior is shared by many.</p>
<p>The authors of the Go compiler respect my desire for low latency: compilation
speed matters to them, and new optimizations are carefully weighed against
whether they will slow down compilation.</p>
<p>A friend of mine had not used Go before. After installing the
<a href="https://robustirc.net">RobustIRC</a> bridge using <code>go get</code>, they concluded that Go
must be an interpreted language and I had to correct them: no, the Go compiler
just is that fast.</p>
<p>Most Go tools are no exception, e.g. <code>gofmt</code> or <code>goimports</code> are blazingly fast.</p>
<h3 id="maximum-resource-usage">Maximum resource usage</h3>
<p>For batch applications (as opposed to interactive applications), utilizing the
available resources to their fullest is usually more important than low latency.</p>
<p>It is delightfully easy to profile and change a Go program to utilize all
available IOPS, network bandwidth or compute. As an example, I wrote about
<a href="https://people.debian.org/~stapelberg/2014/01/17/debmirror-rackspace.html">filling a 1 Gbps
link</a>,
and optimized <a href="https://github.com/Debian/debiman/">debiman</a> to utilize all
available resources, reducing its runtime by hours.</p>
<h2 id="3-rich-standard-library">3. Rich standard library</h2>
<p>The <a href="https://golang.org/pkg">Go standard library</a> provides means to effectively
use common communications protocols and data storage formats/mechanisms, such as
TCP/IP, HTTP, JPEG, SQL, …</p>
<p>Go’s standard library is the best one I have ever seen. I perceive it as
well-organized, clean, small, yet comprehensive: I often find it possible to
write reasonably sized programs with just the standard library, plus one or two
external packages.</p>
<p>Domain-specific data types and algorithms are (in general) not included and live
outside the standard library,
e.g. <a href="https://godoc.org/golang.org/x/net/html"><code>golang.org/x/net/html</code></a>. The
<code>golang.org/x</code> namespace also serves as a staging area for new code before it
enters the standard library: the Go 1 compatibility guarantee precludes any
breaking changes, even if they are clearly worthwhile. A prominent example is
<code>golang.org/x/crypto/ssh</code>, which had to break existing code to <a href="https://github.com/golang/crypto/commit/e4e2799dd7aab89f583e1d898300d96367750991">establish a more
secure
default</a>.</p>
<h2 id="4-tooling">4. Tooling</h2>
<p>To download, compile, install and update Go packages, I use the <code>go get</code> tool.</p>
<p>All Go code bases I have worked with use the built-in
<a href="https://golang.org/pkg/testing/"><code>testing</code></a> facilities. This results not only
in easy and fast testing, but also in <a href="https://blog.golang.org/cover">coverage
reports</a> being readily available.</p>
<p>Whenever a program uses more resources than expected, I fire up <code>pprof</code>. See
this <a href="https://blog.golang.org/profiling-go-programs">golang.org blog post about
<code>pprof</code></a> for an introduction, or
<a href="https://people.debian.org/~stapelberg/2014/12/23/code-search-taming-the-latency-tail.html">my blog post about optimizing Debian Code
Search</a>. After
importing the <a href="https://golang.org/pkg/net/http/pprof/"><code>net/http/pprof</code>
package</a>, you can profile your server
while it’s running, without recompilation or restarting.</p>
<p>Cross-compilation is as easy as setting the <code>GOARCH</code> environment variable,
e.g. <code>GOARCH=arm64</code> for targeting the Raspberry Pi 3. Notably, tools just work
cross-platform, too! For example, I can profile <a href="https://gokrazy.org">gokrazy</a>
from my amd64 computer: <code>go tool pprof ~/go/bin/linux_arm64/dhcp http://gokrazy:3112/debug/pprof/heap</code>.</p>
<p><a href="https://godoc.org/golang.org/x/tools/cmd/godoc">godoc</a> displays documentation
as plain text or serves it via HTTP. <a href="https://godoc.org">godoc.org</a> is a public
instance, but I run a local one to use while offline or for not yet published
packages.</p>
<p>Note that these are standard tools coming with the language. Coming from C, each
of the above would be a significant feat to accomplish. In Go, we take them for
granted.</p>
<h2 id="getting-started">Getting started</h2>
<p>Hopefully I was able to convey why I’m happy working with Go.</p>
<p>If you’re interested in getting started with Go, check out <a href="https://github.com/gopheracademy/gopher/blob/1cdbcd9fc3ba58efd628d4a6a552befc8e3912be/bot/bot.go#L516">the beginner’s
resources</a>
we point people to when they join the Gophers slack channel. See
<a href="https://golang.org/help/">https://golang.org/help/</a>.</p>
<h2 id="caveats">Caveats</h2>
<p>Of course, no programming tool is entirely free of problems. Given that this
article explains why Go is my favorite programming language, it focuses on the
positives. I will mention a few issues in passing, though:</p>
<ul>
<li>If you use Go packages which don’t offer a stable API, you might want to use a specific, known-working version. Your best bet is the <a href="https://github.com/golang/dep">dep</a> tool, which is not part of the language at the time of writing.</li>
<li>Idiomatic Go code does not necessarily translate to the highest performance machine code, and the runtime comes at a (small) cost. In the rare cases where I found performance lacking, I successfully resorted to <a href="https://golang.org/cmd/cgo/">cgo</a> or assembler. If your domain is hard-realtime applications or otherwise extremely performance-critical code, your mileage may vary, though.</li>
<li>I wrote that the Go standard library is the best I have ever seen, but that doesn’t mean it doesn’t have any problems. One example is <a href="https://golang.org/issues/20744">complicated handling of comments</a> when modifying Go code programmatically via one of the standard library’s oldest packages, <code>go/ast</code>.</li>
</ul>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Atomically writing files in Go]]></title>
    <link href="https://michael.stapelberg.ch/posts/2017-01-28-golang_atomically_writing/"/>
    <id>https://michael.stapelberg.ch/posts/2017-01-28-golang_atomically_writing/</id>
    <published>2017-01-28T21:29:00+00:00</published>
    <content type="html"><![CDATA[<p>
<strong>NOTE </strong> that the documented assumptions about fsync skipping are incorrect in the code below. Prefer using <a href="https://github.com/google/renameio">the renameio package</a>.
</p>
<p>
Writing files is simple, but correctly writing files atomically in a performant
way might not be as trivial as one might think. Here’s an extensively commented
function to atomically write compressed files (taken from <a
href="https://github.com/Debian/debiman">debiman</a>, the software behind <a
href="https://manpages.debian.org/">manpages.debian.org</a>):
<p>
<pre>
package main

import (
    "bufio"
    "compress/gzip"
    "io"
    "io/ioutil"
    "log"
    "os"
    "path/filepath"
)

func tempDir(dest string) string {
    tempdir := os.Getenv("TMPDIR")
    if tempdir == "" {
        // Convenient for development: decreases the chance that we
        // cannot move files due to TMPDIR being on a different file
        // system than dest.
        tempdir = filepath.Dir(dest)
    }
    return tempdir
}

func writeAtomically(dest string, compress bool, write func(w io.Writer) error) (err error) {
    f, err := ioutil.TempFile(tempDir(dest), "atomic-")
    if err != nil {
        return err
    }
    defer func() {
        // Clean up (best effort) in case we are returning with an error:
        if err != nil {
            // Prevent file descriptor leaks.
            f.Close()
            // Remove the tempfile to avoid filling up the file system.
            os.Remove(f.Name())
        }
    }()

    // Use a buffered writer to minimize write(2) syscalls.
    bufw := bufio.NewWriter(f)

    w := io.Writer(bufw)
    var gzipw *gzip.Writer
    if compress {
        // NOTE: gzip’s decompression phase takes the same time,
        // regardless of compression level. Hence, we invest the
        // maximum CPU time once to achieve the best compression.
        gzipw, err = gzip.NewWriterLevel(bufw, gzip.BestCompression)
        if err != nil {
            return err
        }
        defer gzipw.Close()
        w = gzipw
    }

    if err := write(w); err != nil {
        return err
    }

    if compress {
        if err := gzipw.Close(); err != nil {
            return err
        }
    }

    if err := bufw.Flush(); err != nil {
        return err
    }

    // Chmod the file world-readable (ioutil.TempFile creates files with
    // mode 0600) before renaming.
    if err := f.Chmod(0644); err != nil {
        return err
    }

    // fsync(2) after fchmod(2) orders writes as per
    // https://lwn.net/Articles/270891/. Can be skipped for performance
    // for idempotent applications (which only ever atomically write new
    // files and tolerate file loss) on an ordered file systems. ext3,
    // ext4, XFS, Btrfs, ZFS are ordered by default.
    f.Sync()

    if err := f.Close(); err != nil {
        return err
    }

    return os.Rename(f.Name(), dest)
}

func main() {
    if err := writeAtomically("demo.txt.gz", true, func(w io.Writer) error {
        _, err := w.Write([]byte("demo"))
        return err
    }); err != nil {
        log.Fatal(err)
    }
}
</pre>
<p>
<a href="https://manpages.debian.org/rsync.1">rsync(1)</a> will fail when it
lacks permission to read files. Hence, if you are synchronizing a repository of
files while updating it, you’ll need to set <code>TMPDIR</code> to point to a
directory on the same file system (for <a
href="https://manpages.debian.org/rename.2">rename(2)</a> to work) which is not
covered by your <a href="https://manpages.debian.org/rsync.1">rsync(1)</a>
invocation.
</p>
<p>
When calling <code>writeAtomically</code> repeatedly to create lots of small
files, you’ll notice that creating <code>gzip.Writer</code>s is actually rather
expensive. Modifying the function to re-use the same <code>gzip.Writer</code>
<a
href="https://github.com/Debian/debiman/commit/2f891341daa6c2b24dc9b0bacd3b722b057d8e9b">yielded
a significant decrease in wall-clock time</a>.
</p>
<p>
Of course, if you’re looking for maximum write performance (as opposed to
minimum resulting file size), you should use a different gzip level than
<code>gzip.BestCompression</code>.
</p>
]]></content>
  </entry>
</feed>
