<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bkaznowski.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bkaznowski.github.io/" rel="alternate" type="text/html" /><updated>2026-07-14T18:16:33+00:00</updated><id>https://bkaznowski.github.io/feed.xml</id><title type="html">Pace &amp;amp; Push</title><subtitle>Notes on tech and running.</subtitle><author><name>Bartosz Thomas Kaznowski</name></author><entry><title type="html">First Long Run of the Season</title><link href="https://bkaznowski.github.io/running/2026/07/10/first-long-run/" rel="alternate" type="text/html" title="First Long Run of the Season" /><published>2026-07-10T00:00:00+00:00</published><updated>2026-07-10T00:00:00+00:00</updated><id>https://bkaznowski.github.io/running/2026/07/10/first-long-run</id><content type="html" xml:base="https://bkaznowski.github.io/running/2026/07/10/first-long-run/"><![CDATA[<p>Notes from today’s long run — distance, pace, how the legs felt, and what’s next.</p>

<h2 id="splits">Splits</h2>

<table>
  <thead>
    <tr>
      <th>Mile</th>
      <th>Pace</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>8:45</td>
    </tr>
    <tr>
      <td>2</td>
      <td>8:40</td>
    </tr>
    <tr>
      <td>3</td>
      <td>8:38</td>
    </tr>
  </tbody>
</table>

<h2 id="takeaways">Takeaways</h2>

<p>Replace this with real notes — route, weather, effort level, and anything to adjust for next week’s long run.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="running" /><category term="training" /><summary type="html"><![CDATA[Notes from today’s long run — distance, pace, how the legs felt, and what’s next.]]></summary></entry><entry><title type="html">The Bug Only a Real Postgres Server Could Find</title><link href="https://bkaznowski.github.io/tech/2026/07/08/the-bug-only-postgres-could-find/" rel="alternate" type="text/html" title="The Bug Only a Real Postgres Server Could Find" /><published>2026-07-08T00:00:00+00:00</published><updated>2026-07-08T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/07/08/the-bug-only-postgres-could-find</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/07/08/the-bug-only-postgres-could-find/"><![CDATA[<p>Every test I had — <code class="language-plaintext highlighter-rouge">pkg/vdso</code>, <code class="language-plaintext highlighter-rouge">pkg/procmem</code>, <code class="language-plaintext highlighter-rouge">pkg/trampoline</code>, <code class="language-plaintext highlighter-rouge">pkg/inject</code>, <code class="language-plaintext highlighter-rouge">pkg/faketime</code>, the end-to-end injection test that checks a target prints timestamps 24 hours ahead — passed. All of it, consistently, for weeks. Then I ran the test that actually mattered: a simulated end-of-day pipeline, ledger service in front, Postgres behind it, <code class="language-plaintext highlighter-rouge">StartWithTracking</code> following the connection-per-backend forks so every backend inherited the fake clock. Advance the clock a day, have the ledger service write the day’s closing entries, then check what Postgres actually recorded for <code class="language-plaintext highlighter-rouge">now()</code> on those rows.</p>

<p>It recorded the real time. Not close to the fake time, not off by a rounding error — the actual, unmodified wall clock, on the very first write, as if injection hadn’t happened at all. In a real system, that’s not a cosmetic bug — it’s every end-of-day ledger entry silently timestamped as having happened on the wrong day, in the one place (the datastore of record) where that’s the hardest kind of wrong to notice after the fact.</p>

<h3 id="reproducing-it-before-assuming-docker-was-the-problem">Reproducing it before assuming Docker was the problem</h3>

<p>My first instinct was to suspect the environment — I was running this in a container with <code class="language-plaintext highlighter-rouge">--cap-add SYS_PTRACE</code>, and containers are where ptrace-adjacent things go to break in surprising ways. I ruled that out directly: same result with <code class="language-plaintext highlighter-rouge">--privileged</code>, no cgroup or namespace change made any difference, and — more convincingly — every test I already had, including ones exercising the exact same <code class="language-plaintext highlighter-rouge">ChildTracker</code>/fork-following machinery from the previous post against fanned-out Go workers, was green throughout. Whatever this was, it wasn’t Docker, and it wasn’t the fork-tracking logic. It was specific to this one target in the pipeline.</p>

<h3 id="finding-the-actual-gap">Finding the actual gap</h3>

<p>The thing that was different about Postgres wasn’t that it forked (my tests already covered fan-out workers) — it’s that it wasn’t a Go binary. Every test I’d written up to this point injected into Go processes, either <code class="language-plaintext highlighter-rouge">test/targets/clockprinter</code> or Go stand-ins for the services in the pipeline. Go’s runtime reads the wall clock through <code class="language-plaintext highlighter-rouge">clock_gettime</code>. Postgres’s C code doesn’t; <code class="language-plaintext highlighter-rouge">GetCurrentTimestamp()</code> — backing <code class="language-plaintext highlighter-rouge">now()</code>, and therefore backing every timestamp the ledger’s closing entries would have gotten — calls <code class="language-plaintext highlighter-rouge">gettimeofday</code>.</p>

<p>I went back to the vDSO discovery code and actually checked, with <code class="language-plaintext highlighter-rouge">readelf -sD</code> on a dumped vDSO, exactly what <code class="language-plaintext highlighter-rouge">clock_gettime</code>, <code class="language-plaintext highlighter-rouge">gettimeofday</code>, and <code class="language-plaintext highlighter-rouge">time</code> are. I’d been treating them as effectively the same thing wearing different names — three ways of asking the kernel “what time is it,” so surely patching one covers all three.</p>

<p>They aren’t the same thing. They’re three independent compiled functions, at three different addresses in the vDSO, each with its own entry point. My injector patched exactly one of them — <code class="language-plaintext highlighter-rouge">clock_gettime</code> — with the JMP that redirects into my trampoline. <code class="language-plaintext highlighter-rouge">gettimeofday</code> and <code class="language-plaintext highlighter-rouge">time</code> were completely untouched, on every single injection I had ever run. Any caller going through <code class="language-plaintext highlighter-rouge">clock_gettime</code> — which included every Go service in my pipeline tests, since that’s what the Go runtime uses — saw the fake time perfectly, which is exactly why the ledger service itself correctly believed the day had ended. Any caller going through <code class="language-plaintext highlighter-rouge">gettimeofday</code> — Postgres, and as I found while checking, glibc and bash’s own wall-clock reads like <code class="language-plaintext highlighter-rouge">$EPOCHREALTIME</code> too — saw completely real time, silently, with no error of any kind. The service deciding <em>whether</em> to close the day was fooled correctly; the datastore recording <em>when</em> it closed the day was not — and nothing in the pipeline would have told you the two disagreed unless you went looking.</p>

<h3 id="the-fix-and-why-it-isnt-three-times-the-previous-work">The fix, and why it isn’t three times the previous work</h3>

<p>The trampoline already had the right shape for this — one shared state struct, read by a stub. The fix was writing two more stubs of the same shape:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">gettimeofday_entry</code></strong> — same structure as the <code class="language-plaintext highlighter-rouge">clock_gettime</code> stub, adding the offset and writing back a <code class="language-plaintext highlighter-rouge">timeval</code> (microseconds) instead of a <code class="language-plaintext highlighter-rouge">timespec</code> (nanoseconds) — <code class="language-plaintext highlighter-rouge">offsetNsec / 1000</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">time_entry</code></strong> — this one had its own trap. My first version called the real <code class="language-plaintext highlighter-rouge">time()</code> syscall internally and added the offset to the result. But the <code class="language-plaintext highlighter-rouge">time()</code> syscall only returns whole seconds — it discards the real fractional second before my stub ever sees it — so <code class="language-plaintext highlighter-rouge">time_entry</code> could disagree with the other two by up to a full second depending on where in the second the real call landed. For a ledger entry, a one-second disagreement between the service’s timestamp and the database’s timestamp is exactly the kind of thing that turns into a confusing off-by-one-second ordering bug in an audit trail. The fix was to have <code class="language-plaintext highlighter-rouge">time_entry</code> call <code class="language-plaintext highlighter-rouge">clock_gettime(CLOCK_REALTIME)</code> internally instead, apply the identical carry-normalized offset add the other two stubs use, and only then truncate to whole seconds. Now all three agree to the second, always.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">pkg/vdso.Locate</code> grew resolution for all three symbols instead of one, <code class="language-plaintext highlighter-rouge">pkg/inject</code> now performs three JMP patches instead of one during the injection sequence, and all three stubs share the same 32-byte state struct — one <code class="language-plaintext highlighter-rouge">SetTime</code>, <code class="language-plaintext highlighter-rouge">Freeze</code>, or <code class="language-plaintext highlighter-rouge">Advance</code> call updates the fake time for all three functions in a target process at once, keeping the ledger service and its datastore consistent with each other by construction rather than by convention.</p>

<p>While I had the vDSO symbol table open, I fixed two smaller things in the same pass: <code class="language-plaintext highlighter-rouge">clock_gettime_entry</code> now also catches <code class="language-plaintext highlighter-rouge">CLOCK_REALTIME_COARSE</code> (same wall clock, lower precision, a distinct clock ID some callers request), and I confirmed — by dumping the full vDSO symbol table, not just the three I already knew about — that <code class="language-plaintext highlighter-rouge">clock_getres</code>, <code class="language-plaintext highlighter-rouge">getcpu</code>, and <code class="language-plaintext highlighter-rouge">__vdso_sgx_enter_enclave</code> are the only other exports on x86-64, and none of them read current time. Three wall-clock functions really is the complete list.</p>

<h3 id="why-this-needed-a-real-pipeline-to-find-not-a-better-unit-test">Why this needed a real pipeline to find, not a better unit test</h3>

<p>I could have written a unit test asserting <code class="language-plaintext highlighter-rouge">gettimeofday</code> gets patched, once I knew to ask the question. What I couldn’t have done is <em>think of the question</em> from inside my own test suite, because every test I’d written was, by construction, exercising services I controlled — and I’d only ever written Go test targets standing in for them. The bug wasn’t a logic error I could have caught by trying harder at the same kind of test; it was a wrong assumption (<code class="language-plaintext highlighter-rouge">clock_gettime</code> and <code class="language-plaintext highlighter-rouge">gettimeofday</code> are basically the same call) that no amount of testing against Go binaries would ever surface, because Go binaries never call the path where the assumption was wrong. A pipeline test that only ever used Go stand-ins for “the database” would have stayed green forever, confidently testing the wrong thing.</p>

<p>That’s the real argument for testing a distributed pipeline against its actual dependencies before trusting the fake-time story end to end: not “more tests,” but a fundamentally different, non-Go caller in the loop, exercising a code path your own assumptions never thought to route around. <code class="language-plaintext highlighter-rouge">pg-faketime-test</code> — a separate project running a real <code class="language-plaintext highlighter-rouge">initdb</code>/<code class="language-plaintext highlighter-rouge">postgres</code> cluster under <code class="language-plaintext highlighter-rouge">WithChildTracker</code> and asserting <code class="language-plaintext highlighter-rouge">SELECT now()</code> tracks the faked clock — is what actually caught this, and it’s stayed in the loop since as the check that a Go-only pipeline test structurally cannot replicate.</p>

<p>This is also, for now, where the series pauses. <code class="language-plaintext highlighter-rouge">pkg/faketime</code> covers testing a pipeline like this end to end on a single machine; the other half of <code class="language-plaintext highlighter-rouge">epochd</code> — turning the same injection mechanism into a Kubernetes-native agent and controller for shifting time across whole pods in a real cluster — is its own story, for another time.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="go" /><category term="postgres" /><category term="vdso" /><category term="testing" /><category term="distributed-systems" /><summary type="html"><![CDATA[Every test I had — pkg/vdso, pkg/procmem, pkg/trampoline, pkg/inject, pkg/faketime, the end-to-end injection test that checks a target prints timestamps 24 hours ahead — passed. All of it, consistently, for weeks. Then I ran the test that actually mattered: a simulated end-of-day pipeline, ledger service in front, Postgres behind it, StartWithTracking following the connection-per-backend forks so every backend inherited the fake clock. Advance the clock a day, have the ledger service write the day’s closing entries, then check what Postgres actually recorded for now() on those rows.]]></summary></entry><entry><title type="html">Following Forked Children</title><link href="https://bkaznowski.github.io/tech/2026/07/05/following-forked-children/" rel="alternate" type="text/html" title="Following Forked Children" /><published>2026-07-05T00:00:00+00:00</published><updated>2026-07-05T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/07/05/following-forked-children</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/07/05/following-forked-children/"><![CDATA[<p>Everything up to this point assumes injecting into a process means injecting into <em>one</em> process, once, at startup. That assumption is fine for a lot of services in a pipeline. It falls apart against two very ordinary shapes of program: a batch job that fans out — forking a worker process per account, per customer, per shard, to parallelize the end-of-day run — and, underneath almost any real pipeline, the datastore itself. PostgreSQL forks a brand-new backend process for every client connection. Inject fake time into the postmaster and open a connection to write the day’s ledger entries, and the actual work — the backend process handling your query — is running code that was never injected, because it didn’t exist yet when injection happened.</p>

<p>The fix has to be automatic. A test asserting an end-of-day batch job’s fanned-out workers all agree on the simulated cutover instant shouldn’t need to know, or care, that each worker is a process that came into existence <em>after</em> the fake clock was set. So <code class="language-plaintext highlighter-rouge">pkg/faketime</code> grew a mode that watches the process tree and injects into new children as they appear, with no action required from the caller beyond opting in.</p>

<h3 id="watching-for-fork-and-exec-with-ptrace-options">Watching for fork and exec with ptrace options</h3>

<p>Linux’s ptrace has options for exactly this, set via <code class="language-plaintext highlighter-rouge">PTRACE_SETOPTIONS</code>:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">PTRACE_O_TRACEFORK</code></strong> (and <code class="language-plaintext highlighter-rouge">TRACEVFORK</code>) — when the tracee calls <code class="language-plaintext highlighter-rouge">fork</code>/<code class="language-plaintext highlighter-rouge">vfork</code>, the tracer gets a stop event carrying the new child’s PID, and the child starts life already ptrace-stopped, before it runs a single instruction.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">PTRACE_O_TRACEEXEC</code></strong> — when the tracee calls <code class="language-plaintext highlighter-rouge">exec</code>, the tracer gets a stop event <em>before</em> the new program image starts running.</li>
</ul>

<p>Both matter here for different reasons. <code class="language-plaintext highlighter-rouge">TRACEFORK</code> is the direct answer to fan-out workers and to Postgres’s connection-per-backend model — a new process appears mid-pipeline, and I need to inject before it does any work, including the very first clock read it makes to decide what “today” is. <code class="language-plaintext highlighter-rouge">TRACEEXEC</code> covers a related case: a batch-job launcher that re-execs itself into the actual worker binary, or forks and then immediately execs a different program. Without catching the exec event, an injection performed before the exec would just get thrown away — the new program image replaces the entire address space, vDSO included, so the trampoline page and the JMP patch are both gone, along with everything else the old process had mapped. A worker that silently reverted to the real wall clock mid-fan-out is exactly the kind of bug that would show up as one shard’s ledger entries landing on the wrong day.</p>

<h3 id="childtracker-the-piece-that-owns-keep-injecting-as-things-appear">ChildTracker: the piece that owns “keep injecting as things appear”</h3>

<p>This became <code class="language-plaintext highlighter-rouge">ChildTracker</code>, reachable through <code class="language-plaintext highlighter-rouge">StartWithTracking</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ct</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">faketime</span><span class="o">.</span><span class="n">StartWithTracking</span><span class="p">(</span><span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"./end-of-day-batch"</span><span class="p">),</span> <span class="n">target</span><span class="p">)</span>
<span class="k">defer</span> <span class="n">ct</span><span class="o">.</span><span class="n">Reset</span><span class="p">()</span>
<span class="k">defer</span> <span class="n">ct</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

<span class="c">// advances the launcher AND every currently-tracked forked worker in one call</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">ct</span><span class="o">.</span><span class="n">Advance</span><span class="p">(</span><span class="m">24</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">ct</span><span class="o">.</span><span class="n">PIDs</span><span class="p">())</span>                       <span class="c">// launcher PID first, then workers</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">ct</span><span class="o">.</span><span class="n">Handle</span><span class="o">.</span><span class="n">EffectiveTime</span><span class="p">())</span>        <span class="c">// same accessor pattern as a plain Handle</span>
</code></pre></div></div>

<p>Internally, a background goroutine calls a non-blocking wait (<code class="language-plaintext highlighter-rouge">WaitAnyNonBlocking</code> in <code class="language-plaintext highlighter-rouge">pkg/procmem</code>) in a loop, watching for fork/exec stop events from any tracked PID. On a fork event it reads the new child’s PID via <code class="language-plaintext highlighter-rouge">PTRACE_GETEVENTMSG</code>, arms the same tracer options on it, injects the same fake-time state, and adds it to the tracked set — so a worker forked to handle shard 47 sees exactly the same simulated end-of-day instant as the launcher and every other shard’s worker. On an exec event it re-injects into the same PID, since the exec wiped out whatever was there before.</p>

<p><code class="language-plaintext highlighter-rouge">Session</code> gained the equivalent as an option rather than a separate type — <code class="language-plaintext highlighter-rouge">faketime.NewSession(target, faketime.WithTracking())</code> — because the same need (auto-inject into anything that appears, across every process in the pipeline) applies just as much to a multi-service <code class="language-plaintext highlighter-rouge">Session</code> as to a single tracked launcher.</p>

<h3 id="the-failure-mode-that-shaped-isalive-and-prune">The failure mode that shaped <code class="language-plaintext highlighter-rouge">IsAlive</code> and <code class="language-plaintext highlighter-rouge">Prune</code></h3>

<p>Tracking children well surfaces a problem that a single untracked process never has: forked workers, and forked database backends, die on their own, all the time, as a completely normal part of the pipeline finishing its work. Every worker that finishes its shard exits. Every client that disconnects from Postgres ends its backend process. A <code class="language-plaintext highlighter-rouge">SetTime</code> call that assumes every tracked PID is still alive will hit <code class="language-plaintext highlighter-rouge">ESRCH</code> (“no such process”) the moment it reaches a handle for a worker that finished five seconds ago — right when a test is trying to advance the clock again for the <em>next</em> simulated day.</p>

<p>I didn’t want callers to have to defensively check <code class="language-plaintext highlighter-rouge">IsAlive()</code> before every clock update just to avoid a spurious error from a worker that was <em>supposed</em> to exit. So <code class="language-plaintext highlighter-rouge">applyAll</code> — the internal helper both <code class="language-plaintext highlighter-rouge">Session</code> and <code class="language-plaintext highlighter-rouge">ChildTracker</code> route bulk updates through — silently drops any handle that returns <code class="language-plaintext highlighter-rouge">ESRCH</code>, and <code class="language-plaintext highlighter-rouge">Session.Prune()</code> exists for callers who want to know the count without waiting for the next update to discover it. <code class="language-plaintext highlighter-rouge">IsAlive()</code> from a couple posts back turned out to be exactly the primitive this needed underneath, and <code class="language-plaintext highlighter-rouge">IsFrozen()</code> followed the same pattern once assertions in tests started wanting to check current mode, not just current time.</p>

<p>With fanned-out workers and dead handles handled gracefully, the theory was that <code class="language-plaintext highlighter-rouge">pkg/faketime</code> was ready for its actual target: a real end-to-end run against a real Postgres cluster backing the ledger, not a synthetic Go test binary standing in for it. It mostly was. The part that wasn’t is the subject of the next post, and it’s the bug I’m least proud of and most glad was found before it silently corrupted a test’s idea of when something happened.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="go" /><category term="linux" /><category term="ptrace" /><category term="distributed-systems" /><summary type="html"><![CDATA[Everything up to this point assumes injecting into a process means injecting into one process, once, at startup. That assumption is fine for a lot of services in a pipeline. It falls apart against two very ordinary shapes of program: a batch job that fans out — forking a worker process per account, per customer, per shard, to parallelize the end-of-day run — and, underneath almost any real pipeline, the datastore itself. PostgreSQL forks a brand-new backend process for every client connection. Inject fake time into the postmaster and open a connection to write the day’s ledger entries, and the actual work — the backend process handling your query — is running code that was never injected, because it didn’t exist yet when injection happened.]]></summary></entry><entry><title type="html">Designing the faketime API</title><link href="https://bkaznowski.github.io/tech/2026/06/26/designing-the-faketime-api/" rel="alternate" type="text/html" title="Designing the faketime API" /><published>2026-06-26T00:00:00+00:00</published><updated>2026-06-26T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/06/26/designing-the-faketime-api</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/06/26/designing-the-faketime-api/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">pkg/inject</code> (the previous post) is correct, but it only knows about one process at a time, and it talks in terms of PIDs, ptrace tracers, and raw offset structs. A test for an end-of-day pipeline doesn’t want any of that — it wants to say “the whole system just crossed midnight” and have every service in it agree. <code class="language-plaintext highlighter-rouge">pkg/faketime</code> is the layer that makes that possible, and its design ended up centered less on injecting into one process than on keeping several in sync.</p>

<h3 id="one-process-at-a-time-first">One process at a time, first</h3>

<p>The base primitives map directly onto the two clock modes the trampoline supports:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">h</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">faketime</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>        <span class="c">// advancing: clock ticks forward from target</span>
<span class="n">h</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">faketime</span><span class="o">.</span><span class="n">StartFrozen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>  <span class="c">// frozen: clock is pinned at target forever</span>
</code></pre></div></div>

<p>Both return a <code class="language-plaintext highlighter-rouge">*Handle</code>, and both share <code class="language-plaintext highlighter-rouge">h.Advance(d)</code> (shift forward by a duration, regardless of mode), <code class="language-plaintext highlighter-rouge">h.Freeze(t)</code>, and <code class="language-plaintext highlighter-rouge">h.SetTime(t)</code>. A single service’s scheduling test doesn’t want to think about “am I in frozen or advancing mode,” it wants to say “move forward one day” and have that mean the same thing either way — whether the test is simulating a single instant (frozen, useful for asserting exactly what the pipeline does <em>at</em> the cutover) or letting time keep ticking forward from a chosen point (advancing, useful for watching a sequence of triggers fire in order after the jump).</p>

<h3 id="session-the-part-that-actually-matters-for-a-pipeline"><code class="language-plaintext highlighter-rouge">Session</code>: the part that actually matters for a pipeline</h3>

<p>A real end-of-day flow is rarely one process. It’s a ledger service, whatever it talks to, and the datastore underneath — each independently deciding, from its own <code class="language-plaintext highlighter-rouge">time.Now()</code> calls, whether the day has ended. If I advance each one separately, one <code class="language-plaintext highlighter-rouge">SetTime</code> call at a time, there’s a real window where one service believes it’s tomorrow and another still believes it’s today — exactly the kind of skew that doesn’t happen (or happens for microseconds, not test-visible seconds) when a real day actually ends, but very much can happen if a test advances four processes’ clocks one after another with unrelated Go function calls in between.</p>

<p><code class="language-plaintext highlighter-rouge">Session</code> exists to close that window:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">s</span> <span class="o">:=</span> <span class="n">faketime</span><span class="o">.</span><span class="n">NewSession</span><span class="p">(</span><span class="n">target</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">ledgerCmd</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">reconciliationCmd</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">notifierCmd</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>

<span class="c">// every tracked process jumps together — writes are issued out</span>
<span class="c">// concurrently so the skew between them is as small as it can be</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Advance</span><span class="p">(</span><span class="m">24</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>

<p>Internally this is still just one <code class="language-plaintext highlighter-rouge">process_vm_writev</code> per process — there’s no cross-process transaction, no lock stepping the writes together at the kernel level. What <code class="language-plaintext highlighter-rouge">Session.Advance</code> does is fire all of those writes concurrently rather than one after another in a loop, which is the difference between a skew window measured in the time it takes several syscalls to complete versus one measured in however long the slowest step of a sequential loop happens to take. For a test asserting ordering between services — reconciliation shouldn’t start before the ledger closes — that’s the difference between a test that’s reliably testing the real ordering logic and one that’s occasionally just testing how fast your test harness happens to be that run.</p>

<h3 id="the-methods-that-came-from-actually-writing-pipeline-tests-not-from-a-design-doc">The methods that came from actually writing pipeline tests, not from a design doc</h3>

<p><code class="language-plaintext highlighter-rouge">Start</code>, <code class="language-plaintext highlighter-rouge">Advance</code>, and <code class="language-plaintext highlighter-rouge">Session</code> were there from the beginning. <code class="language-plaintext highlighter-rouge">Handle.EffectiveTime()</code>, <code class="language-plaintext highlighter-rouge">Handle.PID()</code>, <code class="language-plaintext highlighter-rouge">Handle.IsAlive()</code>, and <code class="language-plaintext highlighter-rouge">Session.Close()</code> were not — they came out of a backlog I only accumulated once I started writing real multi-service tests and kept reaching for something that wasn’t there:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">EffectiveTime() time.Time</code></strong> — for asserting “what time does this service currently believe it is” without redoing the offset arithmetic in the test itself. Useful on its own, and essential once a test is checking that two services in the pipeline agree with each other, not just that either one individually jumped.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">PID() int</code></strong> — needed to correlate a handle with a log line from the actual service, when a pipeline test is trying to work out <em>which</em> process’s scheduler fired first.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">IsAlive() bool</code></strong> — a <code class="language-plaintext highlighter-rouge">kill(pid, 0)</code> check. A pipeline test that expects one service to shut down cleanly after finishing its end-of-day work wants to check that without accidentally erroring out the whole <code class="language-plaintext highlighter-rouge">Session</code> because one handle no longer points at a live process.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Session.Close()</code></strong> — exists because a <code class="language-plaintext highlighter-rouge">Session</code> isn’t always just a bag of handles; once you add tracking (next post), it owns a background goroutine watching for forked children across every tracked process, and something has to stop that goroutine when the test ends.</li>
</ul>

<p>None of these are exotic. That’s the point — they’re the small, obvious things you don’t notice are missing until you’re mid-test, watching four services’ worth of output, and reach for them.</p>

<h3 id="cleanup-that-survives-a-pipeline-test-failing-halfway-through">Cleanup that survives a pipeline test failing halfway through</h3>

<p>The rawest form of the API still leaves cleanup up to the caller — kill each process, wait on it, reset its clock. For a single service that’s a three-line defer block; for a pipeline of several, it’s easy to leak one if the test fails partway through starting them. So the idiomatic entry points wire cleanup through <code class="language-plaintext highlighter-rouge">t.Cleanup</code>, which is the one mechanism in the standard testing package that reliably runs even when the test body panics or fails early:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">faketime</span><span class="o">.</span><span class="n">WithSession</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span>
    <span class="k">func</span><span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">faketime</span><span class="o">.</span><span class="n">Session</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">ledgerCmd</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="n">err</span> <span class="p">}</span>
        <span class="k">return</span> <span class="n">s</span><span class="o">.</span><span class="n">Start</span><span class="p">(</span><span class="n">reconciliationCmd</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="k">func</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">,</span> <span class="n">s</span> <span class="o">*</span><span class="n">faketime</span><span class="o">.</span><span class="n">Session</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">Advance</span><span class="p">(</span><span class="m">24</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="n">t</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span> <span class="p">}</span>
        <span class="c">// assert both services agree the day has ended</span>
    <span class="p">},</span>
<span class="p">)</span>
<span class="c">// every started process is killed, waited, and reset here — even if</span>
<span class="c">// the assertion callback called t.Fatal partway through</span>
</code></pre></div></div>

<p>A leaked, fake-time-shifted service sitting around after a pipeline test finishes is a much worse failure mode than a verbose defer block, because it fails silently in CI and shows up as a mystery in some <em>other</em> test’s flakiness later.</p>

<p>Everything above assumes each process in the pipeline stays exactly one process for its whole life. That assumption breaks the moment any of them forks — which the datastore behind a real ledger pipeline (Postgres, forking a new backend per connection) does immediately. Making injected fake time survive a fork, and an <code class="language-plaintext highlighter-rouge">exec</code> after that, is the subject of the next post.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="go" /><category term="testing" /><category term="api-design" /><category term="distributed-systems" /><summary type="html"><![CDATA[pkg/inject (the previous post) is correct, but it only knows about one process at a time, and it talks in terms of PIDs, ptrace tracers, and raw offset structs. A test for an end-of-day pipeline doesn’t want any of that — it wants to say “the whole system just crossed midnight” and have every service in it agree. pkg/faketime is the layer that makes that possible, and its design ended up centered less on injecting into one process than on keeping several in sync.]]></summary></entry><entry><title type="html">Building the Injector: Remote Mmap and the Trampoline</title><link href="https://bkaznowski.github.io/tech/2026/06/23/building-the-injector-remote-mmap-and-the-trampoline/" rel="alternate" type="text/html" title="Building the Injector: Remote Mmap and the Trampoline" /><published>2026-06-23T00:00:00+00:00</published><updated>2026-06-23T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/06/23/building-the-injector-remote-mmap-and-the-trampoline</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/06/23/building-the-injector-remote-mmap-and-the-trampoline/"><![CDATA[<p>The previous post ended on: a jump instruction needs somewhere to jump to. That somewhere is a small hand-assembled payload — the <strong>trampoline</strong> — that has to live in memory I control, within reach of the vDSO, and it has to end up there without ever giving the target process a moment where its clock reads are broken or inconsistent — which matters a lot more when the “target process” is a live service in a pipeline that other processes are actively talking to, not a standalone test binary. Getting this right turned into <code class="language-plaintext highlighter-rouge">pkg/procmem</code> (ptrace primitives), <code class="language-plaintext highlighter-rouge">pkg/trampoline</code> (the payload), and <code class="language-plaintext highlighter-rouge">pkg/inject</code> (the orchestration that ties them together).</p>

<h3 id="step-1-get-the-target-a-page-of-memory-it-doesnt-know-it-has">Step 1: get the target a page of memory it doesn’t know it has</h3>

<p>A <code class="language-plaintext highlighter-rouge">JMP rel32</code> instruction can only reach ±2GB from where it’s placed, so the trampoline needs to land close to the vDSO — and I need the target process itself to allocate that memory, since I can’t map pages into another process’s address space from the outside. The trick is to make the target call <code class="language-plaintext highlighter-rouge">mmap</code> on my behalf, without it ever running any code of its own to do so:</p>

<ol>
  <li>Scan <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/maps</code> for a free address within ±2GB of the vDSO — close enough for the eventual jump to reach.</li>
  <li>Ptrace-stop the target. Temporarily stash three bytes at the <code class="language-plaintext highlighter-rouge">clock_gettime</code> entry point (safe — I haven’t patched it yet, and I restore these bytes immediately after) with <code class="language-plaintext highlighter-rouge">0F 05 CC</code>: the <code class="language-plaintext highlighter-rouge">syscall</code> instruction followed by <code class="language-plaintext highlighter-rouge">int3</code>.</li>
  <li>Set the target’s registers by hand — <code class="language-plaintext highlighter-rouge">RAX</code> = <code class="language-plaintext highlighter-rouge">__NR_mmap</code>, and the arguments for <code class="language-plaintext highlighter-rouge">mmap(hint, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON|MAP_FIXED_NOREPLACE, -1, 0)</code> — then resume the tracee with <code class="language-plaintext highlighter-rouge">PTRACE_CONT</code>.</li>
  <li>The tracee executes the syscall, immediately hits the <code class="language-plaintext highlighter-rouge">int3</code> I planted right after it, and traps back to me with <code class="language-plaintext highlighter-rouge">SIGTRAP</code>.</li>
  <li>Read <code class="language-plaintext highlighter-rouge">RAX</code> — that’s the address of the freshly mapped page, chosen by the kernel to satisfy my hint. Restore the original three bytes and registers at the <code class="language-plaintext highlighter-rouge">clock_gettime</code> entry as if nothing happened.</li>
</ol>

<p>This is the part of the codebase I’m proudest of, mostly because it’s the part that felt like cheating when it finally worked: the target process — mid-flight, doing whatever it was already doing in its pipeline — just ran a real <code class="language-plaintext highlighter-rouge">mmap</code> syscall, with real kernel bookkeeping behind it, and has no idea it did.</p>

<h3 id="step-2-write-the-payload-into-that-page">Step 2: write the payload into that page</h3>

<p>With a rwx page now sitting in the target’s address space, I copy in the trampoline: three tiny hand-written x86-64 stubs (one each for <code class="language-plaintext highlighter-rouge">clock_gettime</code>, <code class="language-plaintext highlighter-rouge">gettimeofday</code>, and <code class="language-plaintext highlighter-rouge">time</code> — why three separate ones is a couple posts away) plus a 32-byte shared state struct, all pre-assembled from <code class="language-plaintext highlighter-rouge">trampoline.asm</code> into <code class="language-plaintext highlighter-rouge">trampoline.bin</code> and embedded directly in the Go binary. This step uses <code class="language-plaintext highlighter-rouge">process_vm_writev</code>, which needs no ptrace stop at all — just the ptrace <em>relationship</em> (or <code class="language-plaintext highlighter-rouge">CAP_SYS_PTRACE</code>) already established.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>state struct, at offset 436 in the page:
  +0   int64  offsetSec
  +8   int64  offsetNsec
  +16  uint64 enabledMask   // 1 = advancing, 3 = frozen
  +24  uint32 generation    // bumped on each update
  +28  uint32 _pad
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">offsetSec</code>/<code class="language-plaintext highlighter-rouge">offsetNsec</code> are written pre-populated with the fake time’s offset from real time before the page is ever wired up, so there’s no window where the trampoline exists but reads garbage state — no window where a scheduling loop in the target could read a half-written clock and misfire.</p>

<h3 id="step-3-patch-the-vdso-to-jump-into-it">Step 3: patch the vDSO to jump into it</h3>

<p>Now the actual hook: for each of the three functions, ptrace-stop the target again and use <code class="language-plaintext highlighter-rouge">PTRACE_POKETEXT</code> to overwrite its first five bytes with <code class="language-plaintext highlighter-rouge">E9 &lt;disp32&gt;</code> — <code class="language-plaintext highlighter-rouge">JMP rel32</code> — targeting that function’s stub offset inside the new page. <code class="language-plaintext highlighter-rouge">PTRACE_POKETEXT</code> is what makes this possible at all despite the vDSO being mapped read-only-but-executable; it’s the same primitive a debugger uses to plant a breakpoint byte in your <code class="language-plaintext highlighter-rouge">.text</code> section, just writing five bytes of jump instead of one byte of <code class="language-plaintext highlighter-rouge">int3</code>.</p>

<h3 id="step-4-detach-and-never-come-back-for-reads">Step 4: detach, and never come back (for reads)</h3>

<p>The target resumes. From here on, every call to <code class="language-plaintext highlighter-rouge">clock_gettime</code>, <code class="language-plaintext highlighter-rouge">gettimeofday</code>, or <code class="language-plaintext highlighter-rouge">time</code> lands in the trampoline stub first, which does the real syscall (or, for <code class="language-plaintext highlighter-rouge">time</code>, an internal <code class="language-plaintext highlighter-rouge">clock_gettime</code>; more on why in the Postgres post), adds the offset from the shared state struct, and returns — all without a single ptrace round-trip. Updating the fake time later — <code class="language-plaintext highlighter-rouge">SetTime</code>, <code class="language-plaintext highlighter-rouge">Advance</code>, <code class="language-plaintext highlighter-rouge">Freeze</code> — is just another <code class="language-plaintext highlighter-rouge">process_vm_writev</code> write of a new 32-byte state struct. No stop, no signal, no coordination with whatever the target happens to be doing at that moment — no risk of catching a scheduling loop mid-check and leaving it in a torn state.</p>

<p>That last property is the entire payoff of everything above: the cost of <em>changing</em> the fake time, for the rest of the process’s life, is one bulk memory write — and when the pipeline has several processes that all need to leap forward to the same end-of-day instant together, that’s one bulk write per process, issued back to back, rather than a coordination protocol between them. Compare that to the ptrace-every-call approach from two posts back, where every single read, on every process, carried the overhead this whole design exists to avoid.</p>

<p>The mechanics above are what make <code class="language-plaintext highlighter-rouge">pkg/inject</code> work in isolation, on a single process. Turning that into an API that’s actually pleasant to use from a Go test — starting several services, advancing them together, cleaning up automatically when the test ends — is a different kind of design problem, and it’s what the next post is about.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="linux" /><category term="ptrace" /><category term="assembly" /><category term="distributed-systems" /><summary type="html"><![CDATA[The previous post ended on: a jump instruction needs somewhere to jump to. That somewhere is a small hand-assembled payload — the trampoline — that has to live in memory I control, within reach of the vDSO, and it has to end up there without ever giving the target process a moment where its clock reads are broken or inconsistent — which matters a lot more when the “target process” is a live service in a pipeline that other processes are actively talking to, not a standalone test binary. Getting this right turned into pkg/procmem (ptrace primitives), pkg/trampoline (the payload), and pkg/inject (the orchestration that ties them together).]]></summary></entry><entry><title type="html">The vDSO Shortcut</title><link href="https://bkaznowski.github.io/tech/2026/06/21/the-vdso-shortcut/" rel="alternate" type="text/html" title="The vDSO Shortcut" /><published>2026-06-21T00:00:00+00:00</published><updated>2026-06-21T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/06/21/the-vdso-shortcut</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/06/21/the-vdso-shortcut/"><![CDATA[<p>The reason <code class="language-plaintext highlighter-rouge">clock_gettime</code> is fast enough for a real service to call constantly — logging, scheduling loops, request timestamps, all the ordinary background noise of a process that mostly waits for an end-of-day trigger — is that, on modern Linux, it usually isn’t a syscall at all.</p>

<p>Every process on Linux gets a small shared library mapped into its address space automatically, without ever calling <code class="language-plaintext highlighter-rouge">mmap</code> for it — the <strong>vDSO</strong>, “virtual dynamic shared object.” You can see it in any process’s memory map:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat /proc/self/maps | grep vdso
7ffd8a1fe000-7ffd8a200000 r-xp 00000000 00:00 0  [vdso]
</code></pre></div></div>

<p>Glibc’s wall-clock functions — <code class="language-plaintext highlighter-rouge">clock_gettime</code>, <code class="language-plaintext highlighter-rouge">gettimeofday</code>, <code class="language-plaintext highlighter-rouge">time</code> — don’t trap into the kernel through the syscall instruction at all. They call into this mapped vDSO region, which reads the kernel’s timekeeping data directly out of a shared memory page. No context switch, no scheduler involvement, just a function call and a couple of memory reads. That’s the whole reason these calls cost ~20ns instead of the ~200ns-plus a real syscall would cost, and it’s exactly why a ptrace-per-call approach (the previous post) adds four to five orders of magnitude of overhead onto every service in a pipeline, all day, just so one call a day can eventually be faked.</p>

<p>Here’s the insight that makes injected fake time practical: <strong>the vDSO is just a normal mapped region of executable code, sitting in the target’s own address space.</strong> It happens to be mapped read+execute, not read+write+execute — but a region being <em>marked</em> read-only doesn’t mean ptrace can’t write to it. <code class="language-plaintext highlighter-rouge">PTRACE_POKETEXT</code> — the same primitive debuggers use to plant breakpoints in your program’s <code class="language-plaintext highlighter-rouge">.text</code> section — can write to any mapped page in a ptrace-stopped tracee, read-only or not. Debuggers rely on exactly this to insert an <code class="language-plaintext highlighter-rouge">INT3</code> byte at a breakpoint address; nothing distinguishes the vDSO’s pages from any other executable page as far as <code class="language-plaintext highlighter-rouge">PTRACE_POKETEXT</code> is concerned.</p>

<p>So instead of intercepting every call, forever, in every process in the pipeline, the plan becomes:</p>

<ol>
  <li>Stop the target process once, briefly, with ptrace.</li>
  <li>Overwrite the first few bytes of <code class="language-plaintext highlighter-rouge">clock_gettime</code> (and, it turns out, two other functions — more on that in a later post) inside the vDSO with a jump instruction to code I control.</li>
  <li>Detach. The process resumes and keeps running exactly as before — logging, scheduling loop, request handling, all of it — except now every call to those functions redirects through my code first.</li>
  <li>Never touch ptrace again for the rest of the process’s life, unless I want to change the fake time. Updating the fake time afterward — say, jumping the whole pipeline forward by a day to trigger the end-of-day run — is a single <code class="language-plaintext highlighter-rouge">process_vm_writev</code> per process. No ptrace stop required at all.</li>
</ol>

<p>The cost of injection becomes a fixed, one-time price paid once at startup, and the cost of every subsequent clock read is back to native vDSO speed — it’s still just a function call into mapped memory, just a different one. This is the difference between “slow down every process in the distributed system, all day, for one eventual trigger” and “pay a millisecond once per process, then advance the whole pipeline with a handful of bulk writes whenever the test wants the day to end.”</p>

<p>There’s an obvious follow-up question this raises: a jump instruction has to jump <em>somewhere</em>, and that somewhere needs to be memory I control, close enough to the vDSO to be reachable, containing code that knows how to fake a timestamp and then get out of the way. Building that — the trampoline — is the subject of the next post.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="linux" /><category term="vdso" /><category term="distributed-systems" /><summary type="html"><![CDATA[The reason clock_gettime is fast enough for a real service to call constantly — logging, scheduling loops, request timestamps, all the ordinary background noise of a process that mostly waits for an end-of-day trigger — is that, on modern Linux, it usually isn’t a syscall at all.]]></summary></entry><entry><title type="html">The Naive Approach: Ptrace Every Clock Read</title><link href="https://bkaznowski.github.io/tech/2026/06/17/the-naive-approach-ptrace-every-clock-read/" rel="alternate" type="text/html" title="The Naive Approach: Ptrace Every Clock Read" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/06/17/the-naive-approach-ptrace-every-clock-read</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/06/17/the-naive-approach-ptrace-every-clock-read/"><![CDATA[<p>Before writing any of the code that ended up in <code class="language-plaintext highlighter-rouge">epochd</code>, I tried the approach that seems obvious if you already know Linux gives you <code class="language-plaintext highlighter-rouge">ptrace</code>: attach to the target service, catch every <code class="language-plaintext highlighter-rouge">clock_gettime</code> syscall as it happens, and rewrite the return value before the process ever sees it.</p>

<p>Mechanically, this is completely possible. <code class="language-plaintext highlighter-rouge">PTRACE_ATTACH</code> lets you stop a process at every syscall entry and exit (<code class="language-plaintext highlighter-rouge">PTRACE_SYSCALL</code>), inspect its registers with <code class="language-plaintext highlighter-rouge">PTRACE_GETREGS</code>, and mutate them with <code class="language-plaintext highlighter-rouge">PTRACE_SETREGS</code> before resuming it. Catch a <code class="language-plaintext highlighter-rouge">clock_gettime</code> on entry, let it run, catch the exit, and rewrite the <code class="language-plaintext highlighter-rouge">timespec</code> it just wrote into the caller’s buffer via <code class="language-plaintext highlighter-rouge">process_vm_writev</code>. Repeat forever.</p>

<p>I got a proof of concept working. Then I pointed it at a real service — the kind that’s supposed to sit around quietly for hours doing nothing until an end-of-day trigger fires — and watched it fall over.</p>

<p><strong>The problem is volume, not mechanics.</strong> A service that “only” does something once a day is still, moment to moment, calling the wall clock constantly: a scheduling loop checking the cutoff time, the Go runtime’s own internal timekeeping for the scheduler and GC, logging timestamps, request-handling code that stamps every event it processes. None of that is specific to the once-a-day trigger I actually care about — it’s just the ordinary background hum of a running service — but it means the wall clock gets read tens of thousands of times per second even while nothing “interesting” is happening. Under the ptrace-every-syscall approach, every one of those reads means:</p>

<ol>
  <li>Stop the tracee (a context switch into the kernel, and a wakeup for the tracer).</li>
  <li>Wake the tracer process, have it call <code class="language-plaintext highlighter-rouge">waitpid</code>, then <code class="language-plaintext highlighter-rouge">PTRACE_GETREGS</code>.</li>
  <li>Decide what to do, then <code class="language-plaintext highlighter-rouge">PTRACE_SETREGS</code> and <code class="language-plaintext highlighter-rouge">PTRACE_CONT</code>.</li>
  <li>Wake the tracee back up (another context switch).</li>
</ol>

<p>That’s two full scheduler round-trips per clock read. A <code class="language-plaintext highlighter-rouge">clock_gettime</code> call that would normally cost ~20-200ns turns into something costing tens of microseconds — a three-to-four-order-of-magnitude slowdown, applied to a function a real service calls constantly just to exist, long before the specific batch trigger I’m trying to test ever fires. I tried it against <code class="language-plaintext highlighter-rouge">test/targets/clockprinter</code> — a trivial program that just prints <code class="language-plaintext highlighter-rouge">time.Now()</code> in a loop — and it was already visibly janky. Against a real service under any load, it was worse.</p>

<p>There’s a second problem layered on top, and it matters more for a distributed-systems test than a single-process one: this approach requires the tracer to stay attached and responsive for the <em>entire</em> lifetime of every process in the pipeline, for as long as the test runs. Test an end-of-day pipeline with three or four services plus a database, and that’s three or four processes’ worth of clock reads all competing for the same tracer’s attention at once, for the full duration of a test that might be simulating several simulated days back to back. If the tracer falls behind on any one of them, every process it’s attached to slows down in lockstep, which is exactly the kind of test flakiness that erodes trust in a test suite until people stop running it.</p>

<p>That reframing — <em>intercept once, then get out of the way, for however many processes the pipeline has</em> — is what sent me looking for a different mechanism entirely. If I could make the <em>first</em> clock read after injection permanently different, without needing to be in the loop for every subsequent one, the whole performance problem disappears, for one process or a dozen. That’s exactly what Linux’s vDSO makes possible, and it’s the subject of the next post.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="go" /><category term="linux" /><category term="ptrace" /><category term="distributed-systems" /><summary type="html"><![CDATA[Before writing any of the code that ended up in epochd, I tried the approach that seems obvious if you already know Linux gives you ptrace: attach to the target service, catch every clock_gettime syscall as it happens, and rewrite the return value before the process ever sees it.]]></summary></entry><entry><title type="html">Testing Batch Jobs Without Waiting for Them</title><link href="https://bkaznowski.github.io/tech/2026/06/14/why-fake-time-for-go-tests/" rel="alternate" type="text/html" title="Testing Batch Jobs Without Waiting for Them" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://bkaznowski.github.io/tech/2026/06/14/why-fake-time-for-go-tests</id><content type="html" xml:base="https://bkaznowski.github.io/tech/2026/06/14/why-fake-time-for-go-tests/"><![CDATA[<p>A lot of the distributed systems I work on have a shape like this: several independently-deployed services, each with its own scheduler, and a pile of behavior that only happens once a day — end-of-day settlement, nightly reconciliation, report generation, billing-period rollover, token and certificate expiry sweeps. Each service decides “has the day ended yet?” in its own way: a poll loop checking <code class="language-plaintext highlighter-rouge">time.Now()</code> against a cutoff, a cron-like expression evaluated on a timer, a sleep-until-deadline. None of that logic runs more than once every 24 hours, which means it barely gets exercised in normal operation — and the first time it really runs against production conditions is in production.</p>

<p>Testing this properly means testing the actual trigger, not just the handler it eventually calls. Calling <code class="language-plaintext highlighter-rouge">RunEndOfDaySettlement()</code> directly in a test tells you the settlement logic is correct. It tells you nothing about whether the scheduler that’s supposed to call it actually fires at the right moment, across every service in the pipeline, in the right order, without one service’s clock disagreeing with another’s about whether the day has actually rolled over. That gap — between “the handler works” and “the system correctly decides to invoke the handler” — is exactly where I’ve seen real incidents come from.</p>

<p>The two conventional ways to close that gap both have a problem:</p>

<ul>
  <li><strong>Wait for real time to pass.</strong> Technically correct, completely unusable in CI. Nobody is running a test suite that blocks for 24 hours.</li>
  <li><strong>Give every service an injectable clock — a <code class="language-plaintext highlighter-rouge">Clock</code> interface, a test-only flag, a debug endpoint that sets the time.</strong> This works for code you own and control completely. It falls apart the moment the pipeline includes anything you don’t own the source of — the database backing the ledger, a message broker, a vendored scheduler binary — none of which ships a “pretend it’s tomorrow” hook. And even within your own services, any code path that reads time in a way the injected clock doesn’t cover (a library’s internal timer, a syscall issued somewhere you didn’t thread the interface through) quietly falls out of sync with the rest of the system, which is worse than not faking it at all — the test <em>looks</em> like it’s exercising real timing behavior and isn’t.</li>
</ul>

<p>What I wanted instead: take the real, compiled, unmodified binaries — every service in the pipeline, and the datastore underneath them — and shift each one’s actual OS-level wall clock forward, from outside the process, with zero source changes and zero test-only code shipped into production. If a service decides “the day changed” using an ordinary <code class="language-plaintext highlighter-rouge">time.Now()</code> call somewhere deep in a library it doesn’t control, injected fake time makes that call return tomorrow, and the service’s own real scheduling logic does the rest — I never call its internal handler myself. And because the injection happens at the OS level rather than the application level, I can advance every service in the pipeline together, so the whole distributed system crosses midnight at (approximately) the same instant — the way a real day boundary actually arrives, not staggered by whichever service’s test harness happens to poke it first.</p>

<p>That’s the shape of the problem <code class="language-plaintext highlighter-rouge">epochd</code>’s <code class="language-plaintext highlighter-rouge">pkg/faketime</code> solves. A test for an end-of-day pipeline ends up looking roughly like this:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">TestEndOfDaySettlementFires</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ledger</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"./ledger-service"</span><span class="p">)</span>
    <span class="n">target</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span>

    <span class="n">faketime</span><span class="o">.</span><span class="n">WithProcess</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">ledger</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">,</span> <span class="n">h</span> <span class="o">*</span><span class="n">faketime</span><span class="o">.</span><span class="n">Handle</span><span class="p">)</span> <span class="p">{</span>
        <span class="c">// jump the whole service straight to just before midnight</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">h</span><span class="o">.</span><span class="n">Advance</span><span class="p">(</span><span class="m">24</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">t</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="c">// the service's own scheduler — untouched, unmodified — now believes</span>
        <span class="c">// the day has ended, and fires settlement on its own</span>
        <span class="n">assertSettlementRan</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">ledger</span><span class="p">)</span>
    <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>No sleeping, no shortened “test day” config, no handler called directly. The service under test is a real, unmodified binary, deciding for itself that time has moved on.</p>

<p>Getting from “I want this” to “this reliably fools a real service’s scheduler, and every other process downstream of it in the pipeline” took going further down the stack than I expected — ptrace, the vDSO, hand-assembled trampoline code, and eventually a real Postgres cluster that exposed a gap none of my own test binaries ever would have. This series is that build, in the order it actually happened.</p>]]></content><author><name>Bartosz Thomas Kaznowski</name></author><category term="tech" /><category term="epochd" /><category term="go" /><category term="testing" /><category term="distributed-systems" /><summary type="html"><![CDATA[A lot of the distributed systems I work on have a shape like this: several independently-deployed services, each with its own scheduler, and a pile of behavior that only happens once a day — end-of-day settlement, nightly reconciliation, report generation, billing-period rollover, token and certificate expiry sweeps. Each service decides “has the day ended yet?” in its own way: a poll loop checking time.Now() against a cutoff, a cron-like expression evaluated on a timer, a sleep-until-deadline. None of that logic runs more than once every 24 hours, which means it barely gets exercised in normal operation — and the first time it really runs against production conditions is in production.]]></summary></entry></feed>