Skip to content

0.8.4

Latest

Choose a tag to compare

@boazsegev boazsegev released this 02 Nov 14:42
· 2 commits to master since this release

Security: (md2html) rendering failures leave a supplied output buffer unchanged, and allocation failures return NULL instead of partial HTML. Image-alt rendering uses linear delimiter matching without recursive calls, preventing nested-image stack exhaustion and quadratic bracket rescans.

Security: (mustache) oversized section names and incomplete bytecode on allocation failure are rejected. Length-delimited filenames and front matter no longer read past their bounds, partial traversal checks recognize both path separators, and callback values and empty loader buffers are released after their last use without leaking or releasing borrowed contexts.

Security: (http, http1) path and cookie parsing check input bounds, ordinary header names and values count toward the configured limit, and client request lines and authority fields are validated. Response serialization and body-storage failures return errors or request closure; file writes honor offsets, and a short declared file range stops the stream. Static-file 304/416 responses release their descriptors.

Security: (websocket, sse) fragmented input retains parser state, invalid UTF-8 and SSE field injection are rejected, and decoded-size limits apply before full output allocation. Buffer and file writes honor offsets and ownership; failed frame allocation preserves fragment state and releases transferred resources.

Security: (pubsub) malformed IPC lengths and missing payload terminators are rejected before delivery. History cache accounting saturates instead of wrapping, oversized entries are rejected, and asynchronous replay retains its request until completion. Eviction remains per-channel, not globally oldest-first.

Security: (redis) HELLO completes before queued commands are sent, reply allocation failures reject partial values, and written commands are not replayed after connection loss. Publish sizing and IPC payload bounds are checked, worker reply decoding requires complete input, and engine destruction releases partial parser state. Worker callbacks still depend on IPC delivery; there is no outbound queue budget or reply deadline.

Security: (secret) The memory mask is now set once per process; before, every fio_secret_set_at call rotated it, which corrupted earlier fio_secret_set_at values and could change the global fio_secret() (the IPC key). A whitespace-only secret now reports fio_secret_is_random(), so IPC no longer opens a cluster listener with an unshared random key. SECRET_LENGTH is validated against SECRET (before, a negative or oversized value read out of bounds at startup); invalid values are ignored with a warning. Decoded and random secret bytes are wiped from the stack, and fio_secret_at(NULL) returns zeros.

Security: (argon2) fio_argon2_hash rejects parallelism above 2^24-1 (before, huge values wrapped the lane math into a division by zero or a heap overflow), memory matrices exceeding the address space (32-bit heap overflow), inputs over 2^32-1 bytes (silently truncated), outlen 1-3 (clamped to 4, overrunning the caller's buffer) and invalid type values. Near-UINT32_MAX tag lengths no longer wrap H′ arithmetic. Blocks use explicit little-endian order (big-endian hosts now match RFC 9106), secret temporaries are wiped on every path, and fio_argon2 documents its all-zero error result. Hashing is ~20% faster (in-place permutation, fused XOR, no zero-fill).

Security: (lyra2) fio_lyra2_hash rejects outlen, costs, n_cols and password/salt lengths above 2^32-1 (before, they were silently truncated, reaching divisions by zero, zero-size allocations and out-of-bounds access) and matrices exceeding the address space (32-bit heap overflow). Password + salt longer than the matrix no longer overflow the heap; they are now hashed. fio_lyra2 documents its all-zero error result.

Security: (pem) private-key parsing is strict: exact DER tags, every SEQUENCE consumed exactly, extra/unknown/out-of-order fields, bad versions, negative/zero/non-minimal INTEGERs, partial RSA CRT sets and malformed P-256 parameters or public keys are rejected instead of being imported or silently dropped. PEM bodies are no longer accepted when they contain undecodable bytes or data after padding (only the valid prefix was used before) while padding variants, the URL-safe alphabet and non-zero trailing bits are decoded leniently, fio_pem_parse returns the exact consumed length (it stopped inside the END marker before), decoded private-key DER is wiped before release, and fio_pem_parse_certificate no longer leaks: the certificate owns its DER, released by the new fio_pem_certificate_free.

Security: (mlkem) decapsulation implicit rejection now returns the full K̄ = J(z || c); before, an invalid ciphertext kept the top 7 bits of every byte of the re-decrypted key K', taking only the low bit from K̄ (FIPS 203 violation). fio_mlkem768_encaps* now perform the FIPS 203 encapsulation-key modulus check and return -1 for keys with coefficients >= q. Matrix rejection sampling no longer reads uninitialized coefficients, noise/decryption temporaries are wiped, and hybrid decapsulation propagates ML-KEM errors.

Security: (p384) DER signature parsing no longer reads past the input on truncated or malformed signatures, and accepts only strict DER (minimal positive INTEGERs, no trailing data). Public-key coordinates >= p are rejected.

Security: (stream) a file packet that hits EOF or a read error no longer reports unread scratch-buffer bytes as data or spins on a stale EINTR; it is truncated to the bytes read and dropped when empty. fio_stream_pack_data returns NULL (not a freed chain) when a later packet allocation fails, rejects lengths of 1 GiB or more on 64-bit Windows too, and reads use a loop instead of per-packet recursion. fio_stream_advance clamps over-advances without wrapping, fio_stream_pack_fd rejects negative descriptors (accepting 0) and ranges off_t can't represent, and FIO_STREAM_COPY_PER_PACKET must be in [1, 2^30).

Security: (io, openssl, tls13) TLS clients with configured trust now check the server's identity. The OpenSSL backend checked only the certificate chain, so a certificate from any trusted CA passed for any host; it now requires the host name (SSL_set1_host). fio_io_connect to an IP address now requires a matching IP address SAN (native TLS 1.3 skipped the identity check). A reused TLS object no longer verifies against a previous connection's host name. IP literals are never sent as SNI.

Security: (io, openssl, tls13) TLS contexts are reference counted, so accepted or connecting TLS connections whose handshake outlives a stopped listener (or a promoted client) no longer read freed certificates, keys or trust stores. fio_io_attach_fd takes ownership of its tls reference on success and failure; listeners pass a new reference per accepted connection using the transport's dup_context callback (a new fio_io_functions_s field; rebuild code embedding it). Listeners stopped before their deferred attach, and accept tasks queued behind a close, no longer touch freed or closed state. Overlong listen URLs (including ADDRESS) fail instead of looping or binding a default address.

Security: (tls13) A configured certificate or private key file that fails to load (missing file, bad PEM, encrypted key, invalid key) now fails the context build with an error, as the OpenSSL backend does; before, the server silently presented a self-signed certificate instead. Only names added without files are self-signed. A connection started without a context is closed at once.

Security: (io, tls13, openssl) TLS is a must when requested: fio_io_listen returns NULL (calling on_stop) and fio_io_connect returns NULL (running on_failed once, on the IO thread) when the TLS context can't be built; before, the listener looked healthy but closed every connection. When tls is set the IO functions are a TLS variant, so a build_context returning NULL means the TLS context couldn't be built. Native TLS 1.3 now checks that the private key matches the leaf certificate (P-256, Ed25519, RSA; like OpenSSL's X509_check_private_key) and fails the context build on a mismatch.

Security: (tls13) The native TLS adapter no longer overflows its receive buffer when a failing record follows valid data in one read batch; only the failing record is retained and the error surfaces on the next read. Malformed complete handshake records (for example an invalid content type) now fail the connection instead of spinning the reactor thread. A connection-state allocation failure closes the connection instead of treating the shared context as connection state.

Security: (ipc) unauthenticated peers can no longer force large allocations (frames bigger than the read buffer wait until the handshake completes), and allocation or queue failures close the connection instead of aborting or leaking. Cluster connections now prove possession of a per-connection challenge before a declared UUID is accepted, so a replayed declaration can't impersonate a node; the replay filter keeps every ID inside the 30 second window, failing closed on overflow. UDP discovery handles bounded batches with rate-limited logs, failed decryption stays final on retry, and NULL data buffers add zeros instead of uninitialized bytes.

Security: (openssl) A connection setup failure (SSL_new, allocation or BIO creation) now closes the connection instead of leaving the shared context where reads and cleanup treated it as connection state. Malformed handshakes, bad records, close_notify and hard socket errors now close the connection promptly instead of waiting for the IO timeout.

Security: (tls13) the server PEM loader wipes key file contents, rejects a P-256 key whose embedded public key does not match (deriving it when absent; previously a random one was generated), and treats any undecodable certificate block as a load error instead of using a partial chain. CA bundles log and skip undecodable blocks; blocks over 16 KiB now load. The IO write path reserves room for a pending KeyUpdate response in the first record, so it always leads the data. If the response cannot be produced, the write fails with ECONNRESET and the connection closes. Data is never sent under stale keys. On close, close_notify is queued behind pending encrypted output instead of being written straight to the socket, so it can no longer reach the wire ahead of earlier records (the peer would fail to decrypt them out of order). If it can't be encrypted or queued, it is skipped.

Security: (tls13) a HelloRetryRequest cookie can no longer overflow the ClientHello2 stack buffer, record decryption enforces the caller's exact output capacity (it could write up to 15 bytes past it), and fio_tls13_build_client_hello checks capacity before writing (long SNI or many cipher suites overflowed). Servers requesting client certificates now fail closed without a trust store (any self-signed certificate was accepted as verified); client and server certificates are checked for the clientAuth / serverAuth purpose. RSA PKCS#1 v1.5 CertificateVerify signatures, unoffered server ALPN selections and ServerHellos without supported_versions are rejected (RFC 8446 / RFC 7301). Records refuse to wrap the sequence number (nonce reuse), key schedule failures abort the handshake instead of installing stale keys, the standalone encrypt functions send a requested KeyUpdate response before application data, and fio_tls13_client_is_cert_verified returns 0 when verification was skipped. ClientHello2 now repeats ClientHello1 (ALPN was dropped) and a cookie-only HelloRetryRequest is accepted. The client rejects ServerHello key shares it never sent (P-256 without an HRR, a hybrid share it didn't offer) and a ServerHello2 that changes the HRR's group or cipher suite. Handshake messages spanning records are reassembled (bounded by FIO_TLS13_MAX_HANDSHAKE_MSG); previously they were silently dropped (client) or rejected (server). Data after a message that precedes a key change is rejected. Malformed certificate lists are rejected instead of keeping a valid prefix. Every post-handshake message in a record is processed, with strict KeyUpdate framing. Keys whose length doesn't match the cipher are refused. The server's CertificateRequest lists the schemes it verifies (adds ECDSA P-384 and RSA-PSS SHA-384), and so does the client's signature_algorithms (adds ECDSA P-384). A missing trust store is logged once as a warning instead of an error on every connection.

Security: (p256) DER signature parsing no longer reads past the input on truncated or malformed signatures, and accepts only strict DER (minimal positive INTEGERs, no trailing data). Public-key coordinates >= p are rejected, and nonce/key generation fails with -1 instead of looping on a stuck RNG. New fio_p256_public_key derives the public key for an existing secret key.

Security: (der, x509) fio_der_parse_time no longer reads past the element on malformed peer certificate times (e.g., a missing Z), and rejects trailing data, local times and invalid calendar values (zone offsets, optional minutes/seconds and UTCTime fractions are now decoded). The parser rejects tag numbers above 255 (previously truncated, e.g. tag 256 aliased [0]) and stays lenient on unambiguous non-minimal lengths and tags. fio_der_parse_oid decodes multi-byte first arcs (2.999.3) and rejects overflowing or non-minimal arcs. fio_der_is_tag and the typed fio_der_parse_* helpers compare the full identifier (class, constructed bit, number), so a primitive 0x10 is no longer a SEQUENCE, a constructed 0x22 no longer an INTEGER, and [0] no longer universal tag 0. fio_der_parse_integer rejects negative INTEGERs when a value is requested (X.509 enforces minimal INTEGERs itself). fio_der_is_context_tag and fio_der_tag_number handle high-tag numbers. Time encoders no longer narrow to time_t and return 0 when unrepresentable (UTCTime covers 1950-2049); X.509 validity uses GeneralizedTime after 2049.

Security: (x509) trust anchors are matched by key, not DN: fio_x509_is_trusted and chain validation require a byte-identical anchor or a same-DN anchor whose key verifies the signature (all same-DN anchors are tried). Chain validation now enforces issuer Key Usage (keyCertSign) and pathLenConstraint, leaf Key Usage (digitalSignature), Extended Key Usage (new fio_x509_verify_chain_purpose / fio_x509_purpose_e), Name Constraints (dNSName, iPAddress, permitted directoryName; other forms fail closed) and rejects unknown critical extensions, with new FIO_X509_ERR_KEY_USAGE, _PATH_LENGTH, _NAME_CONSTRAINTS and _CRITICAL_EXTENSION codes. Parsing rejects extra elements or trailing bytes inside certificate structures, padded key/signature BIT STRINGs, inner/outer signature algorithm mismatches, duplicate or malformed recognized extensions and invalid RSA keys (negative or even modulus, even/small/oversized exponent), while tolerating unambiguous non-DER encodings (non-minimal INTEGERs, KU padding bits are masked, empty SAN/EKU lists grant nothing). cert->der now covers only the Certificate TLV, so callers can detect trailing bytes. The outer signatureAlgorithm must match the signed copy (by decoded value for supported algorithms, byte for byte otherwise). fio_x509_fingerprint stays the standard SHA-256 of the exact certificate bytes; it is not a unique certificate identity (non-minimal headers outside the signed TBS and ECDSA signatures are malleable), so blocklist by issuer + serial or SPKI instead; RSA-PSS parameters are validated and signature algorithms must match the key type. IP-literal hostnames match only SAN iPAddress entries. fio_tls_parse_certificate_message rejects length mismatches and lists exceeding max_entries. fio_x509_self_signed_cert rejects reversed validity, encodes IP SANs as iPAddress and adds clientAuth to the EKU. fio_x509_cert_s grew (name_constraints, path_len, ext_flags).

Security: (chacha, aes) inputs that would wrap the 32-bit block counter (reusing keystream) now fail with -1 instead of being processed, and never abort the process. fio_chacha20, fio_xchacha20, the ChaCha20-Poly1305 / XChaCha20-Poly1305 / AES-GCM encryption functions and the shared fio_crypto_enc_fn type now return int (0 on success); on failure the data (and tag) are zeroed so no plaintext or valid tag is left behind. Decryption rejects over-limit input without touching the data. fio_x25519_encrypt and TLS 1.3 record encryption propagate the error. All public ChaCha20 / Poly1305 functions also reject NULL key / nonce / mac and NULL data / ad with a non-zero length the same way (previously fio_poly1305_auth and the others dereferenced them, or asserted in debug builds); the MAC-only functions fio_poly1305_auth and fio_chacha20_poly1305_auth now return int (0 on success, -1 on invalid arguments or, for the AEAD variant, over-limit input) and zero a writable MAC on failure, so callers reject instead of comparing a cleared tag (IPC UDP discovery now does). Zero-length NULL buffers remain valid.

Security: (aes) AES-GCM now rejects AAD of 2^61 bytes or more (the bit length would overflow) and wipes key-schedule, GHASH-key and keystream temporaries on every backend.

Security: (ed25519) fio_ed25519_verify rejects small-order public keys and R values, non-canonical keys (y >= p, negative zero) and S >= L; before, an identity public key verified a fixed signature for any message. The base-point table is now a compile-time constant (no racy lazy initialization), and secret scalars/nonces are wiped after use. fio_x25519_decrypt zeroes the plaintext on authentication failure and fio_x25519_encrypt zeroes its output when the cipher fails.

Security: (rsa) verification rejects even moduli and public exponents that are even, below 3 or over 64 bits; before, a certificate key with e = 1 let anyone forge signatures. A zero-prefixed modulus no longer causes out-of-bounds reads, and PSS now works for moduli whose bit length is 8k + 1. Signing no longer divides by the secret CRT primes (a timing leak). Unbalanced primes are handled, and every signature is checked against e, so faults or inconsistent keys return -1 instead of emitting a bad signature. Nothing aborts on bad input. Signing is about 3.5x faster and verification about 90x faster.

Security: (random, ipc) RiskyHash 256/512 adds round constants and per-block feed-forward, so a chosen block can no longer erase earlier input. The new fio_risky256_keyed / fio_risky512_keyed seed all 16 state words from the key, and the HMACs are now nested keyed hashes; before, one lane was unkeyed, allowing key-independent tag forgeries. IPC derives per-message keys with fio_risky256_keyed, keyed by a 32-byte secret window that rotates with the message id. RiskyHash 256/512 outputs change.

Security: (resp3, redis) strict RESP3 framing and length validation prevent reply misassociation, preserve attribute ownership, and enforce chunked-string payload budgets. Large chunks stream incrementally through bounded read buffers; malformed input and container-count or depth errors are rejected before allocation callbacks.

Security: (files) recursive removal normalizes trailing separators before checking links and holds no-follow directory descriptors on POSIX. Windows traversal holds reparse-safe directory handles without write/delete sharing to block directory substitution during descent.

Security: (http1) a rejected Expect: 100-continue (i.e., 413) now closes the connection instead of parsing the declared body as the next request (request smuggling). An over-limit chunked body no longer dispatches a NULL handle (crash) when its last chunk arrives in the same read.

Security: (http1) the parser rejects framing differentials: gzip chunked style transfer-coding lists, missing or bare-CR chunk data delimiters, incomplete empty lines after the last chunk, and status codes that aren't exactly three digits.

Security: (http1) a client receiving an over-limit response (body or headers) closes the connection instead of crashing or sending an error "response" to the server. Expect in a response is a regular header, so a server can no longer make the client write 100 Continue.

Security: (multipart) fragmented part headers and fields no longer cause quadratic rescans (tens of seconds of CPU per 32 MiB request): the parser resumes its search and part headers are capped by FIO_MULTIPART_MAX_HEADER_SIZE (8192). Malformed closing delimiters (e.g., --B--junk, a partial CRLF) are errors, reported before the part's callbacks.

Security: (multipart, http) a closing delimiter ending exactly at the end of a read is no longer trusted: its part waits until the line's tail ([padding] CRLF, or the end of input) is validated, so --B--garbage split at a read edge is an error. Closing-line padding is capped like part headers.

Security: (sha2) SHA-256 / SHA-512 length counters no longer truncate to size_t, so streams over 4 GiB hash correctly on 32-bit targets, and SHA-512 encodes the full 128-bit length. HMAC-SHA-256/384/512 wipe key-derived stack state before returning and accept a NULL key with zero length.

Security: (sha3, mlkem) SHAKE partial-word output is now correct on big-endian systems. fio_sha3_finalize and the one-shot functions wipe their context, fio_sha3_consume accepts (NULL, 0), and ML-KEM wipes its secret temporaries with fio_secure_zero.

Security: (hkdf) fio_hkdf_extract, fio_hkdf_expand and fio_hkdf return int (0 or -1) and reject over-limit output lengths, PRKs shorter than the hash length, and NULL pointers with a length, without writing output (previously lengths were silently clamped and a huge info_len could wrap the work-buffer allocation). NULL info is empty info. Expand no longer allocates, reuses the keyed HMAC states per block (faster), and wipes its secret intermediates.

Security: (otp) fio_otp / fio_otp_at return the new FIO_OTP_ERROR for digits above 10 (before, a division by zero or a code below 15), Hex secrets with invalid digits ("0G" aliased "10") or over 128 bytes (silently truncated), and Base32 secrets with invalid characters or data after = padding (before, silently decoded as a prefix). Secrets otherwise parse leniently: any case, whitespace / - / : separators, and optional or extra Base32 padding. Ten-digit codes are correct on 32-bit targets, keys over 64 bytes follow RFC 2104 (the nonstandard SHA-512 pre-hash is gone, so such keys now match authenticator apps), and key material is wiped. fio_otp_print_key documents its 33-byte requirement for generated keys and returns 0 instead of aborting on error.

Security: (string core) Base32 and Base64 decoding now check the whole input first. They return -1 and leave dest unchanged on invalid characters, data after = padding or impossible lengths; before, they returned success with a partial or zero-filled result. Both stay lenient where decoding is unambiguous: whitespace, any amount of padding (or none), non-zero trailing bits and empty input are accepted, and Base64 still accepts both alphabets.

Security: (websocket) close writers treat a NULL reason as empty instead of emitting uninitialized stack bytes. The parser rejects non-minimal payload-length encodings (1002), and FIO_WEBSOCKET_DEFAULT_MAX_FRAME overrides above UINT32_MAX fail at compile time.

Security: (entities) numeric entity decoding respects the explicit source length, including truncated input passed through GFM and Markdown-to-HTML rendering.

Security: (url, ipc, sock) TLS diagnostics no longer print key, certificate, trust or passphrase values; IPC diagnostics and socket URL-length errors no longer dump complete URLs. URL parsing no longer reads past empty hosts, and TLS detection uses bounded reads for three-byte query names and scheme slices.

Security: (signals) handler snapshots and atomic pending consumption prevent mixed callback/data pairs and lost deferred notifications during review. Immediate NULL callbacks are ignored, and POSIX propagation forwards the original SA_SIGINFO arguments. Reactor stop handlers defer unsafe shutdown work to normal context.

Security: (logging) invalid FIO_LOG_LENGTH_LIMIT values are rejected at compile time. Truncated messages with limits from 3 through 128 no longer emit uninitialized stack bytes; the default limit was unaffected.

Security: (redis) error-response log fields escape control and non-ASCII bytes, preventing peer-supplied terminal commands. Returned error strings remain unchanged.

Security: (atol) fio_aton no longer reads 8 bytes past short inf prefixes such as infi, which could fault on RESP3 or other exact-length input. The wide-integer hex readers no longer read past empty input.

Security: (imap) inserts fail safely instead of writing past a full map, and colliding hashes no longer cause runaway memory growth. Growth leaves the map unchanged on failure, checks capacity arithmetic, and keeps the index aligned for odd-sized elements.

Security: (glob) fio_glob_match no longer reads past a pattern ending in \ (which also wrapped the remaining length); a trailing escape is now a non-match. Malformed class ranges no longer form out-of-bounds pointers.

Security: (ipc) message IDs that cannot be recorded are dropped instead of processed, preventing replays. Filter truncation now keeps the newest 16,384 IDs instead of mostly the oldest.

Security: (core, random) fio_secure_zero uses unaligned-safe word stores followed by the new FIO_COMPILER_GUARD_PTR(ptr) barrier, which makes the buffer visible to the barrier so its stores cannot be removed as dead (a plain FIO_COMPILER_GUARD doesn't cover private stack buffers; no memset, same speed). Risky Hash 256/512 and their HMACs wipe internal state and padded blocks.

Security: (ipc) messages carry a fresh random nonce per encryption, so ChaCha20-Poly1305 keys never repeat even for forced or repeated timestamp/ID pairs; replies default to a fresh ID. Every connection's first message must be the peer's sealed UUID declaration (local: fresh within 1 second, same instance, opposite role; cluster: 30 second window, replay filter, unique UUID) before it receives broadcasts or has messages processed. Cluster duplicates are dropped before decryption, and cluster pings no longer close the connection.

Security: (websocket) generated client masks use all 32 random bits instead of forcing four bits on.

Security: (p256) ECDSA signing no longer reduces secret scalars with variable-time division. Scalar reduction now uses fixed-work Barrett reduction, which also makes it about 200 times faster.

Security: (sort) quicksort falls back to heap sort when partitioning degenerates, so crafted input can no longer force O(n²) comparisons. FIO_SORT_THRESHOLD values below 3, which caused out-of-bounds access, are rejected at compile time.

Security: (pubsub) the history cache no longer dereferences NULL when a message is cached in a channel after an eviction. Eviction now removes the oldest message in that case.

Security: (mem) realloc of page-mapped memory rejects sizes that wrap the mapping length, which could unmap a live allocation. Failures keep the old allocation and set ENOMEM. calloc and calloc_aligned catch every product overflow, and calloc(1, SIZE_MAX) no longer returns the shared zero-size pointer.

Security: (string) fixed-size and allocation-failed writes no longer overflow: JSON escaping and write2 floats truncate in bounds, and fio_string_replace rejects wrapping ranges. Truncated \x, octal, %u and HTML entities no longer read past the input, and HTML unescaping no longer under-reserves &#NN entities without ;. Shrinking a shared fio_bstr and failed fio_bstr_len_set growth no longer write out of bounds, and key-string OOM no longer borrows the caller's buffer.

Security: (mem) in-place realloc2 zeroes old bytes past copy_len, as documented for FIO_MEMORY_INITIALIZE_ALLOCATIONS. In-place growth no longer reserves more block space than requested.

Fix: (tls13) Handshake flights are sized from the certificate chain and fragmented into records of at most 16 KiB of plaintext, instead of failing above 4 KiB of handshake messages (8 KiB in the IO layer), so chains such as Let's Encrypt's with a cross-sign work in both directions; longer flights use a temporary allocation that is wiped and released after flushing, and the IO output queue grows only while such a flight is queued. The IO read path compacts a full receive buffer before reading, so a maximum-size record following a short one (ServerHello + CCS) no longer stalls the handshake. RSA private keys loaded from PEM keep e and the CRT parameters (before, only n and d were stored), restoring CRT signing, blinding and the fault check. Peer signature scheme lists keep the first 16 known schemes rather than the first 16 offered, so long lists of unknown schemes can't crowd out supported ones, and the server's EncryptedExtensions checks its output capacity including the ALPN extension.

Fix: (rand) FIO_DEFINE_RANDOM128_FN (and so fio_rand64 / fio_rand128) could repeat values across racing threads in unoptimized builds (MSVC /Od): the atomic call counter shared a slot with a working lane, so plain stores republished already-consumed counter values. The per-call value is now lane 1 (as before) plus a sequence number from its own slot of the 64 byte state, which is only written atomically; the mixing math is unchanged, but output streams differ from previous versions.

Fix: (tls13) Empty or non-application records (for example a fragmented empty record or NewSessionTicket) no longer read as EOF and close the connection. Native TLS clients now offer the registered ALPN names, and both roles call the negotiated protocol's on_selected callback when the handshake completes.

Fix: (openssl) TLS records that arrive together with the peer's FIN are delivered before EOF instead of being dropped. Records buffered behind a post-handshake message no longer stall. Clients now call the negotiated ALPN protocol's on_selected callback, ALPN names of 255 bytes are accepted, and the deprecated NPN callback is gone.

Fix: (io) Async queues (fio_io_async_attach) are never destroyed while a task may be pending: stopping the reactor or forking workers joins the worker threads and runs pending tasks (before ON_STOP callbacks and forking), late tasks run on the next start, and at exit every queue is drained until empty (including tasks pushed by tasks, timer on_finish or global env on_close) before release. Forked workers reset inherited async locks and threads and still run inherited tasks, workers added before fio_io_start no longer leave the master async queues stopped.

Fix: (io) The wakeup descriptor is closed only after in-flight writers finish, so a late fio_io_defer can't write to a closed or reused descriptor, and a restart no longer swallows wakeups through a stale flag. fio_io_free no longer reads IO flags off the IO thread and always wakes the reactor after queuing the release.

Fix: (io) fio_io_restart stops old workers when no restart signal is configured (it sent signal 0), sentinel threads retry interrupted waitpid, a worker whose sentinel can't start is reaped, and fio_io_workers(INT_MIN) no longer overflows.

Fix: (io) An IO is removed from the poll backend before its descriptor is closed. On epoll, a listener's duplicated descriptor stayed registered after close, so a listener that survived a reactor restart could dispatch its freed IO from the previous cycle (use-after-free and double free; reported by iodine).

Fix: (p256, p384) ECDSA verification with the public key Q = -G no longer treats the point at infinity (G + Q) as the affine point (0,0) during double-scalar multiplication. Valid signatures for that key now verify, and a sum at infinity is rejected.

Fix: (io) Graceful close (fio_io_close) no longer drops output the transport is still holding when the socket is full. This affects encrypted TLS records and the close_notify alert. Before, the reactor called finish and closed the socket once its own stream was empty. It now waits until the transport's flush returns 0, calls finish once, and waits for flush to return 0 again before closing. The wait is bounded: a closing IO that reaches its connection timeout is closed at once, and the unsent data is dropped. This happens even when on_timeout would keep an open connection alive (for example fio_io_touch). The behavior applies to both TLS backends (tls13 and openssl), and no API changed.

Fix: (extern, atol, poll, blake2, deflate, tls13, ipc, string) declarations-only FIO_EXTERN units can now call fio_aton, fio_poll_init / fio_poll_destroy, fio_blake2b / fio_blake2s, fio_deflate_compress_bound / fio_deflate_decompress_bound, fio_tls13_build_nonce, fio_tls13_record_keys_clear, fio_ipc_decrypt and the string type's dealloc. Before, they were always declared static but defined only in the implementation unit, so callers failed to link and GCC failed -Werror builds with -Wunused-function. Header-only builds are unchanged (same static / static inline linkage); tests/extern-split now calls these functions.

Fix: (redis) fio_redis_free transfers its reference to a release task on the main IO queue while the reactor is running, instead of destroying an engine on a calling worker thread. Otherwise it releases directly and must be called from the main thread, including during shutdown; no extra reference or lifetime state is added.

Fix: (extern, fiobj) declarations-only FIO_EXTERN builds: programs using the library header (lib/fio.h) now link FIOBJ big numbers, floats and data objects (their internal helpers were declared static but never defined). With FIO_EXTERN + FIO_EVERYTHING, FIO_EXTERN no longer gets dropped partway through the header, so later modules (IO, TLS 1.3, HTTP) stay declarations-only instead of failing to compile. Debug builds split across translation units report each leak counter once (from the FIO_EXTERN_COMPLETE unit) instead of once per unit; make tests now includes a split-build regression test (make tests/extern-split).

Fix: (core, http, gfm, map, brotli, cli) 32-bit builds: layout assertions in the HTTP params and GFM parsers no longer assume 8-byte pointers (a hard compile error on 32-bit), fio_brotli_decompress_bound saturates instead of overflowing, and the PRNG call counter's 64-bit atomic no longer falls back to a lock-based library call on i386. Headers now compile cleanly under -Wall -Wextra (no -Wmissing-field-initializers, GCC -Wnonnull or 32-bit -Wformat / -Wsign-compare warnings).

Fix: (ipc, pubsub, io) cluster messages, cluster declarations, UDP discovery and pub/sub message timestamps use the wall clock (fio_io_last_tick_time) instead of the monotonic tick, so hosts that didn't boot within 30 seconds of each other no longer reject each other's messages (cluster hosts need synchronized clocks). UDP discovery wrote its timestamp big-endian but read it little-endian, so discovery messages always failed the time window. fio_io_last_tick_time is now initialized at startup and refreshed like fio_io_last_tick.

Fix: (io) fio_io_last_tick and fio_io_last_tick_time return the current time while the reactor is stopped, and fio_io_run_every, fio_io_async_every and new IO objects use them. Timers scheduled between reactor runs used the previous run's last tick, so repeating timers fired a burst of catch-up calls on start, and IO attached before fio_io_start could time out early.

Fix: (http1) the client accepts status lines with an empty or omitted reason phrase (HTTP/1.1 200 or HTTP/1.1 200).

Fix: (files) zero-length bounded filename parsing no longer reads its input. POSIX temporary creation fails if the name cannot be unlinked; save cleanup failures are logged, and the documentation states that atomic replacement does not preserve metadata and cleanup can leave a sibling after a filesystem error.

Fix: (entities) numeric entities reject dividers and duplicate hex prefixes, and surrogate code points become U+FFFD. Named-entity case folding no longer treats control bytes as digits.

Fix: (url) component scans preserve non-ASCII bytes regardless of char signedness. Colon lookahead and query termination keep cursor arithmetic within the input, and zero-length parsing permits a NULL input.

Fix: (threads) Windows thread creation uses a CRT-compatible callback trampoline and retains handles until join/detach; child polling closes handles and returns the child PID. Finite condition waits no longer wrap or become infinite, POSIX create/lock/unlock/priority failures honor their documented -1 result, and invalid priorities fail. Multi-threaded copy treats a failed worker join as fatal rather than returning with live buffer access.

Fix: (string) JSON and URL unescaping pair every high surrogate (D800–DBFF) and replace lone surrogates with U+FFFD. JSON escaping now escapes overlong and surrogate UTF-8, and fio_string_readfd reports short reads.

Fix: (signals) failed removal preserves the watcher, review counts only invoked callbacks, and Windows monitor slots are reusable. Windows rearms before callbacks without overwriting the saved handler; the CRT reset-before-entry window remains. Reactor and OpenSSL check signal setup, and shared SIGPIPE suppression survives reactor shutdown.

Fix: (mem) cleanup at exit is skipped, instead of unlocking another thread's lock, while the allocator is in use. Out-of-memory in DEBUG fio_malloc or no-initialization fio_calloc returns NULL instead of crashing.

Fix: (mem) allocation failures set ENOMEM, and the posix_memalign override never reports success on failure. Windows releases a reservation when its commit fails. FIO_MEMORY_SYS_ALLOCATION_SIZE_LOG values outside 17–24 fail to compile.

Fix: (mem) FIO_MALLOC_OVERRIDE_SYSTEM no longer hangs at startup, and freeing big allocations after exit cleanup no longer crashes. The override also replaces memalign and valloc (plus pvalloc and reallocarray on Linux), and rejects invalid alignments with EINVAL.

Fix: (imap) rehash returns 0 on success. remove returns -1 for maps where zeroed elements stay valid, instead of leaving them visible.

Fix: (atol) hex float exponents saturate to infinity or zero instead of wrapping, so 0x1p18446744073709551615 is no longer 0.5. Hex subnormals such as 0x1p-1073 no longer become zero, and zero with a huge exponent is no longer NaN.

Fix: (atol) fio_u2d / fio_i2d scale subnormals correctly, return infinity rather than NaN at the top exponent, and accept INT64_MIN. The optional FIO___ATON_TIE2EVEN mode now rounds correctly to nearest, ties to even.

Fix: (atol) fio_u128_hex_read and its wider variants parse digits correctly and stop at the first non-hex character. The hex writers NUL-terminate zero.

Fix: (atol) fio_atol_xbase reports E2BIG before an overflow wraps. It and the generic-base helpers reject bases outside 2-36 instead of dividing by zero or looping.

Fix: (memalt) fio_strlen stops at NUL without reading beyond the string, and scalar fio_memchr finds the correct first match on big-endian targets. Copy overlap checks and SIMD search loops avoid invalid pointer operations. fio_memcpy always returns the destination, and fio_memcmp returns exactly -1, 0, or 1.

Fix: (logging) empty messages produce a newline, formatting failures are identified correctly, and logging preserves errno even when output fails. Logs remain best-effort; log-level changes require application synchronization.

Fix: (logging) restored compiler checks for format arguments and corrected mismatches in library and test messages.

Fix: (patches) Windows environment reads reject oversized values and distinguish empty values from errors. Positional writes wait for completion, validate offsets and counts, and restore the file cursor; shared-cursor operations still require caller synchronization. Clock conversion no longer shares mutable state, and file metadata uses 64-bit sizes.

Fix: (files) Windows temporary files use exclusive child names rather than an O_TMPFILE alias. Overlong temporary-directory settings correctly fall back to the current directory.

Fix: (math) fio_math_shl / fio_math_shr handle whole-word, oversized, and in-place shifts without invalid pointers, and fio_math_inv propagates carries across words. fio_math_div rejects lengths above 1024 words instead of using unbounded stack space.

Fix: (core) build support clarification: the C11 <stdatomic.h> atomics fallback is removed and now stops with an #error. fio_atomic_* operates on plain (non-_Atomic) storage, which the C11 generics do not accept (strict compilers reject it at compile time). Only C11 compilers lacking GNU-compatible __atomic / __sync builtins and MSVC Interlocked intrinsics were affected; GCC, Clang and MSVC never selected this branch, so their builds and performance are unchanged. Not a runtime vulnerability.

API: (p384, pem, tls13) P-384 private keys now sign: fio_ecdsa_p384_sign, fio_p384_public_key and fio_p384_keypair use a constant-time Montgomery ladder, like P-256. FIO_PEM_KEY_ECDSA_P384 loads SEC1 and PKCS#8 P-384 keys (new fio_pem_private_key_s.ecdsa_p384). Native TLS 1.3 servers and clients load P-384 certificates and keys (SNI selection and key/certificate matching included) and sign CertificateVerify with ecdsa_secp384r1_sha384 (0x0503), matching OpenSSL.

API: (tls13) Client certificates work like server ones: RSA-PSS (SHA-256/384), P-256, P-384 and Ed25519 keys, with the scheme chosen from the server's CertificateRequest, and the whole configured chain is sent (new fio_tls13_client_set_cert_chain; fio_io_tls_cert_add on a client context now loads the certificate and its intermediates instead of only a leaf). A client whose key matches none of the server's schemes answers with an empty Certificate and no CertificateVerify (RFC 8446 4.4.2.3). New fio_tls13_flight_size returns an output capacity sufficient for every handshake flight of an endpoint presenting a given chain.

API: (io, openssl, tls13) breaking: fixed inconsistent TLS ownership: fio_io_attach_fd consumes its built-context reference on success and failure, and fio_io_listen / fio_io_connect release their .tls settings reference on every path. To keep a reference, pass fio_io_tls_dup(tls) for settings or the transport's dup_context(context) for built contexts; do not free the transferred reference afterward. Listeners pass dup_context results through unchanged, including NULL (valid for plaintext; the bundled TLS backends close a connection without a context).

API: (tls13) the key schedule functions (fio_tls13_hkdf_expand_label, fio_tls13_derive_*, fio_tls13_update_traffic_secret) return int (0 on success, -1 on invalid arguments) instead of void.

API: (string core, otp) fio_string_write_base32enc pads output with = to a multiple of 8 characters (RFC 4648); before, it added a single =. fio_otp_print_key prints unpadded Base32 with no =, as authenticator apps expect, and no longer fails for keys whose length leaves 1 byte after the last 5-byte group.

API: (multipart) breaking: one stateful parser reports each part once, complete: fio_multipart_init copies the boundary, fio_multipart_parse(parser, callbacks, udata, data, len, eof) resumes block by block (!consumed && !err = more data) and eof replaces fio_multipart_stream_parse / fio_multipart_stream_close. The callbacks are on_string and on_file (complete, borrowed values); on_field*, on_file_start/data/end and on_error are gone. Callback errors are opaque and returned unchanged; the result's field and file counts are removed.

API: (http) breaking: on_file receives the complete upload (data, borrowed: RAM and memory mapped bodies are never copied) and returns an optional {obj, err} stored at the field name; on_file_data is removed. Truncated or aborted uploads never create a file object. Without a memory mapping, a pending part is read into the body's heap buffer, so the body size limit bounds memory use.

API: (redis) fio_redis_state returns a snapshot from any thread without traversing the command queue. FIO_REDIS_STATE_STOPPED is distinct from handshake failure (FIO_REDIS_STATE_ERROR); keep an engine reference alive while querying.

API: (io, http) fio_io_connect and fio_http_connect accept an opt-in .verify flag: a TLS connection without trust entries verifies the server against the system trust store, the same as the (now documented) ?trust=sys URL query. Explicit trust is kept, plain connections ignore the flag, and unverified TLS clients remain the default.

API: (io, openssl, tls13) TLS servers with several fio_io_tls_cert_add certificates select one per handshake from the client's SNI, identically in both backends: case-insensitive exact match, then a single-label wildcard (*.example.com), else the default certificate (the one added with an empty server name, else the first one added). Each certificate is loaded once per context and indexed by name; before, OpenSSL presented the last certificate added, and the native backend sent all certificates as one chain with the last key (failing the handshake). fio_tls13_server_on_hello lets a native server pick its credentials once the ClientHello is parsed.

Update: (p256, p384, x509, pem, tls13) Compressed EC public keys (SEC 1 02/03 || x, allowed by RFC 5480) now work as they do in OpenSSL. fio_ecdsa_p256_verify accepts 33-byte and fio_ecdsa_p384_verify 49-byte keys (decompressed and checked against the curve), so certificate chains and TLS 1.3 handshakes with compressed P-256 / P-384 certificates verify, and native TLS servers load compressed P-256 certificates and key files. SEC1 private keys may embed a compressed public key (new fio_pem_private_key_s.ecdsa_p256.public_key_len field).

Update: (data) without a memory mapping, reads reuse the bytes the heap read buffer already holds and only read the missing ones (sliding or growing windows, fio_data_read after fio_data_read_until): spilled multipart uploads parse ~20% faster.

Update: (string) fio_string_write2 writes floats with fio_ftoa instead of snprintf("%.15g"), matching FIOBJ and Redis output. Floats now use %g precision (6 significant digits), and integral values end in .0.