tls: let tls-write yield on a non-blocking connection
On a non-blocking connection tls-write retried WANT_WRITE with a bare continue: a hot loop that never returned and never yielded. So a peer that accepted the connection and then stopped reading could pin a CPU indefinitely, and no caller could bound the write from Sigil at all. That is why sigil-http writes its request while still blocking and only switches to non-blocking for the response read.
Non-blocking now returns the bytes actually committed, possibly 0, so a caller can poll against a deadline. 0 means retry the same slice, which is what mbedTLS requires after WANT_WRITE; a positive return means those bytes are committed. Blocking connections keep the original retry-until-complete behaviour.
tls-write also gains optional start/end byte offsets, mirroring socket-write, so a write loop resending the tail of a large body stays O(n) instead of O(n^2).
The non-blocking flag is recorded only after the fd has actually changed mode, so it cannot claim a mode the socket is not in.
No in-suite test here: exercising it needs a real TLS peer that stops reading, which this suite cannot stand up in-process. It is covered end to end in sigil-http against a TLS server that never drains its receive queue.
native/tls.c | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
src/sigil/tls.sgl | 18 +++++++++++++++++-
2 files changed, 74 insertions(+), 10 deletions(-)native/tls.cmodified
mbedtls_net_context server_fd; mbedtls_x509_crt cacert; int closed; /* Tracks tls-set-non-blocking!. tls-write consults it: in blocking mode * it keeps retrying WANT_WRITE (the original behaviour), in non-blocking * mode it must hand control back instead of spinning on a socket that * will not accept bytes. */ int nonblocking;} TlsConnectionData;/* Symbol used as type tag for TLS connection foreign objects */}/* * tls-write connection data -> integer | #f * Write data to TLS connection. * Returns number of bytes written, or #f on error. * tls-write connection data [start [end]] -> integer | #f * * Write data to a TLS connection. Returns the number of bytes written, or * #f on error. * * BLOCKING connection (the default): retries WANT_READ/WANT_WRITE until the * whole buffer is written, exactly as before. * * NON-BLOCKING connection (after tls-set-non-blocking!): returns the number * of bytes actually committed, which may be 0, instead of spinning. The old * behaviour on a non-blocking socket was a `continue` on WANT_WRITE, i.e. a * busy loop that never returned and never yielded — unbounded AND hot. A * caller can now poll against a deadline. A 0 return means "nothing was * accepted, retry the SAME slice", which is what mbedTLS requires after * WANT_WRITE. * * The optional [start] and [end] byte offsets let a write loop resend the * tail of a buffer without copying it each time, keeping a large body O(n) * instead of O(n^2). This mirrors socket-write. */static Value native_tls_write(SigilVM *vm, int argc, Value *args){ (void)argc; if (!is_tls_connection(args[0])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-write: expected tls-connection"); return SIGIL_UNDEFINED; return SIGIL_UNDEFINED; } /* Optional [start] and [end] byte offsets, clamped into the buffer. */ size_t start = 0; size_t end = len; if (argc >= 3 && sigil_is_fixnum(args[2])) { long s = (long)sigil_as_fixnum(args[2]); if (s < 0) s = 0; start = (size_t)s > len ? len : (size_t)s; } if (argc >= 4 && sigil_is_fixnum(args[3])) { long e = (long)sigil_as_fixnum(args[3]); if (e < 0) e = 0; end = (size_t)e > len ? len : (size_t)e; } if (end < start) end = start; data += start; len = end - start; int ret; size_t total_written = 0; len - total_written); if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { continue; /* Retry */ if (conn->nonblocking) { /* Hand control back so the caller can enforce a deadline * (and let other tasks run) rather than spinning here. */ break; } continue; /* Blocking: retry, as before. */ } if (ret < 0) {#ifdef _WIN32 u_long mode = enable ? 1 : 0; return sigil_bool(ioctlsocket(fd, FIONBIO, &mode) == 0); if (ioctlsocket(fd, FIONBIO, &mode) != 0) return SIGIL_FALSE; conn->nonblocking = enable; return SIGIL_TRUE;#else int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) return SIGIL_FALSE; flags &= ~O_NONBLOCK; } return sigil_bool(fcntl(fd, F_SETFL, flags) != -1); if (fcntl(fd, F_SETFL, flags) == -1) return SIGIL_FALSE; /* Recorded only after the fd actually changed mode, so the flag can * never claim a mode the socket is not in. */ conn->nonblocking = enable; return SIGIL_TRUE;#endif} REGISTER_AND_EXPORT("tls-read-bytevector", native_tls_read_bytevector, SIGIL_ARITY_RANGE(1, 2), "Read raw bytes from TLS connection"); REGISTER_AND_EXPORT("tls-write", native_tls_write, SIGIL_ARITY_EXACT(2), "Write to TLS connection"); SIGIL_ARITY_RANGE(2, 4), "Write to TLS connection"); REGISTER_AND_EXPORT("tls-close", native_tls_close, SIGIL_ARITY_EXACT(1), "Close TLS connection"); REGISTER_AND_EXPORT("tls-closed?", native_tls_closedp,src/sigil/tls.sglmodified
;;; Accepts a string or bytevector. Returns the number of bytes ;;; written, or #f on error. ;;; ;;; On a BLOCKING connection (the default) this writes the whole buffer ;;; before returning, exactly as before. ;;; ;;; On a NON-BLOCKING connection (see `tls-set-non-blocking!`) it returns ;;; the number of bytes actually committed, which may be `0`, so a caller ;;; can poll against a deadline. Previously a non-blocking connection made ;;; this spin in a hot loop that never returned, so a peer that stopped ;;; reading could pin a CPU forever. A `0` return means retry the SAME ;;; slice; a positive return means those bytes are committed and the next ;;; call should start after them. ;;; ;;; The optional `start` and `end` byte offsets let a write loop resend ;;; the tail of a buffer without copying it, keeping a large body O(n) ;;; rather than O(n^2). This mirrors `socket-write`. ;;; ;;; ```scheme ;;; (tls-write conn "GET / HTTP/1.1\r\n\r\n") ; => integer | #f ;;; (tls-write conn body 4096) ; write from byte 4096 on ;;; ``` (define-native (tls-write connection data) (define-native (tls-write connection data . start-end) (: any? (any-of string? bytevector?) -> (any-of integer? boolean?))) ;;; Close a TLS connection.