<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="https://pid1labs.ai/feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>pid1 Labs</title>
    <subtitle>A systems lab rethinking the operating system in the era of agentic AI. Notes on what we measured, what broke, and what we had to take back.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://pid1labs.ai/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://pid1labs.ai"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-08-09T00:00:00+00:00</updated>
    <id>https://pid1labs.ai/atom.xml</id>
    <entry xml:lang="en">
        <title>Results we took back</title>
        <published>2026-08-09T00:00:00+00:00</published>
        <updated>2026-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/results-we-took-back/"/>
        <id>https://pid1labs.ai/notes/results-we-took-back/</id>
        <summary type="text">A five-run sample told us our new probe made startup cheaper. It doesn&#39;t. Three claims we published and then had to withdraw, and what each one taught us about the sampling that produced it.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/results-we-took-back/">&lt;p&gt;A result that flatters the change you just made is the one nobody else will
check. So it should be the one you check hardest. We have not been reliably
good at this, and the three below are the evidence.&lt;/p&gt;
&lt;h2 id=&quot;1-the-probe-pays-for-itself-at-startup&quot;&gt;1. “The probe pays for itself at startup”&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;agentfs&lt;/code&gt; picks its mount backend by attempting a real overlayfs mount in a
throwaway namespace. That probe costs something on every invocation, and we
wanted to know how much.&lt;/p&gt;
&lt;p&gt;The first measurement was one 5-run sample on a one-file project: &lt;code&gt;auto&lt;/code&gt; at
15 ms/run against &lt;code&gt;fuse&lt;/code&gt; at 19 ms. We reported that the new default was
&lt;em&gt;cheaper&lt;/em&gt; to start, attached a plausible story about warm caches, and quoted
the finding in a commit message as though it had a direction.&lt;/p&gt;
&lt;p&gt;Three alternating rounds of twenty runs reverse it:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;fuse&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;kernel&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;round 1&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;7.9 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;10.4 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;round 2&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;7.5 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;10.3 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;round 3&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;7.4 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;9.9 ms&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The probe adds &lt;strong&gt;~2.6 ms&lt;/strong&gt; to every run. It does not pay for itself at
startup. It pays for itself a second later, on the first real filesystem work,
which is a fine thing for it to do and is what we should have said.&lt;/p&gt;
&lt;p&gt;What went wrong is not subtle: n=5, no interleaving, and a result that
happened to agree with the change under test. Note also that the absolute
numbers moved by more than half — 15 ms became 7.4 ms — because the
un-interleaved sample was mostly measuring cache state, not the probe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule.&lt;/strong&gt; A benchmark that isn’t interleaved is not a benchmark. It is an
anecdote about the order you ran things in.&lt;/p&gt;
&lt;h2 id=&quot;2-a-stale-overlay-workdir-breaks-the-next-mount&quot;&gt;2. “A stale overlay workdir breaks the next mount”&lt;/h2&gt;
&lt;p&gt;Every overlay mount leaves &lt;code&gt;workdir/work&lt;/code&gt; behind at mode &lt;strong&gt;000&lt;/strong&gt;. We had
cleanup code for it, and a comment in four places explaining that the next
mount would fail without it.&lt;/p&gt;
&lt;p&gt;While removing something adjacent we deleted the cleanup and re-ran
&lt;code&gt;a_second_run_still_mounts&lt;/code&gt;. It passed. Testing it directly: overlayfs mounts
perfectly happily over a stale, non-empty workdir, and cleans up its own
&lt;code&gt;work&lt;/code&gt; subdirectory as it goes.&lt;/p&gt;
&lt;p&gt;The cleanup is still correct and still there — for a completely different
reason. Mode 000 means the &lt;em&gt;user&lt;/em&gt; cannot &lt;code&gt;rm -rf&lt;/code&gt; their own project directory
afterwards, which is a real bug affecting a real person. We had shipped the
right code with the wrong justification, which meant every future reader would
reason from a false premise about the kernel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule.&lt;/strong&gt; A comment asserting that something breaks is a testable claim. If no
test covers it, it is folklore, and folklore in a codebase compounds.&lt;/p&gt;
&lt;h2 id=&quot;3-a-retraction-that-was-itself-wrong&quot;&gt;3. A retraction that was itself wrong&lt;/h2&gt;
&lt;p&gt;The most uncomfortable one. We had claimed &lt;code&gt;write&lt;/code&gt; dominated our FUSE overhead
at 74%, then retracted it after samples appeared to refute it, then
&lt;a href=&quot;https://pid1labs.ai/notes/thirty-six-percent-from-a-version-number/&quot;&gt;reinstated it&lt;/a&gt; once the
cause was understood.&lt;/p&gt;
&lt;p&gt;The refuting samples were taken during an experimental spike that had already
enabled a different FUSE protocol version. They were measuring an
already-fixed filesystem. The tell was sitting in the data the whole time:
every other operation count in those runs was exactly half a clean build’s,
while &lt;code&gt;write&lt;/code&gt; alone differed by 12×. One changed variable, read as one bad
sample.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule.&lt;/strong&gt; A spike that alters a build flag contaminates every measurement
taken while it is on, not only the one under test. We wrote this one up in
full rather than quietly correcting the file, because the failure mode —
discarding a correct result on the strength of a contaminated sample — is much
harder to notice than the ordinary kind.&lt;/p&gt;
&lt;h2 id=&quot;why-these-are-on-the-website&quot;&gt;Why these are on the website&lt;/h2&gt;
&lt;p&gt;Because the alternative is a benchmarks page where everything worked. We would
rather publish the numbers we do not stand behind next to the ones we do, and
be the sort of lab whose results you can subtract from.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A hash with a hole in it</title>
        <published>2026-08-08T00:00:00+00:00</published>
        <updated>2026-08-08T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/a-hash-with-a-hole-in-it/"/>
        <id>https://pid1labs.ai/notes/a-hash-with-a-hole-in-it/</id>
        <summary type="text">Our change detector digests inode metadata to decide whether to look. Leave one field out of that digest and an agent can edit a file without any checkpoint ever noticing.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/a-hash-with-a-hole-in-it/">&lt;p&gt;&lt;code&gt;agentfs&lt;/code&gt; snapshots an agent’s work &lt;em&gt;during&lt;/em&gt; a run, not only at the end. The
property it is trying to provide is narrow and worth stating precisely: &lt;strong&gt;the
agent cannot prevent a checkpoint.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On our FUSE backend, knowing when to snapshot is free. The server sees every
mutating operation, so it bumps an &lt;code&gt;AtomicU64&lt;/code&gt; and the watcher reads it. Exact,
no tree walk, no inotify registration, and no false positives from our own
snapshot activity.&lt;/p&gt;
&lt;p&gt;The kernel-overlayfs backend has no server. Nobody is in the path to bump
anything. So the question “did anything change?” has to be answered by
looking, and looking on every 500 ms tick has to be cheap — which means
&lt;code&gt;stat&lt;/code&gt;, not content hashing.&lt;/p&gt;
&lt;h2 id=&quot;the-tempting-minimal-digest&quot;&gt;The tempting minimal digest&lt;/h2&gt;
&lt;p&gt;Walk the upper layer, digest each entry’s inode metadata, compare the digest
to last tick’s. The obvious fields are inode number, size, and mtime. That is
what most “has this changed?” caches use, and for a build system it is fine.&lt;/p&gt;
&lt;p&gt;Here it has a hole. Consider an agent that wants to edit a file without being
snapshotted:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write the new contents in place, at exactly the same length.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utimensat&lt;/code&gt; the mtime back to what it was.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Inode number: unchanged. Size: unchanged. Mtime: restored. The digest matches,
the watcher concludes nothing happened, and &lt;strong&gt;it does not walk.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That last clause is what makes this a real failure rather than a latency bug.
A missed bump on the FUSE counter delays a checkpoint; the next mutation bumps
it again and the walk records everything. Here the walk is &lt;em&gt;gated&lt;/em&gt; on the
probe, so a digest that fails to notice a change produces a &lt;strong&gt;suppressed&lt;/strong&gt;
checkpoint, not a late one. The property at the top of this note quietly stops
holding, and nothing reports an error.&lt;/p&gt;
&lt;h2 id=&quot;st-ctime-closes-it&quot;&gt;&lt;code&gt;st_ctime&lt;/code&gt; closes it&lt;/h2&gt;
&lt;p&gt;Add ctime to the digest and the trick stops working. Ctime moves on any inode
change — including the &lt;code&gt;utimensat&lt;/code&gt; intended to cover the tracks — and
userspace cannot set it. Forging it requires &lt;code&gt;CAP_SYS_TIME&lt;/code&gt; and a clock
change, which is a different and much louder kind of attack.&lt;/p&gt;
&lt;p&gt;The general shape is worth keeping: &lt;strong&gt;when a digest is the input to a decision
about whether to look, every field you leave out is an action someone can take
invisibly.&lt;/strong&gt; Mtime is advisory metadata that the owner of a file is allowed to
rewrite. Ctime is bookkeeping the kernel keeps for itself. Only one of those
belongs in a security-relevant fingerprint.&lt;/p&gt;
&lt;h2 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h2&gt;
&lt;p&gt;About &lt;strong&gt;1%&lt;/strong&gt; of an agent-shaped workload — twelve rounds of edit, build, &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, run inside the sandbox:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;watcher off&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;watcher on&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;mean&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;10.177 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;10.293 s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;spread&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.312 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.444 s&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;+1.1%&lt;/strong&gt;, with the ranges overlapping — the &lt;code&gt;off&lt;/code&gt; arm peaked above the &lt;code&gt;on&lt;/code&gt;
arm’s minimum, so at n=4 this is not separable from noise. We trust it because
it matches the cost model rather than because the delta is convincing: a
10.2 s run at a 500 ms interval is ~20 probes, the walk measures 6.3 ms on a
corpus with a full &lt;code&gt;.git&lt;/code&gt;, so predicted 126 ms against 116 ms measured.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The scaling rule is &lt;code&gt;tax ≈ walk_cost / 500 ms&lt;/code&gt;&lt;/strong&gt;, which means what governs
it is the number of non-ignored files, not the workload. The same walk is
0.5 ms on source alone, 6.3 ms with &lt;code&gt;.git&lt;/code&gt;, and &lt;strong&gt;80 ms&lt;/strong&gt; — 16%, extremely
visible — if &lt;code&gt;target/&lt;/code&gt; were not excluded.&lt;/p&gt;
&lt;p&gt;So the ignore list is doing load-bearing performance work, and it is the first
thing to check if this tax ever appears. It also has to be &lt;em&gt;exactly&lt;/em&gt; the same
predicate the snapshot uses. Watch a path the snapshot skips and every build
fires a checkpoint that records nothing at all.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Container-ness is the wrong question</title>
        <published>2026-08-07T00:00:00+00:00</published>
        <updated>2026-08-07T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/container-ness-is-the-wrong-question/"/>
        <id>https://pid1labs.ai/notes/container-ness-is-the-wrong-question/</id>
        <summary type="text">We wanted to know when we could serve an agent&#39;s workspace with kernel overlayfs instead of our own FUSE server. Asking &quot;are we in a container?&quot; gets that wrong in both directions.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/container-ness-is-the-wrong-question/">&lt;p&gt;&lt;code&gt;agentfs&lt;/code&gt; gives an agent a writable view of your project without letting it
touch the original. Until recently that was always a FUSE server we wrote:
your files as a read-only lower layer, an upper layer catching every write,
snapshots taken from the upper.&lt;/p&gt;
&lt;p&gt;It works, and on the inner loop it costs a lot. An incremental &lt;code&gt;cargo build&lt;/code&gt;
through the sandbox ran &lt;strong&gt;2.21×&lt;/strong&gt; slower than the same build outside it. The
kernel has had its own overlay filesystem for a decade. The question was when
we could use it instead.&lt;/p&gt;
&lt;h2 id=&quot;the-obvious-probe-is-wrong-twice&quot;&gt;The obvious probe is wrong twice&lt;/h2&gt;
&lt;p&gt;The intuition is that overlayfs needs privilege, so it works on a host and
not in a container — check &lt;code&gt;/.dockerenv&lt;/code&gt; or &lt;code&gt;/proc/1/cgroup&lt;/code&gt; and branch. Both
halves of that are false.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default Docker cannot mount overlayfs at all.&lt;/strong&gt; No &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;, no
mount. So container-ness is not &lt;em&gt;sufficient&lt;/em&gt; to rule it out — it correctly
predicts this case and nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A plain host can, with no container anywhere in sight.&lt;/strong&gt; An unprivileged
user namespace holds &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; &lt;em&gt;over itself&lt;/em&gt;, and the kernel has
allowed overlayfs inside one since 5.11. We already create a user namespace
for every rootless run, so we already had the privilege we thought we
lacked. &lt;strong&gt;This is a user-namespace feature, not a container feature.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--cap-add SYS_ADMIN&lt;/code&gt; is still not enough&lt;/strong&gt; when &lt;code&gt;upperdir&lt;/code&gt; is itself on
overlayfs — which is exactly where a container’s &lt;code&gt;~/.agentfs&lt;/code&gt; lands by
default. &lt;code&gt;EINVAL&lt;/code&gt;. You may not stack an upper on an overlay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That third case is the one that kills the heuristic outright. The capability
is present, the container is privileged, and the mount still fails — because
the constraint was never about privilege. It was about which filesystem the
upper directory happens to sit on.&lt;/p&gt;
&lt;p&gt;As a bonus, the detection itself has rotted: on cgroup v2, &lt;code&gt;/proc/1/cgroup&lt;/code&gt;
says &lt;code&gt;0::/&lt;/code&gt; and nothing more.&lt;/p&gt;
&lt;h2 id=&quot;so-don-t-ask-try-it&quot;&gt;So don’t ask. Try it.&lt;/h2&gt;
&lt;p&gt;The probe performs the &lt;strong&gt;real mount&lt;/strong&gt;, in a throwaway namespace, against
scratch directories &lt;strong&gt;inside the project directory&lt;/strong&gt; — not &lt;code&gt;/tmp&lt;/code&gt;. That last
detail is the whole point: the constraint is a property of the filesystem the
upper lives on, so probing somewhere else answers a different question
convincingly.&lt;/p&gt;
&lt;p&gt;If the mount succeeds, we tear it down and use the kernel backend. If it
fails for any reason, we fall back to FUSE and say why.&lt;/p&gt;
&lt;h2 id=&quot;what-it-bought&quot;&gt;What it bought&lt;/h2&gt;
&lt;p&gt;Measured end to end through a full &lt;code&gt;agentfs run&lt;/code&gt;, so the numbers include
namespace setup, the bind set and the mount:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;task&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;native&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;fuse&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;kernel&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;cargo build (incremental)&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.824 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.819 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.839 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;grep -r&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;11.6 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;77.1 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;13.4 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;git status (dirty)&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;8.3 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;26.4 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;9.6 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;git add + commit&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;10.2 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;50.1 ms&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;27.7 ms&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The inner loop goes &lt;strong&gt;2.21× → 1.02×&lt;/strong&gt;: on the kernel backend, building inside
the sandbox costs what building outside it costs. &lt;code&gt;grep -r&lt;/code&gt; collapses the same
way, 6.67× → 1.16×.&lt;/p&gt;
&lt;p&gt;What is left is &lt;code&gt;git add + commit&lt;/code&gt; at 2.72×, and it is structural rather than
incidental. Git renames every object into its final place, and a rename across
overlay layers is a copy-up. That is the one primitive where layering itself
has a price.&lt;/p&gt;
&lt;h2 id=&quot;two-things-we-had-to-get-right&quot;&gt;Two things we had to get right&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The mount happens inside the sandbox’s own mount namespace&lt;/strong&gt;, not on the
host. That is what makes it work without root, and it has a second effect
worth more than the first: the workspace is invisible from outside. There is
no mountpoint for a stray editor to write through, and nothing left to unmount
if the sandbox dies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-o userxattr&lt;/code&gt;, always&lt;/strong&gt; — in both privilege modes. Left to itself the
kernel uses &lt;code&gt;trusted.overlay.*&lt;/code&gt; when privileged, and a later host-side restore
running as an ordinary user cannot write those. One dialect everywhere beats
matching the kernel’s default.&lt;/p&gt;
&lt;h2 id=&quot;the-cost-of-asking&quot;&gt;The cost of asking&lt;/h2&gt;
&lt;p&gt;The probe runs on every invocation and adds &lt;strong&gt;~2.6 ms&lt;/strong&gt;. It does not pay for
itself at startup — &lt;a href=&quot;https://pid1labs.ai/notes/results-we-took-back/&quot;&gt;we published the opposite claim first and had to take it
back&lt;/a&gt; — it pays a second later, on the
first real filesystem work.&lt;/p&gt;
&lt;p&gt;One implementation detail that cost an afternoon: the probe forks from a
process that is already multi-threaded, so everything the child touches has to
be built &lt;em&gt;before&lt;/em&gt; the fork. A &lt;code&gt;malloc&lt;/code&gt; in the child of a threaded fork
deadlocks if another thread happened to hold the allocator lock at fork time.
The bug does not reproduce under a debugger, naturally.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>36% from a version number</title>
        <published>2026-08-06T00:00:00+00:00</published>
        <updated>2026-08-06T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/thirty-six-percent-from-a-version-number/"/>
        <id>https://pid1labs.ai/notes/thirty-six-percent-from-a-version-number/</id>
        <summary type="text">Counting bytes rather than operations showed our FUSE server moving 295 MB in 56,634 writes. The transport could carry twelve times more per round trip; it was just never asked to.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/thirty-six-percent-from-a-version-number/">&lt;p&gt;We had spent two commits looking for the source of our FUSE overhead in the
wrong place. Adding byte counters alongside the operation counters settled it
immediately: one incremental build moved &lt;strong&gt;295 MB in 56,634 writes&lt;/strong&gt; — about
5.2 KB per call, against a transport that can carry far more per round trip.&lt;/p&gt;
&lt;p&gt;The operation count was not high because the build writes a lot. It was high
because every write was small.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fuser&lt;/code&gt; gates its FUSE protocol version behind cargo features and defaults to
&lt;code&gt;abi-7-9&lt;/code&gt;. Raising it to 7.23 opts into no new capabilities and changes
nothing else, but the negotiated maximum write goes from ~5 KB to ~65 KB:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;time&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;writes&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;bytes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;default (&lt;code&gt;abi-7-9&lt;/code&gt;)&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;2.837 / 2.811 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;56,694 / 56,591&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;294.8 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;abi-7-23&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.792 / 1.804 s&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;4,527 / 4,499&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;295.0 MB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Identical bytes, &lt;strong&gt;12.5× fewer operations, 36% faster&lt;/strong&gt;. End to end on the
real workload the inner-loop overhead falls from +1.81 s to +1.10 s — 39% of
it removed by a one-line dependency change.&lt;/p&gt;
&lt;p&gt;We measured &lt;code&gt;abi-7-31&lt;/code&gt; too: 4,047 writes, no further change in time. Taking
the lowest version that delivers the win means fewer protocol differences to
reason about and a lower kernel floor — 7.23 is Linux 4.5.&lt;/p&gt;
&lt;p&gt;The part worth keeping: we had been counting operations for two commits and
the answer was in the bytes. An operation count tells you how often you
crossed the boundary. It cannot tell you whether crossing it that often was
necessary.&lt;/p&gt;
&lt;p&gt;This also reinstated a claim we had previously retracted — the retraction was
the mistake, and it is written up &lt;a href=&quot;https://pid1labs.ai/notes/results-we-took-back/&quot;&gt;with the others&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The delete that wasn&#39;t there</title>
        <published>2026-08-05T00:00:00+00:00</published>
        <updated>2026-08-05T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/the-delete-that-wasnt-there/"/>
        <id>https://pid1labs.ai/notes/the-delete-that-wasnt-there/</id>
        <summary type="text">Two filesystems spell &quot;this file is deleted&quot; differently. We read one of those spellings straight into a branch that discarded it, and nothing errored — the next restore resurrected every file the agent had removed.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/the-delete-that-wasnt-there/">&lt;p&gt;An overlay filesystem has to record deletions somehow. The lower layer is
read-only, so removing a file that exists down there cannot actually remove
anything — it has to leave a marker in the upper layer saying “stop looking.”&lt;/p&gt;
&lt;p&gt;There are two conventions for that marker, and they could not be less alike:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;AUFS (what we write)&lt;/th&gt;&lt;th&gt;kernel overlayfs&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;delete&lt;/td&gt;&lt;td&gt;a regular file named &lt;code&gt;.wh.&amp;lt;victim&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a &lt;strong&gt;character device&lt;/strong&gt;, major 0 minor 0, named &lt;code&gt;&amp;lt;victim&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;opaque dir&lt;/td&gt;&lt;td&gt;&lt;code&gt;.wh..wh..opq&lt;/code&gt; inside it&lt;/td&gt;&lt;td&gt;an &lt;code&gt;overlay.opaque&lt;/code&gt; xattr &lt;strong&gt;on&lt;/strong&gt; it&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;We write the AUFS dialect deliberately: it keeps &lt;code&gt;mknod&lt;/code&gt; out of the FUSE
server and it does not block a macOS port. But once we added a kernel-overlay
backend, the snapshot store had to &lt;em&gt;read&lt;/em&gt; both, because an upper layer could
now arrive from either.&lt;/p&gt;
&lt;h2 id=&quot;the-bug&quot;&gt;The bug&lt;/h2&gt;
&lt;p&gt;Our tree builder had a perfectly sensible arm that skipped device nodes.
Snapshots record files, directories and symlinks; a snapshot has no business
serialising a &lt;code&gt;/dev&lt;/code&gt; entry that wandered into a project directory.&lt;/p&gt;
&lt;p&gt;That arm ran &lt;strong&gt;before&lt;/strong&gt; marker classification.&lt;/p&gt;
&lt;p&gt;A kernel whiteout &lt;em&gt;is&lt;/em&gt; a device node. So every delete recorded by a
kernel-overlayfs upper hit the skip arm and vanished. No error, no warning.
The resulting tree held exactly the files that had survived, which is
indistinguishable from a correct snapshot of a directory that never contained
the deleted ones — and the next restore duly resurrected everything the agent
had removed.&lt;/p&gt;
&lt;p&gt;The fix is one line of ordering: &lt;strong&gt;classify first, dispatch on file type
second.&lt;/strong&gt; The cost of getting it wrong is silent data resurrection, which is
about the worst failure mode a snapshot system has.&lt;/p&gt;
&lt;h2 id=&quot;why-the-tests-would-not-have-caught-it&quot;&gt;Why the tests would not have caught it&lt;/h2&gt;
&lt;p&gt;The unit tests build whiteout markers by hand. They encode our model of the
kernel’s behaviour, so they agree with that model whether or not it is right.
They would have passed.&lt;/p&gt;
&lt;p&gt;The only thing that catches this is mounting a real overlayfs and asking the
kernel to produce the markers itself. Those tests need &lt;code&gt;/dev/fuse&lt;/code&gt; and
unprivileged user namespaces, so they are &lt;code&gt;#[ignore]&lt;/code&gt;d — and they run in CI
behind a guard that asserts a non-zero number of them actually executed,
because &lt;code&gt;cargo test -- --ignored&lt;/code&gt; &lt;strong&gt;exits 0 when it selects nothing&lt;/strong&gt;. A
renamed file would otherwise turn the whole step into a green no-op.&lt;/p&gt;
&lt;h2 id=&quot;three-things-this-pinned-down&quot;&gt;Three things this pinned down&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tree objects stay dialect-free.&lt;/strong&gt; Whiteout and opaque-directory entries are
symbolic, and an opaque xattr is normalised into the same synthetic entry the
file-based marker produces. That is precisely what lets a snapshot taken over
one overlay restore onto the other. The moment a tree records which dialect
wrote it, snapshots stop being portable between backends.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The two dialects cannot be confused&lt;/strong&gt;, which is why reading both needs no
mode flag. One is a regular file with a reserved prefix; the other is a device
node. Classification accepts both unambiguously, and only the &lt;em&gt;writer&lt;/em&gt; has to
pick.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;metacopy&lt;/code&gt; is refused rather than tolerated.&lt;/strong&gt; A metacopy upper holds a
file’s metadata while its data is still down in lower. Snapshotting one
records an empty file — silently truncating the user’s work. We never pass
that mount option, and we check for an upper that arrived with it anyway.&lt;/p&gt;
&lt;h2 id=&quot;an-aside-on-privilege&quot;&gt;An aside on privilege&lt;/h2&gt;
&lt;p&gt;Character device 0:0 is the one device number &lt;code&gt;mknod(2)&lt;/code&gt; will create without
&lt;code&gt;CAP_MKNOD&lt;/code&gt;; the kernel special-cases it as &lt;code&gt;WHITEOUT_DEV&lt;/code&gt;. So restoring a
kernel-dialect upper needs no privilege at all.&lt;/p&gt;
&lt;p&gt;Do not generalise that into “we can create device nodes.” Try 1:3 or 8:0 and
you get &lt;code&gt;EPERM&lt;/code&gt;. It is exactly one number, special-cased for exactly this
purpose.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>You cannot authenticate a process that shares your uid</title>
        <published>2026-08-04T00:00:00+00:00</published>
        <updated>2026-08-04T00:00:00+00:00</updated>
        
        <author>
          <name>pid1 Labs</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://pid1labs.ai/notes/you-cannot-authenticate-a-shared-uid/"/>
        <id>https://pid1labs.ai/notes/you-cannot-authenticate-a-shared-uid/</id>
        <summary type="text">We wanted checkpoints that only the harness could request, not the agent. Under a shared uid and a shared process tree that check cannot be built — so we changed the property instead of trying.</summary>
        
        <content type="html" xml:base="https://pid1labs.ai/notes/you-cannot-authenticate-a-shared-uid/">&lt;p&gt;&lt;code&gt;agentfs&lt;/code&gt; takes automatic snapshots while an agent works, so that a run which
goes badly can be walked back. The obvious way to place them well is to let
the harness say when: Claude Code fires a hook at the end of each turn, which
is a natural boundary.&lt;/p&gt;
&lt;p&gt;Which raises the natural next question. How do we know a checkpoint request
came from the harness and not from the agent?&lt;/p&gt;
&lt;h2 id=&quot;you-don-t-and-you-can-t&quot;&gt;You don’t, and you can’t&lt;/h2&gt;
&lt;p&gt;They share a uid. They share a process tree. Whatever secret the harness could
present — a token in a file, an environment variable, a nonce on a socket, a
value in its own memory — the agent can read, because the kernel has no basis
on which to distinguish them. Same uid means same access to &lt;code&gt;/proc&lt;/code&gt;, same
access to the filesystem, same everything. Any proof of identity is replayable
by precisely the party you are trying to exclude.&lt;/p&gt;
&lt;p&gt;This is the same dead end as trying to make a game client trustworthy, and it
fails for the same reason: the thing being authenticated has full control over
the machinery doing the authenticating.&lt;/p&gt;
&lt;p&gt;We spent a while looking for a clever way around it. There isn’t one. Not a
hard one — an impossible one.&lt;/p&gt;
&lt;h2 id=&quot;change-the-property-instead&quot;&gt;Change the property instead&lt;/h2&gt;
&lt;p&gt;The move is to stop trying to build the check and pick a property that does
not need it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;trigger lives outside the sandbox&lt;/strong&gt; and runs unconditionally. A
watcher on the host samples for change, fires on quiet, and has a ceiling
that fires regardless.&lt;/li&gt;
&lt;li&gt;The in-sandbox channel is a &lt;strong&gt;hint&lt;/strong&gt; that can only &lt;em&gt;add&lt;/em&gt; a checkpoint, never
suppress one.&lt;/li&gt;
&lt;li&gt;Forging a hint is therefore a non-event. You get an extra snapshot.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The property we provide is &lt;strong&gt;“the agent cannot prevent a checkpoint”&lt;/strong&gt; —
deliberately &lt;em&gt;not&lt;/em&gt; “only the harness can request one.” The second is
unachievable. The first is what we actually needed, and it survives an agent
that is actively hostile and knows exactly how the mechanism works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This only works because snapshots are additive.&lt;/strong&gt; Nothing destructive can
ever go behind a forgeable signal. &lt;code&gt;restore&lt;/code&gt; is not on that channel and must
not be put there — an agent that can trigger a restore can destroy work, and
no amount of care in the hint parser changes that.&lt;/p&gt;
&lt;h2 id=&quot;the-second-order-problem&quot;&gt;The second-order problem&lt;/h2&gt;
&lt;p&gt;Checkpoints self-prune to a budget, which quietly reintroduces the attack from
the other side. If eviction were naive, an agent could spam hints until every
genuine idle checkpoint had been crowded out of the budget — defeating the
mechanism through exactly the channel we left forgeable on purpose.&lt;/p&gt;
&lt;p&gt;So eviction order is &lt;code&gt;timed&lt;/code&gt; → &lt;code&gt;hint&lt;/code&gt; → &lt;code&gt;idle&lt;/code&gt;: &lt;strong&gt;agent-influenced entries go
first.&lt;/strong&gt; The forgeable channel can add noise; it cannot displace signal.&lt;/p&gt;
&lt;p&gt;Two smaller consequences of the same reasoning. Hint labels are untrusted
input that ends up in a commit message, so they are stripped of control
characters and truncated. And the label never claims a hint came from the
harness, because that is not a thing we can know — it says a hint arrived,
which is all that is true.&lt;/p&gt;
&lt;h2 id=&quot;the-general-shape&quot;&gt;The general shape&lt;/h2&gt;
&lt;p&gt;When two parties are indistinguishable to the kernel, stop looking for a
credential and start looking for an asymmetry that already exists. Ours was
position: the watcher is &lt;em&gt;outside&lt;/em&gt; the boundary the agent is confined by, and
that is a difference no forged message can erase.&lt;/p&gt;
</content>
        
    </entry>
</feed>
