Repository navigation
Pure TLS server: connection fd closed twice, and the socket wrapper leaks on a failed handshake #3138
Description
The pure-Aether TLS server (AETHER_PURE_TLS=1) and std.http's server disagree about who owns the connection's descriptor. On most paths it is closed twice, and on one path the socket wrapper leaks. Found while reviewing #3137 (#2483). That PR doesn't change this ownership; it only adds failure paths that go through it.
Who closes what
pure_tls_server_accept(fd, …) (std/cryptography/tls13_server/module.ae:619) wraps the fd with tcp_socket_from_fd_owned(fd), a malloc'd TcpSocket that owns it. aether_http_server.c also keeps the number and closes it itself.
| Path | Aether side | C side | Result |
|---|---|---|---|
| Handshake succeeds; connection later torn down | pure_tls_server_close → close_conn → tcp.close(c.sock) (tls13_server/module.ae:516) closes the fd and frees the wrapper |
conn_close → close(c->fd) (aether_http_server.c:748) |
fd closed twice |
Cert or key load fails, or the other early returns in pure_tls_server_accept (:624, :626 and three more) |
tcp.close(sock), then return null |
close(client_fd) when pt_server_accept returns NULL (aether_http_server.c:5567-5570) |
fd closed twice |
accept() fails (bad ClientHello, TLS 1.2 client, bad key share, Finished mismatch…) |
returns null without closing or freeing sock |
close(client_fd) |
fd closed once, but the 8-byte TcpSocket leaks on every failed handshake |
Why the double close matters
The http server serves connections from a thread pool. Between the first close() and the second, another worker's accept() can be given the same descriptor number. The second close() then closes that unrelated live connection, or an fd some other part of the program just opened. The client side hit this exact race and fixed it (std/net/aether_http.c:2115: "Closing it again here closed a NUMBER, which another thread may already have been given for a fresh socket of its own").
Repro for the leak
Run a std.http server with AETHER_PURE_TLS=1 under valgrind --leak-check=full, then send it handshakes it refuses (for example the bad_share.ae probe in #3137's tests/integration/https_pure_tls_vertical/). Each refused handshake leaves:
8 bytes in 1 blocks are definitely lost
at malloc
by tcp_socket_from_fd_owned
by aether_pure_tls_server_accept
by http_server_drain_connection
by http_pool_worker
The double close doesn't show up under valgrind. strace -f -e trace=close on the same run shows each connection's fd closed twice.
Suggested fix
Pick one owner and make every path agree. The smallest change matches the client contract (aether_http.c): the Aether side owns the fd once it's handed over.
pure_tls_server_acceptcloses on every failure path (it already does for the early returns), including afteraccept()fails, which also fixes the leak.aether_http_server.cstopsclose(client_fd)whenpt_server_acceptreturns NULL.conn_closeskipsclose(c->fd)whenc->pure_tlswas set, becausept_server_closealready closed it.
The alternative is to make the C side the owner, with a wrapper that frees TcpSocket without closing. But the client bridge already went the other way, and keeping the two sides the same is worth more.
Test idea: a regression that runs many refused and successful pure-TLS handshakes in parallel while another thread holds a long-lived fd, then checks that fd is still open. Plus a valgrind/leaks pass over refused handshakes.
Activity
Merge pull request #3143 from aether-lang-dev/feat/install-contrib-too