Skip to content

permessage-deflate: decide whether to reset the inflater when the peer agreed to no context takeover #3118

Description

@ashtum

Summary

When the peer agreed to no context takeover (server_no_context_takeover for a client, client_no_context_takeover for a server), do_context_takeover_read in websocket/detail/impl_base.hpp is meant to reset the inflater after each message. Since Boost 1.70 that call has been a no-op, so Beast keeps the peer's window and silently accepts peers that break the promise.

#3114 makes zlib::inflate_stream::clear() work, which would change this to rejecting such peers, plus one allocation per message. To keep #3114 a pure port, the call is commented out there with a pointer to this issue. Behaviour with the call commented out matches develop.

History

  • Boost ≤ 1.69: pmd_->zi.reset() emptied the window and kept the buffer, so violating peers were rejected.
  • 8ea282e (Boost 1.70): deprecated flat_static_buffer::reset in favour of clear and renamed the websocket's buffer calls. pmd_->zi.reset() was renamed along with them, but zi is a zlib::inflate_stream, whose clear() had been empty since 5089cf5 (2016). From 1.70 on, Beast accepts violating peers, by accident.
  • Port zlib streams to zlib 1.3.2 #3114: implements clear() as documented (frees the window and resets the state), which would make Beast strict again.

RFC 7692

  • §7.2.1: when the parameter is agreed, the sender MUST start compressing each new message with an empty window.
  • §7.2.2: the receiver MAY then decompress each new message with an empty window; otherwise it MUST use the window from the previous message. Rejecting and accepting a violating peer are both conforming.
  • §7.1.1.1 / §7.1.1.2: the parameters exist so the receiver does not have to reserve window memory between messages.

Decisions

  1. Reject or accept peers that agree to no context takeover but still refer back to earlier messages.
  2. If rejecting: keep the window buffer or free it between messages.
Call at end of message Violating peer Allocations per message Inflater memory between messages (15-bit window)
none (develop, #3114 as-is) accepted 0 ~39.9 KB
zi.reset() (≤ 1.69) rejected (zlib::error::invalid_distance) 0 ~39.9 KB
zi.clear() rejected (zlib::error::invalid_distance) 1 (32 KiB) ~7.1 KB

Measured on #3114 with GCC 13 at -O2. The deflater (~145 KB at websocket defaults) is unaffected: zo.reset() keeps its buffers either way. A rejected message fails the connection, as any other inflate error does.

Other implementations

Implementation Strict Window between messages
Chromium no kept across messages
Firefox no kept across messages
ws yes kept, reset in place
OkHttp yes kept, reset in place
.NET yes freed after each message, recreated for the next
Netty yes freed after each message, recreated for the next
Tomcat yes kept, reset in place
Jetty (server role) yes released after each message
python-websockets yes freed after each message, recreated for the next
gorilla/websocket yes released after each message
aiohttp no kept across messages
uWebSockets yes one inflater shared by all connections, reset after each message
libwebsockets yes freed after each message, recreated for the next
Undertow no kept across messages
Cowboy/Gun (cowlib) yes kept, reset in place
coder/websocket yes released after each message
websocketpp no kept across messages
libsoup (WebKitGTK/WPE) no kept across messages
Autobahn-python yes kept until replaced by a new one at the next message

The tolerant implementations all still reset their own compressor when they agreed to no context takeover.

Activity

  1. self-assigned this
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions