Assert the invariant that holds on both platforms, not macOS's parse

The test I wrote to document a parser differential was itself
platform-specific, passed on this Mac, and failed in CI — which is the
point it was making, at its own expense.

`inet_pton` does not agree with itself across platforms:

    input          Linux           macOS
    0177.0.0.1     rejected        177.0.0.1
    010.0.0.1      rejected        10.0.0.1
    2130706433     rejected        rejected
    127.1          rejected        rejected

Darwin accepts a leading zero and reads it as decimal; Linux is strict
and rejects it; `inet_aton`, curl and every browser read `0177` as octal,
which is 127, which is loopback. Three readings of eleven characters, one
of them "public" and one of them loopback. I pinned the middle one.

The security-relevant fact is not which way any libc jumps. It is that
`isFetchableHost` refuses a four-numeric-label name by SHAPE, before
anything is resolved — no parsing, so nothing platform-dependent, so it
is the part that can actually be relied on. That is what the test asserts
now, across six spellings, plus the `IPAddress` results that genuinely
are the same on both.

Measured rather than reasoned about: the table above came from running
the same Swift program under `swift:6.1-noble` and natively.

664 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-06 17:52:54 -04:00
co-authored by Claude Opus 5
parent a5e30ed18a
commit c626683190
2 changed files with 49 additions and 32 deletions
@@ -25,15 +25,21 @@ struct IPAddress: Sendable, Equatable, CustomStringConvertible {
/// Parses a presentation-form address. Returns nil for anything that is not
/// one, including a hostname.
///
/// 🔴 **For resolver output, never for an untrusted literal.** `inet_pton`
/// on Darwin accepts a leading zero and reads it as DECIMAL: `0177.0.0.1`
/// parses to `177.0.0.1`, a public address. `inet_aton`, curl and every
/// browser read `0177` as OCTAL — 127 — which is loopback. So that string
/// is public to this parser and loopback to whatever would connect, which
/// is a bypass waiting to happen if this is ever used to vet a name.
/// Resolver output is already canonical and cannot carry the ambiguity;
/// `PeerDirectory.isFetchableHost` is what rejects the literal spellings,
/// before any resolution is attempted.
/// 🔴 **For resolver output, never for an untrusted literal.**
/// `inet_pton` does not agree with itself across platforms. On Darwin it
/// accepts a leading zero and reads it as DECIMAL, so `0177.0.0.1` parses
/// to `177.0.0.1` — a public address. On Linux, where this actually runs,
/// it rejects the string. And `inet_aton`, curl and every browser read
/// `0177` as OCTAL — 127 — which is loopback. Three readings of eleven
/// characters, one of which is "public" and one of which connects to
/// loopback.
///
/// Resolver output is already canonical and cannot carry that ambiguity,
/// which is why this is only ever given resolver output.
/// `PeerDirectory.isFetchableHost` is what rejects literal spellings, before
/// any resolution is attempted, and it does so by shape rather than by
/// parsing — which is what makes it platform-independent and therefore the
/// part that can be relied on.
init?(_ text: String) {
var v4 = in_addr()
if text.withCString({ inet_pton(AF_INET, $0, &v4) }) == 1 {
@@ -63,37 +63,48 @@ struct OutboundAddressGuardTests {
#expect(ok.isPubliclyRoutable)
}
/// 🔴 **A parser differential, found by writing the wrong assertion first.**
/// 🔴 **A parser differential — and the differential itself differs by
/// platform, which is how this test first went wrong.**
///
/// I expected `inet_pton` to reject `0177.0.0.1` outright. It does not: on
/// Darwin it accepts the leading zero and reads it as DECIMAL, giving
/// `177.0.0.1` — a public address. `inet_aton`, curl and every browser read
/// `0177` as OCTAL, which is 127, which is loopback. So the same eleven
/// characters are a public host to this parser and loopback to the thing
/// that would actually connect.
/// `inet_pton` on **Darwin** accepts a leading zero and reads it as
/// DECIMAL: `0177.0.0.1` parses to `177.0.0.1`, a public address. On
/// **Linux** — which is where the relay actually runs — it is strict and
/// rejects the string outright. Meanwhile `inet_aton`, curl and every
/// browser read `0177` as OCTAL, which is 127, which is loopback.
///
/// ⚠️ **Which is why `IPAddress` must only ever be given resolver output,
/// never an untrusted literal.** Resolver output is already canonical, so
/// the differential cannot arise there. What protects the literal case is
/// `isFetchableHost`, which rejects any name of four numeric labels before
/// resolution is even attempted — checked below, because that defence is
/// now load-bearing for a reason it was not originally written for.
@Test("odd address spellings do not become a public verdict on a private host")
/// input Linux macOS
/// 0177.0.0.1 rejected 177.0.0.1
/// 010.0.0.1 rejected 10.0.0.1
/// 2130706433 rejected rejected
/// 127.1 rejected rejected
///
/// ⚠️ So this asserts the invariant that holds on BOTH, not one platform's
/// parse. The first version pinned the macOS answer, passed here and failed
/// in CI — proving the point it was written to make, at its own expense.
///
/// 🔴 **The defence is `isFetchableHost`, and it is platform-independent.**
/// `IPAddress` must only ever be given resolver output, which is already
/// canonical and cannot carry the ambiguity. What stops an odd literal is
/// the name filter rejecting four numeric labels before anything is
/// resolved, and that is what is checked here.
@Test("odd address spellings never reach a resolver")
func alternateSpellings() throws {
// Universal: the canonical form parses the same everywhere.
#expect(IPAddress("127.0.0.1")?.bytes == [127, 0, 0, 1])
// Documented, not endorsed: this is what Darwin actually does.
#expect(IPAddress("0177.0.0.1")?.description == "177.0.0.1")
// …and the spelling never reaches a resolver, because the name filter
// rejects four numeric labels first.
#expect(!PeerDirectory.isFetchableHost("0177.0.0.1"))
#expect(!PeerDirectory.isFetchableHost("127.0.0.1"))
#expect(!PeerDirectory.isFetchableHost("2130706433"))
// Strict in the directions people expect it to be.
// Universal: these are not addresses to either libc.
#expect(IPAddress("2130706433") == nil)
#expect(IPAddress("127.1") == nil)
#expect(IPAddress("not.a.host") == nil)
// 🔴 The actual guarantee. Whatever `IPAddress` would make of these —
// and it disagrees with itself across platforms — none of them is ever
// dialled, because the name never survives long enough to be resolved.
for spelling in ["0177.0.0.1", "010.0.0.1", "127.0.0.1", "2130706433",
"169.254.169.254", "192.168.1.1"] {
#expect(!PeerDirectory.isFetchableHost(spelling),
"\(spelling) must be refused as a peer name")
}
}
// MARK: - The guard