<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nahum Litvin</title>
    <description>The latest articles on DEV Community by Nahum Litvin (@nahumlitvin).</description>
    <link>https://dev.to/nahumlitvin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1568834%2Fea8e22b6-6fd8-4427-a139-751bed8ff2a0.jpg</url>
      <title>DEV Community: Nahum Litvin</title>
      <link>https://dev.to/nahumlitvin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nahumlitvin"/>
    <language>en</language>
    <item>
      <title>My fix for this hang was already running. The sandbox hung anyway.</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:23:21 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/my-fix-for-this-hang-was-already-running-the-sandbox-hung-anyway-3gd8</link>
      <guid>https://dev.to/nahumlitvin/my-fix-for-this-hang-was-already-running-the-sandbox-hung-anyway-3gd8</guid>
      <description>&lt;p&gt;&lt;em&gt;&lt;a href="https://www.catchkill9.dev/posts/intro-gvisor-a-kernel-in-go.html" rel="noopener noreferrer"&gt;gVisor&lt;/a&gt; shifts its stub memory by a random gap, but since 2023 one queue was not moved with it. When the gap is small, that queue can land on a thread's stack and the sandbox can hang for good. The fix was one line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same 16-thread workload. Same arm64 VM. Same forced memory layout. Stock gVisor hung 2 runs out of 2, with a stub process stuck at 100% of a core. Add one line, and it ran clean 20 times out of 20.&lt;/p&gt;

&lt;p&gt;The only variable was where one queue sat in memory.&lt;/p&gt;

&lt;p&gt;Getting to that line took me through my own safety net first. In August I had shipped a fail-stop to gVisor: if an app thread stays stuck for 30 seconds and gVisor's interrupt doesn't free it, gVisor kills the one stub process running it instead of letting it hang the sandbox. A month later a handful of sandboxes hung with that fail-stop running. The fix is &lt;a href="https://github.com/google/gvisor/pull/15573" rel="noopener noreferrer"&gt;google/gvisor#15573&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  gVisor, the short version
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Deeper intro: &lt;a href="https://www.catchkill9.dev/posts/intro-gvisor-a-kernel-in-go.html" rel="noopener noreferrer"&gt;gVisor, a kernel in Go&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;gVisor runs a second kernel, the &lt;strong&gt;Sentry&lt;/strong&gt;, written in Go, between the app and Linux. On its default platform, &lt;strong&gt;systrap&lt;/strong&gt;, the app's code runs in host processes called &lt;strong&gt;stubs&lt;/strong&gt;. The Sentry puts work for the stubs on a queue in shared memory, and every system call traps back to it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;context&lt;/strong&gt;: one app thread's saved state, waiting to run or waiting for the Sentry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;interrupt&lt;/strong&gt;: a signal the Sentry sends to the stub thread running a context, to force it back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spinning queue&lt;/strong&gt;: idle stub threads busy-waiting for work, private to the stub. Each entry is stamped with when it started spinning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did I think it was?
&lt;/h2&gt;

&lt;p&gt;The bug from last time (&lt;a href="https://www.catchkill9.dev/posts/gvisor-one-lost-signal.html" rel="noopener noreferrer"&gt;gVisor: one lost signal&lt;/a&gt;): one lost interrupt left an app thread stuck. Same symptoms: pods stuck in Terminating, every Sentry thread asleep, hundreds of kills from Kubernetes queued behind one app thread that never paused.&lt;/p&gt;

&lt;p&gt;One thing didn't match. Last time the stuck sandbox burned no CPU. This time a stub was burning a full core. And my fail-stop was supposed to catch exactly this kind of hang.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why didn't my fail-stop fire?
&lt;/h2&gt;

&lt;p&gt;A core dump of the Sentry showed one app thread waiting on its context, with &lt;code&gt;AckedTime=0&lt;/code&gt; and an owner of &lt;code&gt;0xfefefefe&lt;/code&gt;, the "no thread" marker. No stub had picked it up since it was queued.&lt;/p&gt;

&lt;p&gt;The fail-stop only fires once the Sentry has sent an interrupt, and the Sentry only interrupts a stub thread that took the context. Nobody took this one. No interrupt, no fail-stop.&lt;/p&gt;

&lt;p&gt;So my safety net had a hole, and it was mine. It covered a stub that took the work and then stopped responding. It didn't cover work no stub ever took.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi13en3uth7fh3c78q6bc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi13en3uth7fh3c78q6bc.webp" alt="Two paths: a stub took the context, so an interrupt is sent and the fail-stop kills one stub at 30s; no stub took it, so there is no interrupt, no fail-stop, and the sandbox hangs" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was the stub doing at 100% CPU?
&lt;/h2&gt;

&lt;p&gt;Every sample of the busy stub landed in one tight loop in &lt;code&gt;spinning_queue_remove_first&lt;/code&gt;. It waits while the oldest entry's timestamp reads 0, since a new entry writes its timestamp last. Here it read 0 every time, and that check sits before the timeout check, so the loop never gives up.&lt;/p&gt;

&lt;p&gt;Its other threads never took the context either. From the code, the Sentry only wakes another stub thread when none looks available (&lt;a href="https://github.com/google/gvisor/blob/309599c1ec020ad52c82b7b6657acba73888e367/pkg/sentry/platform/systrap/subprocess.go#L947" rel="noopener noreferrer"&gt;&lt;code&gt;subprocess.go&lt;/code&gt;&lt;/a&gt;). The stuck spinner still counted as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where was the queue, really?
&lt;/h2&gt;

&lt;p&gt;In the stub's &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/maps&lt;/code&gt;, the spinning queue's address wasn't in its own private page. It sat inside a per-thread stack mapping. Its &lt;code&gt;start&lt;/code&gt; field read &lt;code&gt;0x7ff9&lt;/code&gt;, which is where the high half of a &lt;code&gt;0x00007ff9...&lt;/code&gt; pointer lands when a stack stores one there. The queue was reading a thread's stack as queue data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a queue end up on a stack?
&lt;/h2&gt;

&lt;p&gt;gVisor puts a random gap into the stub's memory layout, so attackers can't guess where things are. Every region after it has to move by the gap. Here is &lt;a href="https://github.com/google/gvisor/blob/c5e9146e675ab452b5fcc630c9781adf71257b68/pkg/sentry/platform/systrap/stub_unsafe.go#L265-L268" rel="noopener noreferrer"&gt;&lt;code&gt;stub_unsafe.go&lt;/code&gt; before the fix&lt;/a&gt;, with the missing line marked by me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint64&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hostarch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maximumUserAddress&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;stubStart&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mapLen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stubSysmsgStack&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stubContextQueueRegion&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stubContextRegion&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// missing: stubSpinningThreadQueueAddr += uintptr(gap)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three regions move. The spinning queue, between them in the layout, stays put. With a big gap it sits alone in empty space. With a small one, the shifted stacks slide over its old address. It's been that way since &lt;a href="https://github.com/google/gvisor/commit/0cbe6fc835840b3b99b458229eb785e9859faf0d" rel="noopener noreferrer"&gt;the commit that introduced the spinning queue&lt;/a&gt;, in March 2023.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsycw6z9ryity6a3hd1p4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsycw6z9ryity6a3hd1p4.webp" alt="Stub memory before and after a small random gap: thread stacks, context queue and contexts move by the gap, the spinning queue does not, and the moved stacks land on top of it" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's an availability bug, not an isolation one: the queue lands on memory the stub could already write, which the Sentry treats as untrusted anyway. The damage stays inside the sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did it hide for three years?
&lt;/h2&gt;

&lt;p&gt;The gap is random below the room &lt;em&gt;M&lt;/em&gt; left above the stub block, and &lt;em&gt;M&lt;/em&gt; is itself random up to &lt;em&gt;R&lt;/em&gt;, about 1 TiB on x86-64. The queue gets hit when the gap is smaller than the stack area &lt;em&gt;T&lt;/em&gt;, about 612 KiB for a 16-CPU sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P = (T / R) * (1 + ln(R / T)) ~ 1 / 114,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 1 in 100,000 starts at 16 CPUs, and about 1 in 35,000 at 64. Waiting for it was useless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqsoercufor1v2z04ara.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqsoercufor1v2z04ara.webp" alt="Odds of a hang per sandbox start, derived from stub_unsafe.go: 1 in 100,000 at 16 CPUs, 1 in 35,000 at 64 CPUs" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I prove it with one variable?
&lt;/h2&gt;

&lt;p&gt;So I forced it: a debug-only patch picks a gap that puts the old queue address inside a live thread stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a6hacwoewjenanoviz0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a6hacwoewjenanoviz0.webp" alt="Same forced gap, two builds: stock gVisor hung 2 of 2, with the one-line fix clean 20 of 20" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build, same forced gap&lt;/th&gt;
&lt;th&gt;Queue lands on a stack&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stock gVisor&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;hung 2 of 2, stubs at 100% CPU, no fail-stop log line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock + &lt;code&gt;stubSpinningThreadQueueAddr += uintptr(gap)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no, it moves with the gap&lt;/td&gt;
&lt;td&gt;clean, 20 of 20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The code is shared by both architectures; arm64 was just faster to boot. The fix adds a layout test, and the repro tool is &lt;a href="https://github.com/NahumLitvin/gvisor/tree/systrap-triage/tools/systrap-triage" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are you affected?
&lt;/h2&gt;

&lt;p&gt;Systrap sandboxes on &lt;code&gt;release-20230417.0&lt;/code&gt; through &lt;code&gt;release-20261005.0&lt;/code&gt; can hit it; &lt;a href="https://github.com/google/gvisor/issues/15572" rel="noopener noreferrer"&gt;google/gvisor#15572&lt;/a&gt; has details by version. The tell: a stub pinned at 100% forever, a pod stuck in Terminating, &lt;code&gt;runsc kill&lt;/code&gt; hanging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Upgrade&lt;/strong&gt; to any runsc build that contains &lt;code&gt;c7f458aa3&lt;/code&gt;, the first release after &lt;code&gt;release-20261005.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Until then, &lt;code&gt;--systrap-disable-fast-path&lt;/code&gt;&lt;/strong&gt; (on &lt;code&gt;release-20260622.0&lt;/code&gt; or later) stops stub threads from spinning, so the spinning queue is never used. It's a partial workaround: it costs syscall latency, and a much rarer tiny gap can still hit the context queue. Only the upgrade fixes that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. To clear a hung sandbox,&lt;/strong&gt; kill its Sentry on the host: the &lt;code&gt;runsc-sandbox&lt;/code&gt; process ending with the pod sandbox ID from &lt;code&gt;crictl pods&lt;/code&gt;. Its stubs die with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the AI do?
&lt;/h2&gt;

&lt;p&gt;AI agents read the dump, mapped a program counter to a source line, built the VM repro and drafted the upstream PR. I made the calls, starting with not stopping at the hole in my own fail-stop.&lt;/p&gt;

&lt;p&gt;A few things I'm keeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the cheap check before the theory. One busy core killed my first guess.&lt;/li&gt;
&lt;li&gt;Your own safety net has holes, so look there first. Mine only covered work a stub had taken.&lt;/li&gt;
&lt;li&gt;Your debugging tool can kill the patient. A gcore of the Sentry faults in the sandbox's shared memory and can OOM it; dump the stub first.&lt;/li&gt;
&lt;li&gt;When a bug needs bad luck, make the bad luck happen on demand. A forced gap turned a 1 in 100,000 hang into 2 of 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The line is merged. That queue can't land on a stack anymore.&lt;/p&gt;




&lt;p&gt;Thanks to &lt;a href="https://github.com/konstantin-s-bogom" rel="noopener noreferrer"&gt;konstantin-s-bogom&lt;/a&gt; for reviewing and approving the fix, and to &lt;a href="https://github.com/avagin" rel="noopener noreferrer"&gt;avagin&lt;/a&gt;, who wrote the spinning queue and fixed the same symptom from a different cause in 2024 (&lt;a href="https://github.com/google/gvisor/issues/10000" rel="noopener noreferrer"&gt;google/gvisor#10000&lt;/a&gt;). Investigation assisted by Claude Code, with Codex as a second reviewer.&lt;/p&gt;

</description>
      <category>gvisor</category>
      <category>kubernetes</category>
      <category>go</category>
      <category>debugging</category>
    </item>
    <item>
      <title>STPA-inspired questions tied a plain checklist in my coding agent. Their one win: Claude asking about timing.</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:24:41 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/stpa-inspired-questions-tied-a-plain-checklist-in-my-coding-agent-their-one-win-claude-asking-2iic</link>
      <guid>https://dev.to/nahumlitvin/stpa-inspired-questions-tied-a-plain-checklist-in-my-coding-agent-their-one-win-claude-asking-2iic</guid>
      <description>&lt;p&gt;STPA is a safety method from MIT that asks when an automatic action becomes unsafe. I put its questions into my AI coding agent's rules, then replayed three real past changes with no rule, with a plain "check what this could break" rule, and with STPA. On average STPA did no better than the plain rule. It won clearly in one case, where the risks were about timing. Only with STPA did Claude ask whether a timeout was too long (3 of 6 grades, against 0) or when to stop waiting (4 of 6, against 0).&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;My first version of this post said STPA helped. One question, whether a change breaks apps outside its target, had jumped from 1 in 6 to 5 in 6 in a single replay of case A. Two reviewers asked the obvious thing: would any rule that says "check what you might break" do the same? So I ran it properly. The two rules, word for word.&lt;/p&gt;

&lt;p&gt;The STPA rule (62 words):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When a plan or design adds or changes something that acts automatically (scaler, quarantine, drain, hook, mutating script), list each control action and ask: unsafe if missing, if wrong context, if early/late, if it stops too soon or runs too long. Name the feedback it acts on and what happens when that feedback is stale or empty.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plain checklist (35 words):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before shipping any change, check what it could affect beyond its intended target, how you would notice if it goes wrong in production, and how you would roll it back quickly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96b0ts225bhury8j81fw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96b0ts225bhury8j81fw.png" alt="How the test was set up: 3 past changes, 3 arms, 2 models, 54 plans, 2 blind judges" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The bugs are the ones each change's real review or production run found. The judges agreed on 90% of calls.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The three changes, in generic form. &lt;strong&gt;A&lt;/strong&gt;: a controller taking over a value another controller used to write. &lt;strong&gt;B&lt;/strong&gt;: automatic recovery for a worker that stops responding. &lt;strong&gt;C&lt;/strong&gt;: a dispatcher that picks workers from a cache that can be stale.&lt;/p&gt;

&lt;p&gt;First, the headline from my first version. In case A, regraded with stricter blinding: no rule 4 of 6, plain list 6, STPA 6. The gap shrank, and the plain list closed it too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz094q32s9ica31hp8aox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz094q32s9ica31hp8aox.png" alt="Real bugs found per plan out of 8, per case and arm" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Orange: STPA. Both models pooled. Across all three, the plain list and STPA tie at 6.1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The plain list helped a little everywhere. STPA beat it only in B, and fell below no rule in C.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwm5zs3d12pqcmaofbzw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwm5zs3d12pqcmaofbzw.jpg" alt="Case B, Claude: only the STPA rule made it ask whether the timeout was too long (3 of 6) and when to stop waiting (4 of 6). No rule and the plain list: 0." width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;STPA's "too late" and "applied too long". Out of 6 judge grades, 3 plans each graded twice. Without STPA, zero.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In B, the one thing the method added for Claude was questions about time. Codex asked about the timeout even with no rule, and more with either rule. But neither model asked when waiting stops helping, except Claude with STPA. One honest limit: the STPA rule names timing ("early/late", "runs too long") and the checklist doesn't, so this test can't tell STPA apart from a rule that just mentions time.&lt;/p&gt;

&lt;p&gt;In C, STPA cost Codex the basics, even though Claude gained a little.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ua5ei8dbi3mry3qcig.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ua5ei8dbi3mry3qcig.jpg" alt="Case C, Codex: visible check errors, a metric, a proving test. No rule 4, 3 and 1 of 6. Plain list 6, 4, 3. STPA 0 on all three." width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;With STPA, Codex dropped all three. The plain list kept them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rule came from
&lt;/h2&gt;

&lt;p&gt;My agent learns from a git repo of rules, and until recently almost every rule came from something that had already gone wrong. One says "never conclude from a zero". During an incident, a metrics query with a wrong label returned nothing, with no error. The graph looked calm while the thing it measured was on fire.&lt;/p&gt;

&lt;p&gt;On 13 October I was at SREcon in Dublin, listening to John Thomas of MIT, co-author of the STPA Handbook. His talk: &lt;a href="https://www.usenix.org/conference/srecon26emea/presentation/thomas" rel="noopener noreferrer"&gt;What Your SLOs Can't See: Applying STPA to a Distributed Software Service&lt;/a&gt;. My laptop was open.&lt;/p&gt;

&lt;p&gt;The idea comes from Nancy Leveson at MIT. Her accident model, STAMP (System-Theoretic Accident Model and Processes, 2004), later expanded in her book Engineering a Safer World (2011), treats safety as a control problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotype8t6oki1ztl9qy6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotype8t6oki1ztl9qy6y.png" alt="STAMP's control loop: controller, control action, controlled process, feedback" width="798" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;STAMP's loop. Accidents come from inadequate control: a wrong picture, flawed logic, or missing feedback.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;STPA (System-Theoretic Process Analysis) is the hazard analysis built on it. For every control action you ask when it becomes an unsafe control action (UCA): not provided, provided when it causes a hazard, too early, too late or out of order, stopped too soon or applied too long.&lt;/p&gt;

&lt;p&gt;I asked my agent if any of it applied to me. It said: "You're doing half of it, and only after the fact." So I put the questions where the agent already decides:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndt2n6hctj25gcr2tzpa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndt2n6hctj25gcr2tzpa.png" alt="Where each question runs: plan, review, handover before shipping; post-mortem after" width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The post-mortem step borrows from CAST, Leveson's accident analysis, also built on STAMP.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is mostly STPA's third step. No losses, hazards or control structure written down. A real STPA starts there. Mine doesn't, yet.&lt;/p&gt;

&lt;p&gt;A second model and a review bot then found my rules breaking their own method: a post-mortem line that only blamed bad feedback, a missing UCA type, and a writer check placed after the writers act. A check that runs too late, written during a talk on what monitoring misses. I fixed it and merged before the lecture ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I kept
&lt;/h2&gt;

&lt;p&gt;A plain checklist does most of the work. Across the three cases it matched STPA, 6.1 to 6.1, against 5.7 with no rule. What I keep is the timing questions, whether the credit goes to STPA or just to naming time: my checklist now adds "too early, too late, too long". For Claude nothing else raised them, 0 of 6 without and 3 and 4 with. And I now watch what a method pushes out. In C, Codex's three operational checks went to 0 of 6 with STPA.&lt;/p&gt;

&lt;p&gt;Three changes is not a study, but it was enough to retract my first headline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits and links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The talk: John Thomas, &lt;a href="https://www.usenix.org/conference/srecon26emea/presentation/thomas" rel="noopener noreferrer"&gt;What Your SLOs Can't See: Applying STPA to a Distributed Software Service&lt;/a&gt;, SREcon26 EMEA, Dublin, 13 October 2026.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://psas.scripts.mit.edu/home/get_file.php?name=STPA_handbook.pdf" rel="noopener noreferrer"&gt;STPA Handbook&lt;/a&gt;, Nancy Leveson and John Thomas (2018). &lt;a href="https://psas.scripts.mit.edu/home/get_file4.php?name=CAST_handbook.pdf" rel="noopener noreferrer"&gt;CAST Handbook&lt;/a&gt;, Nancy Leveson (2019).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.catchkill9.dev/posts/stpa-in-my-agent.html" rel="noopener noreferrer"&gt;catchkill9.dev&lt;/a&gt;, where the diagrams follow your light or dark theme.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sre</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built it for my terminal. A week later, strangers were fixing it for each other</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/i-built-it-for-my-terminal-a-week-later-strangers-were-fixing-it-for-each-other-nf6</link>
      <guid>https://dev.to/nahumlitvin/i-built-it-for-my-terminal-a-week-later-strangers-were-fixing-it-for-each-other-nf6</guid>
      <description>&lt;h3&gt;
  
  
  Friday 2 Oct: built for me
&lt;/h3&gt;

&lt;p&gt;Every day I watch Claude Code print tables as pipes and dashes. prismantis is my fix: a Claude Code mod that redraws replies with colored tables, highlighted code and mermaid charts. I &lt;a href="https://dev.to/nahumlitvin/mermaid-charts-in-claude-codes-terminal-what-a-mod-could-do-that-my-vs-code-harness-couldnt-2gkm-temp-slug-4979281"&gt;launched it&lt;/a&gt; on 2 Oct and shipped 8 releases that afternoon, most of the code written by Claude with me saying "almost" at screenshots.&lt;/p&gt;

&lt;p&gt;I use it all day. That is the whole reason it exists. Every reply in my terminal now draws the way I wanted it to, and a chart showing up where a wall of text used to be makes me smile every time.&lt;/p&gt;

&lt;p&gt;It was tested on exactly one setup: my Mac, my terminal, my window size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sunday 4 Oct: other people
&lt;/h3&gt;

&lt;p&gt;16 stars in one day. The first forks showed up. I had no idea who these people were or what terminals they ran, and that was the first time it felt like it wasn't only mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monday 5 Oct: a bug I could never see
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/willowite" rel="noopener noreferrer"&gt;@willowite&lt;/a&gt; opened &lt;a href="https://github.com/NahumLitvin/prismantis/issues/6" rel="noopener noreferrer"&gt;#6&lt;/a&gt;. On Windows, the &lt;code&gt;⏺&lt;/code&gt; reply marker falls back to the Segoe UI Emoji font, so Windows Terminal draws a blue emoji and ignores the theme color. The report named the line, the codepoint and the fix: use &lt;code&gt;●&lt;/code&gt;, which prismantis already used for tool rows.&lt;/p&gt;

&lt;p&gt;I don't have a Windows box. Claude doesn't either. It was fixed and released the same afternoon, and I couldn't have found it in a year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tuesday 6 Oct: the best report, and the first PR
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/cesar32az" rel="noopener noreferrer"&gt;@cesar32az&lt;/a&gt; opened &lt;a href="https://github.com/NahumLitvin/prismantis/issues/28" rel="noopener noreferrer"&gt;#28&lt;/a&gt;, the best bug report I've had on anything: versions, OS build, two terminals, settings, steps, &lt;code&gt;--debug&lt;/code&gt; output. Copy buttons drew fine and did nothing. Two releases came out of it, including keyboard copy commands, because the report also showed my README was wrong. Then they found the cause on their own side, a &lt;code&gt;CLAUDE_CODE_DISABLE_MOUSE=1&lt;/code&gt; left in &lt;code&gt;~/.zshrc&lt;/code&gt; for months, and apologized for the noise. No apology needed. The keyboard commands are how I copy now.&lt;/p&gt;

&lt;p&gt;That evening &lt;a href="https://github.com/Malorn44" rel="noopener noreferrer"&gt;@Malorn44&lt;/a&gt; sent &lt;a href="https://github.com/NahumLitvin/prismantis/pull/38" rel="noopener noreferrer"&gt;#38&lt;/a&gt;, about 50 lines that let another mod hand prismantis some markdown and get it drawn in the user's theme. I built a renderer for my own replies. Now other mods can draw with it. Merged and shipped the same evening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wednesday 7 Oct: the Forks page
&lt;/h3&gt;

&lt;p&gt;In the morning I opened the repo's Forks page, mostly out of curiosity. 6 forks. 3 had real work nobody had sent me: &lt;a href="https://github.com/vrevolverrr" rel="noopener noreferrer"&gt;@vrevolverrr&lt;/a&gt; had LaTeX math and a fix for table borders, &lt;a href="https://github.com/rooterkyberian" rel="noopener noreferrer"&gt;@rooterkyberian&lt;/a&gt; had tables that copy into docs as real tables, &lt;a href="https://github.com/AlexeyHRDesign" rel="noopener noreferrer"&gt;@AlexeyHRDesign&lt;/a&gt; had made the whole thing work in the Claude desktop app.&lt;/p&gt;

&lt;p&gt;That is normal. You hit a pain, fix it for yourself, move on. Opening a PR on a stranger's repo costs something, and nobody pays it without a reason.&lt;/p&gt;

&lt;p&gt;GitHub has no DMs and their forks had Issues turned off, so I opened one issue per fork on my repo and @mentioned the author. This is the one to @vrevolverrr, &lt;a href="https://github.com/NahumLitvin/prismantis/issues/43" rel="noopener noreferrer"&gt;#43&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;@vrevolverrr love the LaTeX math in your fork (&lt;a href="https://github.com/vrevolverrr/prismantis/commit/d49e0c918c34f468b3f9bfa0458e3d50a14b9735" rel="noopener noreferrer"&gt;d49e0c9&lt;/a&gt;). Typeset formulas via RaTeX in kitty/Ghostty, with a text fallback everywhere else, is exactly right.&lt;/p&gt;

&lt;p&gt;Would you open a PR here? Happy to review it fast. If you'd rather not, I can port &lt;code&gt;hooks/latex.ts&lt;/code&gt; and the &lt;code&gt;$$&lt;/code&gt; parsing myself and credit you as co-author on the commit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Specific praise, one ask, a promise I could keep, and an easy out.&lt;/p&gt;

&lt;p&gt;Within 10 minutes @AlexeyHRDesign took the out: they're building their own desktop mod, port it with co-author credit please. Then they left a list of what didn't hold up in daily use, including a stray &lt;code&gt;\x08&lt;/code&gt; in a regex where &lt;code&gt;\b&lt;/code&gt; was meant. I valued that list as much as the code.&lt;/p&gt;

&lt;p&gt;In under 20 minutes @rooterkyberian's PR &lt;a href="https://github.com/NahumLitvin/prismantis/pull/47" rel="noopener noreferrer"&gt;#47&lt;/a&gt; was up, and @vrevolverrr answered that the border fix was coming first. In the same reply they warned me their LaTeX work runs an external program, which breaks a rule in my own repo. They flagged it before I noticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wednesday, an hour later: two strangers and one bug
&lt;/h3&gt;

&lt;p&gt;Without any invite, &lt;a href="https://github.com/b33eep" rel="noopener noreferrer"&gt;@b33eep&lt;/a&gt; had opened &lt;a href="https://github.com/NahumLitvin/prismantis/pull/46" rel="noopener noreferrer"&gt;#46&lt;/a&gt; for the same border bug. Wrapped table rows lost their borders, and in a narrow terminal, as they put it, "that is almost every table". I work in a wide window. I'd never have seen it.&lt;/p&gt;

&lt;p&gt;So two people I'd never met had fixed the same bug two different ways, within half an hour of each other. I pointed @vrevolverrr at #46 so they wouldn't do it twice. Then I mostly watched.&lt;/p&gt;

&lt;p&gt;@b33eep ran both branches side by side in a terminal without clickable links and found a row where @vrevolverrr's version drew an extra line. @vrevolverrr reproduced it and proposed something better than either fix. @b33eep agreed and asked them to fold their own border idea into &lt;a href="https://github.com/NahumLitvin/prismantis/pull/48" rel="noopener noreferrer"&gt;#48&lt;/a&gt;. It was folded in, re-tested, and @b33eep closed their own PR in favor of the combined one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;thanks for picking this up so quickly and for the co-author credit. Dropping the wrapping for link rows was a better idea than mine, and it was good working through this with you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;About an hour, start to finish. The best review of the week happened between two strangers. I just merged it.&lt;/p&gt;

&lt;p&gt;Then @b33eep sent &lt;a href="https://github.com/NahumLitvin/prismantis/pull/49" rel="noopener noreferrer"&gt;#49&lt;/a&gt;, the column-width fix I'd asked for in &lt;a href="https://github.com/NahumLitvin/prismantis/issues/41" rel="noopener noreferrer"&gt;#41&lt;/a&gt;, rebased on top of the merged #48. @vrevolverrr sent &lt;a href="https://github.com/NahumLitvin/prismantis/pull/51" rel="noopener noreferrer"&gt;#51&lt;/a&gt; for a Serena tool name drawn with "serena" in bold, a 9-line fix for a tool I don't run. I merged it within the hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rest of the week: leading it
&lt;/h3&gt;

&lt;p&gt;My job changed that week. I wrote less code and made more calls.&lt;/p&gt;

&lt;p&gt;@rooterkyberian's clipboard PR was solid but touched macOS and Linux at once, so I asked to split it. They had &lt;a href="https://github.com/NahumLitvin/prismantis/pull/54" rel="noopener noreferrer"&gt;#54&lt;/a&gt; and &lt;a href="https://github.com/NahumLitvin/prismantis/pull/55" rel="noopener noreferrer"&gt;#55&lt;/a&gt; up the same evening. Both are merged.&lt;/p&gt;

&lt;p&gt;@vrevolverrr's LaTeX PR, &lt;a href="https://github.com/NahumLitvin/prismantis/pull/50" rel="noopener noreferrer"&gt;#50&lt;/a&gt;, brought back running an external program, the one thing I'd removed earlier on purpose. I asked for six changes: one switch, off by default, nothing runs unless you turn it on. They sent all six, one commit per point. Merged on 9 Oct. Math now draws as real formulas in kitty and Ghostty, for anyone who opts in.&lt;/p&gt;

&lt;p&gt;Most of it was saying yes fast, sometimes saying no kindly, and holding the few rules that keep a mod safe to install. I didn't expect to enjoy that part this much.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took from it
&lt;/h3&gt;

&lt;p&gt;Agents made the code cheap. 19 releases in 5 days says more about what code costs now than about me.&lt;/p&gt;

&lt;p&gt;The pain is the part I can't generate. Windows fonts, narrow terminals, Serena tool names, the desktop app, pasting tables into docs, math. None of it shows up on my Mac. No prompt would have written issue #6.&lt;/p&gt;

&lt;p&gt;So ask the forks. 3 of 6 had fixes nobody told me about. One friendly issue each got me PRs, a list of traps, and a front-row seat to two strangers making each other's code better.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/willowite" rel="noopener noreferrer"&gt;@willowite&lt;/a&gt;, &lt;a href="https://github.com/cesar32az" rel="noopener noreferrer"&gt;@cesar32az&lt;/a&gt;, &lt;a href="https://github.com/Malorn44" rel="noopener noreferrer"&gt;@Malorn44&lt;/a&gt;, &lt;a href="https://github.com/b33eep" rel="noopener noreferrer"&gt;@b33eep&lt;/a&gt;, &lt;a href="https://github.com/vrevolverrr" rel="noopener noreferrer"&gt;@vrevolverrr&lt;/a&gt;, &lt;a href="https://github.com/rooterkyberian" rel="noopener noreferrer"&gt;@rooterkyberian&lt;/a&gt; and &lt;a href="https://github.com/AlexeyHRDesign" rel="noopener noreferrer"&gt;@AlexeyHRDesign&lt;/a&gt;. prismantis is better than anything I'd have built alone, and it's the first thing I see every time Claude answers me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nahum Litvin is a Technical Lead - DevOps Engineering at Wix.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>claude</category>
      <category>github</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Code mod: themes engine, charts, graphs, colors. All you ever wanted.</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:43:06 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/claude-code-mod-themes-engine-charts-graphs-colors-all-you-ever-wanted-2mff</link>
      <guid>https://dev.to/nahumlitvin/claude-code-mod-themes-engine-charts-graphs-colors-all-you-ever-wanted-2mff</guid>
      <description>&lt;p&gt;&lt;strong&gt;Claude Code mod: themes engine, charts, graphs, colors. All you ever wanted.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add NahumLitvin/prismantis
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;prismantis@prismantis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/NahumLitvin/prismantis" rel="noopener noreferrer"&gt;https://github.com/NahumLitvin/prismantis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.daily.dev%2Fimage%2Fupload%2Fs--UNt3ZFs7--%2Ff_auto%2Fv1790951587%2Fugc%2Fcontent_43e8bc06-ed34-4b4e-9d72-80848301526e%3F_a%3DBAMAMicg0" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.daily.dev%2Fimage%2Fupload%2Fs--UNt3ZFs7--%2Ff_auto%2Fv1790951587%2Fugc%2Fcontent_43e8bc06-ed34-4b4e-9d72-80848301526e%3F_a%3DBAMAMicg0" alt="prismantis" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>One lost signal, five days stuck, 45,000 frozen threads: fixing a gVisor hang upstream</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/one-lost-signal-five-days-stuck-45000-frozen-threads-fixing-a-gvisor-hang-upstream-3di2</link>
      <guid>https://dev.to/nahumlitvin/one-lost-signal-five-days-stuck-45000-frozen-threads-fixing-a-gvisor-hang-upstream-3di2</guid>
      <description>&lt;p&gt;It looked like a deadlock in gVisor. It was a single missed wake-up, hiding behind a symptom teams have been reporting since 2021, and two strangers and a maintainer fixed it in about a week.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The hang in one picture: one signal, no retry, and a deadline check whose only action is a log line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On a Tuesday morning our alerting claimed that hundreds of pods were stuck Terminating in production. The real number was one. The rule counts kubelet failed-kill events rather than pods, and a sandbox that never dies gets retried forever, so a single wedged pod read as a fleet on fire. That pod turned out to be the interesting part.&lt;/p&gt;

&lt;p&gt;At Wix, our Velo grid runs users' backend JavaScript as untrusted code inside gVisor sandboxes on Amazon EKS. gVisor puts a userspace kernel between that code and the host kernel, so untrusted code never talks to the host kernel directly. Thousands of sandboxes per node group, short lifecycles, heavy churn. Deleting a pod should take seconds. This one had been Terminating since 07:11. kubelet's kill requests got DeadlineExceeded every 2 minutes and would have continued forever. We intervened by hand after about 90 minutes. A week earlier, several pods on a few nodes had sat like that for days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody's timeout saved us
&lt;/h2&gt;

&lt;p&gt;Before the forensics, the shape of the problem. Deleting a pod is a relay: each layer asks the next one, politely, to make something die. Every layer should hold two more things beyond the polite ask: a bound on how long it waits, and an escalation that works &lt;em&gt;without the cooperation of the thing it is waiting on&lt;/em&gt;. In our case the relay runs from kubelet (the Kubernetes node agent) to containerd (the container runtime) to the gVisor shim (the per-sandbox process containerd talks to) to the sandbox itself. Here is that stack, graded on all three:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three arrows per hop: blue attempts, amber detects, violet recovers. Blue failed once, at the bottom. Amber either did not exist, fired into a log line, or fired and could only re-send blue. Violet existed nowhere, so one lost message at the bottom of the stack became the whole stack's hang.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Look at the violet column. Every escalation asked the very component being escalated against to cooperate. containerd's dead-shim cleanup only fires when the shim disconnects. Ours kept answering Connect and State, which never touched the lock, while Kill and Stats blocked behind it. The connection stayed up, so cleanup never fired. Alive enough to prevent its own cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a frozen sandbox looks like
&lt;/h2&gt;

&lt;p&gt;That userspace kernel is called the sentry. On systrap, the platform we run, the app's threads run in stripped-down stub processes and the sentry coordinates them. Our wedged pod had a sentry that was alive but answering nothing. runsc kill, gVisor's own command for stopping a sandbox, hung. runsc debug --stacks, the tool that is supposed to tell you why something hangs, also hung. The panic log was 0 bytes.&lt;/p&gt;

&lt;p&gt;The node told the rest of the story. Load average climbing about 3 per hour while actual CPU sat near 30%. Hundreds of sentry threads parked, waiting on shared memory. Stub processes accumulating. On the worst nodes, load kept climbing for days with the machine mostly idle. The only remediation that worked was SIGKILL to the whole sandbox process tree, by hand, on the node.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stub processes pile up behind the frozen sentry: load average keeps climbing while the CPU does almost nothing. Threads stuck waiting, not working: the rising top panel against the flat bottom one is the bug.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So we filed &lt;a href="https://github.com/google/gvisor/issues/14408" rel="noopener noreferrer"&gt;gvisor#14408&lt;/a&gt; with what we had: the outside view, two affected releases, and an honest admission that we could not get stacks because the debug tooling was wedged along with the sandbox. Our proposed fix, PR #14201, had been open since the week before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then a stranger showed up with the other half
&lt;/h2&gt;

&lt;p&gt;The same day, another engineer filed &lt;a href="https://github.com/google/gvisor/issues/14405" rel="noopener noreferrer"&gt;gvisor#14405&lt;/a&gt;. Same bug at a different company, and they had the one thing we could not get: a full dump of the sentry's goroutines, Go's lightweight threads, from inside a frozen sentry. Their dump showed one of the sentry's internal worker threads sitting in the same wait loop for over five days, waiting for a stub thread that had missed a single wake-up signal.&lt;/p&gt;

&lt;p&gt;In their shim, about 45,000 waiting threads were queued behind one lock held by a kill that would not finish, and their containerd and kubelet memory grew until nodes ran out.&lt;/p&gt;

&lt;p&gt;The bug itself is painfully simple. The sentry sends the stub one interrupt signal. One. If that signal is lost, and nobody has yet pinned down why it sometimes is, the sentry keeps waiting for the answer with no retry and no escape. There is even a 30 second deadline in the code that notices the wait is too long. Here is the whole thing it did about it:&lt;/p&gt;

&lt;p&gt;if time.Now().After(deadline) { log.Warningf("Systrap task goroutine has been waiting on " + "ThreadContext.State futex too long. ...") // no retry, no escalation }&lt;/p&gt;

&lt;p&gt;Where did 45,000 waiting threads come from? cAdvisor kept polling every container's Stats. Each request added a goroutine that blocked behind the hung kill's lock. Its caller timed out, but a mutex wait cannot observe cancellation, so the goroutine stayed. Over five days, 45,000 accumulated: roughly one every 10 seconds, the scrape interval fossilized in a thread dump. The metrics system asking "how are you?" every 10 seconds had pushed the shim to about 600MB.&lt;/p&gt;

&lt;p&gt;A timeout that only logs a warning is not a timeout. It is a diary.&lt;/p&gt;

&lt;p&gt;And this exact wait sits on the teardown path. Killing a sandbox starts with a freeze-everything step that waits for every worker thread to park, so one stuck worker means the kill command can never answer, the shim call hangs, kubelet gets DeadlineExceeded, and each retry parks another goroutine behind the first one. The pod stays Terminating for days.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, how did we fix it?
&lt;/h2&gt;

&lt;p&gt;The merged fix escalates in stages, gentlest first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tap again.&lt;/strong&gt; The interrupt is resent on each 5 second checkup wakeup instead of once. When the resend lands, a lost signal costs the workload a stall of a few seconds, then it carries on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Photograph the scene.&lt;/strong&gt; If the stub is still unresponsive after the 30 second deadline, the sentry dumps every internal stack trace to the log. A frozen sandbox can not be debugged from outside unless it was started with --panic-signal; ours were not, theirs were, which is where their dump came from. So this failure now documents itself. The next team to hit anything like this gets for free what took two companies a week to assemble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Remove only the broken part.&lt;/strong&gt; Then the sentry kills just the one stuck subprocess, through the same path it already uses when a stub dies naturally. The blocked task unwinds, teardown can finish, and the healthy subprocesses in the sandbox are untouched.&lt;/p&gt;

&lt;p&gt;My first version killed the entire sentry. gVisor maintainer Konstantin Bogomolov pushed for killing only the stuck subprocess, sparing healthy neighbors. He was right. The other reporter had raised the same concern earlier that day. Konstantin also caught a dropped continue, which led me to a race: a context that recovered during the wait could still get killed. Three review rounds in under a week took the PR from nobody having looked at it to merged on master.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The merged fix, staged from least to most disruptive. My first version jumped straight to killing the whole sandbox; review scoped it down.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that bothers me
&lt;/h2&gt;

&lt;p&gt;After the merge, we found reports since 2021 from a Cloudflare engineer, a GKE user, Knative, Talos Linux, Arista and others on the containerd tracker. Roughly eight teams over five years. Causes differed, and some cases got fixes. But the pattern kept returning: pods stuck Terminating, debugging tools hanging, someone killing processes by hand, and more often than not the thread ends without a stack trace from inside the sandbox.&lt;/p&gt;

&lt;p&gt;Two teams filing in the same week broke that pattern: our production evidence and proposed fix met the other team's internal stack dump.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Five years of the same symptom, with different root causes underneath. Most threads ended with the manual workaround, until two half-reports completed each other.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The relay diagram near the top of this post still isn't fully green. The merged fix repairs the layer where the hang started. We filed gvisor#14548 to bound shim kill waits, then SIGKILL the sandbox tree, and containerd#14081 to SIGKILL the shim after repeated kill RPC timeouts. In review of the shim PR, gvisor#14549, the same maintainer asked to narrow it to bounded waits on Kill, Stats and Status, because killing the sandbox from the shim would take out healthy containers too, the same over-reaction he flagged in the first fix. That revision is in progress, and escalation stays open. Either escalation, as originally filed, would have capped our incident at minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;A watchdog that only barks is not a watchdog.&lt;/strong&gt; If code detects a should-never-happen state, it must act: retry, self-report, fail. Grep your own codebase for deadline checks whose only body is a log line. This one surfaced because a sandbox sat stuck for five days behind it.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;File the issue even with half the evidence.&lt;/strong&gt; Our report had no stacks and said so plainly. Within a day a stranger's independent report supplied them, and their dump changed the fix. The half-report you are embarrassed to file is someone else's missing half.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Remediate the smallest thing that unblocks you.&lt;/strong&gt; Kill the subprocess, not the sandbox. The instinct under pressure is the big hammer. Review cut the kill from the whole sentry to one subprocess. The same restraint belongs in your incident runbooks.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Audit your escalation paths for circular dependencies.&lt;/strong&gt; Walk your stack with that diagram's two questions: bounded wait, and escalation that works without the target's cooperation. If the answer to the second bottoms out at "a human with SSH", write that down, because that is your actual design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix shipped in gVisor release-20260831.0. This one has an ending, and the next report of this hang will arrive with its stacks attached.&lt;/p&gt;

&lt;p&gt;This post was written by Nahum Litvin.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A zero timeout in Go is not no timeout</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/a-zero-timeout-in-go-is-not-no-timeout-44gk</link>
      <guid>https://dev.to/nahumlitvin/a-zero-timeout-in-go-is-not-no-timeout-44gk</guid>
      <description>&lt;p&gt;While working on snapshotter timeouts in containerd, I assumed a timeout of 0 meant "no limit". It doesn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// context deadline exceeded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deadline is now, so the context is expired before the first call. containerd's &lt;code&gt;timeout.WithContext&lt;/code&gt; passes the configured value straight to &lt;code&gt;context.WithTimeout&lt;/code&gt;, so a key set to 0 fails every call on the spot.&lt;/p&gt;

&lt;p&gt;That's why the new &lt;code&gt;default_timeout&lt;/code&gt; for proxy snapshotters is off when empty, and only wraps calls whose context has no deadline yet: &lt;a href="https://github.com/containerd/containerd/pull/14187" rel="noopener noreferrer"&gt;https://github.com/containerd/containerd/pull/14187&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 12 second &lt;code&gt;go run&lt;/code&gt; settled it. Cheaper than arguing about it.&lt;/p&gt;

</description>
      <category>containerd</category>
      <category>go</category>
    </item>
    <item>
      <title>containerd part 3: the fix got reverted</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/containerd-part-3-the-fix-got-reverted-2cbl</link>
      <guid>https://dev.to/nahumlitvin/containerd-part-3-the-fix-got-reverted-2cbl</guid>
      <description>&lt;p&gt;People share their victories. A month ago I shared mine: my containerd fix got merged after 40 days and 30 review rounds.&lt;/p&gt;

&lt;p&gt;13 days later it got reverted. It never made it into a release.&lt;/p&gt;

&lt;p&gt;My fix put a timeout on the one call that hung on my nodes. It worked for my incident, and it felt like a win.&lt;/p&gt;

&lt;p&gt;So I opened a second PR to cover more of the calls that could hang the same way.&lt;/p&gt;

&lt;p&gt;That's where it came apart. Working through it with the maintainers, we saw the real problem was one layer out. The thing hanging was an external plugin, and it can hang on any of its calls. Patching them one at a time would never end.&lt;/p&gt;

&lt;p&gt;The right fix is a single timeout where containerd talks to the plugin, covering every call at once. That made my first fix redundant, so with a release about to be cut, Michael Brown reverted it. I agreed.&lt;/p&gt;

&lt;p&gt;In Part 2 I said the review is the distance between a patch that's correct for your incident and one that's correct for everyone. Turns out it doesn't stop at merge.&lt;/p&gt;

&lt;p&gt;A new PR is on its way. Round 31. Not giving up.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>containerd part 2: 17 lines, 40 days, 30 reviews</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/containerd-part-2-17-lines-40-days-30-reviews-14ef</link>
      <guid>https://dev.to/nahumlitvin/containerd-part-2-17-lines-40-days-30-reviews-14ef</guid>
      <description>&lt;p&gt;The fix was 17 lines. Getting it into containerd took 40 days and 30 review rounds, and taught me more than writing it did.&lt;/p&gt;

&lt;p&gt;Part 2 of the bricked-node story: the resilience patch got merged upstream this week. My biggest open source contribution to date.&lt;/p&gt;

&lt;p&gt;Writing the code was the easy part. The review was where the real engineering happened.&lt;/p&gt;

&lt;p&gt;The timeout key got renamed twice, because naming in a project with hundreds of config keys is an API commitment, not a variable name.&lt;/p&gt;

&lt;p&gt;The default went from 30 minutes to 5 and back to 30. I pushed hard for 5, a maintainer pushed back with an argument I hadn't considered: the old behavior was unbounded, so on upgrade the least surprising default is the big one, and anyone running remote snapshotters can tune it down. He was thinking about thousands of clusters upgrading. I was thinking about mine.&lt;/p&gt;

&lt;p&gt;My test got rewritten around Go's synctest, so it runs in milliseconds on a fake clock instead of mutating global state.&lt;/p&gt;

&lt;p&gt;And when CI wedged in the merge queue, Michael Brown restarted the buckets, herded the reviewers, and queued it again until it landed. Maintainers do this invisible work daily, for free, for strangers.&lt;/p&gt;

&lt;p&gt;A patch that's correct for your incident and a patch that's correct for everyone running containerd are two different things. The review is the distance between the two.&lt;/p&gt;

&lt;p&gt;What's the hardest review you've been through, and what did it fix that the code didn't?&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>opensource</category>
      <category>go</category>
    </item>
    <item>
      <title>A node that reported Ready and started nothing: four parts, one containerd fix</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/a-node-that-reported-ready-and-started-nothing-four-parts-one-containerd-fix-5gdk</link>
      <guid>https://dev.to/nahumlitvin/a-node-that-reported-ready-and-started-nothing-four-parts-one-containerd-fix-5gdk</guid>
      <description>&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;In July one of our Kubernetes nodes stopped starting pods for hours. It reported Ready the entire time. The scheduler kept placing pods on it, every new container hung in creation, and nothing on the node's health path looked wrong.&lt;/p&gt;

&lt;p&gt;We run untrusted user code at Wix, so pods come and go all day. A node that silently stops starting them is worse than a node that dies: the scheduler keeps feeding it.&lt;/p&gt;

&lt;p&gt;The easy answer was a file descriptor limit. The snapshotter logs were full of "too many open files". Raise the limit, restart, move on. I almost shipped that.&lt;/p&gt;

&lt;p&gt;This is the whole story in one place: the outage, the two bugs, the fix that got reverted, and the one that stays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Two bugs lined up.&lt;/p&gt;

&lt;p&gt;The first was in containerd. During snapshot garbage collection, containerd calls the snapshotter's Remove while holding that snapshotter's write lock in containerd's metadata layer, with no deadline. It even drops the caller's cancellation. If the snapshotter never answers, the lock is held forever, and every snapshot operation on the node queues behind it, including the one that starts a new pod sandbox. Nothing on the readiness path takes that lock, which is why the node kept saying Ready.&lt;/p&gt;

&lt;p&gt;Readiness is a separate path. Kubelet asks the runtime for its status, the runtime answers from memory, and nobody touches the snapshot metadata. So the node was healthy by every check we had, and useless by the only one that mattered: can it start a container.&lt;/p&gt;

&lt;p&gt;The second was the trigger, in AWS's soci-snapshotter, the plugin that lazily loads container images. Every file verified on first access opened a reader from the span cache and never closed it. The sibling function about 45 lines up closes the exact same reader. One leaked file descriptor per file, on every node running lazy-loaded images. Under a burst of pod starts the snapshotter hit its limit of 65,535 and stopped answering. containerd was waiting on it, holding the lock.&lt;/p&gt;

&lt;p&gt;A proxy snapshotter like soci runs in its own process, and containerd talks to it over gRPC. That boundary is where the real problem lived, though it took me two more PRs to see it.&lt;/p&gt;

&lt;p&gt;The goroutine dumps told the story once we knew where to look. Hundreds of goroutines parked on that snapshotter lock, and one GC goroutine holding it, waiting for a gRPC header from the snapshotter that was never coming. The node was not broken. It was waiting, politely and forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we got wrong first
&lt;/h2&gt;

&lt;p&gt;I got the diagnosis wrong twice before I got it right. After the storm the file descriptor count looked low, 151 out of 65,535, so I concluded the limit theory was wrong. Then I flip-flopped back. The leaked descriptors had been reclaimed once Go's finalizers ran, so the steady state hid the peak.&lt;/p&gt;

&lt;p&gt;I did most of that investigation with Claude, reading goroutine dumps from the wedged node and two unfamiliar codebases side by side. It didn't write the fix. It made the investigation cheap enough to do properly, including catching my own wrong conclusion.&lt;/p&gt;

&lt;p&gt;The bigger mistake came later. My first containerd fix put a timeout on the one call that hung on my nodes, the GC's Remove. It was correct for my incident. It got merged after 40 days and 30 review rounds, and I wrote about it as a win.&lt;/p&gt;

&lt;p&gt;Those 30 rounds were the real engineering. The timeout's config key got renamed twice, because in a project with hundreds of keys a name is an API commitment. The default went from 30 minutes to 5 and back to 30. I pushed hard for 5. A maintainer pushed back: the old behavior was unbounded, so on upgrade the least surprising default is the big one, and anyone running remote snapshotters can tune it down. He was thinking about thousands of clusters upgrading. I was thinking about mine.&lt;/p&gt;

&lt;p&gt;My test got rewritten around Go's synctest, so it runs in milliseconds on a fake clock instead of sleeping and mutating global state. And when CI wedged in the merge queue, Michael Brown restarted the buckets, herded reviewers and queued it again until it landed.&lt;/p&gt;

&lt;p&gt;Then I opened a second PR to cover another call that could hang the same way. Working through it with the maintainers, we saw that I was patching symptoms one call at a time. The thing that hangs is an external plugin, and it can hang on any of its calls. With a release about to be cut, Michael Brown reverted my first fix 13 days after it merged, so it never shipped. I agreed with the call.&lt;/p&gt;

&lt;p&gt;The suggestion that changed the plan came in that second PR's thread: stop bounding components one by one, and put the bound where containerd talks to the plugin. Michael agreed. Reverting my first fix was the price of doing it that way, because two mechanisms bounding the same call would have been worse than one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;soci-snapshotter: close the reader in file Verify. One line, defer r.Close(), merged upstream in July.&lt;/li&gt;
&lt;li&gt;containerd, first attempt: bound the GC path's Remove with a configurable timeout. Merged, then reverted before release, because it fixed one call out of many.&lt;/li&gt;
&lt;li&gt;containerd, the fix that stays: a default_timeout setting on proxy plugins. When it is set, every call containerd makes to a proxy snapshotter gets that deadline if the incoming context has none. A caller that brings its own deadline keeps it. Leave it empty and nothing changes on upgrade.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The proxy snapshotter has 10 gRPC methods and none of them had a deadline unless the caller already set one. Putting the bound at the client covers all 10 at once, including the GC path my first fix targeted. It also covers paths I never hit in production, which is the point.&lt;/p&gt;

&lt;p&gt;Streaming calls needed a decision. Walk gets one deadline for the whole stream, not one per message, so time spent in callbacks counts against it. The content store proxy has the same gap but a different shape: a writer outlives the call that created it, so a per-call deadline would cut an ingest that is still being written. That one needs its own setting and its own PR.&lt;/p&gt;

&lt;p&gt;The review on the final PR was shorter, and it still taught me something. I had added an options argument to the exported constructor. Compatible, I thought. The maintainer pointed out it still breaks anyone who stores that function as a value, so the old constructor stays untouched and a new one takes the options.&lt;/p&gt;

&lt;p&gt;The SOCI side was the quick part. The PR went in on July 15 and AWS merged it five days later. The containerd side took three PRs and two months. The difference is not the size of the change. A one-line fix to an obvious leak has one right answer. A timeout in a runtime that thousands of clusters run has many plausible answers, and the review is where you find out which one the project can live with.&lt;/p&gt;

&lt;p&gt;It doesn't fix everything, and the PR says so. A GC pass over many slow snapshots can still add up while it holds the lock, because each call finishes just under the limit. That needs the lock itself reworked, and it is the next piece.&lt;/p&gt;

&lt;p&gt;Four posts, three PRs, one revert. The fix that finally lands is smaller than the one that got reverted, and it covers more. That is usually how it goes when the review is doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A patch that is correct for your incident and a patch that is correct for everyone running the project are different things. The review is the distance between them.&lt;/li&gt;
&lt;li&gt;Review doesn't stop at merge. Mine got reverted, and the revert was the right engineering.&lt;/li&gt;
&lt;li&gt;Put the bound at the boundary. One deadline where containerd talks to the plugin covers every call; one deadline per call never ends.&lt;/li&gt;
&lt;li&gt;The steady state lies. Look at the peak, not the numbers after the storm.&lt;/li&gt;
&lt;li&gt;Maintainers do invisible work daily, for free, for strangers. Say so when they do it for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits and links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Michael Brown and the containerd maintainers, for 30 review rounds, a revert I agreed with, and a better fix&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/containerd/containerd/pull/13799" rel="noopener noreferrer"&gt;containerd/containerd#13799&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/containerd/containerd/pull/14187" rel="noopener noreferrer"&gt;containerd/containerd#14187&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/soci-snapshotter/pull/2043" rel="noopener noreferrer"&gt;awslabs/soci-snapshotter#2043&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>Krew is K8s Game Changer!</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Thu, 11 Jul 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/krew-is-k8s-game-changer-573d</link>
      <guid>https://dev.to/nahumlitvin/krew-is-k8s-game-changer-573d</guid>
      <description>&lt;p&gt;Kubernetes can be quite complex, but it doesn't have to slow you down. Here's how I simplified my K8s management with some awesome Krew plugins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://krew.sigs.k8s.io/docs/user-guide/setup/install/" rel="noopener noreferrer"&gt;installation guide for Krew&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://krew.sigs.k8s.io/plugins/" rel="noopener noreferrer"&gt;list of all Krew plugins&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://artifacthub.io/packages/search" rel="noopener noreferrer"&gt;alternate list of all plugins&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discovering Krew Plugins
&lt;/h3&gt;

&lt;p&gt;When I first started using Kubernetes, I found it overwhelming to manage all the resources efficiently. That's when I stumbled upon &lt;strong&gt;Krew&lt;/strong&gt; , a package manager for kubectl plugins, which transformed my workflow. Here are the plugins that made a huge difference for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/ahmetb/kubectl-foreach/tree/master" rel="noopener noreferrer"&gt;&lt;strong&gt;foreach&lt;/strong&gt;&lt;/a&gt; This plugin is a game-changer. Imagine being able to execute commands across multiple resources with just one line. It's perfect for bulk operations, saving me a ton of time when managing multiple pods or services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/corneliusweig/ketall" rel="noopener noreferrer"&gt;get-all&lt;/a&gt;&lt;/strong&gt; Getting a comprehensive view of all resources in a namespace used to be a hassle. Not like using kubectl get all With get-all, I can actually retrieve every resource type in one go.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vladimirvivien/ktop" rel="noopener noreferrer"&gt;&lt;strong&gt;ktop&lt;/strong&gt;&lt;/a&gt; Monitoring cluster resources is crucial, and ktop makes it so much easier. It's like having a real-time dashboard for your cluster's health and performance. I check it regularly to ensure everything is running smoothly.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nilic/kubectl-netshoot" rel="noopener noreferrer"&gt;&lt;strong&gt;netshoot&lt;/strong&gt;&lt;/a&gt; This one really changed my life. it allows me to easily debug any networking issues, attaching debug container to any pod with ease.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kvaps/kubectl-node-shell" rel="noopener noreferrer"&gt;&lt;strong&gt;node-shell&lt;/strong&gt;&lt;/a&gt; Sometimes, you just need direct access to the nodes. Node-shell allows me to get a shell on any node in the cluster. It's invaluable for troubleshooting and performing maintenance tasks directly on the node. and doesn't require ssh,ssm,ec2-connect or any of those. it is a very privileged container and might cause your security team to call you to ask WTF?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stern/stern" rel="noopener noreferrer"&gt;&lt;strong&gt;stern&lt;/strong&gt;&lt;/a&gt; Debugging applications can be tedious, especially when you need to tail logs from multiple pods. Stern makes this process seamless by streaming logs from all relevant pods in real-time. It's a lifesaver when tracking down issues across pods.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>kubernetes</category>
      <category>kubectl</category>
      <category>devops</category>
      <category>cli</category>
    </item>
    <item>
      <title>Running kubectl commands on multiple clusters</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Mon, 01 Jul 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/running-kubectl-commands-on-multiple-clusters-493b</link>
      <guid>https://dev.to/nahumlitvin/running-kubectl-commands-on-multiple-clusters-493b</guid>
      <description>&lt;p&gt;Managing multiple Kubernetes clusters can be a real headache. I know firsthand how frustrating it can be to juggle multiple contexts, keep deployments consistent, and maintain stability across different environments.&lt;/p&gt;

&lt;p&gt;so I found this awesome repo: &lt;a href="https://github.com/ahmetb/kubectl-foreach" rel="noopener noreferrer"&gt;https://github.com/ahmetb/kubectl-foreach&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run a kubectl command in one or more contexts (clusters) in parallel (similar to GNU parallel/xargs).&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl foreach &lt;span class="o"&gt;[&lt;/span&gt;OPTIONS] &lt;span class="o"&gt;[&lt;/span&gt;PATTERN]... &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;KUBECTL_ARGS...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and created a small helper function to use it. It's a simple way to run kubectl commands across multiple clusters in one go, making life a bit easier.&lt;/p&gt;

&lt;p&gt;Here's the function you can add to your bashrc or zshrc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;kfe&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"kubectl foreach -q dc1 dc2 dc3 -- &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just use kfe followed by your kubectl command, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kfe get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it will execute across dc1, dc2, and dc3 without any hassle. No more repetitive tasks or constant context switching, just a smoother workflow.&lt;/p&gt;

&lt;p&gt;I hope this helps lighten the load for anyone dealing with the complexities of Kubernetes. We're all in this together, and sharing little tools like this can make a big difference.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>kubectl</category>
      <category>bash</category>
      <category>devops</category>
    </item>
    <item>
      <title>Fixing EKS overprivileged cluster settings</title>
      <dc:creator>Nahum Litvin</dc:creator>
      <pubDate>Sun, 28 Jan 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/nahumlitvin/fixing-eks-overprivileged-cluster-settings-176j</link>
      <guid>https://dev.to/nahumlitvin/fixing-eks-overprivileged-cluster-settings-176j</guid>
      <description>&lt;p&gt;Are you aware that your Amazon EKS cluster could be at risk due to a default permission setting? By default, the IAM role that creates an EKS cluster is granted the &lt;strong&gt;&lt;em&gt;system:master&lt;/em&gt;&lt;/strong&gt; RBAC role, providing broad administrative access. This could be a significant security concern.&lt;/p&gt;

&lt;p&gt;In the past, rectifying this issue was a cumbersome process, often requiring the recreation of the entire cluster. But now, AWS has streamlined this with new cluster access management APIs. With a simple command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks delete-access-entry &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &amp;lt;CLUSTER_NAME&amp;gt; &lt;span class="nt"&gt;--principal-arn&lt;/span&gt; &amp;lt;IAM_PRINCIPAL_ARN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can adjust these permissions efficiently without the need to recreate the cluster.&lt;/p&gt;

&lt;p&gt;This development is a game-changer in EKS cluster security management, aligning with the principle of least privilege access. It's crucial for EKS users to be aware of these changes and implement them to ensure their clusters are secure.&lt;/p&gt;

&lt;p&gt;For more detailed steps and insights on this topic, make sure to read the full AWS blog post &lt;a href="https://aws.amazon.com/blogs/containers/a-deep-dive-into-simplified-amazon-eks-access-management-controls/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
