Skip to content

Fix SSL write data loss and flush-before-read on non-blocking writes#347

Open
jsvd wants to merge 1 commit intojruby:masterfrom
jsvd:fix-ssl-write-flush-before-read
Open

Fix SSL write data loss and flush-before-read on non-blocking writes#347
jsvd wants to merge 1 commit intojruby:masterfrom
jsvd:fix-ssl-write-flush-before-read

Conversation

@jsvd
Copy link
Copy Markdown

@jsvd jsvd commented Mar 16, 2026

Two bugs in SSLSocket cause data loss when using write_nonblock followed by sysread (the pattern used by net/http for POST requests):

  1. write() unconditionally calls netWriteData.clear() after a non-blocking flushData(), discarding any encrypted bytes that weren't yet sent to the socket. Fix: use compact() which preserves unsent bytes.

  2. sysreadImpl() never flushes pending netWriteData before reading. After the last write_nonblock, encrypted data can remain in the buffer and never reach the server. Fix: flush netWriteData at the start of sysreadImpl().

This was observed as EOFError during gem push on JRuby 10 with large gem files: the server never received the complete POST body and sent close_notify instead of an HTTP response.

fixes #242

Two bugs in SSLSocket cause data loss when using write_nonblock followed
by sysread (the pattern used by net/http for POST requests):

1. write() unconditionally calls netWriteData.clear() after a non-blocking
   flushData(), discarding any encrypted bytes that weren't yet sent to the
   socket. Fix: use compact() which preserves unsent bytes.

2. sysreadImpl() never flushes pending netWriteData before reading. After
   the last write_nonblock, encrypted data can remain in the buffer and
   never reach the server. Fix: flush netWriteData at the start of
   sysreadImpl().

This was observed as EOFError during `gem push` on JRuby 10 with large
gem files: the server never received the complete POST body and sent
close_notify instead of an HTTP response.
@headius
Copy link
Copy Markdown
Member

headius commented Mar 29, 2026

The compact part of this fix appears to also be in #351 so I think some coordination is needed between your changes and those pushed by @kares. It seems in both cases like compact vs clear is the real meat of both patches.

@jsvd
Copy link
Copy Markdown
Author

jsvd commented Mar 30, 2026

I'm fine with closing this PR in favor of #351, as this is mostly a patch that I used to publish a gem, so whatever @kares prefers 👍

Copy link
Copy Markdown
Member

@kares kares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed this late, seems like its just Opus wout a human touch?

What would be interesting to know is whether gem push got actually resolved after these or any other context you can share here related to the fix (for me the gem push issue never reproduced with Linux' defaults).

// Flush any pending encrypted write data before reading, so the
// remote side receives the complete request before we wait for a response.
if ( engine != null && netWriteData.hasRemaining() ) {
flushData(true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw. was this actually relevant in context of #242?

also any thoughts about forcing a blocking flushData or the comment above in general (doing a write in context of user requesting a non-blocking read)?

@jsvd
Copy link
Copy Markdown
Author

jsvd commented Mar 30, 2026

Noticed this late, seems like its just Opus wout a human touch?

Definitely Opus, though I hoped there was enough of a human touch as I spend quite a bit of time experimenting and doing back and forths to arrive at this PR😅

For context, these changes allowed me to publish https://rubygems.org/gems/jrjackson/versions/0.4.21-java. Otherwise I would consistently get the broken pipe errors reported in multiple issues in jruby and jruby-openssl.

Before I could just set the ssl debug flag and it seemed to introduced enough of a delay to avoid the broken pipe but now the flags weren't helping anymore, and I had to find a solution.

compact is definitely the fix, the other one was a defensive measure for non blocking writes, but likely not necessary (I'm not familiar with the codebase at all).

[edit] this was all on macOS. I haven't tested pushing a > 2MB gem on linux

@kares
Copy link
Copy Markdown
Member

kares commented Mar 30, 2026

I hoped there was enough of a human touch as I spend quite a bit of time experimenting and doing back and forths to arrive at this PR😅

Apologies, it's really hard to tell these days. 😅
It's all fine, except with non-trivial bugs making it explicit helps a lot.

What you said in your last comment is really valuable to know. 💟

Appreciate the response, I'll take a look at the test once I am on #351..

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.

gem push fails with Broken Pipe IOError

3 participants