Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/ssl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ function on_bio_stream_write(bio::BIO, in::Ptr{Cchar}, inlen::Cint)::Cint
written = unsafe_write(io, in, inlen)
return Cint(written)
catch e
# we don't want to throw a Julia exception from a C callback
return Cint(0)
# we don't want to throw a Julia exception from a C callback; -1 fails the
# SSL call. A 0 would not: with no retry flag set, OpenSSL (3.5 included)
# takes it as "nothing written yet", keeps the record pending and reports
# the call a success, so a write whose socket is gone would look written
return Cint(-1)
end
end

Expand Down Expand Up @@ -472,11 +475,16 @@ macro geterror(ssl, op, expr)
ret = SSL_ERROR_WANT_WRITE
else
# this is usually some other kind of error, like a protocol error
# or OS-level IO error, just close the SSL connection and throw
# or OS-level IO error (a failed socket write in the BIO callback
# among them), just close the SSL connection and throw
# notably, the openssl docs say we should *not* call ssl_disconnect
# in this case, hence the `false` arg to close
# in this case, hence the `false` arg to close. OpenSSL's own account
# of the error is on the thread's error queue: it goes into the
# message rather than staying there for an unrelated call to report
detail = strip(get_error())
close($ssl, false)
throw(Base.IOError(OpenSSLError(err).msg, 0))
msg = string(err)
throw(Base.IOError(isempty(detail) ? msg : "$msg: $detail", 0))
end
end
ret
Expand Down Expand Up @@ -799,6 +807,10 @@ function Base.close(ssl::SSLStream, shutdown::Bool=true)
catch err
@debug "SSL disconnect failed" err
end
# the close_notify's socket write failing (the peer is gone, or the socket
# already closed) fails SSL_shutdown, and OpenSSL 3.2+ records that on the
# thread's error queue; not ours to leave for an unrelated call to report
clear_errors!()
end
free(ssl.ssl)
end
Expand Down
62 changes: 62 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,65 @@ end
close(server)
rm(ca_file)
end

@testset "WriteCallbackFailure" begin
# The socket write inside the BIO callback fails (here: the socket is closed). The
# SSL call has to fail with it, rather than report the bytes written.
server_ctx = selfsigned_server_ctx()
port, server = Sockets.listenany(ip"127.0.0.1", 20000)
client, ssl = connected_pair(server_ctx, server)
close(client.io)
err = @test_throws Base.IOError write(client, UInt8[1, 2, 3])
@test !isopen(client)
# the message carries OpenSSL's account of the failure, and nothing is left of it on
# the thread's error queue for an unrelated call to report
@test occursin("SSL_ERROR_SYSCALL", sprint(showerror, err.value))
@test ccall((:ERR_peek_error, OpenSSL.libcrypto), Culong, ()) == 0
close(ssl, false)
close(server)
end

@testset "RawWriteFailure" begin
# the same failure from a raw call on `ssl.ssl`: the callback's -1 fails the call
# with SSL_ERROR_SYSCALL
server_ctx = selfsigned_server_ctx()
port, server = Sockets.listenany(ip"127.0.0.1", 20000)
client, ssl = connected_pair(server_ctx, server)
close(client.io)
payload = UInt8[1, 2, 3]
n = Ref{Csize_t}(0)
ret = Base.@lock client.lock GC.@preserve payload ccall(
(:SSL_write_ex, OpenSSL.libssl),
Cint,
(OpenSSL.SSL, Ptr{Cvoid}, Csize_t, Ptr{Csize_t}),
client.ssl, pointer(payload), length(payload), n)
@test ret == 0
@test OpenSSL.get_error(client.ssl, ret) == OpenSSL.SSL_ERROR_SYSCALL
# OpenSSL 3.2 and later also leave their account of it on the error queue
# (`tls_retry_write_records failure`), as for any failed raw call; older versions
# record nothing for a SYSCALL error. Either way the queue is the raw caller's to
# read or clear
if OpenSSL.version_number() >= v"3.2"
@test ccall((:ERR_peek_error, OpenSSL.libcrypto), Culong, ()) != 0
end
OpenSSL.clear_errors!()
close(client, false)
close(ssl, false)
close(server)
end

@testset "CloseAfterSocketGone" begin
# `close(ssl)` with the socket already gone: the close_notify write fails, which
# fails SSL_shutdown; OpenSSL 3.2+ records that on the thread's error queue and
# `close` clears it, so an unrelated call later does not report it
server_ctx = selfsigned_server_ctx()
port, server = Sockets.listenany(ip"127.0.0.1", 20000)
client, ssl = connected_pair(server_ctx, server)
close(client.io)
OpenSSL.clear_errors!()
close(client)
@test !isopen(client)
@test ccall((:ERR_peek_error, OpenSSL.libcrypto), Culong, ()) == 0
close(ssl, false)
close(server)
end
Loading