<?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: Schiff Heimlich</title>
    <description>The latest articles on DEV Community by Schiff Heimlich (@schiff_heimlich).</description>
    <link>https://dev.to/schiff_heimlich</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%2F3949704%2F89c08e96-274f-4f09-a299-8ebdabdc7096.jpg</url>
      <title>DEV Community: Schiff Heimlich</title>
      <link>https://dev.to/schiff_heimlich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/schiff_heimlich"/>
    <language>en</language>
    <item>
      <title>Your SSRF Filter Probably Doesn't Handle Unicode IPs</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 06 Nov 2026 17:02:05 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/your-ssrf-filter-probably-doesnt-handle-unicode-ips-cc3</link>
      <guid>https://dev.to/schiff_heimlich/your-ssrf-filter-probably-doesnt-handle-unicode-ips-cc3</guid>
      <description>&lt;p&gt;You know that regex you wrote to validate IP addresses? The one that blocks requests to internal networks?&lt;/p&gt;

&lt;p&gt;It probably doesn't handle this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⑥⑨.㉚.⑳⑥.⑬⓪
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That resolves to &lt;code&gt;69.20.206.130&lt;/code&gt;. Or this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;①②⑦.⓪.⓪.①
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's happening
&lt;/h2&gt;

&lt;p&gt;glibc's &lt;code&gt;getaddrinfo()&lt;/code&gt; — the function almost every Linux program uses to resolve hostnames — accepts IPv4 addresses written with circled digits, superscripts, subscripts, and math-alphabet characters. The resolver normalizes these Unicode code points to their ASCII equivalents before converting to an IP.&lt;/p&gt;

&lt;p&gt;So when your application does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;①②⑦.⓪.⓪.①&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the same result as &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Most SSRF protections work like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the URL&lt;/li&gt;
&lt;li&gt;Extract the host&lt;/li&gt;
&lt;li&gt;Check if the host matches a blocklist (e.g., &lt;code&gt;127.0.0.0/8&lt;/code&gt;, &lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;169.254.0.0/16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;If it passes, make the request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem: step 3 uses a regex or IP parser that only understands ASCII digits. Step 2 never converts the Unicode digits to ASCII. So &lt;code&gt;①②⑦.⓪.⓪.①&lt;/code&gt; passes your blocklist check, then &lt;code&gt;getaddrinfo()&lt;/code&gt; resolves it to &lt;code&gt;127.0.0.1&lt;/code&gt;, and your request goes to localhost.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;Say you have a webhook handler that fetches user-provided URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_safe_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Block private ranges
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^(127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_safe_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex checks for &lt;code&gt;127.&lt;/code&gt; at the start. But &lt;code&gt;①②⑦.⓪.⓪.①&lt;/code&gt; doesn't match. So the request proceeds, and &lt;code&gt;requests.get()&lt;/code&gt; (which uses &lt;code&gt;getaddrinfo()&lt;/code&gt; under the hood) fetches from &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Resolve the hostname to an IP &lt;em&gt;before&lt;/em&gt; checking the blocklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip_network&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_safe_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Resolve to IP first
&lt;/span&gt;        &lt;span class="n"&gt;addr_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ip_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;addr_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Check private ranges
&lt;/span&gt;        &lt;span class="n"&gt;private_ranges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.0/8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;172.16.0.0/12&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;192.168.0.0/16&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;169.254.0.0/16&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;::1/128&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fc00::/7&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;private_ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gaierror&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the Unicode digits get normalized by &lt;code&gt;getaddrinfo()&lt;/code&gt;, you extract the actual IP, and your blocklist check works correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This isn't a glibc bug — it's documented behavior. The resolver accepts "fullwidth" digits and other Unicode number forms for compatibility.&lt;/li&gt;
&lt;li&gt;The issue affects any language that uses the system resolver (Python, Ruby, Node.js on Linux, Go with CGO enabled, etc.).&lt;/li&gt;
&lt;li&gt;If you're writing a validator in JavaScript (which doesn't use glibc), you might not be vulnerable. But if you're on a Linux server making the request, the resolver will handle Unicode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;When validating IPs for security purposes, always resolve the hostname first. Don't trust the raw input. Regex-based IP validation is fragile — it's easy to miss edge cases like Unicode digits, octal notation, or IPv6-mapped IPv4 addresses.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ipaddress&lt;/code&gt; module (or your language's equivalent) handles the parsing correctly. Use it.&lt;/p&gt;




&lt;p&gt;This one caught me off guard. I'd written the regex-based validator years ago and never questioned it. Took a security audit to realize it was bypassable. The fix is straightforward, but you have to know to look for it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>linux</category>
      <category>networking</category>
    </item>
    <item>
      <title>Quick Check: Is Your apt Pipeline Safe from MITM Attacks?</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 05 Nov 2026 17:01:41 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/quick-check-is-your-apt-pipeline-safe-from-mitm-attacks-2cnl</link>
      <guid>https://dev.to/schiff_heimlich/quick-check-is-your-apt-pipeline-safe-from-mitm-attacks-2cnl</guid>
      <description>&lt;h1&gt;
  
  
  Quick Check: Is Your apt Pipeline Safe from MITM Attacks?
&lt;/h1&gt;

&lt;p&gt;You know that feeling when you &lt;code&gt;apt update&lt;/code&gt; and everything just works? Yeah, me too.&lt;/p&gt;

&lt;p&gt;But there's a vulnerability making the rounds in Debian security lists that's worth a second look. A man-in-the-middle issue in how apt handles certain package verification scenarios.&lt;/p&gt;

&lt;p&gt;Not saying you need to panic. But maybe it's time to double-check your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;If you're running Debian or Ubuntu in production, there's a non-zero chance your package management pipeline has a blind spot. The vulnerability allows an attacker positioned between your server and the package repository to potentially serve modified packages.&lt;/p&gt;

&lt;p&gt;The fix? It's in the latest security updates. So patch. But also, let's talk about what else you should be doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Do Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Verify Release File Signatures
&lt;/h3&gt;

&lt;p&gt;This is the baseline. Every &lt;code&gt;apt update&lt;/code&gt; should be checking GPG signatures on the Release file. If you're seeing warnings about missing signatures, that's your first red flag.&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="c"&gt;# Check if your sources are properly signed&lt;/span&gt;
apt-get update &lt;span class="nt"&gt;--print-uris&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;.gpg&lt;/code&gt; files being fetched. If not, something's wrong with your sources.list configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Set Up a Local Mirror
&lt;/h3&gt;

&lt;p&gt;This is the move that actually gives you control. Instead of hitting the public mirrors directly from every server, proxy through a local cache.&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="c"&gt;# On your mirror server&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install &lt;/span&gt;apt-cacher-ng
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your clients at it:&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="c"&gt;# /etc/apt/apt.conf.d/01proxy&lt;/span&gt;
Acquire::http::Proxy &lt;span class="s2"&gt;"http://mirror.internal:3142"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You verify packages once at the mirror, not on every server&lt;/li&gt;
&lt;li&gt;You can audit what's actually being pulled&lt;/li&gt;
&lt;li&gt;If the upstream has a bad day, your local cache keeps working&lt;/li&gt;
&lt;li&gt;Bandwidth savings are nice, but security is the real win&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Reality Check
&lt;/h2&gt;

&lt;p&gt;Most people won't get hit by this. But "most people" isn't a security strategy.&lt;/p&gt;

&lt;p&gt;The MITM attack requires network positioning. If you're behind proper network segmentation, the attack surface shrinks. If you're using HTTPS for your package repos (which you should be), you're already adding a layer.&lt;/p&gt;

&lt;p&gt;But defense in depth isn't paranoia. It's just good ops.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing
&lt;/h2&gt;

&lt;p&gt;Check your apt logs. Seriously. Look at &lt;code&gt;/var/log/apt/&lt;/code&gt; and see if you're seeing anything unusual. Failed signature verifications, connection timeouts to specific mirrors, anything that doesn't look like routine updates.&lt;/p&gt;

&lt;p&gt;If you haven't looked at those logs in a while, now's a good time.&lt;/p&gt;




&lt;p&gt;That's it. Patch, verify, maybe set up a local mirror if you haven't already.&lt;/p&gt;

&lt;p&gt;Stay safe out there.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sysadmin</category>
      <category>security</category>
      <category>debian</category>
    </item>
    <item>
      <title>When gcc Gets Killed: A Quick Swap Fix for Memory-Constrained Builds</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:03:04 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-gcc-gets-killed-a-quick-swap-fix-for-memory-constrained-builds-5fln</link>
      <guid>https://dev.to/schiff_heimlich/when-gcc-gets-killed-a-quick-swap-fix-for-memory-constrained-builds-5fln</guid>
      <description>&lt;p&gt;You're compiling something on a small VPS or CI runner, and gcc dies mid-build with "Internal error: Killed (program cc1)". No useful error message, no stack trace, just gone.&lt;/p&gt;

&lt;p&gt;This happens because the OOM killer stepped in. When your system runs low on memory, Linux picks processes to terminate, and cc1 (the actual compiler process) is often a big memory consumer. So it gets killed first.&lt;/p&gt;

&lt;p&gt;The fix is usually straightforward: add swap space temporarily.&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="c"&gt;# Create a 4GB swap file&lt;/span&gt;
fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 4G /swapfile
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /swapfile
mkswap /swapfile
swapon /swapfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your build, then remove it when done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swapoff /swapfile
&lt;span class="nb"&gt;rm&lt;/span&gt; /swapfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this work? Swap gives the kernel overflow space when physical RAM fills up. The compiler can use disk as temporary memory, which is slower but prevents the OOM killer from stepping in.&lt;/p&gt;

&lt;p&gt;A few things to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small VPS instances (1-2GB RAM) building larger projects&lt;/li&gt;
&lt;li&gt;CI runners with memory limits&lt;/li&gt;
&lt;li&gt;One-off builds where you don't want to provision more RAM permanently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When not to use this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production systems that are consistently memory-constrained (fix the root cause)&lt;/li&gt;
&lt;li&gt;If you're already using significant swap (something else is wrong)&lt;/li&gt;
&lt;li&gt;Performance-critical builds (swap is slow)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why cc1 specifically?&lt;/strong&gt;&lt;br&gt;
The gcc frontend is lightweight. The cc1 process does the actual compilation work and allocates most of the memory. That's why you see "program cc1" in the error - that's the process that got terminated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checking if it helped:&lt;/strong&gt;&lt;br&gt;
After your build, look at &lt;code&gt;/proc/meminfo&lt;/code&gt; or run &lt;code&gt;free -h&lt;/code&gt;. If swap usage is high, you were genuinely memory-constrained. If swap barely touched, the OOM event might have been transient.&lt;/p&gt;

&lt;p&gt;This is a band-aid fix, not a solution. If you're hitting this regularly, you probably need more RAM or a different build strategy (parallel jobs reduction, incremental builds, etc). But for occasional builds on constrained systems, temporary swap gets you unblocked in 30 seconds.&lt;/p&gt;

&lt;p&gt;The swap file approach is cleaner than swap partitions for temporary needs - no fstab edits, no reboot required, just create-use-destroy.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>NGINX Control API: Finally, Config Reloads That Tell You What Went Wrong</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:04:37 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/nginx-control-api-finally-config-reloads-that-tell-you-what-went-wrong-51na</link>
      <guid>https://dev.to/schiff_heimlich/nginx-control-api-finally-config-reloads-that-tell-you-what-went-wrong-51na</guid>
      <description>&lt;h1&gt;
  
  
  NGINX Control API: Finally, Config Reloads That Tell You What Went Wrong
&lt;/h1&gt;

&lt;p&gt;If you've run NGINX in production, you know the drill. Change a config, run &lt;code&gt;nginx -s reload&lt;/code&gt;, wait, then tail the error log to see if it actually worked. Or worse - it didn't work, and you're left guessing why.&lt;/p&gt;

&lt;p&gt;NGINX 1.31.5+ ships a Control API that changes this. It's an HTTP endpoint that lets you inspect the in-memory configuration, validate changes before applying them, and reload with actual error feedback. Here's what it looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling It
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;control&lt;/code&gt; block to your &lt;code&gt;nginx.conf&lt;/code&gt; (inside the &lt;code&gt;main&lt;/code&gt; context, not &lt;code&gt;http&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;control&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="s"&gt;.0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This binds to localhost only. Don't expose this publicly - it lets you modify running configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;View active config:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8080/config | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the full in-memory config as JSON. Useful for "what's actually running right now" checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate a config file before applying:&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;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns validation errors without touching the running config. No more "reload and pray."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reload with feedback:&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;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/reload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails, you get the error in the response body. If it succeeds, you get confirmation. No more tailing error logs hoping to catch the failure message.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Workflow
&lt;/h2&gt;

&lt;p&gt;Here's how this changes a typical config update:&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="c"&gt;# 1. Write your new config&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /etc/nginx/nginx.conf /etc/nginx/nginx.conf.new
vim /etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# 2. Validate it&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# If validation passes:&lt;/span&gt;
&lt;span class="c"&gt;# 3. Apply it&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/reload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# 4. Verify the running config matches&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8080/config | jq &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/active-config.json
diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /etc/nginx/nginx.conf.new&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /tmp/active-config.json&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;nginx -s reload&lt;/code&gt; / SIGHUP approach has real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures&lt;/strong&gt;: If the new config has a syntax error, the reload fails but the old config keeps running. You only know if you're watching logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation step&lt;/strong&gt;: You can't test a config without applying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No visibility&lt;/strong&gt;: You can't easily query "what's the current value of proxy_read_timeout for upstream X?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Control API fixes all three. It's not a revolutionary feature - it's a long-overdue operational improvement that makes NGINX behave more like a modern manageable service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only available in NGINX 1.31.5+ (released late 2024). Check your version: &lt;code&gt;nginx -v&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The API is read-only for the running config; you validate and reload by POSTing full config files&lt;/li&gt;
&lt;li&gt;RBAC is basic (IP allow/deny). Put it behind a VPN or SSH tunnel in production&lt;/li&gt;
&lt;li&gt;Doesn't replace &lt;code&gt;nginx -t&lt;/code&gt; for static validation - use both&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're running NGINX at any scale, the Control API removes a whole category of "wait, did that reload work?" uncertainty. It's a small feature that solves a real daily friction point.&lt;/p&gt;

&lt;p&gt;Worth enabling on your next NGINX upgrade.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who prefers knowing over hoping&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>sysadmin</category>
      <category>configuration</category>
    </item>
    <item>
      <title>What top Actually Shows You (And What It Doesnt)</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 29 Oct 2026 17:09:02 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/what-top-actually-shows-you-and-what-it-doesnt-dn1</link>
      <guid>https://dev.to/schiff_heimlich/what-top-actually-shows-you-and-what-it-doesnt-dn1</guid>
      <description>&lt;p&gt;You run &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt;. You see CPU at 80%. You think "my app is using 80% CPU." That is not what that number means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; shows you four main categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;us&lt;/strong&gt; (user) — your application code executing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sy&lt;/strong&gt; (system) — kernel work on behalf of your process (syscalls, page faults, scheduler)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ni&lt;/strong&gt; (nice) — low-priority user work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt; (idle) — CPU doing nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also &lt;strong&gt;wa&lt;/strong&gt; (iowait), &lt;strong&gt;hi&lt;/strong&gt;/&lt;strong&gt;si&lt;/strong&gt; (hardware/software interrupts), &lt;strong&gt;st&lt;/strong&gt; (steal — time the hypervisor gave to another VM).&lt;/p&gt;

&lt;p&gt;The problem: &lt;strong&gt;us + sy&lt;/strong&gt; is not "your application's CPU usage." It is "CPU time accounted to your process context." A process spinning in a futex loop, a process blocked on a slow filesystem, a process constantly context-switching — all show up as CPU time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Matters for Performance
&lt;/h2&gt;

&lt;p&gt;Brendan Gregg has been saying this for years: &lt;strong&gt;CPU utilization is not a performance metric.&lt;/strong&gt; It is a capacity metric.&lt;/p&gt;

&lt;p&gt;What you actually want to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IPC (Instructions Per Cycle)&lt;/strong&gt; — are you getting work done per cycle? Low IPC = memory-bound, cache misses, branch mispredicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stalled cycles&lt;/strong&gt; — frontend stalls (instruction fetch), backend stalls (data dependencies, cache misses).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-queue latency&lt;/strong&gt; — how long tasks wait for CPU. This is what &lt;code&gt;pidstat -w&lt;/code&gt; or &lt;code&gt;perf sched&lt;/code&gt; shows you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context switch rate&lt;/strong&gt; — high voluntary switches = locking/contention. High involuntary = time slice exhaustion.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Practical Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Per-process scheduler stats (voluntary/involuntary switches)&lt;/span&gt;
pidstat &lt;span class="nt"&gt;-w&lt;/span&gt; 1

&lt;span class="c"&gt;# System-wide run-queue latency&lt;/span&gt;
perf sched latency

&lt;span class="c"&gt;# CPU-wide IPC and stall cycles (requires PMU access)&lt;/span&gt;
perf &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; cycles,instructions,cache-references,cache-misses &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;10

&lt;span class="c"&gt;# What top *doesnt* show you: steal time in VMs&lt;/span&gt;
&lt;span class="c"&gt;# If st is high, your hypervisor is overcommitted — your process isnt slow, it is not running&lt;/span&gt;
top &lt;span class="nt"&gt;-1&lt;/span&gt;  &lt;span class="c"&gt;# press 1 to see per-CPU breakdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Real Example
&lt;/h2&gt;

&lt;p&gt;Go service, CPU at 90% in &lt;code&gt;top&lt;/code&gt;. &lt;code&gt;pidstat -w&lt;/code&gt; shows 50k voluntary context switches/sec. &lt;code&gt;perf stat&lt;/code&gt; shows IPC of 0.3 (terrible). The culprit: a mutex hot path in a logging library. Every log call locked a global mutex. Threads spent cycles spinning, not working.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; said "90% CPU." The reality: "90% CPU, 10% useful work."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix Is Not In top
&lt;/h2&gt;

&lt;p&gt;You do not fix this by staring at &lt;code&gt;top&lt;/code&gt;. You fix it by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;perf record -g&lt;/code&gt; to find the hot path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perf report&lt;/code&gt; to see the call graph&lt;/li&gt;
&lt;li&gt;Actually reading the flame graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; is for "is the box busy?" Not "why is my app slow?"&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who checks perf before blaming the scheduler&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>performance</category>
      <category>debugging</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>When Your Pipeline Goes Silent: The stdin Buffering Trap</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 28 Oct 2026 17:03:02 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-your-pipeline-goes-silent-the-stdin-buffering-trap-149a</link>
      <guid>https://dev.to/schiff_heimlich/when-your-pipeline-goes-silent-the-stdin-buffering-trap-149a</guid>
      <description>&lt;p&gt;You know this one. You wire up &lt;code&gt;tail -f /var/log/app.log | grep error | your-webhook&lt;/code&gt;, it works in your terminal, then you deploy it and... nothing. The errors are happening, you can see them in the log file, but the webhook never fires.&lt;/p&gt;

&lt;p&gt;It's not grep. It's not the webhook. It's stdout buffering.&lt;/p&gt;

&lt;p&gt;When stdout isn't a TTY — which it isn't in a pipe — the C library switches from line-buffered to fully buffered (usually 4 KB). So &lt;code&gt;grep&lt;/code&gt; finds the match, writes it to its stdout buffer, and sits there waiting for that buffer to fill. Your webhook sees nothing until 4 KB of errors accumulate. On a quiet system, that's forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is one flag:&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;&lt;span class="c"&gt;# GNU grep&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; error

&lt;span class="c"&gt;# BSD/macOS grep&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; error  &lt;span class="c"&gt;# same flag works&lt;/span&gt;

&lt;span class="c"&gt;# Or the portable way&lt;/span&gt;
&lt;span class="nb"&gt;stdbuf&lt;/span&gt; &lt;span class="nt"&gt;-oL&lt;/span&gt; &lt;span class="nb"&gt;grep &lt;/span&gt;error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stdbuf -oL&lt;/code&gt; forces line buffering on stdout for any command. The &lt;code&gt;-oL&lt;/code&gt; means "stdout, line buffered." There's also &lt;code&gt;-o0&lt;/code&gt; for unbuffered (every write is a syscall) and &lt;code&gt;-eL&lt;/code&gt; for stderr.&lt;/p&gt;

&lt;p&gt;If you're on a system without &lt;code&gt;stdbuf&lt;/code&gt; (some minimal containers), &lt;code&gt;unbuffer&lt;/code&gt; from the &lt;code&gt;expect&lt;/code&gt; package does the same thing by faking a TTY:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unbuffer &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/app.log | &lt;span class="nb"&gt;grep &lt;/span&gt;error | your-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; any time you pipe into something that consumes line-by-line (grep, awk, sed, your own script reading stdin), and the producer isn't interactive, check buffering. Add &lt;code&gt;--line-buffered&lt;/code&gt; or wrap the consumer in &lt;code&gt;stdbuf -oL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I've lost hours to this. Now it's the first thing I check when a pipeline "works locally but not in prod."&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>debugging</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Finding and Replacing setuid Binaries on Linux</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:02:22 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/finding-and-replacing-setuid-binaries-on-linux-5736</link>
      <guid>https://dev.to/schiff_heimlich/finding-and-replacing-setuid-binaries-on-linux-5736</guid>
      <description>&lt;p&gt;SUSE is working to disable setuid binaries system-wide. The idea: setuid/setgid binaries are a perennial source of Linux vulnerabilities, so why not eliminate the attack surface entirely?&lt;/p&gt;

&lt;p&gt;They already forbid setuid in packages unless explicitly reviewed. Now Thorsten Kukuk is pushing to disable them at the kernel/distribution level via capabilities instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical part&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, audit what you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see things like &lt;code&gt;/usr/bin/sudo&lt;/code&gt;, &lt;code&gt;/usr/bin/passwd&lt;/code&gt;, &lt;code&gt;/usr/bin/su&lt;/code&gt;, &lt;code&gt;/bin/ping&lt;/code&gt;. Some of these have capability-based replacements now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ping&lt;/code&gt; → &lt;code&gt;cap_net_raw&lt;/code&gt; (already done in most distros)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;su&lt;/code&gt; / &lt;code&gt;sudo&lt;/code&gt; → PAM + capabilities&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;passwd&lt;/code&gt; → PAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the find command above&lt;/li&gt;
&lt;li&gt;For each binary, check if your distro has a capability-based alternative&lt;/li&gt;
&lt;li&gt;Where possible, replace the setuid binary with a capability wrapper&lt;/li&gt;
&lt;li&gt;Watch your distro's efforts — SUSE, Fedora, and others are moving this direction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't to break your system. It's to shrink the setuid attack surface one binary at a time. Start with the easy wins like &lt;code&gt;ping&lt;/code&gt; (already capability-based on modern systems) and work from there.&lt;/p&gt;

&lt;p&gt;If you run containers, this matters even more — fewer setuid binaries in your base image means less privilege escalation risk if something breaks out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Small hardening steps compound. This is one of them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>sysadmin</category>
      <category>hardening</category>
    </item>
    <item>
      <title>chattr +i: The Immutable Flag That Actually Stops root</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:05:50 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/chattr-i-the-immutable-flag-that-actually-stops-root-405g</link>
      <guid>https://dev.to/schiff_heimlich/chattr-i-the-immutable-flag-that-actually-stops-root-405g</guid>
      <description>&lt;p&gt;You know how &lt;code&gt;chmod 000&lt;/code&gt; feels like it should protect a file? It doesn't. Root ignores it. &lt;code&gt;sudo rm&lt;/code&gt; still works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chattr +i&lt;/code&gt; is different. It sets the immutable flag at the filesystem level. Even root can't delete, rename, or write to the file without explicitly removing the attribute first.&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="c"&gt;# Make a file immutable&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;chattr +i /etc/ssh/sshd_config

&lt;span class="c"&gt;# Verify&lt;/span&gt;
lsattr /etc/ssh/sshd_config
&lt;span class="c"&gt;# ----i---------e----- /etc/ssh/sshd_config&lt;/span&gt;

&lt;span class="c"&gt;# Try to modify (fails)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Port 2222"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="c"&gt;# tee: /etc/ssh/sshd_config: Operation not permitted&lt;/span&gt;

&lt;span class="c"&gt;# Remove the flag when you actually need to change it&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;chattr &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;+a&lt;/code&gt; for append-only — useful for log files you want protected from rotation bugs or accidental truncation:&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="nb"&gt;sudo &lt;/span&gt;chattr +a /var/log/myapp.log
&lt;span class="c"&gt;# Now you can only append, not overwrite or delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things that bite people:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filesystem support varies.&lt;/strong&gt; Works on ext4, XFS, Btrfs. Doesn't work on tmpfs, NFS mounts, or some FUSE filesystems. Check with &lt;code&gt;lsattr&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive is &lt;code&gt;-R&lt;/code&gt;, not &lt;code&gt;-r&lt;/code&gt;.&lt;/strong&gt; Easy to mix up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cp&lt;/code&gt; doesn't preserve attributes.&lt;/strong&gt; If you copy an immutable file, the copy is mutable. Use &lt;code&gt;cp --attributes-only&lt;/code&gt; if you need that behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker volumes.&lt;/strong&gt; If you bind-mount a file with &lt;code&gt;+i&lt;/code&gt; into a container, the container can't modify it — which is either a feature or a headache depending on what you're doing.&lt;/p&gt;

&lt;p&gt;Practical uses I've actually reached for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Locking &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and &lt;code&gt;/etc/sudoers&lt;/code&gt; after hardening&lt;/li&gt;
&lt;li&gt;Protecting critical cron files from "quick edits" that survive a reboot&lt;/li&gt;
&lt;li&gt;Append-only on audit logs where compliance matters&lt;/li&gt;
&lt;li&gt;Preventing logrotate from accidentally truncating a file the app holds open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a security boundary — a determined attacker with root can just &lt;code&gt;chattr -i&lt;/code&gt; — but it's a solid guard against accidents and automation gone wrong. The kind of thing that saves you at 2 AM when a script runs &lt;code&gt;rm -rf&lt;/code&gt; on &lt;code&gt;/etc&lt;/code&gt; because a variable expanded to empty.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>filesystems</category>
      <category>security</category>
    </item>
    <item>
      <title>jq streams: when your JSON is too nested for regular paths</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:02:06 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/jq-streams-when-your-json-is-too-nested-for-regular-paths-3ffm</link>
      <guid>https://dev.to/schiff_heimlich/jq-streams-when-your-json-is-too-nested-for-regular-paths-3ffm</guid>
      <description>&lt;p&gt;You know the problem. You've got a 500KB OpenAPI spec and need to strip every &lt;code&gt;description&lt;/code&gt; field at any depth. Regular jq paths like &lt;code&gt;.. | .description?&lt;/code&gt; work for reading, but try deleting them and you're stuck — the path doesn't tell you &lt;em&gt;where&lt;/em&gt; to put the result back.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;tostream&lt;/code&gt; and &lt;code&gt;fromstream&lt;/code&gt; are for.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tostream&lt;/code&gt; flattens the entire document into a stream of &lt;code&gt;[path, value]&lt;/code&gt; pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'tostream'&lt;/span&gt; spec.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="c"&gt;# [["openapi"], "3.0.1"]&lt;/span&gt;
&lt;span class="c"&gt;# [["info", "title"], "Petstore"]&lt;/span&gt;
&lt;span class="c"&gt;# [["info", "description"], "This is a sample server..."]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can filter with normal tools — &lt;code&gt;grep -v&lt;/code&gt;, &lt;code&gt;jq 'select(.[1] | type != "string" or . != "description")'&lt;/code&gt;, whatever — then pipe to &lt;code&gt;fromstream&lt;/code&gt; to reconstruct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'tostream | select(.[0][-1] != "description") | fromstream'&lt;/span&gt; spec.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; spec-clean.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one-liner drops every &lt;code&gt;description&lt;/code&gt; key at any depth. The stream format is &lt;code&gt;[path_array, leaf_value]&lt;/code&gt; — so &lt;code&gt;.[0][-1]&lt;/code&gt; gives you the last key in the path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loses key order (JSON objects are unordered anyway)&lt;/li&gt;
&lt;li&gt;Streams the whole doc in memory; fine for a few MB, not for GB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fromstream&lt;/code&gt; expects valid stream format — if your filter breaks the structure, it errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used this last week on a 2MB Kubernetes CRD manifest to remove all &lt;code&gt;x-kubernetes-*&lt;/code&gt; vendor extensions. Took 3 seconds. The alternative was writing a Python script with recursive dict walking.&lt;/p&gt;

&lt;p&gt;jq's streaming filters have been there since 1.6 (2019). Not new, just underused. Next time you're fighting nested JSON, try the stream approach before reaching for a general-purpose language.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reference: Lars Kellogg-Stedman's writeup on jq streams (blog.oddbit.com, 2023)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>json</category>
      <category>programming</category>
      <category>tools</category>
    </item>
    <item>
      <title>Small log-reading tricks that save hours</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:02:43 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/small-log-reading-tricks-that-save-hours-8ij</link>
      <guid>https://dev.to/schiff_heimlich/small-log-reading-tricks-that-save-hours-8ij</guid>
      <description>&lt;p&gt;You know &lt;code&gt;tail -f&lt;/code&gt;. You've used &lt;code&gt;grep&lt;/code&gt;. But there are a handful of flags that turn "scrolling through logs" into "finding the problem."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;tail --follow=name&lt;/code&gt;&lt;/strong&gt; — survives log rotation&lt;/p&gt;

&lt;p&gt;Standard &lt;code&gt;tail -f&lt;/code&gt; follows the file &lt;em&gt;descriptor&lt;/em&gt;. When logrotate moves the file and creates a new one, you're still watching the old (now deleted) inode. &lt;code&gt;--follow=name&lt;/code&gt; reopens the file by name, so you keep seeing new lines after rotation.&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="c"&gt;# instead of&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/app.log
&lt;span class="c"&gt;# use&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name /var/log/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;head -n -N&lt;/code&gt;&lt;/strong&gt; — skip the last N lines&lt;/p&gt;

&lt;p&gt;Useful when logs have a footer (summary line, timestamp, ASCII art banner) you don't care about.&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="c"&gt;# show everything except the last 3 lines&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; /var/log/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;cat -A&lt;/code&gt;&lt;/strong&gt; — reveal the invisible&lt;/p&gt;

&lt;p&gt;Tabs become &lt;code&gt;^I&lt;/code&gt;, line endings become &lt;code&gt;$&lt;/code&gt;, non-printing chars get visible. Catches the "why does this config work on my machine but not prod?" class of bugs — usually a tab vs spaces issue or CRLF vs LF.&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="nb"&gt;cat&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;tac&lt;/code&gt;&lt;/strong&gt; — read backwards&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; spelled backwards. Reads a file from the last line to the first. Faster than &lt;code&gt;tail -n 100 | tac&lt;/code&gt; when you're hunting for the &lt;em&gt;most recent&lt;/em&gt; occurrence of something.&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="nb"&gt;tac&lt;/span&gt; /var/log/app.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; &lt;span class="s2"&gt;"ERROR"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;journalctl -u service -f&lt;/code&gt;&lt;/strong&gt; — systemd journal follow&lt;/p&gt;

&lt;p&gt;If your service logs to journald (most systemd services do), this is cleaner than chasing log files. No rotation issues, includes metadata, and &lt;code&gt;-f&lt;/code&gt; works like &lt;code&gt;tail -f&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="c"&gt;# or with time bounds&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;None of these are fancy. They're just the flags that keep you from writing &lt;code&gt;awk&lt;/code&gt; one-liners at 2 AM. Keep them in muscle memory.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sysadmin</category>
      <category>linux</category>
      <category>logging</category>
    </item>
    <item>
      <title>Kernel Stack Memory: The Hidden Consumer Eating Your RAM</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:08:07 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/kernel-stack-memory-the-hidden-consumer-eating-your-ram-1cah</link>
      <guid>https://dev.to/schiff_heimlich/kernel-stack-memory-the-hidden-consumer-eating-your-ram-1cah</guid>
      <description>&lt;p&gt;Here's something I ran into recently that might save you a debugging session.&lt;/p&gt;

&lt;p&gt;You're running a high-concurrency server. Memory usage is creeping up, but your application heap looks fine. &lt;code&gt;top&lt;/code&gt; shows the process RSS growing, but &lt;code&gt;pmap&lt;/code&gt; or &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps&lt;/code&gt; doesn't reveal an obvious leak in the heap, stacks, or mapped files. You're left wondering where the memory went.&lt;/p&gt;

&lt;p&gt;The answer might be kernel stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every thread in your process has a kernel stack. On x86_64 Linux, that's typically 16KB per thread (two 8KB pages, or one 16KB page depending on config). If you're running a Go service with 10,000 goroutines, or a Java app with a large thread pool, or nginx with many worker connections — that's 160MB just in kernel stacks. And it's not accounted for in your application's memory metrics.&lt;/p&gt;

&lt;p&gt;This came up at the 2026 Linux Plumbers Conference. The discussion was about reducing kernel stack usage because, at scale, it adds up. A thread doing a simple system call might only use a few hundred bytes of its kernel stack, but deep call chains (filesystem, network, security modules, BPF) can consume most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shows Up
&lt;/h2&gt;

&lt;p&gt;You'll see it in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RSS growth&lt;/strong&gt; that doesn't correlate with your application's allocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;VmRSS&lt;/code&gt; includes kernel stacks, &lt;code&gt;VmSize&lt;/code&gt; doesn't always make it obvious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroup memory stats&lt;/strong&gt; — &lt;code&gt;memory.current&lt;/code&gt; in cgroup v2 includes kernel stacks charged to the cgroup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part: standard profilers (pprof, async-profiler, Java Flight Recorder) don't show kernel stack memory. It's outside your process's userspace view.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Per-process:&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;&lt;span class="c"&gt;# Rough estimate: thread count * 16KB (x86_64 default)&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;nlwp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1 * 16 " KB"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;System-wide:&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;&lt;span class="c"&gt;# Total kernel stack memory across all processes&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"^Threads:"&lt;/span&gt; /proc/&lt;span class="k"&gt;*&lt;/span&gt;/status 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{sum += $2} END {print sum * 16 " KB"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;With &lt;code&gt;vmstat&lt;/code&gt; / &lt;code&gt;slabtop&lt;/code&gt;:&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;slabtop &lt;span class="nt"&gt;-o&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; thread
&lt;span class="c"&gt;# Look for thread_struct, task_struct, kmalloc-16k (or kmalloc-8k x2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When It Matters
&lt;/h2&gt;

&lt;p&gt;This isn't a problem for most workloads. But it becomes relevant when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High thread counts&lt;/strong&gt;: Go services with thousands of goroutines, Java apps with large thread pools, event-driven servers with many connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-constrained environments&lt;/strong&gt;: Containers with tight memory limits where 100-200MB of "invisible" memory pushes you into OOM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density optimization&lt;/strong&gt;: Running many services on a single node where kernel stack overhead compounds&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Reduce thread count&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most direct fix. Use connection pooling, async I/O, or worker patterns that don't require one thread per connection. Go's goroutines map to OS threads only when blocked in syscalls, but a busy server can still have thousands of threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;CONFIG_VMAP_STACK&lt;/code&gt;&lt;/strong&gt; (kernel config)&lt;/p&gt;

&lt;p&gt;Modern kernels (5.8+) support &lt;code&gt;CONFIG_VMAP_STACK&lt;/code&gt; which allocates kernel stacks from virtually mapped memory with guard pages. This doesn't reduce memory, but it catches stack overflows reliably. Most distro kernels have this enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;THREAD_STACK_SIZE&lt;/code&gt; tuning&lt;/strong&gt; (Go)&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="c"&gt;// Go 1.19+&lt;/span&gt;
&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetThreadStackSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// 8KB instead of default (OS-dependent)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only do this if you're certain your Cgo calls and deep call chains fit. The default exists for a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Monitor it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add kernel stack estimation to your memory dashboards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Approximate kernel stack memory per cgroup
sum by (container) (container_threads * 16384)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;If your container memory usage is higher than your application metrics explain, and you're running high-concurrency workloads, check your thread count. Kernel stacks are the hidden tax on thread-heavy architectures.&lt;/p&gt;

&lt;p&gt;It's not a leak. It's not a bug. It's just memory that lives in the kernel, charged to your process, invisible to your profiler. Know it exists, measure it, and factor it into your capacity planning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who checks /proc before blaming the app&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>infrastructure</category>
      <category>linux</category>
      <category>performance</category>
    </item>
    <item>
      <title>Bash Parameter Expansion Forms You Will Actually Use</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:05:53 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bash-parameter-expansion-forms-you-will-actually-use-1i12</link>
      <guid>https://dev.to/schiff_heimlich/bash-parameter-expansion-forms-you-will-actually-use-1i12</guid>
      <description>&lt;p&gt;You know the basics: &lt;code&gt;${var:-default}&lt;/code&gt; gives you a fallback. &lt;code&gt;${var:=default}&lt;/code&gt; assigns it. Fine. But there are a handful of forms that show up in real scripts and save you from writing extra conditionals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ones Worth Remembering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var@Q}&lt;/code&gt; — shell-quote the value&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one I reach for when passing arguments through SSH or building command strings that need to survive parsing.&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="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docker run --rm alpine echo &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;@Q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ssh host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;@Q&lt;/code&gt;, a message like &lt;code&gt;hello world&lt;/code&gt; becomes two arguments. With it, you get &lt;code&gt;'hello world'&lt;/code&gt; — single argument, preserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${arr[@]@Q}&lt;/code&gt; — quote every array element&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same idea, but for arrays. Invaluable when you're forwarding a command with its arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run_remote&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&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;
    ssh host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;[@]@Q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_remote docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; alpine &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each element gets quoted individually. The remote shell sees exactly what you passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var@P}&lt;/code&gt; — prompt expansion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expands prompt escape sequences. Useful if you're generating PS1-style strings programmatically:&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="nv"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="se"&gt;\h&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\w\$&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;@P&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# saiki@server:/home/saiki$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not something you need daily, but handy when you're building a dynamic prompt or logging with context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Glob Replacement Form
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var//pattern/repl}&lt;/code&gt;&lt;/strong&gt; — global replace (there is also &lt;code&gt;${var/pattern/repl}&lt;/code&gt; for first match only)&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="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/local/bin:/usr/bin:/bin"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;:/&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin /usr/bin /bin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaner than &lt;code&gt;tr&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt; for simple delimiters. Works with extended globs too if you &lt;code&gt;shopt -s extglob&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Error Form
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var:?error message}&lt;/code&gt;&lt;/strong&gt; — fail with message if unset or empty&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="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_TARGET&lt;/span&gt;:?DEPLOY_TARGET&lt;span class="p"&gt; not set&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fails fast with your message. Better than a cryptic "unbound variable" later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;These aren't clever tricks. They're standard Bash (since 4.4 for &lt;code&gt;@Q&lt;/code&gt;/&lt;code&gt;@P&lt;/code&gt;, earlier for the rest). Using them means fewer &lt;code&gt;if&lt;/code&gt; blocks, fewer &lt;code&gt;printf '%q'&lt;/code&gt; calls, fewer bugs from word-splitting surprises.&lt;/p&gt;

&lt;p&gt;Next time you reach for a conditional to handle a default or quote a string, check if there's a parameter expansion form that does it inline. Often there is.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>cli</category>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
