{"article":{"slug":"nix-wrote-half-of-my-debugger","title":"Nix wrote half of my debugger","subtitle":null,"summary":"Farid Zakaria explains how Rewind VM, his deterministic VM for Nix builds, grew a full debugger with source panels, gdb on any step, run comparison, CPU thread lanes and race-finding, and argues the hard parts (exact inputs, debug symbols, sources) were already provided by Nix's hermetic derivations.","content_type":"blog_post","language":"en","canonical_url":"https://fzakaria.com/2026/10/07/nix-wrote-half-of-my-debugger","author":{"name":"Farid Zakaria","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Farid Zakaria's Blog","url":"https://fzakaria.com/","listing_slug":null,"listing":null},"topics":[{"name":"Nix","slug":"nix","url":"https://listedarticles.com/topics/nix"},{"name":"Debugging","slug":"debugging","url":"https://listedarticles.com/topics/debugging"},{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2000,"reading_minutes":9,"published_at":"2026-10-07T19:00:00.000Z","added_at":"2026-10-10T20:06:54.132Z","updated_at":"2026-10-10T20:06:54.132Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/nix-wrote-half-of-my-debugger","markdown_url":"https://listedarticles.com/articles/nix-wrote-half-of-my-debugger.md","example":false,"citation":"Farid Zakaria, Farid Zakaria's Blog. \"Nix wrote half of my debugger.\" 7 Oct 2026. https://fzakaria.com/2026/10/07/nix-wrote-half-of-my-debugger (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://fzakaria.com/2026/10/07/nix-wrote-half-of-my-debugger"},"body_markdown":"A few days ago I wrote about [Rewind VM](https://fzakaria.com/2026/10/03/rewind-vm-a-flaky-build-you-only-catch-once),\na **deterministic VM** where every run of a Nix build is a pure function of its inputs,\nthread schedule included!\n\n[![Jim Carrey pulling at his hair, going crazy: how I feel about the Nix superpowers nobody else is using](https://fzakaria.com/assets/images/jim_carrey_nix_superpowers.gif)](https://fzakaria.com/assets/images/jim_carrey_nix_superpowers.gif)\n\nI have been using it to find, reproduce and solve numerous race conditions in Nix builds, but it’s been making me feel *a little crazy*. What is this superpower, and why is nobody else using it?\n\nThe tool has quickly grown a source panel, stack frames,\nbookmarks, a “Compare” tab, a lot more gdb support, thread lanes, which show who held\nthe CPU at every step, and “Check from here”, which helps find the exact step where a race\ncondition happens.\n\nI went into each new feature thinking it would be a big code-lift but I kept running into the same thing: *the hard part of each feature was already done, and Nix had done it.*\n\nA debugger for instance, needs the exact inputs of the program, its debug symbols, its sources, the sources of every library under it, and a way for someone else to get all of that on their\nmachine. That’s exactly what a derivation is, and Nix makes that easy.\n\n## §Two tellers, one account\n\nA classic simple example of a race condition is two threads depositing into one bank account. Each deposit reads the balance, writes a line to the ledger, then stores the balance plus the deposit. If one teller runs between the other’s read and store, it writes a stale balance over the other’s deposits. 💥\n\n```\nstatic void deposit(int teller, long amount)\n{\n  long seen = balance;\n  char line[64];\n  int n = snprintf(line, sizeof line, \n                   \"teller %d: %ld + %ld\\n\",\n                   teller, seen, amount);\n\n  if (write(1, line, n) != n)\n      return;\n  balance = seen + amount;\n}\n```\n\nA teller that runs between the other’s read and store writes a stale balance\nover the other’s deposits.11On my 16-core laptop, `bank` lost money in 396 of 1,000 runs. Pinned to a single core with `taskset`, it lost money in none of 1,000: one core alone rarely switches threads in the middle of a deposit, which is why Rewind perturbs the schedule. \n\nRewind’s VM has one CPU, so its first run passes too. `rewind check` runs the\nbuild again under perturbed schedules, each asking the guest kernel to\nreschedule at different steps, and narrows the first failure down to one step:\n\n```\n$ rewind check --where github:fzakaria/rewindvm#bank\n...\nstep 3237 decides it: a reschedule there makes the run fail\n\npassing: run 12feb5f832205a72, schedule 0\nfailing: run c2f1cfac0c288f3e, schedule 1 over steps 3237..3238\nthe two are the same run until step 3237\n\nwhere the threads were:\n        3237   139/140   deposit (bank.c:24), on the CPU at the deciding step\n        3250   139/141   deposit (bank.c:24), at the first event that differs\n```\n\nRunning this check on my laptop took 11 seconds. The two runs are the same machine, exit for exit, until step 3237, where only the failing one gets a reschedule.\n\n## §Free thing one: the inputs\n\nThe argument to `check` is a derivation and can be a flake reference. The beauty of Nix is that it knows the inputs of a derivation, so that’s all we need to make sure the run is reproducible. The derivation’s inputs are the source, the compiler, the libraries, the kernel, and the VM’s configuration\n\n`rewind nix` realises the derivation’s inputs, packs the closure into a read-only erofs image, and boots the VM on it.\n\nThe run’s id is a hash of its inputs, the way a store path is, and `rewind show` prints the command that makes it again:\n\n```\n$ rewind show 12feb5f8\nrewind nix /nix/store/cr8rl40rjb9cmmcd9sxn9mpdrhvp53jj-bank-0.1.0.drv --epoch 1791331200 --clock branches --name bank-0.1.0  # 12feb5f832205a72\n```\n\nWhen the build succeeds, the guest reports each output’s NAR hash, and\n`rewind nix` checks it against the host’s copy and against every binary cache\nNix substitutes from, by fetching only the `.narinfo`:\n\n```\n$ rewind nix github:fzakaria/rewindvm#bank\n...\n/nix/store/27a4q7w90jzp4wg5kaaajvb64zll6yqg-bank-0.1.0 dbf0df3de84b7a84  matches your store\n```\n\nThis helps us validate that the build within the VM is the same as the build on my laptop, and that the VM’s run is reproducible by Nix.\n\n## §Free thing two: every symbol and every source\n\nDebugging code is much simpler when you are looking at the source. A new panel in the Rewind app or `rewind where` in the terminal shows the source of the program at the playhead, and the stack frames that called it.\n\n[![The Rewind app's source panel at step 3,250 of the failing bank run: bank.c open at line 24, the write in deposit, with the frames below running from __syscall_cancel_arch in glibc's syscall_cancel.S through write.c, deposit, teller and start_thread to clone3](https://fzakaria.com/assets/images/rewind-bank-source-crop.png)](https://fzakaria.com/assets/images/rewind-bank-source.png)\n\nThe debugger needs to know where the sources are, and Nix gives us that for free too.\n\n[nixpkgs](https://github.com/NixOS/nixpkgs) builds the packages with `separateDebugInfo`, and the debug info is cached on [cache.nixos.org](https://cache.nixos.org) as a `debug` output. We can leverage [debuginfod](https://fedoraproject.org/wiki/Debuginfod) to fetch the debug info and the sources by build ID, so we can see the source of any binary in the VM, including the Linux kernel! 😲\n\n```\n$ rewind where c2f1cfac 3250\nprocess 139 (bank), thread 141, at step 3250\n#4 deposit (bank.c:24)\n      22  \tint n = snprintf(line, sizeof line, \"teller %d: %ld + %ld\\n\", teller, seen, amount);\n      23\n>     24  \tif (write(1, line, n) != n)\n      25  \t\treturn;\n      26  \tbalance = seen + amount;\ncalled from #5 teller (bank.c:34)\ncalled from #6 start_thread (pthread_create.c:454)\n```\n\n## §gdb, on a fork of any step\n\nSometimes though, the source is not enough, and we need to see the state of the program. `rewind gdb` opens gdb on a fork of the run at the playhead, with every thread of the process. The debugger can set breakpoints, watchpoints, and inspect memory, registers and variables.\n\nAt step 3249, just before teller 2 gets the CPU back, we can watch the `balance` variable and continue until it changes. Nothing gdb does changes the recording, and we can rewind to the same step and fork again, or fork at any other step, and gdb will see the same state.\n\n[![The Rewind app with a gdb pane at step 3,249: watch balance, continue, and thread 3 hits the hardware watchpoint with old value 200 and new value 160 in deposit, teller=2, at bank.c:26](https://fzakaria.com/assets/images/rewind-bank-gdb-crop.png)](https://fzakaria.com/assets/images/rewind-bank-gdb.png)\n\nIf gdb is not enough, `rewind shell --with nixpkgs#strace` opens a shell in the\nVM at a step with any package from nixpkgs on its `PATH`. It is one more closure packed into one more image.\n\n## §Compare two runs\n\nRewind makes it very easy to compare two runs. The Compare tab in the app, or `rewind compare` in the terminal, shows the last shared events of both runs, and the first event that differs.\n\n[![The Rewind app at step 3,250 of the failing run, compared with the passing run: the divergence card says the two runs are the same until step 3,237, where only this run has a reschedule, and that at step 3,250 thread 3 writes \"teller 2: 150 + 10\" where the passing run writes \"teller 2: 200 + 10\"](https://fzakaria.com/assets/images/rewind-bank-divergence-crop.png)](https://fzakaria.com/assets/images/rewind-bank-divergence.png)\n\nThe Compare tab puts both runs’ events side by side from just before they part,\nwith what differs marked. Teller 2 starts from 150 in the failing run and from\n200 in the passing one:\n\n[![The Rewind app's Compare tab: both runs' last shared events, teller 1's writes, then the failing run's teller 2 writing 150, 160, 170 beside the passing run's 200, 210, 220](https://fzakaria.com/assets/images/rewind-bank-compare-crop.png)](https://fzakaria.com/assets/images/rewind-bank-compare.png)\n\n## §Who had the CPU\n\nRewind’s VM has one CPU, so at any step exactly one thread is running. A race\nis a question of order: which thread ran when.\n\nThe trace does not answer that directly. It records what threads did, a write, an open, a fork, an exit, a signal, but not who was running in the gaps between those events.\n\nRewind can fill in the gaps without recording anything new. Every run replays\nexactly, so it can walk any stretch of a run one step at a time and, at each\nstep, ask the guest kernel which thread is on the CPU.\n\nIn the failing run, teller 1 (Thread 140) is interrupted at step 3238, halfway through a\ndeposit: it has read the balance but not stored it. Teller 2 (Thread 141) gets the CPU for\na single step, 3239, long enough to read the balance, and then teller 1 gets\nit back and finishes.22It is a little hard to see since the event from Teller 2 is tucked underneath the Orange bar. \n\n[![The Rewind app's Threads tab on the bank runs: a lane per thread, with thread 141's one-step bar at 3,239 in the failing run, and lines marking the first step another thread held the CPU, where the runs part, and the playhead](https://fzakaria.com/assets/images/rewind-bank-threads-crop.png)](https://fzakaria.com/assets/images/rewind-bank-threads.png)\n\n## §Check from here\n\nFinding one bad interleaving raises the next question: how likely is it? Is the\npassing run the normal case, or did it get lucky?\n\n`rewind check` normally answers that by building the whole derivation again\nunder many schedules. With `--run`, it starts from a run you already have\ninstead, at *any* step you pick\\_. It forks the run there once per schedule, each\nfork taking a different order of threads from that step on, and counts how\nmany end differently. Everything before the step stays exactly as it was.\n\nWe can apply this technique to the bank example, starting from the step where the two runs diverged, 3,221. When we run 16 schedules from there, all 16 lose money33Schedule 0 is the run itself, which passed. Every other line is a fork, and `exited:2` is `make check` failing because the balance came up short. :\n\n```\n$ rewind check --run 12feb5f8 --schedule-from 3221 --schedules 16 --all --no-narrow\nschedule   0: exited:0       4528 steps  dbf0df3de84b  run 12feb5f832205a72\nschedule   1: exited:2       3313 steps    run f6eb462913dce489\nschedule   2: exited:2       3328 steps    run 388cef73a9c545fa\nschedule   3: exited:2       3312 steps    run 4d42012072d11537\n...\nschedule  16: exited:2       3314 steps    run 63f7817972a90273\n16 of 16 perturbed schedules ended differently\n```\n\nWe can do the same thing from the Rewind App with “Check from here” in the context menu of the playhead. It forks the run at the playhead and runs each fork under a different schedule, counting how many end differently.\n\n[![The Rewind app after Check from here at step 3,221 of the passing run: a card saying 16 of 16 schedules ended differently, with 16 blue cells](https://fzakaria.com/assets/images/rewind-bank-check-crop.png)](https://fzakaria.com/assets/images/rewind-bank-check.png)\n\n## §Bookmarks\n\n`b` bookmarks the playhead’s step with a note. Bookmarks are kept with the run,\nand they travel in its `.rwd` export, so the person I hand the run to opens it\nwith my notes on the timeline:\n\n[![The Rewind app's Bookmarks tab with two notes: step 3,239, teller 2 reads balance = 150, then loses the CPU, and step 3,250, teller 2 stores 160 over teller 1's 200](https://fzakaria.com/assets/images/rewind-bank-bookmarks-crop.png)](https://fzakaria.com/assets/images/rewind-bank-bookmarks.png)\n\n## §Other examples to try\n\nEach one is a derivation in the flake, with one bug and one fix:\n\n* `philosophers`: the deadlock from the last post.\n* `bank`: the lost update above.\n* `waiter`: a SIGCHLD that arrives between a flag check and `pause`, so the\n  parent sleeps until `make check`’s ten second timeout kills it.\n* `config-reload`: one process rewrites a config file in place while another\n  rereads it. On many cores it fails nearly every time; on one CPU only some\n  schedules land the reader between the truncate and the last write.\n\n```\n$ rewind check github:fzakaria/rewindvm#waiter\n```\n\n## §Nix is a superpower\n\nMost of the hard work of a debugger is already done by Nix. Rewind adds a little more, but the rest is already there: the inputs, the sources, the debug symbols, and a way for someone else to get all of that on their machine.\n\nWhen you start with something hermetic like a Nix derivation, you can get a debugger for free.\n\n```\n$ nix run github:fzakaria/rewindvm -- check github:fzakaria/rewindvm#bank\n```\n","body_html":"<p>A few days ago I wrote about <a href=\"https://fzakaria.com/2026/10/03/rewind-vm-a-flaky-build-you-only-catch-once\" rel=\"nofollow ugc noopener\">Rewind VM</a>,\na <strong>deterministic VM</strong> where every run of a Nix build is a pure function of its inputs,\nthread schedule included!</p>\n<p><a href=\"https://fzakaria.com/assets/images/jim_carrey_nix_superpowers.gif\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/jim_carrey_nix_superpowers.gif\" alt=\"Jim Carrey pulling at his hair, going crazy: how I feel about the Nix superpowers nobody else is using\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>I have been using it to find, reproduce and solve numerous race conditions in Nix builds, but it’s been making me feel <em>a little crazy</em>. What is this superpower, and why is nobody else using it?</p>\n<p>The tool has quickly grown a source panel, stack frames,\nbookmarks, a “Compare” tab, a lot more gdb support, thread lanes, which show who held\nthe CPU at every step, and “Check from here”, which helps find the exact step where a race\ncondition happens.</p>\n<p>I went into each new feature thinking it would be a big code-lift but I kept running into the same thing: <em>the hard part of each feature was already done, and Nix had done it.</em></p>\n<p>A debugger for instance, needs the exact inputs of the program, its debug symbols, its sources, the sources of every library under it, and a way for someone else to get all of that on their\nmachine. That’s exactly what a derivation is, and Nix makes that easy.</p>\n<h2 id=\"two-tellers-one-account\">§Two tellers, one account</h2>\n<p>A classic simple example of a race condition is two threads depositing into one bank account. Each deposit reads the balance, writes a line to the ledger, then stores the balance plus the deposit. If one teller runs between the other’s read and store, it writes a stale balance over the other’s deposits. 💥</p>\n<pre><code>static void deposit(int teller, long amount)\n{\n  long seen = balance;\n  char line[64];\n  int n = snprintf(line, sizeof line, \n                   &quot;teller %d: %ld + %ld\\n&quot;,\n                   teller, seen, amount);\n\n  if (write(1, line, n) != n)\n      return;\n  balance = seen + amount;\n}</code></pre>\n<p>A teller that runs between the other’s read and store writes a stale balance\nover the other’s deposits.11On my 16-core laptop, <code>bank</code> lost money in 396 of 1,000 runs. Pinned to a single core with <code>taskset</code>, it lost money in none of 1,000: one core alone rarely switches threads in the middle of a deposit, which is why Rewind perturbs the schedule. </p>\n<p>Rewind’s VM has one CPU, so its first run passes too. <code>rewind check</code> runs the\nbuild again under perturbed schedules, each asking the guest kernel to\nreschedule at different steps, and narrows the first failure down to one step:</p>\n<pre><code>$ rewind check --where github:fzakaria/rewindvm#bank\n...\nstep 3237 decides it: a reschedule there makes the run fail\n\npassing: run 12feb5f832205a72, schedule 0\nfailing: run c2f1cfac0c288f3e, schedule 1 over steps 3237..3238\nthe two are the same run until step 3237\n\nwhere the threads were:\n        3237   139/140   deposit (bank.c:24), on the CPU at the deciding step\n        3250   139/141   deposit (bank.c:24), at the first event that differs</code></pre>\n<p>Running this check on my laptop took 11 seconds. The two runs are the same machine, exit for exit, until step 3237, where only the failing one gets a reschedule.</p>\n<h2 id=\"free-thing-one-the-inputs\">§Free thing one: the inputs</h2>\n<p>The argument to <code>check</code> is a derivation and can be a flake reference. The beauty of Nix is that it knows the inputs of a derivation, so that’s all we need to make sure the run is reproducible. The derivation’s inputs are the source, the compiler, the libraries, the kernel, and the VM’s configuration</p>\n<p><code>rewind nix</code> realises the derivation’s inputs, packs the closure into a read-only erofs image, and boots the VM on it.</p>\n<p>The run’s id is a hash of its inputs, the way a store path is, and <code>rewind show</code> prints the command that makes it again:</p>\n<pre><code>$ rewind show 12feb5f8\nrewind nix /nix/store/cr8rl40rjb9cmmcd9sxn9mpdrhvp53jj-bank-0.1.0.drv --epoch 1791331200 --clock branches --name bank-0.1.0  # 12feb5f832205a72</code></pre>\n<p>When the build succeeds, the guest reports each output’s NAR hash, and\n<code>rewind nix</code> checks it against the host’s copy and against every binary cache\nNix substitutes from, by fetching only the <code>.narinfo</code>:</p>\n<pre><code>$ rewind nix github:fzakaria/rewindvm#bank\n...\n/nix/store/27a4q7w90jzp4wg5kaaajvb64zll6yqg-bank-0.1.0 dbf0df3de84b7a84  matches your store</code></pre>\n<p>This helps us validate that the build within the VM is the same as the build on my laptop, and that the VM’s run is reproducible by Nix.</p>\n<h2 id=\"free-thing-two-every-symbol-and-every-source\">§Free thing two: every symbol and every source</h2>\n<p>Debugging code is much simpler when you are looking at the source. A new panel in the Rewind app or <code>rewind where</code> in the terminal shows the source of the program at the playhead, and the stack frames that called it.</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-source.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-source-crop.png\" alt=\"The Rewind app&#39;s source panel at step 3,250 of the failing bank run: bank.c open at line 24, the write in deposit, with the frames below running from __syscall_cancel_arch in glibc&#39;s syscall_cancel.S through write.c, deposit, teller and start_thread to clone3\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>The debugger needs to know where the sources are, and Nix gives us that for free too.</p>\n<p><a href=\"https://github.com/NixOS/nixpkgs\" rel=\"nofollow ugc noopener\">nixpkgs</a> builds the packages with <code>separateDebugInfo</code>, and the debug info is cached on <a href=\"https://cache.nixos.org\" rel=\"nofollow ugc noopener\">cache.nixos.org</a> as a <code>debug</code> output. We can leverage <a href=\"https://fedoraproject.org/wiki/Debuginfod\" rel=\"nofollow ugc noopener\">debuginfod</a> to fetch the debug info and the sources by build ID, so we can see the source of any binary in the VM, including the Linux kernel! 😲</p>\n<pre><code>$ rewind where c2f1cfac 3250\nprocess 139 (bank), thread 141, at step 3250\n#4 deposit (bank.c:24)\n      22      int n = snprintf(line, sizeof line, &quot;teller %d: %ld + %ld\\n&quot;, teller, seen, amount);\n      23\n&gt;     24      if (write(1, line, n) != n)\n      25          return;\n      26      balance = seen + amount;\ncalled from #5 teller (bank.c:34)\ncalled from #6 start_thread (pthread_create.c:454)</code></pre>\n<h2 id=\"gdb-on-a-fork-of-any-step\">§gdb, on a fork of any step</h2>\n<p>Sometimes though, the source is not enough, and we need to see the state of the program. <code>rewind gdb</code> opens gdb on a fork of the run at the playhead, with every thread of the process. The debugger can set breakpoints, watchpoints, and inspect memory, registers and variables.</p>\n<p>At step 3249, just before teller 2 gets the CPU back, we can watch the <code>balance</code> variable and continue until it changes. Nothing gdb does changes the recording, and we can rewind to the same step and fork again, or fork at any other step, and gdb will see the same state.</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-gdb.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-gdb-crop.png\" alt=\"The Rewind app with a gdb pane at step 3,249: watch balance, continue, and thread 3 hits the hardware watchpoint with old value 200 and new value 160 in deposit, teller=2, at bank.c:26\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>If gdb is not enough, <code>rewind shell --with nixpkgs#strace</code> opens a shell in the\nVM at a step with any package from nixpkgs on its <code>PATH</code>. It is one more closure packed into one more image.</p>\n<h2 id=\"compare-two-runs\">§Compare two runs</h2>\n<p>Rewind makes it very easy to compare two runs. The Compare tab in the app, or <code>rewind compare</code> in the terminal, shows the last shared events of both runs, and the first event that differs.</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-divergence.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-divergence-crop.png\" alt=\"The Rewind app at step 3,250 of the failing run, compared with the passing run: the divergence card says the two runs are the same until step 3,237, where only this run has a reschedule, and that at step 3,250 thread 3 writes &quot;teller 2: 150 + 10&quot; where the passing run writes &quot;teller 2: 200 + 10&quot;\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>The Compare tab puts both runs’ events side by side from just before they part,\nwith what differs marked. Teller 2 starts from 150 in the failing run and from\n200 in the passing one:</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-compare.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-compare-crop.png\" alt=\"The Rewind app&#39;s Compare tab: both runs&#39; last shared events, teller 1&#39;s writes, then the failing run&#39;s teller 2 writing 150, 160, 170 beside the passing run&#39;s 200, 210, 220\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h2 id=\"who-had-the-cpu\">§Who had the CPU</h2>\n<p>Rewind’s VM has one CPU, so at any step exactly one thread is running. A race\nis a question of order: which thread ran when.</p>\n<p>The trace does not answer that directly. It records what threads did, a write, an open, a fork, an exit, a signal, but not who was running in the gaps between those events.</p>\n<p>Rewind can fill in the gaps without recording anything new. Every run replays\nexactly, so it can walk any stretch of a run one step at a time and, at each\nstep, ask the guest kernel which thread is on the CPU.</p>\n<p>In the failing run, teller 1 (Thread 140) is interrupted at step 3238, halfway through a\ndeposit: it has read the balance but not stored it. Teller 2 (Thread 141) gets the CPU for\na single step, 3239, long enough to read the balance, and then teller 1 gets\nit back and finishes.22It is a little hard to see since the event from Teller 2 is tucked underneath the Orange bar. </p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-threads.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-threads-crop.png\" alt=\"The Rewind app&#39;s Threads tab on the bank runs: a lane per thread, with thread 141&#39;s one-step bar at 3,239 in the failing run, and lines marking the first step another thread held the CPU, where the runs part, and the playhead\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h2 id=\"check-from-here\">§Check from here</h2>\n<p>Finding one bad interleaving raises the next question: how likely is it? Is the\npassing run the normal case, or did it get lucky?</p>\n<p><code>rewind check</code> normally answers that by building the whole derivation again\nunder many schedules. With <code>--run</code>, it starts from a run you already have\ninstead, at <em>any</em> step you pick_. It forks the run there once per schedule, each\nfork taking a different order of threads from that step on, and counts how\nmany end differently. Everything before the step stays exactly as it was.</p>\n<p>We can apply this technique to the bank example, starting from the step where the two runs diverged, 3,221. When we run 16 schedules from there, all 16 lose money33Schedule 0 is the run itself, which passed. Every other line is a fork, and <code>exited:2</code> is <code>make check</code> failing because the balance came up short. :</p>\n<pre><code>$ rewind check --run 12feb5f8 --schedule-from 3221 --schedules 16 --all --no-narrow\nschedule   0: exited:0       4528 steps  dbf0df3de84b  run 12feb5f832205a72\nschedule   1: exited:2       3313 steps    run f6eb462913dce489\nschedule   2: exited:2       3328 steps    run 388cef73a9c545fa\nschedule   3: exited:2       3312 steps    run 4d42012072d11537\n...\nschedule  16: exited:2       3314 steps    run 63f7817972a90273\n16 of 16 perturbed schedules ended differently</code></pre>\n<p>We can do the same thing from the Rewind App with “Check from here” in the context menu of the playhead. It forks the run at the playhead and runs each fork under a different schedule, counting how many end differently.</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-check.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-check-crop.png\" alt=\"The Rewind app after Check from here at step 3,221 of the passing run: a card saying 16 of 16 schedules ended differently, with 16 blue cells\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h2 id=\"bookmarks\">§Bookmarks</h2>\n<p><code>b</code> bookmarks the playhead’s step with a note. Bookmarks are kept with the run,\nand they travel in its <code>.rwd</code> export, so the person I hand the run to opens it\nwith my notes on the timeline:</p>\n<p><a href=\"https://fzakaria.com/assets/images/rewind-bank-bookmarks.png\" rel=\"nofollow ugc noopener\"><img src=\"https://fzakaria.com/assets/images/rewind-bank-bookmarks-crop.png\" alt=\"The Rewind app&#39;s Bookmarks tab with two notes: step 3,239, teller 2 reads balance = 150, then loses the CPU, and step 3,250, teller 2 stores 160 over teller 1&#39;s 200\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h2 id=\"other-examples-to-try\">§Other examples to try</h2>\n<p>Each one is a derivation in the flake, with one bug and one fix:</p>\n<ul><li><code>philosophers</code>: the deadlock from the last post.</li><li><code>bank</code>: the lost update above.</li><li><p><code>waiter</code>: a SIGCHLD that arrives between a flag check and <code>pause</code>, so the</p><p>parent sleeps until <code>make check</code>’s ten second timeout kills it.</p></li><li><p><code>config-reload</code>: one process rewrites a config file in place while another</p><p>rereads it. On many cores it fails nearly every time; on one CPU only some\nschedules land the reader between the truncate and the last write.</p></li></ul>\n<pre><code>$ rewind check github:fzakaria/rewindvm#waiter</code></pre>\n<h2 id=\"nix-is-a-superpower\">§Nix is a superpower</h2>\n<p>Most of the hard work of a debugger is already done by Nix. Rewind adds a little more, but the rest is already there: the inputs, the sources, the debug symbols, and a way for someone else to get all of that on their machine.</p>\n<p>When you start with something hermetic like a Nix derivation, you can get a debugger for free.</p>\n<pre><code>$ nix run github:fzakaria/rewindvm -- check github:fzakaria/rewindvm#bank</code></pre>","headings":[{"level":2,"text":"§Two tellers, one account","id":"two-tellers-one-account"},{"level":2,"text":"§Free thing one: the inputs","id":"free-thing-one-the-inputs"},{"level":2,"text":"§Free thing two: every symbol and every source","id":"free-thing-two-every-symbol-and-every-source"},{"level":2,"text":"§gdb, on a fork of any step","id":"gdb-on-a-fork-of-any-step"},{"level":2,"text":"§Compare two runs","id":"compare-two-runs"},{"level":2,"text":"§Who had the CPU","id":"who-had-the-cpu"},{"level":2,"text":"§Check from here","id":"check-from-here"},{"level":2,"text":"§Bookmarks","id":"bookmarks"},{"level":2,"text":"§Other examples to try","id":"other-examples-to-try"},{"level":2,"text":"§Nix is a superpower","id":"nix-is-a-superpower"}]}}