-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 28 amissl issue
Filed: https://github.com/jens-maus/amissl/issues/111 (2026-05-31).
This file keeps the full diagnostic + reproduction recipe alongside the rest of the Phase 28 paper trail so the work doesn't have to be re-derived if the GitHub issue is closed or migrates. Mirror updates to both places when material new evidence comes in.
The original report framed this as an AmiSSL or Cloudflare-specific bug; the maintainer pushed back on that framing:
-
AmiSSL is OpenSSL. "There are very few differences" between AmiSSL and stock OpenSSL, and "no functional differences between the Amiga-specific AmiSSL v4 and v5 APIs." The fingerprinting / ClientHello speculation below is wrong.
-
Root cause is processor speed, not Cloudflare strict-fronting. "Broken pipe usually means the processor is too slow" — the server's request-read timeout fires before the Amiga finishes the handshake + writes the request. The maintainer notes that
api.github.comcloses after 4-5 seconds;example.comis a little longer. The "Cloudflare-specific" pattern is a side effect of who happens to use the shortest timeouts, not a fingerprinting heuristic. Their own sites behind Cloudflare don't time out at modern desktop speeds. -
The
s_client <T:req.txtreproduction may itself be broken. The maintainer notes that stdin redirection from a file isn't expected to work in their port ofs_client: "All output in the tool is native, not using stdio/stdout. So, maybe there is an issue with the input. If you type in s_client, it works fine." For oururequestspath the redirection question doesn't apply (we driveSSL_writedirectly from Python), so the CPU-speed limitation is what bites there.
Practical takeaway for the port: TLS works correctly against hosts whose request-read timeout is generous enough for a 68k CPU to complete the handshake + write the request. Modern edges (Cloudflare, GitHub API, etc.) tighten that window aggressively and the Amiga just can't keep up. Document as a CPU-speed limitation rather than an SSL bug; don't try to "fix" it at the SSL layer.
The "Probable cause (speculative)" section near the bottom of this doc is now known-wrong and kept only for historical context.
Title: openssl s_client post-handshake CONNECTION CLOSED BY SERVER + tls_retry_write_records: Broken pipe against
Cloudflare-fronted hosts
- AmiSSL 5.27 (
amissl_v362.library) - AmigaOS 3.x under Amiberry, UAE bsdsocket.library emulation (lib v4.1, NegSize 0x12c — original BSD API, gethostbyname-based DNS, getaddrinfo unavailable)
- Date: 2026-05-31
A clean TLS 1.3 handshake against a Cloudflare-fronted host (e.g.
www.example.com) completes successfully with full cert
verification, then the server closes the connection before any
application data can be sent. The next SSL_write returns EPIPE
("Broken pipe") and recv returns errno 54 (ECONNRESET).
The same trace appears against api.github.com. Direct origins
that don't strict-front (e.g. www.python.org) work cleanly.
From an AmigaShell on a machine with AmiSSL installed:
1> Echo "HEAD / HTTP/1.0*NHost: www.example.com*NConnection: close*N*N" >T:req.txt
1> AmiSSL:OpenSSL s_client -connect www.example.com:443 \
-servername www.example.com -CApath AmiSSL:certs -brief <T:req.txt
Connecting to 104.20.23.154
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = example.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
CONNECTION CLOSED BY SERVER
4000FFC8:error:80000020:system library:tls_retry_write_records:Broken pipe:../../openssl/ssl/record/methods/tls_common.c:1949:tls_retry_write_records failure
With -verify 2 and full chain output, all four certs return
verify return:1 and the handshake reports SSL handshake has read 5051 bytes and written 1626 bytes, Verify return code: 0 (ok).
HTTP/1.0 200 OK (or the response Cloudflare normally serves to a
plain TLS 1.3 client without ALPN).
| Flag | Result |
|---|---|
-groups X25519 (skip post-quantum MLKEM hybrid) |
same close-after-handshake |
-no_ticket (disable session tickets) |
same close-after-handshake |
-tls1_2 (force TLS 1.2) |
Cloudflare rejects ClientHello: unexpected eof while reading
|
So this isn't MLKEM-specific, isn't session-ticket-related, and dropping to TLS 1.2 isn't an option (Cloudflare has phased it out for most edge configurations in 2026).
Pinning the OpenAmiSSLTags() APIVersion to each installed
v5 sub-version in turn (via the
-X sslver=<N> flag added to the MicroPython port for this
purpose) shows the failure is consistent across the entire
available v5 line:
| Library | AMISSL_V* |
APIVersion | Result |
|---|---|---|---|
amissl_v352.library (v5.22, Aug 2025) |
AMISSL_V352 |
41 | handshake ok, write broken pipe |
amissl_v353.library (v5.23, Sep 2025) |
AMISSL_V353 |
42 | handshake ok, write broken pipe |
amissl_v354.library (v5.24, Sep 2025) |
AMISSL_V354 |
43 | handshake ok, write broken pipe |
amissl_v360.library (v5.25, Oct 2025) |
AMISSL_V360 |
44 | handshake ok, write broken pipe |
amissl_v361.library (v5.26, Jan 2026) |
AMISSL_V361 |
45 | handshake ok, write broken pipe |
amissl_v362.library (v5.27, Apr 2026) |
AMISSL_V362 |
46 | same |
So the behaviour is not a v5.27 regression — it has been present continuously across at least 8 months of v5 releases. That points to a structural / fingerprint-shape issue in AmiSSL's TLS 1.3 ClientHello (or its OpenSSL 3.x configuration), not to a specific patch that broke things recently.
| Attempt | Result |
|---|---|
SSL_CTX_set_alpn_protos(ctx, "http/1.1") (advertise ALPN) |
same close-after-handshake |
Switching amiga_ssl_open to OpenAmiSSL() + InitAmiSSL() (v4 init API), keeping the rest of the wrapper unchanged |
SSL_connect now fails earlier with SSL_ERROR_SYSCALL / EIO — the v4 init evidently doesn't bring along the OpenSSL 3.x provider state that v5 needs for TLS 1.3 |
SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY) (transparently retry on post-handshake control records) |
same close-after-handshake |
SSL_CTX_set_ciphersuites(ctx, "TLS_CHACHA20_POLY1305_SHA256") (force a different TLS 1.3 suite than the default TLS_AES_256_GCM_SHA384) |
same close-after-handshake |
Kitchen sink: SSL_OP_ALL | SSL_OP_NO_TICKET + SSL_CTX_set_min/max_proto_version(TLS1_3_VERSION) + SSL_CTX_set_session_cache_mode(SSL_SESS_CACHE_OFF)
|
same close-after-handshake |
SSL_get_peer_certificate(ssl); X509_free(...); between SSL_connect and SSL_write (match the canonical AmiSSL test/https.c "settling" sequence) |
same close-after-handshake |
Explicit BIO chain: BIO_f_buffer → BIO_new_socket(fd) on the write side via SSL_set_bio, replacing SSL_set_fd
|
SSL_write now reports success (61 bytes accepted into the buffer), but the next SSL_read returns SYSCALL/EIO. The buffer just defers the symptom — Cloudflare still closes, we just notice it later. Not a real fix. |
Pass AmiSSL_UsesOpenSSLStructs, TRUE to OpenAmiSSLTags() instead of FALSE (request the actual OpenSSL struct layouts rather than opaque pointers) |
same close-after-handshake |
The v4 init path produces a different broken state, suggesting
the divergence between the two init paths inside AmiSSL is
non-trivial — it's possible that v4 init enables an option that
v5 init doesn't (default ciphers, extension order, SSL_OP_*
flags, etc.) that the server happens to accept.
A useful next investigation would be: instrument both paths to
log the OpenSSL SSL_CTX options / cipher list / sigalgs after
init returns, and diff. Whatever differs is a good candidate for
the actual trigger.
Same s_client invocation against www.python.org:443 completes
handshake, writes the request, reads the response, and closes
cleanly. So TLS 1.3 itself is fine; what's specific is something
about AmiSSL's ClientHello/handshake that Cloudflare-class fronts
choose not to follow up on.
Cloudflare uses TLS client fingerprinting (JA3/JA4) and aggressively closes connections from clients whose fingerprint matches their bot/abuse heuristics. AmiSSL's ClientHello extension order, advertised groups, or signature algorithms may produce a fingerprint Cloudflare doesn't accept. Worth comparing the AmiSSL ClientHello byte-for-byte against a modern desktop OpenSSL 3.x to spot any divergence (extension ordering, omitted extensions, or differing default lists).
Alternatively, the close could be in response to a specific post-handshake message — a NewSessionTicket the client doesn't ACK properly, a KeyUpdate, or a handshake completion alert AmiSSL doesn't emit in the way modern OpenSSL does.
When asyncio-over-TLS landed, the non-blocking memory-BIO stream
path (the one built for ssl_poll) succeeded on HTTPS GETs to both
www.python.org (52 KB body) and example.com where the blocking
fd path (SSL_set_fd, used by urequests) failed in the same
session with OSError(5) / "Broken pipe". The fd path relies on a
single blocking SSL_connect; under the Amiga's slow handshakes that
gets a broken pipe before completing. The stream path pumps ciphertext
itself with proper EAGAIN handling, so it tolerates the slowness.
Lead: routing the blocking urequests client through the stream path
too (i.e. make the fd path the exception, not the default) would likely
cut the intermittent broken-pipe failures — and may also sidestep the
Cloudflare close, since we control the read/close sequencing rather than
leaving it to SSL_set_fd's socket I/O.
modssl.c now drives every TLS socket through the memory-BIO stream
path; the SSL_set_fd fd path is gone. urequests (blocking,
CERT_REQUIRED + set_default_verify_paths) verified live on Amiberry:
a single GET to https://www.python.org/ returns 200 / 52085 bytes, and
3/3 single-request-per-process runs succeeded — reliable now.
The remaining flakiness is not the transport. Issuing several
requests inside one process still fails after the first (EIO / EINVAL),
and a CERT_NONE asyncio client hitting the same hosts back-to-back
succeeds where a CERT_REQUIRED urequests loop fails on either
transport. So the cost/leak is in the per-request SSLContext +
set_default_verify_paths() (a fresh CA store loaded and not freed until
GC piles up). Real fix lives in urequests: reuse one SSLContext
across requests, or force a GC between them — tracked separately from
this AmiSSL handshake issue.