- Framed responses:
Content-Length + Connection: close on every axil_respond(). Responses previously carried no body framing, so a client could only delimit the body by connection close -- over TLS that requires close_notify, which was never sent either. Strict clients (OpenSSL 3.5+, modern browsers) discarded such responses as truncated, dropping Set-Cookie with them. 1xx/204/304 stay length-free per RFC 9110, and 101 upgrade paths bypass axil_respond() untouched. Explicitly set lengths are respected, never duplicated.
- Best-effort TLS
close_notify on teardown. axil_ssl_drop() now runs up to two non-blocking SSL_shutdown() calls before SSL_free(), ensuring clean TLS connection termination.
- Teardown is ungated: unauthenticated connection cleanup (SECURITY S5.4).
axil_disconnect() no longer requires DF_AUTHENTICATED and axil_close() no longer requires DF_CONNECTED. Resolves socket and PTY leakage vulnerabilities where unauthenticated descriptors outlived their TCP connection.
- Per-connection generation tracking: Generational tracking on connection descriptors to eliminate stale hook dispatch and race conditions during rapid reconnects.
- Fixed 303 Redirect Connection Teardown: Fixed connection closing and cookie persistence issues on HTTP 303 See Other redirects.
- Post-Chroot Module Loading: Improved path canonicalization and search mechanics for dynamic modules running post-chroot.
kept as a regression: an AUTHENTICATED PTY connection killed abruptly must
still leave no live child, no PTY master, and clean HTTP on recycled fds.
- Per-connection generation:
axil_generation(). struct descr grows a
monotonic generation bumped on every accept, so fd-keyed module state can
reject stale entries. axil-tty's mux_state and NAWS entry carry it and
mux_get()/mux_wsz_get() refuse a mismatch — even a leaked entry is inert
on a recycled descriptor, which closes the whole class rather than the one
instance above.
DF_AUTH_AUTO (flag 4096): -A publishes an identity, it does not prove
one. AXIL_AUTOAUTH authenticates every connection as the server's own
account, so under -A a name comparison cannot tell an asserted identity
from a published one. The flag is set only by AXIL_AUTOAUTH (so it cannot
survive descriptor reuse), drop_priviledges() refuses to spawn a child
under it — a child would otherwise run as the operator while the request
proved nothing — and any downstream authorization that trusts REMOTE_USER
must refuse a descriptor carrying it (SECURITY.md S5.8).
FD_VALID(fd) before any [FD_SETSIZE] table. One macro, checked
before the first index rather than after the frame has already been touched,
covering descr_map, io, frame_map and ws_flags; on Windows
socket_t is the unsigned SOCKET, so >= 0 would be a tautology and
INVALID_SOCKET (~0) is rejected by the < FD_SETSIZE half. Accept now
spells the sentinel out (fd == INVALID_SOCKET || fd == 0) instead of the
fd <= 0 test that never fires for an unsigned ~0, and every teardown path
closes with axil_sock_close() (closesocket() on Winsock) instead of
close() on a CRT fd.
- Tests:
test-auth, test.sh and test-ws.py gain the teardown,
recycled-fd, DF_AUTH_AUTO and FD_VALID assertions; SECURITY.md records
S5.4 and S5.5.
Verification
gpg --verify CHECKSUMS.asc
sha256sum -c CHECKSUMS