Repository navigation
Fix read/write deadlock (#72): write ciphertext outside ssl.lock; defined close/cancel/timeout semantics #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
krynju
wants to merge
25
commits into
JuliaWeb:main
Choose a base branch
from
krynju:kr/nonblocking-write-bio
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
4e2a937
fix: buffer ciphertext in the write BIO instead of blocking under ssl…
krynju 3e437f3
test: pick the ReadPEMCert certificate by content, not by position
krynju 76fdde6
test: detect the parked writer instead of assuming 8 MB is enough
krynju 8104a5c
fix: close stream on drain failure, chunk SSL_write_ex submissions
krynju e8916bc
chore: bump version to 1.6.2
krynju eb7e3ac
fix: each SSL call owns and orders its own ciphertext
krynju 367e3fd
fix: consume the ticket of a writer cancelled while waiting its turn
krynju 7fc3e89
fix: eof spun forever on a record truncated by the peer closing
krynju ce8741a
test: report where the CancelledWriter server is stuck when it does n…
krynju 3c8fccc
fix: give each SSL call its own byte count
krynju 7f45600
test: have the server close first in CancelledWriter
krynju e1efe0d
fix: send peek output outside eoflock, refuse to write to a closed st…
krynju 7a73396
fix: no close_notify from the finalizer; count queued writers for the…
krynju a7a8429
Address review: close, abort and cancellation semantics, handshake ti…
krynju 3b126e1
test: CloseNotifyEOF and the fallback case work on Windows and macOS
krynju 8545e4e
test: ThrowingLogger's fallback case ends its raw peer reader from th…
krynju a2c6a1e
chore: bump version to 1.7.0
krynju 4fb5008
perf: write one record per SSL_write_ex, and reuse the sent chunk as …
krynju 543711c
test: park_writer writes more than the kernel buffers grow to
krynju 04bffd4
test: CloseWithQueuedWriter reads on the client, so its close does no…
krynju fb331be
Keep the 1.x contracts: one-round accept, raw SSL calls; clean up whe…
krynju f82a407
test: RawSSLCalls asserts the raw write waits only while the parked r…
krynju bfff910
test: skip the in-flight writer cancellations on Windows
krynju 3bb045b
Bound the raw write's wait, cut only a write libuv may hold, fail wit…
krynju abe9d7a
test: a task cancelled inside a raw write's socket write
krynju File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[reviewed by AI] [P1] A minor bump won't protect existing callers from these API changes
Julia's default caret compatibility treats
OpenSSL = "1.6"as>=1.6.0, <2.0.0; 1.7.0 is accepted. I verified this withPkg.Types.semver_spec("1.6"). The PR's claim that the minor bump keeps those environments on 1.6 is incorrect.The timeout keyword and doc changes address my earlier comment, but existing callers still need code changes. With a silent TCP client, an unchanged retry loop with a 0.5 s deadline returns
:deadline_expiredon the base; here it is still blocked in its firstaccept(ssl)call after 1.5 s. A rawSSL_acceptloop also completes on the base but stalls here with 2,316 bytes of unsent handshake ciphertext.Can we preserve the existing contracts in 1.x, or make this a 2.0 change with migration guidance? Documentation and the new keyword won't stop a compatible dependency update from hanging existing callers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[addressed by Claude] Fixed in fb331be. You're right that
"1.6"admits 1.7.0; that claim was wrong. I went with keeping the 1.x contracts rather than a 2.0:Sockets.accept(ssl)withouttimeoutis one non-blocking round again and throwsOpenSSLErrorwhen it needs more bytes, so a retry loop with its own deadline behaves as onmain(LegacyAccepttests a 0.5 s deadline against a silent client). The whole handshake isaccept(ssl; timeout=Inf)or with a deadline.ssl.sslfrom outside the package work again. The write BIO callback checks whether one of the package's own calls is in progress (incall); outside one it writes to the socket itself, as before, after waiting for any ticketed records so order is kept. TLSStreams 0.2.0's rawSSL_acceptloop completes the handshake on this branch unchanged (RawSSLCalls, plus the TLSStreams suite).ssl_accept(::SSL)is restored as it was.IOErrorand closes the stream rather than throwingOpenSSLErrorand leaving it open, and a raw write that fails cuts the socket (libuv may still hold a request pointing into OpenSSL's buffer, whichSSL_freewould otherwise free under it). A raw write parked on a peer that does not read still blocks underssl.lock, as every write did before 1.6.2; only the caller can bound that, by closing the socket.Two undocumented things still differ:
close(ssl)returnsnothinginstead of the@asynctask, and thereadbytes/writebytesfields are gone. Happy to restore either if you think they matter. The version stays 1.7.0 for the newtimeoutkeyword.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[addressed by Claude] Follow-up in 3bb045b, after another review pass over the raw-call path:
ssl.lock, which every close, abort and watch needs first, so a ticket no task would ever drain could have held the stream for good. It gives up once nothing has moved forCLOSE_GRACE, and the call fails (RawWriteGivesUp). The socket write itself still blocks on a peer that does not read, as before 1.6.2.maintoo, so on 3.x a socket error inside it was reported as a successful write.finish_sslcall!gives the reply's ticket up only when the task was not scheduled, so a drain and the catch cannot both pass one turn.CancelledInFlightWriterandThrowingLoggerrun on Linux only: they cancel a writer inside the socket write, and Base'suv_writecb_taskschedules the waiting task unconditionally while the request names it, so a write completing at that moment throws "schedule: Task not runnable" out of the libuv callback. Windows and macOS grow the socket buffers for a non-reading peer, which completes the parked record and hits that window (it hung two Windows jobs). Not something this package can fix.