Skip to content

Fix readVersion() crash on bare LF as first byte (#237) - #238

Merged
josephnoir merged 2 commits into
apple:mainfrom
cyphers:fix/readVersion-bare-lf-crash
Jun 24, 2026
Merged

Fix readVersion() crash on bare LF as first byte (#237)#238
josephnoir merged 2 commits into
apple:mainfrom
cyphers:fix/readVersion-bare-lf-crash

Conversation

@cyphers

@cyphers cyphers commented May 30, 2026

Copy link
Copy Markdown
Contributor

Closes #237.

SSHPacketParser.readVersion() in the server branch accessed
slice[lfIndex.advanced(by: -1)] without checking that
lfIndex > slice.startIndex. A client whose first byte after the TCP
handshake is a bare \n made the subscript index before the buffer
start, trapping the NIO event-loop thread with EXC_BREAKPOINT and
taking down the whole server process. Reproducible with
printf '\n' | nc <host> <port>.

The fix

One-line guard on the -1 access. A leading LF now yields an empty
version string, which is consistent with the existing server-branch
convention that everything before the first LF is the version line.

Client branch

Unaffected — slice.starts(with: \"SSH-\".utf8) is checked first,
which guarantees lfIndex >= 4 > slice.startIndex.

Test

Added testReadVersionLineFeedFirstByteOnServer. Verified that the
test reproduces the production crash signature
(NIOCore/ByteBuffer-views.swift:80: Fatal error: index -1 out of range)
without the fix, and passes with it. All 12 existing SSHPacketParserTests
still pass.

Background

Reported in #237 after we hit this in production after ~4 days of
uptime on a public SSH port.

SSHPacketParser.readVersion() in the server branch accessed
slice[lfIndex.advanced(by: -1)] without checking that
lfIndex > slice.startIndex. A client sending a bare LF as its
first byte after the TCP handshake (e.g., a misbehaving probe
or scanner) caused the subscript to index one byte before the
buffer start, trapping the NIO event-loop thread with
EXC_BREAKPOINT and taking down the whole server process.

Guard the `-1` access. A leading LF now yields an empty
version string, matching the existing server-branch convention
that everything before the first LF is the version line. The
client branch is unaffected because `slice.starts(with: "SSH-".utf8)`
already guarantees at least 4 bytes precede lfIndex.

Adds a regression test that traps without the fix (reproducing
the production crash signature `index -1 out of range`) and
returns `.version("")` with it.
@josephnoir josephnoir added the 🔨 semver/patch No public API change. label Jun 23, 2026
@josephnoir

Copy link
Copy Markdown
Contributor

LGTM. Thank you for the fix!

@josephnoir
josephnoir merged commit 8257bc4 into apple:main Jun 24, 2026
40 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server crash: SSHPacketParser.readVersion() traps on bare LF as first byte

2 participants