Skip to content

Ws over h2 pmd fixes#3637

Open
thefallentree wants to merge 44 commits into
warmcat:mainfrom
thefallentree:ws-over-h2-pmd-fixes
Open

Ws over h2 pmd fixes#3637
thefallentree wants to merge 44 commits into
warmcat:mainfrom
thefallentree:ws-over-h2-pmd-fixes

Conversation

@thefallentree

@thefallentree thefallentree commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

ws-over-h2: permessage-deflate short return + ext tx-drain wedge

lws-team and others added 30 commits July 18, 2026 18:42
Basic h.264 / h.265 mkv / mp4 serving to view in browser
quic-version-negotiation

qir-last-tests

xr-29

qir-l1-fix

sai: QIR with QIR_LWS_BRANCH
fixes-1
lws_system_parse_policy() is compiled whenever LWS_WITH_NETWORK &&
LWS_WITH_FILE_OPS, but it is implemented with the LEJP JSON parser and
referenced lejp_construct()/lejp_parse()/lejp_destruct() unconditionally.

Callers such as the versioned-cert / ACME rotation path in tls.c (gated only on
LWS_WITH_DIR + LWS_WITH_NETWORK + LWS_WITH_FILE_OPS) do not gate on
LWS_WITH_LEJP, so a build with LWS_WITH_LEJP=OFF fails to link with undefined
lejp_* symbols.

Gate the JSON-parsing implementation on LWS_WITH_LEJP and provide a stub that
reports "no policy" (returns nonzero, *_policy = NULL) when LEJP is disabled.
Existing callers already treat a nonzero return as "no policy available", so
they continue to link and behave correctly without a JSON parser.
When a client mux (h2/h3) connection goes idle between transactions it sits in
LRS_IDLING.  On reusing it for a new stream, lws_vhost_active_conns() adopts
the new stream directly onto the network wsi but left it in LRS_IDLING.

LRS_IDLING does not carry LWSIFS_POCB, so lwsi_state_can_handle_POLLOUT() is
false for it and the network wsi's POLLOUT is never serviced: the child-walking
POLLOUT loop never runs, the new stream's HEADERS are never sent
(lws_h2_client_handshake() is never reached), and its response is never read.
The request just hangs.

Put the revived connection back into LRS_ESTABLISHED (the state it uses while
actively muxing) and clear the keep-warm idle timeout, so its POLLOUT is
serviced and the queued stream's headers go out.  Same fix for the h3/quic
branch.
…ring accept

If wsi->told_event_loop_closed was set during a manual close, eg
lws_libuv_closehandle() or elops_close_handle_manually_uv(), it would
remain set even if the wsi is re-used.  This causes SSL timeout issues
and fd leaks with HTTP -> HTTPS client redirects.

Clear wsi->told_event_loop_closed in elops_accept_uv(), to prevent this issue
on wsi re-use.  It's harmless on a new connection.
When handling LMQCPP_PUBACK_PROPERTIES_LEN_VBI/COMPLETED

This vulnerability was discovered by:
Anonymous working with TrendAI Zero Day Initiative
@thefallentree
thefallentree force-pushed the ws-over-h2-pmd-fixes branch from 44936df to 057a687 Compare July 20, 2026 13:14
…e, client extension offer -- with selftest

Three fixes for permessage-deflate on ws-over-h2 (RFC 8441)
encapsulated streams, plus a ctest-wired selftest:

1) The encapsulated tx path returned the h2 role's write result, which
   reflects the POST-COMPRESSION frame size -- with pmd active that is
   routinely smaller than what the caller passed in, so any
   well-compressed message made callers checking (wr < len) conclude a
   short write and kill a healthy connection.  The lws_write() contract
   is to report how much of the CALLER's payload was accepted, and the
   h1 path already returns orig_len for exactly this reason; do the
   same, still propagating write errors as < 0.

2) When pmd's compressed output exceeds its chunk buffer, the ws role
   sets ws->tx_draining_ext and expects the next POLLOUT to send the
   remaining fragments -- the h1 path services this in
   rops_handle_POLLOUT_ws() priority 5, before letting the user write
   anything new.  An h2-encapsulated child never reaches that path: its
   only POLLOUT servicing is the child loop in
   rops_perform_user_POLLOUT_h2(), which went straight to the user
   callback.  Meanwhile lws_send_pipe_choked() reports choked while
   tx_draining_ext is set, so a user write loop gated on it never
   writes again either: the drain never advances and the stream wedges
   permanently.  Service the drain from the child loop, ahead of the
   user callback.  It slots in after the generic 'deal with partial at
   nwsi' branch that 774c64a ("ws-over-h2: deal with choked nwsi")
   routes carries-ws children through, preserving its ordering: a child
   on a backed-up network socket still defers there first, the same
   priority h1 connections get, and the extension drain only runs once
   the nwsi partial has cleared.

3) lws_h2_client_handshake() only emitted sec-websocket-version and
   sec-websocket-protocol, so an lws client could never negotiate
   permessage-deflate (or any extension) over h2 even against a willing
   server.  RFC 8441 Sect 5 carries the ws handshake headers unchanged
   over extended CONNECT, so build the same offer list as
   lws_generate_client_ws_handshake() and emit it after the
   pseudo-headers; the response side already instantiates accepted
   extensions in the shared lws_client_ws_upgrade() path.

api-test-ws-h2-pmd (needs LWS_WITHOUT_EXTENSIONS=OFF): single-process
h2 ws server + client, both offering pmd; the server bulk-sends a 64KB
deterministic pattern in 4KB messages gated only on
lws_send_pipe_choked(), alternating trivially-compressible blocks
(deflated frame far smaller than the payload) with incompressible hash
noise (deflated output overflows pmd's default 1KB chunk buffer,
forcing tx_draining_ext).  It fails if the connection is not genuinely
ws-over-h2, if the extended CONNECT did not offer pmd, if any
lws_write() reports fewer bytes accepted than requested, if the pattern
corrupts, or if the transfer stalls.  Reverting any one of the three
fixes makes it fail in the corresponding distinct way.
@thefallentree
thefallentree force-pushed the ws-over-h2-pmd-fixes branch from 057a687 to d4e6802 Compare July 20, 2026 19:34
@lws-team

Copy link
Copy Markdown
Member

Uh... this was 4 patches which I have taken into _temp... now it's 1 patch... this is replacing the four patches, or it's in addition to the four patches? Or something else entirely?

@thefallentree

Copy link
Copy Markdown
Contributor Author

I am sorry — it was just squashed from previous commits. Nothing new

@thefallentree

Copy link
Copy Markdown
Contributor Author

Actually this is a follow up fix for the commits you merged

@sonarqubecloud

Copy link
Copy Markdown

@lws-team

Copy link
Copy Markdown
Member

So for what I have on _temp branch, trying to add what you have on your branch now - d4e6802 - gives an empty patch. So I think I am up to date. I will push all this to main later today

@lws-team
lws-team force-pushed the main branch 17 times, most recently from 99aac47 to d56159a Compare July 25, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants