<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Bash on Lessons learned</title><link>https://eghuro.cz/tags/bash/</link><description>Recent content in Bash on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Tue, 29 Sep 2026 12:15:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/bash/index.xml" rel="self" type="application/rss+xml"/><item><title>macOS SMB: OSError: [Errno 9] Bad file descriptor after a silent reconnect</title><link>https://eghuro.cz/posts/macos-smb-ebadf-after-reconnect/</link><pubDate>Tue, 29 Sep 2026 12:15:00 +0200</pubDate><guid>https://eghuro.cz/posts/macos-smb-ebadf-after-reconnect/</guid><description>&lt;p&gt;A Python script that had encrypted backup archives on a NAS for months started failing on every run:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Traceback (most recent call last):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; File &amp;#34;/Volumes/backup/./encrypt-one.py&amp;#34;, line 74, in &amp;lt;module&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; main()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; File &amp;#34;/Volumes/backup/./encrypt-one.py&amp;#34;, line 38, in main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; with open(f, &amp;#34;rb&amp;#34;) as src:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OSError: [Errno 9] Bad file descriptor&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then the bash script that called it stopped too. Not intermittently: start the job and something fails sooner or later.&lt;/p&gt;&#10;&lt;p&gt;If you run long jobs from an SMB share on macOS and get &lt;code&gt;EBADF&lt;/code&gt; on files that are plainly there, the cause is probably a session reconnect you never saw, and the kernel log is the only place that records it.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;p&gt;macOS 27 dropped AFP, so an old NAS that speaks only SMB 2.1 got mounted over SMB instead. A bash script on the share runs &lt;code&gt;encrypt-one.py&lt;/code&gt; on a few archives in parallel with &lt;code&gt;xargs -P&lt;/code&gt;. Each run streams a 600 MB archive from the share through &lt;code&gt;openssl enc&lt;/code&gt; and writes the result back to the same share.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-dead-end"&gt;The dead end &lt;/h2&gt;&#10;&lt;p&gt;The traceback points at &lt;code&gt;open()&lt;/code&gt;, so the first move was to open and read that archive by hand. It worked, and so did a read under &lt;code&gt;xargs -P 3&lt;/code&gt;. A file that opens fine and a job that fails anyway tells you the file isn&amp;rsquo;t the problem. Something happens to the handle while it&amp;rsquo;s open.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-kernel-log"&gt;The kernel log &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;smbutil&lt;/code&gt; gave the first hint:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ smbutil statshares -m /Volumes/backup&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SMB_VERSION SMB_2.1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SESSION_RECONNECT_TIME 2026-09-29 12:01:17&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SESSION_RECONNECT_COUNT 4&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four reconnects; the last one was inside the failed run. The kernel log explains what each one does (timestamps shortened, process column removed):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/bin/log show --last 1h --style compact &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --predicate &lt;span class="s1"&gt;&amp;#39;process == &amp;#34;kernel&amp;#34; AND eventMessage CONTAINS[c] &amp;#34;smb&amp;#34;&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:15.502 (smbfs) smb_iod_sendall: id 0 ts &amp;gt; iod_lastrecv &amp;amp;&amp;amp; ts &amp;gt; sr_timesent.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:15.502 (smbfs) smb_iod_reconnect: id 0 Starting reconnect with nas.local&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:15.502 (smbfs) smb2_smb_parse_change_notify: smb_rq_reply failed 60&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:16.523 (smbfs) smb_iod_reconnect: id 0 The negotiate succeeded to nas.local&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.065 (smbfs) smb2fs_reconnect_dur_handle: Missing durable handle on &amp;lt;archive.tar.bz2.enc.tmp&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.173 (smbfs) smb2fs_reconnect_dur_handle: Missing durable handle on &amp;lt;backup.sh&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.186 (smbfs) smb2fs_reconnect_dur_handle: Missing durable handle on &amp;lt;archive.tar.bz2&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.205 (smbfs) smb_iod_reconnect: id 0: Reconnect completed successfully.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.212 (smbfs) smb2_smb_read_write_async: smb_rq_reply failed 9&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.212 (smbfs) smbfs_do_strategy: archive.tar.bz2: READ failed with an error of 9&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11:58:17.283 (smbfs) smbfs_do_strategy: archive.tar.bz2.enc.tmp: WRITE failed with an error of 9&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The NAS stopped answering under sustained I/O (error 60 is &lt;code&gt;ETIMEDOUT&lt;/code&gt;). macOS dropped the session and reconnected, and that part worked. Then it tried to reattach each open file; the server had nothing to reattach them to, and every read and write on those files failed with error 9, &lt;code&gt;EBADF&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Look at the middle line: &lt;code&gt;backup.sh&lt;/code&gt; is on the list. Bash reads a script from disk as it runs, so the script file is open the whole time and goes bad with everything else. That&amp;rsquo;s why the shell stopped as well.&lt;/p&gt;&#10;&#10;&lt;h2 id="why"&gt;Why &lt;/h2&gt;&#10;&lt;p&gt;A durable handle is how an SMB 2.x client keeps an open file across a dropped connection: the server holds the open for a while, and the client reclaims it after reconnecting. This server granted none. So &amp;ldquo;Reconnect completed successfully&amp;rdquo; is true for the session and useless for every file that was open at the time. Nothing reaches the application except &lt;code&gt;EBADF&lt;/code&gt; on the next read or write.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-fixed-it"&gt;What fixed it &lt;/h2&gt;&#10;&lt;p&gt;Stop running long I/O jobs from the share. The pipeline moved to a USB drive, and only finished files go to the NAS. That ended the failures.&lt;/p&gt;&#10;&lt;p&gt;If you can&amp;rsquo;t move off the share, two workarounds got the job past the failing step:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Make bash read the whole script before it starts, so it never goes back to the share for the next line:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ... the whole script ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bash parses the entire &lt;code&gt;{ }&lt;/code&gt; block before running any of it, and the &lt;code&gt;exit&lt;/code&gt; stops it from reading past the closing brace.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Make the I/O survive a reconnect: process the file in segments (64 MiB worked), open, write and &lt;code&gt;fsync&lt;/code&gt; each segment separately, and on &lt;code&gt;EBADF&lt;/code&gt;, &lt;code&gt;EIO&lt;/code&gt; or &lt;code&gt;EAGAIN&lt;/code&gt; reopen and retry only that segment. With AES-CBC you can chain the segments by using the last ciphertext block as the next IV, and the output is byte-identical to one &lt;code&gt;openssl enc&lt;/code&gt; pass. In the test run on the share, no reconnect happened at all, probably because the per-segment &lt;code&gt;fsync&lt;/code&gt; slowed the writes enough for the NAS to keep up.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If the server is a Samba box you control, check whether it grants durable handles: the &lt;code&gt;durable handles&lt;/code&gt; option only takes effect with &lt;code&gt;kernel oplocks = no&lt;/code&gt;, &lt;code&gt;kernel share modes = no&lt;/code&gt; and &lt;code&gt;posix locking = no&lt;/code&gt;. Turning off SMB signing on the client (&lt;code&gt;signing_required=no&lt;/code&gt; in &lt;code&gt;/etc/nsmb.conf&lt;/code&gt;) might also take enough load off a weak NAS CPU to prevent the stalls. Neither was tried here.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;On macOS, an SMB reconnect to a server without durable handles invalidates every open file on the share, including a running shell script, and the only trace is &lt;code&gt;Missing durable handle&lt;/code&gt; in the kernel log.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve seen &lt;code&gt;Errno 9&lt;/code&gt; on an SMB mount, run &lt;code&gt;smbutil statshares -a&lt;/code&gt; and look at &lt;code&gt;SESSION_RECONNECT_COUNT&lt;/code&gt; before you debug your code.&lt;/p&gt;&#10;</description></item></channel></rss>