OpenBSD ftpd: finding a 27-year-old bug (with Codex)
In April 2026, I reported a bug in OpenBSD’s ftpd that had been in the tree since 1996:
undefined behavior in an attacker-reachable error path in the send_data() mmap “fast path”.
The fix was small—and already mirrored by an older “oldway” code path—but it’s exactly the kind of issue that can hide for decades.
What mattered (not the model)
- Constrain scope: one function, one loop, one invariant.
- Force error-path thinking: assume syscalls fail, and ask “what happens next?”.
- Demand a proof: “show the exact statement that becomes UB and why”.
- Minimize fixes: match known-good adjacent behavior.
The target
I focused on libexec/ftpd/ftpd.c, specifically the mmap-based send loop used for certain transfer modes
(binary/image paths where mmap is used to avoid buffering). It’s performance code in a privileged daemon:
a classic place for “fast path” assumptions to outlive their safety.
The bug: UB on write error
The core loop pattern looked like this (simplified):
cnt = write(netfd, bp, len);
/* ... */
len -= cnt;
bp += cnt;
} while (cnt > 0 && len > 0);
This is fine while cnt > 0. But write() can return -1.
When that happens, two bad things occur before the loop condition stops the loop:
-
bp += cntbecomesbp -= 1— pointer arithmetic to one byte before the mapped region (undefined behavior in C). -
lenis typically asize_t;len -= cntmixes signed/unsigned and can wrap to a huge value (nonsense state, and also UB-adjacent territory depending on how the compiler reasons about it).
Trigger: remote abort mid-transfer
A realistic trigger is a remote client performing RETR and then dropping/aborting the data connection mid-transfer.
If SIGPIPE is suppressed (e.g., handler ignored or equivalent behavior), write() returns -1
and the loop executes the UB statements.
Why it matters
Even if it “usually” just crashes under sanitizers, undefined behavior in a privileged network daemon is worth fixing: UB can become miscompilation in optimized builds, and attackers get to choose the timing and path.
The fix: advance only on success
The minimal fix is to only adjust bp/len when cnt > 0, matching the safer “oldway” behavior:
if (cnt > 0) {
len -= (size_t)cnt;
bp += cnt;
byte_count += cnt;
}
(Exact formatting varies; the key is the guard.)
Prompts I used (the “context package”)
I didn’t ask the model to “find a vuln”. I asked it to stress a specific invariant in a bounded slice of code:
Review OpenBSD ftpd send_data() mmap fast path.
Assume write() may return -1 and partial writes happen.
Find any undefined behavior on attacker-reachable error paths.
Point to the exact statements and explain why it’s UB.
Try to prove the loop is safe when write() returns -1.
If you can’t, give the smallest counterexample (bp/len/cnt values).
Propose the smallest patch that eliminates UB while preserving behavior.
Prefer matching the non-mmap “oldway” path if it already handles errors safely.
Timeline
- Report sent with a minimal patch and explanation.
- Fix committed on April 17, 2026 (OpenBSD src:
libexec/ftpd/ftpd.crev 1.236).
Closing
If you want repeatable results, treat prompts like engineering: define the invariant, constrain the search space, include the failure assumptions, and require a verifiable claim. The model is an accelerator; the workflow is the product.