Skip to content

fix: an address family is a choice, not a setting, and a pinned CA is the CA - #47

Merged
Taure merged 4 commits into
mainfrom
fix/address-family-and-ca-source
Aug 11, 2026
Merged

fix: an address family is a choice, not a setting, and a pinned CA is the CA#47
Taure merged 4 commits into
mainfrom
fix/address-family-and-ca-source

Conversation

@Taure

@Taure Taure commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Two defects in what 1.0 does with caller-supplied options. The first is a production outage; the second is a quiet weakening of a CA pin.

socket_options => [inet] was refused

1.0 made the socket transport the default and made minato_socket:setopts/2 refuse options it cannot honour instead of silently dropping them. The policy is right. The accept list was not: I drew it from what a socket can be set to rather than from what callers actually pass, and inet/inet6 are neither - they choose which address a hostname resolves to, and gen_tcp consumes them before connecting.

So {socket_options, [inet]} answered {error, {socket, {unsupported_socket_option, inet}}}, after the TCP handshake. Through a pool that surfaces as {error, disconnected} with a pool that started cleanly, so a container boots, migrations fail, readiness never flips, and nothing crashloops.

This is the documented way to configure a family. asobi's own configuration guide lists inet, inet6 and inet, {nodelay, true} as the supported values of ASOBI_DB_SOCKET_OPTS, and both asobi and asobi_engine bake inet as their image default. Every one of those deployments would have failed to reach Postgres.

The family is now read before connecting and used for resolution. inet6 means AAAA or an error - a fall back to v4 would be the same defect one layer down, the option accepted and not honoured.

{cacertfile, Path} did not replace {cacerts, _}

tls_options/2 merged caller options over defaults by option name, so a caller pinning a private CA by file was handed the public bundle as well, and the wider set decided. It fails closed rather than open, but the shape is a pin bypass: pin a stricter private CA for a host whose certificate also chains to a public root and you silently get the wider trust set.

Defaults are now superseded by the option that answers the same question, with cacertfile and cacerts being one question.

Tests

Both are covered by cases that fail without the fix - I checked by reverting each and watching them go red (unknown_ca for the CA case). 36 in the transport suite across both transports, 12 in the TLS suite.

Full pipeline green: 1398 eunit, 178 ct, xref, dialyzer, eqwalize-all, elp lint, ex_doc, fmt.

Provenance

Found by an architecture-guardian and a security review run against widgrensit/asobi_engine#103, the relock that would have carried 1.0.1 into deployed environments. That PR is a draft until this ships.

Marked fix! because the family is now honoured rather than ignored: a caller who passed inet6 to a dual-stack host was getting v4 and will now get v6.

…s the CA

Two things 1.0 got wrong about the options a caller hands in.

`socket_options => [inet]` was refused. `inet` and `inet6` are not settings a
socket takes, they choose which address a name resolves to, so asking
`setopt/2` about them was the wrong question and the answer was
`{error, {unsupported_socket_option, inet}}` - after the TCP connect, which a
pool reports as `disconnected` rather than as a bad option. Every deployment
that names its address family, which is the documented way to configure one,
could not open a connection. The accept list was drawn from what a socket can
be set to instead of from what callers pass.

They are now read before connecting and honoured: `inet6` resolves AAAA or
fails, rather than falling back to v4 and leaving the option accepted and not
honoured.

`ssl_options => [{cacertfile, Path}]` did not replace the default
`{cacerts, public_key:cacerts_get()}`, because a default was only superseded by
an option spelled the same way. Both reached `m:ssl` and the public bundle won,
so a caller pinning a private CA silently got the wider trust set. Defaults are
now superseded by the option that answers the same question.

Both cases are covered by tests that fail without the fix.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

🟡 Code Coverage — 88.8%

1758 of 1980 lines covered.


✅ ELP Lint

No diagnostics.

Taure added 2 commits August 11, 2026 08:44
A send with no deadline waits on a peer that has stopped reading for as long as
that peer likes, holding the pool slot it borrowed. The driver has
`send_timeout` for exactly that, and the socket transport had nowhere to put it,
so the mitigation existed for the transport that is not the default.

The deadline covers the whole write rather than each attempt, because a peer
accepting one byte at a time would otherwise never reach it. It is read once
when the connection opens rather than off the socket per write: a `getopt` per
send is a NIF call per send, which is the cost this transport exists to avoid.

`send_timeout_close` is honoured too. A write that timed out has left the server
half a message, so the connection is finished either way, and a caller who asked
for it to be closed should not have to notice that for themselves.
Common Test's default is thirty minutes, which is longer than CI gives the whole
step, so a case that blocks arrives as a killed job with no case name and no
stack. Every operation in this suite is bounded by a five second connect or a
ten second read, so sixty seconds means stuck somewhere with no deadline of its
own - and the timetrap's stack trace is the thing worth having.
@Taure Taure changed the title fix!: an address family is a choice, not a setting, and a pinned CA is the CA fix: an address family is a choice, not a setting, and a pinned CA is the CA Aug 11, 2026
@Taure
Taure merged commit b9eb18c into main Aug 11, 2026
19 checks passed
@Taure
Taure deleted the fix/address-family-and-ca-source branch August 11, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant