Skip to content

Potential mismatch in send no more messages on that connection #11071

Description

@LiD0209

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

  • Status: confirmed

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

  • Status: confirmed

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.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions