Some Lab

OpenBSD ftpd: finding a 27-year-old bug (with Codex)

April 19, 2026 · Memory safety · Error paths · “Context beats models”

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.

Thesis: models help, but the repeatable advantage is workflow—choosing a sharp target, packing the right context, asking for invariants, and verifying fast.

What mattered (not the model)

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:

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

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.