From 7a4f10cd8c76e92642a547dc01300a9106a40ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 4 Sep 2026 15:20:05 +0200 Subject: [PATCH] TLS 1.3: compare the 0-RTT fresh start reference against a whole second The RFC 8446 section 8.2 check flags a resumption ticket as belonging to a previous server instance when ticketSeen is below ctx->ticketStartTime. The two are read from different clocks: ticketStartTime comes from TimeNowInMilliseconds(), while ticketSeen is sess->bornOn scaled up and bornOn comes from LowResTimer(). Where those are separate clocks - glibc reads time() from the coarse realtime seconds and gettimeofday() from the fine grained one - the coarse clock can still report the previous second for up to a tick after each second boundary. A ctx created in that window records the new second while a session born microseconds later records the old one, so a freshly minted ticket is dated before the ctx that minted it and 0-RTT is silently refused. The PSK is still accepted, so the handshake resumes and only the early data disappears. cbcaf3110 already dropped a whole second for this, but guarded it to WOLFSSL_32BIT_MILLI_TIME for the unrelated reason that 2^32 is not a multiple of 1000. Apply it everywhere. A ticket minted in the second before the ctx is then accepted, which does not matter for a heuristic about freshly started servers. This is what makes test_tls13_early_data_0rtt_replay fail intermittently on Linux CI. It does not reproduce where time() and gettimeofday() are coherent, which covers macOS and the usual container VMs. Injecting the lag with an LD_PRELOAD time() shim reproduces it within a few hundred iterations, and the fix survives 20000 iterations under the same shim. --- src/internal.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/internal.c b/src/internal.c index 94252a21666..45862ec96ce 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2670,15 +2670,13 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap) } else { ctx->ticketStartTime -= ctx->ticketStartTime % 1000; - #ifdef WOLFSSL_32BIT_MILLI_TIME - /* A 32 bit ms clock is truncated mod 2^32, which is not a multiple - * of 1000, so the modulo above does not remove the true sub-second - * part. Drop a whole further second so a ticket minted in the same - * second as this ctx is never flagged as predating it. */ + /* Drop a further second so a ticket minted in the same second as the + * ctx is never flagged as predating it: ticketSeen is read from + * LowResTimer() rather than this clock, and a wrapped 32 bit ms clock + * keeps its true sub-second part through the modulo above. */ if (ctx->ticketStartTime > 1000) { ctx->ticketStartTime -= 1000; } - #endif } #endif