Skip to content

erts: don't crash getprotocol/1 when the port has closed - #11438

Open
lukebakken wants to merge 1 commit into
erlang:maintfrom
lukebakken:fix-getprotocol-badmatch-on-closed-port
Open

erts: don't crash getprotocol/1 when the port has closed#11438
lukebakken wants to merge 1 commit into
erlang:maintfrom
lukebakken:fix-getprotocol-badmatch-on-closed-port

Conversation

@lukebakken

Copy link
Copy Markdown
Contributor

Note

This PR was prepared by Claude (Anthropic's Claude Code) under the direction of @lukebakken, who reviewed the change before opening it. The underlying crash was surfaced by a 24-hour long-running high-throughput test. The code and analysis are AI-drafted and human-reviewed.

Problem

prim_inet:sendfile/4 guards on erlang:port_info(S, connected), and on success calls sendfile_maybe_cork/1 -> getprotocol/1, which does a second, unguarded {name,Drv} = erlang:port_info(S, name). If the peer closes the socket between those two port_info/2 calls, the second returns the atom undefined and the match raises {badmatch,undefined}, crashing the caller.

A fully-closed socket does not hit this: it fails the earlier connected guard and sendfile/4 returns {error, einval}. Only a close that races the narrow window between the two port_info/2 calls reaches the bad match.

Fix

Match on the port_info/2 result and return undefined for a closed port instead of badmatching:

getprotocol(S) when is_port(S) ->
    case erlang:port_info(S, name) of
        {name,Drv} ->
            drv2protocol(Drv);
        undefined ->
            undefined
    end.

undefined is already part of getprotocol/1's return contract (drv2protocol/1 has a catch-all undefined clause), and both call sites already tolerate it:

  • sendfile_maybe_cork/1 falls through to _ -> false (skip corking, so sendfile_1 proceeds and returns {error, einval} cleanly);
  • bindx/3 falls through to _ -> {error, einval}.

On testing

I have not added a test case, and I want to be up front about why. To reach the fixed line a port must simultaneously (a) pass sendfile/4's port_info(S, connected) guard and (b) return undefined from port_info(S, name). Those two states only coexist in the transient close window, so the condition is not reproducible on demand through any public API. A fully-closed socket, which is deterministic, returns {error, einval} before reaching getprotocol/1, so a test built on that would pass with or without this fix.

The only deterministic test I can see is a unit test calling getprotocol/1 directly on a closed port (returns undefined with this fix, raises {badmatch,undefined} without it). That would require exporting getprotocol/1 from prim_inet, and I did not want to widen a preloaded module's public surface without maintainer input. Happy to add that, or a test in whatever form you prefer, if you would like one.

I did confirm the closed-port behavior empirically: erlang:port_info(P, name) returns the bare atom undefined for a closed port, the pre-fix code raises {badmatch,undefined} on it, and the post-fix code returns undefined. The full sendfile_SUITE passes (14/14) on a runtime built with this change.

Origin

Reported downstream at rabbitmq/osiris#230, where the race surfaced as a noisy osiris_replica_reader crash during high-throughput stream replication over TCP. The osiris side adds a defence-in-depth guard for current OTP releases in rabbitmq/osiris#231; this PR fixes the root cause in OTP.

prim_inet:sendfile/4 guards on erlang:port_info(S, connected), and on
success calls sendfile_maybe_cork/1 -> getprotocol/1, which does a
second, unguarded `{name,Drv} = erlang:port_info(S, name)`. If the peer
closes the socket between those two port_info/2 calls, the second
returns the atom `undefined` and the match raises {badmatch,undefined},
crashing the caller. A fully-closed socket instead fails the connected
guard and returns {error, einval}; only this race raises.

Match on the port_info/2 result and return `undefined` for a closed
port instead of badmatching. `undefined` is already in getprotocol/1's
return contract (drv2protocol/1 has a catch-all `undefined` clause), and
both call sites already tolerate it: sendfile_maybe_cork/1 falls through
to `_ -> false` (skip corking, so sendfile_1 returns {error, einval}
cleanly), and bindx/3 falls through to `_ -> {error, einval}`.

Reported downstream at rabbitmq/osiris#230, where the race surfaced as a
noisy osiris_replica_reader crash during high-throughput stream
replication over TCP.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

CT Test Results

    3 files    136 suites   52m 28s ⏱️
1 683 tests 1 627 ✅ 56 💤 0 ❌
2 326 runs  2 252 ✅ 74 💤 0 ❌

Results for commit 95835d0.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@rickard-green rickard-green added the team:PS Assigned to OTP team PS label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

team:PS Assigned to OTP team PS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants