Potential mismatch in send no more messages on that connection
Problem Description
Focused re-check confirmed a real mismatch in wolfSSL's shutdown behavior for RFC 8446 closure alerts.
After wolfSSL_shutdown() sends close_notify and the peer observes that closure alert, a later wolfSSL_write() on the same connection can still succeed and emit a TLS application-data record. That violates the sender-side requirement that, after sending close_notify, the sender will send no more messages on that connection.
This report deduplicates multiple candidate-level records that resolved to the same root cause.
Standard Requirement
- Official standard: RFC 8446
- Section: Section 6.1 Closure Alerts (lines 4822-4824)
close_notify: This alert notifies the recipient that the sender will
not send any more messages on this connection. Any data received
after a closure alert has been received MUST be ignored.
Interpretation:
Once the sender has sent close_notify, it must not send any later TLS messages on that connection. The receiver-side "MUST be ignored" rule for post-close data does not relax the sender-side prohibition.
Relevant Source Code
wolfSSL does send close_notify in the shutdown path, but the later write path still allows application-data transmission.
src/ssl_api_rw.c:651-670
wolfSSL_shutdown() sends the alert and sets ssl->options.sentNotify = 1. When the peer has not yet replied with its own close_notify, it returns WOLFSSL_SHUTDOWN_NOT_DONE.
if (!ssl->options.isClosed && !ssl->options.connReset &&
!ssl->options.sentNotify) {
ssl->error = SendAlert(ssl, alert_warning, close_notify);
if (ssl->error == 0 || ssl->error == WC_NO_ERR_TRACE(WANT_WRITE))
ssl->options.sentNotify = 1;
if (ssl->options.closeNotify) {
ret = WOLFSSL_SUCCESS;
ssl->options.shutdownDone = 1;
}
else {
ret = WOLFSSL_SHUTDOWN_NOT_DONE;
return ret;
}
}
src/ssl_api_rw.c:48-223
wolfSSL_write_internal() does not reject writes when sentNotify is already set. It delegates directly to SendData().
static int wolfSSL_write_internal(WOLFSSL* ssl, const void* data, size_t sz)
{
...
ret = SendData(ssl, data, sz);
...
if (ret < 0)
return WOLFSSL_FATAL_ERROR;
else
return ret;
}
src/internal.c:27909-28178
SendData() retries pending alerts, then still builds and sends an application_data record. There is no sender-side guard for the "close_notify already sent" state.
ret = RetrySendAlert(ssl);
if (ret != 0) {
ssl->error = ret;
return WOLFSSL_FATAL_ERROR;
}
for (;;) {
...
if (!ssl->options.tls1_3) {
sendSz = BuildMessage(ssl, out, outputSz, sendBuffer, buffSz,
application_data, 0, 0, 1, CUR_ORDER);
}
else {
sendSz = BuildTls13Message(ssl, out, outputSz, sendBuffer, buffSz,
application_data, 0, 0, 1);
}
...
if ((error = SendBuffered(ssl)) < 0) {
...
}
else {
ssl->error = 0;
}
sent += buffSz;
}
Implementation Behavior
wolfSSL_shutdown() sends close_notify and leaves the connection half-closed when the peer has not yet replied.
- In that half-closed state,
wolfSSL_write() can still succeed.
- The later write is not merely accepted by the API surface; it is serialized as a TLS application-data record and sent on the wire.
Runtime Evidence
Focused post-close write reproducer
The focused reproducer used a local TLS 1.2 client/server pair over loopback sockets and instrumented the client send callback plus the server raw socket after closure processing.
Observed result:
{
"shutdown_ret": 2,
"shutdown_err": 0,
"server_read_ret": 0,
"server_read_err": 6,
"saw_shutdown_alert_record": true,
"post_write_ret": 11,
"post_write_err": 0,
"saw_post_close_appdata_record": true,
"server_raw_recv_ret": 32,
"server_raw_recv_wsa_err": 0,
"server_raw_first_byte": 23
}
Interpretation:
shutdown_ret: 2 is WOLFSSL_SHUTDOWN_NOT_DONE, meaning the client has sent close_notify but has not yet completed bidirectional shutdown.
server_read_ret: 0 with server_read_err: 6 (WOLFSSL_ERROR_ZERO_RETURN) shows the server observed the client's close_notify.
post_write_ret: 11 with post_write_err: 0 shows a later wolfSSL_write() succeeded after that close-notify send.
saw_post_close_appdata_record: true shows the client actually emitted a TLS application-data record after shutdown.
server_raw_first_byte: 23 confirms the server's underlying socket received a new TLS record with outer content type application_data after the closure alert had already been processed.
The earlier generic runtime wrapper did not have a requirement-specific adapter for this family, so its prior "unresolved" result was a harness coverage gap rather than evidence against the bug.
TLS 1.3 cross-check
The cross-check repeated the same shutdown-then-write sequence with a TLS 1.3 client/server pair and captured the client send callback plus the server's raw socket.
Observed result:
{
"shutdown_ret": 2,
"shutdown_err": 0,
"server_read_ret": 0,
"server_read_err": 6,
"saw_shutdown_alert_record": false,
"post_write_ret": 11,
"post_write_err": 0,
"saw_post_close_appdata_record": true,
"server_raw_recv_ret": 32,
"server_raw_recv_wsa_err": 0,
"server_raw_first_byte": 23
}
Interpretation:
- The TLS 1.3 client again returned
WOLFSSL_SHUTDOWN_NOT_DONE from wolfSSL_shutdown(), meaning it had sent its close-notify and was waiting for peer shutdown completion.
- The server again observed shutdown with
WOLFSSL_ERROR_ZERO_RETURN before the post-close write.
post_write_ret: 11 and saw_post_close_appdata_record: true show that the later wolfSSL_write() still succeeded and emitted another TLS record after shutdown.
saw_shutdown_alert_record: false is expected in this callback-level trace because TLS 1.3 alerts are carried inside encrypted records whose outer content type is application_data.
server_raw_first_byte: 23 again confirms a later TLS record reached the wire after closure processing.
This TLS 1.3 run is not needed to establish the RFC 8446 violation, but it strengthens the report by showing the same sender-side bug is reachable beyond the TLS 1.2 control path.
Inconsistency Reason
- RFC 8446 requires the sender to send no more messages after sending
close_notify.
- wolfSSL records that
close_notify was sent by setting ssl->options.sentNotify.
- However, the normal write path does not stop on that state and still calls
SendData().
SendData() still constructs and transmits application-data records.
- Runtime testing confirmed the bad path is reachable and not just a static-proof gap.
Impact
- The implementation violates the RFC 8446 closure invariant for the sender side.
- A peer that correctly ignores post-close data may silently discard those records, which can hide application-layer misuse.
- A stricter peer or diagnostic environment may treat the post-close traffic as protocol misuse and surface interoperability problems.
Fix Direction
- Add a sender-side guard before
SendData() when the write side has already sent close_notify.
- Reject later
wolfSSL_write() calls in that half-closed state with an error or zero-return style result consistent with the library's shutdown semantics.
- Re-run the focused reproducer and add it to the native runtime wrapper so future audits do not fall back to a generic unresolved classification.
Potential mismatch in send no more messages on that connection
Problem Description
Focused re-check confirmed a real mismatch in wolfSSL's shutdown behavior for RFC 8446 closure alerts.
After
wolfSSL_shutdown()sendsclose_notifyand the peer observes that closure alert, a laterwolfSSL_write()on the same connection can still succeed and emit a TLS application-data record. That violates the sender-side requirement that, after sendingclose_notify, the sender will send no more messages on that connection.This report deduplicates multiple candidate-level records that resolved to the same root cause.
Standard Requirement
Interpretation:
Once the sender has sent
close_notify, it must not send any later TLS messages on that connection. The receiver-side "MUST be ignored" rule for post-close data does not relax the sender-side prohibition.Relevant Source Code
wolfSSL does send
close_notifyin the shutdown path, but the later write path still allows application-data transmission.src/ssl_api_rw.c:651-670wolfSSL_shutdown()sends the alert and setsssl->options.sentNotify = 1. When the peer has not yet replied with its ownclose_notify, it returnsWOLFSSL_SHUTDOWN_NOT_DONE.src/ssl_api_rw.c:48-223wolfSSL_write_internal()does not reject writes whensentNotifyis already set. It delegates directly toSendData().src/internal.c:27909-28178SendData()retries pending alerts, then still builds and sends anapplication_datarecord. There is no sender-side guard for the "close_notifyalready sent" state.Implementation Behavior
wolfSSL_shutdown()sendsclose_notifyand leaves the connection half-closed when the peer has not yet replied.wolfSSL_write()can still succeed.Runtime Evidence
Focused post-close write reproducer
confirmedThe focused reproducer used a local TLS 1.2 client/server pair over loopback sockets and instrumented the client send callback plus the server raw socket after closure processing.
Observed result:
{ "shutdown_ret": 2, "shutdown_err": 0, "server_read_ret": 0, "server_read_err": 6, "saw_shutdown_alert_record": true, "post_write_ret": 11, "post_write_err": 0, "saw_post_close_appdata_record": true, "server_raw_recv_ret": 32, "server_raw_recv_wsa_err": 0, "server_raw_first_byte": 23 }Interpretation:
shutdown_ret: 2isWOLFSSL_SHUTDOWN_NOT_DONE, meaning the client has sentclose_notifybut has not yet completed bidirectional shutdown.server_read_ret: 0withserver_read_err: 6(WOLFSSL_ERROR_ZERO_RETURN) shows the server observed the client'sclose_notify.post_write_ret: 11withpost_write_err: 0shows a laterwolfSSL_write()succeeded after that close-notify send.saw_post_close_appdata_record: trueshows the client actually emitted a TLS application-data record after shutdown.server_raw_first_byte: 23confirms the server's underlying socket received a new TLS record with outer content typeapplication_dataafter the closure alert had already been processed.The earlier generic runtime wrapper did not have a requirement-specific adapter for this family, so its prior "unresolved" result was a harness coverage gap rather than evidence against the bug.
TLS 1.3 cross-check
confirmedThe cross-check repeated the same shutdown-then-write sequence with a TLS 1.3 client/server pair and captured the client send callback plus the server's raw socket.
Observed result:
{ "shutdown_ret": 2, "shutdown_err": 0, "server_read_ret": 0, "server_read_err": 6, "saw_shutdown_alert_record": false, "post_write_ret": 11, "post_write_err": 0, "saw_post_close_appdata_record": true, "server_raw_recv_ret": 32, "server_raw_recv_wsa_err": 0, "server_raw_first_byte": 23 }Interpretation:
WOLFSSL_SHUTDOWN_NOT_DONEfromwolfSSL_shutdown(), meaning it had sent its close-notify and was waiting for peer shutdown completion.WOLFSSL_ERROR_ZERO_RETURNbefore the post-close write.post_write_ret: 11andsaw_post_close_appdata_record: trueshow that the laterwolfSSL_write()still succeeded and emitted another TLS record after shutdown.saw_shutdown_alert_record: falseis expected in this callback-level trace because TLS 1.3 alerts are carried inside encrypted records whose outer content type isapplication_data.server_raw_first_byte: 23again confirms a later TLS record reached the wire after closure processing.This TLS 1.3 run is not needed to establish the RFC 8446 violation, but it strengthens the report by showing the same sender-side bug is reachable beyond the TLS 1.2 control path.
Inconsistency Reason
close_notify.close_notifywas sent by settingssl->options.sentNotify.SendData().SendData()still constructs and transmits application-data records.Impact
Fix Direction
SendData()when the write side has already sentclose_notify.wolfSSL_write()calls in that half-closed state with an error or zero-return style result consistent with the library's shutdown semantics.