Skip to content

v0.98.5

Choose a tag to compare

@github-actions github-actions released this 07 Jul 04:39
· 980 commits to main since this release

Fixed

  • The participant turn-result parser and the relay-log reader are now hardened against non-finite and
    double-overflowing numbers. turn_result_from_payload reads a participant's cost_usd, token counts,
    and rate-limit signal off an untrusted bus payload with a bare json.loads (which accepts the
    non-standard Infinity/NaN tokens), and its _as_int passed a non-finite float straight to int()
    (which raises); a 400-digit integer also overflowed float(). The three coercers now default a
    non-numeric, non-finite, or overflowing value to zero (or None for the optional signal) so a malformed
    turn result cannot crash the bus handler awaiting it. relay.decode_lite's existing tolerance of a
    malformed log entry (a non-numeric t/i defaulted to zero) now also covers a non-finite value, whose
    int() conversion raised OverflowError outside the caught set. Part of the non-finite-number family.
  • A claim's ttl_seconds and a frame's epoch/expected_version are now guarded against non-finite and
    double-overflowing values. The claim handler converted ttl_seconds with a float() that caught only
    TypeError/ValueError, so a 400-digit integer raised an uncaught OverflowError out of the frame
    handler (dropping the socket), and a 1e400 (or "inf") became an inf lease expiry — a task claimed
    with an inf ttl could never be taken over (a permanent lock), while a nan ttl read as instantly
    expired. SynapseHub._optional_int, which reads epoch/expected_version on the claim/renew/release/
    checkpoint frames, passed a non-finite float straight to int(), which raises. Both now treat a
    non-numeric, non-finite, or overflowing value as absent: the ttl falls back to the hub's default lease
    duration and the optional int to None. Found by live fault-injection of the claim path; part of the
    non-finite-number family below.
  • A chat frame's client-supplied timestamp is now coerced to the hub clock when it is not a usable
    instant, instead of crashing the handler or broadcasting a non-finite time. The handler stamped the
    message with a bare float(data.get("timestamp") or time.time()), so a non-numeric timestamp (a string
    or a list) raised ValueError/TypeError, and a double-overflowing integer raised OverflowError
    none caught by the connection loop (which handles only ConnectionClosed), so a single hostile chat
    dropped the sender's socket with a traceback. A finite-looking 1e400 (or a "timestamp": "inf")
    instead decoded to inf and was retained in history, journalled, broadcast to every socket, and used as
    the dead-letter ledger's ordering key. The timestamp is advisory client metadata, so a missing, falsy,
    non-numeric, non-finite, or overflowing value now falls back to the hub's authoritative time.time();
    a finite client timestamp is still preserved. Found by fault-injection of the chat handler (the hottest
    untrusted path); part of the non-finite-number family below.
  • The federation gate now stays deny-closed when a peer certificate reads but does not parse. The gate
    already wraps the certificate read so a socket in a strange state (or an injected certificate source)
    cannot crash the frame handler, and refuses a peered key's cross-domain claim it cannot pin. Computing
    the pin was outside that guard, so a certificate that read as non-empty bytes but did not parse would
    have raised out of the handler. The pin computation now shares the guard: an unparsable certificate is
    treated exactly like a failed read — deny-closed for a peered key, degrade-to-local for a local key —
    never a crash. Defence in depth: the production certificate source returns the TLS-validated peer DER,
    so this is not reachable on a live mutual-TLS connection, but it completes the gate's "any certificate
    failure fails closed" invariant. Found by fault-injection of the federation trust gate.
  • The federation-bundle and multi-hub numeric guards also reject a JSON integer too large for a double.
    The NaN/Infinity guards added above convert with float() and check math.isfinite, but a
    400-digit integer is finite JSON that passes the decoder yet raises OverflowError on the float()
    conversion (and on math.isfinite of the raw int). A peer bundle's expires_at or a peer event's ts
    set to such an integer therefore still escaped as an unhandled OverflowError; both guards now catch it
    and raise their own malformed-input error (FederationStoreError / MultiHubWireError). Completes the
    non-finite-number family below — the finding coercion helper already caught this case.
  • The bounded frame decoder now rejects the non-standard NaN/Infinity/-Infinity JSON tokens. RFC
    8259 has no such literals, but json.loads accepts them by default, so a non-finite float could enter
    through any inbound frame or peer response and then break an ordering comparison (nan compares
    unequal to everything) or overflow an int()/float() conversion downstream. loads_bounded — the
    single depth-bounded loader every inbound frame, peer sync body, and federation reply passes through —
    now raises json.JSONDecodeError for a non-finite constant, so every consumer's existing malformed-JSON
    handling fails it closed. This is a defence in depth beneath the per-field guards on the federation
    bundle, the multi-hub event ts, and the finding numbers (below); the hub never emits a non-finite
    float, so no legitimate frame is affected.
  • A finding with a non-finite number no longer crashes the finding handler. A finding envelope is
    decoded from an untrusted frame and json.loads yields inf/nan from the Infinity/NaN tokens,
    but the tolerant coercion helpers converted a numeric field with a bare int()/float():
    int(inf) raises OverflowError, int(nan) raises ValueError, and float() of a JSON integer too
    large for a double raises OverflowError. A single frame carrying Infinity in
    provenance.source_event_seq (or nan in a confidence or validity bound) therefore raised an
    unhandled exception out of the handler, dropping the sender's connection with a traceback. The helpers
    now treat a non-finite or double-overflowing value as no usable number (None) — the same signal they
    already return for a non-numeric value — so a hostile finding is rejected cleanly and a nan can no
    longer corrupt finding ranking or validity-window checks. Found by fault-injection of the finding
    decode path.
  • A non-finite timestamp in a peer hub's event no longer breaks the deterministic multi-hub merge. The
    cross-host event codec converted a stored event's ts with a type check that accepted any float, but
    json.loads parses the NaN/Infinity tokens, so a peer's wire body could carry a non-finite ts.
    A nan compares unequal to everything, so the total-order merge key (ts, hub_id, seq) stopped being
    a total order: two hubs folding the same events in different receive orders could sort them
    differently and diverge. The codec now rejects a non-finite ts as a malformed body
    (MultiHubWireError), the same contract it already applies to every other bad field. Found by
    fault-injection of the wire codec.
  • A malformed federation peer bundle no longer crashes the import or the hub. A numeric field in an
    out-of-band bundle — a peer's expires_at or a record's provenance.imported_at — was converted
    with a bare float(), so a hostile or corrupt value (a string, a mapping, a list, or a non-finite
    nan/inf) raised a raw TypeError/ValueError. Every caller catches only FederationStoreError
    (a ValueError subclass, which never matched the TypeError cases), so such a bundle escaped as an
    unhandled traceback — crashing synapse federation import on a peer's bundle and synapse hub --federation-store at startup on a corrupt store. Both numeric fields now parse through a guarded
    conversion that raises FederationStoreError naming the field, and rejects nan/inf (a nan
    expiry would defeat the now >= expires_at check and leave a peering that never expires). Found by
    fault-injection of the federation bundle parser.
  • The hub no longer floods its log with full ERROR tracebacks for benign aborted handshakes. A
    load-balancer TCP health check, a port scan, or a client that drops before completing the WebSocket
    handshake previously logged opening handshake failed with a full traceback each time — on a
    production hub, frequent, benign, and enough noise to bury real errors and grow the log without bound.
    A HandshakeAbortFilter on the log handler now drops exactly those records (a handshake failure whose
    cause chain is a plain connection abort — EOFError/ConnectionError/TimeoutError, matched through
    websockets' wrapping exception) while keeping every other log, including a genuine handshake error from
    a completed-but-invalid request. Found by live fault-injection testing of the hub.

Added

  • An API and wire stability policy (docs/api-stability.md): what counts as a stable surface, the
    test that guards each one against accidental change (the public __all__, the complete wire
    MessageType vocabulary, the federation primitives out-of-tree consumers import, and the tiered CLI),
    the decoupled wire-protocol version, the stability tiers, and the deprecation policy. The wire message
    vocabulary is now frozen in full by tests/test_wire_surface_freeze.py — previously only the count
    was pinned, blind to a rename that keeps the count constant.
  • The hub advertises a wire-protocol version. WIRE_PROTOCOL_VERSION (an integer, baseline 1,
    decoupled from the package version so it changes only on a wire-incompatible change) now rides in the
    welcome handshake as protocol_version and in /health, and a client captures the peer's version
    as hub_protocol_version on connect. This gives a consumer that syncs across possibly version-skewed
    hubs a compatibility signal to read on connect instead of inferring from a separate query. It is
    advertise-only: a client records the peer's version but no compatibility policy is enforced yet
    (the mismatch behaviour is a contract to be agreed with the wire's downstream consumers first). A hub
    or client that predates the field reads it as absent, so the addition is backward-compatible.
  • synapse approvals makes the two-person relay quorum operable. The approval ledger is per-hub live
    state that enforced a second operator but exposed no way to see which relays were pending, so the
    quorum was invisible between the first request and the second approval. The pending set now rides in
    the hub's state snapshot (the same one the dashboard and cockpit read) as pending_relay_approvals,
    and the new read-only synapse approvals query prints it — oldest first, naming each pending action,
    its namespace and task, and the first requester a second, different operator must join to reach
    quorum. It holds only what the ledger holds (never a message body).